Integrate with Sentence Transformers

#2
by tomaarsen HF Staff - opened
README.md CHANGED
@@ -9,8 +9,10 @@ tags:
9
  - multimodal
10
  - information-retrieval
11
  - inference-free
 
 
12
  pipeline_tag: feature-extraction
13
- library_name: transformers
14
  ---
15
 
16
  <p align="center">
@@ -19,9 +21,11 @@ library_name: transformers
19
 
20
  # V-SPLADE: Inference-Free Multimodal Learned Sparse Retrieval for Production-Scale Visual Document Search
21
 
22
- **Paper:** [arXiv:2605.30917](https://arxiv.org/abs/2605.30917) &nbsp;·&nbsp; **Code:** [github.com/naver/v-splade](https://github.com/naver/v-splade)
23
 
24
  > **This repository hosts the `Quality` variant** (higher retrieval quality). For the lower-FLOPs checkpoint, see [`naver/v-splade-efficient`](https://huggingface.co/naver/v-splade-efficient).
 
 
25
 
26
  ## Model Summary
27
 
@@ -65,6 +69,44 @@ Measured on a single H100 GPU with 4 CPU cores, using 1,000 sampled documents ac
65
 
66
  ## Quick Start
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  Install (see the [code repository](https://github.com/naver/v-splade) for full instructions):
69
 
70
  ```bash
@@ -79,7 +121,7 @@ pip install -r requirements_filtered.txt
79
  pip install flash-attn==2.8.3 --no-build-isolation --no-cache-dir
80
  ```
81
 
82
- ### Single-image inference (minimal example)
83
 
84
  The shortest path to seeing V-SPLADE work on your own page image — encode one image into a sparse vocabulary vector, inspect the top-activated tokens, and score a text query against it:
85
 
 
9
  - multimodal
10
  - information-retrieval
11
  - inference-free
12
+ - sentence-transformers
13
+ - sparse-encoder
14
  pipeline_tag: feature-extraction
15
+ library_name: sentence-transformers
16
  ---
17
 
18
  <p align="center">
 
21
 
22
  # V-SPLADE: Inference-Free Multimodal Learned Sparse Retrieval for Production-Scale Visual Document Search
23
 
24
+ **Paper:** [arXiv:2605.30917](https://arxiv.org/abs/2605.30917) &nbsp;·&nbsp; **Code:** [github.com/naver/v-splade](https://github.com/naver/v-splade) &nbsp;·&nbsp; **Demo:** [🤗 Space](https://huggingface.co/spaces/hugging-apps/v-splade-document-retrieval)
25
 
26
  > **This repository hosts the `Quality` variant** (higher retrieval quality). For the lower-FLOPs checkpoint, see [`naver/v-splade-efficient`](https://huggingface.co/naver/v-splade-efficient).
27
+ >
28
+ > 🚀 **Try it live:** an [interactive demo](https://huggingface.co/spaces/hugging-apps/v-splade-document-retrieval) of this model, kindly contributed by [Apolinário](https://huggingface.co/multimodalart) and the Hugging Face open-source team.
29
 
30
  ## Model Summary
31
 
 
69
 
70
  ## Quick Start
71
 
72
+ ### Using Sentence Transformers
73
+
74
+ Install Sentence Transformers (v5.6.0 or later) with image support, and note that the ModernVBERT backbone requires `transformers>=5.3.0`:
75
+
76
+ ```bash
77
+ pip install "sentence_transformers[image]>=5.6.0"
78
+ ```
79
+
80
+ Queries are encoded with the inference-free Li-LSR lookup (no transformer forward pass), while document page images run through the full model:
81
+
82
+ ```python
83
+ from sentence_transformers import SparseEncoder
84
+
85
+ model = SparseEncoder("naver/v-splade-quality", trust_remote_code=True)
86
+
87
+ queries = ["send signed forms", "records office"]
88
+ documents = ["https://raw.githubusercontent.com/naver/v-splade/main/examples/sample_page.png"]
89
+
90
+ query_embeddings = model.encode_query(queries)
91
+ document_embeddings = model.encode_document(documents)
92
+ print(query_embeddings.shape, document_embeddings.shape)
93
+ # torch.Size([2, 50368]) torch.Size([1, 50368])
94
+
95
+ similarities = model.similarity(query_embeddings, document_embeddings)
96
+ print(similarities)
97
+ # tensor([[0.9843],
98
+ # [0.5935]], device='cuda:0')
99
+
100
+ # Inspect the top activated tokens of the page image
101
+ decoded = model.decode(document_embeddings[0], top_k=5)
102
+ print([(token.replace("Ġ", " ").strip(), round(weight, 3)) for token, weight in decoded])
103
+ # [('dog', 1.836), ('dogs', 1.664), ('puppy', 1.586), ('Records', 1.578), ('Bennett', 1.508)]
104
+ ```
105
+
106
+ Images can be passed as PIL images, local paths, URLs (as above), or together with text as `{"image": ..., "text": ...}`. Plain text documents are also supported: `model.encode_document(["some passage text"])`. The model runs in bfloat16 by default. You can pass `model_kwargs={"torch_dtype": "float32"}` for full precision.
107
+
108
+ ### Using the reference implementation
109
+
110
  Install (see the [code repository](https://github.com/naver/v-splade) for full instructions):
111
 
112
  ```bash
 
121
  pip install flash-attn==2.8.3 --no-build-isolation --no-cache-dir
122
  ```
123
 
124
+ #### Single-image inference (minimal example)
125
 
126
  The shortest path to seeing V-SPLADE work on your own page image — encode one image into a sparse vocabulary vector, inspect the top-activated tokens, and score a text query against it:
127
 
additional_chat_templates/sentence_transformers.jinja ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {#- V-SPLADE input formatting for Sentence Transformers.
2
+ - Messages with an image render to the inference format from
3
+ github.com/naver/v-splade (vsplade_inference.py):
4
+ "User:<image><end_of_utterance>\nAssistant:", with any text after the image.
5
+ - Text-only messages render as the raw text (the plain RLHN text-passage
6
+ training format); the tokenizer adds [CLS]/[SEP]. -#}
7
+ {%- for message in messages -%}
8
+ {%- if message['content'] is string -%}
9
+ {{- message['content'] -}}
10
+ {%- else -%}
11
+ {%- set ns = namespace(has_image=false) -%}
12
+ {%- for line in message['content'] -%}
13
+ {%- if line['type'] == 'image' -%}{%- set ns.has_image = true -%}{%- endif -%}
14
+ {%- endfor -%}
15
+ {%- if ns.has_image -%}
16
+ {{- message['role'] | capitalize -}}{{- ':' -}}
17
+ {%- for line in message['content'] -%}
18
+ {%- if line['type'] == 'image' -%}{{- '<image>' -}}{%- endif -%}
19
+ {%- endfor -%}
20
+ {%- for line in message['content'] -%}
21
+ {%- if line['type'] == 'text' -%}{{- line['text'] -}}{%- endif -%}
22
+ {%- endfor -%}
23
+ {{- '<end_of_utterance>\n' -}}{{- 'Assistant:' -}}
24
+ {%- else -%}
25
+ {%- for line in message['content'] -%}
26
+ {%- if line['type'] == 'text' -%}{{- line['text'] -}}{%- endif -%}
27
+ {%- endfor -%}
28
+ {%- endif -%}
29
+ {%- endif -%}
30
+ {%- endfor -%}
chat_template.jinja ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ {% for message in messages %}{{message['role'] | capitalize}}{% if message['content'][0]['type'] == 'image' %}{{':'}}{% else %}{{': '}}{% endif %}{% for line in message['content'] %}{% if line['type'] == 'text' %}{{line['text']}}{% elif line['type'] == 'image' %}{{ '<image>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}<end_of_utterance>
2
+ {% endif %}{% endfor %}{% if add_generation_prompt %}{{ 'Assistant:' }}{% endif %}
chat_template.json DELETED
@@ -1,3 +0,0 @@
1
- {
2
- "chat_template": "{% for message in messages %}{{message['role'] | capitalize}}{% if message['content'][0]['type'] == 'image' %}{{':'}}{% else %}{{': '}}{% endif %}{% for line in message['content'] %}{% if line['type'] == 'text' %}{{line['text']}}{% elif line['type'] == 'image' %}{{ '<image>' }}{% endif %}{% endfor %}{% if add_generation_prompt %}<end_of_utterance>\n{% endif %}{% endfor %}{% if add_generation_prompt %}{{ 'Assistant:' }}{% endif %}"
3
- }
 
 
 
 
config.json CHANGED
@@ -1,4 +1,7 @@
1
  {
 
 
 
2
  "image_token_id": 50407,
3
  "initializer_range": 0.02,
4
  "model_type": "modernvbert",
 
1
  {
2
+ "auto_map": {
3
+ "AutoModelForMaskedLM": "modeling_vsplade.VSPLADEForMaskedLM"
4
+ },
5
  "image_token_id": 50407,
6
  "initializer_range": 0.02,
7
  "model_type": "modernvbert",
config_sentence_transformers.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "__version__": {
3
+ "sentence_transformers": "5.6.0",
4
+ "transformers": "5.13.0",
5
+ "pytorch": "2.10.0"
6
+ },
7
+ "model_type": "SparseEncoder",
8
+ "similarity_fn_name": "dot"
9
+ }
configuration_modernvbert.py DELETED
@@ -1,225 +0,0 @@
1
- # 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
2
- # This file was automatically generated from src/transformers/models/modernvbert/modular_modernvbert.py.
3
- # Do NOT edit this file manually as any edits will be overwritten by the generation of
4
- # the file from the modular. If any change should be done, please apply the change to the
5
- # modular_modernvbert.py file directly. One of our CI enforces this.
6
- # 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
7
- import os
8
- from typing import Any, Union
9
-
10
- from ...configuration_utils import PretrainedConfig
11
- from ..modernbert import ModernBertConfig
12
- from ..siglip import SiglipConfig
13
-
14
-
15
- class ModernVBertTextConfig(PretrainedConfig):
16
- r"""
17
- This is the configuration class to store the configuration of a [`ModernBERT`]. It is used to instantiate an ModernBERT
18
- model according to the specified arguments, defining the model architecture. Instantiating a configuration with the
19
- defaults will yield a similar configuration to that of the [jhu-clsp/ettin-encoder-150m](https://huggingface.co/jhu-clsp/ettin-encoder-150m) architecture.
20
-
21
- Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
22
- documentation from [`PretrainedConfig`] for more information.
23
- """
24
-
25
- model_type = "modernvbert_text"
26
-
27
- def __init__(
28
- self,
29
- text_model_name="jhu-clsp/ettin-encoder-150m",
30
- hidden_size=768,
31
- num_hidden_layers=22,
32
- intermediate_size=1152,
33
- mlp_bias=False,
34
- vocab_size=50368,
35
- **kwargs,
36
- ):
37
- super().__init__(
38
- text_model_name=text_model_name,
39
- hidden_size=hidden_size,
40
- num_hidden_layers=num_hidden_layers,
41
- intermediate_size=intermediate_size,
42
- mlp_bias=mlp_bias,
43
- vocab_size=vocab_size,
44
- **kwargs,
45
- )
46
-
47
- @classmethod
48
- def from_base_model(
49
- cls,
50
- text_model_name,
51
- **kwargs,
52
- ):
53
- text_config = ModernBertConfig.from_pretrained(text_model_name)
54
- if hasattr(text_config, "text_config"):
55
- text_config = text_config.text_config
56
-
57
- return cls(
58
- text_model_name=text_model_name,
59
- hidden_size=text_config.hidden_size,
60
- num_hidden_layers=text_config.num_hidden_layers,
61
- intermediate_size=text_config.intermediate_size,
62
- mlp_bias=text_config.mlp_bias,
63
- vocab_size=text_config.vocab_size,
64
- **kwargs,
65
- )
66
-
67
-
68
- class ModernVBertVisionConfig(PretrainedConfig):
69
- r"""
70
- This is the configuration class to store the configuration of a [`SigLIP`]. It is used to instantiate the vision encoder part of the ModernVBERT
71
- model according to the specified arguments, defining the model architecture. Instantiating a configuration with the
72
- defaults will yield a similar configuration to that of the SigLIP.
73
-
74
- Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
75
- documentation from [`PretrainedConfig`] for more information.
76
- """
77
-
78
- model_type = "modernvbert_vision"
79
-
80
- attribute_map = {
81
- "hidden_size": "embed_dim",
82
- }
83
-
84
- def __init__(
85
- self,
86
- vision_model_name="google/siglip2-base-patch16-512",
87
- embed_dim=768,
88
- image_size=512,
89
- patch_size=16,
90
- num_hidden_layers=12,
91
- intermediate_size=3072,
92
- **kwargs,
93
- ):
94
- super().__init__(
95
- vision_model_name=vision_model_name,
96
- embed_dim=embed_dim,
97
- image_size=image_size,
98
- patch_size=patch_size,
99
- num_hidden_layers=num_hidden_layers,
100
- intermediate_size=intermediate_size,
101
- **kwargs,
102
- )
103
-
104
- @classmethod
105
- def from_base_model(
106
- cls,
107
- vision_model_name,
108
- **kwargs,
109
- ):
110
- vision_config = SiglipConfig.from_pretrained(vision_model_name)
111
- if hasattr(vision_config, "vision_config"):
112
- vision_config = vision_config.vision_config
113
-
114
- return cls(
115
- vision_model_name=vision_model_name,
116
- embed_dim=vision_config.hidden_size,
117
- image_size=vision_config.image_size,
118
- patch_size=vision_config.patch_size,
119
- num_hidden_layers=vision_config.num_hidden_layers,
120
- intermediate_size=vision_config.intermediate_size,
121
- **kwargs,
122
- )
123
-
124
-
125
- class ModernVBertConfig(PretrainedConfig):
126
- r"""
127
- This is the configuration class to store the configuration of a `ModernVBert` model. It is used to
128
- instantiate a ModernVBert model according to the specified arguments and defines the model architecture.
129
-
130
- Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs.
131
- See the documentation for [`PretrainedConfig`] for more details.
132
-
133
- Args:
134
- text_config (`PretrainedConfig` or `dict`, optional):
135
- Custom text config or a dict with a `text_model_name` key for the text encoder. If `None`, the
136
- default text backbone defined by `DEFAULT_TEXT_MODEL_NAME` is used.
137
- vision_config (`PretrainedConfig` or `dict`, optional):
138
- Custom vision config or a dict with a `vision_model_name` key for the vision encoder. If `None`, the
139
- default vision backbone defined by `DEFAULT_VISION_MODEL_NAME` is used.
140
- image_token_id (`int`, optional, defaults to 128257):
141
- Token id reserved for image tokens inserted into the text stream.
142
- vocab_size (`int`, optional, defaults to 128256):
143
- Vocabulary size used by the text embeddings.
144
- tie_word_embeddings (`bool`, optional, defaults to `False`):
145
- Whether to tie input token embeddings and output token embeddings.
146
- pixel_shuffle_factor (`int`, optional, defaults to 4):
147
- Scale factor used by any pixel-shuffle / upsampling operations in the vision head.
148
- additional_vocab_size (`int`, optional, defaults to 0):
149
- Number of extra tokens appended to the base vocabulary (useful for adapters / special tokens).
150
- pad_token_id (`int`, optional):
151
- Padding token id.
152
- initializer_range (`float`, optional, defaults to 0.02):
153
- Stddev used for weight initialization.
154
-
155
- Example:
156
- ```python
157
- >>> from modernvbert import ModernVBertConfig
158
-
159
- >>> # Initializing configuration
160
- >>> configuration = ModernVBertConfig()
161
-
162
- >>> # Initializing a model from the configuration (model class is implemented in
163
- >>> # `modernvbert.modeling_modernvbert`)
164
-
165
- >>> from modernvbert import ModernVBertModel
166
- >>> model = ModernVBertModel(configuration)
167
-
168
- >>> # Accessing the model configuration
169
- >>> cfg = model.config
170
- ```"""
171
-
172
- model_type = "modernvbert"
173
- sub_configs: dict[str, Any] = {"text_config": ModernVBertTextConfig, "vision_config": ModernVBertVisionConfig}
174
-
175
- def __init__(
176
- self,
177
- text_config=None,
178
- vision_config=None,
179
- image_token_id: int = 50407,
180
- initializer_range=0.02,
181
- vocab_size=50368,
182
- pad_token_id=None,
183
- pixel_shuffle_factor=4,
184
- additional_vocab_size=0,
185
- **kwargs,
186
- ):
187
- super().__init__(**kwargs)
188
-
189
- if text_config is None:
190
- text_config = self.sub_configs["text_config"].from_base_model("jhu-clsp/ettin-encoder-150m")
191
- elif isinstance(text_config, dict):
192
- text_config = self.sub_configs["text_config"].from_dict(text_config)
193
- self.text_config = text_config
194
-
195
- if vision_config is None:
196
- vision_config = self.sub_configs["vision_config"].from_base_model("google/siglip2-base-patch16-512")
197
- elif isinstance(vision_config, dict):
198
- vision_config = self.sub_configs["vision_config"].from_dict(vision_config)
199
- self.vision_config = vision_config
200
-
201
- self.initializer_range = initializer_range
202
- self.image_token_id = image_token_id
203
- self.pad_token_id = pad_token_id
204
- self.pixel_shuffle_factor = pixel_shuffle_factor
205
- self.vocab_size = vocab_size
206
- self.additional_vocab_size = additional_vocab_size
207
- self.hidden_size = kwargs.pop("hidden_size", self.text_config.hidden_size)
208
-
209
- @classmethod
210
- def from_pretrained_models(
211
- cls,
212
- text_model_name: Union[str, os.PathLike],
213
- vision_model_name: Union[str, os.PathLike],
214
- **kwargs,
215
- ) -> "PretrainedConfig":
216
- text_model_config = ModernVBertTextConfig.from_base_model(text_model_name)
217
- vision_model_config = ModernVBertVisionConfig.from_base_model(vision_model_name)
218
- return cls(
219
- text_config=text_model_config,
220
- vision_config=vision_model_config,
221
- **kwargs,
222
- )
223
-
224
-
225
- __all__ = ["ModernVBertConfig", "ModernVBertTextConfig", "ModernVBertVisionConfig"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
document_1_SpladePooling/config.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "pooling_strategy": "max",
3
+ "activation_function": "relu",
4
+ "embedding_dimension": 50368
5
+ }
modeling_modernvbert.py DELETED
@@ -1,610 +0,0 @@
1
- # 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
2
- # This file was automatically generated from src/transformers/models/modernvbert/modular_modernvbert.py.
3
- # Do NOT edit this file manually as any edits will be overwritten by the generation of
4
- # the file from the modular. If any change should be done, please apply the change to the
5
- # modular_modernvbert.py file directly. One of our CI enforces this.
6
- # 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
7
- from dataclasses import dataclass
8
- from typing import Optional, Union
9
-
10
- import torch
11
- import torch.nn as nn
12
- import torch.nn.functional as F
13
- from torch.nn import CrossEntropyLoss
14
-
15
- from ...modeling_flash_attention_utils import FlashAttentionKwargs
16
- from ...modeling_outputs import BaseModelOutput, BaseModelOutputWithPoolingAndCrossAttentions, MaskedLMOutput
17
- from ...modeling_utils import PreTrainedModel
18
- from ...processing_utils import Unpack
19
- from ...utils import auto_docstring, can_return_tuple
20
- from ..modernbert import ModernBertConfig, ModernBertForMaskedLM, ModernBertModel
21
- from ..siglip import SiglipVisionConfig, SiglipVisionModel
22
- from .configuration_modernvbert import ModernVBertConfig
23
-
24
-
25
- class DecoupledEmbedding(nn.Embedding):
26
- # Derived from https://pytorch.org/docs/stable/_modules/torch/nn/modules/sparse.html#Embedding
27
- """
28
- Implements a decoupling of parameters to allow freezing (or not) a subset of the embeddings.
29
- In practise, the regular `weight` can be trained or frozen (i.e. `partially_freeze=True`), and if `num_additional_embeddings` > 0, then it will create `num_additional_embeddings` additional parameters that are always trained.
30
- If `num_additional_embeddings=0`, then the module defaults back to the regular behavior of `nn.Embedding`.
31
- """
32
-
33
- def __init__(
34
- self,
35
- num_embeddings,
36
- num_additional_embeddings,
37
- embedding_dim,
38
- partially_freeze=False,
39
- device=None,
40
- dtype=None,
41
- padding_idx=None,
42
- **kwargs,
43
- ) -> None:
44
- """
45
- num_additional_embeddings: int. Number of additional embeddings. Only useful when you `partially_freeze=True`.
46
- partially_freeze: bool. If True, the regular `weight` will be frozen. `additional_weight` is never frozen.
47
-
48
- Note: there are a lot of other parameters to initialize a standard `nn.Embedding` such as `padding_idx`, `max_norm` or `norm_type`. We are not supporting these.
49
- """
50
- if padding_idx is not None and padding_idx > num_embeddings:
51
- raise ValueError(f"padding_idx must be within num_embeddings. Got {padding_idx} and {num_embeddings}")
52
-
53
- super().__init__(
54
- num_embeddings=num_embeddings,
55
- embedding_dim=embedding_dim,
56
- device=device,
57
- dtype=dtype,
58
- padding_idx=padding_idx,
59
- **kwargs,
60
- )
61
- self.num_embeddings = num_embeddings
62
- self.num_additional_embeddings = num_additional_embeddings
63
- self.partially_freeze = partially_freeze
64
-
65
- if partially_freeze:
66
- self.weight.requires_grad_(False)
67
-
68
- if self.num_additional_embeddings > 0:
69
- self.additional_embedding = nn.Embedding(
70
- num_embeddings=num_additional_embeddings,
71
- embedding_dim=embedding_dim,
72
- device=device,
73
- dtype=dtype,
74
- )
75
-
76
- def forward(self, input_ids):
77
- """
78
- we have 2 embeddings, with different indices - one pretrained self.weight and another
79
- self.additional_embedding.weight that is being trained.
80
-
81
- in order to make a lookup of the input ids, we:
82
- 1. find out the indices of the entries belonging to the 2nd embedding
83
- 2. extract those values while subtracting the size of the first embedding (num_embeddings),
84
- since the 2nd embedding starts from 0 and not num_embeddings
85
- 3. perform the 2nd embedding lookup
86
- 4. now we handle the 1st embedding, we overwrite indices belonging to the 2nd embedding with a padding index
87
- 5. perform the 1st embedding lookup
88
- 6. now we overwrite the values in the 1st embedding lookup with the values of the 2nd embedding lookup
89
-
90
- note: for the 1st embedding lookup we could have looked up only the low indices and not do
91
- the padding, but then we have to create a new tensor and populate it with 2 tensors that are
92
- spread out across various indices - i.e. not a simple concat - I haven't benchmarked the
93
- complex case if it's any faster, given that seqlens are usually relatively short it's
94
- probably not faster or if faster not by much - but might be a good idea to measure.
95
-
96
- """
97
- if self.num_additional_embeddings == 0:
98
- return super().forward(input_ids)
99
-
100
- input_ids = input_ids.clone()
101
- additional_vocab_indices = torch.where(input_ids >= self.num_embeddings)
102
- input_ids_additional_vocab = input_ids[additional_vocab_indices]
103
- additional_embeddings = self.additional_embedding(input_ids_additional_vocab - self.num_embeddings)
104
-
105
- # for successful lookup replace input_ids with 0, the results of these will be discarded anyway
106
- input_ids[additional_vocab_indices] = 0
107
- full_vector = F.embedding(input_ids, self.weight)
108
- full_vector[additional_vocab_indices] = additional_embeddings # overwrite the records with high indices
109
- return full_vector
110
-
111
-
112
- @dataclass
113
- class ModernVBertBaseModelOutput(BaseModelOutput):
114
- """
115
- Base class for ModernVBERT model's outputs that may also contain a past key/values (to speed up sequential decoding).
116
- Args:
117
- last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, hidden_size)`):
118
- Sequence of hidden-states at the output of the last layer of the model.
119
- If `past_key_values` is used only the last hidden-state of the sequences of shape `(batch_size, 1,
120
- hidden_size)` is output.
121
- hidden_states (`tuple(torch.FloatTensor)`, *optional*, returned when `output_hidden_states=True` is passed or when `config.output_hidden_states=True`):
122
- Tuple of `torch.FloatTensor` (one for the output of the embeddings, if the model has an embedding layer, +
123
- one for the output of each layer) of shape `(batch_size, sequence_length, hidden_size)`.
124
- Hidden-states of the model at the output of each layer plus the optional initial embedding outputs.
125
- attentions (`tuple(torch.FloatTensor)`, *optional*, returned when `output_attentions=True` is passed or when `config.output_attentions=True`):
126
- Tuple of `torch.FloatTensor` (one for each layer) of shape `(batch_size, num_heads, sequence_length,
127
- sequence_length)`.
128
- Attentions weights after the attention softmax, used to compute the weighted average in the self-attention
129
- heads.
130
- image_hidden_states (`tuple(torch.FloatTensor)`, *optional*):
131
- Tuple of `torch.FloatTensor` (one for the output of the image embeddings, `(batch_size, num_images,
132
- sequence_length, hidden_size)`.
133
- image_hidden_states of the model produced by the vision encoder
134
- """
135
-
136
- last_hidden_state: torch.FloatTensor = None
137
- hidden_states: Optional[tuple[torch.FloatTensor]] = None
138
- attentions: Optional[tuple[torch.FloatTensor]] = None
139
- image_hidden_states: Optional[tuple[torch.FloatTensor]] = None
140
-
141
-
142
- @dataclass
143
- class ModernVBertMaskedLMOutput(MaskedLMOutput):
144
- """
145
- Base class for ModernVBERT model's outputs that may also contain a past key/values (to speed up sequential decoding).
146
- Args:
147
- loss (`torch.FloatTensor`, *optional*, returned when `labels` is provided):
148
- Masked language modeling (MLM) loss.
149
- logits (`torch.FloatTensor`):
150
- Prediction scores of the language modeling head (scores for each vocabulary token before SoftMax).
151
- hidden_states (`tuple(torch.FloatTensor)`, *optional*, returned when `output_hidden_states=True` is passed or when `config.output_hidden_states=True`):
152
- Tuple of `torch.FloatTensor` (one for the output of the embeddings, if the model has an embedding layer, +
153
- one for the output of each layer) of shape `(batch_size, sequence_length, hidden_size)`.
154
- Hidden-states of the model at the output of each layer plus the optional initial embedding outputs.
155
- attentions (`tuple(torch.FloatTensor)`, *optional*, returned when `output_attentions=True` is passed or when `config.output_attentions=True`):
156
- Tuple of `torch.FloatTensor` (one for each layer) of shape `(batch_size, num_heads, sequence_length,
157
- sequence_length)`.
158
- Attentions weights after the attention softmax, used to compute the weighted average in the self-attention
159
- heads.
160
- image_hidden_states (`tuple(torch.FloatTensor)`, *optional*):
161
- Tuple of `torch.FloatTensor` (one for the output of the image embeddings, `(batch_size, num_images,
162
- sequence_length, hidden_size)`.
163
- image_hidden_states of the model produced by the vision encoder
164
- """
165
-
166
- loss: Optional[torch.FloatTensor] = None
167
- logits: torch.FloatTensor = None
168
- hidden_states: Optional[tuple[torch.FloatTensor, ...]] = None
169
- attentions: Optional[tuple[torch.FloatTensor, ...]] = None
170
- image_hidden_states: Optional[torch.FloatTensor] = None
171
-
172
-
173
- class ModernVBertSimpleMLP(nn.Module):
174
- """A simple linear projection layer to project the vision hidden states to the text hidden states."""
175
-
176
- def __init__(self, input_size, output_size):
177
- super().__init__()
178
- self.proj = nn.Linear(input_size, output_size, bias=False)
179
-
180
- def forward(self, x):
181
- return self.proj(x)
182
-
183
-
184
- class ModernVBertConnector(nn.Module):
185
- """
186
- Connector module for ModernVBERT. It performs a pixel shuffle operation followed by a linear projection to match the text model's hidden size.
187
- Based on https://pytorch.org/docs/stable/generated/torch.nn.PixelShuffle.html
188
- """
189
-
190
- def __init__(self, config):
191
- super().__init__()
192
- self.pixel_shuffle_factor = config.pixel_shuffle_factor
193
- self.modality_projection = ModernVBertSimpleMLP(
194
- input_size=config.vision_config.hidden_size * (config.pixel_shuffle_factor**2),
195
- output_size=config.text_config.hidden_size,
196
- )
197
-
198
- def pixel_shuffle(self, x, pixel_shuffle_factor):
199
- bsz, seq, embed_dim = x.size()
200
- height = width = int(seq**0.5)
201
- x = x.view(bsz, height, width, embed_dim)
202
- x = x.view(bsz, height, int(width / pixel_shuffle_factor), embed_dim * pixel_shuffle_factor)
203
- x = x.permute(0, 2, 1, 3)
204
- x = x.reshape(
205
- bsz,
206
- int(width / pixel_shuffle_factor),
207
- int(height / pixel_shuffle_factor),
208
- embed_dim * (pixel_shuffle_factor**2),
209
- )
210
- x = x.permute(0, 2, 1, 3)
211
- return x.reshape(bsz, int(seq / (pixel_shuffle_factor**2)), embed_dim * (pixel_shuffle_factor**2))
212
-
213
- def forward(self, image_hidden_states):
214
- image_hidden_states = self.pixel_shuffle(image_hidden_states, self.pixel_shuffle_factor)
215
- return self.modality_projection(image_hidden_states)
216
-
217
-
218
- class ModernVBertPreTrainedModel(PreTrainedModel):
219
- config_class = ModernVBertConfig
220
- base_model_prefix = "model"
221
- supports_gradient_checkpointing = True
222
- _supports_flash_attn_2 = True
223
- _supports_sdpa = True
224
-
225
- def _init_weights(self, module):
226
- std = getattr(self.config, "initializer_range", 0.02)
227
- if isinstance(module, (nn.Linear, nn.Conv2d)):
228
- module.weight.data.normal_(mean=0.0, std=std)
229
- if module.bias is not None:
230
- module.bias.data.zero_()
231
- elif isinstance(module, nn.Embedding):
232
- module.weight.data.normal_(mean=0.0, std=std)
233
- if module.padding_idx is not None:
234
- module.weight.data[module.padding_idx].zero_()
235
-
236
-
237
- @auto_docstring
238
- class ModernVBertModel(ModernVBertPreTrainedModel):
239
- def __init__(self, config: ModernVBertConfig):
240
- super().__init__(config)
241
-
242
- # init components
243
- self.vision_model = ModernVBertModel.init_vision_model(config)
244
- self.connector = ModernVBertConnector(config)
245
- self.text_model = ModernVBertModel.init_language_model(config)
246
-
247
- # set the correct dtype for vision and text models
248
- self.vision_model.to(self.dtype)
249
- self.text_model.to(self.dtype)
250
- self._use_flash_attention_2 = config._attn_implementation == "flash_attention_2"
251
-
252
- self.image_seq_len = int(
253
- ((config.vision_config.image_size // config.vision_config.patch_size) ** 2)
254
- / (config.pixel_shuffle_factor**2)
255
- )
256
-
257
- self.post_init()
258
-
259
- @staticmethod
260
- def init_vision_model(config: ModernVBertConfig):
261
- vision_model_config = SiglipVisionConfig.from_pretrained(
262
- config.vision_config.vision_model_name,
263
- _attn_implementation=config._attn_implementation,
264
- )
265
- vision_model = SiglipVisionModel(vision_model_config).vision_model
266
- return vision_model
267
-
268
- @staticmethod
269
- def init_language_model(config: ModernVBertConfig):
270
- text_model_config = ModernBertConfig.from_pretrained(
271
- config.text_config.text_model_name,
272
- _attn_implementation=config._attn_implementation,
273
- )
274
- text_model = ModernBertModel(text_model_config)
275
- embed_layer = DecoupledEmbedding(
276
- num_embeddings=text_model_config.vocab_size,
277
- num_additional_embeddings=config.additional_vocab_size,
278
- embedding_dim=config.hidden_size,
279
- partially_freeze=getattr(config, "freeze_config", {"freeze_text_layers": False})["freeze_text_layers"],
280
- padding_idx=config.pad_token_id,
281
- )
282
- text_model.set_input_embeddings(embed_layer)
283
- return text_model
284
-
285
- # Copied from transformers.models.idefics2.modeling_idefics2.Idefics2Model.enable_input_require_grads
286
- def enable_input_require_grads(self):
287
- """
288
- Enables the gradients for the input embeddings.
289
-
290
- This is useful for lora when using gradient checkpointing.
291
- c.f. https://github.com/huggingface/peft/issues/1402#issuecomment-1913675032
292
-
293
- Override to set output.requires_grad = True for both the decoder's and vision model's embeddings.
294
- """
295
-
296
- def get_lowest_module(module):
297
- if len(list(module.children())) == 0:
298
- # If the module has no children, it is a leaf module (e.g., Linear, Conv2d, etc.)
299
- return module
300
- else:
301
- # Recursively call the function on each child module
302
- return get_lowest_module(list(module.children())[0])
303
-
304
- def make_inputs_require_grads(module, input, output):
305
- output.requires_grad_(True)
306
-
307
- self._text_require_grads_hook = self.get_input_embeddings().register_forward_hook(make_inputs_require_grads)
308
- self._vision_require_grads_hook = get_lowest_module(self.vision_model).register_forward_hook(
309
- make_inputs_require_grads
310
- )
311
-
312
- # Copied from transformers.models.idefics2.modeling_idefics2.Idefics2Model.disable_input_require_grads
313
- def disable_input_require_grads(self):
314
- self._text_require_grads_hook.remove()
315
- self._vision_require_grads_hook.remove()
316
-
317
- def get_input_embeddings(self):
318
- return self.text_model.get_input_embeddings()
319
-
320
- def set_input_embeddings(self, value):
321
- self.text_model.set_input_embeddings(value)
322
-
323
- def get_image_features(
324
- self, pixel_values: torch.FloatTensor, pixel_attention_mask: Optional[torch.LongTensor] = None
325
- ):
326
- """
327
- Derived from: https://github.com/huggingface/transformers/blob/main/src/transformers/models/smolvlm/modeling_smolvlm.py
328
- Encodes images into continuous embeddings that can be forwarded to the language model.
329
-
330
- Args:
331
- pixel_values (`torch.FloatTensor` of shape `(batch_size, num_channels, image_size, image_size)`):
332
- The tensors corresponding to the input images.
333
- pixel_attention_mask (`torch.LongTensor`, *optional*):
334
- The attention mask indicating padded regions in the image.
335
- """
336
- batch_size, num_images, num_channels, height, width = pixel_values.shape
337
- pixel_values = pixel_values.to(dtype=self.dtype) # fp16 compatibility
338
- pixel_values = pixel_values.view(batch_size * num_images, *pixel_values.shape[2:])
339
-
340
- # Remove padding images - padding images are full 0.
341
- nb_values_per_image = pixel_values.shape[1:].numel()
342
- real_images_inds = (pixel_values == 0.0).sum(dim=(-1, -2, -3)) != nb_values_per_image
343
-
344
- if not any(real_images_inds):
345
- real_images_inds[0] = True
346
-
347
- pixel_values = pixel_values[real_images_inds].contiguous()
348
- # Handle the vision attention mask
349
- if pixel_attention_mask is None:
350
- pixel_attention_mask = torch.ones(
351
- size=[pixel_values.shape[i] for i in (0, 2, 3)],
352
- dtype=torch.bool,
353
- device=pixel_values.device,
354
- )
355
- else:
356
- # Remove padding images from the mask
357
- pixel_attention_mask = pixel_attention_mask.view(batch_size * num_images, *pixel_attention_mask.shape[2:])
358
- pixel_attention_mask = pixel_attention_mask[real_images_inds].contiguous()
359
-
360
- patch_size = self.config.vision_config.patch_size
361
- patches_subgrid = pixel_attention_mask.unfold(dimension=1, size=patch_size, step=patch_size)
362
- patches_subgrid = patches_subgrid.unfold(dimension=2, size=patch_size, step=patch_size)
363
- patch_attention_mask = (patches_subgrid.sum(dim=(-1, -2)) > 0).bool()
364
-
365
- # Get sequence from the vision encoder
366
- image_hidden_states = self.vision_model(pixel_values=pixel_values, patch_attention_mask=patch_attention_mask)
367
- image_hidden_states = image_hidden_states.last_hidden_state
368
-
369
- return image_hidden_states
370
-
371
- def inputs_merger(self, input_ids, inputs_embeds, image_hidden_states):
372
- """Adapted from https://github.com/huggingface/transformers/blob/main/src/transformers/models/smolvlm/modeling_smolvlm.py
373
-
374
- This method aims at merging the token embeddings with the image hidden states into one single sequence of vectors that are fed to the transformer LM.
375
- The merging happens as follows:
376
- - The text token sequence is: `tok_1 tok_2 tok_3 <fake_token_around_image> <image> <image> ... <image> <fake_token_around_image> tok_4`.
377
- - We get the image hidden states for the image through the vision encoder and that hidden state, after a pixel shuffle operation, is then projected into the text embedding space.
378
- We thus have a sequence of image hidden states of size (1, image_seq_len, hidden_dim), where 1 is for batch_size of 1 image and hidden_dim is the hidden_dim of the LM transformer.
379
- - The merging happens so that we obtain the following sequence: `vector_tok_1 vector_tok_2 vector_tok_3 vector_fake_tok_around_image {sequence of image_seq_len image hidden states} vector_fake_toke_around_image vector_tok_4`. That sequence is fed to the LM.
380
- - To fit the format of that sequence, `input_ids`, `input_embeds`, `attention_mask` are all 3 adapted to insert the image hidden states.
381
- """
382
-
383
- _, patch_size, _ = image_hidden_states.shape
384
-
385
- if input_ids is None:
386
- image_mask = inputs_embeds == self.get_input_embeddings()(
387
- torch.tensor(self.config.image_token_id, dtype=torch.long, device=inputs_embeds.device)
388
- )
389
- image_mask = image_mask[..., 0] # slice off the hidden dim
390
- else:
391
- image_mask = input_ids == self.config.image_token_id
392
-
393
- # Assert that the input <image> tokens are valid (i.e. multiple of patch_size)
394
- num_image_tokens = image_mask.sum(dim=1)
395
- if not torch.all(num_image_tokens % patch_size == 0):
396
- raise ValueError("Number of <image> tokens not divisible by patch_size.")
397
-
398
- blocks_per_sample = num_image_tokens // patch_size
399
-
400
- offsets = torch.nn.functional.pad(blocks_per_sample.cumsum(dim=0), (1, 0), value=0)
401
- block_offset = offsets[:-1]
402
- row_cum = image_mask.cumsum(dim=-1)
403
- chunk_idx = (row_cum - 1) // patch_size
404
- local_idx = (row_cum - 1) % patch_size
405
- block_idx = block_offset.unsqueeze(1) + chunk_idx
406
-
407
- image_embeds = torch.zeros_like(inputs_embeds)
408
- image_embeds[image_mask] = image_hidden_states[block_idx[image_mask], local_idx[image_mask], :]
409
-
410
- return torch.where(image_mask.unsqueeze(-1), image_embeds, inputs_embeds)
411
-
412
- @can_return_tuple
413
- @auto_docstring(
414
- custom_intro="""
415
- Inputs fed to the model can have an arbitrary number of images. To account for this, pixel_values fed to
416
- the model have image padding -> (batch_size, max_num_images, 3, max_heights, max_widths) where
417
- max_num_images is the maximum number of images among the batch_size samples in the batch.
418
- Padding images are not needed beyond padding the pixel_values at the entrance of the model.
419
- For efficiency, we only pass through the vision_model's forward the real images by
420
- discarding the padding images i.e. pixel_values of size (image_batch_size, 3, height, width) where
421
- image_batch_size would be 7 when num_images_per_sample=[1, 3, 1, 2] and max_num_images would be 3.
422
- """,
423
- checkpoint="modernvbert/ModernVBert",
424
- )
425
- def forward(
426
- self,
427
- input_ids: torch.LongTensor = None,
428
- attention_mask: Optional[torch.Tensor] = None,
429
- position_ids: Optional[torch.LongTensor] = None,
430
- inputs_embeds: Optional[torch.FloatTensor] = None,
431
- pixel_values: Optional[torch.FloatTensor] = None,
432
- pixel_attention_mask: Optional[torch.BoolTensor] = None,
433
- image_hidden_states: Optional[torch.FloatTensor] = None,
434
- output_attentions: Optional[bool] = None,
435
- output_hidden_states: Optional[bool] = None,
436
- return_dict: Optional[bool] = None,
437
- **kwargs: Unpack[FlashAttentionKwargs],
438
- ) -> Union[tuple, BaseModelOutputWithPoolingAndCrossAttentions]:
439
- r"""
440
- pixel_attention_mask (`torch.Tensor` of shape `(batch_size, image_size, image_size)`, *optional*):
441
- Mask to avoid performing attention on padding pixel indices.
442
- image_hidden_states (`torch.FloatTensor` of shape `(batch_size, num_channels, image_size, image_size)`):
443
- The hidden states of the image encoder after modality projection.
444
- labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*):
445
- Labels for computing the masked language modeling loss. Indices should either be in `[0, ...,
446
- config.vocab_size]` or `model.image_token_id`. Tokens with indices set to `model.image_token_id` are
447
- ignored (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`.
448
- """
449
- output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
450
- output_hidden_states = (
451
- output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
452
- )
453
- return_dict = return_dict if return_dict is not None else self.config.use_return_dict
454
-
455
- if inputs_embeds is None:
456
- inputs_embeds = self.text_model.get_input_embeddings()(input_ids).to(input_ids.device)
457
-
458
- # Images processing
459
- if pixel_values is not None:
460
- # Vision encoder pass
461
- image_hidden_states = self.get_image_features(
462
- pixel_values=pixel_values, pixel_attention_mask=pixel_attention_mask
463
- )
464
- # Modality projection & resampling
465
- image_hidden_states = self.connector(image_hidden_states)
466
-
467
- # Merge image and text embeddings
468
- if image_hidden_states is not None:
469
- image_hidden_states = image_hidden_states.to(dtype=self.dtype, device=inputs_embeds.device)
470
- inputs_embeds = self.inputs_merger(
471
- input_ids=input_ids, inputs_embeds=inputs_embeds, image_hidden_states=image_hidden_states
472
- )
473
-
474
- # Language model pass
475
- outputs = self.text_model(
476
- inputs_embeds=inputs_embeds,
477
- attention_mask=attention_mask,
478
- position_ids=position_ids,
479
- output_attentions=output_attentions,
480
- output_hidden_states=output_hidden_states,
481
- return_dict=return_dict,
482
- **kwargs,
483
- )
484
-
485
- return ModernVBertBaseModelOutput(
486
- last_hidden_state=outputs.last_hidden_state,
487
- hidden_states=outputs.hidden_states,
488
- attentions=outputs.attentions,
489
- image_hidden_states=image_hidden_states,
490
- )
491
-
492
-
493
- class ModernVBertLMHead(nn.Module):
494
- def __init__(self, config):
495
- super().__init__()
496
- pretrained_config = ModernBertConfig.from_pretrained(config.text_config.text_model_name)
497
- pretrained_model = ModernBertForMaskedLM(pretrained_config)
498
- self.head = pretrained_model.head
499
- self.decoder = pretrained_model.decoder
500
-
501
- def forward(self, hidden_states):
502
- return self.decoder(self.head(hidden_states))
503
-
504
-
505
- @auto_docstring
506
- class ModernVBertForMaskedLM(ModernVBertPreTrainedModel):
507
- _tied_weights_keys = ["lm_head.decoder.weight", "model.text_model.embeddings.word_embeddings.weight"]
508
-
509
- def __init__(self, config):
510
- super().__init__(config)
511
- self.in_features = config.hidden_size
512
- self.out_additional_features = config.additional_vocab_size
513
- self.vocab_size = config.vocab_size
514
- self.model = ModernVBertModel(config)
515
- self.lm_head = ModernVBertLMHead(config)
516
- if self.out_additional_features > 0:
517
- self.additional_fc = nn.Linear(self.in_features, self.out_additional_features, bias=False)
518
- self.lm_head.to(self.dtype)
519
- self.post_init()
520
-
521
- # Copied from transformers.models.idefics2.modeling_idefics2.Idefics2ForConditionalGeneration.disable_input_require_grads
522
- def disable_input_require_grads(self):
523
- self._text_require_grads_hook.remove()
524
- self._vision_require_grads_hook.remove()
525
-
526
- @can_return_tuple
527
- @auto_docstring(
528
- custom_intro="""
529
- Inputs fed to the model can have an arbitrary number of images. To account for this, pixel_values fed to
530
- the model have image padding -> (batch_size, max_num_images, 3, max_heights, max_widths) where
531
- max_num_images is the maximum number of images among the batch_size samples in the batch.
532
- Padding images are not needed beyond padding the pixel_values at the entrance of the model.
533
- For efficiency, we only pass through the vision_model's forward the real images by
534
- discarding the padding images i.e. pixel_values of size (image_batch_size, 3, height, width) where
535
- image_batch_size would be 7 when num_images_per_sample=[1, 3, 1, 2] and max_num_images would be 3.
536
- """,
537
- checkpoint="modernvbert/ModernVBert",
538
- )
539
- def forward(
540
- self,
541
- input_ids: torch.LongTensor = None,
542
- attention_mask: Optional[torch.Tensor] = None,
543
- position_ids: Optional[torch.LongTensor] = None,
544
- inputs_embeds: Optional[torch.FloatTensor] = None,
545
- pixel_values: Optional[torch.FloatTensor] = None,
546
- pixel_attention_mask: Optional[torch.BoolTensor] = None,
547
- image_hidden_states: Optional[torch.FloatTensor] = None,
548
- output_attentions: Optional[bool] = None,
549
- output_hidden_states: Optional[bool] = None,
550
- return_dict: Optional[bool] = None,
551
- labels: Optional[torch.LongTensor] = None,
552
- **kwargs: Unpack[FlashAttentionKwargs],
553
- ) -> Union[tuple, ModernVBertMaskedLMOutput]:
554
- r"""
555
- pixel_attention_mask (`torch.Tensor` of shape `(batch_size, image_size, image_size)`, *optional*):
556
- Mask to avoid performing attention on padding pixel indices.
557
- image_hidden_states (`torch.FloatTensor` of shape `(batch_size, num_channels, image_size, image_size)`):
558
- The hidden states of the image encoder after modality projection.
559
- labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*):
560
- Labels for computing the masked language modeling loss. Indices should either be in `[0, ...,
561
- config.vocab_size]` or `model.image_token_id`. Tokens with indices set to `model.image_token_id` are
562
- ignored (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`.
563
- """
564
-
565
- output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
566
- output_hidden_states = (
567
- output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
568
- )
569
- return_dict = return_dict if return_dict is not None else self.config.use_return_dict
570
-
571
- outputs = self.model(
572
- input_ids=input_ids,
573
- attention_mask=attention_mask,
574
- position_ids=position_ids,
575
- inputs_embeds=inputs_embeds,
576
- pixel_values=pixel_values,
577
- pixel_attention_mask=pixel_attention_mask,
578
- image_hidden_states=image_hidden_states,
579
- output_attentions=output_attentions,
580
- output_hidden_states=output_hidden_states,
581
- return_dict=return_dict,
582
- **kwargs,
583
- )
584
- hidden_states = outputs[0]
585
-
586
- logits = self.lm_head(hidden_states)
587
-
588
- if self.out_additional_features > 0:
589
- proj_states = self.lm_head.head(hidden_states)
590
- additional_features = self.additional_fc(proj_states)
591
- logits = torch.cat((logits, additional_features), -1)
592
-
593
- loss = None
594
- if labels is not None:
595
- loss = CrossEntropyLoss()(logits.view(-1, self.vocab_size + self.out_additional_features), labels.view(-1))
596
-
597
- if not return_dict:
598
- output = (logits,) + outputs[2:]
599
- return ((loss,) + output) if loss is not None else output
600
-
601
- return ModernVBertMaskedLMOutput(
602
- loss=loss,
603
- logits=logits.float(),
604
- hidden_states=outputs.hidden_states,
605
- attentions=outputs.attentions,
606
- image_hidden_states=outputs.image_hidden_states,
607
- )
608
-
609
-
610
- __all__ = ["ModernVBertPreTrainedModel", "ModernVBertModel", "ModernVBertForMaskedLM"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modeling_st_vsplade.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sentence Transformers module for the V-SPLADE inference-free query encoder.
2
+
3
+ Referenced from ``router_config.json``: the "query" route uses
4
+ :class:`VSPLADEStaticEmbedding`, whose weights are the precomputed Li-LSR
5
+ lookup table ``softplus(projection(embedding))`` extracted from the
6
+ ``query_encoder.*`` tensors in ``model.safetensors`` (with the special tokens
7
+ [UNK]/[CLS]/[SEP]/[PAD]/[MASK] zeroed out).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import torch
13
+
14
+ try:
15
+ # sentence-transformers >= 5.6
16
+ from sentence_transformers.sparse_encoder.modules import SparseStaticEmbedding
17
+ except ImportError:
18
+ from sentence_transformers.sparse_encoder.models import SparseStaticEmbedding
19
+
20
+
21
+ class VSPLADEStaticEmbedding(SparseStaticEmbedding):
22
+ """Inference-free Li-LSR query encoder for V-SPLADE.
23
+
24
+ Behaves like :class:`SparseStaticEmbedding` with two differences, matching
25
+ ``InferenceFreeQueryEncoder.encode_with_lookup`` from
26
+ https://github.com/naver/v-splade:
27
+
28
+ * repeated query tokens accumulate their weight (scatter-add) instead of
29
+ being counted once;
30
+ * token ids outside the lookup table (the 40 added vision tokens, e.g.
31
+ ``<image>``) contribute nothing instead of raising an index error;
32
+ * the lookup table covers the base (MLM) vocabulary (50368 entries), which
33
+ is smaller than the full tokenizer vocabulary, so its size is stored in
34
+ the module config (``num_dimensions``) for loading.
35
+ """
36
+
37
+ config_keys: list[str] = ["frozen", "num_dimensions"]
38
+
39
+ def __init__(self, tokenizer, weight: torch.Tensor | None = None, frozen: bool = False, num_dimensions: int | None = None):
40
+ if weight is None and num_dimensions is not None:
41
+ weight = torch.zeros(num_dimensions)
42
+ super().__init__(tokenizer=tokenizer, weight=weight, frozen=frozen)
43
+
44
+ def forward(self, features: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
45
+ input_ids = features["input_ids"]
46
+ attention_mask = features["attention_mask"]
47
+
48
+ valid = (input_ids < self.num_dimensions) & (attention_mask > 0)
49
+ safe_ids = input_ids.clamp(max=self.num_dimensions - 1)
50
+ scores = self.weight[safe_ids] * valid.to(self.weight.dtype)
51
+
52
+ embeddings = torch.zeros(
53
+ input_ids.size(0), self.num_dimensions, device=input_ids.device, dtype=self.weight.dtype
54
+ )
55
+ embeddings.scatter_add_(1, safe_ids, scores)
56
+
57
+ features["sentence_embedding"] = embeddings
58
+ return features
59
+
60
+
61
+ __all__ = ["VSPLADEStaticEmbedding"]
modeling_vsplade.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """V-SPLADE document encoder for Hugging Face Transformers.
2
+
3
+ Wraps the ModernVBERT backbone (``transformers>=5.3.0``) together with the
4
+ V-SPLADE MLM head so that this repository loads directly with
5
+ ``AutoModelForMaskedLM.from_pretrained(..., trust_remote_code=True)``.
6
+
7
+ The module tree deliberately mirrors the checkpoint layout of the V-SPLADE
8
+ export (``encoder.encoder.model.*`` for the backbone, ``encoder.mlm_head.*``
9
+ for the sparse head), so ``model.safetensors`` loads without any key
10
+ remapping. The ``query_encoder.*`` tensors hold the inference-free Li-LSR
11
+ query lookup (used by the Sentence Transformers integration) and are not part
12
+ of the document encoder, so they are ignored here.
13
+
14
+ The returned ``logits`` are the SPLADE term logits: MLM logits scaled by
15
+ ``hidden_size ** -0.25`` with special tokens masked out, exactly as in
16
+ https://github.com/naver/v-splade (``UnifiedRetriever._apply_sparse_head``).
17
+ A sparse document embedding is obtained via ``log1p(relu(logits))`` followed
18
+ by a max-pool over the sequence dimension (see the README).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import torch
24
+ import torch.nn.functional as F
25
+ from torch import nn
26
+ from transformers.modeling_outputs import MaskedLMOutput
27
+
28
+ try:
29
+ from transformers.models.modernvbert.configuration_modernvbert import ModernVBertConfig
30
+ from transformers.models.modernvbert.modeling_modernvbert import (
31
+ ModernVBertModel,
32
+ ModernVBertPreTrainedModel,
33
+ )
34
+ except ImportError as exc:
35
+ raise ImportError(
36
+ "V-SPLADE requires the ModernVBERT architecture, which is available in "
37
+ "transformers>=5.3.0. Please upgrade with `pip install -U transformers`."
38
+ ) from exc
39
+
40
+ # Special tokens that are masked out of the sparse representation:
41
+ # [UNK], [CLS], [SEP], [PAD], [MASK]
42
+ SPECIAL_TOKEN_IDS = [50280, 50281, 50282, 50283, 50284]
43
+
44
+
45
+ class VSPLADEDecoupledEmbedding(nn.Embedding):
46
+ """Word embeddings split into the base vocabulary and the added vision tokens.
47
+
48
+ Matches the V-SPLADE export layout: ``weight`` holds the base (MLM) vocabulary
49
+ and ``additional_embedding.weight`` holds the extra tokens appended for the
50
+ vision chat format (``<image>``, ``<end_of_utterance>``, tile markers, ...).
51
+ """
52
+
53
+ def __init__(self, num_embeddings: int, num_additional_embeddings: int, embedding_dim: int, **kwargs) -> None:
54
+ super().__init__(num_embeddings, embedding_dim, **kwargs)
55
+ self.num_additional_embeddings = num_additional_embeddings
56
+ self.additional_embedding = nn.Embedding(num_additional_embeddings, embedding_dim)
57
+
58
+ def forward(self, input_ids: torch.LongTensor) -> torch.Tensor:
59
+ input_ids = input_ids.clone()
60
+ additional_vocab_indices = torch.where(input_ids >= self.num_embeddings)
61
+ additional_embeddings = self.additional_embedding(input_ids[additional_vocab_indices] - self.num_embeddings)
62
+ input_ids[additional_vocab_indices] = 0
63
+ full_vector = F.embedding(input_ids, self.weight)
64
+ full_vector[additional_vocab_indices] = additional_embeddings
65
+ return full_vector
66
+
67
+
68
+ class VSPLADEModalityProjection(nn.Module):
69
+ """Vision-to-text projection stored as ``modality_projection.proj`` in the export."""
70
+
71
+ def __init__(self, input_size: int, output_size: int) -> None:
72
+ super().__init__()
73
+ self.proj = nn.Linear(input_size, output_size, bias=False)
74
+
75
+ @property
76
+ def weight(self) -> torch.Tensor:
77
+ # ModernVBertPreTrainedModel._init_weights initializes ``modality_projection.weight``
78
+ return self.proj.weight
79
+
80
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
81
+ return self.proj(hidden_states)
82
+
83
+
84
+ class VSPLADEMLMHead(nn.Module):
85
+ """V-SPLADE MLM head: dense -> GELU -> LayerNorm -> decoder (base vocabulary)."""
86
+
87
+ def __init__(self, hidden_size: int, vocab_size: int) -> None:
88
+ super().__init__()
89
+ self.dense = nn.Linear(hidden_size, hidden_size)
90
+ self.norm = nn.LayerNorm(hidden_size)
91
+ self.decoder = nn.Linear(hidden_size, vocab_size)
92
+
93
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
94
+ return self.decoder(self.norm(F.gelu(self.dense(hidden_states))))
95
+
96
+
97
+ class _Wrapper(nn.Module):
98
+ """Empty container used to mirror the checkpoint's key prefixes."""
99
+
100
+
101
+ class VSPLADEForMaskedLM(ModernVBertPreTrainedModel):
102
+ config_class = ModernVBertConfig
103
+ _keys_to_ignore_on_load_unexpected = [r"query_encoder\..*"]
104
+
105
+ def __init__(self, config: ModernVBertConfig) -> None:
106
+ super().__init__(config)
107
+ main_vocab_size = config.text_config.vocab_size - config.additional_vocab_size
108
+
109
+ backbone = ModernVBertModel(config)
110
+ # The export stores the connector projection under an extra ``proj`` level; mirror that.
111
+ backbone.connector.modality_projection = VSPLADEModalityProjection(
112
+ input_size=config.vision_config.hidden_size * (config.pixel_shuffle_factor**2),
113
+ output_size=config.text_config.hidden_size,
114
+ )
115
+ # The export splits the embedding into base + additional tokens; mirror that.
116
+ backbone.text_model.set_input_embeddings(
117
+ VSPLADEDecoupledEmbedding(
118
+ num_embeddings=main_vocab_size,
119
+ num_additional_embeddings=config.additional_vocab_size,
120
+ embedding_dim=config.text_config.hidden_size,
121
+ padding_idx=getattr(config, "pad_token_id", None),
122
+ )
123
+ )
124
+
125
+ self.encoder = _Wrapper()
126
+ self.encoder.encoder = _Wrapper()
127
+ self.encoder.encoder.model = backbone
128
+ self.encoder.mlm_head = VSPLADEMLMHead(config.text_config.hidden_size, main_vocab_size)
129
+
130
+ self.logit_scale = config.text_config.hidden_size**-0.25
131
+
132
+ self.post_init()
133
+
134
+ def get_input_embeddings(self):
135
+ return self.encoder.encoder.model.get_input_embeddings()
136
+
137
+ def set_input_embeddings(self, value):
138
+ self.encoder.encoder.model.set_input_embeddings(value)
139
+
140
+ def forward(
141
+ self,
142
+ input_ids: torch.LongTensor | None = None,
143
+ attention_mask: torch.Tensor | None = None,
144
+ position_ids: torch.LongTensor | None = None,
145
+ inputs_embeds: torch.FloatTensor | None = None,
146
+ pixel_values: torch.FloatTensor | None = None,
147
+ pixel_attention_mask: torch.BoolTensor | None = None,
148
+ image_hidden_states: torch.FloatTensor | None = None,
149
+ return_dict: bool | None = None,
150
+ ) -> MaskedLMOutput:
151
+ outputs = self.encoder.encoder.model(
152
+ input_ids=input_ids,
153
+ attention_mask=attention_mask,
154
+ position_ids=position_ids,
155
+ inputs_embeds=inputs_embeds,
156
+ pixel_values=pixel_values,
157
+ pixel_attention_mask=pixel_attention_mask,
158
+ image_hidden_states=image_hidden_states,
159
+ return_dict=True,
160
+ )
161
+ logits = self.encoder.mlm_head(outputs.last_hidden_state) * self.logit_scale
162
+ # Zero out special tokens so they never activate in the sparse representation
163
+ # (log1p(relu(0)) == 0), matching the reference special_token_mask.
164
+ # Built on the fly: buffers created in __init__ do not survive meta-device loading.
165
+ special_token_ids = torch.tensor(SPECIAL_TOKEN_IDS, dtype=torch.long, device=logits.device)
166
+ logits = logits.index_fill(-1, special_token_ids, 0.0)
167
+
168
+ return MaskedLMOutput(
169
+ logits=logits,
170
+ hidden_states=outputs.hidden_states,
171
+ attentions=outputs.attentions,
172
+ )
173
+
174
+
175
+ __all__ = ["VSPLADEForMaskedLM"]
modules.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "idx": 0,
4
+ "name": "0",
5
+ "path": "",
6
+ "type": "sentence_transformers.base.modules.router.Router"
7
+ }
8
+ ]
query_0_VSPLADEStaticEmbedding/config.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "frozen": true,
3
+ "num_dimensions": 50368
4
+ }
query_0_VSPLADEStaticEmbedding/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:64b26473d8e2a4250e2f0ea6fca51d503fd04b799502d2248fd28ac98616a9a0
3
+ size 201552
query_0_VSPLADEStaticEmbedding/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
query_0_VSPLADEStaticEmbedding/tokenizer_config.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backend": "tokenizers",
3
+ "clean_up_tokenization_spaces": true,
4
+ "cls_token": "[CLS]",
5
+ "is_local": true,
6
+ "legacy": false,
7
+ "local_files_only": false,
8
+ "mask_token": "[MASK]",
9
+ "model_input_names": [
10
+ "input_ids",
11
+ "attention_mask",
12
+ "pixel_values",
13
+ "pixel_attention_mask"
14
+ ],
15
+ "model_max_length": 8192,
16
+ "pad_token": "[PAD]",
17
+ "sep_token": "[SEP]",
18
+ "tokenizer_class": "TokenizersBackend",
19
+ "unk_token": "[UNK]"
20
+ }
router_config.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "types": {
3
+ "query_0_VSPLADEStaticEmbedding": "modeling_st_vsplade.VSPLADEStaticEmbedding",
4
+ "": "sentence_transformers.base.modules.transformer.Transformer",
5
+ "document_1_SpladePooling": "sentence_transformers.sparse_encoder.modules.splade_pooling.SpladePooling"
6
+ },
7
+ "structure": {
8
+ "query": [
9
+ "query_0_VSPLADEStaticEmbedding"
10
+ ],
11
+ "document": [
12
+ "",
13
+ "document_1_SpladePooling"
14
+ ]
15
+ },
16
+ "parameters": {
17
+ "default_route": "document",
18
+ "allow_empty_key": true,
19
+ "route_mappings": {}
20
+ }
21
+ }
sentence_bert_config.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "transformer_task": "fill-mask",
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
+ "image+text": {
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": "token_embeddings",
23
+ "processing_kwargs": {
24
+ "chat_template": {
25
+ "chat_template": "sentence_transformers"
26
+ }
27
+ }
28
+ }