Instructions to use Octopus1/PaGE with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Octopus1/PaGE with Transformers:
# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("Octopus1/PaGE", dtype="auto") - Notebooks
- Google Colab
- Kaggle
| # coding=utf-8 | |
| # PaGE: Patch-level Gaze Estimation with cross-attention scene/head interaction. | |
| # | |
| # This file is self-contained. Dependencies: torch, torchvision, timm, transformers (>=4.56, which ships DINOv3 built-in). | |
| # The DINOv3 backbones are constructed from config only (no external checkpoint download); their weights live in this | |
| # model's safetensors alongside the gaze decoder. | |
| # | |
| # Auto-map entry points: PaGEConfig, PaGEModel, PaGEImageProcessor. | |
| from __future__ import annotations | |
| import math | |
| from typing import Optional, Tuple, Type, Union, List | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| import torchvision | |
| from timm.models.vision_transformer import Block | |
| from timm.layers.mlp import SwiGLU, Mlp | |
| from timm.layers import DropPath, LayerNorm, LayerScale, use_fused_attn | |
| from transformers import PretrainedConfig, PreTrainedModel | |
| from transformers.image_processing_utils import BaseImageProcessor | |
| # --------------------------------------------------------------------------- # | |
| # Robust DINOv3 import (built into transformers >= 4.56). # | |
| # --------------------------------------------------------------------------- # | |
| try: | |
| from transformers.models.dinov3_vit import DINOv3ViTModel, DINOv3ViTConfig | |
| except Exception as e: # pragma: no cover | |
| raise ImportError( | |
| "PaGE requires transformers>=4.56 with built-in DINOv3 support (`transformers.models.dinov3_vit`). " | |
| f"Import failed: {e!r}" | |
| ) | |
| # =========================================================================== # | |
| # Vendored utilities (from gazelle.utils) # | |
| # =========================================================================== # | |
| def repeat_tensors(tensor, repeat_counts): | |
| repeated_tensors = [ | |
| tensor[i:i + 1].repeat(repeat, *[1] * (tensor.ndim - 1)) | |
| for i, repeat in enumerate(repeat_counts) | |
| ] | |
| return torch.cat(repeated_tensors, dim=0) | |
| def split_tensors(tensor, split_counts): | |
| indices = torch.cumsum(torch.tensor([0] + split_counts), dim=0) | |
| return [tensor[indices[i]:indices[i + 1]] for i in range(len(split_counts))] | |
| class TransposeLayerNorm(nn.Module): | |
| """Transpose 2D feature maps for layer norm, then transpose back.""" | |
| def __init__(self, dim): | |
| super().__init__() | |
| self.ln = nn.LayerNorm(dim) | |
| def forward(self, x: torch.Tensor): | |
| x = x.permute(0, 2, 3, 1).contiguous() | |
| x = self.ln(x) | |
| x = x.permute(0, 3, 1, 2).contiguous() | |
| return x | |
| def positionalencoding2d(d_model, height, width): | |
| if d_model % 4 != 0: | |
| raise ValueError( | |
| "Cannot use sin/cos positional encoding with odd dimension (got dim={:d})".format(d_model)) | |
| pe = torch.zeros(d_model, height, width) | |
| d_model = int(d_model / 2) | |
| div_term = torch.exp(torch.arange(0., d_model, 2) | |
| * -(math.log(10000.0) / d_model)) | |
| pos_w = torch.arange(0., width).unsqueeze(1) | |
| pos_h = torch.arange(0., height).unsqueeze(1) | |
| pe[0:d_model:2, :, :] = torch.sin(pos_w * div_term).transpose(0, 1).unsqueeze(1).repeat(1, height, 1) | |
| pe[1:d_model:2, :, :] = torch.cos(pos_w * div_term).transpose(0, 1).unsqueeze(1).repeat(1, height, 1) | |
| pe[d_model::2, :, :] = torch.sin(pos_h * div_term).transpose(0, 1).unsqueeze(2).repeat(1, 1, width) | |
| pe[d_model + 1::2, :, :] = torch.cos(pos_h * div_term).transpose(0, 1).unsqueeze(2).repeat(1, 1, width) | |
| return pe | |
| # =========================================================================== # | |
| # Vendored Axial 2D RoPE self-attention (from gazelle.rope_self_attention) # | |
| # =========================================================================== # | |
| GridSize = Optional[Tuple[int, int]] | |
| Rect = Union[Tuple[float, float, float, float], torch.Tensor] | |
| class Axial2dRotaryEmbedding(nn.Module): | |
| def __init__(self, dim: int, base: float = 100.0) -> None: | |
| super().__init__() | |
| if dim <= 0 or dim % 4 != 0: | |
| raise ValueError(f"`dim` must be a positive multiple of 4, got {dim}.") | |
| self.dim = dim | |
| self.axis_dim = dim // 2 | |
| self.base = base | |
| self.register_buffer("inv_freq", self._compute_inv_freq(), persistent=False) | |
| def _compute_inv_freq(self): | |
| return 1.0 / (self.base ** (torch.arange(0, self.axis_dim, 2, dtype=torch.float32) / self.axis_dim)) | |
| def reset_inv_freq(self): | |
| """Recompute inv_freq (a non-persistent buffer that meta-init in from_pretrained can corrupt).""" | |
| self.inv_freq = self._compute_inv_freq() | |
| def _axis_cos_sin(self, coords, *, device, dtype): | |
| inv_freq = self.inv_freq.to(device=device, dtype=torch.float32) | |
| freqs = coords.to(device=device, dtype=torch.float32)[:, None] * inv_freq[None, :] | |
| return freqs.cos().to(dtype=dtype), freqs.sin().to(dtype=dtype) | |
| def _rotate_axis(x, cos, sin): | |
| x = x.reshape(*x.shape[:-1], -1, 2) | |
| x_even, x_odd = x.unbind(dim=-1) | |
| cos = cos[None, None, :, :] | |
| sin = sin[None, None, :, :] | |
| x_rot = torch.stack((x_even * cos - x_odd * sin, x_even * sin + x_odd * cos), dim=-1) | |
| return x_rot.flatten(-2) | |
| def forward(self, q, k, grid_size, num_front_tokens=0): | |
| _, _, n, head_dim = q.shape | |
| gh, gw = grid_size | |
| num_patch_tokens = gh * gw | |
| expected = num_front_tokens + num_patch_tokens | |
| if n != expected: | |
| raise ValueError(f"Token count mismatch: got N={n}, expected {num_front_tokens} front + {gh}*{gw} = {expected}.") | |
| if self.dim > head_dim: | |
| raise ValueError(f"RoPE dim {self.dim} exceeds head_dim {head_dim}.") | |
| q_front, q_patch = q[:, :, :num_front_tokens], q[:, :, num_front_tokens:] | |
| k_front, k_patch = k[:, :, :num_front_tokens], k[:, :, num_front_tokens:] | |
| yy, xx = torch.meshgrid(torch.arange(gh, device=q.device), torch.arange(gw, device=q.device), indexing="ij") | |
| yy = yy.reshape(-1); xx = xx.reshape(-1) | |
| cos_y, sin_y = self._axis_cos_sin(yy, device=q.device, dtype=q.dtype) | |
| cos_x, sin_x = self._axis_cos_sin(xx, device=q.device, dtype=q.dtype) | |
| def apply_rope(t): | |
| t_rope, t_pass = t[..., :self.dim], t[..., self.dim:] | |
| t_y, t_x = t_rope.split(self.axis_dim, dim=-1) | |
| t_y = self._rotate_axis(t_y, cos_y, sin_y) | |
| t_x = self._rotate_axis(t_x, cos_x, sin_x) | |
| return torch.cat((t_y, t_x, t_pass), dim=-1) | |
| q_patch = apply_rope(q_patch) | |
| k_patch = apply_rope(k_patch) | |
| q = torch.cat((q_front, q_patch), dim=2) | |
| k = torch.cat((k_front, k_patch), dim=2) | |
| return q, k | |
| class AxialRoPEAttention(nn.Module): | |
| fused_attn: bool | |
| def __init__(self, dim, num_heads=8, attn_head_dim=None, dim_out=None, qkv_bias=False, | |
| qk_norm=False, scale_norm=False, proj_bias=True, attn_drop=0.0, proj_drop=0.0, | |
| norm_layer=None, grid_size=None, num_front_tokens=0, rope_base=100.0, | |
| rope_dim=None, device=None, dtype=None): | |
| super().__init__() | |
| dd = {"device": device, "dtype": dtype} | |
| dim_out = dim_out or dim | |
| head_dim = attn_head_dim or dim // num_heads | |
| if attn_head_dim is None: | |
| assert dim % num_heads == 0 | |
| if qk_norm or scale_norm: | |
| assert norm_layer is not None | |
| rope_dim = head_dim if rope_dim is None else rope_dim | |
| if rope_dim > head_dim: | |
| raise ValueError(f"`rope_dim`={rope_dim} exceeds head_dim={head_dim}.") | |
| if rope_dim % 4 != 0: | |
| raise ValueError("For axial 2D RoPE, `rope_dim` must be divisible by 4.") | |
| if num_front_tokens < 0: | |
| raise ValueError("`num_front_tokens` must be non-negative.") | |
| self.num_heads = num_heads | |
| self.head_dim = head_dim | |
| self.attn_dim = num_heads * head_dim | |
| self.scale = head_dim ** -0.5 | |
| self.fused_attn = use_fused_attn() | |
| self.grid_size = grid_size | |
| self.num_front_tokens = num_front_tokens | |
| self.qkv = nn.Linear(dim, self.attn_dim * 3, bias=qkv_bias, **dd) | |
| self.q_norm = norm_layer(head_dim, **dd) if qk_norm else nn.Identity() | |
| self.k_norm = norm_layer(head_dim, **dd) if qk_norm else nn.Identity() | |
| self.rope = Axial2dRotaryEmbedding(rope_dim, base=rope_base) | |
| self.attn_drop = nn.Dropout(attn_drop) | |
| self.norm = norm_layer(self.attn_dim, **dd) if scale_norm else nn.Identity() | |
| self.proj = nn.Linear(self.attn_dim, dim_out, bias=proj_bias, **dd) | |
| self.proj_drop = nn.Dropout(proj_drop) | |
| def set_grid_size(self, grid_size): | |
| self.grid_size = grid_size | |
| def _infer_grid_size(self, num_patch_tokens): | |
| if self.grid_size is not None: | |
| gh, gw = self.grid_size | |
| if gh * gw != num_patch_tokens: | |
| raise ValueError(f"`grid_size={self.grid_size}` implies {gh * gw} patches, got {num_patch_tokens}.") | |
| return gh, gw | |
| side = math.isqrt(num_patch_tokens) | |
| if side * side != num_patch_tokens: | |
| raise ValueError("Cannot infer a non-square patch grid from the token sequence.") | |
| return side, side | |
| def forward(self, x, attn_mask=None, is_causal=False): | |
| b, n, _ = x.shape | |
| num_patch_tokens = n - self.num_front_tokens | |
| if num_patch_tokens <= 0: | |
| raise ValueError(f"Expected patch tokens after {self.num_front_tokens} front tokens, got N={n}.") | |
| qkv = self.qkv(x).reshape(b, n, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4) | |
| q, k, v = qkv.unbind(0) | |
| q = self.q_norm(q); k = self.k_norm(k) | |
| grid_size = self._infer_grid_size(num_patch_tokens) | |
| q, k = self.rope(q, k, grid_size=grid_size, num_front_tokens=self.num_front_tokens) | |
| if self.fused_attn: | |
| x = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask, | |
| dropout_p=self.attn_drop.p if self.training else 0.0, is_causal=is_causal) | |
| else: | |
| q = q * self.scale | |
| attn = q @ k.transpose(-2, -1) | |
| attn = attn.softmax(dim=-1) | |
| attn = self.attn_drop(attn) | |
| x = attn @ v | |
| x = x.transpose(1, 2).reshape(b, n, self.attn_dim) | |
| x = self.norm(x) | |
| x = self.proj(x) | |
| x = self.proj_drop(x) | |
| return x | |
| class AxialRoPEBlock(nn.Module): | |
| def __init__(self, dim, num_heads, mlp_ratio=4.0, qkv_bias=False, qk_norm=False, | |
| scale_attn_norm=False, scale_mlp_norm=False, proj_bias=True, proj_drop=0.0, | |
| attn_drop=0.0, init_values=None, drop_path=0.0, act_layer=nn.GELU, | |
| norm_layer=LayerNorm, mlp_layer=Mlp, attn_layer=None, depth=0, | |
| grid_size=None, num_front_tokens=0, rope_base=100.0, rope_dim=None, device=None, dtype=None): | |
| super().__init__() | |
| dd = {"device": device, "dtype": dtype} | |
| self.norm1 = norm_layer(dim, **dd) | |
| self.attn = AxialRoPEAttention(dim=dim, num_heads=num_heads, qkv_bias=qkv_bias, qk_norm=qk_norm, | |
| scale_norm=scale_attn_norm, proj_bias=proj_bias, attn_drop=attn_drop, | |
| proj_drop=proj_drop, norm_layer=norm_layer, grid_size=grid_size, | |
| num_front_tokens=num_front_tokens, rope_base=rope_base, rope_dim=rope_dim, **dd) | |
| self.ls1 = LayerScale(dim, init_values=init_values, **dd) if init_values is not None else nn.Identity() | |
| self.drop_path1 = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() | |
| self.norm2 = norm_layer(dim, **dd) | |
| self.mlp = mlp_layer(in_features=dim, hidden_features=int(dim * mlp_ratio), act_layer=act_layer, | |
| norm_layer=norm_layer if scale_mlp_norm else None, bias=proj_bias, drop=proj_drop, **dd) | |
| self.ls2 = LayerScale(dim, init_values=init_values, **dd) if init_values is not None else nn.Identity() | |
| self.drop_path2 = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() | |
| def set_grid_size(self, grid_size): | |
| self.attn.set_grid_size(grid_size) | |
| def forward(self, x, attn_mask=None, is_causal=False): | |
| x = x + self.drop_path1(self.ls1(self.attn(self.norm1(x), attn_mask=attn_mask, is_causal=is_causal))) | |
| x = x + self.drop_path2(self.ls2(self.mlp(self.norm2(x)))) | |
| return x | |
| # =========================================================================== # | |
| # Vendored cross-attention (no RoPE) (from gazelle.cross_attention) # | |
| # =========================================================================== # | |
| def _cross_attn_mask(attn_mask, dtype): | |
| if attn_mask is None: | |
| return None | |
| if attn_mask.dtype == torch.bool: | |
| bias = torch.zeros_like(attn_mask, dtype=dtype) | |
| bias.masked_fill_(~attn_mask, float("-inf")) | |
| return bias | |
| return attn_mask | |
| class CrossAttention(nn.Module): | |
| fused_attn: bool | |
| def __init__(self, dim, num_heads=8, attn_head_dim=None, dim_out=None, qkv_bias=False, | |
| qk_norm=False, scale_norm=False, proj_bias=True, attn_drop=0.0, proj_drop=0.0, norm_layer=None): | |
| super().__init__() | |
| dim_out = dim_out or dim | |
| if attn_head_dim is None: | |
| assert dim % num_heads == 0 | |
| head_dim = dim // num_heads | |
| else: | |
| head_dim = attn_head_dim | |
| if qk_norm or scale_norm: | |
| assert norm_layer is not None | |
| self.num_heads = num_heads | |
| self.head_dim = head_dim | |
| self.attn_dim = num_heads * head_dim | |
| self.scale = head_dim ** -0.5 | |
| self.fused_attn = use_fused_attn() | |
| self.q = nn.Linear(dim, self.attn_dim, bias=qkv_bias) | |
| self.k = nn.Linear(dim, self.attn_dim, bias=qkv_bias) | |
| self.v = nn.Linear(dim, self.attn_dim, bias=qkv_bias) | |
| self.q_norm = norm_layer(head_dim) if qk_norm else nn.Identity() | |
| self.k_norm = norm_layer(head_dim) if qk_norm else nn.Identity() | |
| self.attn_drop = nn.Dropout(attn_drop) | |
| self.norm = norm_layer(self.attn_dim) if scale_norm else nn.Identity() | |
| self.proj = nn.Linear(self.attn_dim, dim_out, bias=proj_bias) | |
| self.proj_drop = nn.Dropout(proj_drop) | |
| def forward(self, x_q, x_kv, attn_mask=None): | |
| B, Nq, _ = x_q.shape | |
| Bkv, Nk, _ = x_kv.shape | |
| assert B == Bkv | |
| q = self.q(x_q).reshape(B, Nq, self.num_heads, self.head_dim).transpose(1, 2) | |
| k = self.k(x_kv).reshape(B, Nk, self.num_heads, self.head_dim).transpose(1, 2) | |
| v = self.v(x_kv).reshape(B, Nk, self.num_heads, self.head_dim).transpose(1, 2) | |
| q = self.q_norm(q); k = self.k_norm(k) | |
| if self.fused_attn: | |
| x = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask, | |
| dropout_p=self.attn_drop.p if self.training else 0.0) | |
| else: | |
| q = q * self.scale | |
| attn = q @ k.transpose(-2, -1) | |
| attn_bias = _cross_attn_mask(attn_mask, attn.dtype) | |
| if attn_bias is not None: | |
| attn = attn + attn_bias | |
| attn = attn.softmax(dim=-1) | |
| attn = self.attn_drop(attn) | |
| x = attn @ v | |
| x = x.transpose(1, 2).reshape(B, Nq, self.attn_dim) | |
| x = self.norm(x) | |
| x = self.proj(x) | |
| x = self.proj_drop(x) | |
| return x | |
| class CrossAttentionBlock(nn.Module): | |
| def __init__(self, dim, num_heads, qkv_bias=False, qk_norm=False, scale_attn_norm=False, | |
| proj_bias=True, proj_drop=0.0, attn_drop=0.0, init_values=None, drop_path=0.0, | |
| norm_layer=LayerNorm): | |
| super().__init__() | |
| self.norm_q = norm_layer(dim) | |
| self.norm_kv = norm_layer(dim) | |
| self.attn = CrossAttention(dim=dim, num_heads=num_heads, qkv_bias=qkv_bias, qk_norm=qk_norm, | |
| scale_norm=scale_attn_norm, proj_bias=proj_bias, attn_drop=attn_drop, | |
| proj_drop=proj_drop, norm_layer=norm_layer) | |
| self.ls1 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity() | |
| self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() | |
| def forward(self, x_q, x_kv, attn_mask=None): | |
| x_q = x_q + self.drop_path(self.ls1(self.attn(self.norm_q(x_q), self.norm_kv(x_kv), attn_mask=attn_mask))) | |
| return x_q | |
| # =========================================================================== # | |
| # Vendored Axial 2D RoPE cross-attention (from gazelle.rope_cross_attention) # | |
| # =========================================================================== # | |
| def _infer_square_grid_size(num_patch_tokens, *, name): | |
| side = math.isqrt(num_patch_tokens) | |
| if side * side != num_patch_tokens: | |
| raise ValueError(f"Cannot infer square grid for {name} from {num_patch_tokens} patch tokens.") | |
| return side, side | |
| def _as_batched_rect(rect, *, batch_size, device, dtype, name): | |
| rect = torch.as_tensor(rect, device=device, dtype=dtype) | |
| if rect.ndim == 1: | |
| if rect.shape[0] != 4: | |
| raise ValueError(f"{name}_rect must have shape [4] or [B, 4].") | |
| rect = rect[None, :].expand(batch_size, 4) | |
| elif rect.ndim == 2: | |
| if rect.shape[1] != 4: | |
| raise ValueError(f"{name}_rect must have shape [4] or [B, 4].") | |
| if rect.shape[0] == 1: | |
| rect = rect.expand(batch_size, 4) | |
| elif rect.shape[0] != batch_size: | |
| raise ValueError(f"{name}_rect has batch size {rect.shape[0]}, expected {batch_size}.") | |
| else: | |
| raise ValueError(f"{name}_rect must have shape [4] or [B, 4].") | |
| return rect | |
| def _native_grid_coords(grid_size, *, batch_size, device, dtype): | |
| gh, gw = grid_size | |
| yy, xx = torch.meshgrid(torch.arange(gh, device=device, dtype=dtype), | |
| torch.arange(gw, device=device, dtype=dtype), indexing="ij") | |
| coords = torch.stack((yy.reshape(-1), xx.reshape(-1)), dim=-1) | |
| return coords[None, :, :].expand(batch_size, -1, -1) | |
| def _rect_grid_coords(grid_size, rect, *, align_corners): | |
| b = rect.shape[0]; gh, gw = grid_size | |
| device = rect.device; dtype = rect.dtype | |
| y0, x0, y1, x1 = rect.unbind(dim=-1) | |
| if align_corners: | |
| if gh == 1: | |
| ys = ((y0 + y1 - 1.0) * 0.5)[:, None] | |
| else: | |
| iy = torch.linspace(0.0, 1.0, gh, device=device, dtype=dtype) | |
| ys = y0[:, None] + iy[None, :] * ((y1 - 1.0) - y0)[:, None] | |
| if gw == 1: | |
| xs = ((x0 + x1 - 1.0) * 0.5)[:, None] | |
| else: | |
| ix = torch.linspace(0.0, 1.0, gw, device=device, dtype=dtype) | |
| xs = x0[:, None] + ix[None, :] * ((x1 - 1.0) - x0)[:, None] | |
| else: | |
| iy = torch.arange(gh, device=device, dtype=dtype) + 0.5 | |
| ix = torch.arange(gw, device=device, dtype=dtype) + 0.5 | |
| ys = y0[:, None] + iy[None, :] * ((y1 - y0) / gh)[:, None] - 0.5 | |
| xs = x0[:, None] + ix[None, :] * ((x1 - x0) / gw)[:, None] - 0.5 | |
| yy = ys[:, :, None].expand(b, gh, gw) | |
| xx = xs[:, None, :].expand(b, gh, gw) | |
| return torch.stack((yy.reshape(b, -1), xx.reshape(b, -1)), dim=-1) | |
| def _as_batched_patch_coords(patch_coords, *, batch_size, num_patch_tokens, device, dtype, name): | |
| patch_coords = torch.as_tensor(patch_coords, device=device, dtype=dtype) | |
| if patch_coords.ndim == 2: | |
| if patch_coords.shape != (num_patch_tokens, 2): | |
| raise ValueError(f"{name}_patch_coords must have shape [{num_patch_tokens}, 2] or [B, {num_patch_tokens}, 2], got {tuple(patch_coords.shape)}.") | |
| patch_coords = patch_coords[None, :, :].expand(batch_size, -1, -1) | |
| elif patch_coords.ndim == 3: | |
| if patch_coords.shape[1:] != (num_patch_tokens, 2): | |
| raise ValueError(f"{name}_patch_coords must have shape [B, {num_patch_tokens}, 2], got {tuple(patch_coords.shape)}.") | |
| if patch_coords.shape[0] == 1: | |
| patch_coords = patch_coords.expand(batch_size, -1, -1) | |
| elif patch_coords.shape[0] != batch_size: | |
| raise ValueError(f"{name}_patch_coords has batch size {patch_coords.shape[0]}, expected {batch_size}.") | |
| else: | |
| raise ValueError(f"{name}_patch_coords must have shape [N_patch, 2] or [B, N_patch, 2].") | |
| return patch_coords | |
| def make_stream_patch_coords(*, batch_size, num_patch_tokens, grid_size, rect, patch_coords, | |
| device, dtype=torch.float32, align_corners=False, name): | |
| if grid_size is None: | |
| grid_size = _infer_square_grid_size(num_patch_tokens, name=name) | |
| gh, gw = grid_size | |
| expected = gh * gw | |
| if expected != num_patch_tokens: | |
| raise ValueError(f"{name}_grid_size={grid_size} implies {expected} patch tokens, but {name} stream has {num_patch_tokens}.") | |
| if patch_coords is not None and rect is not None: | |
| raise ValueError(f"Provide either {name}_patch_coords or {name}_rect, not both.") | |
| if patch_coords is not None: | |
| return _as_batched_patch_coords(patch_coords, batch_size=batch_size, num_patch_tokens=num_patch_tokens, | |
| device=device, dtype=dtype, name=name) | |
| if rect is not None: | |
| rect = _as_batched_rect(rect, batch_size=batch_size, device=device, dtype=dtype, name=name) | |
| return _rect_grid_coords(grid_size, rect, align_corners=align_corners) | |
| return _native_grid_coords(grid_size, batch_size=batch_size, device=device, dtype=dtype) | |
| class Axial2dCrossRotaryEmbedding(nn.Module): | |
| def __init__(self, dim, base=100.0): | |
| super().__init__() | |
| if dim <= 0 or dim % 4 != 0: | |
| raise ValueError(f"dim must be a positive multiple of 4, got {dim}.") | |
| self.dim = dim | |
| self.axis_dim = dim // 2 | |
| self.base = base | |
| self.register_buffer("inv_freq", self._compute_inv_freq(), persistent=False) | |
| def _compute_inv_freq(self): | |
| return 1.0 / (self.base ** (torch.arange(0, self.axis_dim, 2, dtype=torch.float32) / self.axis_dim)) | |
| def reset_inv_freq(self): | |
| self.inv_freq = self._compute_inv_freq() | |
| def _axis_cos_sin(self, coords, *, out_dtype): | |
| if coords.ndim != 2: | |
| raise ValueError(f"coords must have shape [B, N], got {tuple(coords.shape)}.") | |
| coords = coords.to(dtype=torch.float32) | |
| inv_freq = self.inv_freq.to(device=coords.device, dtype=torch.float32) | |
| freqs = coords[..., None] * inv_freq[None, None, :] | |
| return freqs.cos().to(dtype=out_dtype), freqs.sin().to(dtype=out_dtype) | |
| def _rotate_axis(x, cos, sin): | |
| x = x.reshape(*x.shape[:-1], -1, 2) | |
| x_even, x_odd = x.unbind(dim=-1) | |
| cos = cos[:, None, :, :] | |
| sin = sin[:, None, :, :] | |
| x_rot = torch.stack((x_even * cos - x_odd * sin, x_even * sin + x_odd * cos), dim=-1) | |
| return x_rot.flatten(-2) | |
| def rotate_one(self, x, coords_yx, *, num_front_tokens, stream_name): | |
| b, _, n_total, head_dim = x.shape | |
| if self.dim > head_dim: | |
| raise ValueError(f"RoPE dim {self.dim} exceeds head_dim {head_dim} for {stream_name}.") | |
| if num_front_tokens < 0: | |
| raise ValueError(f"{stream_name}_num_front_tokens must be non-negative.") | |
| n_patch = n_total - num_front_tokens | |
| if n_patch <= 0: | |
| raise ValueError(f"{stream_name} has no patch tokens after {num_front_tokens} front tokens.") | |
| if coords_yx.shape != (b, n_patch, 2): | |
| raise ValueError(f"{stream_name}_coords_yx must have shape [{b}, {n_patch}, 2], got {tuple(coords_yx.shape)}.") | |
| coords_yx = coords_yx.to(device=x.device) | |
| x_front = x[:, :, :num_front_tokens, :] | |
| x_patch = x[:, :, num_front_tokens:, :] | |
| y = coords_yx[..., 0] | |
| x_coord = coords_yx[..., 1] | |
| cos_y, sin_y = self._axis_cos_sin(y, out_dtype=x.dtype) | |
| cos_x, sin_x = self._axis_cos_sin(x_coord, out_dtype=x.dtype) | |
| x_rope = x_patch[..., :self.dim] | |
| x_pass = x_patch[..., self.dim:] | |
| x_y, x_x = x_rope.split(self.axis_dim, dim=-1) | |
| x_y = self._rotate_axis(x_y, cos_y, sin_y) | |
| x_x = self._rotate_axis(x_x, cos_x, sin_x) | |
| x_patch = torch.cat((x_y, x_x, x_pass), dim=-1) | |
| if num_front_tokens == 0: | |
| return x_patch | |
| return torch.cat((x_front, x_patch), dim=2) | |
| def forward(self, q, k, *, q_coords_yx, kv_coords_yx, q_num_front_tokens=0, kv_num_front_tokens=0): | |
| q = self.rotate_one(q, q_coords_yx, num_front_tokens=q_num_front_tokens, stream_name="q") | |
| k = self.rotate_one(k, kv_coords_yx, num_front_tokens=kv_num_front_tokens, stream_name="kv") | |
| return q, k | |
| class AxialRoPECrossAttention(nn.Module): | |
| fused_attn: bool | |
| def __init__(self, dim, num_heads=8, attn_head_dim=None, dim_out=None, qkv_bias=False, | |
| qk_norm=False, scale_norm=False, proj_bias=True, attn_drop=0.0, proj_drop=0.0, | |
| norm_layer=None, q_num_front_tokens=0, kv_num_front_tokens=0, rope_base=100.0, | |
| rope_dim=None, align_corners=False, device=None, dtype=None): | |
| super().__init__() | |
| dd = {"device": device, "dtype": dtype} | |
| dim_out = dim_out or dim | |
| if attn_head_dim is None: | |
| assert dim % num_heads == 0 | |
| head_dim = dim // num_heads | |
| else: | |
| head_dim = attn_head_dim | |
| if qk_norm or scale_norm: | |
| assert norm_layer is not None | |
| rope_dim = head_dim if rope_dim is None else rope_dim | |
| if rope_dim > head_dim: | |
| raise ValueError(f"rope_dim={rope_dim} exceeds head_dim={head_dim}.") | |
| if rope_dim % 4 != 0: | |
| raise ValueError("For axial 2D RoPE, `rope_dim` must be divisible by 4.") | |
| if q_num_front_tokens < 0: | |
| raise ValueError("q_num_front_tokens must be non-negative.") | |
| if kv_num_front_tokens < 0: | |
| raise ValueError("kv_num_front_tokens must be non-negative.") | |
| self.num_heads = num_heads | |
| self.head_dim = head_dim | |
| self.attn_dim = num_heads * head_dim | |
| self.scale = head_dim ** -0.5 | |
| self.fused_attn = use_fused_attn() | |
| self.q_num_front_tokens = q_num_front_tokens | |
| self.kv_num_front_tokens = kv_num_front_tokens | |
| self.align_corners = align_corners | |
| self.q = nn.Linear(dim, self.attn_dim, bias=qkv_bias, **dd) | |
| self.k = nn.Linear(dim, self.attn_dim, bias=qkv_bias, **dd) | |
| self.v = nn.Linear(dim, self.attn_dim, bias=qkv_bias, **dd) | |
| self.q_norm = norm_layer(head_dim, **dd) if qk_norm else nn.Identity() | |
| self.k_norm = norm_layer(head_dim, **dd) if qk_norm else nn.Identity() | |
| self.rope = Axial2dCrossRotaryEmbedding(dim=rope_dim, base=rope_base) | |
| self.attn_drop = nn.Dropout(attn_drop) | |
| self.norm = norm_layer(self.attn_dim, **dd) if scale_norm else nn.Identity() | |
| self.proj = nn.Linear(self.attn_dim, dim_out, bias=proj_bias, **dd) | |
| self.proj_drop = nn.Dropout(proj_drop) | |
| def forward(self, x_q, x_kv, attn_mask=None, *, q_grid_size=None, kv_grid_size=None, | |
| q_rect=None, kv_rect=None, q_patch_coords=None, kv_patch_coords=None, | |
| q_num_front_tokens=None, kv_num_front_tokens=None): | |
| b, nq, _ = x_q.shape | |
| b_kv, nk, _ = x_kv.shape | |
| if b != b_kv: | |
| raise ValueError(f"x_q and x_kv must have the same batch size, got {b} and {b_kv}.") | |
| q_num_front_tokens = self.q_num_front_tokens if q_num_front_tokens is None else q_num_front_tokens | |
| kv_num_front_tokens = self.kv_num_front_tokens if kv_num_front_tokens is None else kv_num_front_tokens | |
| q_num_patch_tokens = nq - q_num_front_tokens | |
| kv_num_patch_tokens = nk - kv_num_front_tokens | |
| if q_num_patch_tokens <= 0: | |
| raise ValueError(f"x_q has no patch tokens after {q_num_front_tokens} front tokens.") | |
| if kv_num_patch_tokens <= 0: | |
| raise ValueError(f"x_kv has no patch tokens after {kv_num_front_tokens} front tokens.") | |
| coord_dtype = torch.float32 | |
| q_coords_yx = make_stream_patch_coords(batch_size=b, num_patch_tokens=q_num_patch_tokens, grid_size=q_grid_size, | |
| rect=q_rect, patch_coords=q_patch_coords, device=x_q.device, | |
| dtype=coord_dtype, align_corners=self.align_corners, name="q") | |
| kv_coords_yx = make_stream_patch_coords(batch_size=b, num_patch_tokens=kv_num_patch_tokens, grid_size=kv_grid_size, | |
| rect=kv_rect, patch_coords=kv_patch_coords, device=x_kv.device, | |
| dtype=coord_dtype, align_corners=self.align_corners, name="kv") | |
| q = self.q(x_q).reshape(b, nq, self.num_heads, self.head_dim).transpose(1, 2) | |
| k = self.k(x_kv).reshape(b, nk, self.num_heads, self.head_dim).transpose(1, 2) | |
| v = self.v(x_kv).reshape(b, nk, self.num_heads, self.head_dim).transpose(1, 2) | |
| q = self.q_norm(q); k = self.k_norm(k) | |
| q, k = self.rope(q, k, q_coords_yx=q_coords_yx, kv_coords_yx=kv_coords_yx, | |
| q_num_front_tokens=q_num_front_tokens, kv_num_front_tokens=kv_num_front_tokens) | |
| if self.fused_attn: | |
| x = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask, | |
| dropout_p=self.attn_drop.p if self.training else 0.0) | |
| else: | |
| q = q * self.scale | |
| attn = q @ k.transpose(-2, -1) | |
| attn_bias = _cross_attn_mask(attn_mask, attn.dtype) | |
| if attn_bias is not None: | |
| attn = attn + attn_bias | |
| attn = attn.softmax(dim=-1) | |
| attn = self.attn_drop(attn) | |
| x = attn @ v | |
| x = x.transpose(1, 2).reshape(b, nq, self.attn_dim) | |
| x = self.norm(x) | |
| x = self.proj(x) | |
| x = self.proj_drop(x) | |
| return x | |
| class AxialRoPECrossAttentionBlock(nn.Module): | |
| def __init__(self, dim, num_heads, qkv_bias=False, qk_norm=False, scale_attn_norm=False, | |
| proj_bias=True, proj_drop=0.0, attn_drop=0.0, init_values=None, drop_path=0.0, | |
| norm_layer=LayerNorm, q_num_front_tokens=0, kv_num_front_tokens=0, rope_base=100.0, | |
| rope_dim=None, align_corners=False, device=None, dtype=None): | |
| super().__init__() | |
| dd = {"device": device, "dtype": dtype} | |
| self.norm_q = norm_layer(dim, **dd) | |
| self.norm_kv = norm_layer(dim, **dd) | |
| self.attn = AxialRoPECrossAttention(dim=dim, num_heads=num_heads, qkv_bias=qkv_bias, qk_norm=qk_norm, | |
| scale_norm=scale_attn_norm, proj_bias=proj_bias, attn_drop=attn_drop, | |
| proj_drop=proj_drop, norm_layer=norm_layer, q_num_front_tokens=q_num_front_tokens, | |
| kv_num_front_tokens=kv_num_front_tokens, rope_base=rope_base, rope_dim=rope_dim, | |
| align_corners=align_corners, **dd) | |
| self.ls1 = LayerScale(dim, init_values=init_values, **dd) if init_values is not None else nn.Identity() | |
| self.drop_path1 = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() | |
| def forward(self, x_q, x_kv, attn_mask=None, *, q_grid_size=None, kv_grid_size=None, | |
| q_rect=None, kv_rect=None, q_patch_coords=None, kv_patch_coords=None, | |
| q_num_front_tokens=None, kv_num_front_tokens=None): | |
| x_q = x_q + self.drop_path1(self.ls1(self.attn( | |
| self.norm_q(x_q), self.norm_kv(x_kv), attn_mask=attn_mask, q_grid_size=q_grid_size, | |
| kv_grid_size=kv_grid_size, q_rect=q_rect, kv_rect=kv_rect, q_patch_coords=q_patch_coords, | |
| kv_patch_coords=kv_patch_coords, q_num_front_tokens=q_num_front_tokens, | |
| kv_num_front_tokens=kv_num_front_tokens))) | |
| return x_q | |
| # =========================================================================== # | |
| # Block factories + SceneHeadInteraction (from gazelle.cross_attention_model) # | |
| # =========================================================================== # | |
| def get_vit_block(dim=256, num_heads=8, mlp_ratio=4, mlp_layer=SwiGLU, drop_path=0.1, | |
| act_layer=nn.GELU, pos_encoding="rope", num_front_tokens=4, rope_base=100.0): | |
| if pos_encoding not in {"rope", "sinusoidal", "ape"}: | |
| raise ValueError(f"pos_encoding must be one of: rope, sinusoidal, ape, got {pos_encoding}") | |
| if pos_encoding == "rope": | |
| return AxialRoPEBlock(dim=dim, num_heads=num_heads, mlp_ratio=mlp_ratio, mlp_layer=mlp_layer, | |
| drop_path=drop_path, act_layer=act_layer, num_front_tokens=num_front_tokens, rope_base=rope_base) | |
| else: | |
| return Block(dim=dim, num_heads=num_heads, mlp_ratio=mlp_ratio, mlp_layer=mlp_layer, | |
| drop_path=drop_path, act_layer=act_layer) | |
| def get_cross_attn_block(dim=256, num_heads=8, drop_path=0.1, pos_encoding="rope", | |
| q_num_front_tokens=0, kv_num_front_tokens=0, rope_base=100.0): | |
| if pos_encoding not in {"rope", "sinusoidal", "ape"}: | |
| raise ValueError(f"pos_encoding must be one of: rope, sinusoidal, ape, got {pos_encoding}") | |
| if pos_encoding == "rope": | |
| return AxialRoPECrossAttentionBlock(dim=dim, num_heads=num_heads, drop_path=drop_path, | |
| q_num_front_tokens=q_num_front_tokens, kv_num_front_tokens=kv_num_front_tokens, | |
| rope_base=rope_base) | |
| else: | |
| return CrossAttentionBlock(dim=dim, num_heads=num_heads, drop_path=drop_path) | |
| class SceneHeadInteraction(nn.Module): | |
| """Variant A2: synchronous and symmetric feature interaction.""" | |
| def __init__(self, dim, num_heads=8, mlp_ratio=4, mlp_layer=SwiGLU, act_layer=nn.GELU, | |
| drop_path=0.0, num_front_tokens=0, pos_encoding="rope"): | |
| super().__init__() | |
| if pos_encoding not in {"rope", "sinusoidal", "ape"}: | |
| raise ValueError(f"pos_encoding must be one of: rope, sinusoidal, ape, got {pos_encoding}") | |
| self.pos_encoding = pos_encoding | |
| self.cross_attn_scene = get_cross_attn_block(dim=dim, num_heads=num_heads, pos_encoding=pos_encoding, | |
| q_num_front_tokens=num_front_tokens, kv_num_front_tokens=num_front_tokens, drop_path=drop_path) | |
| self.vit_block_scene = get_vit_block(dim=dim, num_heads=num_heads, mlp_ratio=mlp_ratio, mlp_layer=mlp_layer, | |
| drop_path=drop_path, act_layer=act_layer, num_front_tokens=num_front_tokens, pos_encoding=pos_encoding) | |
| self.cross_attn_head = get_cross_attn_block(dim=dim, num_heads=num_heads, pos_encoding=pos_encoding, | |
| q_num_front_tokens=num_front_tokens, kv_num_front_tokens=num_front_tokens, drop_path=drop_path) | |
| self.vit_block_head = get_vit_block(dim=dim, num_heads=num_heads, mlp_ratio=mlp_ratio, mlp_layer=mlp_layer, | |
| drop_path=drop_path, act_layer=act_layer, num_front_tokens=num_front_tokens, pos_encoding=pos_encoding) | |
| def forward(self, tokens): | |
| scene_tokens = tokens["scene_tokens"] | |
| head_tokens = tokens["head_tokens"] | |
| head_rects = tokens["head_rects"] | |
| if self.pos_encoding == "rope": | |
| out_scene_tokens = self.cross_attn_scene(scene_tokens, head_tokens, kv_rect=head_rects) | |
| out_head_tokens = self.cross_attn_head(head_tokens, scene_tokens, q_rect=head_rects) | |
| else: | |
| out_scene_tokens = self.cross_attn_scene(scene_tokens, head_tokens) | |
| out_head_tokens = self.cross_attn_head(head_tokens, scene_tokens) | |
| out_scene_tokens = self.vit_block_scene(out_scene_tokens) | |
| out_head_tokens = self.vit_block_head(out_head_tokens) | |
| return {"scene_tokens": out_scene_tokens, "head_tokens": out_head_tokens, "head_rects": head_rects} | |
| # =========================================================================== # | |
| # DINOv3 backbone wrapper (config-only construction, weights from safetensors) # | |
| # =========================================================================== # | |
| class PaGEBackbone(nn.Module): | |
| """ | |
| Wraps a transformers built-in DINOv3ViTModel. Output: patch tokens -> [B, C, H', W']. | |
| The DINOv3 model is built from config only (no external download). | |
| """ | |
| def __init__(self, dinov3_config: DINOv3ViTConfig, in_size=(512, 512)): | |
| super().__init__() | |
| self.in_size = in_size | |
| self.model = DINOv3ViTModel(dinov3_config) | |
| self.patch_size = int(dinov3_config.patch_size) | |
| self.embed_dim = int(dinov3_config.hidden_size) | |
| # CLS(1) + num_register_tokens | |
| self._num_front = 1 + int(getattr(dinov3_config, "num_register_tokens", 0) or 0) | |
| # DINOv3ViTModel's internal naming differs across transformers versions: | |
| # 4.56.x -> layer stack flattened: model.layer.N.* (model.<dinov3 top-level>) | |
| # 5.6.x -> layer stack nested: model.model.layer.N.* (extra `.model` wrapper) | |
| # A single safetensors file must load under both, so remap incoming keys at load time. | |
| self._register_load_state_dict_pre_hook(self._remap_dinov3_keys) | |
| def _dinov3_has_nested_layer(dinov3_module) -> bool: | |
| """True if this transformers version nests the layer stack under an inner `.model` | |
| (transformers >= 5.x). In 4.56.x the layers are flattened onto the DINOv3ViTModel itself.""" | |
| inner = getattr(dinov3_module, "model", None) | |
| if not isinstance(inner, nn.Module): | |
| return False | |
| # inner's own keys are relative to it: "layer.0.*" when nested, never "embeddings.*". | |
| return any(k.startswith("layer.") for k in inner.state_dict().keys()) | |
| def _remap_dinov3_keys(self, state_dict, prefix, *args, **kwargs): | |
| """Normalize DINOv3 backbone keys (embeddings / layer / norm / rope_embeddings) | |
| from whichever convention the checkpoint uses into the one this transformers | |
| version expects.""" | |
| nested = self._dinov3_has_nested_layer(self.model) | |
| model_pref = prefix + "model." | |
| new = {} | |
| for k in list(state_dict.keys()): | |
| if not k.startswith(model_pref): | |
| continue | |
| rest = k[len(model_pref):] # after "<prefix>model." | |
| # rest is one of: "embeddings...", "norm...", "rope_embeddings...", "layer...", | |
| # or "model.layer..." (nested-conv checkpoint under a flat version, etc.) | |
| if rest.startswith("model.layer."): | |
| core = rest[len("model."):] # -> "layer..." | |
| else: | |
| core = rest # "layer..." / "embeddings..." / "norm..." / "rope_embeddings..." | |
| if nested and core.startswith("layer."): | |
| target = model_pref + "model." + core | |
| else: | |
| target = model_pref + core | |
| if target != k: | |
| new[target] = state_dict.pop(k) | |
| state_dict.update(new) | |
| def _get_patch_tokens(self, x: torch.Tensor) -> torch.Tensor: | |
| out = self.model(pixel_values=x, return_dict=True) | |
| tokens = getattr(out, "last_hidden_state", None) | |
| if tokens is None: | |
| tokens = out[0] | |
| if not torch.is_tensor(tokens) or tokens.dim() != 3: | |
| raise RuntimeError("Unexpected DINOv3 output format.") | |
| tokens = tokens[:, self._num_front:, :] # drop CLS + register tokens | |
| return tokens | |
| def forward(self, x) -> torch.Tensor: | |
| if isinstance(x, (list, tuple)): | |
| # head stream comes in as a 1-element list (one backbone branch); unwrap it | |
| assert len(x) == 1 | |
| x = x[0] | |
| b, c, h, w = x.shape | |
| out_h, out_w = self.get_out_size((h, w)) | |
| patch_tokens = self._get_patch_tokens(x) | |
| if patch_tokens.shape[1] != out_h * out_w: | |
| raise RuntimeError( | |
| f"[PaGEBackbone] token count mismatch: {patch_tokens.shape[1]} vs {out_h * out_w}. " | |
| f"patch_size={self.patch_size}, input={(h, w)}") | |
| feat = patch_tokens.view(b, out_h, out_w, -1).permute(0, 3, 1, 2).contiguous() | |
| return feat | |
| def get_dimension(self): | |
| return self.embed_dim | |
| def get_out_size(self, in_size): | |
| h, w = in_size | |
| return (h // self.patch_size, w // self.patch_size) | |
| # =========================================================================== # | |
| # PaGE config # | |
| # =========================================================================== # | |
| class PaGEConfig(PretrainedConfig): | |
| model_type = "page" | |
| def __init__( | |
| self, | |
| # gaze decoder | |
| dim: int = 256, | |
| num_heads: int = 8, | |
| mlp_ratio: float = 4.0, | |
| mlp_layer: str = "geglu", | |
| pos_encoding: str = "rope", | |
| n_scene_self_attn_layers: int = 1, | |
| n_head_self_attn_layers: int = 1, | |
| n_scene_head_interaction_layers: int = 5, | |
| n_reg_tokens: int = 4, | |
| heatmap_out_size: Tuple[int, int] = (64, 64), | |
| dino_feature_dropout: float = 0.1, | |
| drop_path: float = 0.1, | |
| use_head_prompt: bool = False, | |
| inout: bool = True, | |
| # input sizes | |
| scene_in_size: Tuple[int, int] = (512, 512), | |
| head_in_size: Tuple[int, int] = (256, 256), | |
| # image preprocessing | |
| image_mean: Tuple[float, float, float] = (0.485, 0.456, 0.406), | |
| image_std: Tuple[float, float, float] = (0.229, 0.224, 0.225), | |
| # DINOv3 backbone config (shared by scene & head branches) | |
| dinov3_hidden_size: int = 768, | |
| dinov3_num_hidden_layers: int = 12, | |
| dinov3_num_attention_heads: int = 12, | |
| dinov3_intermediate_size: int = 3072, | |
| dinov3_num_register_tokens: int = 4, | |
| dinov3_patch_size: int = 16, | |
| dinov3_use_gated_mlp: bool = False, | |
| dinov3_layerscale_value: float = 1.0, | |
| dinov3_drop_path_rate: float = 0.0, | |
| dinov3_layer_norm_eps: float = 1e-5, | |
| **kwargs, | |
| ): | |
| self.dim = dim | |
| self.num_heads = num_heads | |
| self.mlp_ratio = mlp_ratio | |
| self.mlp_layer = mlp_layer | |
| self.pos_encoding = pos_encoding | |
| self.n_scene_self_attn_layers = n_scene_self_attn_layers | |
| self.n_head_self_attn_layers = n_head_self_attn_layers | |
| self.n_scene_head_interaction_layers = n_scene_head_interaction_layers | |
| self.n_reg_tokens = n_reg_tokens | |
| self.heatmap_out_size = tuple(heatmap_out_size) | |
| self.dino_feature_dropout = dino_feature_dropout | |
| self.drop_path = drop_path | |
| self.use_head_prompt = use_head_prompt | |
| self.inout = inout | |
| self.scene_in_size = tuple(scene_in_size) | |
| self.head_in_size = tuple(head_in_size) | |
| self.image_mean = tuple(image_mean) | |
| self.image_std = tuple(image_std) | |
| self.dinov3_hidden_size = dinov3_hidden_size | |
| self.dinov3_num_hidden_layers = dinov3_num_hidden_layers | |
| self.dinov3_num_attention_heads = dinov3_num_attention_heads | |
| self.dinov3_intermediate_size = dinov3_intermediate_size | |
| self.dinov3_num_register_tokens = dinov3_num_register_tokens | |
| self.dinov3_patch_size = dinov3_patch_size | |
| self.dinov3_use_gated_mlp = dinov3_use_gated_mlp | |
| self.dinov3_layerscale_value = dinov3_layerscale_value | |
| self.dinov3_drop_path_rate = dinov3_drop_path_rate | |
| self.dinov3_layer_norm_eps = dinov3_layer_norm_eps | |
| super().__init__(**kwargs) | |
| def to_dinov3_config(self) -> DINOv3ViTConfig: | |
| return DINOv3ViTConfig( | |
| hidden_size=self.dinov3_hidden_size, | |
| num_hidden_layers=self.dinov3_num_hidden_layers, | |
| num_attention_heads=self.dinov3_num_attention_heads, | |
| intermediate_size=self.dinov3_intermediate_size, | |
| num_register_tokens=self.dinov3_num_register_tokens, | |
| patch_size=self.dinov3_patch_size, | |
| use_gated_mlp=self.dinov3_use_gated_mlp, | |
| layerscale_value=self.dinov3_layerscale_value, | |
| drop_path_rate=self.dinov3_drop_path_rate, | |
| layer_norm_eps=self.dinov3_layer_norm_eps, | |
| image_size=self.scene_in_size[0], | |
| ) | |
| # =========================================================================== # | |
| # PaGE model (CrossGaze architecture) # | |
| # =========================================================================== # | |
| class PaGEPreTrainedModel(PreTrainedModel): | |
| config_class = PaGEConfig | |
| base_model_prefix = "page" | |
| supports_gradient_checkpointing = False | |
| def _init_weights(self, module): | |
| if isinstance(module, nn.Linear): | |
| nn.init.trunc_normal_(module.weight, std=0.02) | |
| if module.bias is not None: | |
| nn.init.zeros_(module.bias) | |
| elif isinstance(module, nn.Conv2d): | |
| nn.init.trunc_normal_(module.weight, std=0.02) | |
| if module.bias is not None: | |
| nn.init.zeros_(module.bias) | |
| elif isinstance(module, nn.Embedding): | |
| nn.init.trunc_normal_(module.weight, std=0.02) | |
| elif isinstance(module, nn.LayerNorm): | |
| nn.init.zeros_(module.bias) | |
| nn.init.ones_(module.weight) | |
| # ------------------------------------------------------------------ # | |
| # Version-safe loading # | |
| # ------------------------------------------------------------------ # | |
| # The DINOv3 backbones are built from transformers' built-in DINOv3ViTModel, whose | |
| # internal parameter naming changed between transformers 4.56.x (layers flattened: | |
| # `model.layer.N`) and 5.x (layers nested: `model.model.layer.N`). The checkpoints store | |
| # one convention; loading under the other leaves the backbone randomly initialized. | |
| # `from_pretrained` in transformers >= 5 bypasses `nn.Module._load_state_dict_pre_hook`, | |
| # so the remap hook on PaGEBackbone is not invoked by it. We therefore reload the backbone | |
| # weights ourselves through `nn.Module.load_state_dict` (which *does* fire the hook). | |
| def _collect_safetensors(path_or_repo, **kwargs): | |
| """Return the full state_dict from a local dir or a HF repo id.""" | |
| import os as _os | |
| import glob as _glob | |
| from safetensors.torch import load_file as _load_file | |
| state = {} | |
| if _os.path.isdir(path_or_repo): | |
| index = _os.path.join(path_or_repo, "model.safetensors.index.json") | |
| if _os.path.isfile(index): | |
| import json as _json | |
| wm = _json.load(open(index))["weight_map"] | |
| files = sorted(set(wm.values())) | |
| else: | |
| files = ["model.safetensors"] | |
| for f in files: | |
| state.update(_load_file(_os.path.join(path_or_repo, f))) | |
| else: | |
| from huggingface_hub import hf_hub_download | |
| import json as _json | |
| repo_id = path_or_repo | |
| try: | |
| idx_path = hf_hub_download(repo_id=repo_id, filename="model.safetensors.index.json") | |
| wm = _json.load(open(idx_path))["weight_map"] | |
| files = sorted(set(wm.values())) | |
| except Exception: | |
| files = ["model.safetensors"] | |
| for f in files: | |
| p = hf_hub_download(repo_id=repo_id, filename=f) | |
| state.update(_load_file(p)) | |
| return state | |
| def from_pretrained(cls, *args, **kwargs): | |
| model = super().from_pretrained(*args, **kwargs) | |
| # Reload DINOv3 backbone weights version-safely (the remap hook fires here). | |
| try: | |
| path_or_repo = args[0] if args else kwargs.get("pretrained_model_name_or_path") | |
| full_state = cls._collect_safetensors(path_or_repo) | |
| for branch in ("scene_branch_backbone", "head_branch_backbone"): | |
| if not hasattr(model, branch): | |
| continue | |
| bb = getattr(model, branch) | |
| bb_state = {k[len(branch) + 1:]: v for k, v in full_state.items() | |
| if k.startswith(branch + ".")} | |
| if bb_state: | |
| bb.load_state_dict(bb_state, strict=False) | |
| # Recompute RoPE inv_freq buffers: they are non-persistent and transformers' meta-init | |
| # during from_pretrained leaves them as garbage, which would corrupt axial RoPE. | |
| for m in model.modules(): | |
| if hasattr(m, "reset_inv_freq") and callable(m.reset_inv_freq): | |
| m.reset_inv_freq() | |
| except Exception as e: # pragma: no cover | |
| import warnings | |
| warnings.warn(f"PaGE: version-safe backbone reload skipped ({e!r}). " | |
| "Backbone weights may be random if your transformers version mismatches the checkpoint.") | |
| return model | |
| class PaGEModel(PaGEPreTrainedModel): | |
| """ | |
| PaGE gaze target estimation model with ViT-adapter-style cross attention between scene and head features. | |
| Inputs (dict): | |
| - "images": scene image tensor [B, 3, H_scene, W_scene] | |
| - "head_images": list of head-crop tensors, one tensor [sum(Np), 3, H_head, W_head] | |
| - "bboxes": list (len B) of lists of bboxes; each bbox is (xmin, ymin, xmax, ymax) in [0,1] image coords | |
| Outputs (dict): | |
| - "heatmap": list (len B) of [Np, H_out, W_out] heatmaps (sigmoid applied) | |
| - "inout": list (len B) of [Np] in/out scores (sigmoid applied) if inout else None | |
| """ | |
| def __init__(self, config: PaGEConfig): | |
| super().__init__(config) | |
| cfg = config | |
| dinov3_cfg = cfg.to_dinov3_config() | |
| self.scene_branch_backbone = PaGEBackbone(dinov3_cfg, in_size=cfg.scene_in_size) | |
| self.head_branch_backbone = PaGEBackbone(dinov3_cfg, in_size=cfg.head_in_size) | |
| self.dim = cfg.dim | |
| self.n_scene_self_attn_layers = cfg.n_scene_self_attn_layers | |
| self.n_head_self_attn_layers = cfg.n_head_self_attn_layers | |
| self.n_scene_head_interaction_layers = cfg.n_scene_head_interaction_layers | |
| self.scene_featmap_h, self.scene_featmap_w = self.scene_branch_backbone.get_out_size(cfg.scene_in_size) | |
| self.head_featmap_h, self.head_featmap_w = self.head_branch_backbone.get_out_size(cfg.head_in_size) | |
| self.n_reg_tokens = cfg.n_reg_tokens | |
| self.n_front_tokens = cfg.n_reg_tokens + 1 if cfg.inout else cfg.n_reg_tokens | |
| self.heatmap_out_size = tuple(cfg.heatmap_out_size) | |
| self.inout = cfg.inout | |
| self.pos_encoding = cfg.pos_encoding | |
| self.use_head_prompt = cfg.use_head_prompt | |
| self.scene_proj = nn.Sequential( | |
| nn.Dropout2d(cfg.dino_feature_dropout), | |
| nn.Conv2d(self.scene_branch_backbone.get_dimension(), self.dim, 1), | |
| ) | |
| self.head_proj = nn.Sequential( | |
| nn.Dropout2d(cfg.dino_feature_dropout), | |
| nn.Conv2d(self.head_branch_backbone.get_dimension(), self.dim, 1), | |
| ) | |
| if self.use_head_prompt: | |
| self.head_position_token = nn.Embedding(1, self.dim) | |
| if self.pos_encoding == "ape": | |
| self.scene_seq_len = self.n_reg_tokens + self.scene_featmap_h * self.scene_featmap_w | |
| self.head_seq_len = self.n_reg_tokens + self.head_featmap_h * self.head_featmap_w | |
| self.scene_ape = nn.Parameter(torch.zeros((1, self.scene_seq_len, self.dim))) | |
| self.head_ape = nn.Parameter(torch.zeros((1, self.head_seq_len, self.dim))) | |
| elif self.pos_encoding == "sinusoidal": | |
| self.register_buffer("scene_pos_embed", positionalencoding2d(self.dim, self.scene_featmap_h, self.scene_featmap_w).squeeze(0).squeeze(0)) | |
| self.register_buffer("head_pos_embed", positionalencoding2d(self.dim, self.head_featmap_h, self.head_featmap_w).squeeze(0).squeeze(0)) | |
| if self.inout: | |
| self.scene_inout_token = nn.Parameter(torch.zeros((1, 1, self.dim))) | |
| self.head_inout_token = nn.Parameter(torch.zeros((1, 1, self.dim))) | |
| if self.n_reg_tokens > 0: | |
| self.scene_register_tokens = nn.Parameter(torch.zeros((1, self.n_reg_tokens, self.dim))) | |
| self.head_register_tokens = nn.Parameter(torch.zeros((1, self.n_reg_tokens, self.dim))) | |
| if cfg.mlp_layer == "mlp": | |
| mlp_layer = Mlp; act_layer = nn.GELU | |
| elif cfg.mlp_layer == "geglu": | |
| mlp_layer = SwiGLU; act_layer = nn.GELU | |
| elif cfg.mlp_layer == "swiglu": | |
| mlp_layer = SwiGLU; act_layer = nn.SiLU | |
| else: | |
| raise ValueError(f"mlp_layer must be mlp/geglu/swiglu, got {cfg.mlp_layer}") | |
| self.scene_self_attn_layers = nn.Sequential(*[ | |
| get_vit_block(dim=self.dim, num_heads=cfg.num_heads, mlp_ratio=cfg.mlp_ratio, mlp_layer=mlp_layer, | |
| drop_path=cfg.drop_path, act_layer=act_layer, num_front_tokens=self.n_front_tokens, pos_encoding=cfg.pos_encoding) | |
| for _ in range(cfg.n_scene_self_attn_layers) | |
| ]) if cfg.n_scene_self_attn_layers > 0 else nn.Identity() | |
| self.head_self_attn_layers = nn.Sequential(*[ | |
| get_vit_block(dim=self.dim, num_heads=cfg.num_heads, mlp_ratio=cfg.mlp_ratio, mlp_layer=mlp_layer, | |
| drop_path=cfg.drop_path, act_layer=act_layer, num_front_tokens=self.n_front_tokens, pos_encoding=cfg.pos_encoding) | |
| for _ in range(cfg.n_head_self_attn_layers) | |
| ]) if cfg.n_head_self_attn_layers > 0 else nn.Identity() | |
| self.scene_head_interaction_layers = nn.Sequential(*[ | |
| SceneHeadInteraction(dim=self.dim, num_heads=cfg.num_heads, mlp_ratio=cfg.mlp_ratio, mlp_layer=mlp_layer, | |
| drop_path=cfg.drop_path, act_layer=act_layer, pos_encoding=cfg.pos_encoding, num_front_tokens=self.n_front_tokens) | |
| for _ in range(cfg.n_scene_head_interaction_layers) | |
| ]) | |
| self.heatmap_head = nn.Sequential( | |
| nn.ConvTranspose2d(self.dim, self.dim, kernel_size=2, stride=2), | |
| nn.Conv2d(self.dim, 1, kernel_size=1, bias=False), | |
| ) | |
| if self.inout: | |
| self.inout_head = nn.Sequential( | |
| nn.Linear(self.dim * 2, 128), | |
| nn.GELU(), | |
| nn.Dropout(0.1), | |
| nn.Linear(128, 1), | |
| ) | |
| self.post_init() | |
| # ------------------------------------------------------------------ # | |
| def get_input_head_maps(self, bboxes): | |
| head_maps = [] | |
| head_rects = [] | |
| for bbox_list in bboxes: | |
| img_head_maps = [] | |
| img_head_rects = [] | |
| for bbox in bbox_list: | |
| if bbox is None: | |
| img_head_maps.append(torch.zeros(self.scene_featmap_h, self.scene_featmap_w)) | |
| else: | |
| xmin, ymin, xmax, ymax = bbox | |
| width, height = self.scene_featmap_w, self.scene_featmap_h | |
| xmin = round(xmin * width); ymin = round(ymin * height) | |
| xmax = round(xmax * width); ymax = round(ymax * height) | |
| head_map = torch.zeros((height, width)) | |
| head_map[ymin:ymax, xmin:xmax] = 1 | |
| img_head_maps.append(head_map) | |
| img_head_rects.append(torch.Tensor([ymin, xmin, ymax, xmax])) | |
| head_maps.append(torch.stack(img_head_maps)) | |
| head_rects.append(torch.stack(img_head_rects)) | |
| return head_maps, head_rects | |
| def get_logits(self, input, return_tokens=False): | |
| num_ppl_per_img = [len(bbox_list) for bbox_list in input["bboxes"]] | |
| for head_stream_images in input["head_images"]: | |
| if sum(num_ppl_per_img) != len(head_stream_images): | |
| raise ValueError(f"bboxes and head crops mismatch: {sum(num_ppl_per_img)} bboxes vs {len(head_stream_images)} head crops.") | |
| scene_featmap = self.scene_branch_backbone(input["images"]) | |
| scene_featmap = self.scene_proj(scene_featmap) | |
| scene_dino_tokens = scene_featmap.flatten(start_dim=2).permute(0, 2, 1) | |
| if self.pos_encoding == "sinusoidal": | |
| scene_featmap = scene_featmap + self.scene_pos_embed | |
| scene_featmap = repeat_tensors(scene_featmap, num_ppl_per_img) | |
| head_featmap = self.head_branch_backbone(input["head_images"]) | |
| head_featmap = self.head_proj(head_featmap) | |
| head_dino_tokens = head_featmap.flatten(start_dim=2).permute(0, 2, 1) | |
| if self.pos_encoding == "sinusoidal": | |
| head_featmap = head_featmap + self.head_pos_embed | |
| head_maps, head_rects = self.get_input_head_maps(input["bboxes"]) | |
| head_maps = torch.cat(head_maps, dim=0).to(scene_featmap.device) | |
| head_rects = torch.cat(head_rects, dim=0).to(scene_featmap.device) | |
| if self.use_head_prompt: | |
| head_map_embeddings = head_maps.unsqueeze(dim=1) * self.head_position_token.weight.unsqueeze(-1).unsqueeze(-1) | |
| scene_featmap = scene_featmap + head_map_embeddings | |
| scene_tokens = scene_featmap.flatten(start_dim=2).permute(0, 2, 1) | |
| head_tokens = head_featmap.flatten(start_dim=2).permute(0, 2, 1) | |
| if self.n_reg_tokens > 0: | |
| scene_tokens = torch.cat([self.scene_register_tokens.expand(sum(num_ppl_per_img), -1, -1), scene_tokens], dim=1) | |
| head_tokens = torch.cat([self.head_register_tokens.expand(sum(num_ppl_per_img), -1, -1), head_tokens], dim=1) | |
| if self.inout: | |
| scene_tokens = torch.cat([self.scene_inout_token.expand(sum(num_ppl_per_img), -1, -1), scene_tokens], dim=1) | |
| head_tokens = torch.cat([self.head_inout_token.expand(sum(num_ppl_per_img), -1, -1), head_tokens], dim=1) | |
| if self.pos_encoding == "ape": | |
| scene_tokens = scene_tokens + self.scene_ape.expand(sum(num_ppl_per_img), -1, -1) | |
| head_tokens = head_tokens + self.head_ape.expand(sum(num_ppl_per_img), -1, -1) | |
| scene_tokens = self.scene_self_attn_layers(scene_tokens) | |
| head_tokens = self.head_self_attn_layers(head_tokens) | |
| tokens = self.scene_head_interaction_layers({"scene_tokens": scene_tokens, "head_tokens": head_tokens, "head_rects": head_rects}) | |
| scene_tokens = tokens["scene_tokens"][:, self.n_front_tokens:, :] | |
| scene_inout_token = tokens["scene_tokens"][:, 0, :] | |
| head_inout_token = tokens["head_tokens"][:, 0, :] | |
| if self.inout: | |
| inout_features = torch.cat((scene_inout_token, head_inout_token), dim=1) | |
| inout_preds = self.inout_head(inout_features).squeeze(dim=-1) | |
| inout_preds = split_tensors(inout_preds, num_ppl_per_img) | |
| scene_featmap = scene_tokens.reshape(scene_tokens.shape[0], self.scene_featmap_h, self.scene_featmap_w, | |
| scene_tokens.shape[2]).permute(0, 3, 1, 2) | |
| heatmap = self.heatmap_head(scene_featmap).squeeze(dim=1) | |
| heatmap = torchvision.transforms.functional.resize(heatmap, self.heatmap_out_size) | |
| heatmap_preds = split_tensors(heatmap, num_ppl_per_img) | |
| if return_tokens: | |
| return {"scene_tokens": tokens["scene_tokens"], "head_tokens": tokens["head_tokens"], | |
| "scene_dino_tokens": scene_dino_tokens, "head_dino_tokens": head_dino_tokens, | |
| "heatmap": heatmap_preds, "inout": inout_preds if self.inout else None} | |
| return {"heatmap": heatmap_preds, "inout": inout_preds if self.inout else None} | |
| def forward(self, input): | |
| """Inference forward (applies sigmoid). Do NOT use for training (numerical stability).""" | |
| logits = self.get_logits(input) | |
| heatmap_preds = [torch.sigmoid(h) for h in logits["heatmap"]] | |
| inout_preds = [torch.sigmoid(i) for i in logits["inout"]] if logits["inout"] is not None else None | |
| return {"heatmap": heatmap_preds, "inout": inout_preds if self.inout else None} | |
| # =========================================================================== # | |
| # Image processor (dual-stream: scene + per-person head crops) # | |
| # =========================================================================== # | |
| class PaGEImageProcessor(BaseImageProcessor): | |
| """ | |
| Produces the input dict expected by PaGEModel.forward from a scene image + head crops. | |
| Convenience: | |
| proc = AutoImageProcessor.from_pretrained(repo, trust_remote_code=True) | |
| inputs = proc(scene_pil, head_crops=[pil0, pil1, ...], bboxes=[[(xmin,ymin,xmax,ymax), ...]]) | |
| out = model(inputs) | |
| """ | |
| model_input_names = ["pixel_values"] | |
| def __init__(self, scene_size=(512, 512), head_size=(256, 256), | |
| image_mean=(0.485, 0.456, 0.406), image_std=(0.229, 0.224, 0.225), | |
| resample=2, **kwargs): | |
| super().__init__(**kwargs) | |
| self.scene_size = tuple(scene_size) | |
| self.head_size = tuple(head_size) | |
| self.image_mean = tuple(image_mean) | |
| self.image_std = tuple(image_std) | |
| self.resample = resample | |
| def from_dict(cls, image_processor_dict, **kwargs): | |
| return cls(**{**image_processor_dict, **kwargs}) | |
| def _to_tensor(self, pil_img, size): | |
| import numpy as np | |
| from PIL import Image | |
| if not isinstance(pil_img, Image.Image): | |
| pil_img = to_pil_image(pil_img) | |
| pil_img = pil_img.convert("RGB").resize((size[1], size[0]), self.resample) # PIL resize is (W, H) | |
| arr = np.asarray(pil_img, dtype=np.float32) / 255.0 # H, W, 3 | |
| arr = (arr - np.array(self.image_mean, dtype=np.float32)) / np.array(self.image_std, dtype=np.float32) | |
| arr = np.transpose(arr, (2, 0, 1)) # 3, H, W | |
| return torch.from_numpy(arr) | |
| def preprocess(self, scene_image, head_crops=None, bboxes=None, **kwargs): | |
| """ | |
| scene_image: PIL image (or tensor) of the full scene. | |
| head_crops: list of PIL images, one per person (length == total bboxes across scene). | |
| Pass [None] * Np if you only have bboxes (zero head maps); but a real crop is expected. | |
| bboxes: list of bbox lists, one per scene image. Each bbox: (xmin, ymin, xmax, ymax) in [0,1]. | |
| Returns: dict with "images", "head_images", "bboxes" ready for PaGEModel.forward. | |
| """ | |
| if bboxes is None: | |
| raise ValueError("bboxes is required.") | |
| scene_tensor = self._to_tensor(scene_image, self.scene_size).unsqueeze(0) # 1, 3, H, W | |
| head_tensors = [self._to_tensor(hc, self.head_size) for hc in (head_crops or [])] | |
| if head_tensors: | |
| head_batch = torch.stack(head_tensors, dim=0) | |
| else: | |
| head_batch = torch.zeros(0, 3, *self.head_size) | |
| return {"images": scene_tensor, "head_images": [head_batch], "bboxes": bboxes} | |