atakan Claude Opus 5 commited on
Commit
c74703c
·
1 Parent(s): 31f62ef

add: A two-sided CUDA diagnostic for the ZeroGPU worker failure

Browse files

Two guesses at "No CUDA GPUs are available" -- device_map, then CUDA 13 wheels --
were both wrong, and the second made things worse. This gathers facts instead.

/api/gpudiag reports torch version, torch.version.cuda, CUDA_VISIBLE_DEVICES,
cuda.is_initialized/is_available/device_count and pid from three points: the
parent before the model load, the parent now, and inside the @spaces.GPU worker
(plus nvidia-smi there). Comparing the three is the point.

is_initialized() on the parent is the datum being chased. ZeroGPU forks its GPU
worker, and a parent that has genuinely initialised CUDA before forking leaves
the child unable to use the GPU, reporting exactly this error. Loading the model
at import scope -- which was itself necessary -- is the obvious thing that could
have done that.

The route is a plain `def`, so Starlette runs it on its own threadpool: the same
context the real inference path uses, and not the executor thread that broke the
GPU handoff before. It lives in app_space.py rather than app.py, since it is
meaningless off ZeroGPU.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Files changed (1) hide show
  1. app_space.py +69 -0
app_space.py CHANGED
@@ -44,6 +44,38 @@ import uvicorn
44
  import app as app_module
45
  from app import app
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  def _fetch_index() -> None:
49
  """Pull the retrieval index into data/rag_index/ before the agent loads."""
@@ -79,6 +111,7 @@ def _build_agent_at_import() -> None:
79
  from controlai_agent.engine_torch import TorchEngine
80
  from controlai_rag.embeddings import get_embedder
81
 
 
82
  print("[space] building agent at import scope (ZeroGPU CUDA window)")
83
  app_module._agent = ControlAgent(engine=TorchEngine())
84
 
@@ -90,6 +123,8 @@ def _build_agent_at_import() -> None:
90
  get_embedder().encode_query("warmup")
91
  # Route every turn through the GPU-decorated generator above.
92
  app_module.stream_hook = _gpu_stream
 
 
93
  print("[space] agent and embedder ready, GPU stream hook installed")
94
 
95
 
@@ -117,6 +152,40 @@ def _gpu_stream(message: str, history: list) -> Iterator[dict]:
117
  yield from app_module.get_agent().stream(message, history)
118
 
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  @spaces.GPU(duration=60)
121
  def _gpu_probe(text: str) -> str:
122
  """Satisfies ZeroGPU's startup validation.
 
44
  import app as app_module
45
  from app import app
46
 
47
+ # Filled in at import, before and after the model load, so the parent's CUDA
48
+ # state can be compared against the worker's. See /api/gpudiag.
49
+ _parent_state: dict = {}
50
+
51
+
52
+ def _torch_state(label: str) -> dict:
53
+ import os as _os
54
+
55
+ import torch
56
+
57
+ state = {
58
+ "where": label,
59
+ "torch": torch.__version__,
60
+ "torch.version.cuda": torch.version.cuda,
61
+ "CUDA_VISIBLE_DEVICES": _os.environ.get("CUDA_VISIBLE_DEVICES"),
62
+ "ZERO_GPU_PATCH_TORCH": _os.environ.get("ZERO_GPU_PATCH_TORCH"),
63
+ "pid": _os.getpid(),
64
+ }
65
+ # is_initialized() is the one that matters: if the parent has really
66
+ # initialised CUDA before ZeroGPU forks its worker, the child cannot use the
67
+ # GPU and reports "No CUDA GPUs are available" -- which is our exact error.
68
+ for name, fn in (
69
+ ("cuda.is_initialized", lambda: torch.cuda.is_initialized()),
70
+ ("cuda.is_available", lambda: torch.cuda.is_available()),
71
+ ("cuda.device_count", lambda: torch.cuda.device_count()),
72
+ ):
73
+ try:
74
+ state[name] = fn()
75
+ except Exception as exc: # noqa: BLE001 - the message is the datum
76
+ state[name] = f"{type(exc).__name__}: {exc}"
77
+ return state
78
+
79
 
80
  def _fetch_index() -> None:
81
  """Pull the retrieval index into data/rag_index/ before the agent loads."""
 
111
  from controlai_agent.engine_torch import TorchEngine
112
  from controlai_rag.embeddings import get_embedder
113
 
114
+ _parent_state["before_model_load"] = _torch_state("parent-before-load")
115
  print("[space] building agent at import scope (ZeroGPU CUDA window)")
116
  app_module._agent = ControlAgent(engine=TorchEngine())
117
 
 
123
  get_embedder().encode_query("warmup")
124
  # Route every turn through the GPU-decorated generator above.
125
  app_module.stream_hook = _gpu_stream
126
+ _parent_state["after_model_load"] = _torch_state("parent-after-load")
127
+ print(f"[space] parent CUDA state after load: {_parent_state['after_model_load']}")
128
  print("[space] agent and embedder ready, GPU stream hook installed")
129
 
130
 
 
152
  yield from app_module.get_agent().stream(message, history)
153
 
154
 
155
+ @spaces.GPU(duration=60)
156
+ def _gpu_diagnostics() -> dict:
157
+ """Report CUDA state from inside the ZeroGPU worker, where it fails."""
158
+ import subprocess
159
+
160
+ state = _torch_state("gpu-worker")
161
+ try:
162
+ state["nvidia-smi"] = subprocess.run(
163
+ ["nvidia-smi", "--query-gpu=name,driver_version", "--format=csv,noheader"],
164
+ capture_output=True, text=True, timeout=20,
165
+ ).stdout.strip() or "(no output)"
166
+ except Exception as exc: # noqa: BLE001
167
+ state["nvidia-smi"] = f"{type(exc).__name__}: {exc}"
168
+ return state
169
+
170
+
171
+ @app.get("/api/gpudiag")
172
+ def gpudiag() -> dict:
173
+ """Compare parent-process CUDA state with the @spaces.GPU worker's.
174
+
175
+ A plain `def`, so Starlette runs it on its own threadpool -- the same
176
+ context the real inference path uses.
177
+ """
178
+ try:
179
+ worker = _gpu_diagnostics()
180
+ except Exception as exc: # noqa: BLE001
181
+ worker = {"error": f"{type(exc).__name__}: {exc}"}
182
+ return {
183
+ "parent_at_import": _parent_state,
184
+ "parent_now": _torch_state("parent-now"),
185
+ "worker": worker,
186
+ }
187
+
188
+
189
  @spaces.GPU(duration=60)
190
  def _gpu_probe(text: str) -> str:
191
  """Satisfies ZeroGPU's startup validation.