Spaces:
Running on Zero
Running on Zero
sync: from ael backend/space/
Browse files- rerank.py +70 -17
- 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
|
| 100 |
return _router
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
with _router_lock:
|
| 102 |
-
if _router is not None or
|
| 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":
|
| 205 |
}
|
| 206 |
if not ENABLED or not results:
|
| 207 |
-
return out
|
| 208 |
router = _get_router()
|
| 209 |
if router is None:
|
| 210 |
-
|
| 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):
|