cazyundee commited on
Commit
d409fec
·
1 Parent(s): dbcfcbe

sync: from ael backend/space/

Browse files
Files changed (2) hide show
  1. rerank.py +70 -17
  2. searx_bridge.py +8 -2
rerank.py CHANGED
@@ -86,6 +86,14 @@ _router = None
86
  _router_lock = threading.Lock()
87
  _load_error = None
88
  _load_message = None
 
 
 
 
 
 
 
 
89
 
90
 
91
  def _get_router():
@@ -95,28 +103,62 @@ def _get_router():
95
  serves search, and the route needs to be able to ask "any opinion?"
96
  without handling an exception.
97
  """
98
- global _router, _load_error, _load_message
99
- if _router is not None or _load_error is not None:
100
  return _router
 
 
 
 
 
 
 
 
101
  with _router_lock:
102
- if _router is not None or _load_error is not None:
103
  return _router
 
104
  try:
105
  from laya import Router
106
 
107
  # max_loaded=1: one checkpoint resident, which is all this uses.
108
  _router = Router(max_loaded=1)
109
  _router.preload([MODEL])
 
 
 
110
  except Exception as e: # noqa: BLE001 - reported, never raised
111
  _load_error = f"{type(e).__name__}"
 
112
  # The message matters and nothing else about this exception does:
113
  # "the reranker is off" is a decision, "the reranker broke because
114
  # <this>" is a bug report. The caller redacts before logging it.
115
  _load_message = f"{type(e).__name__}: {e}"[:300]
116
  print(f"[rerank] disabled: cannot load laya ({_load_error}): {e}", flush=True)
 
 
117
  return _router
118
 
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  def _no_opinion():
121
  """Why this module has nothing to say, for the caller.
122
 
@@ -130,6 +172,8 @@ def _no_opinion():
130
  """
131
  if not ENABLED:
132
  return "disabled by config"
 
 
133
  if _load_error:
134
  return _load_message or _load_error
135
  return None
@@ -141,6 +185,7 @@ def status():
141
  return {
142
  "enabled": ENABLED,
143
  "ready": router is not None,
 
144
  "model": MODEL,
145
  "drop_below": DROP_BELOW,
146
  "min_results": MIN_RESULTS,
@@ -186,6 +231,20 @@ def _passage(result):
186
  return (title + "\n" + snippet).strip()
187
 
188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  def rerank(query, results, budget_ms=800):
190
  """Score, reorder and optionally drop `results` for `query`.
191
 
@@ -201,19 +260,13 @@ def rerank(query, results, budget_ms=800):
201
  "dropped": 0,
202
  "starved": False,
203
  "scores": [],
204
- "no_opinion": _no_opinion(),
205
  }
206
  if not ENABLED or not results:
207
- return out
208
  router = _get_router()
209
  if router is None:
210
- # Recomputed here, not reused from above: `_no_opinion()` reads
211
- # `_load_error`, and until this line nothing had *tried* to load the
212
- # model, so the value captured in `out` was always None on exactly the
213
- # path that needed it. The first version of this reported "no opinion"
214
- # for a reranker that had never once been installed.
215
- out["no_opinion"] = _no_opinion()
216
- return out
217
 
218
  t0 = time.time()
219
  scored = []
@@ -228,15 +281,15 @@ def rerank(query, results, budget_ms=800):
228
  p = float(res["answers"]["relevant"]["noul"])
229
  except Exception as e: # noqa: BLE001
230
  print(f"[rerank] score failed for result {i}: {type(e).__name__}: {e}", flush=True)
231
- return dict(out, results=results) # fail open, whole set untouched
232
  if (time.time() - t0) * 1000 > budget_ms:
233
  # Over budget: return what we have not yet reordered, in order.
234
  print(f"[rerank] over budget ({budget_ms}ms) after {i} results", flush=True)
235
- return dict(out, results=results)
236
  scored.append((p, i, r))
237
 
238
  if not any(p is not None for p, _, _ in scored):
239
- return out
240
 
241
  # Highest score first. Unscored results keep their relative position at the
242
  # end rather than being dropped: we have no opinion about them.
@@ -255,7 +308,7 @@ def rerank(query, results, budget_ms=800):
255
  # The model rejected everything, which is a model that did not
256
  # understand the query rather than a set with nothing in it. Say so
257
  # instead of pretending.
258
- return dict(out, results=results, starved=True)
259
 
260
  out.update(ok=True, results=kept, dropped=dropped)
261
- return out
 
86
  _router_lock = threading.Lock()
87
  _load_error = None
88
  _load_message = None
89
+ _loading = False
90
+ _load_failed_at = 0.0
91
+
92
+ # A failed load used to latch for the lifetime of the container, which turned one
93
+ # flaky checkpoint download into a permanently disabled reranker — the Space
94
+ # would report "no opinion" for the rest of its life and nobody would know why.
95
+ # Failures are retried on a cooldown instead.
96
+ RETRY_AFTER_S = float(os.environ.get("AEL_RERANK_RETRY_S", "60"))
97
 
98
 
99
  def _get_router():
 
103
  serves search, and the route needs to be able to ask "any opinion?"
104
  without handling an exception.
105
  """
106
+ global _router, _load_error, _load_message, _loading, _load_failed_at
107
+ if _router is not None:
108
  return _router
109
+ if _load_error is not None and (time.time() - _load_failed_at) < RETRY_AFTER_S:
110
+ return None
111
+ if _loading:
112
+ # Never block a search on a checkpoint download. The caller's deadline is
113
+ # ~1s; loading 421M parameters takes minutes the first time and is not
114
+ # something to discover inside a user's request. `warm()` exists so this
115
+ # is the rare path rather than the normal one.
116
+ return None
117
  with _router_lock:
118
+ if _router is not None or _loading:
119
  return _router
120
+ _loading = True
121
  try:
122
  from laya import Router
123
 
124
  # max_loaded=1: one checkpoint resident, which is all this uses.
125
  _router = Router(max_loaded=1)
126
  _router.preload([MODEL])
127
+ _load_error = None
128
+ _load_message = None
129
+ print(f"[rerank] {MODEL} ready", flush=True)
130
  except Exception as e: # noqa: BLE001 - reported, never raised
131
  _load_error = f"{type(e).__name__}"
132
+ _load_failed_at = time.time()
133
  # The message matters and nothing else about this exception does:
134
  # "the reranker is off" is a decision, "the reranker broke because
135
  # <this>" is a bug report. The caller redacts before logging it.
136
  _load_message = f"{type(e).__name__}: {e}"[:300]
137
  print(f"[rerank] disabled: cannot load laya ({_load_error}): {e}", flush=True)
138
+ finally:
139
+ _loading = False
140
  return _router
141
 
142
 
143
+ def warm():
144
+ """Start loading the checkpoint now, in the background.
145
+
146
+ Called once when the Space boots. Without this the first search request of
147
+ every container lifetime paid the full download-and-load cost inside an
148
+ 800ms budget, so the reranker could never once have applied on a cold
149
+ container — and the only symptom was a timeout, which reads like a slow
150
+ network. Warming moves the cost to the ~2 minutes after a deploy, where
151
+ nobody is waiting.
152
+
153
+ Never raises and never blocks: a Space that cannot afford the model still
154
+ serves search.
155
+ """
156
+ if not ENABLED or _router is not None or _loading:
157
+ return
158
+ t = threading.Thread(target=_get_router, name="rerank-warm", daemon=True)
159
+ t.start()
160
+
161
+
162
  def _no_opinion():
163
  """Why this module has nothing to say, for the caller.
164
 
 
172
  """
173
  if not ENABLED:
174
  return "disabled by config"
175
+ if _loading:
176
+ return "still loading"
177
  if _load_error:
178
  return _load_message or _load_error
179
  return None
 
185
  return {
186
  "enabled": ENABLED,
187
  "ready": router is not None,
188
+ "loading": _loading,
189
  "model": MODEL,
190
  "drop_below": DROP_BELOW,
191
  "min_results": MIN_RESULTS,
 
231
  return (title + "\n" + snippet).strip()
232
 
233
 
234
+ def _stamp(out):
235
+ """Attach the *current* reason at the moment of return.
236
+
237
+ Stamping once, at the top, is wrong in both directions and both happened:
238
+ a reason captured before the load was attempted is `None` on exactly the
239
+ path that needs one, and a reason captured before a *successful* retry
240
+ survives into an `ok: true` response. A field that is always populated is a
241
+ field everyone learns to ignore, which is the failure this whole mechanism
242
+ exists to prevent.
243
+ """
244
+ out["no_opinion"] = _no_opinion()
245
+ return out
246
+
247
+
248
  def rerank(query, results, budget_ms=800):
249
  """Score, reorder and optionally drop `results` for `query`.
250
 
 
260
  "dropped": 0,
261
  "starved": False,
262
  "scores": [],
263
+ "no_opinion": None,
264
  }
265
  if not ENABLED or not results:
266
+ return _stamp(out)
267
  router = _get_router()
268
  if router is None:
269
+ return _stamp(out)
 
 
 
 
 
 
270
 
271
  t0 = time.time()
272
  scored = []
 
281
  p = float(res["answers"]["relevant"]["noul"])
282
  except Exception as e: # noqa: BLE001
283
  print(f"[rerank] score failed for result {i}: {type(e).__name__}: {e}", flush=True)
284
+ return _stamp(dict(out, results=results)) # fail open, whole set untouched
285
  if (time.time() - t0) * 1000 > budget_ms:
286
  # Over budget: return what we have not yet reordered, in order.
287
  print(f"[rerank] over budget ({budget_ms}ms) after {i} results", flush=True)
288
+ return _stamp(dict(out, results=results))
289
  scored.append((p, i, r))
290
 
291
  if not any(p is not None for p, _, _ in scored):
292
+ return _stamp(out)
293
 
294
  # Highest score first. Unscored results keep their relative position at the
295
  # end rather than being dropped: we have no opinion about them.
 
308
  # The model rejected everything, which is a model that did not
309
  # understand the query rather than a set with nothing in it. Say so
310
  # instead of pretending.
311
+ return _stamp(dict(out, results=results, starved=True))
312
 
313
  out.update(ok=True, results=kept, dropped=dropped)
314
+ return _stamp(out)
searx_bridge.py CHANGED
@@ -24,6 +24,13 @@ from fastapi.responses import JSONResponse
24
  import rerank
25
  import searx_runtime
26
 
 
 
 
 
 
 
 
27
 
28
  def register_routes(fa_app):
29
  @fa_app.get("/respite/v2/searx/status")
@@ -33,8 +40,7 @@ def register_routes(fa_app):
33
  "installed": searx_runtime.searxng_ready(),
34
  "boot_error": searx_runtime.boot_error(),
35
  "port": searx_runtime.SEARXNG_PORT,
36
- "reranker": rerank.status(),
37
- })
38
 
39
  @fa_app.get("/respite/v2/searx/search")
40
  async def _search(request: Request):
 
24
  import rerank
25
  import searx_runtime
26
 
27
+ # Load the reranker's checkpoint as soon as the container boots, not on the
28
+ # first search request. The first request has a ~1s deadline and the load takes
29
+ # minutes, so without this the reranker can never apply on a cold container and
30
+ # the only symptom is a timeout. A daemon thread, so a failure or a slow mirror
31
+ # cannot hold the Space's startup hostage.
32
+ rerank.warm()
33
+
34
 
35
  def register_routes(fa_app):
36
  @fa_app.get("/respite/v2/searx/status")
 
40
  "installed": searx_runtime.searxng_ready(),
41
  "boot_error": searx_runtime.boot_error(),
42
  "port": searx_runtime.SEARXNG_PORT,
43
+ "reranker": rerank.status(), })
 
44
 
45
  @fa_app.get("/respite/v2/searx/search")
46
  async def _search(request: Request):