import torch import torch.nn.functional as F import torch.utils.checkpoint # flash-attn is a hard dependency in the training / NVIDIA-container environment, # but it is unavailable (or ABI/arch-incompatible) on some inference targets such # as HF Spaces ZeroGPU (torch 2.8+, Blackwell sm_120). Soft-import it and fall # back to torch SDPA, which is numerically equivalent for our usage (non-causal, # no dropout, default scale = 1/sqrt(head_dim)). import os try: import flash_attn _HAS_FLASH = True except Exception: # ImportError or runtime/arch incompatibility on import flash_attn = None _HAS_FLASH = False # Allow forcing the SDPA path even when flash_attn is importable, so the # ZeroGPU code path can be validated on a flash-attn-equipped GPU node. if os.environ.get("DVSM_DISABLE_FLASH") == "1": _HAS_FLASH = False def _attn_func(q, k, v): """Drop-in replacement for ``flash_attn.flash_attn_func``. flash_attn layout: q/k/v are ``[B, L, num_head, head_dim]``, non-causal, softmax_scale = 1/sqrt(head_dim). torch SDPA expects ``[B, num_head, L, head_dim]`` and uses the same default scale + non-causal semantics, so we just transpose in and out. Output dtype matches the inputs (bf16/fp16), same as flash_attn. """ if _HAS_FLASH: return flash_attn.flash_attn_func(q, k, v) qh = q.transpose(1, 2) # [B, H, Lq, D] kh = k.transpose(1, 2) # [B, H, Lk, D] vh = v.transpose(1, 2) # [B, H, Lk, D] out = F.scaled_dot_product_attention(qh, kh, vh) # default scale, non-causal return out.transpose(1, 2).contiguous() # back to [B, Lq, H, D] def checkpoint_wrapper(module): # Wrap model forward function to automatically use activation checkpointing ori_forward = module.forward def new_forward(*args, **kwargs): if module.training: output = torch.utils.checkpoint.checkpoint( ori_forward, *args, **kwargs, use_reentrant=False) else: output = ori_forward(*args, **kwargs) return output return new_forward class Contiguous(torch.nn.Module): def forward(self, x): return x.contiguous() class MLP(torch.nn.Module): def __init__(self, dim, expand_ratio=4, bias=False, activation=torch.nn.GELU): """ Args: dim: Input dimension expand_ratio: Multiplier for hidden dimension bias: Whether to use bias in linear layers activation: Activation function """ super().__init__() hidden_dim = round(dim * expand_ratio) self.mlp = torch.nn.Sequential( torch.nn.Linear(dim, hidden_dim, bias=bias), activation(), torch.nn.Linear(hidden_dim, dim, bias=bias)) def forward(self, x): return self.mlp(x) class SwiGluMLP(torch.nn.Module): def __init__(self, dim, expand_ratio=4, bias=False): """ Args: dim: Input dimension expand_ratio: Multiplier for hidden dimension bias: Whether to use bias in linear layers activation: Activation function """ super().__init__() hidden_dim = round(dim * expand_ratio) self.mlp = torch.nn.Sequential( torch.nn.Linear(dim, hidden_dim, bias=bias), torch.nn.Linear(dim, hidden_dim, bias=bias), torch.nn.Linear(hidden_dim, dim, bias=bias)) def forward(self, x): gate = torch.nn.functional.silu(self.mlp[0](x), inplace=True) return self.mlp[2](gate * self.mlp[1](x)) class IntraViewAttention(torch.nn.Module): def __init__(self, dim, head_dim, bias=False, use_qk_norm=True, use_gating=False, use_sandwich=False, out_dim=None, ): """ dim: Input dimension head_dim: Dimension of each attention head bias: Whether to use bias in QKV and output projection use_qk_norm: Whether to use Q-K normalization use_gating: Whether to apply gating before output ffn """ super().__init__() assert dim % head_dim == 0, f"dim={dim} should be divisible by head_dim={head_dim}" num_head = dim // head_dim self.dim = dim self.out_dim = dim if out_dim is None else out_dim self.head_dim = head_dim self.num_head = num_head self.use_qk_norm = use_qk_norm self.use_gating = use_gating self.use_sandwich = use_sandwich self.attn_dtype = torch.bfloat16 if torch.cuda.get_device_capability()[0] >= 8 else torch.float16 self.to_qkv = torch.nn.Linear(dim, 3 * dim, bias=bias) self.fc = torch.nn.Linear(dim, self.out_dim, bias=bias) # Optional Q-K normalization if self.use_qk_norm: self.q_norm = torch.nn.RMSNorm(head_dim, eps=1e-5) self.k_norm = torch.nn.RMSNorm(head_dim, eps=1e-5) else: self.q_norm = torch.nn.Identity() self.k_norm = torch.nn.Identity() # Optional gating if self.use_gating: self.gate = torch.nn.Sequential( torch.nn.Linear(dim, dim, bias=False), torch.nn.Sigmoid()) # Optional normalization if self.use_sandwich: self.sandwich = torch.nn.LayerNorm(dim, bias=False) def forward(self, x): """ x: [B, L, D] """ # Predict qkv packed as (batch, seq_len, 3, num_head, head_dim) qkv = self.to_qkv(x).unflatten(2, (3, self.num_head, self.head_dim)) # Unpack (q, k, v) each as (batch, seq_len, num_head, head_dim) q, k, v = qkv.moveaxis(2,0).contiguous() # Apply qk normalization if enabled q = self.q_norm(q) k = self.k_norm(k) # Attention with torch.autocast(enabled=False, device_type="cuda"): q = q.to(self.attn_dtype) k = k.to(self.attn_dtype) v = v.to(self.attn_dtype) out = _attn_func(q, k, v) # Output layer to yield (batch, seq_len, dim) out = out.flatten(2,3) if self.use_gating: out = out * self.gate(x) out = self.fc(out) if self.use_sandwich: out = self.sandwich(out) return out class CrossViewAttention(torch.nn.Module): def __init__(self, dim, head_dim, bias=False, use_qk_norm=True, use_gating=False, use_sandwich=False, out_dim=None, ): """ Args: dim: Input dimension head_dim: Dimension of each attention head bias: Whether to use bias in QKV and output projection use_qk_norm: Whether to use Q-K normalization use_gating: Whether to apply gating before output ffn """ super().__init__() assert dim % head_dim == 0, f"dim={dim} should be divisible by head_dim={head_dim}" num_head = dim // head_dim self.dim = dim self.out_dim = dim if out_dim is None else out_dim self.head_dim = head_dim self.num_head = num_head self.use_qk_norm = use_qk_norm self.use_gating = use_gating self.use_sandwich = use_sandwich self.attn_dtype = torch.bfloat16 if torch.cuda.get_device_capability()[0] >= 8 else torch.float16 self.to_q = torch.nn.Linear(dim, dim, bias=bias) self.to_kv = torch.nn.Linear(dim, 2 * dim, bias=bias) self.fc = torch.nn.Linear(dim, self.out_dim, bias=bias) # Optional Q-K normalization if self.use_qk_norm: self.q_norm = torch.nn.RMSNorm(head_dim, eps=1e-5) self.k_norm = torch.nn.RMSNorm(head_dim, eps=1e-5) else: self.q_norm = torch.nn.Identity() self.k_norm = torch.nn.Identity() # Optional gating if self.use_gating: self.gate = torch.nn.Sequential( torch.nn.Linear(dim, dim, bias=False), torch.nn.Sigmoid()) # Optional normalization if self.use_sandwich: self.sandwich = torch.nn.LayerNorm(dim, bias=False) def forward(self, x, kv=None): """ x: [B, L, D] kv: ([B, M, D], [B, M, D]) If provided, x query on kv instead of doing self attention """ q = self.to_q(x) # To shape of (batch, seq_len, num_head, head_dim) q = q.unflatten(2, (self.num_head, self.head_dim)) q = self.q_norm(q) with torch.autocast(enabled=False, device_type="cuda"): q = q.to(self.attn_dtype) # Prepare (k, v) if kv is None: kv = self.to_kv(x) kv = kv.unflatten(2, (2, self.num_head, self.head_dim)) k, v = kv.moveaxis(2, 0).contiguous() k = self.k_norm(k) with torch.autocast(enabled=False, device_type="cuda"): k = k.to(self.attn_dtype) v = v.to(self.attn_dtype) else: k, v = kv assert len(x) == len(k) and len(x) == len(v) # Attention out = _attn_func(q, k, v) # Output layer into (batch, seq_len, dim) out = out.flatten(2, 3) if self.use_gating: out = out * self.gate(x) out = self.fc(out) if self.use_sandwich: out = self.sandwich(out) return out, (k, v)