V-JEPA2 ViT-L SSv2 two-output Core AI bundle (logits + [1024] router embedding)
Browse files- .gitattributes +1 -0
- README.md +115 -0
- export_fp16_embed.py +266 -0
- oracle_embedding.npy +3 -0
- oracle_input.npy +3 -0
- oracle_logits.npy +3 -0
- vjepa2_ssv2_embed_fp16.aimodel/main.hash +1 -0
- vjepa2_ssv2_embed_fp16.aimodel/main.mlirb +3 -0
- vjepa2_ssv2_embed_fp16.aimodel/metadata.json +5 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
vjepa2_ssv2_embed_fp16.aimodel/main.mlirb filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
base_model: facebook/vjepa2-vitl-fpc16-256-ssv2
|
| 4 |
+
tags:
|
| 5 |
+
- core-ai
|
| 6 |
+
- video-classification
|
| 7 |
+
- video-understanding
|
| 8 |
+
- vjepa2
|
| 9 |
+
- apple-silicon
|
| 10 |
+
- feature-extraction
|
| 11 |
+
library_name: coreai
|
| 12 |
+
pipeline_tag: video-classification
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
# V-JEPA 2 ViT-L (SSv2) — Core AI, **two-output** (logits + router embedding)
|
| 16 |
+
|
| 17 |
+
A Core AI export of [`facebook/vjepa2-vitl-fpc16-256-ssv2`](https://huggingface.co/facebook/vjepa2-vitl-fpc16-256-ssv2)
|
| 18 |
+
(375M, MIT) that returns **both** the SSv2 action logits **and the pooled `[1,1024]` router
|
| 19 |
+
embedding**.
|
| 20 |
+
|
| 21 |
+
```
|
| 22 |
+
pixel_values_videos [1,16,3,256,256] fp16
|
| 23 |
+
├─ logits [1,174] SSv2 action classes
|
| 24 |
+
└─ embedding [1,1024] pooled attentive-pooler output
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
## Why two outputs
|
| 28 |
+
|
| 29 |
+
The stock export returns `logits` only. That is the wrong contract for anything using V-JEPA 2 as a
|
| 30 |
+
**content/router feature extractor** rather than an action classifier: the SSv2 checkpoint ships no
|
| 31 |
+
label names, its vocabulary is hand-object interactions, and it is semantically wrong for most
|
| 32 |
+
non-action domains. The `[1024]` pooled embedding is the reusable signal — feed it to a small
|
| 33 |
+
calibrated head for your own task.
|
| 34 |
+
|
| 35 |
+
Built for a video-optimization planner that fits a logistic probe over this embedding to pre-classify
|
| 36 |
+
content before an encode search.
|
| 37 |
+
|
| 38 |
+
## Measured
|
| 39 |
+
|
| 40 |
+
Apple **M5 Max** / 128 GB, fp16, GPU:
|
| 41 |
+
|
| 42 |
+
| | |
|
| 43 |
+
|---|---|
|
| 44 |
+
| Bundle | **675 MB** |
|
| 45 |
+
| Load | **0.16 s** |
|
| 46 |
+
| Warm forward (16-frame clip) | **44 ms** |
|
| 47 |
+
| Engine logits vs torch oracle | cos **0.999964**, top-1 match |
|
| 48 |
+
| Engine embedding vs torch reference | cos **0.999940**, relL2 1.1e-02 (fp16 noise) |
|
| 49 |
+
| Recomposition `classifier(pooler(backbone(x)))` vs `.logits` | cos **1.0000000**, max\|Δ\| **0** — bit-exact |
|
| 50 |
+
|
| 51 |
+
⚠️ Latency is silicon-specific. Published M4 Max figures for the single-output bundle are 150–180 ms;
|
| 52 |
+
that is **different hardware**, not a difference between these bundles.
|
| 53 |
+
|
| 54 |
+
**Embedding separation** (6 signage clips, a semantic check no numeric gate performs): pairwise
|
| 55 |
+
cosine spans **0.146 → 0.987**, and the highest-similarity pair is the same content at two different
|
| 56 |
+
encodes (`_1080p` vs `_master`, cos 0.987). Content-identical inputs collapse together; unrelated
|
| 57 |
+
content separates.
|
| 58 |
+
|
| 59 |
+
## Preprocessing — on the host
|
| 60 |
+
|
| 61 |
+
The model does **not** normalize internally.
|
| 62 |
+
|
| 63 |
+
1. 16 frames, uniformly sampled across the clip
|
| 64 |
+
2. aspect-fill to 256×256, centre-crop (not a squash)
|
| 65 |
+
3. RGB, 0..1
|
| 66 |
+
4. ImageNet mean `[0.485, 0.456, 0.406]` / std `[0.229, 0.224, 0.225]`
|
| 67 |
+
5. `[1,16,3,256,256]`, fp16
|
| 68 |
+
|
| 69 |
+
## Use
|
| 70 |
+
|
| 71 |
+
```python
|
| 72 |
+
import asyncio, numpy as np, coreai.runtime as rt
|
| 73 |
+
|
| 74 |
+
async def main():
|
| 75 |
+
opts = rt.SpecializationOptions.from_preferred_compute_unit_kind(rt.ComputeUnitKind.gpu())
|
| 76 |
+
fn = (await rt.AIModel.load("vjepa2_ssv2_embed_fp16.aimodel", opts)).load_function("main")
|
| 77 |
+
x = ... # [1,16,3,256,256] fp16, preprocessed as above
|
| 78 |
+
out = await fn(inputs={"pixel_values_videos": rt.NDArray(np.ascontiguousarray(x))})
|
| 79 |
+
logits = out["logits"].numpy() # [1,174]
|
| 80 |
+
embedding = out["embedding"].numpy() # [1,1024] ← the signal
|
| 81 |
+
|
| 82 |
+
asyncio.run(main())
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
## Reproduce
|
| 86 |
+
|
| 87 |
+
`export_fp16_embed.py` is included. It needs `coreai_models`, torch, and transformers ≥4.57, plus
|
| 88 |
+
oracles from the reference run (`oracle_*.npy` are included here, so the gate can be re-run without
|
| 89 |
+
recomputing them).
|
| 90 |
+
|
| 91 |
+
Two things in that script are load-bearing and easy to lose in a re-derivation:
|
| 92 |
+
|
| 93 |
+
- **`skip_predictor=True`.** `VJEPA2ForVideoClassification.forward` passes it; `VJEPA2Model` defaults
|
| 94 |
+
it to `False`, which runs the entire JEPA *predictor* stack. `last_hidden_state` is
|
| 95 |
+
`sequence_output` either way, so omitting it changes **no number** — every parity gate still
|
| 96 |
+
passes, and the bundle silently carries a transformer stack nothing reads. **Parity gates certify
|
| 97 |
+
values, never graph extent.**
|
| 98 |
+
- **The 3D-RoPE `squeeze(-1)` overlay.** The reference squeezes a non-1 dim (a torch no-op); the
|
| 99 |
+
converter maps it to `ShrinkDims`, which requires size 1, and fails. Patched out, math unchanged.
|
| 100 |
+
|
| 101 |
+
The submodule resolution is discover-then-prove: it resolves `backbone`/`pooler`/`classifier` by
|
| 102 |
+
candidate name, then asserts the recomposition reproduces the model's own `.logits` before exporting.
|
| 103 |
+
If a future transformers renames them, it fails loudly instead of shipping a wrong "embedding".
|
| 104 |
+
|
| 105 |
+
## Files
|
| 106 |
+
|
| 107 |
+
| | |
|
| 108 |
+
|---|---|
|
| 109 |
+
| `vjepa2_ssv2_embed_fp16.aimodel/` | the bundle (`metadata.json`, `main.mlirb`, `main.hash`) |
|
| 110 |
+
| `export_fp16_embed.py` | the exact export that produced it |
|
| 111 |
+
| `oracle_input.npy` / `oracle_logits.npy` / `oracle_embedding.npy` | gate fixtures |
|
| 112 |
+
|
| 113 |
+
## License
|
| 114 |
+
|
| 115 |
+
MIT, inherited from Meta's V-JEPA 2.
|
export_fp16_embed.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Community port — export V-JEPA 2 (SSv2) to Core AI, fp16, TWO outputs: logits + pooled embedding.
|
| 2 |
+
"""Variant of `export_fp16.py` for ForgeOptimizerKit's §6.3 planner-hint seam.
|
| 3 |
+
|
| 4 |
+
WHY A VARIANT EXISTS. The shipped bundle returns `logits [1,174]` only. Forge does not use those
|
| 5 |
+
labels — `mlxengine-forge/Docs/VJEPA2-HINT-ADAPTER.md` is explicit ("labels are useless, the
|
| 6 |
+
embedding is the signal"): the SSv2 checkpoint ships no label names, its vocabulary is hand-object
|
| 7 |
+
actions, and it is semantically wrong for signage. The planner's actual signal is the pooled
|
| 8 |
+
**[1,1024]** router embedding that `ForgeCore.GraphicProbe` is fitted over. So this export exposes
|
| 9 |
+
both outputs; everything else — the RoPE overlay, fp16, the externalize-drop — is unchanged.
|
| 10 |
+
|
| 11 |
+
THE GATE IS DUAL, and the second half is the point:
|
| 12 |
+
1. `logits` vs the existing oracle → proves nothing regressed relative to the verified bundle.
|
| 13 |
+
2. `embedding` vs a torch reference → proves the NEW output is the vector the probe consumes.
|
| 14 |
+
Parity on logits does not by itself certify the embedding. logits = classifier(pooled) through a
|
| 15 |
+
single Linear, so a high logit cosine is strong evidence — but `GraphicProbe.evaluate` takes a
|
| 16 |
+
1024-dot-product near a decision boundary, and "strong evidence" is not a measurement. Gate #2 is
|
| 17 |
+
held tighter than #1 for exactly that reason.
|
| 18 |
+
|
| 19 |
+
Writes to `artifacts/vjepa2_ssv2_fp16_embed/` — a SEPARATE directory, so a failed run here can
|
| 20 |
+
never clobber the published, verified single-output bundle.
|
| 21 |
+
|
| 22 |
+
Run: python3 export_fp16_embed.py
|
| 23 |
+
Prereq: `reference_run.py` once, for oracle_input.npy / oracle_logits.npy.
|
| 24 |
+
"""
|
| 25 |
+
import os, shutil, asyncio, inspect, numpy as np, torch
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
|
| 28 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 29 |
+
os.environ.setdefault("HF_HUB_OFFLINE", "1"); os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
|
| 30 |
+
|
| 31 |
+
import coreai_models.export.macos as _macos
|
| 32 |
+
from coreai_models.export.macos import export_to_coreai
|
| 33 |
+
_DROP = {"scaled_dot_product_attention", "rope"}
|
| 34 |
+
_macos._EXTERNALIZE_SPECS = [s for s in _macos._EXTERNALIZE_SPECS if s.composite_op_name not in _DROP]
|
| 35 |
+
|
| 36 |
+
from transformers import VJEPA2ForVideoClassification
|
| 37 |
+
import transformers.models.vjepa2.modeling_vjepa2 as _vj
|
| 38 |
+
|
| 39 |
+
REPO = "facebook/vjepa2-vitl-fpc16-256-ssv2"
|
| 40 |
+
OUT_DIR = Path(HERE) / "artifacts" / "vjepa2_ssv2_fp16_embed"
|
| 41 |
+
AIM_NAME = "vjepa2_ssv2_embed_fp16.aimodel"
|
| 42 |
+
|
| 43 |
+
# Gate bars. Logits mirror the original export's bar; the embedding is held tighter because it is
|
| 44 |
+
# the input to a linear probe whose class flips at the boundary.
|
| 45 |
+
LOGIT_COS_BAR = 0.99
|
| 46 |
+
EMBED_COS_BAR = 0.999
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _rotate_queries_or_keys(x, pos):
|
| 50 |
+
# Reference copy MINUS the no-op `squeeze(-1)` on a size-D/2 dim (torch ignores it; the Core AI
|
| 51 |
+
# converter maps it to ShrinkDims which requires size 1 and fails). Math unchanged.
|
| 52 |
+
# Identical to export_fp16.py — the overlay is mandatory for ANY V-JEPA2 export, not incidental
|
| 53 |
+
# to the single-output one.
|
| 54 |
+
B, num_heads, N, D = x.size()
|
| 55 |
+
omega = torch.arange(D // 2, dtype=x.dtype, device=x.device)
|
| 56 |
+
omega /= D / 2.0
|
| 57 |
+
omega = 1.0 / 10000**omega
|
| 58 |
+
freq = pos.unsqueeze(-1) * omega
|
| 59 |
+
emb_sin = freq.sin().repeat(1, 1, 1, 2)
|
| 60 |
+
emb_cos = freq.cos().repeat(1, 1, 1, 2)
|
| 61 |
+
y = x.unflatten(-1, (-1, 2))
|
| 62 |
+
y1, y2 = y.unbind(dim=-1)
|
| 63 |
+
y = torch.stack((-y2, y1), dim=-1)
|
| 64 |
+
y = y.flatten(-2)
|
| 65 |
+
return (x * emb_cos) + (y * emb_sin)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
_vj.rotate_queries_or_keys = _rotate_queries_or_keys
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# ---------------------------------------------------------------------------------------------
|
| 72 |
+
# Submodule resolution — discovered and VERIFIED, never assumed.
|
| 73 |
+
#
|
| 74 |
+
# The single-output export could wrap the whole model and take `.logits`, so it never had to know
|
| 75 |
+
# the internal layout. Reaching `pooled` does. Rather than hardcode attribute names against one
|
| 76 |
+
# transformers version, resolve by candidate list and then PROVE the recomposition reproduces the
|
| 77 |
+
# model's own logits. If the names drift, this fails loudly at author time with the real structure
|
| 78 |
+
# printed — instead of shipping a bundle whose "embedding" is some other tensor.
|
| 79 |
+
# ---------------------------------------------------------------------------------------------
|
| 80 |
+
BACKBONE_NAMES = ("vjepa2", "model", "vjepa2_model", "backbone", "encoder")
|
| 81 |
+
POOLER_NAMES = ("pooler", "attentive_pooler", "classifier_pooler")
|
| 82 |
+
CLASSIFIER_NAMES = ("classifier", "head", "class_head", "score")
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _first_attr(obj, names, what):
|
| 86 |
+
for n in names:
|
| 87 |
+
mod = getattr(obj, n, None)
|
| 88 |
+
if mod is not None and isinstance(mod, torch.nn.Module):
|
| 89 |
+
print(f"[resolve] {what} -> .{n} ({mod.__class__.__name__})", flush=True)
|
| 90 |
+
return mod
|
| 91 |
+
children = [f"{n} ({m.__class__.__name__})" for n, m in obj.named_children()]
|
| 92 |
+
raise RuntimeError(
|
| 93 |
+
f"could not resolve the {what} among {names}.\n"
|
| 94 |
+
f" actual children: {children}\n"
|
| 95 |
+
f" Fix: add the real attribute name to the *_NAMES list above and re-run — the "
|
| 96 |
+
f"verification step below will confirm the choice."
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def _hidden(out):
|
| 101 |
+
"""Backbone output -> last hidden state. Branches on Python type, so it resolves at trace
|
| 102 |
+
time; it is not a data-dependent condition and does not enter the exported graph."""
|
| 103 |
+
if hasattr(out, "last_hidden_state"):
|
| 104 |
+
return out.last_hidden_state
|
| 105 |
+
if isinstance(out, (tuple, list)):
|
| 106 |
+
return out[0]
|
| 107 |
+
return out
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
class VJEPA2ExportWithEmbedding(torch.nn.Module):
|
| 111 |
+
"""`pixel_values_videos [1,16,3,256,256]` -> (`logits [1,174]`, `embedding [1,1024]`).
|
| 112 |
+
|
| 113 |
+
Output ORDER is load-bearing: it pairs with `output_names=("logits", "embedding")` below, and
|
| 114 |
+
the harness reads results by name — but a swap here would silently rename both tensors.
|
| 115 |
+
The gate catches it (an embedding scored against oracle logits cannot reach 0.999).
|
| 116 |
+
|
| 117 |
+
🔑 `skip_predictor=True` IS NOT OPTIONAL, and it is invisible to the gate. The reference
|
| 118 |
+
`VJEPA2ForVideoClassification.forward` passes it (modeling_vjepa2.py:1197); `VJEPA2Model`
|
| 119 |
+
defaults it to **False**, in which case it runs the entire JEPA *predictor* — a second
|
| 120 |
+
transformer stack — and files the result under a SEPARATE output field. `last_hidden_state`
|
| 121 |
+
is `sequence_output` either way (modeling_vjepa2.py:1091/1118), so omitting the flag changes
|
| 122 |
+
no number: the recomposition check below still passes, the logits still match the oracle, and
|
| 123 |
+
the bundle is simply much larger and slower for a stack nothing reads. A numeric gate cannot
|
| 124 |
+
see this class of defect — only reading the reference can.
|
| 125 |
+
"""
|
| 126 |
+
|
| 127 |
+
def __init__(self, backbone, pooler, classifier, skip_predictor: bool):
|
| 128 |
+
super().__init__()
|
| 129 |
+
self.backbone = backbone
|
| 130 |
+
self.pooler = pooler
|
| 131 |
+
self.classifier = classifier
|
| 132 |
+
# Plain bool read at trace time — a Python branch, not a graph condition.
|
| 133 |
+
self.skip_predictor = skip_predictor
|
| 134 |
+
|
| 135 |
+
def forward(self, pixel_values_videos):
|
| 136 |
+
if self.skip_predictor:
|
| 137 |
+
out = self.backbone(pixel_values_videos=pixel_values_videos, skip_predictor=True)
|
| 138 |
+
else:
|
| 139 |
+
out = self.backbone(pixel_values_videos=pixel_values_videos)
|
| 140 |
+
h = _hidden(out)
|
| 141 |
+
pooled = self.pooler(h)
|
| 142 |
+
# The attentive pooler may emit [B, 1, D] (one learned query) or [B, D]. Flatten to
|
| 143 |
+
# [B, D] so the published contract is one fixed rank regardless of the version's shape.
|
| 144 |
+
pooled = pooled.reshape(pooled.shape[0], -1)
|
| 145 |
+
logits = self.classifier(pooled)
|
| 146 |
+
return logits, pooled
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def build():
|
| 150 |
+
model = VJEPA2ForVideoClassification.from_pretrained(REPO, dtype=torch.float32).eval()
|
| 151 |
+
|
| 152 |
+
px = torch.from_numpy(np.load(os.path.join(HERE, "oracle_input.npy"))) # [1,16,3,256,256]
|
| 153 |
+
ref_logits = torch.from_numpy(np.load(os.path.join(HERE, "oracle_logits.npy"))) # [1,174]
|
| 154 |
+
|
| 155 |
+
backbone = _first_attr(model, BACKBONE_NAMES, "backbone")
|
| 156 |
+
pooler = _first_attr(model, POOLER_NAMES, "attentive pooler")
|
| 157 |
+
classifier = _first_attr(model, CLASSIFIER_NAMES, "classifier")
|
| 158 |
+
|
| 159 |
+
# Detected rather than assumed, same discipline as the names above: on a transformers that
|
| 160 |
+
# renamed or dropped the flag we fall back to the default path (correct numbers, fat graph)
|
| 161 |
+
# and SAY SO, instead of dying on an unexpected kwarg.
|
| 162 |
+
skip = "skip_predictor" in inspect.signature(backbone.forward).parameters
|
| 163 |
+
print(f"[resolve] backbone skip_predictor supported={skip}"
|
| 164 |
+
+ ("" if skip else " ⚠️ predictor stack will be traced into the graph — see class docstring"),
|
| 165 |
+
flush=True)
|
| 166 |
+
|
| 167 |
+
w = VJEPA2ExportWithEmbedding(backbone, pooler, classifier, skip_predictor=skip).eval()
|
| 168 |
+
|
| 169 |
+
# --- Verification: does the recomposition reproduce the model's own logits? -----------------
|
| 170 |
+
# This is what makes the resolution above trustworthy. If `pooled` were the wrong tensor, the
|
| 171 |
+
# classifier applied to it could not reproduce `.logits`.
|
| 172 |
+
with torch.inference_mode():
|
| 173 |
+
ref_out = model(pixel_values_videos=px).logits
|
| 174 |
+
logits, pooled = w(px)
|
| 175 |
+
|
| 176 |
+
cos_recompose = torch.nn.functional.cosine_similarity(
|
| 177 |
+
logits.reshape(-1), ref_out.reshape(-1), dim=0).item()
|
| 178 |
+
max_abs = (logits - ref_out).abs().max().item()
|
| 179 |
+
print(f"[verify] recomposed logits vs model.logits cos={cos_recompose:.8f} max|Δ|={max_abs:.3e}",
|
| 180 |
+
flush=True)
|
| 181 |
+
if cos_recompose < 0.99999:
|
| 182 |
+
raise RuntimeError(
|
| 183 |
+
"recomposition does not reproduce the model's logits — the resolved pooler/classifier "
|
| 184 |
+
"are not the real ones. Do NOT export; fix the *_NAMES resolution first.")
|
| 185 |
+
|
| 186 |
+
if pooled.shape[0] != 1 or pooled.dim() != 2:
|
| 187 |
+
raise RuntimeError(f"expected pooled [1, D], got {tuple(pooled.shape)}")
|
| 188 |
+
print(f"[verify] embedding shape {tuple(pooled.shape)} "
|
| 189 |
+
f"(GraphicProbe.weights.count must equal {pooled.shape[1]})", flush=True)
|
| 190 |
+
|
| 191 |
+
cos_oracle = torch.nn.functional.cosine_similarity(
|
| 192 |
+
logits.reshape(-1), ref_logits.reshape(-1), dim=0).item()
|
| 193 |
+
print(f"[gate] torch wrapper vs oracle logits cos={cos_oracle:.6f}", flush=True)
|
| 194 |
+
|
| 195 |
+
# Persist the fp32 torch embedding as the reference for gate #2 — and for any FUTURE re-gate,
|
| 196 |
+
# which then needs neither torch nor a network.
|
| 197 |
+
ref_embedding = pooled.float().cpu().numpy()
|
| 198 |
+
np.save(os.path.join(HERE, "oracle_embedding.npy"), ref_embedding)
|
| 199 |
+
print(f"[gate] saved oracle_embedding.npy {ref_embedding.shape}", flush=True)
|
| 200 |
+
|
| 201 |
+
# --- fp16 export (precision follows traced dtype) -------------------------------------------
|
| 202 |
+
w = w.half()
|
| 203 |
+
ref_inputs = {"pixel_values_videos": px.half()}
|
| 204 |
+
print("[export] export_to_coreai (fp16, 2 outputs)…", flush=True)
|
| 205 |
+
prog = export_to_coreai(w, ref_inputs, dynamic_shapes=None,
|
| 206 |
+
input_names=("pixel_values_videos",),
|
| 207 |
+
output_names=("logits", "embedding"),
|
| 208 |
+
state_names=None)
|
| 209 |
+
print("[export] EXPORT OK ✅", flush=True)
|
| 210 |
+
prog.optimize()
|
| 211 |
+
|
| 212 |
+
if OUT_DIR.exists():
|
| 213 |
+
shutil.rmtree(OUT_DIR)
|
| 214 |
+
OUT_DIR.mkdir(parents=True)
|
| 215 |
+
aim = OUT_DIR / AIM_NAME
|
| 216 |
+
|
| 217 |
+
import coreai.runtime as rt
|
| 218 |
+
prog.save_asset(aim, rt.AIModelAssetMetadata())
|
| 219 |
+
print("[export] saved", aim, flush=True)
|
| 220 |
+
return aim, px, ref_logits.numpy(), ref_embedding
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
async def gate(aim, px, ref_logits, ref_embedding):
|
| 224 |
+
import coreai.runtime as rt
|
| 225 |
+
|
| 226 |
+
def cos(a, b):
|
| 227 |
+
a, b = a.ravel(), b.ravel()
|
| 228 |
+
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9))
|
| 229 |
+
|
| 230 |
+
gpu = rt.SpecializationOptions.from_preferred_compute_unit_kind(rt.ComputeUnitKind.gpu())
|
| 231 |
+
fn = (await rt.AIModel.load(str(aim), gpu)).load_function("main")
|
| 232 |
+
inp = rt.NDArray(np.ascontiguousarray(px.numpy().astype(np.float16)))
|
| 233 |
+
out = await fn(inputs={"pixel_values_videos": inp})
|
| 234 |
+
|
| 235 |
+
missing = [k for k in ("logits", "embedding") if k not in out]
|
| 236 |
+
if missing:
|
| 237 |
+
raise RuntimeError(f"bundle is missing output(s) {missing}; got {list(out.keys())}")
|
| 238 |
+
|
| 239 |
+
lg = out["logits"].numpy().astype(np.float32)
|
| 240 |
+
emb = out["embedding"].numpy().astype(np.float32)
|
| 241 |
+
|
| 242 |
+
c_log = cos(lg, ref_logits)
|
| 243 |
+
t_ref = np.argsort(-ref_logits.ravel())[:5]
|
| 244 |
+
t_eng = np.argsort(-lg.ravel())[:5]
|
| 245 |
+
print(f"[gate] LOGITS cos={c_log:.6f} top5 ref={t_ref.tolist()} eng={t_eng.tolist()}", flush=True)
|
| 246 |
+
|
| 247 |
+
c_emb = cos(emb, ref_embedding)
|
| 248 |
+
rel_l2 = float(np.linalg.norm(emb.ravel() - ref_embedding.ravel()) /
|
| 249 |
+
(np.linalg.norm(ref_embedding.ravel()) + 1e-9))
|
| 250 |
+
print(f"[gate] EMBEDDING cos={c_emb:.6f} relL2={rel_l2:.3e} shape={emb.shape}", flush=True)
|
| 251 |
+
|
| 252 |
+
ok_log = c_log > LOGIT_COS_BAR and t_ref[0] == t_eng[0]
|
| 253 |
+
ok_emb = c_emb > EMBED_COS_BAR and emb.shape[-1] == ref_embedding.shape[-1]
|
| 254 |
+
print(f"[gate] logits {'PASS ✅' if ok_log else 'FAIL ❌'} (bar {LOGIT_COS_BAR}, top1 match)", flush=True)
|
| 255 |
+
print(f"[gate] embed {'PASS ✅' if ok_emb else 'FAIL ❌'} (bar {EMBED_COS_BAR}, dim match)", flush=True)
|
| 256 |
+
print("[gate] OVERALL PASS ✅" if (ok_log and ok_emb) else "[gate] OVERALL FAIL ❌", flush=True)
|
| 257 |
+
|
| 258 |
+
if ok_log and ok_emb:
|
| 259 |
+
print(f"\nNext: point the calibration harness at this bundle —\n"
|
| 260 |
+
f" python3 /Volumes/Satechi/Development/mlxengine-forge/Tools/hintcal/collect_coreai.py \\\n"
|
| 261 |
+
f" --model {aim}", flush=True)
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
if __name__ == "__main__":
|
| 265 |
+
aim, px, ref_logits, ref_embedding = build()
|
| 266 |
+
asyncio.run(gate(aim, px, ref_logits, ref_embedding))
|
oracle_embedding.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:de1533588e52a6775153e8aa6b4b525b3e721c18d7533db6348f54e680d8ab8d
|
| 3 |
+
size 4224
|
oracle_input.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:51a9713715e81ae7f7d5fd84b9098dae2af8fe8178c286301572c89ae9c17eb4
|
| 3 |
+
size 12583040
|
oracle_logits.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:69b9b75c5262560ac7b00b0e578e8445a844dbe1aaf8b2e703e8f6bfbc1860d7
|
| 3 |
+
size 824
|
vjepa2_ssv2_embed_fp16.aimodel/main.hash
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
-5�C(��1@��S�����%����h�u�-�{M
|
vjepa2_ssv2_embed_fp16.aimodel/main.mlirb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2d35c14328efca3140d9c453998caf98eb2515869b97dd681cf475c62db07b4d
|
| 3 |
+
size 707954202
|
vjepa2_ssv2_embed_fp16.aimodel/metadata.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"assetVersion" : "2.0",
|
| 3 |
+
"producer" : "coreai-core 1.0.0b2",
|
| 4 |
+
"creationDate" : "20260817T005719Z"
|
| 5 |
+
}
|