| from __future__ import annotations |
|
|
| import torch |
| from torch import nn |
| import torch.nn.functional as F |
|
|
|
|
| class MLPProjector(nn.Module): |
| def __init__(self, in_dim: int, out_dim: int) -> None: |
| super().__init__() |
| self.net = nn.Sequential(nn.Linear(in_dim, out_dim), nn.GELU(), nn.Linear(out_dim, out_dim)) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| if x.is_inference(): |
| x = x.clone() |
| return self.net(x.to(dtype=self.net[0].weight.dtype)) |
|
|
|
|
| def _feed_forward(dim: int, mult: int = 1) -> nn.Sequential: |
| |
| |
| inner = int(dim * mult) |
| return nn.Sequential( |
| nn.LayerNorm(dim), |
| nn.Linear(dim, inner, bias=False), |
| nn.GELU(), |
| nn.Linear(inner, dim, bias=False), |
| ) |
|
|
|
|
| class PerceiverAttention(nn.Module): |
| """Reference `PerceiverAttention` block (perceiver.py:103). |
| |
| Latents attend to the concatenation of themselves and the media/token |
| context. Dual pre-norm (media + latents), bias-free q/kv/out projections. |
| """ |
|
|
| def __init__(self, dim: int, dim_head: int = 64, heads: int = 8) -> None: |
| super().__init__() |
| self.heads = heads |
| self.dim_head = dim_head |
| inner_dim = dim_head * heads |
| self.norm_media = nn.LayerNorm(dim) |
| self.norm_latents = nn.LayerNorm(dim) |
| self.to_q = nn.Linear(dim, inner_dim, bias=False) |
| self.to_kv = nn.Linear(dim, inner_dim * 2, bias=False) |
| self.to_out = nn.Linear(inner_dim, dim, bias=False) |
|
|
| def _split_heads(self, x: torch.Tensor) -> torch.Tensor: |
| b, n, _ = x.shape |
| return x.view(b, n, self.heads, self.dim_head).transpose(1, 2) |
|
|
| def forward(self, x: torch.Tensor, latents: torch.Tensor, key_padding_mask: torch.Tensor | None = None) -> torch.Tensor: |
| x = self.norm_media(x) |
| latents = self.norm_latents(latents) |
| b, num_latents, _ = latents.shape |
| q = self._split_heads(self.to_q(latents)) |
| kv_input = torch.cat([latents, x], dim=1) |
| k, v = self.to_kv(kv_input).chunk(2, dim=-1) |
| k = self._split_heads(k) |
| v = self._split_heads(v) |
| attn_mask = None |
| if key_padding_mask is not None: |
| |
| |
| latent_valid = torch.ones(b, num_latents, dtype=torch.bool, device=key_padding_mask.device) |
| valid = torch.cat([latent_valid, key_padding_mask.to(torch.bool)], dim=1) |
| attn_mask = valid[:, None, None, :] |
| out = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask) |
| out = out.transpose(1, 2).reshape(b, num_latents, self.heads * self.dim_head) |
| return self.to_out(out) |
|
|
|
|
| def _kaiming_init(module: nn.Module) -> None: |
| |
| |
| if isinstance(module, nn.Linear): |
| nn.init.kaiming_uniform_(module.weight) |
| if module.bias is not None: |
| nn.init.constant_(module.bias, 0) |
| elif isinstance(module, nn.LayerNorm): |
| nn.init.constant_(module.bias, 0) |
| nn.init.constant_(module.weight, 1.0) |
|
|
|
|
| class SharedMediaResampler(nn.Module): |
| """Media resampler matching the reference `perceiver_v2` block structure. |
| |
| Reference `PerceiverResamplerV2` (perceiver.py:261): depth-2 PerceiverAttention |
| blocks (dual-norm, bias-free, 8 heads, dim_head = dim // 8), bias-free ff_mult=1 |
| FeedForward, a final LayerNorm, kaiming init, and additive per-modality latent |
| embeddings. We keep the `shared + modality` latent split so Stage-2 can inherit |
| the image latents for video (`initialize_stage2_from_state_dict`). |
| """ |
|
|
| def __init__(self, dim: int, num_latents: int = 64, num_heads: int = 8, depth: int = 2) -> None: |
| super().__init__() |
| self.shared_latents = nn.Parameter(torch.randn(num_latents, dim)) |
| self.modality_latents = nn.ParameterDict( |
| {name: nn.Parameter(torch.randn(num_latents, dim)) for name in ("image", "video", "audio")} |
| ) |
| dim_head = max(1, dim // num_heads) |
| self.layers = nn.ModuleList( |
| nn.ModuleList([PerceiverAttention(dim, dim_head=dim_head, heads=num_heads), _feed_forward(dim, mult=1)]) |
| for _ in range(depth) |
| ) |
| self.norm = nn.LayerNorm(dim) |
| self.apply(_kaiming_init) |
|
|
| def forward(self, tokens: torch.Tensor, modality: str, mask: torch.Tensor | None = None) -> torch.Tensor: |
| if modality not in self.modality_latents: |
| raise ValueError(f"unknown modality: {modality}") |
| latents = self.shared_latents + self.modality_latents[modality] |
| latents = latents.unsqueeze(0).expand(tokens.shape[0], -1, -1) |
| key_padding_mask = None if mask is None else mask.to(device=tokens.device, dtype=torch.bool) |
| for attn, ff in self.layers: |
| latents = attn(tokens, latents, key_padding_mask) + latents |
| latents = ff(latents) + latents |
| return self.norm(latents) |
|
|
|
|
| class PerceiverLatentAttention(nn.Module): |
| """In-pooler latent-attention pre-pooler used by the reference `sw_attention` |
| head (`SWProj(using_latent_attention=True)`, embedding_proj.py:342). |
| |
| Compresses a variable-length (masked) token sequence into `num_latents` dense |
| latent tokens via a depth-2 PerceiverResampler (dim_head 256, 4 heads, ff_mult 1, |
| final LayerNorm). This is the content-adaptive component that lets the pooler |
| down-weight the shared instruction/BOS tokens; without it the sliced-Wasserstein |
| projection pools the raw frozen sequence and embeddings cannot separate. |
| """ |
|
|
| def __init__(self, dim: int, num_latents: int, depth: int = 2, dim_head: int = 256, heads: int = 4, ff_mult: int = 1) -> None: |
| super().__init__() |
| self.latents = nn.Parameter(torch.randn(num_latents, dim)) |
| self.layers = nn.ModuleList( |
| nn.ModuleList([PerceiverAttention(dim, dim_head=dim_head, heads=heads), _feed_forward(dim, mult=ff_mult)]) |
| for _ in range(depth) |
| ) |
| self.norm = nn.LayerNorm(dim) |
| self.apply(_kaiming_init) |
|
|
| def forward(self, tokens: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor: |
| latents = self.latents.unsqueeze(0).expand(tokens.shape[0], -1, -1) |
| key_padding_mask = None if mask is None else mask.to(device=tokens.device, dtype=torch.bool) |
| for attn, ff in self.layers: |
| latents = attn(tokens, latents, key_padding_mask) + latents |
| latents = ff(latents) + latents |
| return self.norm(latents) |
|
|
|
|
| class AttentionSlicedWassersteinPooling(nn.Module): |
| """Reference `sw_attention` pooler = latent-attention pre-pooler + sliced-Wasserstein |
| embedding (`RandPSWE`, reference_init='uniform', pooling_method='max'). |
| |
| The token sequence is first compressed to `num_references` dense latents, then the |
| sliced-Wasserstein step sorts the latent projections against a learnable uniform |
| reference and pools with a straight-through max. Because the latents are dense and |
| already count `num_references`, the SW step needs no mask or grid-sample |
| interpolation (reference runs `sample=False`). |
| """ |
|
|
| def __init__( |
| self, |
| dim: int, |
| num_references: int = 128, |
| num_projections: int = 4096, |
| depth: int = 2, |
| dim_head: int = 256, |
| heads: int = 4, |
| ) -> None: |
| super().__init__() |
| self.num_references = num_references |
| self.latent_attention = PerceiverLatentAttention(dim, num_references, depth=depth, dim_head=dim_head, heads=heads) |
| theta_v = torch.normal(mean=0, std=1, size=(dim, num_projections)) |
| if num_projections <= dim: |
| theta_v = torch.eye(dim, num_projections, dtype=torch.float32).type(theta_v.dtype) |
| self.theta_v = nn.Parameter(theta_v) |
| |
| |
| |
| uniform_ref = torch.linspace(-1, 1, num_references).unsqueeze(1).repeat(1, num_projections) |
| self.reference = nn.Parameter(uniform_ref) |
|
|
| @property |
| def theta(self) -> torch.Tensor: |
| return self.theta_v |
|
|
| def get_slice(self, X: torch.Tensor) -> torch.Tensor: |
| theta_v = torch.nan_to_num(self.theta_v, nan=0.0, posinf=0.0, neginf=0.0) |
| theta_norm = torch.linalg.vector_norm(theta_v, dim=0, keepdim=True) |
| theta_norm = torch.nan_to_num(theta_norm, nan=0.0, posinf=0.0, neginf=0.0).clamp(min=1e-8) |
| theta = theta_v / theta_norm |
| theta = torch.nan_to_num(theta, nan=0.0, posinf=0.0, neginf=0.0) |
| return torch.matmul(X, theta.to(X.dtype).to(X.device)) |
|
|
| def forward(self, tokens: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor: |
| latents = self.latent_attention(tokens, mask) |
| slices = self.get_slice(latents) |
| sorted_slices = torch.sort(slices, dim=1).values |
| refs = self.reference.to(slices.dtype).unsqueeze(0).expand_as(sorted_slices) |
| _, ref_order = torch.sort(refs, dim=1) |
| sorted_slices = torch.gather(sorted_slices, dim=1, index=ref_order) |
| coupled = refs - sorted_slices |
| if not self.training: |
| return coupled.max(dim=1).values |
| y_soft = F.softmax(coupled, dim=1) |
| indices = torch.argmax(y_soft, dim=1, keepdim=True) |
| y_hard = torch.zeros_like(coupled).scatter_(1, indices, 1) |
| return (coupled * (y_hard - y_soft.detach() + y_soft)).sum(dim=1) |
|
|