Views
No views yet

-4B-exp) is an experimental extension built on top of Qwen3-Reranker-4B, demonstrating that the same recipe transfers to an existing LLM-based reranker without losing ranking quality.| Model | Backbone | Parameters | Hugging Face |
|---|---|---|---|
| Prism-Qwen3.5-Reranker-0.8B | Qwen3.5 | 0.8B | infgrad/Prism-Qwen3.5-Reranker-0.8B |
| Prism-Qwen3.5-Reranker-2B | Qwen3.5 | 2B | infgrad/Prism-Qwen3.5-Reranker-2B |
| Prism-Qwen3.5-Reranker-4B | Qwen3.5 | 4B | infgrad/Prism-Qwen3.5-Reranker-4B |
| Prism-Qwen3.5-Reranker-9B | Qwen3.5 | 9B | infgrad/Prism-Qwen3.5-Reranker-9B |
| Prism-Qwen3-Reranker-4B-exp | Qwen3-Reranker-4B | 4B | infgrad/Prism-Qwen3-Reranker-4B-exp |
s(q, d) = σ(ℓ_yes − ℓ_no) ∈ (0, 1). Calibrated, ranking-ready.<contribution> — one sentence stating every core point the document contributes to the query. Useful for the agent to plan its next step without re-reading the doc.<evidence> — a self-contained, faithfully-rephrased rewrite of the query-relevant content. Drops irrelevant background, preserves verbatim proper nouns / numbers / dates / code / URLs. You can feed <evidence> directly to a downstream LLM and skip the raw document — saving context tokens and removing web-noise.no and stops. No contribution/evidence is generated.yes/no + <contribution> + <evidence>, supervised by a 5-LLM-as-judge ensemble.s(q, d) = σ(ℓ_yes − ℓ_no). Use A when you also want <contribution> / <evidence>. Use B when you only need a score and want a drop-in replacement for any other CrossEncoder reranker.1QUERY = "What is the boiling point of water at sea level?"
2DOCUMENTS = [
3 "Water boils at 100 C (212 F) at standard atmospheric pressure (1 atm), "
4 "which corresponds to sea-level conditions.",
5 "Mount Everest is the highest mountain on Earth, with a peak elevation "
6 "of 8,848 meters above sea level.",
7]1import torch
2from transformers import AutoModelForCausalLM, AutoTokenizer
3
4MODEL_PATH = "infgrad/Prism-Qwen3.5-Reranker-4B" # or any sibling repo above
5
6SYSTEM_PROMPT = (
7 "Judge whether the Document meets the requirements based on "
8 "the Query and the Instruct provided. "
9)
10
11INSTRUCTION = (
12 'Judge if the document is relevant to the query. Reply "yes" or "no".\n'
13 'On "yes", also emit:\n'
14 "<contribution>One sentence covering every core point the document "
15 "contributes to the query, without elaboration.</contribution>\n"
16 "<evidence>Self-contained rewrite of the query-relevant content. Rules:\n"
17 "- Faithful: rephrase only; add or infer nothing.\n"
18 "- Self-contained: evidence alone must fully answer the query.\n"
19 "- Concise: drop query-irrelevant background.\n"
20 "- Verbatim (no translation): proper nouns, terms, abbreviations, "
21 "numbers, dates, code, URLs.\n"
22 "- Output language: multilingual doc → query's language; else doc's language."
23 "</evidence>"
24)
25
26PROMPT_TEMPLATE = (
27 "<|im_start|>system\n{system}<|im_end|>\n"
28 "<|im_start|>user\n"
29 "<Instruct>: {instruction}\n"
30 "<Query>: {query}\n"
31 "<Document>: {doc}<|im_end|>\n"
32 "<|im_start|>assistant\n<think>\n\n</think>\n\n"
33)
34
35
36def build_prompt(query: str, doc: str) -> str:
37 return PROMPT_TEMPLATE.format(
38 system=SYSTEM_PROMPT, instruction=INSTRUCTION, query=query, doc=doc
39 )
40
41
42tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)
43model = AutoModelForCausalLM.from_pretrained(
44 MODEL_PATH,
45 torch_dtype=torch.bfloat16,
46 device_map="cuda",
47 attn_implementation="sdpa",
48).eval()
49
50yes_id = tokenizer.encode("yes", add_special_tokens=False)[0]
51no_id = tokenizer.encode("no", add_special_tokens=False)[0]
52
53
54@torch.no_grad()
55def rerank(query: str, doc: str, max_new_tokens: int = 512):
56 prompt = build_prompt(query, doc)
57 input_ids = tokenizer(prompt, return_tensors="pt").input_ids.to(model.device)
58
59 out = model.generate(
60 input_ids=input_ids,
61 max_new_tokens=max_new_tokens,
62 do_sample=False,
63 return_dict_in_generate=True,
64 output_scores=True,
65 pad_token_id=tokenizer.pad_token_id or tokenizer.eos_token_id,
66 )
67
68 # Relevance score = softmax over {yes, no} at the first generated token.
69 first_logprobs = torch.log_softmax(out.scores[0][0].float(), dim=-1)
70 yes_p = first_logprobs[yes_id].exp()
71 no_p = first_logprobs[no_id].exp()
72 score = (yes_p / (yes_p + no_p)).item()
73
74 # Decoded text holds yes/no plus <contribution>...</contribution><evidence>...</evidence>
75 gen_ids = out.sequences[0, input_ids.shape[1]:]
76 text = tokenizer.decode(gen_ids, skip_special_tokens=True)
77 return {"score": score, "text": text}
78
79
80for doc in DOCUMENTS:
81 print(rerank(QUERY, doc))1{"score": 0.98, "text": "yes\n<contribution>...</contribution>\n<evidence>...</evidence>"}
2{"score": 0.01, "text": "no"}text is just "no".sentence-transformers >= 5.4.0. Note: in this mode <contribution> and <evidence> are not produced — only the calibrated relevance score.chat_template.jinja and are not configurable — the model was trained with one fixed prompt and only that prompt produces calibrated scores. You only pass (query, document); the rest is hardcoded.1import torch
2from sentence_transformers import CrossEncoder
3
4MODEL_PATH = "infgrad/Prism-Qwen3.5-Reranker-4B" # or any sibling repo above
5
6ce = CrossEncoder(MODEL_PATH, model_kwargs={"torch_dtype": torch.bfloat16})
7
8# 1) Score (q, d) pairs. The default activation is Sigmoid, so scores are in (0, 1)
9# and equal to s(q, d) = sigmoid(logit_yes - logit_no) — identical to path A above.
10pairs = [(QUERY, doc) for doc in DOCUMENTS]
11scores = ce.predict(pairs)
12print(scores)
13# array([0.98, 0.01], dtype=float32)
14
15# 2) Rank documents directly.
16ranked = ce.rank(QUERY, DOCUMENTS, return_documents=True)
17for r in ranked:
18 print(f"{r['score']:.3f}\t{r['corpus_id']}\t{r['text'][:80]}")activation_fn=torch.nn.Identity() to ce.predict(...).batch_size > 1), CE scores can drift from path A by ~1–3% for individual pairs. The cause is bf16 SDPA: when CrossEncoder pads shorter sequences to the longest in the batch, the bf16 attention numerics differ by a few ULPs vs running each pair alone, and the difference accumulates across layers before the final sigmoid. Ranking order is unaffected. If you need bit-for-bit parity with path A:1# Option 1: keep bf16, disable batching
2ce.predict(pairs, batch_size=1)
3
4# Option 2: use fp32 (slower, larger memory)
5ce = CrossEncoder(MODEL_PATH, model_kwargs={"torch_dtype": torch.float32})yes or no — the score is well-defined even if you stop generation immediately (cheap mode: max_new_tokens=1). Generate further only when you also want contribution/evidence.temperature=0.3-0.5.1@misc{zhang2025prismreranker,
2 title = {Prism-Reranker: Beyond Relevance Scoring -- Jointly Producing Contributions and Evidence for Agentic Retrieval},
3 author = {Dun Zhang},
4 year = {2025},
5 eprint = {2604.23734},
6 archivePrefix = {arXiv},
7 primaryClass = {cs.IR},
8 url = {https://arxiv.org/abs/2604.23734},
9}dunnzhang0@gmail.com (independent researcher).