Skip to content

Fixtures and golden references

This page mirrors tests/fixtures/README.md from the repository.

Parity fixtures

Everything the parity tests need: small media inputs (media/) and PyTorch golden outputs (ref/). Only the eight COCO images are tracked in git; the clips and the dumps are git-ignored, and the dumps are either downloaded — they are published as the dataset jepacpp/jepa.cpp-fixtures, 156 files, 660 MB — or regenerated locally:

scripts/download_fixtures.sh                     # media (8 COCO jpgs + 6 Kinetics-mini clips) + all of ref/
scripts/download_fixtures.sh ref lewm-pusht      # or: media | ref | ref <model>...
scripts/dump_reference.py --model all            # regenerate ref/ instead (~1 min on 32 cores; needs the torch venv)
scripts/compare.py OUT_DIR tests/fixtures/ref/<model>   # jepa.cpp output vs reference, non-zero exit on regression

The dataset carries ref/ only. The COCO images and the Kinetics clips stay at their own sources (cocodataset.org and nateraw/kinetics-mini) and are not redistributed there; download_fixtures.sh fetches them from those. The dumps themselves are CC BY-NC 4.0 — the most restrictive licence among the checkpoints whose outputs they store, I-JEPA's and LeVJEPA's.

dump_reference.py needs the project venv (.venv, torch CPU + transformers + av + pillow + timm + einops), the checkpoints from scripts/download_models.sh --convert under models/, and for vjepa2_1-vitb-384 a clone of facebookresearch/vjepa2 at tmp/vjepa2-src (cloned automatically if absent). For lejepa-vits16 the model directory must also contain the hf_src/ package of the HF repo (its modelling_vitv2.py imports it); levjepa-vitl16 loads the modeling_levjepa.py / configuration_levjepa.py that ship next to its weights (trust_remote_code=True). Use --root to point at another checkout; caches go to <root>/tmp/hf-home and <root>/tmp/torch-home. Everything runs in float32 eval mode, 32 threads (--threads), no autocast.

media/

file content notes
coco_*.jpg (8) COCO val2017 images, various sizes (e.g. 640x480, 480x640) tracked in git
archery.mp4 Kinetics-mini val, 300 frames, 480x360, 29.97 fps, h264 yuv420p primary clip
bowling.mp4 Kinetics-mini val, 150 frames, 480x270, 15 fps primary clip
flying_kite.mp4, high_jump.mp4, marching.mp4, high_jump2.mp4 more Kinetics-mini clips (10 s each) spare; --n-clips N

ref// layout

One directory per model with a manifest.json and one .npy per tensor per sample, named <sample>.<tensor>.npy. All arrays are float32 C-order except frames_u8 (uint8) and top5_idx (int64). Shapes have no batch dim unless the tensor is the model input (input), which is stored exactly as fed to the model (batch = 1).

Manifest schema (abridged):

{
 "model": "vjepa2-vitl-fpc64-256", "source": "/abs/path/models/facebook/vjepa2-vitl-fpc64-256", "hf_id": "...",
 "dtype": "float32", "framework": {"torch": "2.13.0+cpu", "transformers": "5.16.1", "threads": 32, ...},
 "hparams": {"embed_dim": 1024, "n_layer": 24, "n_head": 16, "patch_size": 16, "tubelet_size": 2, "ln_eps": 1e-6, ...},
 "preprocessing": {"description": "...exact pipeline...", "resize": {...}, "center_crop": 256, "mean": [...], "std": [...],
                   "processor": {"class": "VJEPA2VideoProcessor", "backend": "torchvision", "config": {...}}},
 "outputs": {"last_hidden_state": "what this tensor is", ...},
 "labels": ["..."],                               // classifiers only (id2label in order)
 "timing_s": {"model_load": 2.1, "forward_total": 9.8, "forward_mean": 2.4, "wall_total": 14.0},
 "samples": [
   {"name": "archery_f64", "media": "archery.mp4", "frames": 64, "frame_indices": [0, 5, ...], "n_frames_total": 300, "fps": 29.97,
    "timing_s": {"decode_s": 0.4, "preprocess_s": 0.2, "forward_s": 7.9},
    "tensors": {"input": {"file": "archery_f64.input.npy", "shape": [1, 64, 3, 256, 256], "dtype": "float32", "layout": "NTCHW"}, ...}}
 ]
}

timing_s.forward_s is the PyTorch CPU wall time of the model forward alone (no decode / preprocessing) and is the baseline for speed comparisons in docs/parity.md.

Per-model tensors

Token order is always t-major, then h, then w (images: h-major then w); hfvit-style models and levjepa put CLS first.

model samples tensors (shape)
ijepa-vith14-1k 8 images input [1,3,224,224] NCHW · last_hidden_state [256,1280] (after final LN, no CLS) · pooled_mean [1280]
lejepa-vits16 8 images input [1,3,224,224] · last_hidden_state [197,384] = [CLS; 196 patches] after the final LN (ViTv2.forward_backbone) · cls [384] · cls_raw [384] (CLS before the final LN) · pooled_mean [384] (mean of the 196 patch tokens)
lewm-pusht 2 images + seq per image: input [1,3,224,224] · last_hidden_state [257,192] (HF ViT, CLS first, after final LN) · cls [192] · emb [192] = projector(cls) · action [10] · act_emb [192] · pred_next [192] (one predictor step, T=1). seq: input [3,3,224,224] · emb_seq/act_emb_seq/pred_seq [3,192] · action_seq [3,10] (T=3 causal rollout, prediction at every step)
vjepa2-vitl-fpc64-256 archery/bowling x {64, 16} frames frames_u8 [T,H,W,3] uint8 · input [1,T,3,256,256] NTCHW · last_hidden_state [T/2·256, 1024] · pooled_mean [1024] · predictor_last_hidden_state [T/2·256, 1024] (default masks: full context, predict every token)
vjepa2-vitl-fpc16-256-ssv2 archery/bowling x 16 frames frames_u8 · input [1,16,3,256,256] · last_hidden_state [2048,1024] · pooled [1024] (attentive-pooler output = classifier input, via forward hook) · logits [174] · top5_idx [5] int64; labels (id2label) in the manifest
vjepa2_1-vitb-384 archery x 16 frames + 2 images frames_u8 · input [1,3,16,384,384] NCTHW (video) / [1,3,1,384,384] (image path: patch_embed_img + img_mod_embed) · last_hidden_state [4608,768] / [576,768] = norms_block[-1](x) (the encoder's default inference output) · pooled_mean [768]
levjepa-vitl16 archery/bowling x 16 frames + 2 images frames_u8 [16,H,W,3] uint8 · input [1,3,16,224,224] NCTHW · last_hidden_state [3137,1024] = [CLS; 16·14·14 patches] after the final LN · cls [1024] (= pooler_output, the feature this model is used through) · pooled_mean [1024] (mean of the 3136 patch tokens). The two image samples take the model card's still-image path: frames_u8 is the one decoded frame repeated 16 times, so the stored input and our own preprocessing describe the same clip

Preprocessing actually applied (also in each manifest)

All resizing is torchvision.transforms.v2.functional.resize on the uint8 CHW tensor with antialias=True (rounded back to uint8), which is what every transformers>=5 image/video processor does (TorchvisionBackend); verified against a re-implementation to 2.4e-7, whereas PIL resampling differs by up to 1.8e-2 (1-2 uint8 levels). Center crop uses top = int((H-c)/2), left = int((W-c)/2); short-side resize keeps the aspect ratio with the long side floored: other = int(s * other / short) (transformers get_resize_output_image_size; e.g. 640×426 → 256 short gives 384, not 385).

model pipeline
ijepa-vith14-1k ViTImageProcessor: resize to exactly 224x224 (aspect not kept), bilinear; x/255; mean = std = 0.5
lejepa-vits16 BitImageProcessor: short side -> 256, bicubic; center crop 224; x/255; ImageNet mean/std
lewm-pusht resize to exactly 224x224 (aspect not kept), bilinear; x/255; ImageNet mean/std (upstream trains/evals on 224x224 renders, so the resize only exists to feed COCO images)
vjepa2-* VJEPA2VideoProcessor: per frame, short side -> 292 = int(256*256/224), bilinear; center crop 256; x/255; ImageNet mean/std; output [B,T,3,H,W]
vjepa2_1-vitb-384 same processor with crop 384 (short side -> 438), then permuted to [B,3,T,H,W]; images are 1-frame videos
levjepa-vitl16 no processor ships with the checkpoint: per frame, short side -> 224, bicubic; center crop 224; x/255; ImageNet mean/std; output [B,3,T,H,W]. The model card's notebook uses PIL BICUBIC instead of the torchvision resampler used here — measured on archery_f16, 99.599 % of the normalised values are bit-identical, the largest difference is 2.02 uint8 levels and exactly 1 value of 2 408 448 is off by more than one level, worth a worst-token cosine of 0.999984 (median 1.000000, CLS 1.000000) through the encoder

Frame sampling for clips: all frames decoded with PyAV (rgb24), then idx = round(linspace(0, T_total-1, n)); the indices are stored per sample so the C++ side can be fed frames_u8 directly (the library has no video decoder — jepa_encode is frames-in). tests/test-video.cpp (ctest -R video) replays each of those samples through the tools' own ffmpeg ingest, tools/video-decode.cpp, and requires the same total frame count, the same indices and byte-identical pixels — which is what makes jepa-embed --video archery.mp4 --frames 16 interchangeable with --frames-npy archery_f16.frames_u8.npy.

The parity test feeds the stored input tensor first (bypassing preprocessing) and only then runs our own preprocessor, so a preprocessing mismatch shows up separately from a graph bug.

LeWM: what the predictor consumes

Documented in ref/lewm-pusht/manifest.json (predictor_semantics), in short: the encoder is a stock HF ViTModel ViT-Ti/14 @224 (257 tokens, learned pos-embed incl. CLS slot, LayerNorm eps 1e-12, GELU-erf). Only the CLS token is used: emb = projector(cls) with projector = Linear(192->2048) -> BatchNorm1d (eval: running stats, eps 1e-5) -> GELU -> Linear(2048->192). The action (10 = frameskip 5 x 2, z-scored at train time; here N(0,1) with seed 0) goes through Embedder = Conv1d(10->10, k=1) -> Linear(10->768) -> SiLU -> Linear(768->192). The predictor adds pos_embedding[:T] (T<=3) and runs 6 AdaLN-zero blocks conditioned per step on act_emb (SiLU -> Linear(192->1152) -> shift/scale/gate for attn and MLP; non-affine LN eps 1e-6), each with an inner affine LayerNorm before the qkv (16 heads x 64, no qkv bias, causal) and before the MLP (192->2048->192, GELU), then a final LayerNorm(192). pred_next = pred_proj(out[:, -1]) (same MLP shape as the projector) is the predicted projected embedding of the next frame. weights.pt loads strictly into this re-implementation.

compare.py

scripts/compare.py A B [--topk 5] [--tensors t1,t2] [--skip frames_u8] [--min-cos 0.9999] [--max-rel 1e-3] [--max-abs X]
                       [--min-top1 1.0] [--min-topk F] [--json rows.json] [--quiet]

A is the candidate, B the reference; both .npy files or both directories (samples matched by name via manifest.json, or by <sample>.<tensor>.npy file names). Per tensor it prints the per-token cosine (mean and worst), max abs error, rel_max = max|a-b| / max|b|, rel_fro = ||a-b|| / ||b||, and for logits (name substring, --logits) the top-1 match and top-k overlap; integer tensors (top5_idx) are compared as sets. Exit status 1 if any threshold is violated. The defaults are the F32 thresholds of docs/architecture.md (cosine >= 0.9999, relative max-abs <= 1e-3, top-1 = 100 %); for Q8_0 use --min-cos 0.999 --max-rel -1 --min-top1 0. compare_arrays() / compare_dirs() are importable for the C++ test's Python twin.