Spaces:
Sleeping
Sleeping
| """ZeroGPU Space: ZeroEntropy zembed-1 (embed) + zerank-2 (rerank) on a free H200 — for the | |
| ZeroEntropy-vs-current A/B benchmark. Both are 4B (Qwen3-based), so they need the GPU. | |
| Endpoints (call via gradio_client): | |
| - `/embed` (texts, mode) -> L2-normalised vectors. mode='query' -> encode_query (search-query | |
| task prompt); anything else -> encode_document. Optional Matryoshka | |
| truncation via EMBED_DIM. | |
| - `/rerank` (groups) -> per-group relevance logits, groups = [[query, [passages]], ...]. | |
| Models load to CPU once at startup, `.to('cuda')` per call (no re-download). `@spaces.GPU(duration=)` | |
| is generous because 4B inference is much slower than a 300M embedder. | |
| """ | |
| import os | |
| import gradio as gr | |
| import numpy as np | |
| import spaces | |
| from sentence_transformers import CrossEncoder, SentenceTransformer | |
| EMB_ID = os.environ.get("EMBED_MODEL_ID", "zeroentropy/zembed-1-embedding") | |
| RR_ID = os.environ.get("RERANK_MODEL_ID", "zeroentropy/zerank-2-reranker") | |
| EMB_DIM = int(os.environ.get("EMBED_DIM", "0")) # 0 = full (2560); else Matryoshka-truncate + renorm | |
| TOK = os.environ.get("HF_TOKEN") | |
| EMB = SentenceTransformer(EMB_ID, trust_remote_code=True, | |
| model_kwargs={"torch_dtype": "bfloat16"}, device="cpu", token=TOK) | |
| RR = CrossEncoder(RR_ID, trust_remote_code=True, device="cpu", token=TOK) | |
| _LOG = {"emb": 0, "rr": 0} | |
| print(f"[startup] loaded embed={EMB_ID} rerank={RR_ID} dim={EMB_DIM or 'full'}", flush=True) | |
| def _emb_dur(texts, mode="document"): | |
| return min(300, 30 + len(texts or []) // 4) # 4B embed is slow | |
| def _rr_dur(groups): | |
| n = sum(len(g[1]) for g in (groups or [])) | |
| return min(300, 30 + n // 20) | |
| def embed(texts, mode="document"): | |
| if not texts: | |
| return [] | |
| _LOG["emb"] += len(texts) | |
| EMB.to("cuda") | |
| enc = EMB.encode_query if mode == "query" else EMB.encode_document | |
| print(f"[embed:{mode}] +{len(texts)} on cuda -> {_LOG['emb']} total", flush=True) | |
| v = np.asarray(enc([(t or "")[:6000] for t in texts], batch_size=16, | |
| convert_to_numpy=True, device="cuda"), dtype=np.float32) | |
| if EMB_DIM and EMB_DIM < v.shape[1]: | |
| v = v[:, :EMB_DIM] | |
| v = v / (np.linalg.norm(v, axis=1, keepdims=True) + 1e-9) # (re)normalise (and after truncation) | |
| return v.tolist() | |
| def rerank(groups): | |
| """groups: list of [query, [passages]]. Returns one list of relevance logits per group.""" | |
| if not groups: | |
| return [] | |
| _LOG["rr"] += len(groups) | |
| RR.model.to("cuda") | |
| pairs, spans = [], [0] | |
| for q, ps in groups: | |
| pairs += [(q, (p or "")[:4000]) for p in ps] | |
| spans.append(len(pairs)) | |
| print(f"[rerank] +{len(groups)} q / {len(pairs)} pairs -> {_LOG['rr']} total", flush=True) | |
| scores = RR.predict(pairs, batch_size=16, convert_to_numpy=True) if pairs else np.array([]) | |
| return [list(map(float, scores[spans[i]:spans[i + 1]])) for i in range(len(groups))] | |
| with gr.Blocks() as demo: | |
| gr.Markdown("ZeroGPU zembed-1 + zerank-2 — call /embed (texts, mode) and /rerank (groups) via gradio_client.") | |
| ein, emode, eout = gr.JSON(label="texts"), gr.Textbox(label="mode", value="document"), gr.JSON(label="vectors") | |
| gr.Button("embed").click(embed, [ein, emode], eout, api_name="embed") | |
| rin, rout = gr.JSON(label="groups [[query,[passages]],...]"), gr.JSON(label="scores") | |
| gr.Button("rerank").click(rerank, rin, rout, api_name="rerank") | |
| if __name__ == "__main__": | |
| demo.queue(max_size=256, default_concurrency_limit=1).launch() | |