multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
6aa8fa0 verified
Raw
History Blame Contribute Delete
6.37 kB
# V-SPLADE
# Copyright (c) 2026-present NAVER Corp.
# Apache-2.0
"""
V_SPLADE modular components.
A V_SPLADE retriever is composed of:
Encoder + Pooling + SparseHead + (BOW | Li-LSR) QueryEncoder + Losses
"""
from pathlib import Path
import torch
from models.model import UnifiedRetriever, RetrievalOutput, compute_logits
from models.encoder import EncoderType, build_encoder
from models.pooling import PoolingType, Pooling
from models.head import HeadType, build_head, SparseHead
from models.query_encoder import (
QueryEncoderType,
build_query_encoder,
BOWQueryEncoder,
InferenceFreeQueryEncoder,
)
from models.losses import FLOPSLoss, NCELoss, CaptionPushUpLoss
DEFAULT_POOLING = {
"vbert": "max",
}
def build_model(
path: str = None,
mode: str = "inference_only",
*,
encoder_type: str = "vbert",
head_type: str = "sparse",
query_encoder_type: str = "li_lsr",
pooling_type: str = None,
query_lsr_lora_r: int = 0,
query_lsr_activation: str = "softplus",
dtype: torch.dtype = torch.bfloat16,
**kwargs,
) -> UnifiedRetriever:
"""Factory: build a V-SPLADE retriever in one of two modes.
``mode='inference_only'`` (default):
``path`` is a V-SPLADE HF export directory containing
``model.safetensors`` + ``config.json``. The retriever is constructed
as an empty shell and every weight (backbone + SPLADE head + Li-LSR
query head) is dispatched from the export in a single pass. No base
model download, no LoRA wrapping.
``mode='from_scratch'``:
``path`` is the base BiModernVBert backbone directory (e.g. the
canonical ``ModernVBERT/modernvbert`` checkpoint). The retriever is
built for training β€” encoder/LM-head LoRA, fresh query head,
loss/regularizer hooks. Extra ``**kwargs`` are forwarded to
:class:`UnifiedRetriever`.
"""
if mode == "inference_only":
if path is None:
raise ValueError("inference_only mode requires path= to the HF export dir")
model = UnifiedRetriever.from_hf_export(
path,
query_lsr_activation=query_lsr_activation,
dtype=dtype,
)
load_hf_export(model, path, dtype=dtype)
return model
if mode == "from_scratch":
if pooling_type is None:
pooling_type = DEFAULT_POOLING.get(encoder_type, "max")
if path is not None:
kwargs.setdefault("model_name", path)
return UnifiedRetriever(
encoder_type=encoder_type,
pooling_type=pooling_type,
head_type=head_type,
query_encoder_type=query_encoder_type,
query_lsr_lora_r=query_lsr_lora_r,
query_lsr_activation=query_lsr_activation,
**kwargs,
)
raise ValueError(f"Unknown mode: {mode!r}. Choose 'inference_only' or 'from_scratch'.")
def _resolve_export_file(hf_dir: str, filename: str) -> str:
"""Resolve a file from a V-SPLADE HF export given as a local dir or Hub id.
Returns a local filesystem path: the file under ``hf_dir`` if it exists,
otherwise the result of downloading ``filename`` from the Hub repo
``hf_dir`` (cached by huggingface_hub).
"""
local = Path(hf_dir) / filename
if local.is_file():
return str(local)
from huggingface_hub import hf_hub_download
return hf_hub_download(repo_id=str(hf_dir), filename=filename)
def load_hf_export(model: UnifiedRetriever, hf_dir: str,
dtype: torch.dtype = torch.bfloat16) -> None:
"""Load a V-SPLADE HF export ``model.safetensors`` into ``model``.
Dispatches the export's three logical slices to the right sub-modules
by stripping the training-wrapper prefix:
encoder.encoder.model.* -> model.encoder.encoder.model.*
encoder.mlm_head.* -> model.encoder.mlm_head.*
query_encoder.* -> model.query_encoder.*
``hf_dir`` may be a local directory or a HuggingFace Hub repo id; in the
latter case ``model.safetensors`` is downloaded automatically.
Raises if any safetensors tensor is not consumed by these three slices.
"""
from safetensors.torch import load_file
full_sd = load_file(_resolve_export_file(hf_dir, "model.safetensors"))
# Remap keys to match native transformers 5.x ModernVBertModel structure:
# 1. connector.modality_projection.proj.weight β†’ connector.modality_projection.weight
# 2. vision_model.embeddings.* β†’ vision_model.vision_model.embeddings.*
# 3. vision_model.encoder.* β†’ vision_model.vision_model.encoder.*
# 4. vision_model.head.* β†’ vision_model.vision_model.head.*
# 5. vision_model.post_layernorm.* β†’ vision_model.vision_model.post_layernorm.*
remapped = {}
for k, v in full_sd.items():
new_k = k
if "connector.modality_projection.proj." in new_k:
new_k = new_k.replace("connector.modality_projection.proj.",
"connector.modality_projection.")
if ".vision_model.embeddings." in new_k:
new_k = new_k.replace(".vision_model.embeddings.", ".vision_model.vision_model.embeddings.")
elif ".vision_model.encoder." in new_k:
new_k = new_k.replace(".vision_model.encoder.", ".vision_model.vision_model.encoder.")
elif ".vision_model.head." in new_k:
new_k = new_k.replace(".vision_model.head.", ".vision_model.vision_model.head.")
elif ".vision_model.post_layernorm." in new_k:
new_k = new_k.replace(".vision_model.post_layernorm.", ".vision_model.vision_model.post_layernorm.")
remapped[new_k] = v
full_sd = remapped
dispatch = [
(model.encoder, "encoder."),
(model.query_encoder, "query_encoder."),
]
consumed = set()
for module, prefix in dispatch:
slice_sd = {k[len(prefix):]: v.to(dtype)
for k, v in full_sd.items() if k.startswith(prefix)}
module.load_state_dict(slice_sd, strict=False)
consumed.update(prefix + k for k in slice_sd)
leftover = set(full_sd) - consumed
if leftover:
raise RuntimeError(
f"{len(leftover)} tensor(s) in {hf_dir}/model.safetensors were not "
f"dispatched to any sub-module. First few: {sorted(leftover)[:3]}"
)