apiantonio commited on
Commit
21dd249
·
verified ·
1 Parent(s): a6f8b17

Fix transformers 4.x/5.x compat, implement output_hidden_states/attentions and out_layers, fix hierarchical predictor input, add video processor

Browse files
Files changed (2) hide show
  1. verify_big_models.py +590 -0
  2. 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()