File size: 13,418 Bytes
b63d4e6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 | # Community port β export V-JEPA 2 (SSv2) to Core AI, fp16, TWO outputs: logits + pooled embedding.
"""Variant of `export_fp16.py` for ForgeOptimizerKit's Β§6.3 planner-hint seam.
WHY A VARIANT EXISTS. The shipped bundle returns `logits [1,174]` only. Forge does not use those
labels β `mlxengine-forge/Docs/VJEPA2-HINT-ADAPTER.md` is explicit ("labels are useless, the
embedding is the signal"): the SSv2 checkpoint ships no label names, its vocabulary is hand-object
actions, and it is semantically wrong for signage. The planner's actual signal is the pooled
**[1,1024]** router embedding that `ForgeCore.GraphicProbe` is fitted over. So this export exposes
both outputs; everything else β the RoPE overlay, fp16, the externalize-drop β is unchanged.
THE GATE IS DUAL, and the second half is the point:
1. `logits` vs the existing oracle β proves nothing regressed relative to the verified bundle.
2. `embedding` vs a torch reference β proves the NEW output is the vector the probe consumes.
Parity on logits does not by itself certify the embedding. logits = classifier(pooled) through a
single Linear, so a high logit cosine is strong evidence β but `GraphicProbe.evaluate` takes a
1024-dot-product near a decision boundary, and "strong evidence" is not a measurement. Gate #2 is
held tighter than #1 for exactly that reason.
Writes to `artifacts/vjepa2_ssv2_fp16_embed/` β a SEPARATE directory, so a failed run here can
never clobber the published, verified single-output bundle.
Run: python3 export_fp16_embed.py
Prereq: `reference_run.py` once, for oracle_input.npy / oracle_logits.npy.
"""
import os, shutil, asyncio, inspect, numpy as np, torch
from pathlib import Path
HERE = os.path.dirname(os.path.abspath(__file__))
os.environ.setdefault("HF_HUB_OFFLINE", "1"); os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
import coreai_models.export.macos as _macos
from coreai_models.export.macos import export_to_coreai
_DROP = {"scaled_dot_product_attention", "rope"}
_macos._EXTERNALIZE_SPECS = [s for s in _macos._EXTERNALIZE_SPECS if s.composite_op_name not in _DROP]
from transformers import VJEPA2ForVideoClassification
import transformers.models.vjepa2.modeling_vjepa2 as _vj
REPO = "facebook/vjepa2-vitl-fpc16-256-ssv2"
OUT_DIR = Path(HERE) / "artifacts" / "vjepa2_ssv2_fp16_embed"
AIM_NAME = "vjepa2_ssv2_embed_fp16.aimodel"
# Gate bars. Logits mirror the original export's bar; the embedding is held tighter because it is
# the input to a linear probe whose class flips at the boundary.
LOGIT_COS_BAR = 0.99
EMBED_COS_BAR = 0.999
def _rotate_queries_or_keys(x, pos):
# Reference copy MINUS the no-op `squeeze(-1)` on a size-D/2 dim (torch ignores it; the Core AI
# converter maps it to ShrinkDims which requires size 1 and fails). Math unchanged.
# Identical to export_fp16.py β the overlay is mandatory for ANY V-JEPA2 export, not incidental
# to the single-output one.
B, num_heads, N, D = x.size()
omega = torch.arange(D // 2, dtype=x.dtype, device=x.device)
omega /= D / 2.0
omega = 1.0 / 10000**omega
freq = pos.unsqueeze(-1) * omega
emb_sin = freq.sin().repeat(1, 1, 1, 2)
emb_cos = freq.cos().repeat(1, 1, 1, 2)
y = x.unflatten(-1, (-1, 2))
y1, y2 = y.unbind(dim=-1)
y = torch.stack((-y2, y1), dim=-1)
y = y.flatten(-2)
return (x * emb_cos) + (y * emb_sin)
_vj.rotate_queries_or_keys = _rotate_queries_or_keys
# ---------------------------------------------------------------------------------------------
# Submodule resolution β discovered and VERIFIED, never assumed.
#
# The single-output export could wrap the whole model and take `.logits`, so it never had to know
# the internal layout. Reaching `pooled` does. Rather than hardcode attribute names against one
# transformers version, resolve by candidate list and then PROVE the recomposition reproduces the
# model's own logits. If the names drift, this fails loudly at author time with the real structure
# printed β instead of shipping a bundle whose "embedding" is some other tensor.
# ---------------------------------------------------------------------------------------------
BACKBONE_NAMES = ("vjepa2", "model", "vjepa2_model", "backbone", "encoder")
POOLER_NAMES = ("pooler", "attentive_pooler", "classifier_pooler")
CLASSIFIER_NAMES = ("classifier", "head", "class_head", "score")
def _first_attr(obj, names, what):
for n in names:
mod = getattr(obj, n, None)
if mod is not None and isinstance(mod, torch.nn.Module):
print(f"[resolve] {what} -> .{n} ({mod.__class__.__name__})", flush=True)
return mod
children = [f"{n} ({m.__class__.__name__})" for n, m in obj.named_children()]
raise RuntimeError(
f"could not resolve the {what} among {names}.\n"
f" actual children: {children}\n"
f" Fix: add the real attribute name to the *_NAMES list above and re-run β the "
f"verification step below will confirm the choice."
)
def _hidden(out):
"""Backbone output -> last hidden state. Branches on Python type, so it resolves at trace
time; it is not a data-dependent condition and does not enter the exported graph."""
if hasattr(out, "last_hidden_state"):
return out.last_hidden_state
if isinstance(out, (tuple, list)):
return out[0]
return out
class VJEPA2ExportWithEmbedding(torch.nn.Module):
"""`pixel_values_videos [1,16,3,256,256]` -> (`logits [1,174]`, `embedding [1,1024]`).
Output ORDER is load-bearing: it pairs with `output_names=("logits", "embedding")` below, and
the harness reads results by name β but a swap here would silently rename both tensors.
The gate catches it (an embedding scored against oracle logits cannot reach 0.999).
π `skip_predictor=True` IS NOT OPTIONAL, and it is invisible to the gate. The reference
`VJEPA2ForVideoClassification.forward` passes it (modeling_vjepa2.py:1197); `VJEPA2Model`
defaults it to **False**, in which case it runs the entire JEPA *predictor* β a second
transformer stack β and files the result under a SEPARATE output field. `last_hidden_state`
is `sequence_output` either way (modeling_vjepa2.py:1091/1118), so omitting the flag changes
no number: the recomposition check below still passes, the logits still match the oracle, and
the bundle is simply much larger and slower for a stack nothing reads. A numeric gate cannot
see this class of defect β only reading the reference can.
"""
def __init__(self, backbone, pooler, classifier, skip_predictor: bool):
super().__init__()
self.backbone = backbone
self.pooler = pooler
self.classifier = classifier
# Plain bool read at trace time β a Python branch, not a graph condition.
self.skip_predictor = skip_predictor
def forward(self, pixel_values_videos):
if self.skip_predictor:
out = self.backbone(pixel_values_videos=pixel_values_videos, skip_predictor=True)
else:
out = self.backbone(pixel_values_videos=pixel_values_videos)
h = _hidden(out)
pooled = self.pooler(h)
# The attentive pooler may emit [B, 1, D] (one learned query) or [B, D]. Flatten to
# [B, D] so the published contract is one fixed rank regardless of the version's shape.
pooled = pooled.reshape(pooled.shape[0], -1)
logits = self.classifier(pooled)
return logits, pooled
def build():
model = VJEPA2ForVideoClassification.from_pretrained(REPO, dtype=torch.float32).eval()
px = torch.from_numpy(np.load(os.path.join(HERE, "oracle_input.npy"))) # [1,16,3,256,256]
ref_logits = torch.from_numpy(np.load(os.path.join(HERE, "oracle_logits.npy"))) # [1,174]
backbone = _first_attr(model, BACKBONE_NAMES, "backbone")
pooler = _first_attr(model, POOLER_NAMES, "attentive pooler")
classifier = _first_attr(model, CLASSIFIER_NAMES, "classifier")
# Detected rather than assumed, same discipline as the names above: on a transformers that
# renamed or dropped the flag we fall back to the default path (correct numbers, fat graph)
# and SAY SO, instead of dying on an unexpected kwarg.
skip = "skip_predictor" in inspect.signature(backbone.forward).parameters
print(f"[resolve] backbone skip_predictor supported={skip}"
+ ("" if skip else " β οΈ predictor stack will be traced into the graph β see class docstring"),
flush=True)
w = VJEPA2ExportWithEmbedding(backbone, pooler, classifier, skip_predictor=skip).eval()
# --- Verification: does the recomposition reproduce the model's own logits? -----------------
# This is what makes the resolution above trustworthy. If `pooled` were the wrong tensor, the
# classifier applied to it could not reproduce `.logits`.
with torch.inference_mode():
ref_out = model(pixel_values_videos=px).logits
logits, pooled = w(px)
cos_recompose = torch.nn.functional.cosine_similarity(
logits.reshape(-1), ref_out.reshape(-1), dim=0).item()
max_abs = (logits - ref_out).abs().max().item()
print(f"[verify] recomposed logits vs model.logits cos={cos_recompose:.8f} max|Ξ|={max_abs:.3e}",
flush=True)
if cos_recompose < 0.99999:
raise RuntimeError(
"recomposition does not reproduce the model's logits β the resolved pooler/classifier "
"are not the real ones. Do NOT export; fix the *_NAMES resolution first.")
if pooled.shape[0] != 1 or pooled.dim() != 2:
raise RuntimeError(f"expected pooled [1, D], got {tuple(pooled.shape)}")
print(f"[verify] embedding shape {tuple(pooled.shape)} "
f"(GraphicProbe.weights.count must equal {pooled.shape[1]})", flush=True)
cos_oracle = torch.nn.functional.cosine_similarity(
logits.reshape(-1), ref_logits.reshape(-1), dim=0).item()
print(f"[gate] torch wrapper vs oracle logits cos={cos_oracle:.6f}", flush=True)
# Persist the fp32 torch embedding as the reference for gate #2 β and for any FUTURE re-gate,
# which then needs neither torch nor a network.
ref_embedding = pooled.float().cpu().numpy()
np.save(os.path.join(HERE, "oracle_embedding.npy"), ref_embedding)
print(f"[gate] saved oracle_embedding.npy {ref_embedding.shape}", flush=True)
# --- fp16 export (precision follows traced dtype) -------------------------------------------
w = w.half()
ref_inputs = {"pixel_values_videos": px.half()}
print("[export] export_to_coreai (fp16, 2 outputs)β¦", flush=True)
prog = export_to_coreai(w, ref_inputs, dynamic_shapes=None,
input_names=("pixel_values_videos",),
output_names=("logits", "embedding"),
state_names=None)
print("[export] EXPORT OK β
", flush=True)
prog.optimize()
if OUT_DIR.exists():
shutil.rmtree(OUT_DIR)
OUT_DIR.mkdir(parents=True)
aim = OUT_DIR / AIM_NAME
import coreai.runtime as rt
prog.save_asset(aim, rt.AIModelAssetMetadata())
print("[export] saved", aim, flush=True)
return aim, px, ref_logits.numpy(), ref_embedding
async def gate(aim, px, ref_logits, ref_embedding):
import coreai.runtime as rt
def cos(a, b):
a, b = a.ravel(), b.ravel()
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9))
gpu = rt.SpecializationOptions.from_preferred_compute_unit_kind(rt.ComputeUnitKind.gpu())
fn = (await rt.AIModel.load(str(aim), gpu)).load_function("main")
inp = rt.NDArray(np.ascontiguousarray(px.numpy().astype(np.float16)))
out = await fn(inputs={"pixel_values_videos": inp})
missing = [k for k in ("logits", "embedding") if k not in out]
if missing:
raise RuntimeError(f"bundle is missing output(s) {missing}; got {list(out.keys())}")
lg = out["logits"].numpy().astype(np.float32)
emb = out["embedding"].numpy().astype(np.float32)
c_log = cos(lg, ref_logits)
t_ref = np.argsort(-ref_logits.ravel())[:5]
t_eng = np.argsort(-lg.ravel())[:5]
print(f"[gate] LOGITS cos={c_log:.6f} top5 ref={t_ref.tolist()} eng={t_eng.tolist()}", flush=True)
c_emb = cos(emb, ref_embedding)
rel_l2 = float(np.linalg.norm(emb.ravel() - ref_embedding.ravel()) /
(np.linalg.norm(ref_embedding.ravel()) + 1e-9))
print(f"[gate] EMBEDDING cos={c_emb:.6f} relL2={rel_l2:.3e} shape={emb.shape}", flush=True)
ok_log = c_log > LOGIT_COS_BAR and t_ref[0] == t_eng[0]
ok_emb = c_emb > EMBED_COS_BAR and emb.shape[-1] == ref_embedding.shape[-1]
print(f"[gate] logits {'PASS β
' if ok_log else 'FAIL β'} (bar {LOGIT_COS_BAR}, top1 match)", flush=True)
print(f"[gate] embed {'PASS β
' if ok_emb else 'FAIL β'} (bar {EMBED_COS_BAR}, dim match)", flush=True)
print("[gate] OVERALL PASS β
" if (ok_log and ok_emb) else "[gate] OVERALL FAIL β", flush=True)
if ok_log and ok_emb:
print(f"\nNext: point the calibration harness at this bundle β\n"
f" python3 /Volumes/Satechi/Development/mlxengine-forge/Tools/hintcal/collect_coreai.py \\\n"
f" --model {aim}", flush=True)
if __name__ == "__main__":
aim, px, ref_logits, ref_embedding = build()
asyncio.run(gate(aim, px, ref_logits, ref_embedding))
|