Spaces:
Running on Zero
fix: Build the Space's agent at import scope, where ZeroGPU is watching
Browse filesThird failure, same RuntimeError, but the traceback finally names the line:
engine_torch.py:102, `self.model.to("cuda")`. So it was never device_map
specifically -- .to("cuda") fails too, and the previous commit's reasoning was
only half right.
ZeroGPU patches torch during the import of the Space's entry module, and only
CUDA operations inside that window are intercepted. app.py builds the model in
FastAPI's lifespan, on a ThreadPoolExecutor worker: after import, on another
thread, outside the window. Any CUDA op there reaches the real
torch._C._cuda_init and raises. That is also why the note from the last working
deploy -- "explicit .to('cuda') works" -- did not carry over: back then the model
was built at import, not in a lifespan.
So where the engine is constructed matters as much as how.
app_space.py::_build_agent_at_import builds it during its own import and assigns
app._agent directly, leaving lifespan's get_agent() a no-op. _fetch_index moves
ahead of it, since the agent touches retrieval as it starts.
device_map remains excluded for its own separate reason (caching_allocator_warmup
calls torch.empty(device="cuda") directly), and bitsandbytes with it, so bf16 and
a model small enough to carry in it stand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- CLAUDE.md +13 -10
- app_space.py +31 -4
- controlai_agent/engine_torch.py +11 -8
|
@@ -193,16 +193,19 @@ mlx_lm` several frames from the real cause.
|
|
| 193 |
cannot express either of the two things that matter: `DynamicCache.crop()` for prefix reuse across
|
| 194 |
tool steps, and injecting `</think>` to close an overrunning reasoning block.
|
| 195 |
|
| 196 |
-
**It loads bf16
|
| 197 |
-
|
| 198 |
-
`RuntimeError: Low-level CUDA init
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
| 206 |
|
| 207 |
**ZeroGPU platform gotchas, each learned by having the Space fail:**
|
| 208 |
- A `@spaces.GPU` function is only detected if wired to a real Gradio event handler. One called
|
|
|
|
| 193 |
cannot express either of the two things that matter: `DynamicCache.crop()` for prefix reuse across
|
| 194 |
tool steps, and injecting `</think>` to close an overrunning reasoning block.
|
| 195 |
|
| 196 |
+
**It loads bf16 with an explicit `.to("cuda")`, and `app_space.py` constructs it at module import
|
| 197 |
+
scope. Do not reintroduce `device_map` or bitsandbytes, and do not move the construction into a
|
| 198 |
+
lifespan or a request.** All of those fail identically with `RuntimeError: Low-level CUDA init
|
| 199 |
+
(torch._C._cuda_init) reached`. ZeroGPU patches torch during the import of the Space's entry module
|
| 200 |
+
and attaches real hardware only inside a `@spaces.GPU` call; only CUDA operations inside that import
|
| 201 |
+
window are intercepted. `app.py` normally builds the model in FastAPI's `lifespan`, on a
|
| 202 |
+
`ThreadPoolExecutor` worker — after import, on another thread — and `.to("cuda")` there reaches real
|
| 203 |
+
CUDA init and raises. `app_space.py::_build_agent_at_import` builds the agent during import and
|
| 204 |
+
assigns `app._agent`, so `lifespan`'s `get_agent()` is a no-op. `device_map` fails independently, by
|
| 205 |
+
routing transformers through `caching_allocator_warmup` and its direct
|
| 206 |
+
`torch.empty(..., device="cuda")`; bitsandbytes requires `device_map`, so **4-bit quantisation is
|
| 207 |
+
unavailable**, which is what forces a model small enough to carry in bf16: `Qwen/Qwen3-8B`, ~16GB
|
| 208 |
+
against ~28GB for the 14B run locally.
|
| 209 |
|
| 210 |
**ZeroGPU platform gotchas, each learned by having the Space fail:**
|
| 211 |
- A `@spaces.GPU` function is only detected if wired to a real Gradio event handler. One called
|
|
@@ -40,7 +40,8 @@ import gradio as gr
|
|
| 40 |
import spaces
|
| 41 |
import uvicorn
|
| 42 |
|
| 43 |
-
|
|
|
|
| 44 |
|
| 45 |
|
| 46 |
def _fetch_index() -> None:
|
|
@@ -57,6 +58,34 @@ def _fetch_index() -> None:
|
|
| 57 |
print(f"[space] could not fetch the index ({exc}); retrieval disabled")
|
| 58 |
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
@spaces.GPU(duration=120)
|
| 61 |
def _gpu_probe(text: str) -> str:
|
| 62 |
"""Satisfies ZeroGPU's startup validation, and warms the model on first use.
|
|
@@ -64,8 +93,7 @@ def _gpu_probe(text: str) -> str:
|
|
| 64 |
Takes only a plain string. The agent is reached through the module global --
|
| 65 |
see note 4 above; passing it in is what hangs the whole Space.
|
| 66 |
"""
|
| 67 |
-
|
| 68 |
-
return agent.run(text).answer if text else "ready"
|
| 69 |
|
| 70 |
|
| 71 |
with gr.Blocks() as _gpu_demo:
|
|
@@ -76,7 +104,6 @@ with gr.Blocks() as _gpu_demo:
|
|
| 76 |
|
| 77 |
|
| 78 |
def main() -> None:
|
| 79 |
-
_fetch_index()
|
| 80 |
port = int(os.environ.get("PORT", 7860))
|
| 81 |
# Separate port, non-blocking: see note 3.
|
| 82 |
_gpu_demo.launch(
|
|
|
|
| 40 |
import spaces
|
| 41 |
import uvicorn
|
| 42 |
|
| 43 |
+
import app as app_module
|
| 44 |
+
from app import app
|
| 45 |
|
| 46 |
|
| 47 |
def _fetch_index() -> None:
|
|
|
|
| 58 |
print(f"[space] could not fetch the index ({exc}); retrieval disabled")
|
| 59 |
|
| 60 |
|
| 61 |
+
def _build_agent_at_import() -> None:
|
| 62 |
+
"""Construct the agent *now*, while this module is still being imported.
|
| 63 |
+
|
| 64 |
+
ZeroGPU installs its CUDA emulation by patching torch during the import of
|
| 65 |
+
the Space's entry module, and only operations that happen inside that window
|
| 66 |
+
are intercepted. `app.py` normally builds the model in FastAPI's `lifespan`,
|
| 67 |
+
on a ThreadPoolExecutor worker -- long after import, on another thread, where
|
| 68 |
+
the patching does not apply. `.to("cuda")` there reaches the real
|
| 69 |
+
`torch._C._cuda_init` and raises:
|
| 70 |
+
|
| 71 |
+
RuntimeError: Low-level CUDA init (`torch._C._cuda_init`) reached.
|
| 72 |
+
|
| 73 |
+
Building here and handing the finished agent to `app.py` keeps the load
|
| 74 |
+
inside the window. `lifespan` then finds `_agent` already set and its
|
| 75 |
+
`get_agent()` is a no-op.
|
| 76 |
+
"""
|
| 77 |
+
from controlai_agent.agent import ControlAgent
|
| 78 |
+
from controlai_agent.engine_torch import TorchEngine
|
| 79 |
+
|
| 80 |
+
print("[space] building agent at import scope (ZeroGPU CUDA window)")
|
| 81 |
+
app_module._agent = ControlAgent(engine=TorchEngine())
|
| 82 |
+
print("[space] agent ready")
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
_fetch_index() # the agent prewarms retrieval, so the index must precede it
|
| 86 |
+
_build_agent_at_import()
|
| 87 |
+
|
| 88 |
+
|
| 89 |
@spaces.GPU(duration=120)
|
| 90 |
def _gpu_probe(text: str) -> str:
|
| 91 |
"""Satisfies ZeroGPU's startup validation, and warms the model on first use.
|
|
|
|
| 93 |
Takes only a plain string. The agent is reached through the module global --
|
| 94 |
see note 4 above; passing it in is what hangs the whole Space.
|
| 95 |
"""
|
| 96 |
+
return app_module.get_agent().run(text).answer if text else "ready"
|
|
|
|
| 97 |
|
| 98 |
|
| 99 |
with gr.Blocks() as _gpu_demo:
|
|
|
|
| 104 |
|
| 105 |
|
| 106 |
def main() -> None:
|
|
|
|
| 107 |
port = int(os.environ.get("PORT", 7860))
|
| 108 |
# Separate port, non-blocking: see note 3.
|
| 109 |
_gpu_demo.launch(
|
|
@@ -20,19 +20,22 @@ It mirrors `LocalEngine`'s design decisions rather than reaching for
|
|
| 20 |
punishes the `[`, `0`, `,` that matrices and JSON are made of.
|
| 21 |
|
| 22 |
**Loading is bf16 and moves to the GPU with an explicit `.to("cuda")`. Do not
|
| 23 |
-
reintroduce `device_map` or bitsandbytes
|
| 24 |
-
|
|
|
|
| 25 |
|
| 26 |
RuntimeError: Low-level CUDA init (`torch._C._cuda_init`) reached. This
|
| 27 |
means ZeroGPU's PyTorch CUDA emulation mode did not intercept a CUDA
|
| 28 |
operation in your code.
|
| 29 |
|
| 30 |
-
ZeroGPU
|
| 31 |
-
`@spaces.GPU` call.
|
| 32 |
-
|
| 33 |
-
`
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
| 36 |
|
| 37 |
That is why the model is Qwen3-8B rather than the 14B run locally: bf16 8B is
|
| 38 |
~16GB to download against ~28GB, and a Space rebuild re-downloads from scratch.
|
|
|
|
| 20 |
punishes the `[`, `0`, `,` that matrices and JSON are made of.
|
| 21 |
|
| 22 |
**Loading is bf16 and moves to the GPU with an explicit `.to("cuda")`. Do not
|
| 23 |
+
reintroduce `device_map` or bitsandbytes, and do not construct this class lazily
|
| 24 |
+
at request time.** All three break on ZeroGPU, which is the only place this file
|
| 25 |
+
runs, and all three fail the same way:
|
| 26 |
|
| 27 |
RuntimeError: Low-level CUDA init (`torch._C._cuda_init`) reached. This
|
| 28 |
means ZeroGPU's PyTorch CUDA emulation mode did not intercept a CUDA
|
| 29 |
operation in your code.
|
| 30 |
|
| 31 |
+
ZeroGPU patches torch during the import of the Space's entry module and attaches
|
| 32 |
+
real hardware only inside a `@spaces.GPU` call. Only CUDA operations inside that
|
| 33 |
+
import window are intercepted, so **where this object is constructed matters as
|
| 34 |
+
much as how**: `app_space.py` builds it at module scope for exactly that reason.
|
| 35 |
+
`device_map` fails on top of that, because it routes transformers through
|
| 36 |
+
`caching_allocator_warmup`, which calls `torch.empty(..., device="cuda")`
|
| 37 |
+
directly. bitsandbytes in turn *requires* `device_map`, so 4-bit quantisation is
|
| 38 |
+
unavailable here — which is why the model has to be small enough in bf16.
|
| 39 |
|
| 40 |
That is why the model is Qwen3-8B rather than the 14B run locally: bf16 8B is
|
| 41 |
~16GB to download against ~28GB, and a Space rebuild re-downloads from scratch.
|