Update ui/agents.py
Browse files- ui/agents.py +57 -10
ui/agents.py
CHANGED
|
@@ -9,6 +9,27 @@ equivalent, and it serves requests from a thread pool, so a naive "load if
|
|
| 9 |
None" would let two simultaneous first-visitors each start a ~2 GB model load.
|
| 10 |
Hence the explicit double-checked locking below: the lock is held across the
|
| 11 |
load, and a second caller blocks and then sees the finished object.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
"""
|
| 13 |
|
| 14 |
import threading
|
|
@@ -19,6 +40,10 @@ _agents = None
|
|
| 19 |
_chatbot_lock = threading.Lock()
|
| 20 |
_chatbot_models = None
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
def load_agents():
|
| 24 |
"""Import the compiled LangGraph agents. Importing the search graph also
|
|
@@ -57,15 +82,37 @@ def warm_chatbot_models():
|
|
| 57 |
return _chatbot_models
|
| 58 |
|
| 59 |
|
| 60 |
-
def
|
| 61 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
the
|
| 66 |
-
|
| 67 |
-
user has framed an intent, run a search, and picked a paper, which is far
|
| 68 |
-
longer than the load takes. ensure_chat_ready() calls warm_chatbot_models()
|
| 69 |
-
anyway, so if the thread hasn't finished it simply blocks on the same lock.
|
| 70 |
"""
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
None" would let two simultaneous first-visitors each start a ~2 GB model load.
|
| 10 |
Hence the explicit double-checked locking below: the lock is held across the
|
| 11 |
load, and a second caller blocks and then sees the finished object.
|
| 12 |
+
|
| 13 |
+
WHY LOADING NOW HAPPENS AT IMPORT, ON THE MAIN THREAD
|
| 14 |
+
-----------------------------------------------------
|
| 15 |
+
This used to load lazily: `load_agents()` from inside `demo.load`, and the
|
| 16 |
+
chatbot models on a daemon thread via `warm_chatbot_models_async()`. That kept
|
| 17 |
+
the boot splash responsive and got the Space green early — but it meant NOVA
|
| 18 |
+
loaded *every* model on a thread that was not the main thread, which is exactly
|
| 19 |
+
what ZeroGPU cannot tolerate. `spaces` fakes CUDA so nothing really initialises
|
| 20 |
+
it before the GPU fork, but the TorchFunctionMode/TorchDispatchMode doing that
|
| 21 |
+
work are thread-local to the thread that called `patch()` — the main thread, at
|
| 22 |
+
import — while `torch.cuda.is_available()` is patched globally to return True
|
| 23 |
+
for every thread. So off-main-thread model loading runs against a torch that
|
| 24 |
+
claims a GPU exists with none of the interception that makes the claim safe.
|
| 25 |
+
|
| 26 |
+
`preload_all()` below therefore runs at import, on the main thread, before
|
| 27 |
+
Gradio ever spawns a worker. See the CUDA-VIRGINITY RULE in ui/gpu.py.
|
| 28 |
+
|
| 29 |
+
The cost is honest and worth naming: the server now binds its port only after
|
| 30 |
+
~2 GB of weights are resident, so the Space shows "Starting" for longer instead
|
| 31 |
+
of showing NOVA's own splash. We trade a nicer boot animation for a GPU path
|
| 32 |
+
that actually works.
|
| 33 |
"""
|
| 34 |
|
| 35 |
import threading
|
|
|
|
| 40 |
_chatbot_lock = threading.Lock()
|
| 41 |
_chatbot_models = None
|
| 42 |
|
| 43 |
+
# Set by preload_all() when a load fails, so nova_app.py can show a named error
|
| 44 |
+
# on the splash instead of the Space dying at import with a raw traceback.
|
| 45 |
+
BOOT_ERROR: str | None = None
|
| 46 |
+
|
| 47 |
|
| 48 |
def load_agents():
|
| 49 |
"""Import the compiled LangGraph agents. Importing the search graph also
|
|
|
|
| 82 |
return _chatbot_models
|
| 83 |
|
| 84 |
|
| 85 |
+
def preload_all() -> "str | None":
|
| 86 |
+
"""Load every model NOW, on the calling thread. Call this at import from
|
| 87 |
+
nova_app.py, which runs on the main thread.
|
| 88 |
+
|
| 89 |
+
Replaces the old pair of lazy paths (`load_agents()` inside `demo.load`, and
|
| 90 |
+
`warm_chatbot_models_async()`'s daemon thread). Both put multi-hundred-MB
|
| 91 |
+
transformer loads on non-main threads, which is the one thing a ZeroGPU
|
| 92 |
+
Space must not do — see the module docstring above and ui/gpu.py.
|
| 93 |
|
| 94 |
+
Never raises. A failure here is almost always a missing API key, and the
|
| 95 |
+
old lazy path had the nice property of surfacing that as a readable message
|
| 96 |
+
on the splash rather than killing the Space at boot. That property is worth
|
| 97 |
+
keeping, so the error is stashed in BOOT_ERROR and rendered by do_boot().
|
|
|
|
|
|
|
|
|
|
| 98 |
"""
|
| 99 |
+
global BOOT_ERROR
|
| 100 |
+
current = threading.current_thread()
|
| 101 |
+
if current is not threading.main_thread():
|
| 102 |
+
# Not fatal — but it means the caller moved this off the main thread,
|
| 103 |
+
# which silently reintroduces the exact bug this function exists to
|
| 104 |
+
# prevent. Say so loudly rather than fail mysteriously an hour later.
|
| 105 |
+
print(
|
| 106 |
+
f"[preload_all] WARNING: running on thread {current.name!r}, not the main "
|
| 107 |
+
f"thread. ZeroGPU's torch patching is thread-local; models loaded here are "
|
| 108 |
+
f"not covered by it and the GPU fork may fail.",
|
| 109 |
+
flush=True,
|
| 110 |
+
)
|
| 111 |
+
try:
|
| 112 |
+
load_agents()
|
| 113 |
+
warm_chatbot_models()
|
| 114 |
+
except Exception as e:
|
| 115 |
+
import traceback
|
| 116 |
+
traceback.print_exc()
|
| 117 |
+
BOOT_ERROR = f"{type(e).__name__}: {e}"
|
| 118 |
+
return BOOT_ERROR
|