Update ui/gpu.py
Browse files
ui/gpu.py
CHANGED
|
@@ -19,16 +19,48 @@ an explicit `device` instead of auto-detecting.
|
|
| 19 |
Off ZeroGPU (local, or Spaces "CPU basic") @spaces.GPU is a transparent
|
| 20 |
passthrough and ON_ZEROGPU is False, so this module quietly degrades to plain
|
| 21 |
CPU work and nothing else in the app changes.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
"""
|
| 23 |
import os
|
|
|
|
| 24 |
|
| 25 |
import spaces
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
# Set by the ZeroGPU runtime; `spaces.config` reads the same variable.
|
| 28 |
ON_ZEROGPU = os.getenv("SPACES_ZERO_GPU", "").lower() in ("1", "t", "true")
|
| 29 |
|
| 30 |
# The device to use *inside* a GPU window. Outside one, always "cpu".
|
| 31 |
-
GPU_DEVICE = "
|
| 32 |
|
| 33 |
# Generous but bounded. The window has to cover PDF text extraction and chunking
|
| 34 |
# (CPU work that unavoidably happens inside build_vectorstore) plus the encode
|
|
@@ -36,6 +68,38 @@ GPU_DEVICE = "cpu" #"cuda" if ON_ZEROGPU else "cpu"
|
|
| 36 |
_VECTORIZE_SECONDS = 120
|
| 37 |
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
@spaces.GPU(duration=_VECTORIZE_SECONDS)
|
| 40 |
def vectorize_on_gpu(pdf_path: str) -> None:
|
| 41 |
"""Build and persist this paper's vectorstore with the embedder on GPU.
|
|
@@ -50,14 +114,10 @@ def vectorize_on_gpu(pdf_path: str) -> None:
|
|
| 50 |
short-circuits to a plain load as soon as the persist dir exists.
|
| 51 |
"""
|
| 52 |
# Runs fresh on every call, inside the GPU worker — unlike a module-level
|
| 53 |
-
# print, this actually tells you what's true for *this* invocation.
|
| 54 |
-
|
| 55 |
-
print(
|
| 56 |
-
"[GPU FN] pid=" + str(os.getpid())
|
| 57 |
-
+ ", in ZeroGPU ctx: " + str(zero_gpu_flag)
|
| 58 |
-
+ ", device=" + GPU_DEVICE
|
| 59 |
-
)
|
| 60 |
|
| 61 |
from vectorizeer import build_vectorstore
|
| 62 |
|
| 63 |
-
build_vectorstore(pdf_path, device=GPU_DEVICE)
|
|
|
|
| 19 |
Off ZeroGPU (local, or Spaces "CPU basic") @spaces.GPU is a transparent
|
| 20 |
passthrough and ON_ZEROGPU is False, so this module quietly degrades to plain
|
| 21 |
CPU work and nothing else in the app changes.
|
| 22 |
+
|
| 23 |
+
THE CUDA-VIRGINITY RULE
|
| 24 |
+
-----------------------
|
| 25 |
+
ZeroGPU forks its GPU worker from this process (`multiprocessing.get_context
|
| 26 |
+
('fork')`), and the very first thing the child does is:
|
| 27 |
+
|
| 28 |
+
os.environ['CUDA_VISIBLE_DEVICES'] = nvidia_uuid
|
| 29 |
+
torch.Tensor([0]).cuda()
|
| 30 |
+
|
| 31 |
+
CUDA_VISIBLE_DEVICES is honoured by the driver *exactly once per process*, at
|
| 32 |
+
first CUDA init. So if anything has really initialised CUDA in THIS process
|
| 33 |
+
before the fork, the child inherits a driver that already decided there are zero
|
| 34 |
+
devices, the env var is ignored, and the worker dies with
|
| 35 |
+
|
| 36 |
+
RuntimeError: No CUDA GPUs are available
|
| 37 |
+
|
| 38 |
+
`spaces` prevents that by monkey-patching torch at `import spaces`. But note how
|
| 39 |
+
it does it (spaces/zero/torch/patching.py::patch): the `torch.cuda.*` attribute
|
| 40 |
+
fakes are module-global, while the TorchFunctionMode/TorchDispatchMode that
|
| 41 |
+
intercept real tensor ops are **thread-local to the thread that called patch()**
|
| 42 |
+
— the main thread, at import. The library's own source carries the TODO
|
| 43 |
+
admitting the inconsistency. Consequence for us: heavy model loading must happen
|
| 44 |
+
on the MAIN THREAD, not on Gradio worker threads or threads we spawn ourselves.
|
| 45 |
+
See ui/agents.preload_all().
|
| 46 |
+
|
| 47 |
+
`cuda_state()` below is the telemetry that proves whether that rule is holding.
|
| 48 |
"""
|
| 49 |
import os
|
| 50 |
+
import threading
|
| 51 |
|
| 52 |
import spaces
|
| 53 |
|
| 54 |
+
# Safe: `spaces` imports torch itself (spaces/zero/torch/__init__.py) and calls
|
| 55 |
+
# torch.patch() immediately after, so by the time this line runs torch is already
|
| 56 |
+
# in sys.modules and already patched. This is NOT the import that decides order.
|
| 57 |
+
import torch
|
| 58 |
+
|
| 59 |
# Set by the ZeroGPU runtime; `spaces.config` reads the same variable.
|
| 60 |
ON_ZEROGPU = os.getenv("SPACES_ZERO_GPU", "").lower() in ("1", "t", "true")
|
| 61 |
|
| 62 |
# The device to use *inside* a GPU window. Outside one, always "cpu".
|
| 63 |
+
GPU_DEVICE = "cuda" if ON_ZEROGPU else "cpu"
|
| 64 |
|
| 65 |
# Generous but bounded. The window has to cover PDF text extraction and chunking
|
| 66 |
# (CPU work that unavoidably happens inside build_vectorstore) plus the encode
|
|
|
|
| 68 |
_VECTORIZE_SECONDS = 120
|
| 69 |
|
| 70 |
|
| 71 |
+
def cuda_state(tag: str) -> str:
|
| 72 |
+
"""One-line snapshot of this process's CUDA state, for the Space logs.
|
| 73 |
+
|
| 74 |
+
Reads the two fields that actually decide whether a ZeroGPU fork will
|
| 75 |
+
succeed, neither of which `spaces` patches (so both tell the truth):
|
| 76 |
+
|
| 77 |
+
torch.cuda._initialized True once CUDA has REALLY been brought up here.
|
| 78 |
+
Must still be False in the parent at the moment
|
| 79 |
+
of the GPU call — otherwise the fork is doomed.
|
| 80 |
+
torch.cuda._is_in_bad_fork()
|
| 81 |
+
True when this process inherited an already-
|
| 82 |
+
initialised CUDA context across a fork.
|
| 83 |
+
|
| 84 |
+
Also reports the thread, because "which thread" is the whole ballgame: the
|
| 85 |
+
spaces Torch modes only cover the main thread.
|
| 86 |
+
"""
|
| 87 |
+
try:
|
| 88 |
+
bad_fork = torch.cuda._is_in_bad_fork()
|
| 89 |
+
except Exception as e: # pragma: no cover — never let telemetry break the app
|
| 90 |
+
bad_fork = f"<{type(e).__name__}>"
|
| 91 |
+
current = threading.current_thread()
|
| 92 |
+
return (
|
| 93 |
+
f"[cuda-state:{tag}] pid={os.getpid()}"
|
| 94 |
+
f" thread={current.name!r}"
|
| 95 |
+
f" is_main_thread={current is threading.main_thread()}"
|
| 96 |
+
f" torch.cuda._initialized={getattr(torch.cuda, '_initialized', '?')}"
|
| 97 |
+
f" _is_in_bad_fork={bad_fork}"
|
| 98 |
+
f" CUDA_VISIBLE_DEVICES={os.environ.get('CUDA_VISIBLE_DEVICES')!r}"
|
| 99 |
+
f" ON_ZEROGPU={ON_ZEROGPU} GPU_DEVICE={GPU_DEVICE}"
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
@spaces.GPU(duration=_VECTORIZE_SECONDS)
|
| 104 |
def vectorize_on_gpu(pdf_path: str) -> None:
|
| 105 |
"""Build and persist this paper's vectorstore with the embedder on GPU.
|
|
|
|
| 114 |
short-circuits to a plain load as soon as the persist dir exists.
|
| 115 |
"""
|
| 116 |
# Runs fresh on every call, inside the GPU worker — unlike a module-level
|
| 117 |
+
# print, this actually tells you what's true for *this* invocation. If you
|
| 118 |
+
# see this line at all, ZeroGPU's worker bootstrap succeeded.
|
| 119 |
+
print(cuda_state("gpu-worker"), flush=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
from vectorizeer import build_vectorstore
|
| 122 |
|
| 123 |
+
build_vectorstore(pdf_path, device=GPU_DEVICE)
|