xiaoxiaoshadiao commited on
Commit
2ada2c7
·
verified ·
1 Parent(s): ddbc4af

Add Sentence Transformers support and ship bidirectional attention fix

Browse files

Applies the MultiVectorEncoder integration from PR #1 by @tomaarsen , verified against the ColPali path (identical shapes, MaxSim within bfloat16 noise).

Also ships bidirectional.py: released colpali-engine (through 0.3.17) exposes no enable_bidirectional_attention, so infer.py and reproduce.py raised AttributeError on a plain install, and running causal shifts top-hit MaxSim by about 1.1.

1_Dense/config.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "in_features": 2560,
3
+ "out_features": 128,
4
+ "bias": true,
5
+ "activation_function": "torch.nn.modules.linear.Identity",
6
+ "module_input_name": "token_embeddings",
7
+ "module_output_name": "token_embeddings"
8
+ }
2_Normalize/config.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "module_input_name": "token_embeddings",
3
+ "module_output_name": "token_embeddings"
4
+ }
3_MultiVectorMask/config.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "skiplist_words": []
3
+ }
README.md CHANGED
@@ -24,6 +24,7 @@ tags:
24
  - document-retrieval
25
  - multimodal
26
  - state-of-the-art
 
27
  base_model:
28
  - Qwen/Qwen3.5-4B
29
  datasets:
@@ -181,6 +182,8 @@ Queries in **English, French, German, Italian, Spanish, Portuguese and Chinese**
181
 
182
  ## ⚡ Quick Start
183
 
 
 
184
  ### Installation
185
 
186
  ```bash
@@ -194,6 +197,8 @@ import torch
194
  from PIL import Image
195
  from colpali_engine.models import ColQwen3_5, ColQwen3_5Processor
196
 
 
 
197
  model_id = "tencent/EVIE-Preview-4.5B"
198
 
199
  # 1. Load model and enable bidirectional attention
@@ -203,7 +208,7 @@ model = ColQwen3_5.from_pretrained(
203
  device_map="cuda",
204
  attn_implementation="flash_attention_2",
205
  ).eval()
206
- model.enable_bidirectional_attention()
207
 
208
  # 2. Load processor
209
  processor = ColQwen3_5Processor.from_pretrained(model_id)
@@ -225,7 +230,7 @@ scores = processor.score(query_embeddings, image_embeddings)
225
  print("Late-interaction retrieval scores:", scores)
226
  ```
227
 
228
- > ⚠️ Call `model.enable_bidirectional_attention()` and reset `model.rope_deltas = None` before every query forward pass. Both are required to reach the scores above.
229
 
230
  ### CLI
231
 
@@ -233,6 +238,42 @@ print("Late-interaction retrieval scores:", scores)
233
  python infer.py --query "Quarterly revenue report" --image page_1.png --image page_2.png
234
  ```
235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  ---
237
 
238
  ## 🔬 Reproducing
 
24
  - document-retrieval
25
  - multimodal
26
  - state-of-the-art
27
+ - sentence-transformers
28
  base_model:
29
  - Qwen/Qwen3.5-4B
30
  datasets:
 
182
 
183
  ## ⚡ Quick Start
184
 
185
+ ColPali Engine is the reference path — every number on this card comes from it. A [Sentence Transformers](#sentence-transformers) path is also available for late-interaction pipelines already built on that API.
186
+
187
  ### Installation
188
 
189
  ```bash
 
197
  from PIL import Image
198
  from colpali_engine.models import ColQwen3_5, ColQwen3_5Processor
199
 
200
+ from bidirectional import enable_bidirectional_attention
201
+
202
  model_id = "tencent/EVIE-Preview-4.5B"
203
 
204
  # 1. Load model and enable bidirectional attention
 
208
  device_map="cuda",
209
  attn_implementation="flash_attention_2",
210
  ).eval()
211
+ enable_bidirectional_attention(model)
212
 
213
  # 2. Load processor
214
  processor = ColQwen3_5Processor.from_pretrained(model_id)
 
230
  print("Late-interaction retrieval scores:", scores)
231
  ```
232
 
233
+ > ⚠️ Apply `enable_bidirectional_attention(model)` once after loading, and reset `model.rope_deltas = None` before every query forward pass. Both are required to reach the scores above. [`bidirectional.py`](bidirectional.py) ships with this repository because released `colpali-engine` builds ColQwen3.5 with causal masks.
234
 
235
  ### CLI
236
 
 
238
  python infer.py --query "Quarterly revenue report" --image page_1.png --image page_2.png
239
  ```
240
 
241
+ ### Sentence Transformers
242
+
243
+ EVIE also loads as a [Sentence Transformers](https://www.sbert.net/) `MultiVectorEncoder`, exposing the familiar `encode_query` / `encode_document` / `similarity` API with MaxSim scoring built in. Bidirectional attention is baked into the shipped configuration, so no extra call is needed.
244
+
245
+ `MultiVectorEncoder` requires Sentence Transformers 6.0.0, which is not on PyPI yet — install from source until it is released:
246
+
247
+ ```bash
248
+ pip install "sentence-transformers[image] @ git+https://github.com/huggingface/sentence-transformers.git"
249
+ ```
250
+
251
+ ```python
252
+ from sentence_transformers import MultiVectorEncoder
253
+
254
+ model = MultiVectorEncoder("tencent/EVIE-Preview-4.5B")
255
+
256
+ queries = ["What key insights are presented on this page?"]
257
+ documents = ["document_page.png"]
258
+
259
+ query_embeddings = model.encode_query(queries)
260
+ document_embeddings = model.encode_document(documents)
261
+
262
+ scores = model.similarity(query_embeddings, document_embeddings)
263
+ ```
264
+
265
+ Documents may be file paths, URLs or `PIL.Image` objects. Text passed to `encode_document` is rendered as a query, since this model has no separate text-document format.
266
+
267
+ The default page budget is the 768-token tier. To score the 1,792-token tier, raise the pixel budget through `processor_kwargs`:
268
+
269
+ ```python
270
+ model = MultiVectorEncoder(
271
+ "tencent/EVIE-Preview-4.5B",
272
+ model_kwargs={"attn_implementation": "flash_attention_2", "device_map": "cuda:0"},
273
+ processor_kwargs={"size": {"longest_edge": 1792 * 32 * 32, "shortest_edge": 65536}},
274
+ )
275
+ ```
276
+
277
  ---
278
 
279
  ## 🔬 Reproducing
bidirectional.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Bidirectional attention for ColQwen3.5 retrieval.
2
+
3
+ Released `colpali-engine` (through 0.3.17) builds ColQwen3.5 with the causal
4
+ Qwen3.5 masks it inherits from the generative backbone. EVIE was trained and
5
+ evaluated with the full-attention layers encoder-ized, so the checkpoint must be
6
+ switched before it reproduces the reported scores.
7
+
8
+ Qwen3.5 interleaves GatedDeltaNet (`linear_attention`) and `full_attention`
9
+ layers. Only the full-attention layers are flipped here; the recurrent layers
10
+ are order-dependent by construction and are left untouched.
11
+ """
12
+
13
+ from typing import Any
14
+
15
+ _ATTENTION_CLASSES = ("Qwen3_5Attention", "Qwen3Attention")
16
+
17
+
18
+ def enable_bidirectional_attention(model: Any) -> None:
19
+ """Encoder-ize the full-attention layers of a ColQwen3.5 model, in place."""
20
+ config = getattr(model, "config", None)
21
+ for cfg in (config, getattr(config, "text_config", None)):
22
+ # `create_causal_mask` falls back to `create_bidirectional_mask` on this flag.
23
+ if cfg is not None:
24
+ cfg.is_causal = False
25
+
26
+ for module in model.modules():
27
+ if module.__class__.__name__ in _ATTENTION_CLASSES and hasattr(module, "is_causal"):
28
+ module.is_causal = False
chat_template.jinja ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- for message in messages -%}
2
+ {%- set ns = namespace(has_image=false, text='') -%}
3
+ {%- if message['content'] is string -%}
4
+ {%- set ns.text = message['content'] -%}
5
+ {%- else -%}
6
+ {%- for item in message['content'] -%}
7
+ {%- if 'image' in item or 'image_url' in item or item.type == 'image' -%}
8
+ {%- set ns.has_image = true -%}
9
+ {%- elif 'text' in item -%}
10
+ {%- set ns.text = ns.text + item.text -%}
11
+ {%- endif -%}
12
+ {%- endfor -%}
13
+ {%- endif -%}
14
+ {%- if ns.has_image -%}
15
+ {{- '<|im_start|>user\n<|vision_start|><|image_pad|><|vision_end|>Describe the image.<|im_end|><|endoftext|>' -}}
16
+ {%- else -%}
17
+ {{- ns.text + '<|endoftext|>' * 10 -}}
18
+ {%- endif -%}
19
+ {%- endfor -%}
config_sentence_transformers.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "__version__": {
3
+ "sentence_transformers": "6.0.0"
4
+ },
5
+ "model_type": "MultiVectorEncoder",
6
+ "similarity_fn_name": "maxsim",
7
+ "prompts": {},
8
+ "default_prompt_name": null
9
+ }
infer.py CHANGED
@@ -8,6 +8,8 @@ from PIL import Image
8
 
9
  from colpali_engine.models import ColQwen3_5, ColQwen3_5Processor
10
 
 
 
11
 
12
  def load(model_id: str, device: str = "cuda"):
13
  model = ColQwen3_5.from_pretrained(
@@ -16,7 +18,7 @@ def load(model_id: str, device: str = "cuda"):
16
  device_map=device,
17
  attn_implementation="flash_attention_2",
18
  ).eval()
19
- model.enable_bidirectional_attention()
20
  return model, ColQwen3_5Processor.from_pretrained(model_id)
21
 
22
 
 
8
 
9
  from colpali_engine.models import ColQwen3_5, ColQwen3_5Processor
10
 
11
+ from bidirectional import enable_bidirectional_attention
12
+
13
 
14
  def load(model_id: str, device: str = "cuda"):
15
  model = ColQwen3_5.from_pretrained(
 
18
  device_map=device,
19
  attn_implementation="flash_attention_2",
20
  ).eval()
21
+ enable_bidirectional_attention(model)
22
  return model, ColQwen3_5Processor.from_pretrained(model_id)
23
 
24
 
modules.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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_Dense",
12
+ "type": "sentence_transformers.base.modules.dense.Dense"
13
+ },
14
+ {
15
+ "idx": 2,
16
+ "name": "2",
17
+ "path": "2_Normalize",
18
+ "type": "sentence_transformers.sentence_transformer.modules.normalize.Normalize"
19
+ },
20
+ {
21
+ "idx": 3,
22
+ "name": "3",
23
+ "path": "3_MultiVectorMask",
24
+ "type": "sentence_transformers.multi_vector_encoder.modules.multi_vector_mask.MultiVectorMask"
25
+ }
26
+ ]
processor_config.json CHANGED
@@ -25,7 +25,7 @@
25
  },
26
  "temporal_patch_size": 2
27
  },
28
- "processor_class": "ColQwen3_5Processor",
29
  "video_processor": {
30
  "do_convert_rgb": true,
31
  "do_normalize": true,
 
25
  },
26
  "temporal_patch_size": 2
27
  },
28
+ "processor_class": "Qwen3VLProcessor",
29
  "video_processor": {
30
  "do_convert_rgb": true,
31
  "do_normalize": true,
reproduce.py CHANGED
@@ -28,6 +28,8 @@ from PIL import Image
28
  from colpali_engine.models import ColQwen3_5, ColQwen3_5Processor
29
  from colpali_engine.utils.maxsim import maxsim_inbatch
30
 
 
 
31
  V1 = [
32
  "arxivqa_test_subsampled",
33
  "docvqa_test_subsampled",
@@ -241,7 +243,7 @@ def main() -> int:
241
  model = ColQwen3_5.from_pretrained(
242
  args.model, torch_dtype=torch.bfloat16, attn_implementation=args.attn
243
  )
244
- model.enable_bidirectional_attention()
245
  model = model.to("cuda").eval()
246
 
247
  result, cache = {}, {}
 
28
  from colpali_engine.models import ColQwen3_5, ColQwen3_5Processor
29
  from colpali_engine.utils.maxsim import maxsim_inbatch
30
 
31
+ from bidirectional import enable_bidirectional_attention
32
+
33
  V1 = [
34
  "arxivqa_test_subsampled",
35
  "docvqa_test_subsampled",
 
243
  model = ColQwen3_5.from_pretrained(
244
  args.model, torch_dtype=torch.bfloat16, attn_implementation=args.attn
245
  )
246
+ enable_bidirectional_attention(model)
247
  model = model.to("cuda").eval()
248
 
249
  result, cache = {}, {}
sentence_bert_config.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "transformer_task": "feature-extraction",
3
+ "modality_config": {
4
+ "text": {
5
+ "method": "forward",
6
+ "method_output_name": "last_hidden_state"
7
+ },
8
+ "image": {
9
+ "method": "forward",
10
+ "method_output_name": "last_hidden_state"
11
+ },
12
+ "message": {
13
+ "method": "forward",
14
+ "method_output_name": "last_hidden_state",
15
+ "format": "structured"
16
+ }
17
+ },
18
+ "module_output_name": "token_embeddings",
19
+ "unpad_inputs": false,
20
+ "config_kwargs": {
21
+ "text_config": {
22
+ "is_causal": false
23
+ }
24
+ }
25
+ }
tokenizer_config.json CHANGED
@@ -21,8 +21,9 @@
21
  "vision_eos_token": "<|vision_end|>"
22
  },
23
  "pad_token": "<|endoftext|>",
 
24
  "pretokenize_regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?[\\p{L}\\p{M}]+|\\p{N}| ?[^\\s\\p{L}\\p{M}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
25
- "processor_class": "ColQwen3_5Processor",
26
  "split_special_tokens": false,
27
  "tokenizer_class": "Qwen2Tokenizer",
28
  "unk_token": null,
 
21
  "vision_eos_token": "<|vision_end|>"
22
  },
23
  "pad_token": "<|endoftext|>",
24
+ "padding_side": "left",
25
  "pretokenize_regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?[\\p{L}\\p{M}]+|\\p{N}| ?[^\\s\\p{L}\\p{M}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
26
+ "processor_class": "Qwen3VLProcessor",
27
  "split_special_tokens": false,
28
  "tokenizer_class": "Qwen2Tokenizer",
29
  "unk_token": null,