Studio: precache RAG reranker on startup; instrument loader + predict
The reranker model (BAAI/bge-reranker-base by default, ~1.1 GB) was
never precached, so the first user-facing rerank call paid the full
download cost — which on slow connections looked like a hang and got
retried by upstream timeouts. The deprecation warning that surfaced
during the hang was actually from sentence-transformers internals
firing while the download was still in flight.
Mirror the precache_helper_gguf pattern: add precache_reranker() that
calls snapshot_download in a daemon thread at FastAPI startup. The
first opt-in rerank now finds the weights already on disk and only
pays the in-process model load.
Also tighten the loader:
- explicit device selection (cuda when torch.cuda.is_available,
else cpu) so we don't rely on sentence-transformers auto-detect
behaviour that has historically picked cpu under odd
CUDA_VISIBLE_DEVICES configs;
- structlog-shaped logs with elapsed_seconds around load + predict
so a real runtime hang is visible in /tmp/studio.log with
'RAG reranker predict starting' / 'RAG reranker predict done'.
This commit is contained in:
parent
8198459597
commit
3f6a390df6
2 changed files with 82 additions and 2 deletions
|
|
@ -7,6 +7,7 @@ from __future__ import annotations
|
|||
|
||||
import gc
|
||||
import threading
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from loggers import get_logger
|
||||
|
|
@ -21,11 +22,70 @@ _model: Any | None = None
|
|||
_model_name: str | None = None
|
||||
|
||||
|
||||
def _resolve_device() -> str:
|
||||
"""Prefer CUDA when available; otherwise CPU. Explicit so we don't rely
|
||||
on sentence-transformers' auto-detect (which historically picks CPU when
|
||||
CUDA_VISIBLE_DEVICES is set funny)."""
|
||||
try:
|
||||
import torch
|
||||
|
||||
if torch.cuda.is_available():
|
||||
return "cuda"
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
return "cpu"
|
||||
|
||||
|
||||
def _load(model_name: str) -> Any:
|
||||
from sentence_transformers import CrossEncoder
|
||||
|
||||
logger.info("Loading RAG reranker: %s", model_name)
|
||||
return CrossEncoder(model_name)
|
||||
device = _resolve_device()
|
||||
logger.info(
|
||||
"Loading RAG reranker",
|
||||
model = model_name,
|
||||
device = device,
|
||||
)
|
||||
started = time.perf_counter()
|
||||
model = CrossEncoder(model_name, device = device)
|
||||
logger.info(
|
||||
"RAG reranker loaded",
|
||||
model = model_name,
|
||||
device = device,
|
||||
elapsed_seconds = round(time.perf_counter() - started, 2),
|
||||
)
|
||||
return model
|
||||
|
||||
|
||||
def precache_reranker(model_name: str | None = None) -> None:
|
||||
"""Download reranker weights into the HF cache (no instantiation).
|
||||
|
||||
Mirrors ``precache_helper_gguf``: runs in a background thread on
|
||||
FastAPI startup so the first user-facing rerank doesn't pay the
|
||||
~1.1 GB download. Safe to call when the model is already cached
|
||||
(huggingface_hub no-ops on existing files).
|
||||
"""
|
||||
target = model_name or RAG_RERANKER_MODEL
|
||||
try:
|
||||
from huggingface_hub import snapshot_download
|
||||
from huggingface_hub.utils import disable_progress_bars
|
||||
|
||||
disable_progress_bars()
|
||||
logger.info("Pre-caching RAG reranker", model = target)
|
||||
started = time.perf_counter()
|
||||
snapshot_download(repo_id = target, repo_type = "model")
|
||||
logger.info(
|
||||
"RAG reranker cached",
|
||||
model = target,
|
||||
elapsed_seconds = round(time.perf_counter() - started, 2),
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
# Non-critical: the lazy loader will retry the download on first
|
||||
# use. We log so the user can see what happened.
|
||||
logger.warning(
|
||||
"RAG reranker precache failed; will download lazily",
|
||||
model = target,
|
||||
error = str(exc),
|
||||
)
|
||||
|
||||
|
||||
def get_reranker(model_name: str | None = None) -> Any:
|
||||
|
|
@ -72,11 +132,22 @@ def rerank(
|
|||
model = get_reranker(model_name)
|
||||
if text_pairs:
|
||||
inputs = [(query, text) for _, text in text_pairs]
|
||||
logger.info(
|
||||
"RAG reranker predict starting",
|
||||
n_inputs = len(inputs),
|
||||
batch_size = RAG_RERANK_BATCH_SIZE,
|
||||
)
|
||||
started = time.perf_counter()
|
||||
scores = model.predict(
|
||||
inputs,
|
||||
batch_size = RAG_RERANK_BATCH_SIZE,
|
||||
show_progress_bar = False,
|
||||
)
|
||||
logger.info(
|
||||
"RAG reranker predict done",
|
||||
n_inputs = len(inputs),
|
||||
elapsed_seconds = round(time.perf_counter() - started, 2),
|
||||
)
|
||||
ranked = sorted(
|
||||
zip(text_pairs, scores),
|
||||
key = lambda item: float(item[1]),
|
||||
|
|
|
|||
|
|
@ -260,7 +260,16 @@ async def lifespan(app: FastAPI):
|
|||
except Exception:
|
||||
pass # non-critical
|
||||
|
||||
def _precache_reranker():
|
||||
try:
|
||||
from core.rag.reranker import precache_reranker
|
||||
|
||||
precache_reranker()
|
||||
except Exception:
|
||||
pass # non-critical
|
||||
|
||||
threading.Thread(target = _precache, daemon = True).start()
|
||||
threading.Thread(target = _precache_reranker, daemon = True).start()
|
||||
|
||||
# Initialize RSA key pair for API key encryption (external providers)
|
||||
from core.inference.key_exchange import init_key_pair
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue