Feature Extraction
sentence-transformers
Safetensors
English
modernvbert
sparse-retrieval
splade
visual-document-retrieval
multimodal
information-retrieval
inference-free
sparse-encoder
custom_code
Instructions to use naver/v-splade-quality with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- sentence-transformers
How to use naver/v-splade-quality with sentence-transformers:
from sentence_transformers import SentenceTransformer model = SentenceTransformer("naver/v-splade-quality", trust_remote_code=True) sentences = [ "The weather is lovely today.", "It's so sunny outside!", "He drove to the stadium." ] embeddings = model.encode(sentences) similarities = model.similarity(embeddings, embeddings) print(similarities.shape) # [3, 3] - Notebooks
- Google Colab
- Kaggle
| """V-SPLADE document encoder for Hugging Face Transformers. | |
| Wraps the ModernVBERT backbone (``transformers>=5.3.0``) together with the | |
| V-SPLADE MLM head so that this repository loads directly with | |
| ``AutoModelForMaskedLM.from_pretrained(..., trust_remote_code=True)``. | |
| The module tree deliberately mirrors the checkpoint layout of the V-SPLADE | |
| export (``encoder.encoder.model.*`` for the backbone, ``encoder.mlm_head.*`` | |
| for the sparse head), so ``model.safetensors`` loads without any key | |
| remapping. The ``query_encoder.*`` tensors hold the inference-free Li-LSR | |
| query lookup (used by the Sentence Transformers integration) and are not part | |
| of the document encoder, so they are ignored here. | |
| The returned ``logits`` are the SPLADE term logits: MLM logits scaled by | |
| ``hidden_size ** -0.25`` with special tokens masked out, exactly as in | |
| https://github.com/naver/v-splade (``UnifiedRetriever._apply_sparse_head``). | |
| A sparse document embedding is obtained via ``log1p(relu(logits))`` followed | |
| by a max-pool over the sequence dimension (see the README). | |
| """ | |
| from __future__ import annotations | |
| import torch | |
| import torch.nn.functional as F | |
| from torch import nn | |
| from transformers.modeling_outputs import MaskedLMOutput | |
| try: | |
| from transformers.models.modernvbert.configuration_modernvbert import ModernVBertConfig | |
| from transformers.models.modernvbert.modeling_modernvbert import ( | |
| ModernVBertModel, | |
| ModernVBertPreTrainedModel, | |
| ) | |
| except ImportError as exc: | |
| raise ImportError( | |
| "V-SPLADE requires the ModernVBERT architecture, which is available in " | |
| "transformers>=5.3.0. Please upgrade with `pip install -U transformers`." | |
| ) from exc | |
| # Special tokens that are masked out of the sparse representation: | |
| # [UNK], [CLS], [SEP], [PAD], [MASK] | |
| SPECIAL_TOKEN_IDS = [50280, 50281, 50282, 50283, 50284] | |
| class VSPLADEDecoupledEmbedding(nn.Embedding): | |
| """Word embeddings split into the base vocabulary and the added vision tokens. | |
| Matches the V-SPLADE export layout: ``weight`` holds the base (MLM) vocabulary | |
| and ``additional_embedding.weight`` holds the extra tokens appended for the | |
| vision chat format (``<image>``, ``<end_of_utterance>``, tile markers, ...). | |
| """ | |
| def __init__(self, num_embeddings: int, num_additional_embeddings: int, embedding_dim: int, **kwargs) -> None: | |
| super().__init__(num_embeddings, embedding_dim, **kwargs) | |
| self.num_additional_embeddings = num_additional_embeddings | |
| self.additional_embedding = nn.Embedding(num_additional_embeddings, embedding_dim) | |
| def forward(self, input_ids: torch.LongTensor) -> torch.Tensor: | |
| input_ids = input_ids.clone() | |
| additional_vocab_indices = torch.where(input_ids >= self.num_embeddings) | |
| additional_embeddings = self.additional_embedding(input_ids[additional_vocab_indices] - self.num_embeddings) | |
| input_ids[additional_vocab_indices] = 0 | |
| full_vector = F.embedding(input_ids, self.weight) | |
| full_vector[additional_vocab_indices] = additional_embeddings | |
| return full_vector | |
| class VSPLADEModalityProjection(nn.Module): | |
| """Vision-to-text projection stored as ``modality_projection.proj`` in the export.""" | |
| def __init__(self, input_size: int, output_size: int) -> None: | |
| super().__init__() | |
| self.proj = nn.Linear(input_size, output_size, bias=False) | |
| def weight(self) -> torch.Tensor: | |
| # ModernVBertPreTrainedModel._init_weights initializes ``modality_projection.weight`` | |
| return self.proj.weight | |
| def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: | |
| return self.proj(hidden_states) | |
| class VSPLADEMLMHead(nn.Module): | |
| """V-SPLADE MLM head: dense -> GELU -> LayerNorm -> decoder (base vocabulary).""" | |
| def __init__(self, hidden_size: int, vocab_size: int) -> None: | |
| super().__init__() | |
| self.dense = nn.Linear(hidden_size, hidden_size) | |
| self.norm = nn.LayerNorm(hidden_size) | |
| self.decoder = nn.Linear(hidden_size, vocab_size) | |
| def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: | |
| return self.decoder(self.norm(F.gelu(self.dense(hidden_states)))) | |
| class _Wrapper(nn.Module): | |
| """Empty container used to mirror the checkpoint's key prefixes.""" | |
| class VSPLADEForMaskedLM(ModernVBertPreTrainedModel): | |
| config_class = ModernVBertConfig | |
| _keys_to_ignore_on_load_unexpected = [r"query_encoder\..*"] | |
| def __init__(self, config: ModernVBertConfig) -> None: | |
| super().__init__(config) | |
| main_vocab_size = config.text_config.vocab_size - config.additional_vocab_size | |
| backbone = ModernVBertModel(config) | |
| # The export stores the connector projection under an extra ``proj`` level; mirror that. | |
| backbone.connector.modality_projection = VSPLADEModalityProjection( | |
| input_size=config.vision_config.hidden_size * (config.pixel_shuffle_factor**2), | |
| output_size=config.text_config.hidden_size, | |
| ) | |
| # The export splits the embedding into base + additional tokens; mirror that. | |
| backbone.text_model.set_input_embeddings( | |
| VSPLADEDecoupledEmbedding( | |
| num_embeddings=main_vocab_size, | |
| num_additional_embeddings=config.additional_vocab_size, | |
| embedding_dim=config.text_config.hidden_size, | |
| padding_idx=getattr(config, "pad_token_id", None), | |
| ) | |
| ) | |
| self.encoder = _Wrapper() | |
| self.encoder.encoder = _Wrapper() | |
| self.encoder.encoder.model = backbone | |
| self.encoder.mlm_head = VSPLADEMLMHead(config.text_config.hidden_size, main_vocab_size) | |
| self.logit_scale = config.text_config.hidden_size**-0.25 | |
| self.post_init() | |
| def get_input_embeddings(self): | |
| return self.encoder.encoder.model.get_input_embeddings() | |
| def set_input_embeddings(self, value): | |
| self.encoder.encoder.model.set_input_embeddings(value) | |
| def forward( | |
| self, | |
| input_ids: torch.LongTensor | None = None, | |
| attention_mask: torch.Tensor | None = None, | |
| position_ids: torch.LongTensor | None = None, | |
| inputs_embeds: torch.FloatTensor | None = None, | |
| pixel_values: torch.FloatTensor | None = None, | |
| pixel_attention_mask: torch.BoolTensor | None = None, | |
| image_hidden_states: torch.FloatTensor | None = None, | |
| return_dict: bool | None = None, | |
| ) -> MaskedLMOutput: | |
| outputs = self.encoder.encoder.model( | |
| input_ids=input_ids, | |
| attention_mask=attention_mask, | |
| position_ids=position_ids, | |
| inputs_embeds=inputs_embeds, | |
| pixel_values=pixel_values, | |
| pixel_attention_mask=pixel_attention_mask, | |
| image_hidden_states=image_hidden_states, | |
| return_dict=True, | |
| ) | |
| logits = self.encoder.mlm_head(outputs.last_hidden_state) * self.logit_scale | |
| # Zero out special tokens so they never activate in the sparse representation | |
| # (log1p(relu(0)) == 0), matching the reference special_token_mask. | |
| # Built on the fly: buffers created in __init__ do not survive meta-device loading. | |
| special_token_ids = torch.tensor(SPECIAL_TOKEN_IDS, dtype=torch.long, device=logits.device) | |
| logits = logits.index_fill(-1, special_token_ids, 0.0) | |
| return MaskedLMOutput( | |
| logits=logits, | |
| hidden_states=outputs.hidden_states, | |
| attentions=outputs.attentions, | |
| ) | |
| __all__ = ["VSPLADEForMaskedLM"] | |