Video Classification
Transformers
Safetensors
vjepa21
feature-extraction
video
vjepa
vjepa2
v-jepa-2.1
self-supervised
world-model
custom_code
Instructions to use apiantonio/vjepa2.1-vit-base-384 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use apiantonio/vjepa2.1-vit-base-384 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("video-classification", model="apiantonio/vjepa2.1-vit-base-384", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("apiantonio/vjepa2.1-vit-base-384", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Fix transformers 4.x/5.x compat, implement output_hidden_states/attentions and out_layers, fix hierarchical predictor input, add video processor
Browse files- verify_big_models.py +590 -0
- verify_vjepa21_port.py +550 -0
verify_big_models.py
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""Check C/D/E per i checkpoint grandi (ViT-g, ViT-G) senza andare in OOM.
|
| 3 |
+
|
| 4 |
+
`verify_vjepa21_port.py` tiene in RAM contemporaneamente il .pt di Meta, la
|
| 5 |
+
mappa dei tensori attesi, il safetensors pubblicato, il modello di reference e
|
| 6 |
+
il port. Su base e large ci sta; su giant (1.07 G parametri) e gigantic (1.90 G)
|
| 7 |
+
no. Qui le stesse tre verifiche sono riorganizzate cosi':
|
| 8 |
+
|
| 9 |
+
[C] provenienza — il .pt viene aperto in mmap (le pagine restano su disco) e
|
| 10 |
+
il safetensors pubblicato viene letto UN TENSORE ALLA VOLTA
|
| 11 |
+
con `safe_open`. Gestisce anche i checkpoint shardati:
|
| 12 |
+
gigantic e' distribuito in 4 file piu' un index.json, e
|
| 13 |
+
`hf_hub_download(repo, "model.safetensors")` fallisce.
|
| 14 |
+
Picco: qualche centinaio di MB.
|
| 15 |
+
|
| 16 |
+
[D] encoder — il reference e il port NON sono mai vivi insieme. Fase 1:
|
| 17 |
+
[E] predictor costruisci il reference, calcola, scrivi gli output su
|
| 18 |
+
disco, libera. Fase 2: carica il port, calcola, confronta.
|
| 19 |
+
Picco: un solo modello alla volta.
|
| 20 |
+
|
| 21 |
+
[F] precisione — niente `copy.deepcopy` del modello gia' su GPU: il modello
|
| 22 |
+
a precisione ridotta viene ricaricato da disco direttamente
|
| 23 |
+
nel dtype voluto. Con gigantic il deepcopy chiedeva 7.1 GB
|
| 24 |
+
di VRAM oltre ai 7.1 gia' occupati, contro i 14.6 di una T4.
|
| 25 |
+
|
| 26 |
+
[G] merge PEFT — gira su un modello piccolo derivato dalla config reale: il
|
| 27 |
+
merge esatto e' una proprieta' algebrica, non dei pesi.
|
| 28 |
+
|
| 29 |
+
Entrambi i lati girano in SDPA. Il reference encoder accetta `use_sdpa=True`, e
|
| 30 |
+
il predictor del reference usa SDPA comunque (`use_sdpa` non e' un suo
|
| 31 |
+
parametro, finisce in **kwargs). Oltre a essere il confronto corretto a kernel
|
| 32 |
+
appaiato, evita di materializzare la matrice di attenzione: a 4608 token con 22
|
| 33 |
+
teste sarebbero 1.9 GB per layer con il kernel eager.
|
| 34 |
+
|
| 35 |
+
Uso:
|
| 36 |
+
|
| 37 |
+
!python verify_big_models.py --repo apiantonio/vjepa2.1-vit-giant-384 \
|
| 38 |
+
--vjepa2-repo ./vjepa2 --checks CDE
|
| 39 |
+
|
| 40 |
+
Su Colab free (12.7 GB di RAM) girano entrambi. Serve spazio su disco per il
|
| 41 |
+
.pt di Meta (~4 GB per giant, ~8 GB per gigantic) piu' il safetensors.
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
from __future__ import annotations
|
| 45 |
+
|
| 46 |
+
import argparse
|
| 47 |
+
import gc
|
| 48 |
+
import json
|
| 49 |
+
import os
|
| 50 |
+
import resource
|
| 51 |
+
import sys
|
| 52 |
+
import urllib.request
|
| 53 |
+
|
| 54 |
+
import torch
|
| 55 |
+
|
| 56 |
+
OFFICIAL_URL = "https://dl.fbaipublicfiles.com/vjepa2"
|
| 57 |
+
|
| 58 |
+
SPEC = {
|
| 59 |
+
"apiantonio/vjepa2.1-vit-base-384": dict(
|
| 60 |
+
ckpt="vjepa2_1_vitb_dist_vitG_384.pt", key="ema_encoder", arch="vit_base",
|
| 61 |
+
hidden=768, n_distill=1, pred_depth=12, teacher=1664),
|
| 62 |
+
"apiantonio/vjepa2.1-vit-large-384": dict(
|
| 63 |
+
ckpt="vjepa2_1_vitl_dist_vitG_384.pt", key="ema_encoder", arch="vit_large",
|
| 64 |
+
hidden=1024, n_distill=1, pred_depth=12, teacher=1664),
|
| 65 |
+
"apiantonio/vjepa2.1-vit-giant-384": dict(
|
| 66 |
+
ckpt="vjepa2_1_vitg_384.pt", key="target_encoder", arch="vit_giant_xformers",
|
| 67 |
+
hidden=1408, n_distill=4, pred_depth=24, teacher=None),
|
| 68 |
+
"apiantonio/vjepa2.1-vit-gigantic-384": dict(
|
| 69 |
+
ckpt="vjepa2_1_vitG_384.pt", key="target_encoder", arch="vit_gigantic_xformers",
|
| 70 |
+
hidden=1664, n_distill=4, pred_depth=24, teacher=None),
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
G, R, Y, N = "\033[32m", "\033[31m", "\033[33m", "\033[0m"
|
| 74 |
+
FAILURES: list[str] = []
|
| 75 |
+
PRED_DIM = 384
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def ok(m):
|
| 79 |
+
print(f"{G} PASS{N} {m}")
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def fail(m):
|
| 83 |
+
print(f"{R} FAIL{N} {m}")
|
| 84 |
+
FAILURES.append(m)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def warn(m):
|
| 88 |
+
print(f"{Y} WARN{N} {m}")
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def peak_ram_gb() -> float:
|
| 92 |
+
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / (1024 * 1024)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def report_ram(tag=""):
|
| 96 |
+
print(f" [picco RAM {peak_ram_gb():.2f} GB{(' — ' + tag) if tag else ''}]")
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# ---------------------------------------------------------------------------
|
| 100 |
+
# accesso frugale ai due checkpoint
|
| 101 |
+
# ---------------------------------------------------------------------------
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def official_path(repo, cache="."):
|
| 105 |
+
name = SPEC[repo]["ckpt"]
|
| 106 |
+
path = os.path.join(cache, name)
|
| 107 |
+
if not os.path.exists(path):
|
| 108 |
+
print(f" scarico {name} (una volta sola) ...")
|
| 109 |
+
urllib.request.urlretrieve(f"{OFFICIAL_URL}/{name}", path)
|
| 110 |
+
print(f" {name}: {os.path.getsize(path) / 2**30:.2f} GB su disco")
|
| 111 |
+
return path
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def load_official_mmap(repo, cache="."):
|
| 115 |
+
"""Apre il .pt in mmap e scarta subito tutto cio' che non serve.
|
| 116 |
+
|
| 117 |
+
I checkpoint di training di Meta contengono anche l'encoder non-EMA e lo
|
| 118 |
+
stato dell'ottimizzatore: possono pesare 3-4 volte il modello.
|
| 119 |
+
"""
|
| 120 |
+
path = official_path(repo, cache)
|
| 121 |
+
try:
|
| 122 |
+
raw = torch.load(path, map_location="cpu", mmap=True, weights_only=False)
|
| 123 |
+
except (RuntimeError, TypeError) as e:
|
| 124 |
+
warn(f"mmap non disponibile ({type(e).__name__}), carico normalmente")
|
| 125 |
+
raw = torch.load(path, map_location="cpu", weights_only=False)
|
| 126 |
+
|
| 127 |
+
key = SPEC[repo]["key"]
|
| 128 |
+
keep = {key, "predictor"}
|
| 129 |
+
dropped = [k for k in list(raw.keys()) if k not in keep]
|
| 130 |
+
print(f" chiavi nel .pt: {sorted(raw.keys())}")
|
| 131 |
+
for k in dropped:
|
| 132 |
+
del raw[k]
|
| 133 |
+
gc.collect()
|
| 134 |
+
|
| 135 |
+
clean = lambda sd: {k.replace("module.", "").replace("backbone.", ""): v
|
| 136 |
+
for k, v in sd.items()}
|
| 137 |
+
return clean(raw[key]), clean(raw["predictor"])
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def published_tensors(repo):
|
| 141 |
+
"""Genera (nome, tensore) leggendo un tensore alla volta.
|
| 142 |
+
|
| 143 |
+
Gestisce sia il file unico sia i checkpoint shardati (gigantic ha 4 shard
|
| 144 |
+
piu' `model.safetensors.index.json`).
|
| 145 |
+
"""
|
| 146 |
+
from huggingface_hub import hf_hub_download
|
| 147 |
+
from safetensors import safe_open
|
| 148 |
+
|
| 149 |
+
try:
|
| 150 |
+
files = [hf_hub_download(repo, "model.safetensors")]
|
| 151 |
+
except Exception:
|
| 152 |
+
idx = hf_hub_download(repo, "model.safetensors.index.json")
|
| 153 |
+
with open(idx) as fh:
|
| 154 |
+
shards = sorted(set(json.load(fh)["weight_map"].values()))
|
| 155 |
+
print(f" checkpoint shardato in {len(shards)} file")
|
| 156 |
+
files = [hf_hub_download(repo, s) for s in shards]
|
| 157 |
+
|
| 158 |
+
for f in files:
|
| 159 |
+
with safe_open(f, framework="pt", device="cpu") as h:
|
| 160 |
+
for name in h.keys():
|
| 161 |
+
yield name, h.get_tensor(name)
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
# ---------------------------------------------------------------------------
|
| 165 |
+
# mappa reference -> port (identica a verify_vjepa21_port.py)
|
| 166 |
+
# ---------------------------------------------------------------------------
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def _map_block(prefix, idx, sub, tensor, hidden, out):
|
| 170 |
+
if sub.startswith("attn.qkv."):
|
| 171 |
+
kind = sub.rsplit(".", 1)[-1]
|
| 172 |
+
q, k, v = tensor.split(hidden, dim=0)
|
| 173 |
+
out[f"{prefix}.layer.{idx}.attention.query.{kind}"] = q
|
| 174 |
+
out[f"{prefix}.layer.{idx}.attention.key.{kind}"] = k
|
| 175 |
+
out[f"{prefix}.layer.{idx}.attention.value.{kind}"] = v
|
| 176 |
+
elif sub.startswith("attn.proj."):
|
| 177 |
+
out[f"{prefix}.layer.{idx}.attention.proj." + sub.rsplit(".", 1)[-1]] = tensor
|
| 178 |
+
else:
|
| 179 |
+
out[f"{prefix}.layer.{idx}.{sub}"] = tensor
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def reference_to_port(enc_sd, pred_sd, hidden, pred_hidden):
|
| 183 |
+
out = {}
|
| 184 |
+
for k, v in enc_sd.items():
|
| 185 |
+
if k in ("img_mod_embed", "video_mod_embed"):
|
| 186 |
+
out[f"encoder.embeddings.{k}"] = v
|
| 187 |
+
elif k.startswith("patch_embed_img."):
|
| 188 |
+
out["encoder.embeddings.patch_embeddings_img." + k[len("patch_embed_img."):]] = v
|
| 189 |
+
elif k.startswith("patch_embed."):
|
| 190 |
+
out["encoder.embeddings.patch_embeddings." + k[len("patch_embed."):]] = v
|
| 191 |
+
elif k.startswith("norms_block."):
|
| 192 |
+
out["encoder." + k] = v
|
| 193 |
+
elif k.startswith("blocks."):
|
| 194 |
+
idx, sub = k[len("blocks."):].split(".", 1)
|
| 195 |
+
_map_block("encoder", idx, sub, v, hidden, out)
|
| 196 |
+
elif k == "pos_embed":
|
| 197 |
+
continue
|
| 198 |
+
else:
|
| 199 |
+
warn(f"chiave encoder non mappata: {k}")
|
| 200 |
+
for k, v in pred_sd.items():
|
| 201 |
+
if k in ("img_mod_embed", "video_mod_embed"):
|
| 202 |
+
out[f"predictor.embeddings.{k}"] = v
|
| 203 |
+
elif k.startswith("predictor_embed."):
|
| 204 |
+
out["predictor.embeddings.predictor_embed." + k[len("predictor_embed."):]] = v
|
| 205 |
+
elif k.startswith("mask_tokens."):
|
| 206 |
+
out["predictor.embeddings." + k] = v
|
| 207 |
+
elif k.startswith("predictor_norm."):
|
| 208 |
+
out["predictor.layernorm." + k[len("predictor_norm."):]] = v
|
| 209 |
+
elif k.startswith("predictor_proj_context."):
|
| 210 |
+
out["predictor.proj_context." + k[len("predictor_proj_context."):]] = v
|
| 211 |
+
elif k.startswith("predictor_proj."):
|
| 212 |
+
out["predictor.proj." + k[len("predictor_proj."):]] = v
|
| 213 |
+
elif k.startswith("predictor_blocks."):
|
| 214 |
+
idx, sub = k[len("predictor_blocks."):].split(".", 1)
|
| 215 |
+
_map_block("predictor", idx, sub, v, pred_hidden, out)
|
| 216 |
+
elif k == "predictor_pos_embed":
|
| 217 |
+
continue
|
| 218 |
+
else:
|
| 219 |
+
warn(f"chiave predictor non mappata: {k}")
|
| 220 |
+
return out
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
# ---------------------------------------------------------------------------
|
| 224 |
+
# [C] provenienza in streaming
|
| 225 |
+
# ---------------------------------------------------------------------------
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def check_provenance(repo, cache="."):
|
| 229 |
+
print(f"\n[C] provenienza dei pesi pubblicati — {repo}")
|
| 230 |
+
spec = SPEC[repo]
|
| 231 |
+
enc_sd, pred_sd = load_official_mmap(repo, cache)
|
| 232 |
+
expected = reference_to_port(enc_sd, pred_sd, spec["hidden"], PRED_DIM)
|
| 233 |
+
print(f" tensori attesi dalla conversione: {len(expected)}")
|
| 234 |
+
|
| 235 |
+
seen, orphans, worst, worst_key, mismatched = 0, [], 0.0, None, []
|
| 236 |
+
for name, tensor in published_tensors(repo):
|
| 237 |
+
seen += 1
|
| 238 |
+
ref = expected.pop(name, None)
|
| 239 |
+
if ref is None:
|
| 240 |
+
orphans.append(name)
|
| 241 |
+
continue
|
| 242 |
+
if tuple(ref.shape) != tuple(tensor.shape):
|
| 243 |
+
mismatched.append(f"{name}: {tuple(tensor.shape)} vs {tuple(ref.shape)}")
|
| 244 |
+
continue
|
| 245 |
+
d = (tensor.float() - ref.float()).abs().max().item()
|
| 246 |
+
if d > worst:
|
| 247 |
+
worst, worst_key = d, name
|
| 248 |
+
del tensor, ref
|
| 249 |
+
|
| 250 |
+
(ok if not orphans else fail)(
|
| 251 |
+
f"tensori pubblicati senza origine nel checkpoint: {len(orphans)}"
|
| 252 |
+
+ (f" -> {sorted(orphans)[:6]}" if orphans else ""))
|
| 253 |
+
(ok if not mismatched else fail)(
|
| 254 |
+
f"tensori con shape diversa: {len(mismatched)}"
|
| 255 |
+
+ (f" -> {mismatched[:4]}" if mismatched else ""))
|
| 256 |
+
if expected:
|
| 257 |
+
warn(f"tensori del reference non pubblicati: {len(expected)} "
|
| 258 |
+
f"-> {sorted(expected)[:6]}")
|
| 259 |
+
(ok if worst == 0.0 else fail)(
|
| 260 |
+
f"max|Δ| su {seen - len(orphans)} tensori = {worst:.3e}"
|
| 261 |
+
+ (f" (peggiore: {worst_key})" if worst else ""))
|
| 262 |
+
report_ram("dopo C")
|
| 263 |
+
del expected, enc_sd, pred_sd
|
| 264 |
+
gc.collect()
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
# ---------------------------------------------------------------------------
|
| 268 |
+
# [D] + [E] parita' del forward, reference e port mai vivi insieme
|
| 269 |
+
# ---------------------------------------------------------------------------
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
@torch.no_grad()
|
| 273 |
+
def phase_reference(repo, vjepa2_repo, frames, device, work, cache="."):
|
| 274 |
+
print(f"\n[D/E] fase 1 — reference (il port non e' ancora caricato)")
|
| 275 |
+
sys.path.insert(0, os.path.abspath(vjepa2_repo))
|
| 276 |
+
from app.vjepa_2_1.models import vision_transformer as vit
|
| 277 |
+
from app.vjepa_2_1.models.predictor import vit_predictor
|
| 278 |
+
|
| 279 |
+
spec = SPEC[repo]
|
| 280 |
+
enc_sd, pred_sd = load_official_mmap(repo, cache)
|
| 281 |
+
|
| 282 |
+
encoder = vit.__dict__[spec["arch"]](
|
| 283 |
+
patch_size=16, img_size=(384, 384), num_frames=64, tubelet_size=2,
|
| 284 |
+
use_sdpa=True, uniform_power=False, use_rope=True, img_temporal_dim_size=1,
|
| 285 |
+
interpolate_rope=True, modality_embedding=True,
|
| 286 |
+
n_output_distillation=spec["n_distill"],
|
| 287 |
+
).eval()
|
| 288 |
+
encoder.load_state_dict(enc_sd, strict=True)
|
| 289 |
+
del enc_sd
|
| 290 |
+
gc.collect()
|
| 291 |
+
ok("encoder di reference caricato con strict=True")
|
| 292 |
+
|
| 293 |
+
torch.manual_seed(0)
|
| 294 |
+
x = torch.randn(1, 3, frames, 384, 384)
|
| 295 |
+
encoder = encoder.to(device)
|
| 296 |
+
# `forward(training=True)` restituisce i livelli concatenati; l'ultima fetta e'
|
| 297 |
+
# `norms_block[-1]` applicata all'ultimo layer, cioe' esattamente il
|
| 298 |
+
# `last_hidden_state`. Vale sia per n_distill=1 sia per n_distill=4.
|
| 299 |
+
z = encoder(x.to(device), training=True).cpu()
|
| 300 |
+
a = z[..., -spec["hidden"]:].contiguous()
|
| 301 |
+
del encoder
|
| 302 |
+
gc.collect()
|
| 303 |
+
if device == "cuda":
|
| 304 |
+
torch.cuda.empty_cache()
|
| 305 |
+
print(f" encoder: {tuple(a.shape)} token, hierarchical {tuple(z.shape)}")
|
| 306 |
+
report_ram("dopo l'encoder di reference")
|
| 307 |
+
|
| 308 |
+
predictor = vit_predictor(
|
| 309 |
+
img_size=(384, 384), patch_size=16, use_mask_tokens=True,
|
| 310 |
+
embed_dim=spec["hidden"], predictor_embed_dim=PRED_DIM,
|
| 311 |
+
teacher_embed_dim=spec["teacher"], num_frames=64, tubelet_size=2,
|
| 312 |
+
depth=spec["pred_depth"], num_heads=12, num_mask_tokens=8,
|
| 313 |
+
use_rope=True, uniform_power=False, use_silu=False, wide_silu=True,
|
| 314 |
+
n_output_distillation=spec["n_distill"], return_all_tokens=True,
|
| 315 |
+
img_temporal_dim_size=1, modality_embedding=True, zero_init_mask_tokens=True,
|
| 316 |
+
interpolate_rope=True,
|
| 317 |
+
).eval()
|
| 318 |
+
predictor.load_state_dict(pred_sd, strict=True)
|
| 319 |
+
del pred_sd
|
| 320 |
+
gc.collect()
|
| 321 |
+
ok("predictor di reference caricato con strict=True")
|
| 322 |
+
|
| 323 |
+
mt = max(m.abs().max().item() for m in predictor.mask_tokens)
|
| 324 |
+
(warn if mt == 0 else ok)(
|
| 325 |
+
f"norma max dei mask token del checkpoint = {mt:.3e}"
|
| 326 |
+
+ (" (zero: il confronto sul mask token resta degenere)" if mt == 0 else ""))
|
| 327 |
+
|
| 328 |
+
n_tokens = z.shape[1]
|
| 329 |
+
ctx = torch.arange(0, n_tokens // 2).unsqueeze(0)
|
| 330 |
+
tgt = torch.arange(n_tokens // 2, n_tokens).unsqueeze(0)
|
| 331 |
+
predictor = predictor.to(device)
|
| 332 |
+
|
| 333 |
+
idx = ctx.unsqueeze(-1).expand(-1, -1, z.size(-1)).to(device)
|
| 334 |
+
ctx_tokens = torch.gather(z.to(device), 1, idx)
|
| 335 |
+
rp, rc = predictor(ctx_tokens, [ctx.to(device)], [tgt.to(device)], mod="video")
|
| 336 |
+
rp, rc = rp.cpu(), rc.cpu()
|
| 337 |
+
del predictor, ctx_tokens, idx
|
| 338 |
+
gc.collect()
|
| 339 |
+
if device == "cuda":
|
| 340 |
+
torch.cuda.empty_cache()
|
| 341 |
+
report_ram("dopo il predictor di reference")
|
| 342 |
+
|
| 343 |
+
blob = os.path.join(work, "reference_outputs.pt")
|
| 344 |
+
torch.save({"x": x, "a": a, "z": z, "rp": rp, "rc": rc, "ctx": ctx, "tgt": tgt}, blob)
|
| 345 |
+
print(f" output di reference scritti in {blob} "
|
| 346 |
+
f"({os.path.getsize(blob) / 2**30:.2f} GB)")
|
| 347 |
+
return blob
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
@torch.no_grad()
|
| 351 |
+
def phase_port(repo, blob, device):
|
| 352 |
+
print(f"\n[D/E] fase 2 — port (il reference e' stato liberato)")
|
| 353 |
+
from transformers import AutoModel
|
| 354 |
+
|
| 355 |
+
cached = torch.load(blob, map_location="cpu", weights_only=False)
|
| 356 |
+
port = AutoModel.from_pretrained(repo, trust_remote_code=True).eval()
|
| 357 |
+
port.config._attn_implementation = "sdpa"
|
| 358 |
+
port = port.to(device)
|
| 359 |
+
report_ram("port caricato")
|
| 360 |
+
|
| 361 |
+
def rel(u, v):
|
| 362 |
+
return ((u - v).abs().mean() / u.abs().mean()).item()
|
| 363 |
+
|
| 364 |
+
def cos(u, v):
|
| 365 |
+
return torch.nn.functional.cosine_similarity(
|
| 366 |
+
u.flatten(0, 1), v.flatten(0, 1)).min().item()
|
| 367 |
+
|
| 368 |
+
# --- D ---
|
| 369 |
+
out = port(pixel_values_videos=cached["x"].to(device), skip_predictor=True,
|
| 370 |
+
return_hierarchical=True)
|
| 371 |
+
b = out.last_hidden_state.cpu()
|
| 372 |
+
zh = out.hierarchical_hidden_state.cpu()
|
| 373 |
+
del out
|
| 374 |
+
if device == "cuda":
|
| 375 |
+
torch.cuda.empty_cache()
|
| 376 |
+
|
| 377 |
+
a = cached["a"]
|
| 378 |
+
d, r, c = (a - b).abs().max().item(), rel(a, b), cos(a, b)
|
| 379 |
+
(ok if (d == 0.0 or (r < 1e-6 and c > 0.999999)) else fail)(
|
| 380 |
+
f"[D] encoder: max|Δ| = {d:.3e} rel = {r:.3e} cos = {c:.6f} "
|
| 381 |
+
f"(tokens={a.shape[1]})")
|
| 382 |
+
|
| 383 |
+
dz = (cached["z"] - zh).abs().max().item()
|
| 384 |
+
(ok if dz == 0.0 else fail)(
|
| 385 |
+
f"[D] hierarchical: max|Δ| = {dz:.3e} (dim={zh.shape[-1]})")
|
| 386 |
+
del b, zh
|
| 387 |
+
gc.collect()
|
| 388 |
+
|
| 389 |
+
# --- E ---
|
| 390 |
+
got = port.predictor(cached["z"].to(device),
|
| 391 |
+
[cached["ctx"].to(device)], [cached["tgt"].to(device)],
|
| 392 |
+
mode="video")
|
| 393 |
+
gp = got.last_hidden_state.cpu()
|
| 394 |
+
gc_ = got.context_hidden_state.cpu()
|
| 395 |
+
del got, port
|
| 396 |
+
gc.collect()
|
| 397 |
+
if device == "cuda":
|
| 398 |
+
torch.cuda.empty_cache()
|
| 399 |
+
|
| 400 |
+
rp, rc = cached["rp"], cached["rc"]
|
| 401 |
+
pr, pc = rel(rp, gp), cos(rp, gp)
|
| 402 |
+
cr, cc = rel(rc, gc_), cos(rc, gc_)
|
| 403 |
+
peak = (rp - gp).abs().max().item()
|
| 404 |
+
(ok if pr < 1e-4 and pc > 0.9999 and cr < 1e-4 and cc > 0.9999 else fail)(
|
| 405 |
+
f"[E] predictor: target rel = {pr:.3e} cos = {pc:.6f} | "
|
| 406 |
+
f"context rel = {cr:.3e} cos = {cc:.6f} | max|Δ| = {peak:.3e} "
|
| 407 |
+
f"(out dim = {gp.shape[-1]})")
|
| 408 |
+
report_ram("fine")
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
# ---------------------------------------------------------------------------
|
| 412 |
+
# [F] precisione ridotta senza tenere due copie del modello in memoria
|
| 413 |
+
# ---------------------------------------------------------------------------
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def _from_pretrained(repo, dtype=None):
|
| 417 |
+
"""`dtype=` su transformers 5, `torch_dtype=` su transformers 4."""
|
| 418 |
+
from transformers import AutoModel
|
| 419 |
+
|
| 420 |
+
kw = dict(trust_remote_code=True)
|
| 421 |
+
if dtype is not None:
|
| 422 |
+
try:
|
| 423 |
+
return AutoModel.from_pretrained(repo, dtype=dtype, **kw)
|
| 424 |
+
except TypeError:
|
| 425 |
+
return AutoModel.from_pretrained(repo, torch_dtype=dtype, **kw)
|
| 426 |
+
return AutoModel.from_pretrained(repo, **kw)
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
@torch.no_grad()
|
| 430 |
+
def check_precision_streaming(repo, device):
|
| 431 |
+
"""Come il check F di verify_vjepa21_port.py, ma senza `copy.deepcopy`.
|
| 432 |
+
|
| 433 |
+
Il deepcopy avviene sul modello gia' spostato su GPU: per gigantic sono
|
| 434 |
+
7.1 GB in fp32 piu' altri 7.1 GB per la copia, contro i 14.6 GB di una T4.
|
| 435 |
+
Qui il modello a precisione ridotta viene RICARICATO da disco direttamente
|
| 436 |
+
nel dtype voluto, quindi non c'e' mai piu' di un modello per volta. E' anche
|
| 437 |
+
piu' pulito del deepcopy rispetto al problema originale: ogni forward parte
|
| 438 |
+
da pesi freschi, quindi nessuna misura puo' contaminare la successiva.
|
| 439 |
+
"""
|
| 440 |
+
print(f"\n[F] gap di precisione ridotta — {repo}")
|
| 441 |
+
if device == "cuda":
|
| 442 |
+
torch.cuda.reset_peak_memory_stats()
|
| 443 |
+
|
| 444 |
+
model = _from_pretrained(repo).eval()
|
| 445 |
+
crop = model.config.crop_size
|
| 446 |
+
torch.manual_seed(1)
|
| 447 |
+
x = torch.randn(1, 3, 4, crop, crop)
|
| 448 |
+
model = model.to(device)
|
| 449 |
+
|
| 450 |
+
ref = model(pixel_values_videos=x.to(device), skip_predictor=True).last_hidden_state
|
| 451 |
+
twice = model(pixel_values_videos=x.to(device), skip_predictor=True).last_hidden_state
|
| 452 |
+
(ok if torch.equal(ref, twice) else fail)("due forward identici sono bit-identici")
|
| 453 |
+
ref = ref.float().cpu()
|
| 454 |
+
del model, twice
|
| 455 |
+
gc.collect()
|
| 456 |
+
if device == "cuda":
|
| 457 |
+
torch.cuda.empty_cache()
|
| 458 |
+
print(f" [picco VRAM fp32 {torch.cuda.max_memory_allocated() / 2**30:.2f} GB]")
|
| 459 |
+
|
| 460 |
+
for dtype in (torch.bfloat16, torch.float16):
|
| 461 |
+
if dtype is torch.float16 and device == "cpu":
|
| 462 |
+
warn("fp16 saltato su CPU")
|
| 463 |
+
continue
|
| 464 |
+
for impl in ("sdpa", "eager"):
|
| 465 |
+
if device == "cuda":
|
| 466 |
+
torch.cuda.reset_peak_memory_stats()
|
| 467 |
+
low = _from_pretrained(repo, dtype=dtype).eval()
|
| 468 |
+
low.config._attn_implementation = impl
|
| 469 |
+
low = low.to(device)
|
| 470 |
+
out = low(pixel_values_videos=x.to(device).to(dtype),
|
| 471 |
+
skip_predictor=True).last_hidden_state
|
| 472 |
+
finite = bool(torch.isfinite(out).all())
|
| 473 |
+
got = out.float().cpu()
|
| 474 |
+
del low, out
|
| 475 |
+
gc.collect()
|
| 476 |
+
if device == "cuda":
|
| 477 |
+
torch.cuda.empty_cache()
|
| 478 |
+
|
| 479 |
+
label = f"{str(dtype).split('.')[-1]}/{impl}"
|
| 480 |
+
if not finite:
|
| 481 |
+
# V-JEPA 2.1 e' addestrato in bfloat16, che ha il range di esponente
|
| 482 |
+
# del fp32: i logits dell'attenzione possono uscire dal range fp16, e
|
| 483 |
+
# il kernel eager li calcola in fp16 nativo. Limite del checkpoint.
|
| 484 |
+
warn(f"{label:>16}: NaN/inf, overflow fp16 nel kernel eager")
|
| 485 |
+
else:
|
| 486 |
+
r = ((ref - got).abs().mean() / ref.abs().mean()).item()
|
| 487 |
+
c = torch.nn.functional.cosine_similarity(
|
| 488 |
+
ref.flatten(0, 1), got.flatten(0, 1)).min().item()
|
| 489 |
+
(ok if c > 0.99 else fail)(f"{label:>16}: rel = {r:.3e} min cos = {c:.6f}")
|
| 490 |
+
del got
|
| 491 |
+
report_ram("dopo F")
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
# ---------------------------------------------------------------------------
|
| 495 |
+
# [G] merge di LoRA e DoRA — modello piccolo, la proprieta' e' algebrica
|
| 496 |
+
# ---------------------------------------------------------------------------
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
@torch.no_grad()
|
| 500 |
+
def check_dora(repo):
|
| 501 |
+
print(f"\n[G] merge di LoRA e DoRA (modello piccolo)")
|
| 502 |
+
try:
|
| 503 |
+
from peft import LoraConfig, get_peft_model
|
| 504 |
+
except ImportError:
|
| 505 |
+
warn("peft non installato, salto")
|
| 506 |
+
return
|
| 507 |
+
import copy as _copy
|
| 508 |
+
|
| 509 |
+
from transformers import AutoConfig, AutoModelForVideoClassification
|
| 510 |
+
|
| 511 |
+
cfg = AutoConfig.from_pretrained(repo, trust_remote_code=True)
|
| 512 |
+
small = _copy.deepcopy(cfg)
|
| 513 |
+
small.crop_size, small.hidden_size, small.num_attention_heads = 64, 96, 6
|
| 514 |
+
small.num_hidden_layers, small.pred_num_hidden_layers = 12, 12
|
| 515 |
+
small.pred_hidden_size, small.pred_num_attention_heads = 48, 6
|
| 516 |
+
small.n_output_distillation, small.pred_teacher_embed_dim = 1, 96
|
| 517 |
+
small.num_pooler_heads, small.num_labels = 6, 5
|
| 518 |
+
|
| 519 |
+
base = AutoModelForVideoClassification.from_config(small, trust_remote_code=True)
|
| 520 |
+
for use_dora in (False, True):
|
| 521 |
+
torch.manual_seed(0)
|
| 522 |
+
model = _copy.deepcopy(base).eval()
|
| 523 |
+
peft_model = get_peft_model(model, LoraConfig(
|
| 524 |
+
r=8, lora_alpha=16, lora_dropout=0.0, use_dora=use_dora,
|
| 525 |
+
target_modules=r".*vjepa21\.encoder\.layer\.\d+\.attention\.(query|key|value|proj)$",
|
| 526 |
+
modules_to_save=["classifier", "pooler"],
|
| 527 |
+
))
|
| 528 |
+
for name, p in peft_model.named_parameters():
|
| 529 |
+
if "lora_B" in name:
|
| 530 |
+
torch.nn.init.normal_(p, std=0.02)
|
| 531 |
+
x = torch.randn(2, 3, 4, 64, 64)
|
| 532 |
+
before = peft_model(pixel_values_videos=x).logits
|
| 533 |
+
merged = peft_model.merge_and_unload().eval()
|
| 534 |
+
after = merged(pixel_values_videos=x).logits
|
| 535 |
+
d = (before - after).abs().max().item()
|
| 536 |
+
leftover = [n for n, _ in merged.named_parameters() if "lora" in n.lower()]
|
| 537 |
+
label = "DoRA" if use_dora else "LoRA"
|
| 538 |
+
(ok if d < 1e-4 and not leftover else fail)(
|
| 539 |
+
f"{label}: max|Δ| dopo merge = {d:.3e}, tensori adapter residui = {len(leftover)}")
|
| 540 |
+
del model, peft_model, merged
|
| 541 |
+
gc.collect()
|
| 542 |
+
|
| 543 |
+
|
| 544 |
+
def main():
|
| 545 |
+
ap = argparse.ArgumentParser()
|
| 546 |
+
ap.add_argument("--repo", required=True, choices=sorted(SPEC))
|
| 547 |
+
ap.add_argument("--vjepa2-repo", default="./vjepa2")
|
| 548 |
+
ap.add_argument("--cache", default=".", help="dove tenere il .pt di Meta")
|
| 549 |
+
ap.add_argument("--work", default="/tmp", help="dove scrivere gli output intermedi")
|
| 550 |
+
ap.add_argument("--frames", type=int, default=16)
|
| 551 |
+
ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
| 552 |
+
ap.add_argument("--checks", default="CDEFG")
|
| 553 |
+
ap.add_argument("--keep-blob", action="store_true")
|
| 554 |
+
args = ap.parse_args()
|
| 555 |
+
|
| 556 |
+
print(f"torch {torch.__version__} | device {args.device} | repo {args.repo}")
|
| 557 |
+
print(f"RAM iniziale in uso: {peak_ram_gb():.2f} GB")
|
| 558 |
+
|
| 559 |
+
if "C" in args.checks:
|
| 560 |
+
check_provenance(args.repo, args.cache)
|
| 561 |
+
|
| 562 |
+
blob = None
|
| 563 |
+
if set("DE") & set(args.checks):
|
| 564 |
+
blob = phase_reference(args.repo, args.vjepa2_repo, args.frames,
|
| 565 |
+
args.device, args.work, args.cache)
|
| 566 |
+
gc.collect()
|
| 567 |
+
phase_port(args.repo, blob, args.device)
|
| 568 |
+
if blob and not args.keep_blob:
|
| 569 |
+
os.remove(blob)
|
| 570 |
+
gc.collect()
|
| 571 |
+
|
| 572 |
+
if "F" in args.checks:
|
| 573 |
+
check_precision_streaming(args.repo, args.device)
|
| 574 |
+
gc.collect()
|
| 575 |
+
|
| 576 |
+
if "G" in args.checks:
|
| 577 |
+
check_dora(args.repo)
|
| 578 |
+
|
| 579 |
+
print("\n" + "=" * 70)
|
| 580 |
+
print(f"picco RAM del processo: {peak_ram_gb():.2f} GB")
|
| 581 |
+
if FAILURES:
|
| 582 |
+
print(f"{R}{len(FAILURES)} controlli falliti{N}")
|
| 583 |
+
for f in FAILURES:
|
| 584 |
+
print(" -", f)
|
| 585 |
+
sys.exit(1)
|
| 586 |
+
print(f"{G}tutti i controlli eseguiti sono passati{N}")
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
if __name__ == "__main__":
|
| 590 |
+
main()
|
verify_vjepa21_port.py
ADDED
|
@@ -0,0 +1,550 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""Verifica end-to-end dei port V-JEPA 2.1 pubblicati su HuggingFace.
|
| 3 |
+
|
| 4 |
+
Copre le tre cose che la test suite spedita nei repo NON verifica sugli
|
| 5 |
+
artefatti effettivamente pubblicati:
|
| 6 |
+
|
| 7 |
+
A) i valori di config.json corrispondono al costruttore ufficiale
|
| 8 |
+
(`src/hub/backbones.py::_make_vjepa2_1_model`);
|
| 9 |
+
B) il checkpoint pubblicato si carica senza chiavi mancanti, inattese o
|
| 10 |
+
con shape sbagliata (un parametro orfano resta inizializzato a caso e
|
| 11 |
+
HF lo segnala solo con un warning);
|
| 12 |
+
C) ogni tensore del safetensors pubblicato ha un'origine nel checkpoint
|
| 13 |
+
ufficiale di Meta ed e' identico bit a bit;
|
| 14 |
+
D) il forward dell'encoder a 384 sui pesi veri coincide con quello del
|
| 15 |
+
reference;
|
| 16 |
+
E) il forward del PREDICTOR sui pesi veri coincide con quello del
|
| 17 |
+
reference (mai testato: i mask token addestrati non sono zero, quindi
|
| 18 |
+
il test spedito con pesi random e' degenere);
|
| 19 |
+
F) il gap di precisione ridotta misurato senza corrompere il modello.
|
| 20 |
+
|
| 21 |
+
Uso su Colab
|
| 22 |
+
------------
|
| 23 |
+
!pip -q install "transformers>=4.57" safetensors huggingface_hub
|
| 24 |
+
!pip -q install timm einops # solo per i check D/E
|
| 25 |
+
!git clone -q https://github.com/facebookresearch/vjepa2.git
|
| 26 |
+
!python verify_vjepa21_port.py --repo apiantonio/vjepa2.1-vit-base-384 \
|
| 27 |
+
--vjepa2-repo ./vjepa2 --checks ABCDEF
|
| 28 |
+
|
| 29 |
+
RAM richiesta (i check C/D/E tengono in memoria port + reference):
|
| 30 |
+
base ~2 GB | large ~4 GB | giant ~13 GB | gigantic ~20 GB
|
| 31 |
+
Su Colab free (12.7 GB) girano base e large; per giant/gigantic serve una
|
| 32 |
+
runtime High-RAM, oppure si eseguono solo A e B.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
from __future__ import annotations
|
| 36 |
+
|
| 37 |
+
import argparse
|
| 38 |
+
import copy
|
| 39 |
+
import json
|
| 40 |
+
import math
|
| 41 |
+
import os
|
| 42 |
+
import sys
|
| 43 |
+
import urllib.request
|
| 44 |
+
|
| 45 |
+
import torch
|
| 46 |
+
|
| 47 |
+
# Meta ha lasciato VJEPA_BASE_URL puntato a http://localhost:8300 nel main
|
| 48 |
+
# corrente di facebookresearch/vjepa2 (la riga vera e' commentata sopra), quindi
|
| 49 |
+
# torch.hub.load(...) fallisce. Scarichiamo il checkpoint direttamente.
|
| 50 |
+
OFFICIAL_URL = "https://dl.fbaipublicfiles.com/vjepa2"
|
| 51 |
+
|
| 52 |
+
GREEN, RED, YELLOW, RESET = "\033[32m", "\033[31m", "\033[33m", "\033[0m"
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def ok(msg):
|
| 56 |
+
print(f"{GREEN} PASS{RESET} {msg}")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def fail(msg):
|
| 60 |
+
print(f"{RED} FAIL{RESET} {msg}")
|
| 61 |
+
FAILURES.append(msg)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def warn(msg):
|
| 65 |
+
print(f"{YELLOW} WARN{RESET} {msg}")
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
FAILURES: list[str] = []
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# ---------------------------------------------------------------------------
|
| 72 |
+
# A) config.json vs costruttore ufficiale
|
| 73 |
+
# ---------------------------------------------------------------------------
|
| 74 |
+
|
| 75 |
+
# Derivato da src/hub/backbones.py::_make_vjepa2_1_model + le factory in
|
| 76 |
+
# app/vjepa_2_1/models/vision_transformer.py. NON toccare senza rileggere il
|
| 77 |
+
# reference: e' la specifica contro cui si verifica.
|
| 78 |
+
COMMON = dict(
|
| 79 |
+
patch_size=16,
|
| 80 |
+
crop_size=384,
|
| 81 |
+
tubelet_size=2,
|
| 82 |
+
frames_per_clip=64, # num_frames=64
|
| 83 |
+
in_chans=3,
|
| 84 |
+
img_temporal_dim_size=1,
|
| 85 |
+
interpolate_rope=True,
|
| 86 |
+
modality_embedding=True,
|
| 87 |
+
hidden_act="gelu", # use_silu=False
|
| 88 |
+
qkv_bias=True,
|
| 89 |
+
n_registers=0,
|
| 90 |
+
has_cls_first=False,
|
| 91 |
+
layer_norm_eps=1e-6,
|
| 92 |
+
drop_path_rate=0.0,
|
| 93 |
+
num_pooler_layers=3, # +1 cross-attention block = num_probe_blocks: 4
|
| 94 |
+
num_pooler_heads=16, # classifier.num_heads: 16 in every configs/eval_2_1 file
|
| 95 |
+
pred_hidden_size=384, # predictor_embed_dim
|
| 96 |
+
pred_num_attention_heads=12, # num_heads=12 nel predictor
|
| 97 |
+
pred_mlp_ratio=4.0,
|
| 98 |
+
pred_num_mask_tokens=8, # predictor_num_mask_tokens
|
| 99 |
+
pred_zero_init_mask_tokens=True,
|
| 100 |
+
pred_return_all_tokens=True, # return_all_tokens=True
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
EXPECTED = {
|
| 104 |
+
"apiantonio/vjepa2.1-vit-base-384": dict(
|
| 105 |
+
COMMON,
|
| 106 |
+
hidden_size=768, num_hidden_layers=12, num_attention_heads=12, mlp_ratio=4.0,
|
| 107 |
+
n_output_distillation=1, pred_num_hidden_layers=12, pred_teacher_embed_dim=1664,
|
| 108 |
+
_ckpt="vjepa2_1_vitb_dist_vitG_384.pt", _key="ema_encoder", _arch="vit_base",
|
| 109 |
+
),
|
| 110 |
+
"apiantonio/vjepa2.1-vit-large-384": dict(
|
| 111 |
+
COMMON,
|
| 112 |
+
hidden_size=1024, num_hidden_layers=24, num_attention_heads=16, mlp_ratio=4.0,
|
| 113 |
+
n_output_distillation=1, pred_num_hidden_layers=12, pred_teacher_embed_dim=1664,
|
| 114 |
+
_ckpt="vjepa2_1_vitl_dist_vitG_384.pt", _key="ema_encoder", _arch="vit_large",
|
| 115 |
+
),
|
| 116 |
+
"apiantonio/vjepa2.1-vit-giant-384": dict(
|
| 117 |
+
COMMON,
|
| 118 |
+
hidden_size=1408, num_hidden_layers=40, num_attention_heads=22, mlp_ratio=48 / 11,
|
| 119 |
+
n_output_distillation=4, pred_num_hidden_layers=24, pred_teacher_embed_dim=None,
|
| 120 |
+
_ckpt="vjepa2_1_vitg_384.pt", _key="target_encoder", _arch="vit_giant_xformers",
|
| 121 |
+
),
|
| 122 |
+
"apiantonio/vjepa2.1-vit-gigantic-384": dict(
|
| 123 |
+
COMMON,
|
| 124 |
+
hidden_size=1664, num_hidden_layers=48, num_attention_heads=26, mlp_ratio=64 / 13,
|
| 125 |
+
n_output_distillation=4, pred_num_hidden_layers=24, pred_teacher_embed_dim=None,
|
| 126 |
+
_ckpt="vjepa2_1_vitG_384.pt", _key="target_encoder", _arch="vit_gigantic_xformers",
|
| 127 |
+
),
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def check_config(repo):
|
| 132 |
+
print(f"\n[A] config.json vs costruttore ufficiale — {repo}")
|
| 133 |
+
from transformers import AutoConfig
|
| 134 |
+
|
| 135 |
+
cfg = AutoConfig.from_pretrained(repo, trust_remote_code=True)
|
| 136 |
+
spec = EXPECTED[repo]
|
| 137 |
+
bad = []
|
| 138 |
+
for key, want in spec.items():
|
| 139 |
+
if key.startswith("_"):
|
| 140 |
+
continue
|
| 141 |
+
got = getattr(cfg, key, "<assente>")
|
| 142 |
+
same = math.isclose(got, want, rel_tol=0, abs_tol=0) if isinstance(want, float) else got == want
|
| 143 |
+
if not same:
|
| 144 |
+
bad.append(f"{key}: atteso {want!r}, trovato {got!r}")
|
| 145 |
+
if bad:
|
| 146 |
+
for b in bad:
|
| 147 |
+
fail(b)
|
| 148 |
+
else:
|
| 149 |
+
ok(f"{len([k for k in spec if not k.startswith(chr(95))])} campi coincidono")
|
| 150 |
+
|
| 151 |
+
# le proprieta' derivate devono coincidere con la mappa del reference
|
| 152 |
+
hier = {12: [2, 5, 8, 11], 24: [5, 11, 17, 23], 40: [9, 19, 29, 39], 48: [11, 23, 37, 47]}
|
| 153 |
+
if cfg.encoder_hierarchical_layers != hier[cfg.num_hidden_layers]:
|
| 154 |
+
fail(f"encoder_hierarchical_layers {cfg.encoder_hierarchical_layers}")
|
| 155 |
+
else:
|
| 156 |
+
ok(f"encoder_hierarchical_layers = {cfg.encoder_hierarchical_layers}")
|
| 157 |
+
|
| 158 |
+
# dimensione della proiezione del predictor
|
| 159 |
+
n_hier = len(cfg.predictor_hierarchical_layers)
|
| 160 |
+
out = (cfg.pred_teacher_embed_dim // n_hier) if cfg.pred_teacher_embed_dim else cfg.hidden_size
|
| 161 |
+
ok(f"predictor proj out_dim = {n_hier * out} (n_hier={n_hier})")
|
| 162 |
+
|
| 163 |
+
# MLP: int(dim * ratio) deve dare esattamente il valore del reference
|
| 164 |
+
for name, d, r in (("encoder", cfg.hidden_size, cfg.mlp_ratio),
|
| 165 |
+
("predictor", cfg.pred_hidden_size, cfg.pred_mlp_ratio)):
|
| 166 |
+
h = int(d * r)
|
| 167 |
+
exact = int(d * (48 / 11)) if abs(r - 48 / 11) < 1e-12 else (
|
| 168 |
+
int(d * (64 / 13)) if abs(r - 64 / 13) < 1e-12 else int(d * r))
|
| 169 |
+
if h != exact:
|
| 170 |
+
fail(f"{name} mlp hidden {h} != {exact} (round-trip JSON del mlp_ratio)")
|
| 171 |
+
else:
|
| 172 |
+
ok(f"{name} mlp hidden = {h}")
|
| 173 |
+
return cfg
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
# ---------------------------------------------------------------------------
|
| 177 |
+
# B) nessun parametro orfano al caricamento
|
| 178 |
+
# ---------------------------------------------------------------------------
|
| 179 |
+
|
| 180 |
+
def check_loading(repo, dtype=torch.float32):
|
| 181 |
+
print(f"\n[B] caricamento senza chiavi orfane — {repo}")
|
| 182 |
+
from transformers import AutoModel, AutoModelForVideoClassification
|
| 183 |
+
|
| 184 |
+
model, info = AutoModel.from_pretrained(
|
| 185 |
+
repo, trust_remote_code=True, dtype=dtype, output_loading_info=True
|
| 186 |
+
)
|
| 187 |
+
for name in ("missing_keys", "unexpected_keys", "mismatched_keys"):
|
| 188 |
+
v = info.get(name) or []
|
| 189 |
+
(ok if not v else fail)(f"AutoModel {name}: {len(v)}" + (f" -> {v[:6]}" if v else ""))
|
| 190 |
+
|
| 191 |
+
n = sum(p.numel() for p in model.parameters())
|
| 192 |
+
ok(f"parametri totali: {n:,}")
|
| 193 |
+
|
| 194 |
+
# la testa di classificazione deve ereditare i pesi dell'encoder:
|
| 195 |
+
# se base_model_prefix e' sbagliato, missing_keys esplode e il backbone
|
| 196 |
+
# riparte da zero senza che nulla fallisca.
|
| 197 |
+
clf, cinfo = AutoModelForVideoClassification.from_pretrained(
|
| 198 |
+
repo, trust_remote_code=True, dtype=dtype, num_labels=2, output_loading_info=True
|
| 199 |
+
)
|
| 200 |
+
missing = [k for k in (cinfo.get("missing_keys") or [])
|
| 201 |
+
if not k.startswith(("pooler.", "classifier."))]
|
| 202 |
+
(ok if not missing else fail)(
|
| 203 |
+
f"ForVideoClassification: solo pooler/classifier reinizializzati"
|
| 204 |
+
+ (f", ma anche {missing[:6]}" if missing else "")
|
| 205 |
+
)
|
| 206 |
+
for k, v in model.encoder.state_dict().items():
|
| 207 |
+
if not torch.equal(v, clf.vjepa21.encoder.state_dict()[k]):
|
| 208 |
+
fail(f"peso encoder diverso dopo il wrapper: {k}")
|
| 209 |
+
break
|
| 210 |
+
else:
|
| 211 |
+
ok("i pesi dell'encoder sopravvivono al wrapper di classificazione")
|
| 212 |
+
del clf
|
| 213 |
+
return model
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
# ---------------------------------------------------------------------------
|
| 217 |
+
# mappa reference -> port (identica a quella usata dalla conversione)
|
| 218 |
+
# ---------------------------------------------------------------------------
|
| 219 |
+
|
| 220 |
+
def _map_block(prefix, idx, sub, tensor, hidden, out):
|
| 221 |
+
if sub.startswith("attn.qkv."):
|
| 222 |
+
kind = sub.rsplit(".", 1)[-1]
|
| 223 |
+
q, k, v = tensor.split(hidden, dim=0)
|
| 224 |
+
out[f"{prefix}.layer.{idx}.attention.query.{kind}"] = q
|
| 225 |
+
out[f"{prefix}.layer.{idx}.attention.key.{kind}"] = k
|
| 226 |
+
out[f"{prefix}.layer.{idx}.attention.value.{kind}"] = v
|
| 227 |
+
elif sub.startswith("attn.proj."):
|
| 228 |
+
out[f"{prefix}.layer.{idx}.attention.proj." + sub.rsplit(".", 1)[-1] ] = tensor
|
| 229 |
+
else:
|
| 230 |
+
out[f"{prefix}.layer.{idx}.{sub}"] = tensor
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def reference_to_port(enc_sd, pred_sd, hidden, pred_hidden):
|
| 234 |
+
out = {}
|
| 235 |
+
for k, v in enc_sd.items():
|
| 236 |
+
if k in ("img_mod_embed", "video_mod_embed"):
|
| 237 |
+
out[f"encoder.embeddings.{k}"] = v
|
| 238 |
+
elif k.startswith("patch_embed_img."):
|
| 239 |
+
out["encoder.embeddings.patch_embeddings_img." + k[len("patch_embed_img."):]] = v
|
| 240 |
+
elif k.startswith("patch_embed."):
|
| 241 |
+
out["encoder.embeddings.patch_embeddings." + k[len("patch_embed."):]] = v
|
| 242 |
+
elif k.startswith("norms_block."):
|
| 243 |
+
out["encoder." + k] = v
|
| 244 |
+
elif k.startswith("blocks."):
|
| 245 |
+
idx, sub = k[len("blocks."):].split(".", 1)
|
| 246 |
+
_map_block("encoder", idx, sub, v, hidden, out)
|
| 247 |
+
elif k in ("pos_embed",):
|
| 248 |
+
continue # non usato: il modello usa RoPE
|
| 249 |
+
else:
|
| 250 |
+
warn(f"chiave encoder di reference non mappata: {k}")
|
| 251 |
+
for k, v in pred_sd.items():
|
| 252 |
+
if k in ("img_mod_embed", "video_mod_embed"):
|
| 253 |
+
out[f"predictor.embeddings.{k}"] = v
|
| 254 |
+
elif k.startswith("predictor_embed."):
|
| 255 |
+
out["predictor.embeddings.predictor_embed." + k[len("predictor_embed."):]] = v
|
| 256 |
+
elif k.startswith("mask_tokens."):
|
| 257 |
+
out["predictor.embeddings." + k] = v
|
| 258 |
+
elif k.startswith("predictor_norm."):
|
| 259 |
+
out["predictor.layernorm." + k[len("predictor_norm."):]] = v
|
| 260 |
+
elif k.startswith("predictor_proj_context."):
|
| 261 |
+
out["predictor.proj_context." + k[len("predictor_proj_context."):]] = v
|
| 262 |
+
elif k.startswith("predictor_proj."):
|
| 263 |
+
out["predictor.proj." + k[len("predictor_proj."):]] = v
|
| 264 |
+
elif k.startswith("predictor_blocks."):
|
| 265 |
+
idx, sub = k[len("predictor_blocks."):].split(".", 1)
|
| 266 |
+
_map_block("predictor", idx, sub, v, pred_hidden, out)
|
| 267 |
+
elif k in ("predictor_pos_embed",):
|
| 268 |
+
continue
|
| 269 |
+
else:
|
| 270 |
+
warn(f"chiave predictor di reference non mappata: {k}")
|
| 271 |
+
return out
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def download_official(repo, cache="."):
|
| 275 |
+
name = EXPECTED[repo]["_ckpt"]
|
| 276 |
+
path = os.path.join(cache, name)
|
| 277 |
+
if not os.path.exists(path):
|
| 278 |
+
print(f" scarico {name} ...")
|
| 279 |
+
urllib.request.urlretrieve(f"{OFFICIAL_URL}/{name}", path)
|
| 280 |
+
return path
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
def load_official(repo, cache="."):
|
| 284 |
+
path = download_official(repo, cache)
|
| 285 |
+
raw = torch.load(path, map_location="cpu", weights_only=False)
|
| 286 |
+
clean = lambda sd: {k.replace("module.", "").replace("backbone.", ""): v for k, v in sd.items()}
|
| 287 |
+
return clean(raw[EXPECTED[repo]["_key"]]), clean(raw["predictor"])
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
# ---------------------------------------------------------------------------
|
| 291 |
+
# C) provenienza bit a bit di ogni tensore pubblicato
|
| 292 |
+
# ---------------------------------------------------------------------------
|
| 293 |
+
|
| 294 |
+
def check_provenance(repo, cache="."):
|
| 295 |
+
print(f"\n[C] provenienza dei pesi pubblicati — {repo}")
|
| 296 |
+
from huggingface_hub import hf_hub_download
|
| 297 |
+
from safetensors.torch import load_file
|
| 298 |
+
|
| 299 |
+
cfg = EXPECTED[repo]
|
| 300 |
+
enc_sd, pred_sd = load_official(repo, cache)
|
| 301 |
+
expected = reference_to_port(enc_sd, pred_sd, cfg["hidden_size"], cfg["pred_hidden_size"])
|
| 302 |
+
published = load_file(hf_hub_download(repo, "model.safetensors"))
|
| 303 |
+
|
| 304 |
+
orphans = sorted(set(published) - set(expected))
|
| 305 |
+
unused = sorted(set(expected) - set(published))
|
| 306 |
+
(ok if not orphans else fail)(
|
| 307 |
+
f"tensori pubblicati senza origine nel checkpoint: {len(orphans)}"
|
| 308 |
+
+ (f" -> {orphans[:8]}" if orphans else "")
|
| 309 |
+
)
|
| 310 |
+
if unused:
|
| 311 |
+
warn(f"tensori del reference non pubblicati: {len(unused)} -> {unused[:8]}")
|
| 312 |
+
|
| 313 |
+
worst, worst_key = 0.0, None
|
| 314 |
+
for k in sorted(set(published) & set(expected)):
|
| 315 |
+
a, b = published[k].float(), expected[k].float()
|
| 316 |
+
if a.shape != b.shape:
|
| 317 |
+
fail(f"shape diversa per {k}: {tuple(a.shape)} vs {tuple(b.shape)}")
|
| 318 |
+
continue
|
| 319 |
+
d = (a - b).abs().max().item()
|
| 320 |
+
if d > worst:
|
| 321 |
+
worst, worst_key = d, k
|
| 322 |
+
(ok if worst == 0.0 else fail)(
|
| 323 |
+
f"max|Δ| su {len(set(published) & set(expected))} tensori = {worst:.3e}"
|
| 324 |
+
+ (f" (peggiore: {worst_key})" if worst else "")
|
| 325 |
+
)
|
| 326 |
+
del published, expected, enc_sd, pred_sd
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
# ---------------------------------------------------------------------------
|
| 330 |
+
# D/E) parita' del forward sui pesi veri, encoder e predictor
|
| 331 |
+
# ---------------------------------------------------------------------------
|
| 332 |
+
|
| 333 |
+
def build_reference(repo, vjepa2_repo, cache="."):
|
| 334 |
+
sys.path.insert(0, os.path.abspath(vjepa2_repo))
|
| 335 |
+
from app.vjepa_2_1.models import vision_transformer as vit
|
| 336 |
+
from app.vjepa_2_1.models.predictor import vit_predictor
|
| 337 |
+
|
| 338 |
+
spec = EXPECTED[repo]
|
| 339 |
+
enc = vit.__dict__[spec["_arch"]](
|
| 340 |
+
patch_size=16, img_size=(384, 384), num_frames=64, tubelet_size=2,
|
| 341 |
+
use_sdpa=False, uniform_power=False, use_rope=True, img_temporal_dim_size=1,
|
| 342 |
+
interpolate_rope=True, modality_embedding=True,
|
| 343 |
+
n_output_distillation=spec["n_output_distillation"],
|
| 344 |
+
).eval()
|
| 345 |
+
# NOTE: `VisionTransformerPredictor.__init__` has no `use_sdpa` parameter —
|
| 346 |
+
# it would be swallowed by `**kwargs` — so the reference predictor blocks
|
| 347 |
+
# always run SDPA while the port runs eager. That is why the predictor
|
| 348 |
+
# tolerance below is 1e-3 rather than exact.
|
| 349 |
+
pred = vit_predictor(
|
| 350 |
+
img_size=(384, 384), patch_size=16, use_mask_tokens=True,
|
| 351 |
+
embed_dim=spec["hidden_size"], predictor_embed_dim=384,
|
| 352 |
+
teacher_embed_dim=spec["pred_teacher_embed_dim"], num_frames=64, tubelet_size=2,
|
| 353 |
+
depth=spec["pred_num_hidden_layers"], num_heads=12, num_mask_tokens=8,
|
| 354 |
+
use_rope=True, uniform_power=False, use_silu=False, wide_silu=True,
|
| 355 |
+
n_output_distillation=spec["n_output_distillation"], return_all_tokens=True,
|
| 356 |
+
img_temporal_dim_size=1, modality_embedding=True, zero_init_mask_tokens=True,
|
| 357 |
+
interpolate_rope=True,
|
| 358 |
+
).eval()
|
| 359 |
+
enc_sd, pred_sd = load_official(repo, cache)
|
| 360 |
+
enc.load_state_dict(enc_sd, strict=True)
|
| 361 |
+
pred.load_state_dict(pred_sd, strict=True)
|
| 362 |
+
ok("encoder e predictor di reference caricati con strict=True")
|
| 363 |
+
return enc, pred
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
@torch.no_grad()
|
| 367 |
+
def check_forward_parity(repo, vjepa2_repo, port, frames=16, cache="."):
|
| 368 |
+
print(f"\n[D] parita' del forward encoder a 384, pesi pubblicati — {repo}")
|
| 369 |
+
ref_enc, ref_pred = build_reference(repo, vjepa2_repo, cache)
|
| 370 |
+
torch.manual_seed(0)
|
| 371 |
+
x = torch.randn(1, 3, frames, 384, 384)
|
| 372 |
+
|
| 373 |
+
saved_impl = getattr(port.config, "_attn_implementation", "sdpa")
|
| 374 |
+
port.config._attn_implementation = "eager" # il reference encoder usa use_sdpa=False
|
| 375 |
+
a = ref_enc(x)
|
| 376 |
+
b = port(pixel_values_videos=x, skip_predictor=True).last_hidden_state
|
| 377 |
+
d = (a - b).abs().max().item()
|
| 378 |
+
(ok if d < 1e-4 else fail)(f"T={frames}: max|Δ| = {d:.3e} (tokens={a.shape[1]})")
|
| 379 |
+
|
| 380 |
+
# [E] predictor sui pesi VERI: i mask token addestrati non sono zero, quindi
|
| 381 |
+
# questo esercita davvero il percorso che il test spedito non copre.
|
| 382 |
+
print(f"\n[E] parita' del forward predictor, pesi pubblicati — {repo}")
|
| 383 |
+
mt = torch.stack([m.flatten() for m in ref_pred.mask_tokens]).abs().max().item()
|
| 384 |
+
(warn if mt == 0 else ok)(f"norma max dei mask token del checkpoint = {mt:.3e}"
|
| 385 |
+
+ (" (zero: il test resta degenere)" if mt == 0 else ""))
|
| 386 |
+
z = ref_enc(x, training=True) if EXPECTED[repo]["n_output_distillation"] > 1 else a
|
| 387 |
+
N = z.shape[1]
|
| 388 |
+
ctx = torch.arange(0, N // 2).unsqueeze(0)
|
| 389 |
+
tgt = torch.arange(N // 2, N).unsqueeze(0)
|
| 390 |
+
|
| 391 |
+
from importlib import import_module
|
| 392 |
+
apply_masks = import_module(type(port).__module__).apply_masks
|
| 393 |
+
rp, rc = ref_pred(apply_masks(z, [ctx]), [ctx], [tgt], mod="video")
|
| 394 |
+
# `VisionTransformerPredictor.__init__` non ha `use_sdpa` (finisce in **kwargs),
|
| 395 |
+
# quindi il predictor del reference gira SEMPRE in SDPA. Appaiamo il kernel,
|
| 396 |
+
# altrimenti si misura la differenza eager-vs-SDPA e non la parita' del port.
|
| 397 |
+
port.config._attn_implementation = "sdpa"
|
| 398 |
+
got = port.predictor(z, [ctx], [tgt], mode="video")
|
| 399 |
+
port.config._attn_implementation = saved_impl
|
| 400 |
+
|
| 401 |
+
def _r(u, v):
|
| 402 |
+
return ((u - v).abs().mean() / u.abs().mean()).item()
|
| 403 |
+
|
| 404 |
+
def _c(u, v):
|
| 405 |
+
return torch.nn.functional.cosine_similarity(
|
| 406 |
+
u.flatten(0, 1), v.flatten(0, 1)).min().item()
|
| 407 |
+
|
| 408 |
+
pr, pc = _r(rp, got.last_hidden_state), _c(rp, got.last_hidden_state)
|
| 409 |
+
cr, cc = _r(rc, got.context_hidden_state), _c(rc, got.context_hidden_state)
|
| 410 |
+
peak = (rp - got.last_hidden_state).abs().max().item()
|
| 411 |
+
# L'assert e' su errore relativo e cosine similarity: il massimo assoluto e'
|
| 412 |
+
# preso su milioni di elementi e non significa nulla senza la scala delle
|
| 413 |
+
# attivazioni.
|
| 414 |
+
(ok if pr < 1e-4 and pc > 0.9999 and cr < 1e-4 and cc > 0.9999 else fail)(
|
| 415 |
+
f"target rel = {pr:.3e} cos = {pc:.6f} | context rel = {cr:.3e} cos = {cc:.6f}"
|
| 416 |
+
f" | target peak = {peak:.3e}"
|
| 417 |
+
)
|
| 418 |
+
del ref_enc, ref_pred
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
# ---------------------------------------------------------------------------
|
| 422 |
+
# F) precisione ridotta senza corrompere il modello
|
| 423 |
+
# ---------------------------------------------------------------------------
|
| 424 |
+
|
| 425 |
+
@torch.no_grad()
|
| 426 |
+
def check_precision(port):
|
| 427 |
+
print("\n[F] gap di precisione ridotta (su copia, il modello non viene alterato)")
|
| 428 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 429 |
+
torch.manual_seed(1)
|
| 430 |
+
x = torch.randn(1, 3, 4, port.config.crop_size, port.config.crop_size, device=device)
|
| 431 |
+
base = port.to(device)
|
| 432 |
+
ref = base(pixel_values_videos=x, skip_predictor=True).last_hidden_state.float()
|
| 433 |
+
|
| 434 |
+
saved = getattr(base.config, "_attn_implementation", "sdpa")
|
| 435 |
+
for dtype in (torch.bfloat16, torch.float16):
|
| 436 |
+
if dtype is torch.float16 and device == "cpu":
|
| 437 |
+
warn("fp16 saltato su CPU")
|
| 438 |
+
continue
|
| 439 |
+
for impl in ("sdpa", "eager"):
|
| 440 |
+
low = copy.deepcopy(base).to(dtype) # <- la copia e' il punto
|
| 441 |
+
low.config._attn_implementation = impl
|
| 442 |
+
out = low(pixel_values_videos=x.to(dtype), skip_predictor=True).last_hidden_state
|
| 443 |
+
finite = bool(torch.isfinite(out).all())
|
| 444 |
+
got = out.float()
|
| 445 |
+
label = f"{str(dtype).split('.')[-1]}/{impl}"
|
| 446 |
+
# V-JEPA 2.1 e' addestrato in bfloat16 (use_bfloat16: true nei config di
|
| 447 |
+
# eval), che ha il range di esponente del fp32: le attivazioni possono
|
| 448 |
+
# uscire dal range fp16. E' un limite del checkpoint, non del port.
|
| 449 |
+
if not finite:
|
| 450 |
+
warn(f"{label:>16}: NaN/inf, overflow fp16 nel kernel {impl}")
|
| 451 |
+
else:
|
| 452 |
+
rel = ((ref - got).abs().mean() / ref.abs().mean()).item()
|
| 453 |
+
cos = torch.nn.functional.cosine_similarity(
|
| 454 |
+
ref.flatten(0, 1), got.flatten(0, 1)).min().item()
|
| 455 |
+
(ok if cos > 0.99 else fail)(
|
| 456 |
+
f"{label:>16}: rel = {rel:.3e} min cos = {cos:.6f}")
|
| 457 |
+
del low
|
| 458 |
+
base.config._attn_implementation = saved
|
| 459 |
+
|
| 460 |
+
a = base(pixel_values_videos=x, skip_predictor=True).last_hidden_state
|
| 461 |
+
b = base(pixel_values_videos=x, skip_predictor=True).last_hidden_state
|
| 462 |
+
(ok if torch.equal(a, b) else fail)("due forward identici sono bit-identici")
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
# ---------------------------------------------------------------------------
|
| 466 |
+
# G) DoRA: il merge deve essere esatto quanto quello di LoRA
|
| 467 |
+
# ---------------------------------------------------------------------------
|
| 468 |
+
|
| 469 |
+
@torch.no_grad()
|
| 470 |
+
def check_dora(repo):
|
| 471 |
+
print("\n[G] merge di DoRA (modello piccolo, la proprieta' e' algebrica)")
|
| 472 |
+
try:
|
| 473 |
+
from peft import LoraConfig, get_peft_model
|
| 474 |
+
except ImportError:
|
| 475 |
+
warn("peft non installato, salto")
|
| 476 |
+
return
|
| 477 |
+
from transformers import AutoConfig, AutoModelForVideoClassification
|
| 478 |
+
|
| 479 |
+
cfg = AutoConfig.from_pretrained(repo, trust_remote_code=True)
|
| 480 |
+
small = copy.deepcopy(cfg)
|
| 481 |
+
small.crop_size, small.hidden_size, small.num_attention_heads = 64, 96, 6
|
| 482 |
+
small.num_hidden_layers, small.pred_num_hidden_layers = 12, 12
|
| 483 |
+
small.pred_hidden_size, small.pred_num_attention_heads = 48, 6
|
| 484 |
+
small.n_output_distillation, small.pred_teacher_embed_dim = 1, 96
|
| 485 |
+
small.num_pooler_heads, small.num_labels = 6, 5
|
| 486 |
+
|
| 487 |
+
cls = AutoModelForVideoClassification.from_config(small, trust_remote_code=True)
|
| 488 |
+
for use_dora in (False, True):
|
| 489 |
+
torch.manual_seed(0)
|
| 490 |
+
model = copy.deepcopy(cls).eval()
|
| 491 |
+
peft_model = get_peft_model(model, LoraConfig(
|
| 492 |
+
r=8, lora_alpha=16, lora_dropout=0.0, use_dora=use_dora,
|
| 493 |
+
target_modules=r".*vjepa21\.encoder\.layer\.\d+\.attention\.(query|key|value|proj)$",
|
| 494 |
+
modules_to_save=["classifier", "pooler"],
|
| 495 |
+
))
|
| 496 |
+
for name, p in peft_model.named_parameters():
|
| 497 |
+
if "lora_B" in name:
|
| 498 |
+
torch.nn.init.normal_(p, std=0.02)
|
| 499 |
+
x = torch.randn(2, 3, 4, 64, 64)
|
| 500 |
+
before = peft_model(pixel_values_videos=x).logits
|
| 501 |
+
merged = peft_model.merge_and_unload().eval()
|
| 502 |
+
after = merged(pixel_values_videos=x).logits
|
| 503 |
+
d = (before - after).abs().max().item()
|
| 504 |
+
leftover = [n for n, _ in merged.named_parameters() if "lora" in n.lower()]
|
| 505 |
+
label = "DoRA" if use_dora else "LoRA"
|
| 506 |
+
(ok if d < 1e-4 and not leftover else fail)(
|
| 507 |
+
f"{label}: max|Δ| dopo merge = {d:.3e}, tensori adapter residui = {len(leftover)}")
|
| 508 |
+
|
| 509 |
+
|
| 510 |
+
# ---------------------------------------------------------------------------
|
| 511 |
+
|
| 512 |
+
def main():
|
| 513 |
+
ap = argparse.ArgumentParser()
|
| 514 |
+
ap.add_argument("--repo", required=True, choices=sorted(EXPECTED))
|
| 515 |
+
ap.add_argument("--vjepa2-repo", default="./vjepa2")
|
| 516 |
+
ap.add_argument("--cache", default=".")
|
| 517 |
+
ap.add_argument("--frames", type=int, default=16)
|
| 518 |
+
ap.add_argument("--checks", default="ABCDEFG")
|
| 519 |
+
args = ap.parse_args()
|
| 520 |
+
|
| 521 |
+
print(f"torch {torch.__version__} | cuda {torch.cuda.is_available()}")
|
| 522 |
+
port = None
|
| 523 |
+
if "A" in args.checks:
|
| 524 |
+
check_config(args.repo)
|
| 525 |
+
if set("BDEFG") & set(args.checks):
|
| 526 |
+
port = check_loading(args.repo) if "B" in args.checks else None
|
| 527 |
+
if port is None:
|
| 528 |
+
from transformers import AutoModel
|
| 529 |
+
port = AutoModel.from_pretrained(args.repo, trust_remote_code=True).eval()
|
| 530 |
+
port.eval()
|
| 531 |
+
if "C" in args.checks:
|
| 532 |
+
check_provenance(args.repo, args.cache)
|
| 533 |
+
if set("DE") & set(args.checks):
|
| 534 |
+
check_forward_parity(args.repo, args.vjepa2_repo, port, args.frames, args.cache)
|
| 535 |
+
if "F" in args.checks:
|
| 536 |
+
check_precision(port)
|
| 537 |
+
if "G" in args.checks:
|
| 538 |
+
check_dora(args.repo)
|
| 539 |
+
|
| 540 |
+
print("\n" + "=" * 70)
|
| 541 |
+
if FAILURES:
|
| 542 |
+
print(f"{RED}{len(FAILURES)} controlli falliti{RESET}")
|
| 543 |
+
for f in FAILURES:
|
| 544 |
+
print(" -", f)
|
| 545 |
+
sys.exit(1)
|
| 546 |
+
print(f"{GREEN}tutti i controlli eseguiti sono passati{RESET}")
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
if __name__ == "__main__":
|
| 550 |
+
main()
|