Text Ranking
Transformers
Safetensors
sentence-transformers
English
qwen3_vl
image-text-to-text
multimodal rerank
text rerank
conversational
custom_code
Instructions to use nlpai-lab/RenderRank-2B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use nlpai-lab/RenderRank-2B with Transformers:
# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("nlpai-lab/RenderRank-2B", trust_remote_code=True) model = AutoModelForMultimodalLM.from_pretrained("nlpai-lab/RenderRank-2B", trust_remote_code=True, device_map="auto") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] inputs = processor.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=256) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - sentence-transformers
How to use nlpai-lab/RenderRank-2B with sentence-transformers:
from sentence_transformers import CrossEncoder model = CrossEncoder("nlpai-lab/RenderRank-2B", trust_remote_code=True) query = "Which planet is known as the Red Planet?" passages = [ "Venus is often called Earth's twin because of its similar size and proximity.", "Mars, known for its reddish appearance, is often referred to as the Red Planet.", "Jupiter, the largest planet in our solar system, has a prominent red spot.", "Saturn, famous for its rings, is sometimes mistaken for the Red Planet." ] scores = model.predict([(query, passage) for passage in passages]) print(scores) - Notebooks
- Google Colab
- Kaggle
init
Browse files- .gitattributes +2 -0
- 1_LogitScore/config.json +5 -0
- README.md +226 -0
- Roboto-Regular.ttf +3 -0
- additional_chat_templates/reranker.jinja +52 -0
- assets/struc.pdf +0 -0
- assets/struc.png +0 -0
- chat_template.jinja +120 -0
- config.json +72 -0
- config_sentence_transformers.json +13 -0
- generation_config.json +13 -0
- model.safetensors +3 -0
- modeling_renderrank.py +66 -0
- modules.json +14 -0
- processor_config.json +62 -0
- qwen3_vl_reranker.py +314 -0
- rendering.py +239 -0
- sentence_bert_config.json +30 -0
- tokenizer.json +3 -0
- tokenizer_config.json +18 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
Roboto-Regular.ttf filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
1_LogitScore/config.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"true_token_id": 9693,
|
| 3 |
+
"false_token_id": 2152,
|
| 4 |
+
"module_input_name": "causal_logits"
|
| 5 |
+
}
|
README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
library_name: transformers
|
| 4 |
+
pipeline_tag: text-ranking
|
| 5 |
+
language:
|
| 6 |
+
- en
|
| 7 |
+
base_model:
|
| 8 |
+
- Qwen/Qwen3-VL-2B-Instruct
|
| 9 |
+
tags:
|
| 10 |
+
- sentence-transformers
|
| 11 |
+
- transformers
|
| 12 |
+
- multimodal rerank
|
| 13 |
+
- text rerank
|
| 14 |
+
- conversational
|
| 15 |
+
---
|
| 16 |
+
# RenderRank
|
| 17 |
+
|
| 18 |
+
**Learning to Rerank Text through Visual Compression**
|
| 19 |
+
|
| 20 |
+
RenderRank converts text into compact visual token sequences by rendering it as images. This change in input modality reduces the number of tokens used to represent documents, allowing more content to fit within the same context budget. Queries remain text, and relevance is scored against the visual document representations.
|
| 21 |
+
|
| 22 |
+
[](assets/struc.pdf)
|
| 23 |
+
|
| 24 |
+
## Highlights
|
| 25 |
+
|
| 26 |
+
- **Text as visual tokens:** represent document text through rendered images instead of conventional text token sequences.
|
| 27 |
+
- **Shorter input sequences:** compact visual representations reduce input length and allow more document content within a fixed context budget.
|
| 28 |
+
- **Three input modes:** score document text directly with `text`, render it automatically with `render`, or provide existing document pages with `image`.
|
| 29 |
+
|
| 30 |
+
| Property | Value |
|
| 31 |
+
| :--- | :--- |
|
| 32 |
+
| Model type | Pairwise document reranker |
|
| 33 |
+
| Architecture | Qwen3-VL |
|
| 34 |
+
| Parameters | Approximately 2.1B, including the vision encoder |
|
| 35 |
+
| Language evaluated | English |
|
| 36 |
+
| Maximum context length | 32,768 tokens, including query, template, document text, and visual tokens |
|
| 37 |
+
| Scoring | Yes/no relevance scoring |
|
| 38 |
+
|
| 39 |
+
## BEIR Performance
|
| 40 |
+
|
| 41 |
+
All rerankers score the top 100 BM25 candidates for each query. Baselines use text queries and documents; RenderRank uses text queries and documents rendered with the default 12pt configuration below. Scores are NDCG@10; Avg. is the mean across 11 datasets. Tokens is the mean input length per query–document pair before truncation, averaged across the same datasets. For RenderRank, this includes both textual and visual tokens.
|
| 42 |
+
|
| 43 |
+
| Model | AA | CFV | DBP | FQA | FVR | HQA | NFC | SD | SF | TC | TCH | Avg. | Tokens |
|
| 44 |
+
| :--- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
| 45 |
+
| gte-reranker-modernbert-base | 66.14 | 25.80 | 42.10 | 42.54 | 89.84 | 76.15 | 34.81 | 18.90 | 75.81 | 79.66 | 33.44 | 53.20 | 359.25 |
|
| 46 |
+
| mxbai-rerank-large-v1 | 16.16 | 25.61 | 45.37 | 40.29 | 82.19 | 71.58 | 37.32 | 18.96 | 75.26 | 85.84 | 37.44 | 48.73 | 347.45 |
|
| 47 |
+
| bge-reranker-large | 30.94 | 34.11 | 44.21 | 37.47 | 89.30 | 80.09 | 33.91 | 16.68 | 73.98 | 73.21 | 34.69 | 49.87 | 409.32 |
|
| 48 |
+
| LAMAR-600m | 66.21 | 36.21 | 46.23 | 41.14 | 89.49 | 79.14 | 35.04 | 20.22 | 76.97 | 80.65 | 35.76 | 55.19 | 409.32 |
|
| 49 |
+
| Qwen3-Reranker-0.6B | 68.04 | 34.34 | 43.76 | 39.74 | 87.13 | 77.25 | 36.60 | 20.44 | 77.00 | 85.54 | 30.32 | 54.56 | 438.70 |
|
| 50 |
+
| llama-nemotron-rerank-1b-v2 | 54.29 | 27.37 | 45.13 | 47.11 | 87.66 | 80.34 | 38.14 | 21.53 | 79.82 | 83.14 | 32.47 | 54.27 | 363.99 |
|
| 51 |
+
| mxbai-rerank-large-v2 | 42.23 | 23.03 | 38.01 | 28.85 | 70.84 | 69.50 | 35.17 | 17.49 | 78.81 | 67.78 | 46.94 | 47.15 | 449.76 |
|
| 52 |
+
| LightOn-rerank-PW-2B | 45.52 | 23.21 | 42.60 | 35.66 | 86.51 | 73.05 | 35.61 | 16.64 | 77.70 | 81.24 | 36.91 | 50.42 | 416.22 |
|
| 53 |
+
| bge-reranker-v2-gemma | 74.98 | 32.03 | 45.01 | 43.18 | 87.45 | 80.47 | 36.25 | 19.58 | 77.43 | 80.08 | 37.56 | 55.82 | 399.11 |
|
| 54 |
+
| Qwen3-Reranker-4B | 75.40 | 37.84 | 47.02 | 44.63 | 89.14 | 79.06 | 37.48 | 23.59 | 79.47 | 85.47 | 38.77 | 57.99 | 438.70 |
|
| 55 |
+
| zerank-2-reranker | 44.87 | 23.65 | 44.95 | 42.75 | 83.22 | 70.97 | 38.50 | 20.23 | 79.35 | 85.24 | 38.10 | 51.98 | 380.69 |
|
| 56 |
+
| LightOn-rerank-PW-4B | 52.50 | 28.24 | 44.80 | 40.27 | 87.70 | 72.79 | 37.33 | 17.29 | 76.72 | 79.54 | 36.70 | 52.17 | 416.22 |
|
| 57 |
+
| **RenderRank (Ours)** | 69.11 | 35.91 | 46.19 | 42.66 | 88.92 | 78.60 | 37.31 | 20.75 | 77.00 | 84.82 | 34.27 | **55.96** | **290.07** |
|
| 58 |
+
|
| 59 |
+
AA: ArguAna; CFV: Climate-FEVER; DBP: DBPedia; FQA: FiQA; FVR: FEVER; HQA: HotpotQA; NFC: NFCorpus; SD: SCIDOCS; SF: SciFact; TC: TREC-COVID; TCH: Touché-2020.
|
| 60 |
+
|
| 61 |
+
## Long-Document Performance
|
| 62 |
+
|
| 63 |
+
Results on the English subset of MLDR and three LongEmbed datasets: 2WikiMQA, QMSum, and SummScreenFD. The comparison includes models supporting at least 16K input tokens. MLDR follows the MMTEB reranking protocol; for LongEmbed, each query reranks eight candidates retrieved with Qwen3-Embedding-0.6B.
|
| 64 |
+
|
| 65 |
+
Each dataset cell shows NDCG@10 followed by the average input token count in brackets: **score [tokens]**. Token counts are measured per query–document pair before truncation and include both textual and visual tokens for RenderRank. Avg. reports the mean score across the four datasets.
|
| 66 |
+
|
| 67 |
+
| Model | MLDR | 2WikiMQA | QMSum | SummScreenFD | Avg. |
|
| 68 |
+
| :--- | ---: | ---: | ---: | ---: | ---: |
|
| 69 |
+
| Qwen3-Reranker-0.6B | 99.63 [8863.4] | 94.54 [9230.2] | 56.43 [13543.6] | 98.25 [8606.3] | 87.21 |
|
| 70 |
+
| LightOn-rerank-PW-2B | 98.91 [8878.0] | 71.27 [9230.1] | 54.30 [13876.6] | 93.05 [8977.2] | 79.38 |
|
| 71 |
+
| Qwen3-Reranker-4B | 99.85 [8863.4] | 94.54 [9230.2] | 59.11 [13543.6] | 99.11 [8606.3] | 88.15 |
|
| 72 |
+
| zerank-2-reranker | 99.57 [8805.6] | 94.42 [9173.1] | 60.10 [13485.9] | 98.97 [8548.5] | 88.26 |
|
| 73 |
+
| LightOn-rerank-PW-4B | 99.68 [8878.0] | 93.84 [9230.1] | 58.87 [13876.6] | 98.91 [8977.2] | 87.82 |
|
| 74 |
+
| **RenderRank (Ours)** | 99.74 [4198.0] | 94.30 [4262.2] | 59.94 [6388.8] | 99.10 [3701.9] | 88.27 |
|
| 75 |
+
|
| 76 |
+
## Usage
|
| 77 |
+
|
| 78 |
+
The examples load the model from `nlpai-lab/RenderRank-2B` on Hugging Face.
|
| 79 |
+
|
| 80 |
+
The model applies its reranking chat template internally. Pass the instruction as shown below; do not manually prepend a chat template to the document.
|
| 81 |
+
|
| 82 |
+
### Rendering
|
| 83 |
+
|
| 84 |
+
With Transformers, `{"render": document_text}` automatically renders the document before scoring. The font is included in this repository.
|
| 85 |
+
|
| 86 |
+
For Sentence Transformers, use the bundled `rendering.py` to create a list of PIL page images first:
|
| 87 |
+
|
| 88 |
+
```python
|
| 89 |
+
import sys
|
| 90 |
+
from pathlib import Path
|
| 91 |
+
|
| 92 |
+
from huggingface_hub import snapshot_download
|
| 93 |
+
|
| 94 |
+
repo_dir = Path(snapshot_download(
|
| 95 |
+
"nlpai-lab/RenderRank-2B",
|
| 96 |
+
allow_patterns=["rendering.py", "Roboto-Regular.ttf"],
|
| 97 |
+
))
|
| 98 |
+
sys.path.insert(0, str(repo_dir))
|
| 99 |
+
from rendering import DocumentImageConfig, DocumentImageRenderer
|
| 100 |
+
|
| 101 |
+
renderer = DocumentImageRenderer(
|
| 102 |
+
DocumentImageConfig(font_path=repo_dir / "Roboto-Regular.ttf")
|
| 103 |
+
)
|
| 104 |
+
pages = renderer.render_document("Paris is the capital of France.")
|
| 105 |
+
# pages is a list of PIL images, in document order.
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
The returned PIL images can be passed directly as image content or saved as numbered page files. The Transformers `render` mode calls this renderer internally, so no separate rendering step is needed there.
|
| 109 |
+
|
| 110 |
+
| Setting | Value |
|
| 111 |
+
| :--- | :--- |
|
| 112 |
+
| Font | Roboto Regular |
|
| 113 |
+
| Font size | 12pt at 96 DPI (16px) |
|
| 114 |
+
| Line spacing | 1.0 (16px line height) |
|
| 115 |
+
| Page width | 896px |
|
| 116 |
+
| Page height | 32–896px, rounded up to a multiple of 32px |
|
| 117 |
+
| Lines per page | Up to 56 |
|
| 118 |
+
| Margins | 0px on all sides |
|
| 119 |
+
| Colors | Black text on a white background |
|
| 120 |
+
| Rendering | Pillow default text antialiasing |
|
| 121 |
+
|
| 122 |
+
Whitespace, including paragraph breaks and tabs, is normalized to a single space. Lines wrap according to the font's actual pixel width; words wider than the page are split by character. Overflow continues onto subsequent pages without overlap. The final page is sized to its content, and empty documents produce one 896 × 32px page.
|
| 123 |
+
|
| 124 |
+
All generated pages are supplied in order as one document, producing one relevance score. Rendering is not limited to the first page.
|
| 125 |
+
|
| 126 |
+
### Sentence Transformers
|
| 127 |
+
|
| 128 |
+
Use a Sentence Transformers version supporting multimodal `CrossEncoder` models.
|
| 129 |
+
|
| 130 |
+
```bash
|
| 131 |
+
pip install -U sentence-transformers
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
The same call can score text, a single document image, and multiple page images. In the example below, `1.png`, `2.png`, and `3.png` are ordered pages of one document.
|
| 135 |
+
|
| 136 |
+
```python
|
| 137 |
+
import torch
|
| 138 |
+
from PIL import Image
|
| 139 |
+
from sentence_transformers import CrossEncoder
|
| 140 |
+
|
| 141 |
+
model = CrossEncoder(
|
| 142 |
+
"nlpai-lab/RenderRank-2B",
|
| 143 |
+
device="cuda",
|
| 144 |
+
max_length=32768,
|
| 145 |
+
model_kwargs={"dtype": torch.bfloat16},
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
query = "What is the capital of France?"
|
| 149 |
+
documents = [
|
| 150 |
+
[{"type": "text", "text": "Paris is the capital of France."}],
|
| 151 |
+
[{"type": "image", "image": Image.open("/path/to/document.png")}],
|
| 152 |
+
[
|
| 153 |
+
{"type": "image", "image": Image.open("/path/to/1.png")},
|
| 154 |
+
{"type": "image", "image": Image.open("/path/to/2.png")},
|
| 155 |
+
{"type": "image", "image": Image.open("/path/to/3.png")},
|
| 156 |
+
# Add further pages here in document order.
|
| 157 |
+
],
|
| 158 |
+
]
|
| 159 |
+
messages = [
|
| 160 |
+
[
|
| 161 |
+
{"role": "query", "content": [{"type": "text", "text": query}]},
|
| 162 |
+
{"role": "document", "content": document},
|
| 163 |
+
]
|
| 164 |
+
for document in documents
|
| 165 |
+
]
|
| 166 |
+
|
| 167 |
+
scores = model.predict(
|
| 168 |
+
messages,
|
| 169 |
+
prompt="Retrieve text relevant to the user's query.",
|
| 170 |
+
batch_size=1,
|
| 171 |
+
)
|
| 172 |
+
print(scores) # One score per document, not per page.
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
Larger scores indicate greater relevance. Scores are the difference between the yes and no logits, as used in our evaluation. Sentence Transformers requires one image content entry per page; `render` and `{"image": [page1, page2]}` are supported by the Transformers interface below, not this interface.
|
| 176 |
+
|
| 177 |
+
### Transformers
|
| 178 |
+
|
| 179 |
+
This repository provides a custom `AutoModel` entry point with a `process()` method. It supports all three document input modes.
|
| 180 |
+
|
| 181 |
+
**Requirements**
|
| 182 |
+
|
| 183 |
+
The Transformers interface was tested with the following versions:
|
| 184 |
+
|
| 185 |
+
```text
|
| 186 |
+
transformers==5.9.0
|
| 187 |
+
qwen-vl-utils==0.0.14
|
| 188 |
+
torch==2.11.0
|
| 189 |
+
torchvision==0.26.0
|
| 190 |
+
scipy
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
`qwen-vl-utils` handles image preparation in this interface; `scipy` is used for score normalization. NumPy and Pillow are installed through the dependencies above. Install PyTorch and torchvision builds matching your CUDA environment.
|
| 194 |
+
|
| 195 |
+
```python
|
| 196 |
+
import torch
|
| 197 |
+
from transformers import AutoModel
|
| 198 |
+
|
| 199 |
+
model = AutoModel.from_pretrained(
|
| 200 |
+
"nlpai-lab/RenderRank-2B",
|
| 201 |
+
trust_remote_code=True,
|
| 202 |
+
dtype=torch.bfloat16,
|
| 203 |
+
).to("cuda").eval()
|
| 204 |
+
|
| 205 |
+
inputs = {
|
| 206 |
+
"instruction": "Retrieve text relevant to the user's query.",
|
| 207 |
+
"query": {"text": "What is the capital of France?"},
|
| 208 |
+
"documents": [
|
| 209 |
+
# Score text directly.
|
| 210 |
+
{"text": "Paris is the capital of France."},
|
| 211 |
+
# Render text into document pages, then score the images.
|
| 212 |
+
{"render": "Paris is the capital of France."},
|
| 213 |
+
# Score an existing document image.
|
| 214 |
+
{"image": "/path/to/document.png"},
|
| 215 |
+
# Score multiple ordered pages as one document.
|
| 216 |
+
{"image": ["/path/to/page1.png", "/path/to/page2.png"]},
|
| 217 |
+
],
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
with torch.inference_mode():
|
| 221 |
+
scores = model.process(inputs)
|
| 222 |
+
|
| 223 |
+
ranked_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)
|
| 224 |
+
print(scores) # One score in [0, 1] per document, in input order.
|
| 225 |
+
print(ranked_indices)
|
| 226 |
+
```
|
Roboto-Regular.ttf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:797e35f7f5d6020a5c6ea13b42ecd668bcfb3bbc4baa0e74773527e5b6cb3174
|
| 3 |
+
size 305608
|
additional_chat_templates/reranker.jinja
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{%- set default_instruction = "Given a search query, retrieve relevant candidates that answer the query." -%}
|
| 2 |
+
{%- set ns = namespace(instruction="", found_instruction=false) -%}
|
| 3 |
+
{%- for message in messages -%}
|
| 4 |
+
{%- if message.role == "system" -%}
|
| 5 |
+
{%- if message.content is string -%}
|
| 6 |
+
{%- set ns.instruction = message.content -%}
|
| 7 |
+
{%- else -%}
|
| 8 |
+
{%- for content in message.content -%}
|
| 9 |
+
{%- if 'text' in content -%}
|
| 10 |
+
{%- set ns.instruction = ns.instruction + content.text -%}
|
| 11 |
+
{%- endif -%}
|
| 12 |
+
{%- endfor -%}
|
| 13 |
+
{%- endif -%}
|
| 14 |
+
{%- set ns.found_instruction = true -%}
|
| 15 |
+
{%- endif -%}
|
| 16 |
+
{%- endfor -%}
|
| 17 |
+
{%- if not ns.found_instruction -%}
|
| 18 |
+
{%- set ns.instruction = default_instruction -%}
|
| 19 |
+
{%- endif -%}
|
| 20 |
+
{%- set image_count = namespace(value=0) -%}
|
| 21 |
+
{%- set video_count = namespace(value=0) -%}
|
| 22 |
+
{%- macro render_multimodal(message) -%}
|
| 23 |
+
{%- if message.content is string -%}
|
| 24 |
+
{{- message.content -}}
|
| 25 |
+
{%- else -%}
|
| 26 |
+
{%- for content in message.content -%}
|
| 27 |
+
{%- if content.type == 'image' or 'image' in content or 'image_url' in content -%}
|
| 28 |
+
{%- set image_count.value = image_count.value + 1 -%}
|
| 29 |
+
{%- if add_vision_id %}Picture {{ image_count.value }}: {% endif -%}
|
| 30 |
+
<|vision_start|><|image_pad|><|vision_end|>
|
| 31 |
+
{%- elif content.type == 'video' or 'video' in content -%}
|
| 32 |
+
{%- set video_count.value = video_count.value + 1 -%}
|
| 33 |
+
{%- if add_vision_id %}Video {{ video_count.value }}: {% endif -%}
|
| 34 |
+
<|vision_start|><|video_pad|><|vision_end|>
|
| 35 |
+
{%- elif 'text' in content -%}
|
| 36 |
+
{{- content.text -}}
|
| 37 |
+
{%- endif -%}
|
| 38 |
+
{%- endfor -%}
|
| 39 |
+
{%- endif -%}
|
| 40 |
+
{%- endmacro -%}
|
| 41 |
+
{{- '<|im_start|>system\nJudge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be "yes" or "no".<|im_end|>\n<|im_start|>user\n<Instruct>: ' + ns.instruction + '<Query>:' -}}
|
| 42 |
+
{%- for message in messages if message.role == "query" -%}
|
| 43 |
+
{{- render_multimodal(message) -}}
|
| 44 |
+
{%- endfor -%}
|
| 45 |
+
{{- '\n<Document>:' -}}
|
| 46 |
+
{%- for message in messages if message.role == "document" -%}
|
| 47 |
+
{{- render_multimodal(message) -}}
|
| 48 |
+
{%- endfor -%}
|
| 49 |
+
{{- '<|im_end|>\n' -}}
|
| 50 |
+
{%- if add_generation_prompt -%}
|
| 51 |
+
{{- '<|im_start|>assistant\n' -}}
|
| 52 |
+
{%- endif -%}
|
assets/struc.pdf
ADDED
|
Binary file (99.3 kB). View file
|
|
|
assets/struc.png
ADDED
|
chat_template.jinja
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{%- if tools %}
|
| 2 |
+
{{- '<|im_start|>system\n' }}
|
| 3 |
+
{%- if messages[0].role == 'system' %}
|
| 4 |
+
{%- if messages[0].content is string %}
|
| 5 |
+
{{- messages[0].content }}
|
| 6 |
+
{%- else %}
|
| 7 |
+
{%- for content in messages[0].content %}
|
| 8 |
+
{%- if 'text' in content %}
|
| 9 |
+
{{- content.text }}
|
| 10 |
+
{%- endif %}
|
| 11 |
+
{%- endfor %}
|
| 12 |
+
{%- endif %}
|
| 13 |
+
{{- '\n\n' }}
|
| 14 |
+
{%- endif %}
|
| 15 |
+
{{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
|
| 16 |
+
{%- for tool in tools %}
|
| 17 |
+
{{- "\n" }}
|
| 18 |
+
{{- tool | tojson }}
|
| 19 |
+
{%- endfor %}
|
| 20 |
+
{{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
|
| 21 |
+
{%- else %}
|
| 22 |
+
{%- if messages[0].role == 'system' %}
|
| 23 |
+
{{- '<|im_start|>system\n' }}
|
| 24 |
+
{%- if messages[0].content is string %}
|
| 25 |
+
{{- messages[0].content }}
|
| 26 |
+
{%- else %}
|
| 27 |
+
{%- for content in messages[0].content %}
|
| 28 |
+
{%- if 'text' in content %}
|
| 29 |
+
{{- content.text }}
|
| 30 |
+
{%- endif %}
|
| 31 |
+
{%- endfor %}
|
| 32 |
+
{%- endif %}
|
| 33 |
+
{{- '<|im_end|>\n' }}
|
| 34 |
+
{%- endif %}
|
| 35 |
+
{%- endif %}
|
| 36 |
+
{%- set image_count = namespace(value=0) %}
|
| 37 |
+
{%- set video_count = namespace(value=0) %}
|
| 38 |
+
{%- for message in messages %}
|
| 39 |
+
{%- if message.role == "user" %}
|
| 40 |
+
{{- '<|im_start|>' + message.role + '\n' }}
|
| 41 |
+
{%- if message.content is string %}
|
| 42 |
+
{{- message.content }}
|
| 43 |
+
{%- else %}
|
| 44 |
+
{%- for content in message.content %}
|
| 45 |
+
{%- if content.type == 'image' or 'image' in content or 'image_url' in content %}
|
| 46 |
+
{%- set image_count.value = image_count.value + 1 %}
|
| 47 |
+
{%- if add_vision_id %}Picture {{ image_count.value }}: {% endif -%}
|
| 48 |
+
<|vision_start|><|image_pad|><|vision_end|>
|
| 49 |
+
{%- elif content.type == 'video' or 'video' in content %}
|
| 50 |
+
{%- set video_count.value = video_count.value + 1 %}
|
| 51 |
+
{%- if add_vision_id %}Video {{ video_count.value }}: {% endif -%}
|
| 52 |
+
<|vision_start|><|video_pad|><|vision_end|>
|
| 53 |
+
{%- elif 'text' in content %}
|
| 54 |
+
{{- content.text }}
|
| 55 |
+
{%- endif %}
|
| 56 |
+
{%- endfor %}
|
| 57 |
+
{%- endif %}
|
| 58 |
+
{{- '<|im_end|>\n' }}
|
| 59 |
+
{%- elif message.role == "assistant" %}
|
| 60 |
+
{{- '<|im_start|>' + message.role + '\n' }}
|
| 61 |
+
{%- if message.content is string %}
|
| 62 |
+
{{- message.content }}
|
| 63 |
+
{%- else %}
|
| 64 |
+
{%- for content_item in message.content %}
|
| 65 |
+
{%- if 'text' in content_item %}
|
| 66 |
+
{{- content_item.text }}
|
| 67 |
+
{%- endif %}
|
| 68 |
+
{%- endfor %}
|
| 69 |
+
{%- endif %}
|
| 70 |
+
{%- if message.tool_calls %}
|
| 71 |
+
{%- for tool_call in message.tool_calls %}
|
| 72 |
+
{%- if (loop.first and message.content) or (not loop.first) %}
|
| 73 |
+
{{- '\n' }}
|
| 74 |
+
{%- endif %}
|
| 75 |
+
{%- if tool_call.function %}
|
| 76 |
+
{%- set tool_call = tool_call.function %}
|
| 77 |
+
{%- endif %}
|
| 78 |
+
{{- '<tool_call>\n{"name": "' }}
|
| 79 |
+
{{- tool_call.name }}
|
| 80 |
+
{{- '", "arguments": ' }}
|
| 81 |
+
{%- if tool_call.arguments is string %}
|
| 82 |
+
{{- tool_call.arguments }}
|
| 83 |
+
{%- else %}
|
| 84 |
+
{{- tool_call.arguments | tojson }}
|
| 85 |
+
{%- endif %}
|
| 86 |
+
{{- '}\n</tool_call>' }}
|
| 87 |
+
{%- endfor %}
|
| 88 |
+
{%- endif %}
|
| 89 |
+
{{- '<|im_end|>\n' }}
|
| 90 |
+
{%- elif message.role == "tool" %}
|
| 91 |
+
{%- if loop.first or (messages[loop.index0 - 1].role != "tool") %}
|
| 92 |
+
{{- '<|im_start|>user' }}
|
| 93 |
+
{%- endif %}
|
| 94 |
+
{{- '\n<tool_response>\n' }}
|
| 95 |
+
{%- if message.content is string %}
|
| 96 |
+
{{- message.content }}
|
| 97 |
+
{%- else %}
|
| 98 |
+
{%- for content in message.content %}
|
| 99 |
+
{%- if content.type == 'image' or 'image' in content or 'image_url' in content %}
|
| 100 |
+
{%- set image_count.value = image_count.value + 1 %}
|
| 101 |
+
{%- if add_vision_id %}Picture {{ image_count.value }}: {% endif -%}
|
| 102 |
+
<|vision_start|><|image_pad|><|vision_end|>
|
| 103 |
+
{%- elif content.type == 'video' or 'video' in content %}
|
| 104 |
+
{%- set video_count.value = video_count.value + 1 %}
|
| 105 |
+
{%- if add_vision_id %}Video {{ video_count.value }}: {% endif -%}
|
| 106 |
+
<|vision_start|><|video_pad|><|vision_end|>
|
| 107 |
+
{%- elif 'text' in content %}
|
| 108 |
+
{{- content.text }}
|
| 109 |
+
{%- endif %}
|
| 110 |
+
{%- endfor %}
|
| 111 |
+
{%- endif %}
|
| 112 |
+
{{- '\n</tool_response>' }}
|
| 113 |
+
{%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
|
| 114 |
+
{{- '<|im_end|>\n' }}
|
| 115 |
+
{%- endif %}
|
| 116 |
+
{%- endif %}
|
| 117 |
+
{%- endfor %}
|
| 118 |
+
{%- if add_generation_prompt %}
|
| 119 |
+
{{- '<|im_start|>assistant\n' }}
|
| 120 |
+
{%- endif %}
|
config.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"auto_map": {
|
| 3 |
+
"AutoModel": "modeling_renderrank.RenderRank"
|
| 4 |
+
},
|
| 5 |
+
"architectures": [
|
| 6 |
+
"Qwen3VLForConditionalGeneration"
|
| 7 |
+
],
|
| 8 |
+
"dtype": "bfloat16",
|
| 9 |
+
"hidden_size": 2048,
|
| 10 |
+
"image_token_id": 151655,
|
| 11 |
+
"model_type": "qwen3_vl",
|
| 12 |
+
"pad_token_id": 151643,
|
| 13 |
+
"text_config": {
|
| 14 |
+
"attention_bias": false,
|
| 15 |
+
"attention_dropout": 0.0,
|
| 16 |
+
"bos_token_id": 151643,
|
| 17 |
+
"dtype": "bfloat16",
|
| 18 |
+
"eos_token_id": 151645,
|
| 19 |
+
"head_dim": 128,
|
| 20 |
+
"hidden_act": "silu",
|
| 21 |
+
"hidden_size": 2048,
|
| 22 |
+
"initializer_range": 0.02,
|
| 23 |
+
"intermediate_size": 6144,
|
| 24 |
+
"max_position_embeddings": 262144,
|
| 25 |
+
"model_type": "qwen3_vl_text",
|
| 26 |
+
"num_attention_heads": 16,
|
| 27 |
+
"num_hidden_layers": 28,
|
| 28 |
+
"num_key_value_heads": 8,
|
| 29 |
+
"pad_token_id": 151643,
|
| 30 |
+
"rms_norm_eps": 1e-06,
|
| 31 |
+
"rope_parameters": {
|
| 32 |
+
"mrope_interleaved": true,
|
| 33 |
+
"mrope_section": [
|
| 34 |
+
24,
|
| 35 |
+
20,
|
| 36 |
+
20
|
| 37 |
+
],
|
| 38 |
+
"rope_theta": 5000000,
|
| 39 |
+
"rope_type": "default"
|
| 40 |
+
},
|
| 41 |
+
"tie_word_embeddings": true,
|
| 42 |
+
"use_cache": false,
|
| 43 |
+
"vocab_size": 151936
|
| 44 |
+
},
|
| 45 |
+
"tie_word_embeddings": true,
|
| 46 |
+
"transformers_version": "5.14.1",
|
| 47 |
+
"video_token_id": 151656,
|
| 48 |
+
"vision_config": {
|
| 49 |
+
"deepstack_visual_indexes": [
|
| 50 |
+
5,
|
| 51 |
+
11,
|
| 52 |
+
17
|
| 53 |
+
],
|
| 54 |
+
"depth": 24,
|
| 55 |
+
"dtype": "bfloat16",
|
| 56 |
+
"hidden_act": "gelu_pytorch_tanh",
|
| 57 |
+
"hidden_size": 1024,
|
| 58 |
+
"in_channels": 3,
|
| 59 |
+
"initializer_range": 0.02,
|
| 60 |
+
"intermediate_size": 4096,
|
| 61 |
+
"model_type": "qwen3_vl_vision",
|
| 62 |
+
"num_heads": 16,
|
| 63 |
+
"num_position_embeddings": 2304,
|
| 64 |
+
"out_hidden_size": 2048,
|
| 65 |
+
"pad_token_id": 151643,
|
| 66 |
+
"patch_size": 16,
|
| 67 |
+
"spatial_merge_size": 2,
|
| 68 |
+
"temporal_patch_size": 2
|
| 69 |
+
},
|
| 70 |
+
"vision_end_token_id": 151653,
|
| 71 |
+
"vision_start_token_id": 151652
|
| 72 |
+
}
|
config_sentence_transformers.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"__version__": {
|
| 3 |
+
"pytorch": "2.11.0+cu129",
|
| 4 |
+
"sentence_transformers": "5.7.0.dev0",
|
| 5 |
+
"transformers": "5.14.1"
|
| 6 |
+
},
|
| 7 |
+
"activation_fn": "torch.nn.modules.linear.Identity",
|
| 8 |
+
"default_prompt_name": "query",
|
| 9 |
+
"model_type": "CrossEncoder",
|
| 10 |
+
"prompts": {
|
| 11 |
+
"query": "Retrieve text relevant to the user's query."
|
| 12 |
+
}
|
| 13 |
+
}
|
generation_config.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"bos_token_id": 151643,
|
| 3 |
+
"do_sample": true,
|
| 4 |
+
"eos_token_id": [
|
| 5 |
+
151645,
|
| 6 |
+
151643
|
| 7 |
+
],
|
| 8 |
+
"pad_token_id": 151643,
|
| 9 |
+
"temperature": 0.7,
|
| 10 |
+
"top_k": 20,
|
| 11 |
+
"top_p": 0.8,
|
| 12 |
+
"transformers_version": "5.14.1"
|
| 13 |
+
}
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0add172e901d1f99bd2cd42a1a3e38b2d2016788862386961456019dd4346fff
|
| 3 |
+
size 4255140312
|
modeling_renderrank.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AutoModel entry point: render preprocessing plus Qwen's existing scorer."""
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
from transformers import AutoProcessor, Qwen3VLForConditionalGeneration
|
| 5 |
+
from transformers.utils.hub import cached_file
|
| 6 |
+
|
| 7 |
+
from .qwen3_vl_reranker import Qwen3VLReranker
|
| 8 |
+
from .rendering import DocumentImageConfig, DocumentImageRenderer, prepare_render_inputs
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class _QwenScorer(Qwen3VLReranker):
|
| 12 |
+
def tokenize(self, pairs, **kwargs):
|
| 13 |
+
inputs = super().tokenize(pairs, **kwargs)
|
| 14 |
+
# Transformers 5 adds modality IDs. Align them with the upstream's
|
| 15 |
+
# finalized input IDs, then convert every model input to tensors.
|
| 16 |
+
if "mm_token_type_ids" in inputs:
|
| 17 |
+
inputs["mm_token_type_ids"] = self.processor.create_mm_token_type_ids(
|
| 18 |
+
inputs["input_ids"].tolist()
|
| 19 |
+
)
|
| 20 |
+
return inputs.convert_to_tensors("pt")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class RenderRank(Qwen3VLForConditionalGeneration):
|
| 24 |
+
"""Keep the checkpoint architecture; expose the official process API."""
|
| 25 |
+
|
| 26 |
+
@classmethod
|
| 27 |
+
def from_pretrained(cls, pretrained_model_name_or_path, *args, **kwargs):
|
| 28 |
+
loaded = super().from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
|
| 29 |
+
model = loaded[0] if isinstance(loaded, tuple) else loaded
|
| 30 |
+
asset_kwargs = {
|
| 31 |
+
key: kwargs[key] for key in
|
| 32 |
+
("cache_dir", "revision", "token", "local_files_only", "subfolder", "force_download")
|
| 33 |
+
if key in kwargs
|
| 34 |
+
}
|
| 35 |
+
model.processor = AutoProcessor.from_pretrained(
|
| 36 |
+
pretrained_model_name_or_path, padding_side="left", **asset_kwargs
|
| 37 |
+
)
|
| 38 |
+
model._render_font_path = cached_file(
|
| 39 |
+
pretrained_model_name_or_path, "Roboto-Regular.ttf", **asset_kwargs
|
| 40 |
+
)
|
| 41 |
+
return loaded
|
| 42 |
+
|
| 43 |
+
def process(self, inputs):
|
| 44 |
+
"""Return scores in document order; render is only an input conversion."""
|
| 45 |
+
if not isinstance(inputs, dict):
|
| 46 |
+
raise TypeError("inputs must be a dictionary")
|
| 47 |
+
renderer = DocumentImageRenderer(DocumentImageConfig(Path(self._render_font_path)))
|
| 48 |
+
prepared = prepare_render_inputs(inputs, renderer)
|
| 49 |
+
|
| 50 |
+
# Bind the official wrapper to the already-loaded model (no second load).
|
| 51 |
+
scorer = _QwenScorer.__new__(_QwenScorer)
|
| 52 |
+
scorer.model = self.model
|
| 53 |
+
scorer.processor = self.processor
|
| 54 |
+
scorer.device = self.device
|
| 55 |
+
scorer.max_length = 32768
|
| 56 |
+
scorer.min_pixels = self.processor.image_processor.size["shortest_edge"]
|
| 57 |
+
scorer.max_pixels = self.processor.image_processor.size["longest_edge"]
|
| 58 |
+
scorer.total_pixels = 10 * 768 * 32 * 32
|
| 59 |
+
scorer.fps = 1
|
| 60 |
+
scorer.num_frames = 64
|
| 61 |
+
scorer.max_frames = 64
|
| 62 |
+
scorer.default_instruction = "Retrieve text relevant to the user's query."
|
| 63 |
+
vocab = self.processor.tokenizer.get_vocab()
|
| 64 |
+
scorer.score_linear = scorer.get_binary_linear(self, vocab["yes"], vocab["no"])
|
| 65 |
+
scorer.score_linear = scorer.score_linear.to(device=self.device, dtype=self.dtype).eval()
|
| 66 |
+
return scorer.process(prepared)
|
modules.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"idx": 0,
|
| 4 |
+
"name": "0",
|
| 5 |
+
"path": "",
|
| 6 |
+
"type": "sentence_transformers.base.modules.transformer.Transformer"
|
| 7 |
+
},
|
| 8 |
+
{
|
| 9 |
+
"idx": 1,
|
| 10 |
+
"name": "1",
|
| 11 |
+
"path": "1_LogitScore",
|
| 12 |
+
"type": "sentence_transformers.cross_encoder.modules.logit_score.LogitScore"
|
| 13 |
+
}
|
| 14 |
+
]
|
processor_config.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"image_processor": {
|
| 3 |
+
"do_convert_rgb": true,
|
| 4 |
+
"do_normalize": true,
|
| 5 |
+
"do_rescale": true,
|
| 6 |
+
"do_resize": true,
|
| 7 |
+
"image_mean": [
|
| 8 |
+
0.5,
|
| 9 |
+
0.5,
|
| 10 |
+
0.5
|
| 11 |
+
],
|
| 12 |
+
"image_processor_type": "Qwen2VLImageProcessor",
|
| 13 |
+
"image_std": [
|
| 14 |
+
0.5,
|
| 15 |
+
0.5,
|
| 16 |
+
0.5
|
| 17 |
+
],
|
| 18 |
+
"merge_size": 2,
|
| 19 |
+
"patch_size": 16,
|
| 20 |
+
"resample": 3,
|
| 21 |
+
"rescale_factor": 0.00392156862745098,
|
| 22 |
+
"size": {
|
| 23 |
+
"longest_edge": 802816,
|
| 24 |
+
"shortest_edge": 4096
|
| 25 |
+
},
|
| 26 |
+
"temporal_patch_size": 2
|
| 27 |
+
},
|
| 28 |
+
"processor_class": "Qwen3VLProcessor",
|
| 29 |
+
"video_processor": {
|
| 30 |
+
"data_format": "channels_first",
|
| 31 |
+
"default_to_square": true,
|
| 32 |
+
"do_convert_rgb": true,
|
| 33 |
+
"do_normalize": true,
|
| 34 |
+
"do_rescale": true,
|
| 35 |
+
"do_resize": true,
|
| 36 |
+
"do_sample_frames": true,
|
| 37 |
+
"fps": 2,
|
| 38 |
+
"image_mean": [
|
| 39 |
+
0.5,
|
| 40 |
+
0.5,
|
| 41 |
+
0.5
|
| 42 |
+
],
|
| 43 |
+
"image_std": [
|
| 44 |
+
0.5,
|
| 45 |
+
0.5,
|
| 46 |
+
0.5
|
| 47 |
+
],
|
| 48 |
+
"max_frames": 768,
|
| 49 |
+
"merge_size": 2,
|
| 50 |
+
"min_frames": 4,
|
| 51 |
+
"patch_size": 16,
|
| 52 |
+
"resample": 3,
|
| 53 |
+
"rescale_factor": 0.00392156862745098,
|
| 54 |
+
"return_metadata": false,
|
| 55 |
+
"size": {
|
| 56 |
+
"longest_edge": 25165824,
|
| 57 |
+
"shortest_edge": 4096
|
| 58 |
+
},
|
| 59 |
+
"temporal_patch_size": 2,
|
| 60 |
+
"video_processor_type": "Qwen3VLVideoProcessor"
|
| 61 |
+
}
|
| 62 |
+
}
|
qwen3_vl_reranker.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Based on Qwen/Qwen3-VL-Reranker-2B, Hugging Face revision 4bd860a.
|
| 2 |
+
# Apache-2.0. Changes: ordered multi-image input and frame-helper call repair.
|
| 3 |
+
import torch
|
| 4 |
+
import numpy as np
|
| 5 |
+
import logging
|
| 6 |
+
|
| 7 |
+
from PIL import Image
|
| 8 |
+
from scipy import special
|
| 9 |
+
from typing import List
|
| 10 |
+
from qwen_vl_utils import process_vision_info
|
| 11 |
+
from transformers import Qwen3VLForConditionalGeneration, AutoProcessor
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
MAX_LENGTH = 8192
|
| 16 |
+
IMAGE_BASE_FACTOR = 16
|
| 17 |
+
IMAGE_FACTOR = IMAGE_BASE_FACTOR * 2
|
| 18 |
+
MIN_PIXELS = 4 * IMAGE_FACTOR * IMAGE_FACTOR # 4 tokens
|
| 19 |
+
MAX_PIXELS = 1280 * IMAGE_FACTOR * IMAGE_FACTOR # 1280 tokens
|
| 20 |
+
MAX_RATIO = 200
|
| 21 |
+
|
| 22 |
+
FRAME_FACTOR = 2
|
| 23 |
+
FPS = 1
|
| 24 |
+
MIN_FRAMES = 2
|
| 25 |
+
MAX_FRAMES = 64
|
| 26 |
+
MIN_TOTAL_PIXELS = 1 * FRAME_FACTOR * MIN_PIXELS # 1 frames
|
| 27 |
+
MAX_TOTAL_PIXELS = 4 * FRAME_FACTOR * MAX_PIXELS # 4 frames
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def sample_frames(frames, num_segments, max_segments):
|
| 31 |
+
duration = len(frames)
|
| 32 |
+
frame_id_array = np.linspace(0, duration - 1, num_segments, dtype=int)
|
| 33 |
+
frame_id_list = frame_id_array.tolist()
|
| 34 |
+
last_frame_id = frame_id_list[-1]
|
| 35 |
+
|
| 36 |
+
sampled_frames = []
|
| 37 |
+
for frame_idx in frame_id_list:
|
| 38 |
+
try:
|
| 39 |
+
single_frame_path = frames[frame_idx]
|
| 40 |
+
except:
|
| 41 |
+
break
|
| 42 |
+
sampled_frames.append(single_frame_path)
|
| 43 |
+
# Pad with last frame if total frames less than num_segments
|
| 44 |
+
while len(sampled_frames) < num_segments:
|
| 45 |
+
sampled_frames.append(frames[last_frame_id])
|
| 46 |
+
return sampled_frames[:max_segments]
|
| 47 |
+
|
| 48 |
+
class Qwen3VLReranker():
|
| 49 |
+
def __init__(
|
| 50 |
+
self,
|
| 51 |
+
model_name_or_path: str,
|
| 52 |
+
max_length: int = MAX_LENGTH,
|
| 53 |
+
min_pixels: int = MIN_PIXELS,
|
| 54 |
+
max_pixels: int = MAX_PIXELS,
|
| 55 |
+
total_pixels: int = MAX_TOTAL_PIXELS,
|
| 56 |
+
fps: float = FPS,
|
| 57 |
+
num_frames: int = MAX_FRAMES,
|
| 58 |
+
max_frames: int = MAX_FRAMES,
|
| 59 |
+
default_instruction: str = "Given a search query, retrieve relevant candidates that answer the query.",
|
| 60 |
+
**kwargs,
|
| 61 |
+
):
|
| 62 |
+
|
| 63 |
+
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 64 |
+
|
| 65 |
+
self.max_length = max_length
|
| 66 |
+
self.min_pixels = min_pixels
|
| 67 |
+
self.max_pixels = max_pixels
|
| 68 |
+
self.total_pixels = total_pixels
|
| 69 |
+
self.fps = fps
|
| 70 |
+
self.num_frames = num_frames
|
| 71 |
+
self.max_frames = max_frames
|
| 72 |
+
|
| 73 |
+
self.default_instruction = default_instruction
|
| 74 |
+
|
| 75 |
+
lm = Qwen3VLForConditionalGeneration.from_pretrained(
|
| 76 |
+
model_name_or_path,
|
| 77 |
+
trust_remote_code=True, **kwargs
|
| 78 |
+
).to(self.device)
|
| 79 |
+
|
| 80 |
+
self.model = lm.model
|
| 81 |
+
self.processor = AutoProcessor.from_pretrained(
|
| 82 |
+
model_name_or_path, trust_remote_code=True,
|
| 83 |
+
padding_side='left'
|
| 84 |
+
)
|
| 85 |
+
self.model.eval()
|
| 86 |
+
|
| 87 |
+
token_true_id = self.processor.tokenizer.get_vocab()["yes"]
|
| 88 |
+
token_false_id = self.processor.tokenizer.get_vocab()["no"]
|
| 89 |
+
self.score_linear = self.get_binary_linear(lm, token_true_id, token_false_id)
|
| 90 |
+
self.score_linear.eval()
|
| 91 |
+
self.score_linear.to(self.device).to(self.model.dtype)
|
| 92 |
+
|
| 93 |
+
def get_binary_linear(self, model, token_yes, token_no):
|
| 94 |
+
|
| 95 |
+
lm_head_weights = model.lm_head.weight.data
|
| 96 |
+
|
| 97 |
+
weight_yes = lm_head_weights[token_yes]
|
| 98 |
+
weight_no = lm_head_weights[token_no]
|
| 99 |
+
|
| 100 |
+
D = weight_yes.size()[0]
|
| 101 |
+
linear_layer = torch.nn.Linear(D, 1, bias=False)
|
| 102 |
+
with torch.no_grad():
|
| 103 |
+
linear_layer.weight[0] = weight_yes - weight_no
|
| 104 |
+
return linear_layer
|
| 105 |
+
|
| 106 |
+
@torch.no_grad()
|
| 107 |
+
def compute_scores(self, inputs):
|
| 108 |
+
batch_scores = self.model(**inputs).last_hidden_state[:, -1]
|
| 109 |
+
scores = self.score_linear(batch_scores)
|
| 110 |
+
scores = torch.sigmoid(scores).squeeze(-1).cpu().detach().tolist()
|
| 111 |
+
return scores
|
| 112 |
+
|
| 113 |
+
def truncate_tokens_optimized(
|
| 114 |
+
self,
|
| 115 |
+
tokens: List[str],
|
| 116 |
+
max_length: int,
|
| 117 |
+
special_tokens: List[str]
|
| 118 |
+
) -> List[str]:
|
| 119 |
+
if len(tokens) <= max_length:
|
| 120 |
+
return tokens
|
| 121 |
+
|
| 122 |
+
special_tokens_set = set(special_tokens)
|
| 123 |
+
|
| 124 |
+
# Calculate budget: how many non-special tokens we can keep
|
| 125 |
+
num_special = sum(1 for token in tokens if token in special_tokens_set)
|
| 126 |
+
num_non_special_to_keep = max_length - num_special
|
| 127 |
+
|
| 128 |
+
# Build final list according to budget
|
| 129 |
+
final_tokens = []
|
| 130 |
+
non_special_kept_count = 0
|
| 131 |
+
for token in tokens:
|
| 132 |
+
if token in special_tokens_set:
|
| 133 |
+
final_tokens.append(token)
|
| 134 |
+
elif non_special_kept_count < num_non_special_to_keep:
|
| 135 |
+
final_tokens.append(token)
|
| 136 |
+
non_special_kept_count += 1
|
| 137 |
+
|
| 138 |
+
return final_tokens
|
| 139 |
+
|
| 140 |
+
def tokenize(self, pairs: list, **kwargs):
|
| 141 |
+
max_length = self.max_length
|
| 142 |
+
text = self.processor.apply_chat_template(pairs, tokenize=False, add_generation_prompt=True)
|
| 143 |
+
try:
|
| 144 |
+
images, videos, video_kwargs = process_vision_info(
|
| 145 |
+
pairs, image_patch_size=16,
|
| 146 |
+
return_video_kwargs=True,
|
| 147 |
+
return_video_metadata=True
|
| 148 |
+
)
|
| 149 |
+
except Exception as e:
|
| 150 |
+
logger.error(f"Error in processing vision info: {e}")
|
| 151 |
+
images = None
|
| 152 |
+
videos = None
|
| 153 |
+
video_kwargs = {'do_sample_frames': False}
|
| 154 |
+
text = self.processor.apply_chat_template(
|
| 155 |
+
[{'role': 'user', 'content': [{'type': 'text', 'text': 'NULL'}]}],
|
| 156 |
+
add_generation_prompt=True, tokenize=False
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
if videos is not None:
|
| 160 |
+
videos, video_metadatas = zip(*videos)
|
| 161 |
+
videos, video_metadatas = list(videos), list(video_metadatas)
|
| 162 |
+
else:
|
| 163 |
+
video_metadatas = None
|
| 164 |
+
inputs = self.processor(
|
| 165 |
+
text=text,
|
| 166 |
+
images=images,
|
| 167 |
+
videos=videos,
|
| 168 |
+
video_metadata=video_metadatas,
|
| 169 |
+
truncation=False,
|
| 170 |
+
padding=False,
|
| 171 |
+
do_resize=False,
|
| 172 |
+
**video_kwargs
|
| 173 |
+
)
|
| 174 |
+
for i, ele in enumerate(inputs['input_ids']):
|
| 175 |
+
inputs['input_ids'][i] = self.truncate_tokens_optimized(
|
| 176 |
+
inputs['input_ids'][i][:-5], max_length,
|
| 177 |
+
self.processor.tokenizer.all_special_ids
|
| 178 |
+
) + inputs['input_ids'][i][-5:]
|
| 179 |
+
temp_inputs = self.processor.tokenizer.pad(
|
| 180 |
+
{'input_ids': inputs['input_ids']}, padding=True,
|
| 181 |
+
return_tensors="pt", max_length=self.max_length
|
| 182 |
+
)
|
| 183 |
+
for key in temp_inputs:
|
| 184 |
+
inputs[key] = temp_inputs[key]
|
| 185 |
+
return inputs
|
| 186 |
+
|
| 187 |
+
def format_mm_content(
|
| 188 |
+
self,
|
| 189 |
+
text, image, video,
|
| 190 |
+
prefix='Query:',
|
| 191 |
+
fps=None, max_frames=None,
|
| 192 |
+
):
|
| 193 |
+
content = []
|
| 194 |
+
|
| 195 |
+
content.append({'type': 'text', 'text': prefix})
|
| 196 |
+
if not text and not image and not video:
|
| 197 |
+
content.append({'type': 'text', 'text': "NULL"})
|
| 198 |
+
return content
|
| 199 |
+
|
| 200 |
+
if video:
|
| 201 |
+
video_content = None
|
| 202 |
+
video_kwargs = { 'total_pixels': self.total_pixels }
|
| 203 |
+
if isinstance(video, list):
|
| 204 |
+
video_content = video
|
| 205 |
+
if self.num_frames is not None or self.max_frames is not None:
|
| 206 |
+
video_content = sample_frames(video_content, self.num_frames or len(video_content), self.max_frames or len(video_content))
|
| 207 |
+
video_content = [
|
| 208 |
+
('file://' + ele if isinstance(ele, str) else ele)
|
| 209 |
+
for ele in video_content
|
| 210 |
+
]
|
| 211 |
+
elif isinstance(video, str):
|
| 212 |
+
video_content = video if video.startswith(('http://', 'https://')) else 'file://' + video
|
| 213 |
+
video_kwargs = {'fps': fps or self.fps, 'max_frames': max_frames or self.max_frames,}
|
| 214 |
+
else:
|
| 215 |
+
raise TypeError(f"Unrecognized video type: {type(video)}")
|
| 216 |
+
|
| 217 |
+
if video_content:
|
| 218 |
+
content.append({
|
| 219 |
+
'type': 'video', 'video': video_content,
|
| 220 |
+
**video_kwargs
|
| 221 |
+
})
|
| 222 |
+
|
| 223 |
+
# RenderRank: one document may contain multiple ordered page images.
|
| 224 |
+
for image in (image if isinstance(image, list) else [image] if image is not None else []):
|
| 225 |
+
image_content = None
|
| 226 |
+
if isinstance(image, Image.Image):
|
| 227 |
+
image_content = image
|
| 228 |
+
elif isinstance(image, str):
|
| 229 |
+
image_content = image if image.startswith(('http', 'oss')) else 'file://' + image
|
| 230 |
+
else:
|
| 231 |
+
raise TypeError(f"Unrecognized image type: {type(image)}")
|
| 232 |
+
|
| 233 |
+
if image_content:
|
| 234 |
+
content.append({
|
| 235 |
+
'type': 'image', 'image': image_content,
|
| 236 |
+
"min_pixels": self.min_pixels,
|
| 237 |
+
"max_pixels": self.max_pixels
|
| 238 |
+
})
|
| 239 |
+
|
| 240 |
+
if text:
|
| 241 |
+
content.append({'type': 'text', 'text': text})
|
| 242 |
+
return content
|
| 243 |
+
|
| 244 |
+
def format_mm_instruction(
|
| 245 |
+
self,
|
| 246 |
+
query_text, query_image, query_video,
|
| 247 |
+
doc_text, doc_image, doc_video,
|
| 248 |
+
instruction=None,
|
| 249 |
+
fps=None, max_frames=None
|
| 250 |
+
):
|
| 251 |
+
inputs = []
|
| 252 |
+
inputs.append({
|
| 253 |
+
"role": "system",
|
| 254 |
+
"content": [{
|
| 255 |
+
"type": "text",
|
| 256 |
+
"text": "Judge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be \"yes\" or \"no\"."
|
| 257 |
+
}
|
| 258 |
+
]
|
| 259 |
+
})
|
| 260 |
+
if isinstance(query_text, tuple):
|
| 261 |
+
instruct, query_text = query_text
|
| 262 |
+
else:
|
| 263 |
+
instruct = instruction
|
| 264 |
+
contents = []
|
| 265 |
+
contents.append({
|
| 266 |
+
"type": "text",
|
| 267 |
+
"text": '<Instruct>: ' + instruct
|
| 268 |
+
})
|
| 269 |
+
query_content = self.format_mm_content(
|
| 270 |
+
query_text, query_image, query_video, prefix='<Query>:',
|
| 271 |
+
fps=fps, max_frames=max_frames
|
| 272 |
+
)
|
| 273 |
+
contents.extend(query_content)
|
| 274 |
+
doc_content = self.format_mm_content(
|
| 275 |
+
doc_text, doc_image, doc_video, prefix='\n<Document>:',
|
| 276 |
+
fps=fps, max_frames=max_frames
|
| 277 |
+
)
|
| 278 |
+
contents.extend(doc_content)
|
| 279 |
+
inputs.append({
|
| 280 |
+
"role": "user",
|
| 281 |
+
"content": contents
|
| 282 |
+
})
|
| 283 |
+
return inputs
|
| 284 |
+
|
| 285 |
+
def process(
|
| 286 |
+
self,
|
| 287 |
+
inputs,
|
| 288 |
+
) -> list[torch.Tensor]:
|
| 289 |
+
instruction = inputs.get('instruction', self.default_instruction)
|
| 290 |
+
|
| 291 |
+
query = inputs.get("query", {})
|
| 292 |
+
documents = inputs.get("documents", [])
|
| 293 |
+
if not query or not documents:
|
| 294 |
+
return []
|
| 295 |
+
|
| 296 |
+
pairs = [self.format_mm_instruction(
|
| 297 |
+
query.get('text', None),
|
| 298 |
+
query.get('image', None),
|
| 299 |
+
query.get('video', None),
|
| 300 |
+
document.get('text', None),
|
| 301 |
+
document.get('image', None),
|
| 302 |
+
document.get('video', None),
|
| 303 |
+
instruction=instruction,
|
| 304 |
+
fps=inputs.get('fps', self.fps),
|
| 305 |
+
max_frames=inputs.get('max_frames', self.max_frames)
|
| 306 |
+
) for document in documents]
|
| 307 |
+
|
| 308 |
+
final_scores = []
|
| 309 |
+
for pair in pairs:
|
| 310 |
+
inputs = self.tokenize([pair])
|
| 311 |
+
inputs = inputs.to(self.model.device)
|
| 312 |
+
scores = self.compute_scores(inputs)
|
| 313 |
+
final_scores.extend(scores)
|
| 314 |
+
return final_scores
|
rendering.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Existing evaluation renderer, packaged without evaluation/cache dependencies."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import math
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any, Sequence
|
| 8 |
+
|
| 9 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 10 |
+
|
| 11 |
+
@dataclass(frozen=True)
|
| 12 |
+
class DocumentImageConfig:
|
| 13 |
+
"""Exact document rendering parameters used by the vision evaluation."""
|
| 14 |
+
|
| 15 |
+
font_path: Path
|
| 16 |
+
font_index: int = 0
|
| 17 |
+
font_family: str = "Roboto"
|
| 18 |
+
font_style: str = "Regular"
|
| 19 |
+
font_size_px: int = 16
|
| 20 |
+
dpi: int = 96
|
| 21 |
+
line_height_px: int | None = None
|
| 22 |
+
width_px: int = 896
|
| 23 |
+
min_height_px: int = 32
|
| 24 |
+
height_step_px: int = 32
|
| 25 |
+
max_height_px: int = 896
|
| 26 |
+
max_lines_per_page: int | None = None
|
| 27 |
+
|
| 28 |
+
def __post_init__(self) -> None:
|
| 29 |
+
object.__setattr__(self, "font_path", Path(self.font_path))
|
| 30 |
+
line_height_px = self.font_size_px if self.line_height_px is None else self.line_height_px
|
| 31 |
+
object.__setattr__(self, "line_height_px", line_height_px)
|
| 32 |
+
max_lines_per_page = (
|
| 33 |
+
min(56, self.max_height_px // line_height_px)
|
| 34 |
+
if self.max_lines_per_page is None
|
| 35 |
+
else self.max_lines_per_page
|
| 36 |
+
)
|
| 37 |
+
object.__setattr__(self, "max_lines_per_page", max_lines_per_page)
|
| 38 |
+
numeric_values = {
|
| 39 |
+
"font_index": self.font_index,
|
| 40 |
+
"font_size_px": self.font_size_px,
|
| 41 |
+
"dpi": self.dpi,
|
| 42 |
+
"line_height_px": line_height_px,
|
| 43 |
+
"width_px": self.width_px,
|
| 44 |
+
"min_height_px": self.min_height_px,
|
| 45 |
+
"height_step_px": self.height_step_px,
|
| 46 |
+
"max_height_px": self.max_height_px,
|
| 47 |
+
"max_lines_per_page": max_lines_per_page,
|
| 48 |
+
}
|
| 49 |
+
for name, value in numeric_values.items():
|
| 50 |
+
if name == "font_index" and value < 0:
|
| 51 |
+
raise ValueError("font_index cannot be negative")
|
| 52 |
+
if name != "font_index" and value < 1:
|
| 53 |
+
raise ValueError(f"{name} must be positive")
|
| 54 |
+
if self.min_height_px > self.max_height_px:
|
| 55 |
+
raise ValueError("min_height_px cannot exceed max_height_px")
|
| 56 |
+
if self.min_height_px % self.height_step_px or self.max_height_px % self.height_step_px:
|
| 57 |
+
raise ValueError("page heights must be exact multiples of height_step_px")
|
| 58 |
+
if line_height_px * max_lines_per_page > self.max_height_px:
|
| 59 |
+
raise ValueError("max_lines_per_page does not fit in max_height_px")
|
| 60 |
+
|
| 61 |
+
def to_dict(self) -> dict[str, Any]:
|
| 62 |
+
rendering = {
|
| 63 |
+
"font": f"{self.font_family} {self.font_style}",
|
| 64 |
+
"font_path": str(self.font_path),
|
| 65 |
+
"font_size_px": self.font_size_px,
|
| 66 |
+
"dpi": self.dpi,
|
| 67 |
+
"line_height_px": self.line_height_px,
|
| 68 |
+
"width_px": self.width_px,
|
| 69 |
+
"min_height_px": self.min_height_px,
|
| 70 |
+
"height_step_px": self.height_step_px,
|
| 71 |
+
"max_height_px": self.max_height_px,
|
| 72 |
+
"max_lines_per_page": self.max_lines_per_page,
|
| 73 |
+
"margins_px": 0,
|
| 74 |
+
"background": "white",
|
| 75 |
+
"foreground": "black",
|
| 76 |
+
"whitespace_normalization": "all_runs_to_single_space",
|
| 77 |
+
"wrap_measurement": (
|
| 78 |
+
"roboto_pixel_width"
|
| 79 |
+
if self.font_family.casefold() == "roboto"
|
| 80 |
+
else "font_pixel_width"
|
| 81 |
+
),
|
| 82 |
+
"overwide_word_split": "character",
|
| 83 |
+
"page_overlap": 0,
|
| 84 |
+
}
|
| 85 |
+
if self.font_index:
|
| 86 |
+
rendering["font_index"] = self.font_index
|
| 87 |
+
return rendering
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class DocumentImageRenderer:
|
| 91 |
+
"""Normalize, pixel-wrap, paginate, and render document text."""
|
| 92 |
+
|
| 93 |
+
def __init__(self, config: DocumentImageConfig) -> None:
|
| 94 |
+
self.config = config
|
| 95 |
+
if not config.font_path.is_file():
|
| 96 |
+
raise FileNotFoundError(f"font not found: {config.font_path}")
|
| 97 |
+
self.font = ImageFont.truetype(
|
| 98 |
+
str(config.font_path),
|
| 99 |
+
config.font_size_px,
|
| 100 |
+
index=config.font_index,
|
| 101 |
+
)
|
| 102 |
+
family, style = self.font.getname()
|
| 103 |
+
if (
|
| 104 |
+
family.casefold() != config.font_family.casefold()
|
| 105 |
+
or style.casefold() != config.font_style.casefold()
|
| 106 |
+
):
|
| 107 |
+
raise ValueError(
|
| 108 |
+
f"{config.font_family} {config.font_style} is required, "
|
| 109 |
+
f"but {config.font_path} reports {family} {style}"
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
@staticmethod
|
| 113 |
+
def normalize_text(text: str) -> str:
|
| 114 |
+
return " ".join(str(text).split())
|
| 115 |
+
|
| 116 |
+
def text_length(self, text: str) -> float:
|
| 117 |
+
return self.font.getlength(text)
|
| 118 |
+
|
| 119 |
+
def draw_line(self, draw: ImageDraw.ImageDraw, line: str, y: int) -> None:
|
| 120 |
+
draw.text(
|
| 121 |
+
(0, y),
|
| 122 |
+
line,
|
| 123 |
+
font=self.font,
|
| 124 |
+
fill="black",
|
| 125 |
+
anchor="lt",
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
def rendering_metadata(self) -> dict[str, Any]:
|
| 129 |
+
return self.config.to_dict()
|
| 130 |
+
|
| 131 |
+
def _split_overwide_word(self, word: str, max_width: int) -> list[str]:
|
| 132 |
+
chunks: list[str] = []
|
| 133 |
+
offset = 0
|
| 134 |
+
while offset < len(word):
|
| 135 |
+
low, high = offset + 1, len(word)
|
| 136 |
+
best = offset
|
| 137 |
+
while low <= high:
|
| 138 |
+
middle = (low + high) // 2
|
| 139 |
+
if self.text_length(word[offset:middle]) <= max_width:
|
| 140 |
+
best = middle
|
| 141 |
+
low = middle + 1
|
| 142 |
+
else:
|
| 143 |
+
high = middle - 1
|
| 144 |
+
if best == offset:
|
| 145 |
+
raise ValueError(
|
| 146 |
+
f"width {max_width}px is narrower than one rendered character"
|
| 147 |
+
)
|
| 148 |
+
chunks.append(word[offset:best])
|
| 149 |
+
offset = best
|
| 150 |
+
return chunks
|
| 151 |
+
|
| 152 |
+
def wrap_text(self, text: str, *, max_width: int | None = None) -> list[str]:
|
| 153 |
+
max_width = self.config.width_px if max_width is None else max_width
|
| 154 |
+
if max_width < 1:
|
| 155 |
+
raise ValueError("max_width must be positive")
|
| 156 |
+
normalized = self.normalize_text(text)
|
| 157 |
+
if not normalized:
|
| 158 |
+
return []
|
| 159 |
+
|
| 160 |
+
lines: list[str] = []
|
| 161 |
+
current = ""
|
| 162 |
+
for word in normalized.split(" "):
|
| 163 |
+
if self.text_length(word) > max_width:
|
| 164 |
+
if current:
|
| 165 |
+
lines.append(current)
|
| 166 |
+
current = ""
|
| 167 |
+
chunks = self._split_overwide_word(word, max_width)
|
| 168 |
+
lines.extend(chunks[:-1])
|
| 169 |
+
current = chunks[-1]
|
| 170 |
+
continue
|
| 171 |
+
|
| 172 |
+
candidate = word if not current else f"{current} {word}"
|
| 173 |
+
if self.text_length(candidate) <= max_width:
|
| 174 |
+
current = candidate
|
| 175 |
+
else:
|
| 176 |
+
lines.append(current)
|
| 177 |
+
current = word
|
| 178 |
+
if current:
|
| 179 |
+
lines.append(current)
|
| 180 |
+
return lines
|
| 181 |
+
|
| 182 |
+
def paginate_lines(self, lines: Sequence[str]) -> list[list[str]]:
|
| 183 |
+
if not lines:
|
| 184 |
+
return [[]]
|
| 185 |
+
page_size = self.config.max_lines_per_page
|
| 186 |
+
return [list(lines[start : start + page_size]) for start in range(0, len(lines), page_size)]
|
| 187 |
+
|
| 188 |
+
def page_height(self, line_count: int) -> int:
|
| 189 |
+
if line_count < 0 or line_count > self.config.max_lines_per_page:
|
| 190 |
+
raise ValueError(
|
| 191 |
+
f"line_count must be between 0 and {self.config.max_lines_per_page}"
|
| 192 |
+
)
|
| 193 |
+
content_height = max(1, line_count) * self.config.line_height_px
|
| 194 |
+
stepped_height = math.ceil(content_height / self.config.height_step_px) * self.config.height_step_px
|
| 195 |
+
return min(
|
| 196 |
+
self.config.max_height_px,
|
| 197 |
+
max(self.config.min_height_px, stepped_height),
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
def render_document(self, text: str) -> list[Image.Image]:
|
| 201 |
+
lines = self.wrap_text(text)
|
| 202 |
+
rendered: list[Image.Image] = []
|
| 203 |
+
for page_lines in self.paginate_lines(lines):
|
| 204 |
+
image = Image.new(
|
| 205 |
+
"RGB",
|
| 206 |
+
(self.config.width_px, self.page_height(len(page_lines))),
|
| 207 |
+
"white",
|
| 208 |
+
)
|
| 209 |
+
draw = ImageDraw.Draw(image)
|
| 210 |
+
for line_index, line in enumerate(page_lines):
|
| 211 |
+
self.draw_line(draw, line, line_index * self.config.line_height_px)
|
| 212 |
+
rendered.append(image)
|
| 213 |
+
return rendered
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def prepare_render_inputs(inputs: dict, renderer: DocumentImageRenderer) -> dict:
|
| 218 |
+
"""Convert render documents to images; leave query/instruction/scoring untouched."""
|
| 219 |
+
if not isinstance(inputs, dict):
|
| 220 |
+
raise TypeError("inputs must be a dictionary")
|
| 221 |
+
documents = inputs.get("documents", [])
|
| 222 |
+
if not isinstance(documents, list):
|
| 223 |
+
raise TypeError("documents must be a list")
|
| 224 |
+
converted = []
|
| 225 |
+
for document in documents:
|
| 226 |
+
if not isinstance(document, dict):
|
| 227 |
+
raise TypeError("each document must be a dictionary")
|
| 228 |
+
if "render" not in document:
|
| 229 |
+
converted.append(document)
|
| 230 |
+
continue
|
| 231 |
+
if any(key in document for key in ("text", "image", "video")):
|
| 232 |
+
raise ValueError("render cannot be combined with text, image, or video")
|
| 233 |
+
if not isinstance(document["render"], str):
|
| 234 |
+
raise TypeError("render must contain document text as a string")
|
| 235 |
+
converted.append({
|
| 236 |
+
**{key: value for key, value in document.items() if key != "render"},
|
| 237 |
+
"image": renderer.render_document(document["render"]),
|
| 238 |
+
})
|
| 239 |
+
return {**inputs, "documents": converted}
|
sentence_bert_config.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"transformer_task": "any-to-any",
|
| 3 |
+
"modality_config": {
|
| 4 |
+
"text": {
|
| 5 |
+
"method": "forward",
|
| 6 |
+
"method_output_name": "logits"
|
| 7 |
+
},
|
| 8 |
+
"image": {
|
| 9 |
+
"method": "forward",
|
| 10 |
+
"method_output_name": "logits"
|
| 11 |
+
},
|
| 12 |
+
"video": {
|
| 13 |
+
"method": "forward",
|
| 14 |
+
"method_output_name": "logits"
|
| 15 |
+
},
|
| 16 |
+
"message": {
|
| 17 |
+
"method": "forward",
|
| 18 |
+
"method_output_name": "logits",
|
| 19 |
+
"format": "structured"
|
| 20 |
+
}
|
| 21 |
+
},
|
| 22 |
+
"module_output_name": "causal_logits",
|
| 23 |
+
"processing_kwargs": {
|
| 24 |
+
"chat_template": {
|
| 25 |
+
"chat_template": "reranker",
|
| 26 |
+
"add_generation_prompt": true
|
| 27 |
+
}
|
| 28 |
+
},
|
| 29 |
+
"unpad_inputs": false
|
| 30 |
+
}
|
tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:be75606093db2094d7cd20f3c2f385c212750648bd6ea4fb2bf507a6a4c55506
|
| 3 |
+
size 11422650
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_prefix_space": false,
|
| 3 |
+
"backend": "tokenizers",
|
| 4 |
+
"bos_token": null,
|
| 5 |
+
"clean_up_tokenization_spaces": false,
|
| 6 |
+
"eos_token": "<|im_end|>",
|
| 7 |
+
"errors": "replace",
|
| 8 |
+
"is_local": true,
|
| 9 |
+
"local_files_only": false,
|
| 10 |
+
"max_pixels": 802816,
|
| 11 |
+
"min_pixels": 4096,
|
| 12 |
+
"model_max_length": 262144,
|
| 13 |
+
"pad_token": "<|endoftext|>",
|
| 14 |
+
"processor_class": "Qwen3VLProcessor",
|
| 15 |
+
"split_special_tokens": false,
|
| 16 |
+
"tokenizer_class": "Qwen2Tokenizer",
|
| 17 |
+
"unk_token": null
|
| 18 |
+
}
|