Instructions to use apiantonio/vjepa2.1-vit-large-384 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use apiantonio/vjepa2.1-vit-large-384 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("video-classification", model="apiantonio/vjepa2.1-vit-large-384", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("apiantonio/vjepa2.1-vit-large-384", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
V-JEPA 2.1 ViT-L/16 384 β HuggingFace port
A HuggingFace-format conversion of Meta AI's V-JEPA 2.1 ViT-L/16 video encoder and predictor, operating at 384x384 resolution.
The weights are Meta's, copied without modification. This repository provides the
transformers-compatible packaging plus a documented numerical validation against the original
implementation.
An equivalent community port already exists (Dev-Jahn/vjepa2.1-vitl-fpc64-384). This repository adds an
independently reproduced conversion together with the validation results below. Forward outputs match the existing port to all reported digits.
Provenance
| Original weights | Meta AI β facebookresearch/vjepa2 |
| Source checkpoint | vjepa2_1_vitl_dist_vitG_384.pt |
| State dict key | ema_encoder |
| Conversion script | convert_vjepa21_to_hf.py from Dev-Jahn/vjepa2-hf |
| Modeling code | modeling_vjepa21.py / configuration_vjepa21.py, same repository |
| Weight modifications | None. Every tensor is a bit-exact copy of the original. |
The only structural change is that the fused QKV projection of each attention block is split into
separate query / key / value matrices, following the convention used by transformers. This
is a re-parameterization, not a change of weights. It is also convenient downstream: PEFT adapters
apply to independent q, k, v projections as in the original LoRA formulation, rather than to a
fused projection.
If you want to reproduce the comparisons below, note that torch.hub.load against the current
main of facebookresearch/vjepa2 does not work: src/hub/backbones.py ships with
VJEPA_BASE_URL = "http://localhost:8300" and the real URL commented out above it. Download the
checkpoint directly from https://dl.fbaipublicfiles.com/vjepa2/vjepa2_1_vitl_dist_vitG_384.pt
and build the reference modules from app/vjepa_2_1/models/, which is what the shipped scripts do.
Architecture
| Encoder | Predictor | |
|---|---|---|
| Hidden size | 1024 | 384 |
| Layers | 24 | 12 |
| Attention heads | 16 | 12 |
| MLP ratio | 4.0 | 4.0 |
| Patch size | 16 x 16, tubelet 2 | β |
| Input resolution | 384 x 384 | β |
| Position encoding | 3D RoPE | 3D RoPE |
| Parameters | 304.7 M | 23.0 M |
Total: 327.7 M.
Distilled from ViT-G, with a single distillation output (n_output_distillation = 1). The
predictor takes the last encoder representation and projects to the 1664-dim teacher space.
The encoder carries a separate patch embedding for single images (img_temporal_dim_size = 1),
learnable image/video modality embeddings, and four per-layer normalizations used for hierarchical
feature extraction.
The four per-layer normalizations sit at encoder layers [5, 11, 17, 23]. Because this checkpoint
was trained with a single distillation output, only the last of them
(norms_block.3) carries a trained affine transform; the other three are at
PyTorch's default LayerNorm initialization (weight = 1, bias = 0) in Meta's checkpoint as
well. They are not dead β a LayerNorm with identity affine still normalizes β and the out_layers path uses them exactly as the reference implementation does. If you need
intermediate features with trained affine parameters, use the ViT-g or ViT-G checkpoints.
The attentive pooler used by VJEPA21ForVideoClassification defaults to num_pooler_heads = 16
and num_pooler_layers = 3 β three self-attention blocks plus one cross-attention block, which is
num_probe_blocks: 4 and classifier.num_heads: 16 in every frozen-probe config under
configs/eval_2_1/. The pooler is always trained from scratch, so this only sets the architecture
of the probe you train.
RoPE is interpolatable: positions are rescaled by
(pretrained_grid_size - 1) / (n_patches - 1) with pretrained_grid_size = 256 / patch_size,
following app/vjepa_2_1/models/utils/modules.py in the reference repository. This is what allows
the model to accept resolutions other than 384 and non-square inputs.
Usage
Feature extraction
import torch
from transformers import AutoModel, AutoVideoProcessor
CKPT = "apiantonio/vjepa2.1-vit-large-384"
processor = AutoVideoProcessor.from_pretrained(CKPT, trust_remote_code=True)
model = AutoModel.from_pretrained(CKPT, trust_remote_code=True, dtype=torch.float32).eval().cuda()
# list_of_clips: list of clips, each a list of HxWx3 uint8 frames
inputs = processor(list_of_clips, return_tensors="pt").to("cuda")
with torch.no_grad():
out = model(**inputs, skip_predictor=True)
feats = out.last_hidden_state # (B, T/2 * 24 * 24, 1024)
pixel_values_videos accepts (B, C, T, H, W), (B, T, C, H, W) β the layout produced by the
video processor and by the transformers V-JEPA 2 classes β and (B, T, H, W, C). The channel axis
is identified by matching config.in_chans; when more than one axis could match, the channels-first
reading wins and a warning is emitted. A clip of exactly 3 frames is the realistic ambiguous case.
Token grid: (frames / 2) x 24 x 24. A 64-frame clip yields 32 x 24 x 24 = 18,432 tokens, so
attention cost grows quadratically with clip length β use model.gradient_checkpointing_enable()
and small batches for long clips.
Intermediate features
out = model(
**inputs,
skip_predictor=True,
out_layers=model.config.encoder_hierarchical_layers, # [5, 11, 17, 23]
)
levels = out.multilevel_hidden_states # 4 tensors, each normalized with its own LayerNorm
This reproduces the out_layers recipe of the reference frozen-evaluation probes. Levels come back
in network order regardless of the order they were requested in, so zipping them with
encoder_hierarchical_layers is correct. output_hidden_states=True is also available and returns
the raw, un-normalized output of the embedding layer plus every transformer layer.
Predictor
out = model(**inputs) # runs the predictor
pred = out.predictor_output.last_hidden_state # (B, N_target, 1664)
ctx = out.predictor_output.context_hidden_state # (B, N_context, 1664)
Without explicit masks, context_mask and target_mask both default to all tokens. Pass lists of
(B, K) index tensors for a real masked-prediction setup, and mask_index to select which of the
eight learnable mask tokens is injected β the reference passes the index of the sequence-length
group and defaults to 1. A single (context_mask, target_mask) pair is supported; more raises
NotImplementedError rather than silently producing wrong shapes.
For the pre-training forward, pass masks to the encoder as well. Tokens are then dropped before
the transformer layers, so attention only ever sees the context and RoPE receives the true token
positions β this is z = encoder(clips, masks_enc) in the reference, and it is not the same thing
as masking the encoder output:
out = model(
**inputs,
masks=[context_idx], # (B, K_ctx)
context_mask=[context_idx],
target_mask=[target_idx], # (B, K_tgt)
)
Classification head
from transformers import AutoModelForVideoClassification
model = AutoModelForVideoClassification.from_pretrained(
CKPT, trust_remote_code=True, num_labels=101, label2id=label2id, id2label=id2label
)
The attentive pooler and the classifier are initialized from scratch, so the warning about newly
initialized weights is expected and the video-classification pipeline tag is for discoverability
only β the head must be trained before the model predicts anything meaningful. Multi-label targets
work through config.problem_type = "multi_label_classification".
Parameter-efficient fine-tuning
LoRA and DoRA work out of the box with peft. Scope target_modules explicitly, because matching
on bare names such as ["query", "key", "value", "proj"] also adapts the predictor, which
VJEPA21ForVideoClassification never executes:
from peft import LoraConfig, get_peft_model
config = LoraConfig(
r=16, lora_alpha=32, lora_dropout=0.05,
target_modules=r".*vjepa21\.encoder\.layer\.\d+\.attention\.(query|key|value|proj)$",
modules_to_save=["classifier", "pooler"],
)
model = get_peft_model(model, config)
Preprocessing
VJEPA21VideoProcessor reproduces EvalVideoTransform from
evals/video_classification_frozen/utils.py: the short side is resized to 384, a 384x384 crop is
taken, pixels are scaled to [0, 1] and normalized with ImageNet statistics β mean
(0.485, 0.456, 0.406), std (0.229, 0.224, 0.225), confirmed at
evals/video_classification_frozen/eval.py:411.
This differs from the stock VJEPA2VideoProcessor in two ways, both of which require a dedicated
class. It resizes the short side to exactly the crop size rather than to crop_size * 256 / 224,
and it disables torchvision's antialiasing so the resize matches the reference
cv2.resize(..., cv2.INTER_LINEAR). The first point is worth being precise about: make_transforms
only selects EvalVideoTransform when num_views_per_clip > 1, and falls back to the
crop_size * 256 / 224 transform otherwise β but every V-JEPA 2.1 frozen-probe config under
configs/eval_2_1/ sets num_views_per_segment: 3, so EvalVideoTransform is the transform the
2.1 evaluations actually use. The second point only shows up when downscaling: on a 1080p frame the
antialiased and non-antialiased results differ by up to 144/255 per pixel, which is a visibly
different image fed to a model that never saw antialiased inputs. With antialiasing disabled the two
pipelines agree to 1/255, the 8-bit quantisation floor, at every resolution tested.
The reference transform takes several sliding crops along the long axis and averages the predictions; this processor takes a single centre crop. Multi-view aggregation, if you want it, is the caller's job.
Frame sampling is left to the caller. The reference configs for V-JEPA 2.1 β under
configs/eval_2_1/, not configs/eval/, which holds the V-JEPA 2 settings β use 16 frames with
frame_step = 4 and 8 segments for Kinetics-400 and COIN; 32 frames with frame_step = 2 and 4
segments for Jester and Diving48; and for Something-Something v2 either 16x4 on ViT-B or 64x2 with
2 segments on ViT-g and ViT-G. All of them use 3 spatial views per segment.
config.frames_per_clip = 64 records the pre-training clip length and is not an inference
constraint.
Data pipeline
The processor covers the spatial transform. Which frames a clip is built from, and what a decoder
returns for them, are the caller's responsibility and both fail silently, so video_io.py ships a
reference implementation of each and tests/test_frame_sampling.py checks any implementation
against it.
from video_io import clip_indices, decode_frames
indices = clip_indices(video_len=len(video), frames_per_clip=16, frame_step=4, num_clips=1)
frames = decode_frames(path, indices[0]) # (16, H, W, 3) uint8, RGB
inputs = processor([list(frames)], return_tensors="pt")
Two details are worth stating because they are easy to get wrong and produce no error.
The reference places the frames of a clip with np.linspace across a window of
frames_per_clip * frame_step, so the effective stride is fpc*fstp/(fpc-1), not frame_step.
Writing range(0, fpc*fstp, fstp) is the intuitive reading of "16 frames, step 4" and it selects a
different frame 12 times out of 16. The resulting clip looks entirely plausible.
The decoder must return RGB. cv2.VideoCapture returns BGR, and decode_frames converts
explicitly; it also decodes sequentially rather than seeking with CAP_PROP_POS_FRAMES, which snaps
to the nearest keyframe on several codecs and quietly returns a neighbouring frame. decord, the
backend the reference pipeline uses, is preferred when installed; the shipped decoder was checked
against it and agrees exactly, max|Ξ| = 0/255.
Long videos and multiple clips
The reference evaluation splits a video into num_segments partitions, takes one clip and
num_views_per_segment spatial crops from each, and averages the resulting softmax
distributions into a single prediction per video. That is variance reduction for classifying a
short video, and it does not transfer to a long one: with the reference settings for this
checkpoint, eight segments cover 98.7% of a ten-second video but only 14.2% of a two-minute one,
leaving a blind gap of 386 frames β thirteen seconds during which nothing is observed.
clip_indices reproduces that partitioned sampling. When you need a score per position in time
rather than one prediction per video, use dense_clip_indices instead, and map the results back
onto frames:
from video_io import dense_clip_indices, temporal_coverage, clip_scores_to_frame_scores
clips = dense_clip_indices(video_len, frames_per_clip=16, frame_step=4) # stride=window
print(temporal_coverage(clips, video_len)) # {'covered_fraction': 1.0, 'max_gap': 0, ...}
frame_scores = clip_scores_to_frame_scores(clips, scores, video_len, reduce="max")
stride controls overlap and therefore temporal resolution. temporal_coverage reports what a
set of clips actually looks at, which is worth checking before trusting any per-frame metric.
For video-level predictions, aggregate_predictions reproduces the reference combination β
the mean of the softmax outputs, not of the logits. The two are different estimators and can rank
classes differently; on a two-view example they give [0.52, 0.19, 0.29] against
[0.77, 0.10, 0.13].
Note that dense extraction has a storage cost. A two-minute video at 30 fps needs 57 non-overlapping clips, and a 16-frame clip at 384 is 4608 tokens, so caching token-level features costs about 1.1 GB at this hidden size. Pooling to one vector per clip is four orders of magnitude smaller, but forecloses any probe that consumes the token sequence.
Validation
All tests use float32 and torch.no_grad(). Where the port and the reference are compared, the
attention kernel is matched on both sides: the reference encoder is built with the same use_sdpa
setting as the port, and the reference predictor always runs SDPA because
VisionTransformerPredictor.__init__ has no use_sdpa parameter and swallows it through **kwargs.
Comparing across kernels measures the kernel, not the port.
1. Weight-level provenance
Every tensor in the published safetensors was traced back to a tensor in the official checkpoint
and compared element-wise.
| Check | Result |
|---|---|
| Tensors compared | 608 |
max|Ξ| across all tensors |
0.000e+00 |
| Tensors with a shape mismatch | 0 |
| Parameters with no origin in the checkpoint | 0 |
The last row matters: the conversion script calls load_state_dict(..., strict=False), which
silently leaves unmatched parameters randomly initialized. Zero orphans means no such parameter
survived β for the predictor as well as the encoder. Loading through AutoModel and through
AutoModelForVideoClassification was also checked with output_loading_info=True: zero missing,
zero unexpected, zero mismatched keys on the first, and only pooler.* and classifier.* newly
initialized on the second.
2. Forward parity on the published weights, full resolution
The same input tensor was passed through Meta's official modules, loaded with the official checkpoint, and through this port, at 384x384.
| Path | Shape | max|Ξ| |
rel | cos-sim (min) |
|---|---|---|---|---|
| Encoder, T = 16 | (1, 4608, 1024) | 0.000e+00 |
0.000e+00 |
1.000000 |
| Concatenated hierarchical output | (1, 4608, 1024) | 0.000e+00 |
0.000e+00 |
β |
| Predictor, target tokens | (1, 2304, 1664) | 0.000e+00 |
0.000e+00 |
1.000000* |
| Predictor, context tokens | (1, 2304, 1664) | 0.000e+00 |
0.000e+00 |
1.000000* |
* cosine_similarity computes aΒ·b / (βaβΒ·βbβ); numerator and denominator follow different
rounding paths, so the metric can print as 0.999999 even for bit-identical tensors. The absolute
differences are exactly zero.
Summary. Bit-exact agreement with the reference implementation, encoder and predictor, on the
published weights at full resolution. The predictor comparison is not vacuous here: the mask tokens
of this checkpoint have a maximum magnitude of 3.684e-01, so the mask-token lookup is genuinely
exercised. A comparison run with zero_init_mask_tokens=True on both sides would pass even with a
broken index, because every mask token would be exactly zero.
Reproduce with verify_vjepa21_port.py --repo apiantonio/vjepa2.1-vit-large-384 --checks ABCDEFG,
or with verify_big_models.py for the ViT-g and ViT-G variants, which never holds the reference and
the port in memory at the same time. --frames controls the clip length; 32 and 64 reproduce the
same zero.
3. Implementation parity, code paths not covered above
To cover the paths Β§2 does not reach, reference modules were built directly from
app/vjepa_2_1/models/ and their weights copied into the port, then outputs compared. These runs
use reduced hidden sizes so the suite stays fast: they establish that the code paths are
equivalent, which is width-independent, and complement the full-resolution published-weight results
in Β§2.
| Path | max|Ξ| |
|---|---|
Encoder, video branch, n_output_distillation = 1 |
0.000e+00 |
Encoder, video branch, n_output_distillation = 4 |
0.000e+00 |
Encoder, image branch (T = 1) |
0.000e+00 |
| Encoder, non-square input (96x128) | 0.000e+00 |
| Encoder, masked JEPA forward | 0.000e+00 |
out_layers per-level features, all 4 levels, both regimes |
0.000e+00 |
| Concatenated hierarchical output | 0.000e+00 |
Predictor, 4 (n_output_distillation, teacher_embed_dim) combos |
β€ 4.880e-07 |
Predictor with non-zero mask tokens, mask_index 0, 1, 3, 7, 9 |
β€ 1.192e-07 |
Predictor, image modality (mod="image") |
β€ 1e-03 |
| Full JEPA forward, masked encoder into predictor | β€ 3.427e-07 |
Both distillation regimes are covered: n_output_distillation = 1 is the ViT-B and ViT-L
configuration, 4 the ViT-g and ViT-G one, where the encoder emits concatenated levels and the
predictor fuses them through a 2-layer MLP.
The remaining sub-microvolt residuals on the predictor rows are a kernel difference, not an
algorithmic one: this suite runs the port in eager mode while the reference predictor always runs
SDPA. Matching the kernel removes them entirely, which is why Β§2 reports exactly zero. The port
reorders tokens with gather where the reference uses stack; that is a pure permutation and
contributes nothing, as the full-resolution result confirms.
The masked-encoder row is worth calling out. Passing masks drops tokens before the transformer
layers, so attention sees only the context and RoPE receives the true positions. A control test
checks that this is not equal to gathering the encoder output afterwards β otherwise the parity
result would be meaningless.
tests/test_end_to_end_pipeline.py closes the loop with the preprocessing: one clip is pushed
through the reference Resize -> crop -> ClipToTensor -> Normalize and through
VJEPA21VideoProcessor, at 240p, 480p, 720p and 1080p. All four land on max|Ξ| = 1.751e-02, which
is one 8-bit grey level divided by the smallest normalisation std β the floor two different bilinear
implementations can reach. A control test forces antialiasing back on and confirms the comparison
then fails by 2.65, so the tolerance is tight enough to catch the regression it was written for.
Pushed through the model, that pixel-level residual costs:
| Source | rel vs ref | min cos-sim |
|---|---|---|
| 240p | 4.294e-03 |
0.999903 |
| 480p | 5.698e-03 |
0.999796 |
| 720p | 6.777e-03 |
0.998771 |
| 1080p | 6.356e-03 |
0.999819 |
4. Functional test suite
122 tests ship with the repository, green on both transformers==4.57.1 and transformers==5.14.1.
| File | Tests | Needs |
|---|---|---|
tests/test_vjepa21.py |
49 | nothing |
tests/test_frame_sampling.py |
32 | ffmpeg; VJEPA21_USER_SAMPLER / _DECODER for contracts |
tests/test_parity_official.py |
16 | VJEPA2_REPO |
tests/test_thesis_robustness.py |
13 | optionally VJEPA21_CKPT |
tests/test_end_to_end_pipeline.py |
12 | VJEPA2_REPO, optionally VJEPA21_CKPT |
The first file covers configuration properties and validation, input layouts and the ambiguous-layout
warning, hidden states and attentions, multi-level extraction and its ordering, the image branch,
variable resolution, sdpa-vs-eager agreement, the masked encoder forward, the predictor with default,
explicit and pre-masked contexts, mask_index selection, classification loss for both single- and
multi-label targets, gradient checkpointing parity on the encoder and the predictor path, rejection
of unknown keyword arguments, low-precision weight loading, the video processor's geometry and
normalization constants, and a full save_pretrained / from_pretrained round-trip through the Auto
classes. It needs neither weights nor the reference repository.
5. Properties relevant to downstream pipelines
tests/test_thesis_robustness.py checks assumptions an experimental pipeline tends to make
implicitly. Measured on this checkpoint, float32 on an NVIDIA GPU unless stated otherwise.
| Property | Result |
|---|---|
| Two identical forwards in eval mode | bit-identical |
| Same clip at batch 1 vs batch 8 | rel 5.902e-06, cos-sim 1.000000 (abs 7.532e-04) |
| Storing float32 features as fp16 | 1.760e-04, min cos-sim 1.000000 |
merge_and_unload changes the function |
LoRA max|Ξ| = 4.7e-09, DoRA max|Ξ| = 5.6e-09 |
| Merged model vs baseline parameter count | identical, 0 residual adapter tensors |
Reduced precision, split by attention kernel, because the two do not behave the same way:
| dtype | kernel | rel vs float32 | min cos-sim |
|---|---|---|---|
| bfloat16 | sdpa | 1.647e-02 |
0.997579 |
| bfloat16 | eager | 2.542e-02 |
0.998220 |
| float16 | sdpa | 2.372e-03 |
0.999755 |
| float16 | eager | overflow β NaN | β |
Repeated forwards of the same shape are bit-identical, so a cached feature is reproducible from its input. Changing the batch size is not guaranteed to be bit-exact on GPU: cuBLAS and the fused attention kernels select tiling and reduction order as a function of the input shape. Where the residual appears it is around 1e-05 relative and irrelevant to a probe, but it means a feature store should be built with a fixed batch size.
Read absolute differences with care: they track the magnitude of the activations, not the quality of the agreement, which is why the relative error is the meaningful number.
The last two rows of the first table are the reparameterisation property that makes LoRA and DoRA free at inference time. They are measured on a small model, since exact merging follows from the algebra rather than from the weights. DoRA is checked explicitly and not assumed to follow from LoRA: it decomposes the update into direction and magnitude, so merging has to fold the magnitude vector back in as well.
To refresh the first two rows on this checkpoint:
VJEPA21_CKPT=apiantonio/vjepa2.1-vit-large-384 python -m pytest tests/test_thesis_robustness.py -s -q
Precision guidance
float16 is several times more accurate than bfloat16 for inference with this model β ten mantissa
bits against eight β and the gap is consistent across the whole family. But float16 is only safe
with a fused attention kernel. The model was pre-trained in bfloat16, which shares float32's
exponent range, so nothing ever constrained the attention logits to stay inside float16's maximum of
65504. SDPA and flash attention accumulate the softmax in float32 and are unaffected; the eager
kernel computes qΒ·kα΅ in float16 natively and can overflow to NaN. The residual stream itself stays
well inside range β it peaks around 430 β so the failure is specific to the attention logits.
On this checkpoint the eager kernel does overflow: float16 inference returns NaN. The same happens on ViT-B, ViT-L and ViT-g; ViT-G is the one variant tested where it does not. It is a property of the individual checkpoint's activation magnitudes, not of the architecture.
Since output_attentions=True forces the eager kernel β it is the only one that materializes
attention probabilities β extracting attention maps in float16 is the concrete way to hit this.
For feature extraction, prefer float32, or float16 with SDPA. bfloat16 costs a worst-case
per-token cosine similarity of 0.997579; if features feed a cosine-similarity or
nearest-neighbour decision rule, that noise enters the decision directly. Storing float32 features
as float16 is nearly free by comparison and halves the cache, which keeps the inference-precision
and storage-precision decisions separate.
For training, the usual bfloat16 argument still applies: the wider exponent range is safer for gradients. These are two separate decisions and it is worth making them separately.
Compatibility
Requires transformers >= 4.50; tested on 4.57.1 and 5.14.1. sdpa and flash_attention_2 are
supported; requesting output_attentions=True falls back to the eager kernel, the only one that
materializes attention probabilities β see the precision guidance before combining that with
float16. peft 0.19.1 was verified for LoRA and DoRA, including merge_and_unload and the
resulting parameter count. Weights held in bfloat16 or float16 are supported.
Not verified
Stated explicitly so the scope of the validation above is not overread:
- Downstream benchmarks. No evaluation numbers from the V-JEPA 2.1 release were reproduced. This port is validated for numerical equivalence, not for task performance.
- Attentive pooler initialization. The pooler reproduces the reference
AttentivePoolerstructurally, but the reference calls_rescale_blocks()after initialization, dividingattn.proj.weightandmlp.fc2.weightof layer i bysqrt(2*(i+1)). That is not reproduced here. It touches no published weight β the pooler is always trained from scratch β but a probe trained here starts from a different initialization scale than Meta's. - Your own data pipeline.
video_io.pyreproduces the reference sampler and decoder and is tested against them, but if you write your own, only the contract tests intests/test_frame_sampling.pystand between you and a silent mismatch. - Aggregation policy.
aggregate_predictionsreproduces the reference combination anddense_clip_indicescovers the whole timeline, but which policy suits a given task β mean over clips, max, top-k β is a modelling decision that is neither made nor evaluated here. - Multiple mask pairs in the predictor. Only a single
(context_mask, target_mask)pair is supported. - Reduced precision downstream. bf16 and fp16 deviations are measured above, but their effect on any downstream metric was not assessed.
License and attribution
The weights are released by Meta AI under the license of the original V-JEPA 2 release (MIT). This repository redistributes them unmodified and adds no additional restrictions.
Credit where it is due:
- Meta AI β the V-JEPA 2 / V-JEPA 2.1 research and the pretrained weights.
- Dev-Jahn β the original
transformers-compatible modeling code and the conversion script, which this repository builds on.
The contribution specific to this repository is the conversion of the L variant, the revised modeling code and video processor described above, and the numerical validation documented here.
Citation
@article{assran2025vjepa2,
title={V-JEPA~2: Self-Supervised Video Models Enable Understanding, Prediction and Planning},
author={Assran, Mahmoud and Bardes, Adrien and Fan, David and Garrido, Quentin and Howes, Russell and
Komeili, Mojtaba and Muckley, Matthew and Rizvi, Ammar and Roberts, Claire and Sinha, Koustuv and Zholus, Artem and
Arnaud, Sergio and Gejji, Abha and Martin, Ada and Robert Hogan, Francois and Dugas, Daniel and
Bojanowski, Piotr and Khalidov, Vasil and Labatut, Patrick and Massa, Francisco and Szafraniec, Marc and
Krishnakumar, Kapil and Li, Yong and Ma, Xiaodong and Chandar, Sarath and Meier, Franziska and LeCun, Yann and
Rabbat, Michael and Ballas, Nicolas},
journal={arXiv preprint arXiv:2506.09985},
year={2025}
}
@article{murlabadia2026vjepa2_1,
title={V-JEPA 2.1: Unlocking Dense Features in Video Self-Supervised Learning},
author={Mur-Labadia, Lorenzo and Muckley, Matthew and Bar, Amir and Assran, Mahmoud and
Sinha, Koustuv and Rabbat, Michael and LeCun, Yann and Ballas, Nicolas and Bardes, Adrien},
journal={arXiv preprint arXiv:2603.14482},
year={2026}
}
Please cite the original V-JEPA 2 work. If the format conversion itself was useful, a link back to Dev-Jahn/vjepa2-hf is appreciated.
- Downloads last month
- 338