multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
6b3a024 verified
Raw
History Blame Contribute Delete
10.3 kB
# V-SPLADE
# Copyright (c) 2026-present NAVER Corp.
# Apache-2.0
"""
Backbone conversion: upstream ModernVBERT -> V-SPLADE-compatible layout.
The public ``ModernVBERT/modernvbert`` checkpoint stores its embeddings/LM-head
in a layout the decoupled-embedding model can't load (combined 50408 embedding,
plain LM head, double-nested vision keys, no decoupled-vocab config). This
module re-packages it.
It is used two ways:
* ``ensure_compatible_backbone(ref)`` β€” called automatically by the training
loader; converts on the fly and caches, so ``from_scratch`` "just works"
with the upstream Hub id.
* ``scripts/convert_modernvbert_backbone.py`` β€” a thin CLI over the same code.
Verified transform (every tensor reproduced bit-identically from upstream):
tok_embeddings.weight <- upstream[:50368]
tok_embeddings.additional_embedding.weight <- upstream[50368:50408]
lm_head.decoder.weight / .bias <- upstream lm_head.weight/bias[:50368]
additional_fc.weight <- upstream lm_head.weight[50368:50408]
lm_head.head.dense.weight <- upstream projection_head.dense.weight
lm_head.head.norm.weight <- upstream projection_head.norm.weight
model.vision_model.X <- upstream model.vision_model.vision_model.X
model.connector.modality_projection.proj.weight <- upstream ...modality_projection.weight
(all other model.text_model.* keys copied unchanged)
"""
import json
import os
import shutil
from pathlib import Path
import torch
from safetensors.torch import load_file, save_file
MAIN_VOCAB = 50368
FULL_VOCAB = 50408
ADDITIONAL_VOCAB = FULL_VOCAB - MAIN_VOCAB # 40
UPSTREAM_REPO = "ModernVBERT/modernvbert"
TOKENIZER_REPO = "jhu-clsp/ettin-encoder-150m"
PREPROCESSOR_CONFIG = {
"do_convert_rgb": True, "do_image_splitting": True, "do_normalize": True,
"do_pad": True, "do_rescale": True, "do_resize": True,
"image_mean": [0.5, 0.5, 0.5], "image_std": [0.5, 0.5, 0.5],
"image_processor_type": "Idefics3ImageProcessor",
"processor_class": "Idefics3Processor",
"max_image_size": {"longest_edge": 512}, "resample": 1,
"rescale_factor": 0.00392156862745098, "size": {"longest_edge": 2048},
}
PROCESSOR_CONFIG = {"image_seq_len": 64, "processor_class": "Idefics3Processor"}
# ──────────────────────────────────────────────────────────────────────────────
# Core transform
# ──────────────────────────────────────────────────────────────────────────────
def transform_state_dict(u: dict) -> dict:
"""Apply the verified upstream -> V-SPLADE backbone weight recipe."""
out = {}
tok = u["model.text_model.embeddings.tok_embeddings.weight"]
assert tok.shape[0] == FULL_VOCAB, f"unexpected embedding rows: {tuple(tok.shape)}"
out["model.text_model.embeddings.tok_embeddings.weight"] = tok[:MAIN_VOCAB].clone()
out["model.text_model.embeddings.tok_embeddings.additional_embedding.weight"] = \
tok[MAIN_VOCAB:FULL_VOCAB].clone()
lw, lb = u["lm_head.weight"], u["lm_head.bias"]
out["lm_head.decoder.weight"] = lw[:MAIN_VOCAB].clone()
out["lm_head.decoder.bias"] = lb[:MAIN_VOCAB].clone()
out["additional_fc.weight"] = lw[MAIN_VOCAB:FULL_VOCAB].clone()
out["lm_head.head.dense.weight"] = u["projection_head.dense.weight"].clone()
out["lm_head.head.norm.weight"] = u["projection_head.norm.weight"].clone()
out["model.connector.modality_projection.proj.weight"] = \
u["model.connector.modality_projection.weight"].clone()
vp = "model.vision_model.vision_model."
for k, v in u.items():
if k.startswith(vp):
out["model.vision_model." + k[len(vp):]] = v.clone()
for k, v in u.items():
if k.startswith("model.text_model.") and "tok_embeddings" not in k:
out[k] = v.clone()
return out
def build_config(u_cfg: dict) -> dict:
"""Patch the upstream config into the decoupled-embedding V-SPLADE layout."""
cfg = json.loads(json.dumps(u_cfg))
cfg["vocab_size"] = MAIN_VOCAB
cfg["additional_vocab_size"] = ADDITIONAL_VOCAB
cfg["freeze_config"] = {
"freeze_lm_head": True, "freeze_text_layers": True, "freeze_vision_layers": True,
}
cfg.setdefault("architectures", ["BiModernVBert"])
if "text_config" in cfg:
cfg["text_config"]["vocab_size"] = MAIN_VOCAB
return cfg
def is_compatible_config(cfg: dict) -> bool:
"""True if the config already uses the decoupled-embedding V-SPLADE layout."""
return cfg.get("freeze_config") is not None and cfg.get("additional_vocab_size") is not None
# ──────────────────────────────────────────────────────────────────────────────
# Conversion + auto-ensure
# ──────────────────────────────────────────────────────────────────────────────
def _load_config(ref: str) -> dict:
if os.path.isdir(ref):
return json.load(open(os.path.join(ref, "config.json")))
from huggingface_hub import hf_hub_download
return json.load(open(hf_hub_download(ref, "config.json")))
def convert_backbone(ref: str, out_dir, tokenizer_repo: str = TOKENIZER_REPO,
with_tokenizer: bool = True) -> str:
"""Convert backbone ``ref`` (Hub id or local dir) into ``out_dir``. Returns out_dir."""
out = Path(out_dir); out.mkdir(parents=True, exist_ok=True)
if os.path.isdir(ref):
sd_path = os.path.join(ref, "model.safetensors")
cfg = json.load(open(os.path.join(ref, "config.json")))
else:
from huggingface_hub import hf_hub_download
sd_path = hf_hub_download(ref, "model.safetensors")
cfg = json.load(open(hf_hub_download(ref, "config.json")))
save_file(transform_state_dict(load_file(sd_path)),
str(out / "model.safetensors"), metadata={"format": "pt"})
json.dump(build_config(cfg), open(out / "config.json", "w"), indent=2)
if with_tokenizer:
from huggingface_hub import hf_hub_download
for fn in ["tokenizer.json", "tokenizer_config.json", "special_tokens_map.json"]:
try:
shutil.copy2(hf_hub_download(tokenizer_repo, fn), out / fn)
except Exception:
pass
json.dump(PREPROCESSOR_CONFIG, open(out / "preprocessor_config.json", "w"), indent=2)
json.dump(PROCESSOR_CONFIG, open(out / "processor_config.json", "w"), indent=2)
return str(out)
def _cache_root() -> Path:
root = os.environ.get("VSPLADE_BACKBONE_CACHE")
if root:
return Path(root)
return Path.home() / ".cache" / "v-splade" / "backbones"
def ensure_compatible_backbone(ref: str, tokenizer_repo: str = TOKENIZER_REPO,
verbose: bool = True) -> str:
"""Return a local path to a V-SPLADE-compatible backbone for ``ref``.
If ``ref`` already uses the decoupled layout, it is returned unchanged
(``from_pretrained`` will download a Hub id as usual). Otherwise the upstream
checkpoint is converted once into a cache directory and that path is returned,
so ``from_scratch`` training works directly from the raw upstream Hub id.
"""
try:
cfg = _load_config(ref)
except Exception:
return ref # can't introspect (offline/unknown) β€” let from_pretrained handle it
if is_compatible_config(cfg):
return ref
out = _cache_root() / ref.replace("/", "__")
if (out / "model.safetensors").is_file() and (out / "config.json").is_file():
if verbose:
print(f"[convert] using cached converted backbone: {out}")
return str(out)
if verbose:
print(f"[convert] '{ref}' is an upstream-layout backbone; "
f"converting once -> {out}")
return convert_backbone(ref, out, tokenizer_repo=tokenizer_repo)
# ──────────────────────────────────────────────────────────────────────────────
# Double-check (used by the CLI)
# ──────────────────────────────────────────────────────────────────────────────
def double_check(out_dir, ref_dir) -> bool:
out_dir, ref_dir = Path(out_dir), Path(ref_dir)
print(f"\n[verify] comparing {out_dir} vs reference {ref_dir}")
o = load_file(out_dir / "model.safetensors")
r = load_file(ref_dir / "model.safetensors")
ok = True
if set(o) != set(r):
ok = False
print(f" [FAIL] key sets differ "
f"(only_out={sorted(set(o)-set(r))[:3]} only_ref={sorted(set(r)-set(o))[:3]})")
else:
print(f" [ok] key sets match ({len(o)} tensors)")
mismatched = [k for k in set(o) & set(r)
if o[k].shape != r[k].shape or not torch.equal(o[k].float(), r[k].float())]
if mismatched:
ok = False
print(f" [FAIL] {len(mismatched)} tensor(s) differ, e.g. {mismatched[:5]}")
else:
print(f" [ok] all {len(set(o) & set(r))} shared tensors bit-identical")
oc, rc = json.load(open(out_dir / "config.json")), json.load(open(ref_dir / "config.json"))
for f in ["vocab_size", "additional_vocab_size", "freeze_config"]:
if oc.get(f) != rc.get(f):
ok = False; print(f" [FAIL] config.{f}: {oc.get(f)} != {rc.get(f)}")
else:
print(f" [ok] config.{f} == {oc.get(f)}")
print(f"\n[verify] {'PASSED' if ok else 'FAILED'}.")
return ok