arcisvlm / model /predictor.py
Hardik Sanghvi
feat: integrate Gemma 4 E2B backbone for production-quality VLM inference
7a564e3
Raw
History Blame Contribute Delete
4.78 kB
"""
JEPA Predictor β€” the core of VL-JEPA architecture.
Takes visual tokens (from V-JEPA encoder) and query tokens (from BPE tokenizer),
and predicts a dense embedding in the same space as the Y-encoder output.
Key design choices:
- Bidirectional attention (NOT causal) β€” visual and query tokens attend to each other freely
- Non-autoregressive β€” single forward pass produces the embedding (no sequential generation)
- Output is a 1536-D embedding vector, not text tokens
"""
import torch
import torch.nn as nn
from model.transformer import TransformerBlock
class JEPAPredictor(nn.Module):
"""
JEPA Predictor β€” predicts text embeddings from visual + query tokens.
Architecture:
[visual_tokens (576Γ—768), query_tokens (≀512Γ—768)] β†’ N Γ— TransformerBlock(bidirectional) β†’ AvgPool β†’ Linear β†’ L2 normalize
This is the trainable core. During JEPA pretraining, the X-Encoder may be frozen
while the predictor learns to map visual+query β†’ text embedding space.
Args:
hidden_dim: Transformer dimension (768)
embed_dim: Output embedding dimension (1536)
num_heads: Number of attention heads (12)
num_blocks: Number of transformer blocks (8)
vocab_size: BPE vocabulary size (for query token embedding)
max_query_len: Maximum query token length (512)
dropout: Dropout rate
"""
def __init__(
self,
hidden_dim: int = 768,
embed_dim: int = 1536,
num_heads: int = 12,
num_blocks: int = 8,
vocab_size: int = 8192,
max_query_len: int = 512,
dropout: float = 0.1,
):
super().__init__()
self.hidden_dim = hidden_dim
self.embed_dim = embed_dim
# Query token embedding (for text queries)
self.query_embed = nn.Embedding(vocab_size, hidden_dim, padding_idx=0)
self.query_pos = nn.Parameter(torch.randn(1, max_query_len, hidden_dim) * 0.02)
# Modality type embeddings to distinguish visual vs query tokens
self.visual_type_embed = nn.Parameter(torch.zeros(1, 1, hidden_dim))
self.query_type_embed = nn.Parameter(torch.zeros(1, 1, hidden_dim))
self.embed_dropout = nn.Dropout(dropout)
# Bidirectional transformer blocks β€” visual and query attend to each other
self.blocks = nn.ModuleList([
TransformerBlock(hidden_dim, num_heads, dropout, mode="bidirectional")
for _ in range(num_blocks)
])
self.norm = nn.LayerNorm(hidden_dim)
# Project to embedding space
self.proj = nn.Linear(hidden_dim, embed_dim)
def forward(
self,
visual_tokens: torch.Tensor,
query_ids: torch.Tensor | None = None,
query_padding_mask: torch.Tensor | None = None,
) -> torch.Tensor:
"""
Args:
visual_tokens: [batch, num_patches, hidden_dim] β€” from V-JEPA encoder (576Γ—768)
query_ids: [batch, query_len] β€” BPE token IDs for the query (optional for captioning)
query_padding_mask: [batch, query_len] β€” True for non-pad positions
Returns:
[batch, embed_dim] β€” L2-normalized predicted embedding (1536-D)
"""
B = visual_tokens.shape[0]
# Add visual type embedding
visual = visual_tokens + self.visual_type_embed # [B, 576, 768]
if query_ids is not None:
Q = query_ids.shape[1]
# Embed query tokens + positional + type
query = self.query_embed(query_ids) + self.query_pos[:, :Q, :] + self.query_type_embed
# Concatenate: [visual_tokens, query_tokens]
x = torch.cat([visual, query], dim=1) # [B, 576+Q, 768]
else:
x = visual # [B, 576, 768] β€” captioning mode (no query)
x = self.embed_dropout(x)
# Pass through bidirectional transformer blocks
for block in self.blocks:
x = block(x)
x = self.norm(x)
# Average pooling over all non-padding positions
if query_ids is not None and query_padding_mask is not None:
# Build combined mask: all visual tokens are valid + query padding mask
visual_mask = torch.ones(B, visual_tokens.shape[1], device=visual_tokens.device, dtype=torch.bool)
combined_mask = torch.cat([visual_mask, query_padding_mask], dim=1) # [B, 576+Q]
mask = combined_mask.unsqueeze(-1).float() # [B, 576+Q, 1]
x = (x * mask).sum(dim=1) / mask.sum(dim=1).clamp(min=1)
else:
x = x.mean(dim=1) # [B, 768]
# Project to embedding space
x = self.proj(x) # [B, 1536]
# L2 normalize
x = nn.functional.normalize(x, p=2, dim=-1)
return x