Video Classification
Transformers
Safetensors
vjepa21
feature-extraction
video
vjepa
vjepa2
v-jepa-2.1
self-supervised
world-model
custom_code
Instructions to use apiantonio/vjepa2.1-vit-base-384 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use apiantonio/vjepa2.1-vit-base-384 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("video-classification", model="apiantonio/vjepa2.1-vit-base-384", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("apiantonio/vjepa2.1-vit-base-384", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Fix transformers 4.x/5.x compat, implement output_hidden_states/attentions and out_layers, fix hierarchical predictor input, add video processor
d60b444 verified | """V-JEPA 2.1 model implementation for HuggingFace Transformers. | |
| Faithful port of the reference implementation in | |
| ``facebookresearch/vjepa2`` under ``app/vjepa_2_1/models/``. | |
| Key differences from V-JEPA 2: | |
| - Multi-modality: separate patch embedding for images (tubelet_size=1) + modality embeddings | |
| - Hierarchical output: intermediate layer features with per-layer norms | |
| - Interpolatable RoPE: variable input resolution support | |
| - Dense predictor: hierarchical input fusion + context token prediction | |
| Compatible with transformers >= 4.50 (both the 4.x and 5.x attention APIs). | |
| Note on initialisation: the reference `VisionTransformer` and | |
| `VisionTransformerPredictor` call `_rescale_blocks()` after `_init_weights`, | |
| dividing `attn.proj.weight` and `mlp.fc2.weight` of layer *i* by `sqrt(2*(i+1))`. | |
| That is deliberately **not** reproduced here: it only affects randomly | |
| initialised models, and applying it inside `_init_weights` would risk touching | |
| weights that `from_pretrained` has already loaded. Every published checkpoint | |
| loads its weights, so the two agree. | |
| """ | |
| from __future__ import annotations | |
| from collections.abc import Callable | |
| from dataclasses import dataclass | |
| from typing import Optional, Union | |
| import torch | |
| import torch.nn.functional as F | |
| from torch import nn | |
| from transformers.activations import ACT2FN | |
| from transformers.modeling_outputs import ImageClassifierOutput | |
| from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel | |
| from transformers.utils import ModelOutput, logging | |
| from .configuration_vjepa21 import VJEPA21Config | |
| logger = logging.get_logger(__name__) | |
| _warn_once = getattr(logger, "warning_once", logger.warning) | |
| # Keyword arguments the model forward understands. Anything else is reported | |
| # once and dropped, instead of being swallowed by `**kwargs`: a typo such as | |
| # `out_layer=[11]` used to be a silent no-op. | |
| _ENCODER_FORWARD_KWARGS = frozenset( | |
| {"masks", "out_layers", "return_hierarchical", "output_attentions", "output_hidden_states"} | |
| ) | |
| _MODEL_FORWARD_KWARGS = _ENCODER_FORWARD_KWARGS | frozenset( | |
| {"context_mask", "target_mask", "skip_predictor", "mask_index"} | |
| ) | |
| # Injected by the Trainer or by generic HF plumbing; harmless and not worth a warning. | |
| _SILENTLY_IGNORED_KWARGS = frozenset( | |
| {"num_items_in_batch", "return_dict", "return_loss", "interpolate_pos_encoding"} | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Output dataclasses | |
| # --------------------------------------------------------------------------- | |
| class VJEPA21EncoderOutput(ModelOutput): | |
| """Encoder output. | |
| Attributes: | |
| last_hidden_state: Final layer output, normalised with the last | |
| hierarchical norm, `(B, N, hidden_size)`. | |
| hierarchical_hidden_state: Distillation levels concatenated along the | |
| channel axis, `(B, N, n_output_distillation * hidden_size)`. | |
| Only returned when `return_hierarchical=True`. | |
| multilevel_hidden_states: Tuple of per-level normalised features, one | |
| entry per requested `out_layers` index, in the order the layers occur | |
| in the network (not in the order they were requested). Mirrors the | |
| `out_layers` argument of the reference implementation. | |
| hidden_states: Raw (un-normalised) outputs of the embedding layer and of | |
| every transformer layer, when `output_hidden_states=True`. | |
| attentions: Attention probabilities of every layer, when | |
| `output_attentions=True`. | |
| """ | |
| last_hidden_state: Optional[torch.FloatTensor] = None | |
| hierarchical_hidden_state: Optional[torch.FloatTensor] = None | |
| multilevel_hidden_states: Optional[tuple[torch.FloatTensor, ...]] = None | |
| hidden_states: Optional[tuple[torch.FloatTensor, ...]] = None | |
| attentions: Optional[tuple[torch.FloatTensor, ...]] = None | |
| class VJEPA21PredictorOutput(ModelOutput): | |
| """Predictor output. | |
| Attributes: | |
| last_hidden_state: Predicted target tokens `(B, N_target, proj_dim)`. | |
| context_hidden_state: Predicted context tokens, when the model was | |
| configured with `pred_return_all_tokens=True`. | |
| hidden_states: Raw outputs of every predictor layer. | |
| attentions: Attention probabilities of every predictor layer. | |
| """ | |
| last_hidden_state: Optional[torch.FloatTensor] = None | |
| context_hidden_state: Optional[torch.FloatTensor] = None | |
| hidden_states: Optional[tuple[torch.FloatTensor, ...]] = None | |
| attentions: Optional[tuple[torch.FloatTensor, ...]] = None | |
| class VJEPA21ModelOutput(ModelOutput): | |
| """Full model output combining encoder and predictor. | |
| `masked_hidden_state` is the tensor the predictor actually consumed, gathered | |
| at `context_mask`. Its channel width therefore depends on the checkpoint: | |
| `hidden_size` when `n_output_distillation == 1` (ViT-B, ViT-L) and | |
| `n_output_distillation * hidden_size` when the predictor fuses several levels | |
| (ViT-g, ViT-G). | |
| """ | |
| last_hidden_state: Optional[torch.FloatTensor] = None | |
| hierarchical_hidden_state: Optional[torch.FloatTensor] = None | |
| multilevel_hidden_states: Optional[tuple[torch.FloatTensor, ...]] = None | |
| masked_hidden_state: Optional[torch.FloatTensor] = None | |
| predictor_output: Optional[VJEPA21PredictorOutput] = None | |
| hidden_states: Optional[tuple[torch.FloatTensor, ...]] = None | |
| attentions: Optional[tuple[torch.FloatTensor, ...]] = None | |
| # --------------------------------------------------------------------------- | |
| # Utility functions | |
| # --------------------------------------------------------------------------- | |
| def apply_masks(tensor: torch.Tensor, masks: list[torch.Tensor]) -> torch.Tensor: | |
| """Gather tokens at mask indices. | |
| Args: | |
| tensor: `(B, N, D)` tensor. | |
| masks: List of `(B, K)` index tensors. | |
| Returns: | |
| `(len(masks)*B, K, D)` gathered tensor. | |
| """ | |
| parts = [] | |
| for mask in masks: | |
| mask = mask.to(tensor.device) | |
| idx = mask.unsqueeze(-1).expand(-1, -1, tensor.size(-1)) | |
| parts.append(torch.gather(tensor, dim=1, index=idx)) | |
| return torch.cat(parts, dim=0) | |
| def _as_mask_list(masks) -> Optional[list[torch.Tensor]]: | |
| """Accept a single tensor or a list of tensors, as the reference does.""" | |
| if masks is None: | |
| return None | |
| if isinstance(masks, torch.Tensor): | |
| return [masks] | |
| return list(masks) | |
| def normalize_video_layout(pixel_values_videos: torch.Tensor, in_chans: int = 3) -> torch.Tensor: | |
| """Bring any accepted video layout to channels-first `(B, C, T, H, W)`. | |
| Accepted layouts, resolved in this order: | |
| - `(B, C, T, H, W)` channels-first (native) | |
| - `(B, T, C, H, W)` the layout produced by HF video processors | |
| - `(B, T, H, W, C)` channels-last | |
| - `(B, C, H, W)` a batch of single images, promoted to `T = 1` | |
| The channel axis is identified by matching `config.in_chans`. When more than | |
| one axis matches — a 3-frame clip is the realistic case — the channels-first | |
| reading wins and a warning is emitted, because the alternative is a silent | |
| transposition. | |
| """ | |
| x = pixel_values_videos | |
| if x.ndim == 4: | |
| # (B, C, H, W) -> (B, C, 1, H, W) | |
| if x.shape[1] != in_chans: | |
| raise ValueError( | |
| f"Expected a 4D tensor shaped (B, {in_chans}, H, W), got {tuple(x.shape)}." | |
| ) | |
| return x.unsqueeze(2) | |
| if x.ndim != 5: | |
| raise ValueError( | |
| "pixel_values_videos must be a 4D or 5D tensor, got " | |
| f"{x.ndim} dimensions with shape {tuple(x.shape)}." | |
| ) | |
| candidates = [axis for axis in (1, 2, 4) if x.shape[axis] == in_chans] | |
| if len(candidates) > 1: | |
| _warn_once( | |
| "ambiguous video layout: " | |
| f"pixel_values_videos of shape {tuple(x.shape)} has {len(candidates)} candidate " | |
| f"channel axes {candidates} of size in_chans={in_chans}; " | |
| "reading it as (B, C, T, H, W). " | |
| "Pass an unambiguous layout if that is not what you meant." | |
| ) | |
| if x.shape[1] == in_chans: # (B, C, T, H, W) | |
| return x | |
| if x.shape[2] == in_chans: # (B, T, C, H, W) | |
| return x.permute(0, 2, 1, 3, 4) | |
| if x.shape[-1] == in_chans: # (B, T, H, W, C) | |
| return x.permute(0, 4, 1, 2, 3) | |
| raise ValueError( | |
| f"Could not locate a channel axis of size {in_chans} in a tensor of shape " | |
| f"{tuple(x.shape)}. Supported layouts are (B, C, T, H, W), (B, T, C, H, W) " | |
| "and (B, T, H, W, C)." | |
| ) | |
| def drop_path(x: torch.Tensor, drop_prob: float = 0.0, training: bool = False) -> torch.Tensor: | |
| if drop_prob == 0.0 or not training: | |
| return x | |
| keep = 1.0 - drop_prob | |
| shape = (x.shape[0],) + (1,) * (x.ndim - 1) | |
| mask = keep + torch.rand(shape, dtype=x.dtype, device=x.device) | |
| mask.floor_() | |
| return x.div(keep) * mask | |
| class VJEPA21DropPath(nn.Module): | |
| def __init__(self, p: Optional[float] = None): | |
| super().__init__() | |
| self.p = p | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| return drop_path(x, self.p, self.training) | |
| def extra_repr(self) -> str: | |
| return f"p={self.p}" | |
| def rotate_queries_or_keys( | |
| x: torch.Tensor, | |
| pos: torch.Tensor, | |
| n_registers: int = 0, | |
| has_cls_first: bool = False, | |
| ) -> torch.Tensor: | |
| """Apply rotary position embeddings with register/CLS token handling. | |
| Args: | |
| x: `(B, H, N, D)` query or key tensor. | |
| pos: Position ids broadcastable to `(..., N_ctx)`. | |
| n_registers: Number of register tokens at end of sequence (not rotated). | |
| has_cls_first: Whether first token is CLS (not rotated). | |
| """ | |
| B, num_heads, N, D = x.size() | |
| if D % 2 != 0: | |
| raise ValueError(f"RoPE requires an even dimension per slice, got {D}.") | |
| n_cls = 1 if has_cls_first else 0 | |
| start_ctx = n_cls | |
| end_ctx = N - n_registers | |
| x_cls = x[..., :n_cls, :] if n_cls else None | |
| x_ctx = x[..., start_ctx:end_ctx, :] | |
| x_reg = x[..., end_ctx:, :] if n_registers > 0 else None | |
| # Position ids are computed over the context tokens only. If they were built | |
| # over the full sequence, trim them so CLS/registers stay unrotated. | |
| if pos.shape[-1] == N and (n_cls or n_registers): | |
| pos = pos[..., start_ctx:end_ctx] | |
| # RoPE frequencies are computed in float32 and cast back to the input dtype. | |
| # The reference implementation builds them in `x.dtype`, which is equivalent in | |
| # float32 but promotes q/k to float32 when the weights are held in bf16/fp16 — | |
| # the fused attention kernels then reject the mismatch against v. Meta's training | |
| # code never hits this because it runs under `torch.autocast` rather than casting | |
| # the weights. Computing in float32 is also the numerically safer choice. | |
| omega = torch.arange(D // 2, dtype=torch.float32, device=x.device) | |
| omega = omega / (D / 2.0) | |
| omega = 1.0 / (10000.0**omega) | |
| freq = torch.einsum("..., f -> ... f", pos.to(torch.float32), omega) | |
| emb_sin = freq.sin().repeat_interleave(2, dim=-1).to(x.dtype) | |
| emb_cos = freq.cos().repeat_interleave(2, dim=-1).to(x.dtype) | |
| y = x_ctx.unflatten(-1, (-1, 2)) | |
| y1, y2 = y.unbind(dim=-1) | |
| y = torch.stack((-y2, y1), dim=-1).flatten(-2) | |
| out_ctx = x_ctx * emb_cos + y * emb_sin | |
| parts = [] | |
| if x_cls is not None: | |
| parts.append(x_cls) | |
| parts.append(out_ctx) | |
| if x_reg is not None: | |
| parts.append(x_reg) | |
| return torch.cat(parts, dim=-2) | |
| def eager_attention_forward( | |
| module: nn.Module, | |
| query: torch.Tensor, | |
| key: torch.Tensor, | |
| value: torch.Tensor, | |
| attention_mask: Optional[torch.Tensor], | |
| scaling: float, | |
| dropout: float = 0.0, | |
| **kwargs, | |
| ): | |
| attn_weights = torch.matmul(query, key.transpose(-1, -2)) * scaling | |
| attn_weights = F.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query.dtype) | |
| attn_weights = F.dropout(attn_weights, p=dropout, training=module.training) | |
| attn_output = torch.matmul(attn_weights, value).transpose(1, 2).contiguous() | |
| return attn_output, attn_weights | |
| def resolve_attention_interface(config, output_attentions: bool = False) -> Callable: | |
| """Return the attention kernel for ``config._attn_implementation``. | |
| Works on transformers 4.x, where ``ALL_ATTENTION_FUNCTIONS`` is a plain | |
| mapping, and on 5.x, where it exposes ``get_interface``. When attention | |
| probabilities are requested we fall back to the eager kernel, since the | |
| fused kernels do not materialise them. | |
| """ | |
| if output_attentions: | |
| return eager_attention_forward | |
| impl = getattr(config, "_attn_implementation", None) or "eager" | |
| if impl == "eager": | |
| return eager_attention_forward | |
| getter = getattr(ALL_ATTENTION_FUNCTIONS, "get_interface", None) | |
| if getter is not None: # transformers >= 5 | |
| return getter(impl, eager_attention_forward) | |
| return ALL_ATTENTION_FUNCTIONS.get(impl, eager_attention_forward) # transformers 4.x | |
| # --------------------------------------------------------------------------- | |
| # Patch Embeddings | |
| # --------------------------------------------------------------------------- | |
| class VJEPA21PatchEmbeddings3D(nn.Module): | |
| """3D patch embedding via Conv3d.""" | |
| def __init__(self, config: VJEPA21Config, tubelet_size: Optional[int] = None): | |
| super().__init__() | |
| ts = tubelet_size if tubelet_size is not None else config.tubelet_size | |
| ps = config.patch_size | |
| self.proj = nn.Conv3d( | |
| config.in_chans, | |
| config.hidden_size, | |
| kernel_size=(ts, ps, ps), | |
| stride=(ts, ps, ps), | |
| ) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| # x: (B, C, T, H, W) | |
| return self.proj(x).flatten(2).transpose(1, 2) | |
| class VJEPA21Embeddings(nn.Module): | |
| """Patch embeddings with modality-aware processing.""" | |
| def __init__(self, config: VJEPA21Config): | |
| super().__init__() | |
| self.config = config | |
| # Video patch embedding (tubelet_size from config) | |
| self.patch_embeddings = VJEPA21PatchEmbeddings3D(config) | |
| # Image patch embedding (tubelet_size=1) if img_temporal_dim_size is set | |
| self.patch_embeddings_img = None | |
| if config.img_temporal_dim_size is not None: | |
| self.patch_embeddings_img = VJEPA21PatchEmbeddings3D(config, tubelet_size=1) | |
| # Modality embeddings | |
| self.img_mod_embed = None | |
| self.video_mod_embed = None | |
| if config.modality_embedding: | |
| self.img_mod_embed = nn.Parameter(torch.zeros(1, 1, config.hidden_size)) | |
| self.video_mod_embed = nn.Parameter(torch.zeros(1, 1, config.hidden_size)) | |
| def forward(self, pixel_values_videos: torch.Tensor) -> tuple[torch.Tensor, str]: | |
| """ | |
| Args: | |
| pixel_values_videos: `(B, C, T, H, W)`, already layout-normalised. | |
| Returns: | |
| embeddings: `(B, N, hidden_size)`. | |
| mode: "img" or "video". | |
| """ | |
| target_dtype = self.patch_embeddings.proj.weight.dtype | |
| pixel_values_videos = pixel_values_videos.to(dtype=target_dtype) | |
| T = pixel_values_videos.shape[2] | |
| is_image = ( | |
| self.config.img_temporal_dim_size is not None | |
| and T == self.config.img_temporal_dim_size | |
| ) | |
| if is_image and self.patch_embeddings_img is not None: | |
| embeddings = self.patch_embeddings_img(pixel_values_videos) | |
| mode = "img" | |
| else: | |
| # Ensure at least tubelet_size frames | |
| if T < self.config.tubelet_size: | |
| pixel_values_videos = pixel_values_videos.repeat( | |
| 1, 1, self.config.tubelet_size, 1, 1 | |
| ) | |
| embeddings = self.patch_embeddings(pixel_values_videos) | |
| mode = "video" | |
| if self.img_mod_embed is not None: | |
| if mode == "img": | |
| embeddings = embeddings + self.img_mod_embed | |
| else: | |
| embeddings = embeddings + self.video_mod_embed | |
| return embeddings, mode | |
| # --------------------------------------------------------------------------- | |
| # Attention | |
| # --------------------------------------------------------------------------- | |
| class VJEPA21RopeAttention(nn.Module): | |
| """RoPE-based multi-head attention with interpolation and register support.""" | |
| def __init__(self, config: VJEPA21Config, hidden_size: int, num_attention_heads: int): | |
| super().__init__() | |
| self.config = config | |
| self.hidden_size = hidden_size | |
| self.num_attention_heads = num_attention_heads | |
| self.attention_head_size = hidden_size // num_attention_heads | |
| self.all_head_size = num_attention_heads * self.attention_head_size | |
| self.query = nn.Linear(hidden_size, self.all_head_size, bias=config.qkv_bias) | |
| self.key = nn.Linear(hidden_size, self.all_head_size, bias=config.qkv_bias) | |
| self.value = nn.Linear(hidden_size, self.all_head_size, bias=config.qkv_bias) | |
| self.proj = nn.Linear(hidden_size, hidden_size) | |
| self.dropout_prob = config.attention_probs_dropout_prob | |
| self.scaling = self.attention_head_size**-0.5 | |
| self.is_causal = False | |
| # RoPE dimension split: depth, height, width | |
| self.d_dim = int(2 * ((self.attention_head_size // 3) // 2)) | |
| self.h_dim = int(2 * ((self.attention_head_size // 3) // 2)) | |
| self.w_dim = int(2 * ((self.attention_head_size // 3) // 2)) | |
| self.grid_size = config.crop_size // config.patch_size | |
| self.n_registers = config.n_registers | |
| self.has_cls_first = config.has_cls_first | |
| self.interpolate_rope = config.interpolate_rope | |
| self.pretrained_grid_size = config.pretrained_grid_size | |
| def _separate_positions( | |
| self, | |
| ids: torch.Tensor, | |
| H_patches: Optional[int] = None, | |
| W_patches: Optional[int] = None, | |
| ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: | |
| """Decompose flat token ids into (depth, height, width) components.""" | |
| hp = H_patches if H_patches is not None else self.grid_size | |
| wp = W_patches if W_patches is not None else self.grid_size | |
| tokens_per_frame = hp * wp | |
| frame_ids = ids // tokens_per_frame | |
| remainder = ids - tokens_per_frame * frame_ids | |
| height_ids = remainder // wp | |
| width_ids = remainder - wp * height_ids | |
| return frame_ids.float(), height_ids.float(), width_ids.float() | |
| def forward( | |
| self, | |
| hidden_states: torch.Tensor, | |
| position_mask: Optional[torch.Tensor] = None, | |
| T: Optional[int] = None, | |
| H_patches: Optional[int] = None, | |
| W_patches: Optional[int] = None, | |
| output_attentions: bool = False, | |
| ) -> tuple[torch.Tensor, Optional[torch.Tensor]]: | |
| B, N, C = hidden_states.shape | |
| q = ( | |
| self.query(hidden_states) | |
| .view(B, N, self.num_attention_heads, self.attention_head_size) | |
| .transpose(1, 2) | |
| ) | |
| k = ( | |
| self.key(hidden_states) | |
| .view(B, N, self.num_attention_heads, self.attention_head_size) | |
| .transpose(1, 2) | |
| ) | |
| v = ( | |
| self.value(hidden_states) | |
| .view(B, N, self.num_attention_heads, self.attention_head_size) | |
| .transpose(1, 2) | |
| ) | |
| # Compute position ids | |
| if position_mask is not None: | |
| ids = position_mask.unsqueeze(1).repeat(1, self.num_attention_heads, 1) | |
| else: | |
| ids = torch.arange(N, device=hidden_states.device) | |
| d_mask, h_mask, w_mask = self._separate_positions(ids, H_patches, W_patches) | |
| # Interpolate RoPE for variable resolution. | |
| # Mirrors app/vjepa_2_1/models/utils/modules.py (`interpolate_rope`). | |
| if self.interpolate_rope: | |
| hp = H_patches if H_patches is not None else self.grid_size | |
| wp = W_patches if W_patches is not None else self.grid_size | |
| h_mask = h_mask * (self.pretrained_grid_size - 1) / max(hp - 1, 1) | |
| w_mask = w_mask * (self.pretrained_grid_size - 1) / max(wp - 1, 1) | |
| # Apply RoPE to each dimension slice | |
| s = 0 | |
| qd = rotate_queries_or_keys( | |
| q[..., s : s + self.d_dim], d_mask, self.n_registers, self.has_cls_first | |
| ) | |
| kd = rotate_queries_or_keys( | |
| k[..., s : s + self.d_dim], d_mask, self.n_registers, self.has_cls_first | |
| ) | |
| s += self.d_dim | |
| qh = rotate_queries_or_keys( | |
| q[..., s : s + self.h_dim], h_mask, self.n_registers, self.has_cls_first | |
| ) | |
| kh = rotate_queries_or_keys( | |
| k[..., s : s + self.h_dim], h_mask, self.n_registers, self.has_cls_first | |
| ) | |
| s += self.h_dim | |
| qw = rotate_queries_or_keys( | |
| q[..., s : s + self.w_dim], w_mask, self.n_registers, self.has_cls_first | |
| ) | |
| kw = rotate_queries_or_keys( | |
| k[..., s : s + self.w_dim], w_mask, self.n_registers, self.has_cls_first | |
| ) | |
| s += self.w_dim | |
| if s < self.attention_head_size: | |
| q = torch.cat([qd, qh, qw, q[..., s:]], dim=-1) | |
| k = torch.cat([kd, kh, kw, k[..., s:]], dim=-1) | |
| else: | |
| q = torch.cat([qd, qh, qw], dim=-1) | |
| k = torch.cat([kd, kh, kw], dim=-1) | |
| attention_interface = resolve_attention_interface(self.config, output_attentions) | |
| context_layer, attn_weights = attention_interface( | |
| self, | |
| q, | |
| k, | |
| v, | |
| None, | |
| is_causal=self.is_causal, | |
| scaling=self.scaling, | |
| dropout=0.0 if not self.training else self.dropout_prob, | |
| ) | |
| output = self.proj(context_layer.reshape(B, N, self.all_head_size)) | |
| return output, attn_weights | |
| # --------------------------------------------------------------------------- | |
| # MLP | |
| # --------------------------------------------------------------------------- | |
| class VJEPA21MLP(nn.Module): | |
| """Standard GELU MLP.""" | |
| def __init__(self, config: VJEPA21Config, hidden_size: int, mlp_ratio: float = 4.0): | |
| super().__init__() | |
| hidden_features = int(hidden_size * mlp_ratio) | |
| self.fc1 = nn.Linear(hidden_size, hidden_features) | |
| self.act = ACT2FN[config.hidden_act] | |
| self.fc2 = nn.Linear(hidden_features, hidden_size) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| return self.fc2(self.act(self.fc1(x))) | |
| class VJEPA21SwiGLUMLP(nn.Module): | |
| """SwiGLU FFN as used in V-JEPA 2.1.""" | |
| def __init__(self, config: VJEPA21Config, hidden_size: int, mlp_ratio: float = 4.0): | |
| super().__init__() | |
| hidden_features = int(hidden_size * mlp_ratio) | |
| if config.wide_silu: | |
| swiglu_hidden = int(2 * hidden_features / 3) | |
| align_as = 8 | |
| swiglu_hidden = (swiglu_hidden + align_as - 1) // align_as * align_as | |
| else: | |
| swiglu_hidden = hidden_features | |
| self.fc1 = nn.Linear(hidden_size, swiglu_hidden) | |
| self.fc2 = nn.Linear(hidden_size, swiglu_hidden) | |
| self.fc3 = nn.Linear(swiglu_hidden, hidden_size) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| return self.fc3(F.silu(self.fc1(x)) * self.fc2(x)) | |
| def _make_mlp(config: VJEPA21Config, hidden_size: int, mlp_ratio: float) -> nn.Module: | |
| if config.hidden_act == "silu": | |
| return VJEPA21SwiGLUMLP(config, hidden_size, mlp_ratio) | |
| return VJEPA21MLP(config, hidden_size, mlp_ratio) | |
| # --------------------------------------------------------------------------- | |
| # Transformer Layer | |
| # --------------------------------------------------------------------------- | |
| class VJEPA21Layer(nn.Module): | |
| """Single transformer block: LN -> Attention -> DropPath + Residual -> LN -> MLP -> DropPath + Residual.""" | |
| def __init__( | |
| self, | |
| config: VJEPA21Config, | |
| hidden_size: int, | |
| num_attention_heads: int, | |
| mlp_ratio: float, | |
| drop_path_rate: float = 0.0, | |
| ): | |
| super().__init__() | |
| self.norm1 = nn.LayerNorm(hidden_size, eps=config.layer_norm_eps) | |
| self.attention = VJEPA21RopeAttention(config, hidden_size, num_attention_heads) | |
| self.drop_path = ( | |
| VJEPA21DropPath(drop_path_rate) if drop_path_rate > 0.0 else nn.Identity() | |
| ) | |
| self.norm2 = nn.LayerNorm(hidden_size, eps=config.layer_norm_eps) | |
| self.mlp = _make_mlp(config, hidden_size, mlp_ratio) | |
| def forward( | |
| self, | |
| hidden_states: torch.Tensor, | |
| position_mask: Optional[torch.Tensor] = None, | |
| T: Optional[int] = None, | |
| H_patches: Optional[int] = None, | |
| W_patches: Optional[int] = None, | |
| output_attentions: bool = False, | |
| ) -> tuple[torch.Tensor, Optional[torch.Tensor]]: | |
| residual = hidden_states | |
| h = self.norm1(hidden_states) | |
| attn_out, attn_weights = self.attention( | |
| h, position_mask, T, H_patches, W_patches, output_attentions | |
| ) | |
| hidden_states = residual + self.drop_path(attn_out) | |
| residual = hidden_states | |
| hidden_states = residual + self.drop_path(self.mlp(self.norm2(hidden_states))) | |
| return hidden_states, attn_weights | |
| # --------------------------------------------------------------------------- | |
| # Encoder | |
| # --------------------------------------------------------------------------- | |
| class VJEPA21Encoder(nn.Module): | |
| """V-JEPA 2.1 encoder with hierarchical and multi-level output support.""" | |
| def __init__(self, config: VJEPA21Config): | |
| super().__init__() | |
| self.config = config | |
| self.embeddings = VJEPA21Embeddings(config) | |
| dpr = [ | |
| config.drop_path_rate * i / max(config.num_hidden_layers - 1, 1) | |
| for i in range(config.num_hidden_layers) | |
| ] | |
| self.layer = nn.ModuleList( | |
| [ | |
| VJEPA21Layer( | |
| config, | |
| hidden_size=config.hidden_size, | |
| num_attention_heads=config.num_attention_heads, | |
| mlp_ratio=config.mlp_ratio, | |
| drop_path_rate=dpr[i], | |
| ) | |
| for i in range(config.num_hidden_layers) | |
| ] | |
| ) | |
| # Per-layer norms for hierarchical outputs. One norm per hierarchical | |
| # level, exactly as in the reference VisionTransformer. | |
| hier_layers = config.encoder_hierarchical_layers | |
| self.norms_block = nn.ModuleList( | |
| [nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps) for _ in hier_layers] | |
| ) | |
| self._hier_layers = hier_layers | |
| self._distill_layers = config.encoder_distillation_layers | |
| self.gradient_checkpointing = False | |
| def forward( | |
| self, | |
| pixel_values_videos: torch.Tensor, | |
| masks: Optional[Union[torch.Tensor, list[torch.Tensor]]] = None, | |
| return_hierarchical: bool = False, | |
| out_layers: Optional[list[int]] = None, | |
| output_attentions: bool = False, | |
| output_hidden_states: bool = False, | |
| **kwargs, | |
| ) -> VJEPA21EncoderOutput: | |
| """ | |
| Args: | |
| masks: Optional list of `(B, K)` index tensors. When given, the patch | |
| tokens are gathered at those indices *before* the transformer | |
| layers, so attention only ever sees the context tokens and RoPE | |
| receives their true positions. This is the JEPA training-time | |
| forward (`z = encoder(clips, masks_enc)` in the reference); the | |
| default `None` runs the full sequence, which is what feature | |
| extraction wants. | |
| """ | |
| unexpected = set(kwargs) - _SILENTLY_IGNORED_KWARGS | |
| if unexpected: | |
| _warn_once( | |
| f"VJEPA21Encoder.forward received unexpected keyword arguments " | |
| f"{sorted(unexpected)}; they are ignored. Accepted arguments: " | |
| f"{sorted(_ENCODER_FORWARD_KWARGS)}." | |
| ) | |
| pixel_values_videos = normalize_video_layout( | |
| pixel_values_videos, self.config.in_chans | |
| ) | |
| embeddings, _ = self.embeddings(pixel_values_videos) | |
| B, C, T_raw, H, W = pixel_values_videos.shape | |
| is_image = ( | |
| self.config.img_temporal_dim_size is not None | |
| and T_raw == self.config.img_temporal_dim_size | |
| ) | |
| T_patches = T_raw if is_image else max(T_raw // self.config.tubelet_size, 1) | |
| H_patches = H // self.config.patch_size | |
| W_patches = W // self.config.patch_size | |
| if out_layers is not None: | |
| unknown = [i for i in out_layers if i not in self._hier_layers] | |
| if unknown: | |
| raise ValueError( | |
| f"out_layers={out_layers} contains indices {unknown} that are not " | |
| f"hierarchical layers of this model. Valid indices: {self._hier_layers}." | |
| ) | |
| # Masked (JEPA) forward: drop tokens before the layers and carry their | |
| # original indices as RoPE positions, as the reference does. | |
| masks = _as_mask_list(masks) | |
| position_mask = None | |
| if masks is not None: | |
| n_tokens = embeddings.shape[1] | |
| for m in masks: | |
| if m.dim() != 2 or m.shape[0] != embeddings.shape[0]: | |
| raise ValueError( | |
| f"each mask must be a (B, K) index tensor with B={embeddings.shape[0]}, " | |
| f"got {tuple(m.shape)}." | |
| ) | |
| if int(m.max()) >= n_tokens: | |
| raise ValueError( | |
| f"mask index {int(m.max())} is out of range for a sequence of " | |
| f"{n_tokens} tokens." | |
| ) | |
| embeddings = apply_masks(embeddings, masks) | |
| position_mask = torch.cat([m.to(embeddings.device) for m in masks], dim=0) | |
| hidden_states = embeddings | |
| hier_outputs: list[torch.Tensor] = [] | |
| multilevel_outputs: list[torch.Tensor] = [] | |
| all_hidden_states: Optional[tuple[torch.Tensor, ...]] = () if output_hidden_states else None | |
| all_attentions: Optional[tuple[torch.Tensor, ...]] = () if output_attentions else None | |
| for i, layer_module in enumerate(self.layer): | |
| if output_hidden_states: | |
| all_hidden_states = all_hidden_states + (hidden_states,) | |
| if self.gradient_checkpointing and self.training: | |
| layer_out = self._gradient_checkpointing_func( | |
| layer_module.__call__, | |
| hidden_states, | |
| position_mask, | |
| T_patches, | |
| H_patches, | |
| W_patches, | |
| output_attentions, | |
| ) | |
| else: | |
| layer_out = layer_module( | |
| hidden_states, | |
| position_mask=position_mask, | |
| T=T_patches, | |
| H_patches=H_patches, | |
| W_patches=W_patches, | |
| output_attentions=output_attentions, | |
| ) | |
| hidden_states = layer_out[0] | |
| if output_attentions: | |
| all_attentions = all_attentions + (layer_out[1],) | |
| if out_layers is not None and i in out_layers: | |
| idx = self._hier_layers.index(i) | |
| multilevel_outputs.append(self.norms_block[idx](hidden_states)) | |
| if i in self._distill_layers: | |
| idx = self._hier_layers.index(i) | |
| hier_outputs.append(self.norms_block[idx](hidden_states)) | |
| if output_hidden_states: | |
| all_hidden_states = all_hidden_states + (hidden_states,) | |
| # Reference implementation: `x = self.norms_block[-1](x)` on the final layer. | |
| last_hidden_state = self.norms_block[-1](hidden_states) | |
| hierarchical_hidden_state = None | |
| if return_hierarchical and hier_outputs: | |
| hierarchical_hidden_state = torch.cat(hier_outputs, dim=2) | |
| return VJEPA21EncoderOutput( | |
| last_hidden_state=last_hidden_state, | |
| hierarchical_hidden_state=hierarchical_hidden_state, | |
| multilevel_hidden_states=tuple(multilevel_outputs) if out_layers else None, | |
| hidden_states=all_hidden_states, | |
| attentions=all_attentions, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Predictor | |
| # --------------------------------------------------------------------------- | |
| class VJEPA21PredictorEmbeddings(nn.Module): | |
| """Predictor embeddings with hierarchical input fusion.""" | |
| def __init__(self, config: VJEPA21Config): | |
| super().__init__() | |
| self.config = config | |
| n_hier = len(config.predictor_hierarchical_layers) | |
| if n_hier <= 1: | |
| self.predictor_embed = nn.Linear(config.hidden_size, config.pred_hidden_size) | |
| else: | |
| act = nn.SiLU if config.hidden_act == "silu" else nn.GELU | |
| self.predictor_embed = nn.Sequential( | |
| nn.Linear(config.hidden_size * n_hier, config.hidden_size), | |
| act(), | |
| nn.Linear(config.hidden_size, config.pred_hidden_size), | |
| ) | |
| self.num_mask_tokens = config.pred_num_mask_tokens | |
| self.mask_tokens = nn.ParameterList( | |
| [ | |
| nn.Parameter(torch.zeros(1, 1, config.pred_hidden_size)) | |
| for _ in range(self.num_mask_tokens) | |
| ] | |
| ) | |
| self.img_mod_embed = None | |
| self.video_mod_embed = None | |
| if config.img_temporal_dim_size is not None and config.modality_embedding: | |
| self.img_mod_embed = nn.Parameter(torch.zeros(1, 1, config.pred_hidden_size)) | |
| self.video_mod_embed = nn.Parameter(torch.zeros(1, 1, config.pred_hidden_size)) | |
| def forward( | |
| self, | |
| hidden_states: torch.Tensor, | |
| context_mask: list[torch.Tensor], | |
| target_mask: list[torch.Tensor], | |
| mask_index: int = 1, | |
| ) -> tuple[torch.Tensor, torch.Tensor]: | |
| # `hidden_states` already carries len(context_mask) * B rows. | |
| batch_size = hidden_states.size(0) // len(context_mask) | |
| context = self.predictor_embed(hidden_states) | |
| mask_index = mask_index % self.num_mask_tokens | |
| pred_tokens = self.mask_tokens[mask_index].repeat( | |
| batch_size, self._max_patches(target_mask), 1 | |
| ) | |
| pred_tokens = apply_masks(pred_tokens, target_mask) | |
| context = context.repeat(len(context_mask), 1, 1) | |
| embeddings = torch.cat([context, pred_tokens], dim=1) | |
| cm = torch.cat(context_mask, dim=0) | |
| tm = torch.cat(target_mask, dim=0) | |
| masks = torch.cat([cm, tm], dim=1) | |
| return embeddings, masks | |
| def _max_patches(masks: list[torch.Tensor]) -> int: | |
| return int(max(m.max().item() for m in masks)) + 1 | |
| class VJEPA21Predictor(nn.Module): | |
| """V-JEPA 2.1 predictor with hierarchical input and context projection.""" | |
| def __init__(self, config: VJEPA21Config): | |
| super().__init__() | |
| self.config = config | |
| self.embeddings = VJEPA21PredictorEmbeddings(config) | |
| dpr = [ | |
| config.drop_path_rate * i / max(config.pred_num_hidden_layers - 1, 1) | |
| for i in range(config.pred_num_hidden_layers) | |
| ] | |
| self.layer = nn.ModuleList( | |
| [ | |
| VJEPA21Layer( | |
| config, | |
| hidden_size=config.pred_hidden_size, | |
| num_attention_heads=config.pred_num_attention_heads, | |
| mlp_ratio=config.pred_mlp_ratio, | |
| drop_path_rate=dpr[i], | |
| ) | |
| for i in range(config.pred_num_hidden_layers) | |
| ] | |
| ) | |
| self.layernorm = nn.LayerNorm(config.pred_hidden_size, eps=config.layer_norm_eps) | |
| n_hier = len(config.predictor_hierarchical_layers) | |
| if config.pred_teacher_embed_dim is not None: | |
| out_dim = config.pred_teacher_embed_dim // n_hier | |
| else: | |
| out_dim = config.hidden_size | |
| proj_out_dim = n_hier * out_dim | |
| self.proj = nn.Linear(config.pred_hidden_size, proj_out_dim) | |
| self.proj_context = None | |
| if config.pred_return_all_tokens: | |
| self.proj_context = nn.Linear(config.pred_hidden_size, proj_out_dim) | |
| self.gradient_checkpointing = False | |
| def forward( | |
| self, | |
| encoder_hidden_states: torch.Tensor, | |
| context_mask: Union[torch.Tensor, list[torch.Tensor]], | |
| target_mask: Union[torch.Tensor, list[torch.Tensor]], | |
| mode: str = "video", | |
| mask_index: int = 1, | |
| context_is_masked: bool = False, | |
| output_attentions: bool = False, | |
| output_hidden_states: bool = False, | |
| **kwargs, | |
| ) -> VJEPA21PredictorOutput: | |
| """ | |
| Args: | |
| encoder_hidden_states: Encoder output. By default this is the *full* | |
| token sequence and the predictor gathers the context itself at | |
| `context_mask`. Set `context_is_masked=True` when passing an | |
| encoder output that was already produced with | |
| `encoder(..., masks=context_mask)`, which is the reference | |
| convention. | |
| mask_index: Which learnable mask token to inject. The reference uses | |
| the index of the sequence-length group (`mask_index=i` in | |
| `PredictorMultiSeqWrapper`); its default is 1. | |
| mode: "video" or "img" ("image" is accepted as an alias of "img", | |
| since the reference spells it that way). | |
| """ | |
| if kwargs: | |
| _warn_once( | |
| f"VJEPA21Predictor.forward received unexpected keyword arguments " | |
| f"{sorted(kwargs)}; they are ignored." | |
| ) | |
| context_mask = _as_mask_list(context_mask) | |
| target_mask = _as_mask_list(target_mask) | |
| if context_mask is None or target_mask is None: | |
| raise ValueError("the predictor requires both context_mask and target_mask") | |
| if len(context_mask) != len(target_mask): | |
| raise ValueError( | |
| "context_mask and target_mask must have the same length, got " | |
| f"{len(context_mask)} and {len(target_mask)}." | |
| ) | |
| if len(context_mask) > 1: | |
| raise NotImplementedError( | |
| "The predictor currently supports a single (context_mask, target_mask) " | |
| "pair. Call it once per mask pair instead." | |
| ) | |
| if context_is_masked: | |
| masked_states = encoder_hidden_states | |
| if masked_states.shape[1] != context_mask[0].shape[1]: | |
| raise ValueError( | |
| f"context_is_masked=True but the encoder output has " | |
| f"{masked_states.shape[1]} tokens while context_mask has " | |
| f"{context_mask[0].shape[1]}." | |
| ) | |
| else: | |
| masked_states = apply_masks(encoder_hidden_states, context_mask) | |
| _, N_ctxt, _ = masked_states.shape | |
| hidden_states, position_masks = self.embeddings( | |
| masked_states, context_mask, target_mask, mask_index=mask_index | |
| ) | |
| # Sort tokens by position so RoPE sees monotonically increasing ids | |
| argsort = torch.argsort(position_masks, dim=1) | |
| idx_expand = argsort.unsqueeze(-1).expand(-1, -1, hidden_states.size(-1)) | |
| hidden_states = torch.gather(hidden_states, 1, idx_expand.to(hidden_states.device)) | |
| position_masks = torch.gather(position_masks, 1, argsort.to(position_masks.device)) | |
| if self.embeddings.img_mod_embed is not None: | |
| if mode in ("img", "image"): | |
| hidden_states = hidden_states + self.embeddings.img_mod_embed | |
| else: | |
| hidden_states = hidden_states + self.embeddings.video_mod_embed | |
| all_hidden_states: Optional[tuple] = () if output_hidden_states else None | |
| all_attentions: Optional[tuple] = () if output_attentions else None | |
| for layer_module in self.layer: | |
| if output_hidden_states: | |
| all_hidden_states = all_hidden_states + (hidden_states,) | |
| if self.gradient_checkpointing and self.training: | |
| layer_out = self._gradient_checkpointing_func( | |
| layer_module.__call__, | |
| hidden_states, | |
| position_masks, | |
| None, | |
| None, | |
| None, | |
| output_attentions, | |
| ) | |
| else: | |
| layer_out = layer_module( | |
| hidden_states, | |
| position_mask=position_masks, | |
| output_attentions=output_attentions, | |
| ) | |
| hidden_states = layer_out[0] | |
| if output_attentions: | |
| all_attentions = all_attentions + (layer_out[1],) | |
| hidden_states = self.layernorm(hidden_states) | |
| if output_hidden_states: | |
| all_hidden_states = all_hidden_states + (hidden_states,) | |
| # Unsort | |
| reverse = torch.argsort(argsort, dim=1) | |
| rev_expand = reverse.unsqueeze(-1).expand(-1, -1, hidden_states.size(-1)) | |
| hidden_states = torch.gather(hidden_states, 1, rev_expand.to(hidden_states.device)) | |
| pred = self.proj(hidden_states[:, N_ctxt:]) | |
| ctx = None | |
| if self.config.pred_return_all_tokens: | |
| ctx = self.proj_context(hidden_states[:, :N_ctxt]) | |
| return VJEPA21PredictorOutput( | |
| last_hidden_state=pred, | |
| context_hidden_state=ctx, | |
| hidden_states=all_hidden_states, | |
| attentions=all_attentions, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Attentive Pooler (for downstream tasks) | |
| # --------------------------------------------------------------------------- | |
| class VJEPA21PoolerSelfAttention(nn.Module): | |
| def __init__(self, config: VJEPA21Config): | |
| super().__init__() | |
| self.config = config | |
| self.embed_dim = config.hidden_size | |
| self.num_heads = config.num_pooler_heads | |
| self.head_dim = self.embed_dim // self.num_heads | |
| self.scale = self.head_dim**-0.5 | |
| self.dropout = config.attention_probs_dropout_prob | |
| self.is_causal = False | |
| self.q_proj = nn.Linear(self.embed_dim, self.embed_dim) | |
| self.k_proj = nn.Linear(self.embed_dim, self.embed_dim) | |
| self.v_proj = nn.Linear(self.embed_dim, self.embed_dim) | |
| self.out_proj = nn.Linear(self.embed_dim, self.embed_dim) | |
| def forward( | |
| self, hidden_states: torch.Tensor, output_attentions: bool = False | |
| ) -> tuple[torch.Tensor, Optional[torch.Tensor]]: | |
| B, N, C = hidden_states.shape | |
| q = self.q_proj(hidden_states).view(B, N, self.num_heads, self.head_dim).transpose(1, 2) | |
| k = self.k_proj(hidden_states).view(B, N, self.num_heads, self.head_dim).transpose(1, 2) | |
| v = self.v_proj(hidden_states).view(B, N, self.num_heads, self.head_dim).transpose(1, 2) | |
| attention_interface = resolve_attention_interface(self.config, output_attentions) | |
| attn_output, attn_weights = attention_interface( | |
| self, | |
| q, | |
| k, | |
| v, | |
| None, | |
| is_causal=False, | |
| scaling=self.scale, | |
| dropout=0.0 if not self.training else self.dropout, | |
| ) | |
| return self.out_proj(attn_output.reshape(B, N, C)), attn_weights | |
| class VJEPA21PoolerCrossAttention(nn.Module): | |
| def __init__(self, config: VJEPA21Config): | |
| super().__init__() | |
| self.config = config | |
| self.embed_dim = config.hidden_size | |
| self.num_heads = config.num_pooler_heads | |
| self.head_dim = self.embed_dim // self.num_heads | |
| self.scale = self.head_dim**-0.5 | |
| self.dropout = config.attention_probs_dropout_prob | |
| self.is_causal = False | |
| self.q_proj = nn.Linear(self.embed_dim, self.embed_dim) | |
| self.k_proj = nn.Linear(self.embed_dim, self.embed_dim) | |
| self.v_proj = nn.Linear(self.embed_dim, self.embed_dim) | |
| def forward( | |
| self, queries: torch.Tensor, kv: torch.Tensor, output_attentions: bool = False | |
| ) -> tuple[torch.Tensor, Optional[torch.Tensor]]: | |
| B, Nq, C = queries.shape | |
| Nkv = kv.shape[1] | |
| q = self.q_proj(queries).view(B, Nq, self.num_heads, self.head_dim).transpose(1, 2) | |
| k = self.k_proj(kv).view(B, Nkv, self.num_heads, self.head_dim).transpose(1, 2) | |
| v = self.v_proj(kv).view(B, Nkv, self.num_heads, self.head_dim).transpose(1, 2) | |
| attention_interface = resolve_attention_interface(self.config, output_attentions) | |
| attn_output, attn_weights = attention_interface( | |
| self, | |
| q, | |
| k, | |
| v, | |
| None, | |
| is_causal=False, | |
| scaling=self.scale, | |
| dropout=0.0 if not self.training else self.dropout, | |
| ) | |
| return attn_output.reshape(B, Nq, C), attn_weights | |
| class VJEPA21PoolerSelfAttentionLayer(nn.Module): | |
| def __init__(self, config: VJEPA21Config): | |
| super().__init__() | |
| self.layer_norm1 = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps) | |
| self.self_attn = VJEPA21PoolerSelfAttention(config) | |
| self.layer_norm2 = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps) | |
| self.mlp = VJEPA21MLP(config, hidden_size=config.hidden_size) | |
| def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: | |
| residual = hidden_states | |
| hidden_states, _ = self.self_attn(self.layer_norm1(hidden_states)) | |
| hidden_states = residual + hidden_states | |
| residual = hidden_states | |
| hidden_states = residual + self.mlp(self.layer_norm2(hidden_states)) | |
| return hidden_states | |
| class VJEPA21PoolerCrossAttentionLayer(nn.Module): | |
| def __init__(self, config: VJEPA21Config): | |
| super().__init__() | |
| self.layer_norm1 = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps) | |
| self.cross_attn = VJEPA21PoolerCrossAttention(config) | |
| self.layer_norm2 = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps) | |
| self.mlp = VJEPA21MLP(config, hidden_size=config.hidden_size) | |
| def forward(self, queries: torch.Tensor, kv: torch.Tensor) -> torch.Tensor: | |
| residual = queries | |
| hidden, _ = self.cross_attn(queries, self.layer_norm1(kv)) | |
| hidden = residual + hidden | |
| residual = hidden | |
| hidden = residual + self.mlp(self.layer_norm2(hidden)) | |
| return hidden | |
| class VJEPA21AttentivePooler(nn.Module): | |
| """Attentive pooler matching `AttentivePooler(depth=num_pooler_layers + 1)`. | |
| The reference frozen-probe configs under `configs/eval_2_1/` use | |
| `num_probe_blocks: 4` and `num_heads: 16`, i.e. three self-attention blocks | |
| followed by one cross-attention block, with 16 heads. Those are the defaults | |
| of `num_pooler_layers` and `num_pooler_heads`. | |
| """ | |
| def __init__(self, config: VJEPA21Config): | |
| super().__init__() | |
| self.query_tokens = nn.Parameter(torch.zeros(1, 1, config.hidden_size)) | |
| self.cross_attention_layer = VJEPA21PoolerCrossAttentionLayer(config) | |
| self.self_attention_layers = nn.ModuleList( | |
| [VJEPA21PoolerSelfAttentionLayer(config) for _ in range(config.num_pooler_layers)] | |
| ) | |
| def forward(self, hidden_state: torch.Tensor) -> torch.Tensor: | |
| for layer in self.self_attention_layers: | |
| hidden_state = layer(hidden_state) | |
| queries = self.query_tokens.expand(hidden_state.shape[0], -1, -1) | |
| return self.cross_attention_layer(queries, hidden_state).squeeze(1) | |
| # --------------------------------------------------------------------------- | |
| # PreTrainedModel base | |
| # --------------------------------------------------------------------------- | |
| class VJEPA21PreTrainedModel(PreTrainedModel): | |
| config_class = VJEPA21Config | |
| base_model_prefix = "vjepa21" | |
| main_input_name = "pixel_values_videos" | |
| supports_gradient_checkpointing = True | |
| _no_split_modules = ["VJEPA21Layer"] | |
| _supports_sdpa = True | |
| _supports_flash_attn = True | |
| _supports_flash_attn_2 = True | |
| def _init_weights(self, module: nn.Module): | |
| std = self.config.initializer_range | |
| if isinstance(module, VJEPA21AttentivePooler): | |
| nn.init.trunc_normal_(module.query_tokens, std=std) | |
| elif isinstance(module, VJEPA21PredictorEmbeddings): | |
| if self.config.pred_zero_init_mask_tokens: | |
| for mt in module.mask_tokens: | |
| nn.init.zeros_(mt) | |
| else: | |
| for mt in module.mask_tokens: | |
| nn.init.trunc_normal_(mt, std=std) | |
| elif isinstance(module, (nn.Linear, nn.Conv2d, nn.Conv3d)): | |
| nn.init.trunc_normal_(module.weight, std=std) | |
| if module.bias is not None: | |
| nn.init.zeros_(module.bias) | |
| elif isinstance(module, nn.LayerNorm): | |
| nn.init.zeros_(module.bias) | |
| nn.init.ones_(module.weight) | |
| if isinstance(module, VJEPA21Embeddings): | |
| if module.img_mod_embed is not None: | |
| nn.init.normal_(module.img_mod_embed, std=1e-6) | |
| nn.init.normal_(module.video_mod_embed, std=1e-6) | |
| if isinstance(module, VJEPA21PredictorEmbeddings): | |
| if module.img_mod_embed is not None: | |
| nn.init.normal_(module.img_mod_embed, std=1e-6) | |
| nn.init.normal_(module.video_mod_embed, std=1e-6) | |
| # --------------------------------------------------------------------------- | |
| # Main Models | |
| # --------------------------------------------------------------------------- | |
| class VJEPA21Model(VJEPA21PreTrainedModel): | |
| """V-JEPA 2.1 model (encoder + predictor). | |
| Example, feature extraction: | |
| ```python | |
| model = VJEPA21Model.from_pretrained("apiantonio/vjepa2.1-vit-base-384") | |
| outputs = model(pixel_values_videos, skip_predictor=True) | |
| features = outputs.last_hidden_state | |
| ``` | |
| Example, multi-level features (the `out_layers` recipe of the official | |
| frozen-evaluation probes): | |
| ```python | |
| levels = model( | |
| pixel_values_videos, | |
| skip_predictor=True, | |
| out_layers=model.config.encoder_hierarchical_layers, | |
| ).multilevel_hidden_states | |
| ``` | |
| Example, the JEPA masked forward (encoder sees only the context tokens): | |
| ```python | |
| out = model( | |
| pixel_values_videos, | |
| masks=[context_idx], # (B, K_ctx) | |
| context_mask=[context_idx], | |
| target_mask=[target_idx], # (B, K_tgt) | |
| ) | |
| prediction = out.predictor_output.last_hidden_state | |
| ``` | |
| """ | |
| def __init__(self, config: VJEPA21Config): | |
| super().__init__(config) | |
| self.encoder = VJEPA21Encoder(config) | |
| self.predictor = VJEPA21Predictor(config) | |
| self.post_init() | |
| def get_input_embeddings(self): | |
| return self.encoder.embeddings.patch_embeddings | |
| def forward( | |
| self, | |
| pixel_values_videos: torch.Tensor, | |
| masks: Optional[Union[torch.Tensor, list[torch.Tensor]]] = None, | |
| context_mask: Optional[Union[torch.Tensor, list[torch.Tensor]]] = None, | |
| target_mask: Optional[Union[torch.Tensor, list[torch.Tensor]]] = None, | |
| skip_predictor: bool = False, | |
| return_hierarchical: bool = False, | |
| out_layers: Optional[list[int]] = None, | |
| mask_index: int = 1, | |
| output_attentions: Optional[bool] = None, | |
| output_hidden_states: Optional[bool] = None, | |
| **kwargs, | |
| ) -> VJEPA21ModelOutput: | |
| """ | |
| Args: | |
| pixel_values_videos: Video tensor. Accepted layouts: | |
| `(B, C, T, H, W)`, `(B, T, C, H, W)`, `(B, T, H, W, C)`. | |
| masks: Optional list of `(B, K)` index tensors applied to the patch | |
| tokens *before* the encoder layers. This reproduces the JEPA | |
| training forward. When set, `context_mask` and `target_mask` must | |
| be given explicitly, since the encoder output no longer spans the | |
| full token grid. | |
| context_mask: List of `(B, K)` index tensors for context tokens. | |
| Defaults to all tokens when `masks` is None. | |
| target_mask: List of `(B, K)` index tensors for target tokens. | |
| Defaults to all tokens when `masks` is None. | |
| skip_predictor: Skip the predictor forward (encoder only). | |
| return_hierarchical: Return the concatenated distillation levels. | |
| out_layers: Encoder layer indices whose normalised features should be | |
| returned in `multilevel_hidden_states`. Must be a subset of | |
| `config.encoder_hierarchical_layers`. | |
| mask_index: Which learnable predictor mask token to inject. | |
| """ | |
| if pixel_values_videos is None: | |
| raise ValueError("pixel_values_videos is required") | |
| unexpected = set(kwargs) - _SILENTLY_IGNORED_KWARGS | |
| if unexpected: | |
| _warn_once( | |
| f"VJEPA21Model.forward received unexpected keyword arguments " | |
| f"{sorted(unexpected)}; they are ignored. Accepted arguments: " | |
| f"{sorted(_MODEL_FORWARD_KWARGS)}." | |
| ) | |
| output_attentions = ( | |
| output_attentions if output_attentions is not None else self.config.output_attentions | |
| ) | |
| output_hidden_states = ( | |
| output_hidden_states | |
| if output_hidden_states is not None | |
| else self.config.output_hidden_states | |
| ) | |
| masks = _as_mask_list(masks) | |
| context_mask = _as_mask_list(context_mask) | |
| target_mask = _as_mask_list(target_mask) | |
| # When the predictor fuses several distillation levels (n_output_distillation > 1, | |
| # e.g. the ViT-g and ViT-G checkpoints) its input projection expects the | |
| # concatenated hierarchical features, not the last hidden state. | |
| needs_hierarchical_input = ( | |
| not skip_predictor and len(self.config.predictor_hierarchical_layers) > 1 | |
| ) | |
| encoder_out = self.encoder( | |
| pixel_values_videos, | |
| masks=masks, | |
| return_hierarchical=return_hierarchical or needs_hierarchical_input, | |
| out_layers=out_layers, | |
| output_attentions=output_attentions, | |
| output_hidden_states=output_hidden_states, | |
| ) | |
| seq_output = encoder_out.last_hidden_state | |
| predictor_output = None | |
| masked_hidden_state = None | |
| if not skip_predictor: | |
| predictor_input = ( | |
| encoder_out.hierarchical_hidden_state | |
| if needs_hierarchical_input | |
| else seq_output | |
| ) | |
| batch_size = seq_output.size(0) | |
| num_tokens = seq_output.size(1) | |
| device = seq_output.device | |
| if masks is not None: | |
| # The encoder output only covers the context tokens, so the | |
| # "all tokens" default is meaningless here. | |
| if context_mask is None: | |
| context_mask = list(masks) | |
| if target_mask is None: | |
| raise ValueError( | |
| "target_mask must be given explicitly when the encoder runs with " | |
| "`masks`, since the encoder output no longer spans the full token " | |
| "grid. Pass `skip_predictor=True` if you only want the encoder." | |
| ) | |
| context_is_masked = True | |
| else: | |
| context_is_masked = False | |
| if context_mask is None: | |
| context_mask = [ | |
| torch.arange(num_tokens, device=device).unsqueeze(0).expand(batch_size, -1) | |
| ] | |
| if target_mask is None: | |
| target_mask = [ | |
| torch.arange(num_tokens, device=device).unsqueeze(0).expand(batch_size, -1) | |
| ] | |
| mode = self._detect_mode(pixel_values_videos) | |
| predictor_output = self.predictor( | |
| predictor_input, | |
| context_mask, | |
| target_mask, | |
| mode=mode, | |
| mask_index=mask_index, | |
| context_is_masked=context_is_masked, | |
| output_attentions=output_attentions, | |
| output_hidden_states=output_hidden_states, | |
| ) | |
| masked_hidden_state = ( | |
| predictor_input | |
| if context_is_masked | |
| else apply_masks(predictor_input, context_mask) | |
| ) | |
| return VJEPA21ModelOutput( | |
| last_hidden_state=seq_output, | |
| hierarchical_hidden_state=( | |
| encoder_out.hierarchical_hidden_state if return_hierarchical else None | |
| ), | |
| multilevel_hidden_states=encoder_out.multilevel_hidden_states, | |
| masked_hidden_state=masked_hidden_state, | |
| predictor_output=predictor_output, | |
| hidden_states=encoder_out.hidden_states, | |
| attentions=encoder_out.attentions, | |
| ) | |
| def _detect_mode(self, pixel_values_videos: torch.Tensor) -> str: | |
| x = normalize_video_layout(pixel_values_videos, self.config.in_chans) | |
| T = x.shape[2] | |
| if self.config.img_temporal_dim_size is not None and T == self.config.img_temporal_dim_size: | |
| return "img" | |
| return "video" | |
| def get_vision_features(self, pixel_values_videos: torch.Tensor) -> torch.Tensor: | |
| """Extract encoder features (convenience method for VLM integration).""" | |
| return self.forward(pixel_values_videos, skip_predictor=True).last_hidden_state | |
| class VJEPA21ForVideoClassification(VJEPA21PreTrainedModel): | |
| """V-JEPA 2.1 with attentive pooler + classification head. | |
| The pooler and the classifier are always randomly initialised: this class is | |
| the frozen-probe / fine-tuning entry point, not a pretrained classifier. | |
| """ | |
| def __init__(self, config: VJEPA21Config): | |
| super().__init__(config) | |
| self.num_labels = config.num_labels | |
| self.vjepa21 = VJEPA21Model(config) | |
| self.pooler = VJEPA21AttentivePooler(config) | |
| self.classifier = nn.Linear(config.hidden_size, config.num_labels) | |
| self.post_init() | |
| def forward( | |
| self, | |
| pixel_values_videos: torch.Tensor, | |
| labels: Optional[torch.Tensor] = None, | |
| output_attentions: Optional[bool] = None, | |
| output_hidden_states: Optional[bool] = None, | |
| **kwargs, | |
| ) -> ImageClassifierOutput: | |
| r""" | |
| labels (`torch.LongTensor` of shape `(batch_size,)` or `(batch_size, num_labels)`, *optional*): | |
| Labels for computing the classification loss. Integer indices in | |
| `[0, ..., config.num_labels - 1]` give single-label classification; | |
| a float multi-hot tensor gives multi-label classification (BCE), which | |
| is what a dataset such as XD-Violence needs. Set | |
| `config.problem_type` explicitly to remove the ambiguity. | |
| """ | |
| # Only forward the arguments the backbone understands; the Trainer injects | |
| # extras such as `num_items_in_batch` that would otherwise reach the encoder. | |
| forwarded = {k: v for k, v in kwargs.items() if k in _MODEL_FORWARD_KWARGS} | |
| ignored = set(kwargs) - set(forwarded) - _SILENTLY_IGNORED_KWARGS | |
| if ignored: | |
| _warn_once( | |
| f"VJEPA21ForVideoClassification.forward received unexpected keyword " | |
| f"arguments {sorted(ignored)}; they are ignored." | |
| ) | |
| forwarded.pop("skip_predictor", None) | |
| outputs = self.vjepa21( | |
| pixel_values_videos, | |
| skip_predictor=True, | |
| output_attentions=output_attentions, | |
| output_hidden_states=output_hidden_states, | |
| **forwarded, | |
| ) | |
| pooled = self.pooler(outputs.last_hidden_state) | |
| logits = self.classifier(pooled) | |
| loss = None | |
| if labels is not None: | |
| loss = self.loss_function(pooled_logits=logits, labels=labels, config=self.config) | |
| return ImageClassifierOutput( | |
| loss=loss, | |
| logits=logits, | |
| hidden_states=outputs.hidden_states, | |
| attentions=outputs.attentions, | |
| ) | |
| __all__ = [ | |
| "VJEPA21Model", | |
| "VJEPA21PreTrainedModel", | |
| "VJEPA21ForVideoClassification", | |
| ] | |