jepa.cpp GGUF schema (v1)¶
One GGUF file is one model bundle: an encoder, and optionally a predictor and/or a head. The converter, the loader and the graph builder all implement exactly this document — change it here first. The graph these keys describe is in Architecture; the Python side that writes them is in Converter.
General metadata¶
| key | type | notes |
|---|---|---|
general.architecture |
str | always "jepa" |
general.name |
str | e.g. "vjepa2-vitl-fpc64-256" |
general.license |
str | "mit", "cc-by-nc-4.0", "apache-2.0" — copied from the source |
general.source_url |
str | HF repo or download URL |
general.file_type |
u32 | ggml ftype (0 = f32, 1 = f16, 7 = q8_0, ...) |
jepa.schema_version |
u32 | 1 |
jepa.family |
str | ijepa · vjepa · vjepa2 · vjepa2_1 · levjepa · hfvit (LeJEPA-style / LeWM encoder) · lewm · hjepa (H-JEPA hierarchy, see the family note) |
jepa.modality |
str | image · video · image+video |
Encoder (jepa.enc.*)¶
| key | type | notes |
|---|---|---|
jepa.enc.embed_dim |
u32 | 1024 for ViT-L |
jepa.enc.n_layer |
u32 | |
jepa.enc.n_head |
u32 | V-JEPA 2 ViT-g = 22, I-JEPA ViT-g = 16 |
jepa.enc.ffn_dim |
u32 | explicit, not a ratio (6144 for g) |
jepa.enc.patch_size |
u32 | 14 or 16 |
jepa.enc.tubelet_size |
u32 | 1 for image models, 2 for video |
jepa.enc.img_size |
u32 | training crop (224 / 256 / 384); inference may differ if pos scheme allows |
jepa.enc.n_frames |
u32 | training frames (1 for image, 16 or 64 for video) |
jepa.enc.in_chans |
u32 | 3 |
jepa.enc.ln_eps |
f32 | 1e-6 (HF I-JEPA config says 1e-6 too) |
jepa.enc.act |
str | gelu_erf (all Meta JEPAs) · gelu_tanh · silu |
jepa.enc.pos_type |
str | sincos2d · sincos3d · learned · rope3d |
jepa.enc.rope_theta |
f32 | 10000 |
jepa.enc.rope_interpolate |
bool | V-JEPA 2.1 interpolate_rope. The "pretrained grid" it rescales h/w to is not img_size / patch_size: Meta hard-codes 256 / patch_size (16 for patch 16, 18 for patch 14) — the loader derives train_grid_h = train_grid_w = 256 / patch_size = jepa.enc.rope_ref_grid (see src/rope3d.h). family == vjepa2_1 also selects the interleaved cos/sin layout (variant = 1); vjepa2 uses the tiled HF layout (variant = 0). |
jepa.enc.rope_freq_layout |
str | tiled (V-JEPA 2: per-axis cos/sin table is [f_0..f_{d/2-1}, f_0..f_{d/2-1}], the Meta/HF quirk) · interleaved (V-JEPA 2.1: [f_0,f_0,f_1,f_1,...], textbook RoPE). See scripts/jepa_convert/VJEPA_NOTES.md |
jepa.enc.rope_ref_grid |
u32 | V-JEPA 2.1 only: spatial RoPE positions are rescaled h *= (ref_grid-1)/(gh-1) (16 for patch 16) |
jepa.enc.cls_token |
bool | true for hfvit (DINOv2-style) and levjepa (the only video family with one), false for Meta JEPAs |
jepa.enc.n_registers |
u32 | 0 |
jepa.enc.qkv_fused |
bool | true → attn_qkv.* tensor with rows [q;k;v]; false → separate q/k/v |
jepa.enc.modality_embed |
bool | V-JEPA 2.1 |
jepa.enc.image_patch_embed |
bool | V-JEPA 2.1 separate 1×16×16 image tokenizer |
jepa.enc.hier_layers |
[u32] | V-JEPA 2.1 hierarchical layer ids (per-layer norms exist for these) |
jepa.enc.layer_scale |
bool | DINOv2-style ls1/ls2 present |
jepa.enc.attn_mode |
str | full (every existing file: the key is absent and the loader defaults to it) · block_causal (levjepa: key j is visible to query i iff frame_id(i) >= frame_id(j), with the CLS row open and the CLS column closed — see the levjepa family note) |
jepa.enc.proj_act |
str | lewm only: activation of the enc.proj.* MLP (gelu_erf) |
Encoder tensors (ggml dim order is reversed from PyTorch; PyTorch row-major is stored as-is, i.e. ne[0] = last PyTorch dim):
enc.patch_embed.weight [embed_dim, in_chans * tubelet * patch * patch] (conv flattened to a matmul, C-T-H-W order)
enc.patch_embed.bias [embed_dim]
enc.patch_embed_img.weight [embed_dim, in_chans * 1 * patch * patch] (2.1 only)
enc.patch_embed_img.bias
enc.pos_embed [n_tokens, embed_dim] (sincos*/learned; rows = [CLS?; patches] — registers are appended AFTER the pos add and have no pos rows)
enc.cls_token [embed_dim]
enc.reg_tokens [n_registers, embed_dim]
enc.mod_embed_img [embed_dim] (2.1)
enc.mod_embed_video [embed_dim] (2.1)
enc.blk.{i}.ln1.weight / .bias
enc.blk.{i}.attn_qkv.weight [3*embed_dim, embed_dim] (+ .bias) -- or attn_q / attn_k / attn_v
enc.blk.{i}.attn_out.weight [embed_dim, embed_dim] (+ .bias)
enc.blk.{i}.ln2.weight / .bias
enc.blk.{i}.ffn_up.weight [ffn_dim, embed_dim] (+ .bias)
enc.blk.{i}.ffn_down.weight [embed_dim, ffn_dim] (+ .bias)
enc.blk.{i}.ls1 / .ls2 [embed_dim] (layer_scale only)
enc.norm.weight / .bias (final LN; for 2.1 this is norms_block[-1])
enc.hier_norm.{k}.weight/.bias (2.1: one per hier layer, k = index into hier_layers)
enc.proj.0.weight/.bias [proj_hidden, embed_dim] (lewm: projector MLP on the CLS token, BatchNorm folded)
enc.proj.2.weight/.bias [embed_dim, proj_hidden] (lewm; act between = jepa.enc.proj_act)
Sincos tables are precomputed at conversion for the training grid (enc.pos_embed); the runtime bicubic/trilinear-interpolates for other grids.
Predictor (jepa.pred.*) — optional¶
| key | notes |
|---|---|
jepa.pred.kind |
masked (I-JEPA / V-JEPA / V-JEPA 2 / 2.1) · ac (V-JEPA 2-AC, see the family note) · lewm |
jepa.pred.embed_dim, n_layer, n_head, ffn_dim |
|
jepa.pred.n_mask_tokens |
10 (V-JEPA 2), 8 (2.1) |
jepa.pred.out_dim |
width of pred.proj output (enc_dim for V-JEPA 2; teacher_dim / n_hier_in = 1664 for 2.1 ViT-B) |
jepa.pred.rope_freq_layout, jepa.pred.rope_interpolate, jepa.pred.rope_ref_grid, jepa.pred.grid_size |
predictor RoPE convention (2.1: interleaved, not interpolated, mask ids decoded on the fixed img_size/patch grid = grid_size) |
jepa.pred.n_hier_in |
2.1: number of hierarchical encoder outputs concatenated into pred.embed (1 for ViT-B/L, 4 for g/G) |
jepa.pred.modality_embed, jepa.pred.context_proj |
2.1: pred.mod_embed_* / pred.proj_context.* present |
jepa.pred.action_dim, jepa.pred.state_dim |
7 / 7 for V-JEPA 2-AC; 10 / 0 for LeWM |
jepa.pred.n_cond_tokens |
u32, AC only: conditioning rows prepended to each frame's patch tokens (2 = action, state; Meta's use_extrinsics would make it 3). It multiplies the sequence length, so the loader bounds it |
jepa.pred.cond_order |
str, AC only: the order of those rows, "action,state" |
jepa.pred.normalize_reps |
bool, AC only: the released world-model loop passes every latent through a non-affine LayerNorm before and after the predictor (F.layer_norm(h, (D,))). jepa_ac_rollout applies it between steps; jepa_ac_normalize is the same thing for a caller feeding latents in by hand |
jepa.pred.norm_reps_eps |
f32, AC only: eps of that LayerNorm (1e-5, torch's default) |
jepa.pred.frame_causal |
bool (AC, LeWM) |
jepa.pred.n_frames |
LeWM: 3. AC: the frame slots the block-causal mask was built for (32 = num_frames / tubelet_size); it caps a context and a rollout |
jepa.pred.head_dim |
u32, only when n_head * head_dim != embed_dim (LeWM: 16 × 64 in a 192-d model) |
jepa.pred.ln_eps |
f32, eps of the affine LayerNorms in the predictor (LeWM: 1e-5) |
jepa.pred.adaln_eps |
f32, eps of the non-affine adaLN norms (LeWM: 1e-6) |
jepa.pred.act |
str, FFN activation (gelu_erf) |
jepa.pred.qkv_bias |
bool (LeWM: false → no attn_qkv.bias) |
jepa.pred.action_act |
str, activation inside the 2-layer pred.action_embed MLP (LeWM: silu) |
jepa.pred.proj_act |
str, activation inside the 2-layer pred.proj MLP (LeWM: gelu_erf) |
pred.embed.weight/bias [pred_dim, enc_dim] (context projection; single Linear when jepa.pred.n_hier_in == 1 (V-JEPA 2, 2.1 ViT-B/L); 2.1 with n_hier_in > 1 (ViT-g/G): 2-layer MLP pred.embed.0 / pred.embed.2 over the concatenated hier features)
pred.mask_tokens [n_mask_tokens, pred_dim]
pred.pos_embed [n_tokens, pred_dim] (sincos models only)
pred.blk.{i}.* same layout as enc.blk
pred.norm.weight/bias
pred.proj.weight/bias [enc_dim (or teacher_dim), pred_dim]
pred.action_embed.weight/bias [pred_dim, action_dim] (AC: single linear)
pred.action_embed.0 / .2 [4*pred_dim, action_dim] / [pred_dim, 4*pred_dim] (lewm: 2-layer MLP, act = jepa.pred.action_act)
pred.proj_context.weight/bias [teacher_dim, pred_dim] (2.1 return_all_tokens: projection of the *context* tokens)
pred.mod_embed_img / pred.mod_embed_video [pred_dim] (2.1: added to every predictor token)
pred.action_embed.weight/bias [pred_dim, action_dim] (ac: single Linear, 7 -> 1024)
pred.state_embed.weight/bias [pred_dim, state_dim] (ac: single Linear, 7 -> 1024)
pred.blk.{i}.adaln.weight/bias [6*pred_dim, pred_dim] (lewm: adaLN-zero modulation, see the lewm section)
pred.proj.0 / .2 [proj_hidden, pred_dim] / [out_dim, proj_hidden] (lewm: 2-layer MLP, BatchNorm folded, act = jepa.pred.proj_act)
Head (jepa.head.*) — optional¶
| key | notes |
|---|---|
jepa.head.kind |
attentive_pool (V-JEPA 2 classifiers) · linear_cls · none |
jepa.head.n_classes |
|
jepa.head.n_pool_layers |
3 |
jepa.head.labels |
[str] id2label, in order |
head.blk.{i}.* self-attention blocks over all tokens, run FIRST (same layout as enc.blk)
head.query [1, embed_dim] (raw query token = residual of the cross-attn)
head.xattn.ln_kv.weight/bias (HF layer_norm1: applied to keys/values only, NOT to the query)
head.xattn.q / .k / .v .weight [embed_dim, embed_dim] + .bias (no output projection in HF)
head.xattn.ln2.weight/bias (HF layer_norm2, on query + attn)
head.xattn.ffn_up / ffn_down MLP after the cross-attention (fc1/fc2)
head.cls.weight/bias [n_classes, embed_dim] (no final norm: classifier acts directly on the pooled token)
Preprocessing metadata (jepa.pre.*)¶
| key | notes |
|---|---|
jepa.pre.mean, jepa.pre.std |
[f32 ×3], ImageNet (0.485,0.456,0.406)/(0.229,0.224,0.225) for V-JEPA, LeJEPA, LeWM. The HF I-JEPA checkpoint ships 0.5/0.5/0.5 (ViTImageProcessor) and that is what the file carries — parity is against the HF processor |
jepa.pre.resize_short |
short-side resize before crop (V-JEPA: crop×256/224; LeJEPA: 256; LeWM: 224). For resize_mode=squash it is the target square size (I-JEPA HF: 224) |
jepa.pre.crop |
center crop size |
jepa.pre.resample |
bilinear · bicubic (must match the reference processor) |
jepa.pre.resize_mode |
optional — loader defaults to shortest_edge when absent (video-family files omit it). shortest_edge (resize short side to resize_short, keep aspect, centre-crop crop) · squash (resize directly to crop×crop, aspect not preserved — HF I-JEPA ViTImageProcessor with size={height,width}) |
jepa.pre.rescale |
f32, pixel scale before normalisation (1/255) |
Reference resampler. The HF 5.x processors (ViTImageProcessor, BitImageProcessor, VJEPA2VideoProcessor) are torchvision-backed: torchvision.transforms.v2.functional.resize on the uint8 CHW tensor with antialias=True, rounded back to uint8, then center-crop, /255, normalize. PIL resampling differs by up to 1 uint8 LSB on ~0.1–0.3 % of pixels; the C++ preprocessor targets the torchvision result (see tests/fixtures/README.md).
Token order¶
Video tokens are T-major, then H, then W (i = t*gh*gw + h*gw + w). Image tokens are H-major then W. hfvit and levjepa prepend CLS (and, for hfvit, registers); a prepended CLS carries no position information at all — no pos-embed row and no RoPE rotation — so the grid ids above are those of the patch tokens only, shifted by the prefix.
Quantization rules¶
Quantize only 2-D weight matrices of *.attn_qkv / attn_q / attn_k / attn_v / attn_out, *.ffn_up / ffn_down, pred.proj* (incl. pred.proj_context), pred.embed*, enc.proj.*, the hjepa per-level projections (lvl.{k}.enc.{proj,fc1,fc2}, lvl.{k}.pred.{proj,in_proj,cond_proj,out_proj}), head.blk.*, head.xattn.{q,k,v}, head.cls. pred.action_embed / pred.state_embed are matmul weights too, but their 7-wide rows are not a multiple of 32, so every quantizer falls back to F16 for them. K-quants need ne[0] % 256 == 0; fall back per tensor to q8_0 (or the q4_0/q5_0 sibling) otherwise.
Everything else stays F32, and this is a requirement rather than a convention: norms, biases, layer scales, position tables, CLS / register / mask / modality tokens and pred.blk.*.adaln biases reach ggml as the right-hand side of an add, a mul or a concat, and those ops take an f32 operand and assert on anything else. The loader checks it and refuses a file that gets it wrong (docs/architecture.md "Robustness"). Patch embeddings and pred.action_embed.* are matmul weights and so may be F16, but the converter writes them F32 today.
Converter dtype rule for --ftype f16: the quantizable set above is written as F16, everything else as F32.
Family notes¶
ijepa (facebook/ijepa_vith14_1k)¶
- The HF checkpoint stores the position table as a parameter (
embeddings.position_embeddings, [1,256,1280]). It is a 2-D sincos table, but with the halves ordered[emb_w | emb_h]— bit-identical tosincos_2d(16, 16, 1280, w_first=True)inscripts/jepa_convert/common.py(max abs diff 0.0), while the[emb_h | emb_w]ordering ofvjepa2/src/models/utils/pos_embs.pydiffers by up to 2.0. The converter stores the checkpoint table verbatim inenc.pos_embed; the runtime interpolates it for other grids (as HF does). - No CLS token, no predictor in the HF release; pooled feature = mean over the 256 patch tokens after
enc.norm. general.license = cc-by-nc-4.0.
levjepa (galilai-group/LeVJEPA-VideoMix-Large)¶
ViT-L/16 video encoder (modeling_levjepa.py, shipped with the weights; the file is the reference
implementation and trust_remote_code=True loads it). Three things separate it from vjepa2, and the
schema says all three:
tubelet_size = 1withn_frames = 16, so a 16×224×224 clip is16·14·14 = 3136patch tokens in the usual T-major/H/W order.cls_token = true— the first video family with one.enc.cls_tokenis prepended after the patch embedding and receives no position information: there is noenc.pos_embed(RoPE model) and RoPE skips it (RoPEAttention.forwardrotatesq/k[..., 1:, :]), so the runtime's cos/sin tables get an identity row (cos 1, sin 0) at index 0. Total 3137 tokens. Pooled feature = CLS afterenc.norm, i.e.last_hidden_state[:, 0].attn_mode = "block_causal"— an additive attention mask, not an option: with full attention the same weights give CLS cosine 0.945 and a worst patch token of 0.834 against the reference (archery_f16). Key j is visible to query i iffframe_id(i) >= frame_id(j), whereframe_idof a patch token isid // (gh*gw); the CLS row is fully open (it reads the whole clip) and the CLS column is closed to every patch query (it is a read-only sink, so nothing routes last-frame information into a first-frame token). Density 53.1 % at 3137 tokens. Everything else followsvjepa2: 3-D RoPE θ 10000 in the tiled layout (rope_freq_layout = tiled;rotate_queries_or_keysuses Meta'srepeat(1,1,1,2)expansion — the interleaved 2.1 layout scores CLS cosine 0.841 on these weights), nointerpolate_rope, LN eps 1e-6, GELU(erf), fused qkv with bias, pre-LN, no layer-scale.
The checkpoint is the EMA encoder only: no predictor, no head, no projector. jepa.pre.* comes from the
model card (there is no preprocessor_config.json): ImageNet mean/std, short side → 224, centre crop 224,
bicubic, rescale 1/255. A still image is fed as a 16-frame clip by repeating the frame, which is what
the model card's ImageNet probe does. general.license = cc-by-nc-4.0.
vjepa2_ac (V-JEPA 2-AC, jepa.pred.kind = "ac")¶
jepa.family stays vjepa2 — the encoder is an ordinary V-JEPA 2 ViT-g/16 and the whole delta is in
the predictor. One bundle carries both. scripts/jepa_convert/vjepa2_ac.py writes it from Meta's
single vjepa2-ac-vitg.pt; src/vjepa2_ac.cpp builds the graph and
scripts/jepa_convert/selftest.py::ac_predictor_forward is the executable spec.
The encoder in that checkpoint is not the HF facebook/vjepa2-vitg-fpc64-256 release. Measured
per tensor over all 484 encoder tensors, the cosine is median 0.99995, p05 0.99896, min
0.99647 (blocks.0.norm1.weight), with 39 of 484 below 0.999 — the low tail is the early blocks'
attention projections (blocks.1.attn.proj.weight 0.99767, blocks.0.attn.proj.weight 0.99827,
where max |Δ| is 2.1e-2 on values up to 1.35). Across all 484 tensors the largest absolute difference
is 4.18e-2, on enc.blk.31.attn_qkv.bias, whose values reach 8.2. Close, in other words, but not the
same weights. (Method, so the count is reproducible: every tensor of the checkpoint's encoder
against its HF counterpart, with Meta's fused attn.qkv compared against concat(query, key, value)
in that order; cosine over the flattened tensor, accumulated in float64. The accumulation matters:
blocks.2.mlp.fc1.weight sits 2.3e-7 below the boundary at 0.998999767, and a float32 dot product
over its 8.65 M elements carries enough error to move it across, reading 38.)
Meanwhile encoder and target_encoder inside the AC checkpoint are bit-identical — the encoder
was frozen during AC training. src/hub/backbones.py::_make_vjepa2_ac_model loads state_dict["encoder"], so that is
what the bundle ships, and it gets its own parity fixtures.
Predictor (src/models/ac_predictor.py::VisionTransformerPredictorAC, 24 blocks × 1024 dims,
16 heads × 64, ffn 4096, GELU(erf), LN eps 1e-6, fused qkv with bias, no mask tokens and no
position table):
x = pred.embed(context_latents) # [T*HW, 1024] HW = grid_size^2 = 256
a_t = pred.action_embed(action_t) # [T, 1024] action_dim 7
s_t = pred.state_embed(state_t) # [T, 1024] state_dim 7
seq = per frame t: [a_t, s_t, x_{t,0} .. x_{t,HW-1}] # T * (2 + HW) rows
24 pre-LN blocks, full-width attention under the block-causal mask, 3-D RoPE (tiled) on q and k
seq = seq minus the 2 conditioning rows of every frame # [T*HW, 1024]
out = pred.proj(pred.norm(seq)) # [T*HW, 1408]
Two details decide whether an implementation is right:
- The mask is block-causal over whole frames, not over tokens: every row of frame t attends
every row of frames 0..t, conditioning rows included
(
build_action_block_causal_attention_maskfills wholeN_T × N_Tblocks,N_T = 2 + HW). It is built on the host and passed throughjepa_attn_opts::mask, assrc/lewm.cppdoes. Row block t of the output is therefore the prediction of frame t+1 given frames 0..t: one call with T frames also answers every shorter prefix. - The action and state tokens are rotated on the depth axis only, with
pos = t, and their height/width lanes are left alone (ACRoPEAttentionrotatesq[..., :d_dim]witharange(T)and passesq[..., d_dim:]through). A grid id oft·grid²hash = w = 0, whose cos/sin rows are exactly 1 and 0 — the identity — so one id list feeds the ordinaryjepa_rope3d_tables_idsfor every row:
| row | grid id |
|---|---|
| action / state of frame t | t·grid² |
| patch (t, h, w) | t·grid² + h·grid + w |
head_dim is 64 here, so d = 2·⌊⌊64/3⌋/2⌋ = 20 per axis: lanes 0–59 rotate, 60–63 are untouched.
ACRoPEAttention also rescales h/w by grid_size / H; the released checkpoint runs at its own
16×16 grid, where that factor is 1, and jepa_ac_predict decodes the ids on jepa.pred.grid_size.
extrinsics_encoder.* is instantiated unconditionally by the reference module but only read when
use_extrinsics=True, which the released hub entry never sets; the converter skips it and says so.
Preprocessing is not the V-JEPA 2 video-processor pipeline. The demo builds
app/vjepa_droid/transforms.py::make_transforms with random_resize_scale=(1,1) and
random_resize_aspect_ratio=(1,1), which degenerates random_resized_crop to a centre crop of the
largest square followed by torch.nn.functional.interpolate(bilinear, align_corners=False, no
antialias) to 256×256, then (x − 255·mean) / (255·std). On the square DROID/Franka renders the
model consumes, both the crop and the resize are the identity, which is what resize_short = crop =
256 reproduces; the file records that, and docs/parity.md measures the residual (2.4e-07, one
float32 ulp). general.license = mit (the facebookresearch/vjepa2 LICENSE).
hfvit (OK-AI/lejepa-vits16-pretrain-in1k)¶
DINOv2-style ViT-S/16 from Open-Knowledge-AI/lite_ssl ("ViTv2"): LN eps 1e-6, GELU(erf), qkv/proj/ffn biases,
learned pos_embed with the CLS slot at row 0, no registers, no LayerScale (init_values=null). The checkpoint's
backbone.cva_module_proj.* (loss-side DINOHead) is not converted. Pooled feature = CLS after enc.norm.
lewm (quentinll/lewm-pusht)¶
Encoder = HF ViTModel tiny/14 written in the hfvit layout (LN eps 1e-12, ViTConfig default). The world-model
state is emb = enc.proj(CLS) (2-layer MLP, BatchNorm folded, GELU). Predictor (jepa.pred.kind = lewm,
192-d, 6 layers, 16 heads × 64, ffn 2048, n_frames 3, action_dim 10, causal over frames):
a_t = action_embed.2( silu( action_embed.0( action_t ) ) ) # (T, 192) [Conv1d(k=1) folded into .0]
x = emb + pred.pos_embed[:T]
for each block i:
sh_a, sc_a, g_a, sh_m, sc_m, g_m = split6( adaln( silu(a) ) ) # each (T, 192)
h = LN(x, no affine, eps=adaln_eps) * (1 + sc_a) + sh_a
h = ln1(h) # affine, eps=ln_eps
q,k,v = attn_qkv(h) (no bias) ; causal softmax(q kᵀ / 8) v over the T frames, 16 heads × 64
x = x + g_a * attn_out(...)
h = LN(x, no affine, eps=adaln_eps) * (1 + sc_m) + sh_m
x = x + g_m * ffn_down( gelu( ffn_up( ln2(h) ) ) )
x = pred.norm(x)
pred_t = pred.proj.2( gelu( pred.proj.0( x_t ) ) ) # next-state embedding in the same space as emb
pred_{T-1} as the next emb, slide a window of n_frames. Tensor names: enc.proj.{0,2},
pred.pos_embed, pred.blk.{i}.{adaln,ln1,attn_qkv,attn_out,ln2,ffn_up,ffn_down}, pred.norm,
pred.action_embed.{0,2}, pred.proj.{0,2}. Preprocessing: ImageNet mean/std, bilinear resize to 224 (square renders).
scripts/jepa_convert/selftest.py::lewm_predictor_forward is the executable reference of this graph.
hjepa (jepa-world-models/h-jepa — H-JEPA, HWM and flat LeWM runs)¶
H-JEPA is a stack of LeWM-style world models. Level 1 predicts one environment
step ahead in the latent space of the image encoder; level k ≥ 2 re-encodes every stride-th state of level k − 1
and predicts one of its steps ahead, conditioned on a latent macro-action that summarises the stride actions of the
level below. A 1-level file is the paper's flat LeWM baseline; hwm_* runs are the HWM baseline, whose upper encoders
are the identity.
The encoder is an HF ViTModel in the hfvit layout (enc.*, LN eps 1e-12) and jepa.pred.kind is absent: the
hierarchy has its own metadata, one array entry per level (level 1 first), and its tensors live under lvl.{k}.
(k is 1-based).
| key | type | notes |
|---|---|---|
jepa.hjepa.n_levels |
u32 | 1 – 4 in the released files |
jepa.hjepa.ln_eps / adaln_eps |
f32 | 1e-5 (every affine LayerNorm outside the ViT) / 1e-6 (the non-affine adaLN norms) |
jepa.hjepa.proj_kind |
str | level-1 pixel projector on the CLS token: mlp (BatchNorm folded) · layernorm · identity |
jepa.hjepa.pixel_dim / proprio_dim |
u32 | width of the projected CLS / of the proprio input (0 = none) |
jepa.hjepa.raw_action_dim / frameskip |
u32 | a level-1 action is frameskip raw actions stacked (0 = not recorded) |
jepa.hjepa.embed_dim |
[u32] | width of each level's state |
jepa.hjepa.stride / window |
[u32] | level k reads every stride-th state of the level below; window is 1 (the loader refuses anything else) |
jepa.hjepa.target_length |
[u32] | training clip length at the level (history + 1); informational |
jepa.hjepa.enc_kind |
[str] | level 1: vit · vit_proprio; above: latent_mlp · identity |
jepa.hjepa.action_dim / act_embed_dim |
[u32] | width of the level's action / of its embedding |
jepa.hjepa.act_enc_kind |
[str] | none (level 1) · seq (one-layer transformer over the chunk) · mlp (flattened chunk) |
jepa.hjepa.act_enc_{dim,n_layer,n_head,max_chunk} |
[u32] | the seq encoder; 0 where there is none |
jepa.hjepa.pred_kind |
[str] | adaln (the LeWM predictor) · adaln_bare (DROID: no inner LayerNorms, no projector, own action embedder) |
jepa.hjepa.pred_{dim,n_layer,n_head,head_dim,ffn_dim,n_frames} |
[u32] | each level's predictor; pred_dim may differ from embed_dim |
level 1 pix = proj( ViT(pixels)[CLS] ) # lvl.1.enc.proj.{0,2} | lvl.1.enc.proj_ln | identity
s1 = pix # enc_kind vit
s1 = concat( pix, proprio.2( gelu( proprio.ln( proprio.0( proprio ) ) ) ) ) # vit_proprio
level k sk_T = [out_norm]( fc2( gelu( fc1( in_norm( s{k-1}_{T*stride} ) ) ) ) ) # latent_mlp; identity: a copy
actions e_t = act_embed.2( silu( act_embed.0( a_t ) ) ) # absent tensors = identity
seq: x = [ pos[0] ; step( e{k-1}_{T*stride + i} ) + pos[1 + i] ], i < stride # a CLS row, then the chunk
x = pre-LN transformer layers (ReLU FFN, every row sees every row)
ak_T = out_norm( head.2( head.ln( gelu( head.0( x[CLS] ) ) ) ) ) # out_norm eps 1e-6
mlp: ak_T = [out_norm]( fc2( relu( fc1( flatten(chunk) ) ) ) ) # short chunks zero-padded
predict x = in_proj( s + pos_embed[:T] ) ; c = cond_proj( e ) # both projections optional
adaln_bare: c = pred.act_embed.2( silu( pred.act_embed.0( e ) ) )
blocks as in the lewm section (adaln_bare has no ln1 / ln2), then norm
next = proj.2( gelu( proj.0( out_proj( x ) ) ) ) # out_proj and proj optional
A seq chunk shorter than stride (the tail of a stream) is encoded at its true length. The reference pads it and
masks the padding as attention keys; only the CLS row is read, so the two are the same function.
Tensor names, all under lvl.{k}.: enc.proj.{0,2} · enc.proj_ln · enc.proprio.{0,ln,2} (level 1);
enc.{in_norm,fc1,fc2,out_norm} (above); act_embed.{0,2}; act_enc.{step,pos,blk.{i}.*,head.{0,ln,2},out_norm} or
act_enc.{fc1,fc2,out_norm}; act_queue [n, action_dim], the latent actions seen in training, which the reference
planner samples its initial candidates from; pred.{pos_embed,in_proj,cond_proj,out_proj,norm,proj.{0,2},act_embed.{0,2}}
and pred.blk.{i}.{adaln,ln1,attn_qkv,attn_out,ln2,ffn_up,ffn_down}. Outside the levels: norm.action.{mean,std} and
norm.proprio.{mean,std}, the z-score statistics the model's action and proprio inputs were normalised with.
Folds done by the converter: eval-mode BatchNorm1d of every mlp projector into the Linear before it; the Conv1d(k=1)
of an action Embedder into the Linear after it. Preprocessing: ImageNet mean/std, bilinear squash to the encoder's
image size (the datasets are stored at that size). The source files are pickled module objects;
scripts/jepa_convert/torch_object.py reads them without running any code they name.
scripts/jepa_convert/hjepa_numpy_ref.py is the executable reference of these graphs.