Skip to content

jepa.cpp — measured benchmarks

Raw measurement report — the curated view is Benchmarks → Performance.

Every number here comes from tools/jepa-bench on the box described below, on synthetic but deterministic input (a seeded uint8 stream put through the model's own jepa.pre.* normalisation), so the tables can be reproduced without the fixture media or a Python environment. They are cross-checked against docs/parity.md, which times the same graphs on the real reference clips (see the cross-check table below).

How to reproduce

git submodule update --init ggml
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release && cmake --build build -j 32

# the whole matrix at 32 threads (every f32/f16/q8_0 GGUF in models/gguf, every mode it supports)
scripts/bench_all.sh 32

# the q4 encoder rows, appended (the size/speed trade-off; --include-quants alone adds q5/q6 too)
scripts/bench_all.sh 32 --keep --include-quants --only '\-(q4_0|q4_k)$' --modes encoder

# the big configurations at 96 threads, appended to the same tmp/bench directory
scripts/bench_all.sh 96 --keep --only 'ijepa.*-f16|vjepa2-vitl.*-f16|vjepa2_1.*-f16'

# a single configuration by hand
build/jepa-bench -m models/gguf/vjepa2-vitl-fpc64-256-f16.gguf --frames 64 --threads 32,96 --md

bench_all.sh writes one JSON per (file, mode, shape) into tmp/bench/ plus a meta.json, then rebuilds this document with scripts/gen_benchmarks_md.py --bench-dir tmp/bench --ref-dir tests/fixtures/ref --parity docs/parity.md -o docs/benchmarks.md --results-json tests/results/benchmarks.json. tmp/bench/ is git-ignored; the committed tests/results/benchmarks.json is the machine-readable twin of every table below, one row per configuration, so a number quoted elsewhere in the repo can be traced without it. It passes each file's --ftype-label from the filename: general.file_type records the most common stored tensor type, and a q4_k mix falls back to q4_0 for every tensor whose rows are not a multiple of the 256-element super-block, so the small models' q4_k files read back as q4_0 and would otherwise be tabulated as such (ftype_gguf in the JSONs keeps what the file actually says).

Box and build

setting value
CPU AMD Ryzen Threadripper PRO 7995WX 96-Cores — 192 hardware threads, AVX-512
RAM 251 GB
Kernel 6.17.0-1032-oem
Compiler c++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0, -O3 -march=native (JEPA_NATIVE=ON)
ggml 36da5713, GGML_LLAMAFILE=ON
Attention ggml_flash_attn_ext; K/V dtype auto (F32 for f32 files, F16 otherwise)
Thread counts 32, 96

Measurement sessions (one bench_all.sh invocation each):

threads warmup + measured start end 1-min load avg note
32 1 + 3 2026-08-31 11:32 UTC 2026-08-31 11:37 UTC 0.04 → 30.84 official re-sweep, idle box (previous agents finished)
32 1 + 3 2026-08-31 11:39 UTC 2026-08-31 11:42 UTC 3.24 → 30.08 q4_0/q4_k encoder rows only (size/speed trade-off), idle box
96 1 + 3 2026-08-31 11:44 UTC 2026-08-31 11:46 UTC 3.14 → 65.00 the single budgeted 96-thread session, idle box
32 1 + 3 2026-08-31 23:02 UTC 2026-08-31 23:03 UTC 1.41 → 10.16 levjepa f32/f16/q8_0 encoder, idle box
96 1 + 3 2026-08-31 23:05 UTC 2026-08-31 23:05 UTC 1.81 → 9.35 levjepa 96-thread row, idle box
32 1 + 3 2026-08-31 23:13 UTC 2026-08-31 23:13 UTC 0.82 → 7.80 levjepa q4_0/q4_k encoder rows, idle box

The 1-minute load average is recorded per session, before the first run and after the last (out of 192 hardware threads; a session's own run contributes its thread count, which is what the end-of-session figure mostly is). Every session here started on an idle box — the highest starting load average is 3.24 — so ms mean is a fair figure and ms min sits within a per cent or two of it.

What the milliseconds are. ms is the wall time of ggml_backend_graph_compute for the named graph (jepa_context_last_compute_ms) — model load, graph build/allocation, the host-side patchify and the output copy are excluded. Measured as wall_ms_mean − ms_mean over this sweep's own encoder runs at 32 threads they add 0.3–0.8 ms up to 1024 tokens and 3.4–98.9 ms above it (dominated by the patchify and the output copy, not by graph build). The JSONs keep the full API-call time as wall_ms_mean. tokens/s is tokens / ms_mean. peak RSS is the process VmHWM after the run, i.e. weights + the largest graph allocation, not a per-graph figure. For lewm-rollout the reported ms is per rollout step (the K graphs of one jepa_lewm_rollout call divided by K, so its rate column is steps/s).

Encoder

model ftype shape tokens threads ms mean ms min tokens/s PyTorch mean ms PyTorch median ms speedup
ijepa_vith14_1k f32 224x224 256 32 174.4 173.4 1468 263 250 1.43x
ijepa_vith14_1k f16 224x224 256 32 147.0 145.9 1741 263 250 1.70x
ijepa_vith14_1k f16 224x224 256 96 113.1 112.0 2264 263 250 2.21x
ijepa_vith14_1k q8_0 224x224 256 32 129.1 128.5 1983 263 250 1.94x
ijepa_vith14_1k q4_k 224x224 256 32 197.8 197.5 1295 263 250 1.26x
ijepa_vith14_1k q4_0 224x224 256 32 140.2 139.4 1826 263 250 1.78x
lejepa-vits16-pretrain-in1k f32 224x224 197 32 13.1 13.1 15019 22.6 15.4 1.17x
lejepa-vits16-pretrain-in1k f16 224x224 197 32 12.8 12.7 15383 22.6 15.4 1.20x
lejepa-vits16-pretrain-in1k q8_0 224x224 197 32 11.3 11.2 17379 22.6 15.4 1.36x
lejepa-vits16-pretrain-in1k q4_k 224x224 197 32 12.9 12.8 15301 22.6 15.4 1.20x
lejepa-vits16-pretrain-in1k q4_0 224x224 197 32 12.0 11.9 16411 22.6 15.4 1.28x
levjepa-vitl16 f32 16f 224x224 3 137 32 1519.5 1512.0 2065 1752 1735 1.15x
levjepa-vitl16 f16 16f 224x224 3 137 32 1496.2 1479.9 2097 1752 1735 1.17x
levjepa-vitl16 f16 16f 224x224 3 137 96 894.7 882.2 3506 1752 1735 1.96x
levjepa-vitl16 q8_0 16f 224x224 3 137 32 1547.3 1508.4 2027 1752 1735 1.13x
levjepa-vitl16 q4_k 16f 224x224 3 137 32 1874.6 1851.4 1673 1752 1735 0.93x
levjepa-vitl16 q4_0 16f 224x224 3 137 32 1551.0 1536.7 2023 1752 1735 1.13x
lewm-pusht f32 224x224 257 32 9.2 9.2 28022 16.8lewm 16.0 1.83x
lewm-pusht f16 224x224 257 32 9.8 9.7 26356 16.8lewm 16.0 1.72x
lewm-pusht q8_0 224x224 257 32 9.1 8.9 28212 16.8lewm 16.0 1.84x
lewm-pusht q4_k 224x224 257 32 10.5 10.3 24561 16.8lewm 16.0 1.60x
lewm-pusht q4_0 224x224 257 32 9.1 9.0 28243 16.8lewm 16.0 1.84x
vjepa2-vitl-fpc16-256-ssv2 f32 16f 256x256 2 048 32 943.5 938.6 2171 1051ssv2 1046 n/assv2
vjepa2-vitl-fpc16-256-ssv2 f16 16f 256x256 2 048 32 822.8 820.7 2489 1051ssv2 1046 n/assv2
vjepa2-vitl-fpc16-256-ssv2 f16 16f 256x256 2 048 96 564.4 560.8 3629 1051ssv2 1046 n/assv2
vjepa2-vitl-fpc16-256-ssv2 q8_0 16f 256x256 2 048 32 793.3 789.7 2582 1051ssv2 1046 n/assv2
vjepa2-vitl-fpc16-256-ssv2 q4_k 16f 256x256 2 048 32 1092.6 1087.0 1874 1051ssv2 1046 n/assv2
vjepa2-vitl-fpc16-256-ssv2 q4_0 16f 256x256 2 048 32 853.3 837.4 2400 1051ssv2 1046 n/assv2
vjepa2-vitl-fpc64-256 f32 16f 256x256 2 048 32 941.3 938.0 2176 1293fpc64 1296 n/afpc64
vjepa2-vitl-fpc64-256 f16 16f 256x256 2 048 32 820.7 817.1 2495 1293fpc64 1296 n/afpc64
vjepa2-vitl-fpc64-256 f16 16f 256x256 2 048 96 566.6 552.3 3614 1293fpc64 1296 n/afpc64
vjepa2-vitl-fpc64-256 q8_0 16f 256x256 2 048 32 794.1 781.2 2579 1293fpc64 1296 n/afpc64
vjepa2-vitl-fpc64-256 q4_k 16f 256x256 2 048 32 1096.1 1095.7 1868 1293fpc64 1296 n/afpc64
vjepa2-vitl-fpc64-256 q4_0 16f 256x256 2 048 32 844.4 835.2 2425 1293fpc64 1296 n/afpc64
vjepa2-vitl-fpc64-256 f32 64f 256x256 8 192 32 7020.1 6973.4 1167 10114fpc64 9694 n/afpc64
vjepa2-vitl-fpc64-256 f16 64f 256x256 8 192 32 6388.1 6368.7 1282 10114fpc64 9694 n/afpc64
vjepa2-vitl-fpc64-256 f16 64f 256x256 8 192 96 4026.9 3992.3 2034 10114fpc64 9694 n/afpc64
vjepa2-vitl-fpc64-256 q8_0 64f 256x256 8 192 32 6482.4 6474.6 1264 10114fpc64 9694 n/afpc64
vjepa2-vitl-fpc64-256 q4_k 64f 256x256 8 192 32 7405.5 7401.7 1106 10114fpc64 9694 n/afpc64
vjepa2-vitl-fpc64-256 q4_0 64f 256x256 8 192 32 6616.3 6537.0 1238 10114fpc64 9694 n/afpc64
vjepa2_1-vitb-384 f32 384x384 576 32 70.0 69.8 8228 110 109 1.57x
vjepa2_1-vitb-384 f16 384x384 576 32 60.3 60.1 9547 110 109 1.82x
vjepa2_1-vitb-384 f16 384x384 576 96 52.7 52.2 10935 110 109 2.09x
vjepa2_1-vitb-384 q8_0 384x384 576 32 58.5 57.9 9846 110 109 1.88x
vjepa2_1-vitb-384 q4_k 384x384 576 32 90.7 90.5 6351 110 109 1.21x
vjepa2_1-vitb-384 q4_0 384x384 576 32 59.7 58.6 9654 110 109 1.84x
vjepa2_1-vitb-384 f32 16f 384x384 4 608 32 825.9 819.0 5579 908 918 1.10x
vjepa2_1-vitb-384 f16 16f 384x384 4 608 32 853.5 849.8 5399 908 918 1.06x
vjepa2_1-vitb-384 f16 16f 384x384 4 608 96 636.0 621.5 7246 908 918 1.43x
vjepa2_1-vitb-384 q8_0 16f 384x384 4 608 32 913.6 899.6 5044 908 918 0.99x
vjepa2_1-vitb-384 q4_k 16f 384x384 4 608 32 1148.5 1144.5 4012 908 918 0.79x
vjepa2_1-vitb-384 q4_0 16f 384x384 4 608 32 895.2 893.3 5147 908 918 1.01x
vjepa2_1-vitb-384 f32 64f 384x384 18 432 32 9050.4 8885.2 2037
vjepa2_1-vitb-384 f16 64f 384x384 18 432 32 9036.1 8973.9 2040
vjepa2_1-vitb-384 f16 64f 384x384 18 432 96 5040.2 5008.8 3657
vjepa2_1-vitb-384 q8_0 64f 384x384 18 432 32 9487.1 9425.4 1943
vjepa2_1-vitb-384 q4_k 64f 384x384 18 432 32 10234.3 10212.9 1801
vjepa2_1-vitb-384 q4_0 64f 384x384 18 432 32 9526.9 9392.3 1935

PyTorch baseline = timing_s.forward_s over the reference samples with the same frame count in tests/fixtures/ref/<model>/manifest.json — the same box, CPU float32, torch 2.13.0+cpu, transformers 5.16.1, 32 threads. It is the model forward alone (no decode, no preprocessing). Two summaries are given: the mean over every such sample, and the median of the samples after the first, because a manifest's first forward of a frame group can be a cold one (weights paged in, kernels selected) that the mean then carries into every row. The bold column is the one the speedup divides by — the drop-first median wherever that first sample is ≥ 1.2x the median of the rest, the mean otherwise. The speedup column is filled only where the reference forward is the same work as our encoder; see the footnotes for the three models where it is not.

Which manifest samples feed each baseline | model | manifest | frames | samples (manifest order) | first sample ms | mean ms | median ms (drop-first) | used for the speedup | |---|---|---:|---|---:|---:|---:|---| | ijepa_vith14_1k | `ijepa-vith14-1k` | 1 | coco_000000000139, … (8 samples) | 331.8 | 263 | 250 | median (cold first sample) | | lejepa-vits16-pretrain-in1k | `lejepa-vits16` | 1 | coco_000000000139, … (8 samples) | 72.2 | 22.6 | 15.4 | median (cold first sample) | | levjepa-vitl16 | `levjepa-vitl16` | 16 | archery_f16, bowling_f16, coco_000000000139, coco_000000000285 | 1682.3 | 1752 | 1735 | mean | | lewm-pusht | `lewm-pusht` | 1 | coco_000000000139, coco_000000000285 | 17.5 | 16.8 | 16.0 | mean | | lewm-pusht | `lewm-pusht` | 3 | seq | 20.9 | 20.9 | – | mean | | vjepa2-vitl-fpc16-256-ssv2 | `vjepa2-vitl-fpc16-256-ssv2` | 16 | archery_f16, bowling_f16 | 1056.0 | 1051 | 1046 | mean | | vjepa2-vitl-fpc64-256 | `vjepa2-vitl-fpc64-256` | 16 | archery_f16, bowling_f16 | 1289.6 | 1293 | 1296 | mean | | vjepa2-vitl-fpc64-256 | `vjepa2-vitl-fpc64-256` | 64 | archery_f64, bowling_f64 | 10533.4 | 10114 | 9694 | mean | | vjepa2_1-vitb-384 | `vjepa2_1-vitb-384` | 1 | coco_000000000139, coco_000000000285 | 111.2 | 110 | 109 | mean | | vjepa2_1-vitb-384 | `vjepa2_1-vitb-384` | 16 | archery_f16, bowling_f16 | 897.4 | 908 | 918 | mean | A frame group is matched to an encoder row by frame count, so a sample that is not a plain encoder forward never reaches one: `lewm-pusht`'s 3-frame `seq` sample times encode + projector + a 3-frame predictor call and forms its own group, and only the two 1-frame samples feed the 224x224 LeWM rows above. Groups of one sample have no drop-first median and always use the mean.

Encoder throughput summary (tokens/s)

model shape tokens f32 t=32 f16 t=32 q8_0 t=32 q4_k t=32 q4_0 t=32 f16 t=96
ijepa_vith14_1k 224x224 256 1468 1741 1983 1295 1826 2264
lejepa-vits16-pretrain-in1k 224x224 197 15019 15383 17379 15301 16411
levjepa-vitl16 16f 224x224 3 137 2065 2097 2027 1673 2023 3506
lewm-pusht 224x224 257 28022 26356 28212 24561 28243
vjepa2-vitl-fpc16-256-ssv2 16f 256x256 2 048 2171 2489 2582 1874 2400 3629
vjepa2-vitl-fpc64-256 16f 256x256 2 048 2176 2495 2579 1868 2425 3614
vjepa2-vitl-fpc64-256 64f 256x256 8 192 1167 1282 1264 1106 1238 2034
vjepa2_1-vitb-384 384x384 576 8228 9547 9846 6351 9654 10935
vjepa2_1-vitb-384 16f 384x384 4 608 5579 5399 5044 4012 5147 7246
vjepa2_1-vitb-384 64f 384x384 18 432 2037 2040 1943 1801 1935 3657

Effect of the weight dtype (encoder, t=32)

model shape tokens f32 ms f16 ms q8_0 ms f32 → f16 f16 → q8_0
ijepa_vith14_1k 224x224 256 174 147 129 1.19x 1.14x
lejepa-vits16-pretrain-in1k 224x224 197 13.1 12.8 11.3 1.02x 1.13x
levjepa-vitl16 16f 224x224 3 137 1519 1496 1547 1.02x 0.97x
lewm-pusht 224x224 257 9.2 9.8 9.1 0.94x 1.07x
vjepa2-vitl-fpc16-256-ssv2 16f 256x256 2 048 943 823 793 1.15x 1.04x
vjepa2-vitl-fpc64-256 16f 256x256 2 048 941 821 794 1.15x 1.03x
vjepa2-vitl-fpc64-256 64f 256x256 8 192 7020 6388 6482 1.10x 0.99x
vjepa2_1-vitb-384 384x384 576 70.0 60.3 58.5 1.16x 1.03x
vjepa2_1-vitb-384 16f 384x384 4 608 826 853 914 0.97x 0.93x
vjepa2_1-vitb-384 64f 384x384 18 432 9050 9036 9487 1.00x 0.95x

Quantisation buys memory, not reliably time. The matmuls do get faster (docs/ggml-notes.md §5 measures 2.2 → 3.3 TFLOP/s going F32 → F16, and ~3.6-4.1 for Q8_0 on this box), but q8_0 pays for quantising the activations on every matmul, and on the long clips the flash-attention time — F32 work whatever the weights are — dominates the layer (docs/ggml-notes.md §3: 158 ms per ViT-L layer at 8192 tokens against ~62 ms of matmul). The small image models are launch- and LayerNorm-bound and barely move at all. Pick the dtype on the accuracy tables in docs/parity.md and docs/quantization.md and on the memory table below, not on these milliseconds.

Sub-8-bit weights: what q4 costs and what it buys (encoder, t=32)

model shape tokens f16 MiB q4 MiB of f16 f16 ms q4_0 ms q4_k ms q4_0 vs f16 q4_k vs q4_0
ijepa_vith14_1k 224x224 256 1206 344 28 % 147 140 198 1.05x 0.71x
lejepa-vits16-pretrain-in1k 224x224 197 42 13 31 % 12.8 12.0 12.9 1.07x 0.93x
levjepa-vitl16 16f 224x224 3 137 579 166 29 % 1496 1551 1875 0.96x 0.83x
lewm-pusht 224x224 257 38 15 41 % 9.8 9.1 10.5 1.07x 0.87x
vjepa2-vitl-fpc16-256-ssv2 16f 256x256 2 048 717 205 29 % 823 853 1093 0.96x 0.78x
vjepa2-vitl-fpc64-256 16f 256x256 2 048 622 178 29 % 821 844 1096 0.97x 0.77x
vjepa2-vitl-fpc64-256 64f 256x256 8 192 622 178 29 % 6388 6616 7405 0.97x 0.89x
vjepa2_1-vitb-384 384x384 576 210 62 30 % 60.3 59.7 90.7 1.01x 0.66x
vjepa2_1-vitb-384 16f 384x384 4 608 210 62 30 % 853 895 1148 0.95x 0.78x
vjepa2_1-vitb-384 64f 384x384 18 432 210 62 30 % 9036 9527 10234 0.95x 0.93x

q4 is a memory win and, at best, time-neutral: the weights fall to 28–41 % of f16, q4_0 lands within 7 % of f16's time either way, and q4_k costs another 1.07–1.52x on top of q4_0 — ggml's Q4_K vec-dot does more work per block than Q4_0's, and nothing in these graphs is weight-bandwidth-bound enough to pay that back. Where a model's rows are not a multiple of the 256-element super-block the k-quant falls back to q4_0 tensor by tensor, so the small models' q4_k files are mostly q4_0 and their two rows nearly coincide. Neither is a parity configuration: docs/parity.md puts every file below 8 bits per weight in the advisory tier, and docs/quantization.md has the per-model cosines. These rows are here so the size/speed side of that trade-off is measured rather than assumed.

Thread scaling (32 → 96 threads)

model ftype mode shape tokens ms t=32 ms t=96 speedup
ijepa_vith14_1k f16 encoder 224x224 256 147 113 1.30x
levjepa-vitl16 f16 encoder 16f 224x224 3 137 1496 895 1.67x
vjepa2-vitl-fpc16-256-ssv2 f16 encoder 16f 256x256 2 048 823 564 1.46x
vjepa2-vitl-fpc16-256-ssv2 f16 head 16f 256x256 2 048 99.0 67.1 1.48x
vjepa2-vitl-fpc16-256-ssv2 f16 predictor 16f 256x256 2 048 341 208 1.64x
vjepa2-vitl-fpc64-256 f16 encoder 16f 256x256 2 048 821 567 1.45x
vjepa2-vitl-fpc64-256 f16 encoder 64f 256x256 8 192 6388 4027 1.59x
vjepa2-vitl-fpc64-256 f16 predictor 16f 256x256 2 048 344 207 1.66x
vjepa2_1-vitb-384 f16 encoder 384x384 576 60.3 52.7 1.15x
vjepa2_1-vitb-384 f16 encoder 16f 384x384 4 608 853 636 1.34x
vjepa2_1-vitb-384 f16 encoder 64f 384x384 18 432 9036 5040 1.79x
vjepa2_1-vitb-384 f16 predictor 16f 384x384 4 608 1304 854 1.53x

Tripling the threads never triples the throughput: the 32-thread runs already saturate a good part of the memory bandwidth, and both the LayerNorm/GELU passes and the graph launch overhead scale poorly. The gain is largest where a single matmul or flash-attention tile is big enough to keep 96 workers busy.

Cross-check against docs/parity.md

docs/parity.md times the same encoder graphs on the real preprocessed fixture inputs, reporting the second sample of a run (after the weights are paged in) — effectively a best-of figure, so ms min is what it should be compared against. The synthetic input used here has the same shape and scale, so what is left is run-to-run and input-dependent noise: every row below agrees to within ±12 %, and the two rows where docs/parity.md was itself re-measured on this idle box (the f32 fpc64 clips) to within 3.3 %. The right-hand column is parsed out of docs/parity.md (its ms/item t=N / ms/clip t=N columns), so the two documents cannot drift apart without this table saying so.

model ftype shape threads bench ms min parity.md ms delta
ijepa_vith14_1k f32 224x224 32 173 185 -6.3 %
ijepa_vith14_1k f16 224x224 32 146 156 -6.5 %
ijepa_vith14_1k f16 224x224 96 112 122 -8.2 %
ijepa_vith14_1k q8_0 224x224 32 129 138 -6.9 %
lejepa-vits16-pretrain-in1k f32 224x224 32 13.1 14.0 -6.6 %
lejepa-vits16-pretrain-in1k f16 224x224 32 12.7 13.4 -5.2 %
lejepa-vits16-pretrain-in1k q8_0 224x224 32 11.2 12.6 -11.0 %
levjepa-vitl16 f32 16f 224x224 32 1512 1661 -9.0 %
levjepa-vitl16 f16 16f 224x224 32 1480 1542 -4.0 %
levjepa-vitl16 f16 16f 224x224 96 882 881 +0.1 %
levjepa-vitl16 q8_0 16f 224x224 32 1508 1609 -6.3 %
levjepa-vitl16 q4_k 16f 224x224 32 1851 1935 -4.3 %
levjepa-vitl16 q4_0 16f 224x224 32 1537 1610 -4.6 %
lewm-pusht f32 224x224 32 9.2 10.3 -11.1 %
lewm-pusht f16 224x224 32 9.7 10.7 -9.6 %
lewm-pusht q8_0 224x224 32 8.9 9.7 -8.7 %
vjepa2-vitl-fpc16-256-ssv2 f32 16f 256x256 32 939 924 +1.6 %
vjepa2-vitl-fpc16-256-ssv2 f16 16f 256x256 32 821 814 +0.8 %
vjepa2-vitl-fpc16-256-ssv2 q8_0 16f 256x256 32 790 771 +2.4 %
vjepa2-vitl-fpc64-256 f32 16f 256x256 32 938 970 -3.3 %
vjepa2-vitl-fpc64-256 f16 16f 256x256 32 817 827 -1.2 %
vjepa2-vitl-fpc64-256 q8_0 16f 256x256 32 781 852 -8.3 %
vjepa2-vitl-fpc64-256 f32 64f 256x256 32 6973 6845 +1.9 %
vjepa2-vitl-fpc64-256 f16 64f 256x256 32 6369 6386 -0.3 %
vjepa2-vitl-fpc64-256 f16 64f 256x256 96 3992 4076 -2.1 %
vjepa2-vitl-fpc64-256 q8_0 64f 256x256 32 6475 7203 -10.1 %
vjepa2_1-vitb-384 f32 384x384 32 69.8 70.0 -0.2 %
vjepa2_1-vitb-384 f16 384x384 32 60.1 63.0 -4.6 %
vjepa2_1-vitb-384 q8_0 384x384 32 57.9 59.0 -1.8 %
vjepa2_1-vitb-384 f32 16f 384x384 32 819 909 -9.9 %
vjepa2_1-vitb-384 f16 16f 384x384 32 850 875 -2.9 %
vjepa2_1-vitb-384 q8_0 16f 384x384 32 900 878 +2.5 %

Memory

model ftype shape weights MiB peak RSS MiB model load ms
ijepa_vith14_1k f32 224x224 2406 2432 1150
ijepa_vith14_1k f16 224x224 1206 1230 513
ijepa_vith14_1k q8_0 224x224 644 660 279
ijepa_vith14_1k q4_k 224x224 344 360 160
ijepa_vith14_1k q4_0 224x224 344 360 157
lejepa-vits16-pretrain-in1k f32 224x224 83 94 49
lejepa-vits16-pretrain-in1k f16 224x224 42 52 27
lejepa-vits16-pretrain-in1k q8_0 224x224 23 33 22
lejepa-vits16-pretrain-in1k q4_k 224x224 13 23 10
lejepa-vits16-pretrain-in1k q4_0 224x224 13 23 10
levjepa-vitl16 f32 16f 224x224 1156 1335 547
levjepa-vitl16 f16 16f 224x224 579 779 276
levjepa-vitl16 q8_0 16f 224x224 310 490 147
levjepa-vitl16 q4_k 16f 224x224 166 348 96
levjepa-vitl16 q4_0 16f 224x224 166 354 86
lewm-pusht f32 224x224 69 79 37
lewm-pusht f16 224x224 38 47 23
lewm-pusht q8_0 224x224 23 33 14
lewm-pusht q4_k 224x224 15 25 11
lewm-pusht q4_0 224x224 15 25 12
vjepa2-vitl-fpc16-256-ssv2 f32 16f 256x256 1432 1522 635
vjepa2-vitl-fpc16-256-ssv2 f16 16f 256x256 717 808 332
vjepa2-vitl-fpc16-256-ssv2 q8_0 16f 256x256 383 466 184
vjepa2-vitl-fpc16-256-ssv2 q4_k 16f 256x256 205 297 111
vjepa2-vitl-fpc16-256-ssv2 q4_0 16f 256x256 205 291 106
vjepa2-vitl-fpc64-256 f32 16f 256x256 1243 1336 581
vjepa2-vitl-fpc64-256 f16 16f 256x256 622 720 292
vjepa2-vitl-fpc64-256 q8_0 16f 256x256 333 426 158
vjepa2-vitl-fpc64-256 q4_k 16f 256x256 178 272 90
vjepa2-vitl-fpc64-256 q4_0 16f 256x256 178 265 97
vjepa2-vitl-fpc64-256 f32 64f 256x256 1243 1599 567
vjepa2-vitl-fpc64-256 f16 64f 256x256 622 1034 295
vjepa2-vitl-fpc64-256 q8_0 64f 256x256 333 713 179
vjepa2-vitl-fpc64-256 q4_k 64f 256x256 178 564 95
vjepa2-vitl-fpc64-256 q4_0 64f 256x256 178 561 106
vjepa2_1-vitb-384 f32 384x384 419 435 197
vjepa2_1-vitb-384 f16 384x384 210 226 97
vjepa2_1-vitb-384 q8_0 384x384 113 127 60
vjepa2_1-vitb-384 q4_k 384x384 62 75 36
vjepa2_1-vitb-384 q4_0 384x384 62 75 35
vjepa2_1-vitb-384 f32 16f 384x384 419 569 246
vjepa2_1-vitb-384 f16 16f 384x384 210 376 111
vjepa2_1-vitb-384 q8_0 16f 384x384 113 270 72
vjepa2_1-vitb-384 q4_k 16f 384x384 62 218 49
vjepa2_1-vitb-384 q4_0 16f 384x384 62 220 35
vjepa2_1-vitb-384 f32 64f 384x384 419 1060 198
vjepa2_1-vitb-384 f16 64f 384x384 210 948 105
vjepa2_1-vitb-384 q8_0 64f 384x384 113 809 62
vjepa2_1-vitb-384 q4_k 64f 384x384 62 759 39
vjepa2_1-vitb-384 q4_0 64f 384x384 62 764 37

weights MiB is jepa_model_n_bytes() (the tensor bytes resident after the load, i.e. the GGUF payload); peak RSS additionally covers the graph allocation, the host-side patch buffer and the output rows, so it grows with the token count — the same weights are listed once per shape so that growth is visible. The loader freads every tensor into its own buffer (no mmap), so model load ms tracks the file size and the page-cache state; these numbers are all warm-cache. Compare the weight column with the GGUF file sizes in docs/quantization.md.

Attentive-pool head

model ftype shape tokens threads ms mean ms min encoder ms peak RSS MiB
vjepa2-vitl-fpc16-256-ssv2 f32 16f 256x256 2 048 32 106.9 106.8 935.5 1512
vjepa2-vitl-fpc16-256-ssv2 f16 16f 256x256 2 048 32 99.0 98.9 815.8 815
vjepa2-vitl-fpc16-256-ssv2 f16 16f 256x256 2 048 96 67.1 67.0 573.5 773
vjepa2-vitl-fpc16-256-ssv2 q8_0 16f 256x256 2 048 32 98.2 98.2 793.3 467

The classifier head (3 self-attention blocks over the tokens + one cross-attention query + MLP + linear) on top of the encoder output of the same clip, so the end-to-end classification cost is encoder + head. encoder ms is the pass that produced this row's input — the faster of two warm encoder graphs (a third, cold one runs first and is discarded), not an average of repeat runs like the ms columns, so read the Encoder table for the encoder cost proper.

End-to-end classification against the reference — this is like-for-like, because the manifest's forward is VJEPA2ForVideoClassification (encoder + attentive pooler + classifier, predictor skipped):

model ftype shape threads encoder ms head ms total ms PyTorch ms speedup
vjepa2-vitl-fpc16-256-ssv2 f32 16f 256x256 32 943 107 1050 1051 1.00x
vjepa2-vitl-fpc16-256-ssv2 f16 16f 256x256 32 823 99.0 922 1051 1.14x
vjepa2-vitl-fpc16-256-ssv2 f16 16f 256x256 96 564 67.1 631 1051 1.66x
vjepa2-vitl-fpc16-256-ssv2 q8_0 16f 256x256 32 793 98.2 891 1051 1.18x

Masked predictor

model ftype shape tokens threads ms mean ms min encoder ms peak RSS MiB
vjepa2-vitl-fpc16-256-ssv2 f32 16f 256x256 2 048 32 333.1 328.9 937.4 1531
vjepa2-vitl-fpc16-256-ssv2 f16 16f 256x256 2 048 32 340.8 340.5 814.6 822
vjepa2-vitl-fpc16-256-ssv2 f16 16f 256x256 2 048 96 208.0 196.5 593.4 773
vjepa2-vitl-fpc16-256-ssv2 q8_0 16f 256x256 2 048 32 336.8 335.7 810.6 472
vjepa2-vitl-fpc64-256 f32 16f 256x256 2 048 32 338.3 330.1 942.9 1344
vjepa2-vitl-fpc64-256 f16 16f 256x256 2 048 32 343.8 341.0 827.6 724
vjepa2-vitl-fpc64-256 f16 16f 256x256 2 048 96 207.4 197.8 557.0 681
vjepa2-vitl-fpc64-256 q8_0 16f 256x256 2 048 32 332.7 332.3 796.8 416
vjepa2_1-vitb-384 f32 16f 384x384 4 608 32 1296.7 1286.0 824.1 593
vjepa2_1-vitb-384 f16 16f 384x384 4 608 32 1303.6 1291.2 871.2 410
vjepa2_1-vitb-384 f16 16f 384x384 4 608 96 854.4 813.6 630.3 347
vjepa2_1-vitb-384 q8_0 16f 384x384 4 608 32 1272.6 1269.9 901.1 295

Worst case for the predictor: context = target = every token, i.e. a sequence of 2 x tokens through the 12-layer 384-d predictor. encoder ms is the pass that produced this row's input — the faster of two warm encoder graphs (a third, cold one runs first and is discarded), not an average of repeat runs like the ms columns, so read the Encoder table for the encoder cost proper.

LeWM world model

model ftype mode shape threads ms mean ms min steps/s
lewm-pusht f32 lewm-step 3f x 192d 32 0.911 0.863 1097
lewm-pusht f16 lewm-step 3f x 192d 32 0.918 0.904 1089
lewm-pusht q8_0 lewm-step 3f x 192d 32 0.739 0.718 1354
lewm-pusht f32 lewm-rollout rollout K=20 32 0.817 0.809 1225
lewm-pusht f16 lewm-rollout rollout K=20 32 0.865 0.850 1156
lewm-pusht q8_0 lewm-rollout rollout K=20 32 0.744 0.729 1344

lewm-step is one jepa_lewm_predict over the predictor's full 3-frame window; lewm-rollout is jepa_lewm_rollout and its ms is per step (the growing window means the first steps are cheaper than the last). Neither includes the encoder or the projector — see the encoder table for lewm-pusht for the cost of turning an image into a world-model state.

GPU (CUDA)

The same tools/jepa-bench, the same synthetic input, one CUDA device instead of the CPU backend (-DJEPA_CUDA=ON, then --gpu N). These tables are keyed by device and accumulation precision where the ones above are keyed by thread count, which is why they have an artifact of their own: tests/results/benchmarks-gpu.json, written by scripts/bench_gpu.sh and read back by this generator.

Every row is the best of 5 runs after 2 warmups, and the warmups are not a formality: ggml's CUDA backend captures a CUDA graph once it has seen the same topology and the same tensor addresses twice in a row, so from the third call the encoder is one graph launch instead of hundreds of kernel launches. ms sd is the spread of the measured runs and is the width to read a difference between two rows against.

cmake -S . -B build-cuda -G Ninja -DCMAKE_BUILD_TYPE=Release -DJEPA_CUDA=ON \
  && cmake --build build-cuda -j 32

# the PyTorch baseline of the last table (needs a CUDA-enabled torch)
python scripts/torch_gpu_baseline.py --device 0 -o tmp/bench-gpu/torch-gpu.json

# every configuration in scripts/bench_gpu.grid on device 0, then this section and its JSON
scripts/bench_gpu.sh 0

The configurations live in scripts/bench_gpu.grid, one line per (model, mode, shape, dtypes), and are the ones performance.md publishes. Without --gpu-dir the generator rebuilds this section straight out of tests/results/benchmarks-gpu.json, so the document survives the loss of tmp/bench-gpu/ — which is git-ignored — without the card.

Card and build

setting value
GPU NVIDIA RTX 4500 Ada Generation, 24570 MiB, compute 8.9, 210.00 W board limit
Device index 0 — every run below has the card to itself
Driver 580.173.02 (CUDA 13.0 driver API)
Toolkit nvcc 13.0.88
Kernel 6.17.0-1032-oem
Host compiler c++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
ggml 36da5713, GGML_LLAMAFILE=ON (a host-side path, unused here)
jepa.cpp 35d77369
Precision GGML_PREC_F32 on every mul_mat unless a row says --gpu-prec f16; K/V F16 in flash attention for every file but f32

Measurement sessions (one bench_gpu.sh invocation each):

device warmup + measured start end 1-min load avg foreign cores note
CUDA0 2 + 5 2026-09-01 09:13 UTC 2026-09-01 09:14 UTC 0.68 → 1.09 0.24 GPU twin sweep, idle box, device 0

foreign cores is the CPU time the whole machine spent out of idle over the session minus the CPU time this sweep's own processes spent, divided by the wall clock: how much of the box belonged to somebody else while the card was timed. The highest here is 0.24 of one core out of 192, i.e. an idle box. A GPU row is host-idle by construction, so the load average alone would not have caught a second tenant.

GPU encoder

model ftype shape tokens device ms mean ms min ms sd tokens/s peak RSS MiB CPU f16 t=32 ms vs CPU f16 t=32
ijepa_vith14_1k f32 224x224 256 CUDA0 11.86 11.81 0.074 21580 356 147.0 12.4x
ijepa_vith14_1k f16 224x224 256 CUDA0 15.52 15.45 0.044 16500 359 147.0 9.5x
ijepa_vith14_1k q8_0 224x224 256 CUDA0 7.99 7.99 0.002 32034 346 147.0 18.4x
ijepa_vith14_1k q4_k 224x224 256 CUDA0 7.77 7.77 0.002 32947 347 147.0 18.9x
lejepa-vits16-pretrain-in1k f16 224x224 197 CUDA0 1.08 1.08 0.000 181734 358 12.8 11.8x
levjepa-vitl16 f32 16f 224x224 3 137 CUDA0 85.89 85.72 0.120 36524 399 1496.2 17.4x
levjepa-vitl16 f16 16f 224x224 3 137 CUDA0 87.64 87.36 0.277 35794 390 1496.2 17.1x
levjepa-vitl16 q8_0 16f 224x224 3 137 CUDA0 71.39 70.88 0.402 43942 391 1496.2 21.0x
levjepa-vitl16 q4_k 16f 224x224 3 137 CUDA0 71.23 71.00 0.198 44038 393 1496.2 21.0x
lewm-pusht f16 224x224 257 CUDA0 0.86 0.85 0.001 300409 358 9.8 11.4x
vjepa2-vitl-fpc64-256 f32 16f 256x256 2 048 CUDA0 43.57 42.94 0.338 47006 382 820.7 18.8x
vjepa2-vitl-fpc64-256 f16 16f 256x256 2 048 CUDA0 46.47 46.29 0.096 44068 374 820.7 17.7x
vjepa2-vitl-fpc64-256 q8_0 16f 256x256 2 048 CUDA0 33.86 32.87 0.595 60488 379 820.7 24.2x
vjepa2-vitl-fpc64-256 q4_k 16f 256x256 2 048 CUDA0 34.66 34.22 0.302 59095 380 820.7 23.7x
vjepa2-vitl-fpc64-256 f32 64f 256x256 8 192 CUDA0 302.59 302.18 0.259 27073 465 6388.1 21.1x
vjepa2-vitl-fpc64-256 f16 64f 256x256 8 192 CUDA0 305.43 305.22 0.223 26821 468 6388.1 20.9x
vjepa2-vitl-fpc64-256 q8_0 64f 256x256 8 192 CUDA0 280.42 280.35 0.071 29214 473 6388.1 22.8x
vjepa2-vitl-fpc64-256 q4_k 64f 256x256 8 192 CUDA0 281.38 281.28 0.090 29113 476 6388.1 22.7x
vjepa2_1-vitb-384 f16 384x384 576 CUDA0 4.38 4.38 0.004 131411 346 60.3 13.8x
vjepa2_1-vitb-384 q4_k 384x384 576 CUDA0 3.44 3.44 0.002 167252 352 60.3 17.5x
vjepa2_1-vitb-384 f16 16f 384x384 4 608 CUDA0 42.46 42.09 0.467 108535 410 853.5 20.1x
vjepa2_1-vitb-384 q4_k 16f 384x384 4 608 CUDA0 37.35 37.13 0.146 123366 416 853.5 22.9x
vjepa2_1-vitb-384 f16 64f 384x384 18 432 CUDA0 424.21 420.98 1.70 43450 618 9036.1 21.3x

peak RSS is host memory (the process VmHWM), not device memory: the weights are uploaded and the host copy is released, so it says little beyond the size of the graph arena and the patch buffer. The speed-up column divides the 32-thread f16 run of the same graph and shape — from the Encoder table above, i.e. 96 Zen 4 cores' worth of machine against one workstation card — by this row, whatever this row's dtype is.

Effect of the weight dtype on a GPU (encoder)

model shape tokens f32 ms f16 ms q8_0 ms q4_k ms f16 → q8_0 f16 → q4_k
ijepa_vith14_1k 224x224 256 11.9 15.5 8.0 7.8 1.94x 2.00x
levjepa-vitl16 16f 224x224 3 137 85.9 87.6 71.4 71.2 1.23x 1.23x
vjepa2-vitl-fpc64-256 16f 256x256 2 048 43.6 46.5 33.9 34.7 1.37x 1.34x
vjepa2-vitl-fpc64-256 64f 256x256 8 192 303 305 280 281 1.09x 1.09x
vjepa2_1-vitb-384 384x384 576 4.4 3.4 1.27x
vjepa2_1-vitb-384 16f 384x384 4 608 42.5 37.4 1.14x

The CPU ordering inverts here. Every type jepa.cpp ships takes mmq, a real INT8 tensor-core kernel, so q8_0 and q4_k both beat f16 while being half and a quarter of the weight bytes — where on the CPU the k-quants fall off llamafile's accelerated sgemm and lose. The f32 column is not slower than f16 because ggml's CUDA F32 path is TF32, while the f16 path pays for GGML_PREC_F32 accumulation. Accuracy per type does not invert with the backend: docs/parity.md Results — encoders on CUDA0 has the cosines.

What GGML_PREC_F32 costs (--gpu-prec f16)

model ftype shape tokens GGML_PREC_F32 ms --gpu-prec f16 ms cost of F32 accumulation
ijepa_vith14_1k f16 224x224 256 15.5 8.8 1.76x
levjepa-vitl16 f16 16f 224x224 3 137 87.6 83.2 1.05x
vjepa2-vitl-fpc64-256 f16 16f 256x256 2 048 46.5 37.1 1.25x
vjepa2-vitl-fpc64-256 f16 64f 256x256 8 192 305 303 1.01x

--gpu-prec f16 hands the mul_mats cuBLAS' own f16 compute type instead of forcing F32 accumulation. It is bench-only: it is not exposed in the runtime tools and not parity-gated (docs/parity.md measures a 177x wider f16 error with it), so these milliseconds are a measured upper bound rather than a shipping configuration. The cost is a strong function of the sequence — it is what holds the small image models back against the long clips' twenties in the speed-up column above.

Predictor, head and world model on a GPU

model ftype mode shape tokens ms mean ms min ms sd encoder ms CPU f16 t=32 ms vs CPU f16 t=32
vjepa2-vitl-fpc16-256-ssv2 f16 head 16f 256x256 2 048 5.612 5.497 0.089 46.9 98.97 17.63x
vjepa2-vitl-fpc16-256-ssv2 f16 predictor 16f 256x256 2 048 112.744 112.654 0.060 45.6 340.76 3.02x
vjepa2-vitl-fpc64-256 f16 predictor 16f 256x256 2 048 112.804 112.721 0.063 46.3 343.84 3.05x
lewm-pusht f16 lewm-step 3f x 192d 3 0.450 0.441 0.006 0.92 2.04x
lewm-pusht f16 lewm-rollout rollout K=20 1 0.448 0.439 0.013 0.86 1.93x

These are the synthetic-input graphs of the Masked predictor, Attentive-pool head and LeWM world model tables above, run on the card. The masked predictor is the one encoder-sized graph that does not gain twentyfold: at head_dim 32 no CUDA flash-attention kernel exists, so it takes the naive mul_mat + soft_max_ext path — genuinely F32, and about 3 TFLOP/s against flash's 50–70 (docs/architecture.md "GPU backend"). The LeWM graphs are the opposite end: three rows of 192 dimensions is far below the size at which a kernel launch pays for itself, and docs/parity.md Results — predictors on CUDA0 times the same two graphs on the real fixture state.

PyTorch on the same card

model shape tokens jepa.cpp CUDA f16 ms torch fp16 ms torch fp32 ms ggml / torch fp16 torch fp16 peak GiB
ijepa_vith14_1k 224x224 256 15.5 5.51 23.39 2.8x 1.19
vjepa2-vitl-fpc64-256 16f 256x256 2 048 46.5 28.92 117.9 1.6x 0.67
vjepa2-vitl-fpc64-256 64f 256x256 8 192 305 148.1 836.4 2.1x 0.83
levjepa-vitl16 16f 224x224 3 137 87.6 58.75 224.3 1.5x 0.68

scripts/torch_gpu_baseline.py on the same device: torch 2.13.0+cu130, transformers 5.16.1, batch 1, TF32 off, 3 warmup + 7 timed forwards, cuda.synchronize() around each, on the stored preprocessed tensor of a reference fixture — the same pixels, not merely the same shape. VJEPA2Model runs with skip_predictor=True, so its forward is the encoder alone. torch fp16 peak GiB is max_memory_allocated after the warmups, one model per precision, so it is the steady-state device footprint of that precision and nothing else.

Footnotes

fpc64 the vjepa2-vitl-fpc64-256 manifest times one VJEPA2Model forward, which always runs the predictor as well (its predictor_last_hidden_state comes from the same call), so it is an upper bound on the encoder and no speedup is claimed against it.

ssv2 the SSv2 manifest times VJEPA2ForVideoClassification, i.e. encoder + attentive pooler + classifier with the predictor skipped. It is therefore not comparable with the encoder row alone; the end-to-end table under Attentive-pool head adds our encoder and head and makes the comparison there.

lewm the LeWM manifest times encode + projector + one 1-frame predictor call; the two extra graphs are ~1 ms of it (see the world-model table), so the speedup is a slight over-estimate.


Generated by scripts/gen_benchmarks_md.py from 80 runs in tmp/bench and 32 GPU runs in tmp/bench-gpu. Cross-check against docs/parity.md (same graphs, real fixture inputs) and docs/quantization.md (accuracy per dtype).