S-4-G-4-R commited on
Commit
fd076ad
·
verified ·
1 Parent(s): 2589a11

Update ui/agents.py

Browse files
Files changed (1) hide show
  1. 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 warm_chatbot_models_async():
61
- """Kick the chatbot warm-up onto a daemon thread.
 
 
 
 
 
 
62
 
63
- The Streamlit app warmed BOTH model sets behind one blocking splash, which
64
- meant nobody saw a usable page until ~2 GB of weights had downloaded. Only
65
- the agents are needed to act on the very first click, so we block on those
66
- and let the chatbot models finish in the background they have until the
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
- threading.Thread(target=warm_chatbot_models, daemon=True).start()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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