Spaces:
Running on Zero
Running on Zero
Download src/models/multi_scale_predictor.py from hugging-apps/rigidformer: direct link, hf CLI and curl.
- Browser
- Download file 36.3 kB
-
https://huggingface.co/spaces/hugging-apps/rigidformer/resolve/main/src/models/multi_scale_predictor.py
- Command line
-
hf download hf://spaces/hugging-apps/rigidformer/src/models/multi_scale_predictor.py
-
curl -L -o multi_scale_predictor.py https://huggingface.co/spaces/hugging-apps/rigidformer/resolve/main/src/models/multi_scale_predictor.py
36.3 kB
| """ | |
| Multi-scale prediction head with cross-attention to decoder features. | |
| Query-based predictor using RoPE-aware attention for position encoding. | |
| """ | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from typing import List, Optional, Tuple | |
| # Import RoPE modules for position-aware attention | |
| from src.models.decoder import CrossAttentionWithRoPE, SelfAttentionWithRoPE | |
| class QueryBasedMultiScalePredictor(nn.Module): | |
| """ | |
| Query-based predictor with cross-attention to multi-scale features. | |
| Anchors attend to multi-scale decoder features instead of pooling them. | |
| """ | |
| def __init__( | |
| self, | |
| state_dim: int = 768, | |
| query_dim: int = 3, | |
| hidden_dim: int = 256, | |
| output_dim: int = 3, | |
| layer_indices: List[int] = [0, 6, 9, 12], | |
| num_heads: int = 8, | |
| dropout: float = 0.1, | |
| use_ref_pose: bool = False, # Whether to use reference pose | |
| norm_type: str = 'layer_norm', # 'layer_norm' | 'rms_norm' | |
| qk_norm: bool = False, # normalize q/k before attention | |
| fusion_activation: str = 'swiglu', # 'swiglu' | 'gelu' | 'silu' | |
| use_rope: bool = True, # Position-aware attention with RoPE | |
| rope_dim: int = None, # Custom RoPE dimension | |
| dim_per_coord_3d: int = None, # Dim per coordinate for single-point (3D) RoPE | |
| dim_per_coord_multi: int = None, # Dim per coordinate for multi-point RoPE | |
| layer_index_mapping: dict = None, # Maps original layer index to saved position | |
| elementwise_gate: bool = False, # Elementwise gated attention | |
| cross_object_attention: bool = False, # Cross-object anchor attention | |
| cross_object_kv: bool = False, # Cross-object key-value: anchors query all objects | |
| no_global_state: bool = False # Skip state modules entirely when True | |
| ): | |
| super().__init__() | |
| self.cross_object_attention = cross_object_attention | |
| self.cross_object_kv = cross_object_kv | |
| self.no_global_state = no_global_state | |
| self._cross_object_attention_logged = False # One-time log flag | |
| self._cross_object_kv_logged = False # One-time log flag | |
| self._p0_mask_logged = False # One-time padding mask log | |
| self._p1_no_state_logged = False # One-time no-state log | |
| if cross_object_attention: | |
| print(f"[Predictor] Cross-object anchor attention enabled: anchors from different objects can interact") | |
| if cross_object_kv: | |
| print(f"[Predictor] Cross-object key-value enabled: anchors query all objects' features") | |
| if no_global_state: | |
| print(f"[Predictor] no_global_state=True: state modules omitted, fusion uses object features only") | |
| self.layer_indices = layer_indices | |
| self.num_scales = len(layer_indices) | |
| # Maps original layer index to its position in the saved feature list. | |
| # If None, all layers are assumed saved. | |
| if layer_index_mapping is not None: | |
| self.layer_index_mapping = layer_index_mapping | |
| else: | |
| # Default: assume sequential saving (all layers saved) | |
| self.layer_index_mapping = {idx: idx for idx in layer_indices} | |
| self.use_ref_pose = use_ref_pose | |
| self.use_rope = use_rope | |
| self.hidden_dim = hidden_dim | |
| # Scale-specific projections for object features | |
| self.scale_projections = nn.ModuleList([ | |
| nn.Linear(state_dim, hidden_dim) for _ in range(self.num_scales) | |
| ]) | |
| # Scale-specific projections for state features (skipped when no_global_state) | |
| if not no_global_state: | |
| self.state_scale_projections = nn.ModuleList([ | |
| nn.Linear(state_dim, hidden_dim) for _ in range(self.num_scales) | |
| ]) | |
| # Query projection; query_dim already includes the ref-pose concatenation | |
| self.query_proj = nn.Linear(query_dim, hidden_dim) | |
| # Cross-attention to object features for each scale with RoPE | |
| if use_rope: | |
| self.cross_attentions = nn.ModuleList([ | |
| CrossAttentionWithRoPE( | |
| d_model=hidden_dim, | |
| num_heads=num_heads, | |
| dropout=dropout, | |
| use_rope=True, | |
| rope_dim=rope_dim, | |
| dim_per_coord_3d=dim_per_coord_3d, | |
| dim_per_coord_multi=dim_per_coord_multi, | |
| elementwise_gate=elementwise_gate | |
| ) for _ in range(self.num_scales) | |
| ]) | |
| # Cross-attention to state features (skipped when no_global_state) | |
| if not no_global_state: | |
| self.state_cross_attentions = nn.ModuleList([ | |
| CrossAttentionWithRoPE( | |
| d_model=hidden_dim, | |
| num_heads=num_heads, | |
| dropout=dropout, | |
| use_rope=True, | |
| rope_dim=rope_dim, | |
| dim_per_coord_3d=dim_per_coord_3d, | |
| dim_per_coord_multi=dim_per_coord_multi, | |
| elementwise_gate=elementwise_gate | |
| ) for _ in range(self.num_scales) | |
| ]) | |
| # Self-attention for anchors within each object (with RoPE) | |
| self.self_attentions = nn.ModuleList([ | |
| SelfAttentionWithRoPE( | |
| d_model=hidden_dim, | |
| num_heads=num_heads, | |
| dropout=dropout, | |
| use_rope=True, | |
| rope_dim=rope_dim, | |
| dim_per_coord_3d=dim_per_coord_3d, | |
| dim_per_coord_multi=dim_per_coord_multi, | |
| elementwise_gate=elementwise_gate | |
| ) for _ in range(self.num_scales) | |
| ]) | |
| else: | |
| # Standard attention | |
| self.cross_attentions = nn.ModuleList([ | |
| nn.MultiheadAttention( | |
| hidden_dim, num_heads, dropout=dropout, batch_first=True | |
| ) for _ in range(self.num_scales) | |
| ]) | |
| if not no_global_state: | |
| self.state_cross_attentions = nn.ModuleList([ | |
| nn.MultiheadAttention( | |
| hidden_dim, num_heads, dropout=dropout, batch_first=True | |
| ) for _ in range(self.num_scales) | |
| ]) | |
| # Self-attention for anchors (standard attention) | |
| self.self_attentions = nn.ModuleList([ | |
| nn.MultiheadAttention( | |
| hidden_dim, num_heads, dropout=dropout, batch_first=True | |
| ) for _ in range(self.num_scales) | |
| ]) | |
| # Norm modules (configurable type) for norm-first and qk-norm | |
| Norm = nn.LayerNorm if norm_type == 'layer_norm' else getattr(nn, 'RMSNorm', nn.LayerNorm) | |
| # For object attention | |
| self.query_in_norms = nn.ModuleList([Norm(hidden_dim) for _ in range(self.num_scales)]) | |
| self.kv_in_norms = nn.ModuleList([Norm(hidden_dim) for _ in range(self.num_scales)]) | |
| self.post_attn_norms = nn.ModuleList([Norm(hidden_dim) for _ in range(self.num_scales)]) | |
| # For state attention (skipped when no_global_state) | |
| if not no_global_state: | |
| self.state_kv_in_norms = nn.ModuleList([Norm(hidden_dim) for _ in range(self.num_scales)]) | |
| self.state_post_attn_norms = nn.ModuleList([Norm(hidden_dim) for _ in range(self.num_scales)]) | |
| # For self-attention (within same object) | |
| self.self_attn_in_norms = nn.ModuleList([Norm(hidden_dim) for _ in range(self.num_scales)]) | |
| self.self_attn_post_norms = nn.ModuleList([Norm(hidden_dim) for _ in range(self.num_scales)]) | |
| self._use_qk_norm = qk_norm | |
| # Cross-object anchor self-attention | |
| # Allows anchors from different objects to interact directly | |
| if cross_object_attention: | |
| if use_rope: | |
| self.cross_obj_self_attentions = nn.ModuleList([ | |
| SelfAttentionWithRoPE( | |
| d_model=hidden_dim, | |
| num_heads=num_heads, | |
| dropout=dropout, | |
| use_rope=True, | |
| rope_dim=rope_dim, | |
| dim_per_coord_3d=dim_per_coord_3d, | |
| dim_per_coord_multi=dim_per_coord_multi, | |
| elementwise_gate=elementwise_gate | |
| ) for _ in range(self.num_scales) | |
| ]) | |
| else: | |
| self.cross_obj_self_attentions = nn.ModuleList([ | |
| nn.MultiheadAttention( | |
| hidden_dim, num_heads, dropout=dropout, batch_first=True | |
| ) for _ in range(self.num_scales) | |
| ]) | |
| self.cross_obj_in_norms = nn.ModuleList([Norm(hidden_dim) for _ in range(self.num_scales)]) | |
| self.cross_obj_post_norms = nn.ModuleList([Norm(hidden_dim) for _ in range(self.num_scales)]) | |
| # Fusion and prediction | |
| # With no_global_state, fusion uses object features only (no dead zero channels) | |
| if no_global_state: | |
| fusion_input_dim = hidden_dim * self.num_scales # Only object features | |
| print(f"[Predictor] fusion_input_dim = {fusion_input_dim} (object features only)") | |
| else: | |
| fusion_input_dim = hidden_dim * self.num_scales * 2 # × 2 for object + state | |
| if use_ref_pose: | |
| # Add extra projection for reference pose in fusion | |
| # Reference pose is always 3D (positions) | |
| self.ref_pose_proj = nn.Linear(3, hidden_dim) | |
| fusion_input_dim += hidden_dim | |
| print(f"[Predictor] fusion_input_dim with ref_pose = {fusion_input_dim}") | |
| # Fusion head: norm-first + SwiGLU (default) | |
| def _fusion_block(in_dim: int, out_dim: int) -> nn.Sequential: | |
| if fusion_activation == 'swiglu': | |
| return nn.Sequential( | |
| nn.LayerNorm(in_dim) if norm_type == 'layer_norm' else getattr(nn, 'RMSNorm', nn.LayerNorm)(in_dim), | |
| nn.Linear(in_dim, out_dim), | |
| nn.Linear(in_dim, out_dim), # gate | |
| ) | |
| else: | |
| act = nn.GELU() if fusion_activation == 'gelu' else nn.SiLU() | |
| return nn.Sequential( | |
| nn.LayerNorm(in_dim) if norm_type == 'layer_norm' else getattr(nn, 'RMSNorm', nn.LayerNorm)(in_dim), | |
| nn.Linear(in_dim, out_dim), | |
| act | |
| ) | |
| # Build two-stage fusion: [fusion_dim -> hidden_dim*2 -> hidden_dim] then linear to output | |
| self.fusion_dim1 = hidden_dim * 2 | |
| self.fusion_norm1 = nn.LayerNorm(fusion_input_dim) if norm_type == 'layer_norm' else getattr(nn, 'RMSNorm', nn.LayerNorm)(fusion_input_dim) | |
| self.fusion_w1 = nn.Linear(fusion_input_dim, self.fusion_dim1) | |
| self.fusion_g1 = nn.Linear(fusion_input_dim, self.fusion_dim1) | |
| self.fusion_drop = nn.Dropout(dropout) | |
| self.fusion_norm2 = nn.LayerNorm(self.fusion_dim1) if norm_type == 'layer_norm' else getattr(nn, 'RMSNorm', nn.LayerNorm)(self.fusion_dim1) | |
| self.fusion_w2 = nn.Linear(self.fusion_dim1, hidden_dim) | |
| self.fusion_g2 = nn.Linear(self.fusion_dim1, hidden_dim) | |
| self.fusion_out = nn.Linear(hidden_dim, output_dim) | |
| # Lightweight penetration head for vertex-level sign prediction: +1 (outside) | |
| # vs -1 (inside) for all vertices, computed from the last-layer object | |
| # feature, a global state feature, and the vertex position. | |
| # Two 32-d projections + a 16-d position projection -> 1-d tanh score. | |
| self._pen_obj_proj = nn.Linear(state_dim, 32) | |
| self._pen_state_proj = nn.Linear(state_dim, 32) | |
| self._pen_pos_proj = nn.Linear(3, 16) | |
| self._pen_head_norm = nn.LayerNorm(32 + 32 + 16) if norm_type == 'layer_norm' else getattr(nn, 'RMSNorm', nn.LayerNorm)(32 + 32 + 16) | |
| self._pen_head = nn.Linear(32 + 32 + 16, 1) | |
| # torch.nn.RMSNorm built without an explicit eps uses torch.finfo(input.dtype).eps: 0.0078125 for the bf16 inputs | |
| # four module groups receive under autocast in training, 1.19e-7 for fp32 inputs, and the evaluation rollouts run | |
| # without autocast. pin_rmsnorm_eps() gives every module the eps of the dtype it receives in bf16-autocast | |
| # training (input dtypes measured in bf16-autocast training), so evaluation computes the trained function; | |
| # bf16-autocast training itself is unchanged. | |
| _RMSNORM_BF16_TRAINING_INPUT = ('query_in_norms', 'kv_in_norms', 'post_attn_norms', 'fusion_norm2') | |
| _RMSNORM_FP32_TRAINING_INPUT = ('self_attn_in_norms', 'self_attn_post_norms', 'cross_obj_in_norms', | |
| 'cross_obj_post_norms', 'fusion_norm1', '_pen_head_norm') | |
| def pin_rmsnorm_eps(self) -> int: | |
| bf16, fp32 = torch.finfo(torch.bfloat16).eps, torch.finfo(torch.float32).eps | |
| n = 0 | |
| for name, m in self.named_modules(): | |
| if not isinstance(m, nn.RMSNorm): | |
| continue | |
| top = name.split('.')[0] | |
| if top in self._RMSNORM_BF16_TRAINING_INPUT: | |
| m.eps = bf16 | |
| elif top in self._RMSNORM_FP32_TRAINING_INPUT: | |
| m.eps = fp32 | |
| else: | |
| raise ValueError(f"pin_rmsnorm_eps: training input dtype of RMSNorm '{name}' was not measured") | |
| n += 1 | |
| return n | |
| def forward( | |
| self, | |
| multi_scale_features: List[torch.Tensor], | |
| query: torch.Tensor, | |
| ref_pose: Optional[torch.Tensor] = None, | |
| query_pos: Optional[torch.Tensor] = None, # Anchor 3D positions for RoPE | |
| key_pos: Optional[torch.Tensor] = None, # Object anchor positions for RoPE | |
| multi_scale_state_features: Optional[List[torch.Tensor]] = None, # State features from decoder | |
| state_pos: Optional[torch.Tensor] = None, # State 3D grid positions for RoPE | |
| obj_mask: Optional[torch.Tensor] = None # (B, Nobj) bool mask for valid objects | |
| ) -> torch.Tensor: | |
| """ | |
| Query-based prediction using cross-attention to multi-scale object and state features. | |
| Supports two input formats: | |
| 1. Grouped: query (B, Nobj, Nq, feature_dim) - preferred for per-object attention | |
| 2. Flat: query (B, M, feature_dim) where M=Nobj*Nq | |
| Args: | |
| multi_scale_features: List of object features from decoder layers (B, Nobj, D) | |
| query: Query features | |
| - Grouped: (B, Nobj, Nq, 9/12) - dist_vec + velocity + rel_pos | |
| - Flat: (B, Nobj*Nq, 9/12) | |
| ref_pose: Reference pose | |
| - Grouped: (B, Nobj, Nq, 3) | |
| - Flat: (B, Nobj*Nq, 3) | |
| query_pos: Anchor 3D positions for RoPE | |
| - Grouped: (B, Nobj, Nq, 3) | |
| - Flat: (B, Nobj*Nq, 3) | |
| key_pos: Object anchor positions (B, Nobj, Nq*3) for multi-point RoPE | |
| (RenderFormer-style: flattened multi-point representation) | |
| multi_scale_state_features: List of state features from decoder layers (B, N_state, D) | |
| state_pos: State 3D grid positions (B, N_state, 3) for RoPE | |
| Returns: | |
| Predictions matching input format: | |
| - Grouped input -> (B, Nobj, Nq, output_dim) | |
| - Flat input -> (B, Nobj*Nq, output_dim) | |
| """ | |
| # Detect input format | |
| is_grouped = (query.dim() == 4) # (B, Nobj, Nq, D) vs (B, M, D) | |
| if is_grouped: | |
| B, Nobj, Nq, query_dim = query.shape | |
| M = Nobj * Nq | |
| else: | |
| B, M, query_dim = query.shape | |
| # Infer Nobj from key_pos or from the first available feature layer | |
| if key_pos is not None: | |
| Nobj = key_pos.shape[1] | |
| else: | |
| # Safely access the first available layer in the saved features | |
| # (layer 0 might not be saved with memory optimization) | |
| Nobj = multi_scale_features[0].shape[1] if len(multi_scale_features) > 0 else 1 | |
| Nq = M // Nobj | |
| assert M == Nobj * Nq, f"M={M} must be divisible by Nobj={Nobj}" | |
| # Select object features from specified layers using the mapping | |
| # The mapping converts from original layer index to position in saved list | |
| selected_features = [multi_scale_features[self.layer_index_mapping[i]] for i in self.layer_indices] | |
| # Select state features from specified layers (if provided) | |
| selected_state_features = None | |
| if multi_scale_state_features is not None: | |
| selected_state_features = [multi_scale_state_features[self.layer_index_mapping[i]] for i in self.layer_indices] | |
| # Convert to grouped format for processing | |
| if is_grouped: | |
| # Already grouped | |
| query_grouped = query # (B, Nobj, Nq, query_dim) | |
| ref_pose_grouped = ref_pose # (B, Nobj, Nq, 3) | |
| query_pos_grouped = query_pos if query_pos is not None else None # (B, Nobj, Nq, 3) | |
| else: | |
| # Reshape flat to grouped | |
| query_grouped = query.view(B, Nobj, Nq, query_dim) # (B, Nobj, Nq, query_dim) | |
| if ref_pose is not None: | |
| ref_pose_grouped = ref_pose.view(B, Nobj, Nq, 3) | |
| if query_pos is not None: | |
| query_pos_grouped = query_pos.view(B, Nobj, Nq, 3) | |
| # Prepare query with reference pose | |
| assert ref_pose_grouped is not None | |
| query_input_grouped = torch.cat([query_grouped, ref_pose_grouped], dim=-1) # (B, Nobj, Nq, query_dim+3) | |
| # Reshape for batched per-object attention: (B*Nobj, Nq, query_dim+3) | |
| query_input = query_input_grouped.view(B * Nobj, Nq, -1) | |
| # Validate query dimensionality against the projection layer | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| expected_dim = self.query_proj.in_features | |
| actual_dim = query_input.shape[-1] | |
| if expected_dim != actual_dim: | |
| logger.error(f"Dimension mismatch: query_proj expects {expected_dim}D input, got {actual_dim}D") | |
| logger.error(f" query shape: {query.shape}, ref_pose shape: {ref_pose.shape if ref_pose is not None else None}") | |
| raise RuntimeError(f"Predictor input dimension mismatch: expected {expected_dim}, got {actual_dim}") | |
| # Project query: (B*Nobj, Nq, query_dim+3) -> (B*Nobj, Nq, hidden_dim) | |
| query_proj = self.query_proj(query_input) # (B*Nobj, Nq, hidden_dim) | |
| # Process each scale with cross-attention (with RoPE if enabled) | |
| # For batched per-object attention, we reshape features to (B*Nobj, 1, hidden_dim) | |
| # so each object's features can be attended by its Nq anchors | |
| # When cross_object_kv=True, each anchor can attend to ALL objects' features (B*Nobj, Nobj, hidden_dim) | |
| attended_features = [] | |
| attended_state_features = [] | |
| # Build attention masks for cross-object attention from obj_mask | |
| kv_mask_for_cross_obj = None # For cross-object KV attention | |
| anchor_mask_for_global = None # For global anchor self-attention | |
| if obj_mask is not None and (self.cross_object_kv or self.cross_object_attention): | |
| # kv mask for cross-object KV: each anchor group may only attend to | |
| # valid objects' features; obj_mask (B, Nobj) expands to all anchor groups | |
| kv_mask_expanded = obj_mask.unsqueeze(1).expand(B, Nobj, Nobj) # (B, Nobj, Nobj) | |
| kv_mask_expanded = kv_mask_expanded.reshape(B * Nobj, Nobj) # (B*Nobj, Nobj) | |
| # SDPA additive format: (B*Nobj, 1, Nq, Nobj); True=attend becomes 0.0 | |
| kv_mask_for_cross_obj = kv_mask_expanded.unsqueeze(1).unsqueeze(1).expand(-1, 1, Nq, -1) # (B*Nobj, 1, Nq, Nobj) | |
| kv_mask_for_cross_obj = torch.where(kv_mask_for_cross_obj, 0.0, float('-inf')) # SDPA additive mask | |
| # Mask for global anchor self-attention: expand obj_mask to per-anchor | |
| # (B, Nobj) -> (B, Nobj*Nq) | |
| anchor_mask_flat = obj_mask.unsqueeze(-1).expand(B, Nobj, Nq).reshape(B, Nobj * Nq) # (B, Nobj*Nq) | |
| # SDPA mask: (B, 1, Nobj*Nq, Nobj*Nq); only valid anchors attend to valid anchors | |
| anchor_mask_2d = anchor_mask_flat.unsqueeze(1) & anchor_mask_flat.unsqueeze(2) # (B, Nobj*Nq, Nobj*Nq) | |
| anchor_mask_for_global = torch.where(anchor_mask_2d.unsqueeze(1), 0.0, float('-inf')) # (B, 1, Nobj*Nq, Nobj*Nq) | |
| if not self._p0_mask_logged: | |
| n_valid = obj_mask.sum(dim=1).float().mean().item() | |
| n_pad = Nobj - n_valid | |
| print(f"[Predictor] Padding mask active: {n_valid:.0f} valid / {n_pad:.0f} padding objects, " | |
| f"{n_pad*Nq:.0f} padded anchors masked in attention") | |
| self._p0_mask_logged = True | |
| for i, feat in enumerate(selected_features): | |
| # Project features: (B, Nobj, hidden_dim) | |
| feat_proj = self.scale_projections[i](feat) # (B, Nobj, hidden_dim) | |
| if self.cross_object_kv: | |
| # === Cross-Object Key-Value Mode === | |
| # Each anchor can attend to ALL objects' features | |
| # Expand feat_proj for all objects: (B, Nobj, D) -> (B*Nobj, Nobj, D) | |
| # Each of the B*Nobj anchor-groups sees all Nobj object features | |
| kv_batched = feat_proj.unsqueeze(1).expand(B, Nobj, Nobj, -1).reshape(B * Nobj, Nobj, -1) | |
| # kv_batched: (B*Nobj, Nobj, hidden_dim) | |
| # One-time runtime confirmation log | |
| if not self._cross_object_kv_logged: | |
| print(f"[Predictor Runtime] Cross-object KV active: each anchor queries {Nobj} objects' features") | |
| self._cross_object_kv_logged = True | |
| else: | |
| # === Standard Per-Object Mode === | |
| # Each object has 1 feature vector that Nq anchors will attend to | |
| kv_batched = feat_proj.view(B * Nobj, 1, -1) # (B*Nobj, 1, hidden_dim) | |
| # Norm-first with optional qk-norm | |
| q_in = self.query_in_norms[i](query_proj) # (B*Nobj, Nq, hidden_dim) | |
| kv_in = self.kv_in_norms[i](kv_batched) if self._use_qk_norm else kv_batched | |
| # Per-object attention: Nq anchors attend to object feature(s) | |
| if self.use_rope: | |
| # Use RoPE-aware cross-attention | |
| if query_pos_grouped is None or key_pos is None: | |
| raise ValueError( | |
| "QueryBasedMultiScalePredictor with use_rope=True requires " | |
| "query_pos (anchor positions) and key_pos (object positions)" | |
| ) | |
| # Reshape positions for batched per-object | |
| # query_pos: individual anchor positions (B, Nobj, Nq, 3) -> (B*Nobj, Nq, 3) | |
| query_pos_batched = query_pos_grouped.view(B * Nobj, Nq, 3) # (B*Nobj, Nq, 3) | |
| if self.cross_object_kv: | |
| # Key positions for ALL objects: (B, Nobj, Nq*3) -> (B*Nobj, Nobj, Nq*3) | |
| # Each anchor-group sees all objects' anchor positions | |
| key_pos_batched = key_pos.unsqueeze(1).expand(B, Nobj, Nobj, -1).reshape(B * Nobj, Nobj, -1) | |
| # key_pos_batched: (B*Nobj, Nobj, Nq*3) | |
| else: | |
| # key_pos: flattened multi-point anchors (B, Nobj, Nq*3) -> (B*Nobj, 1, Nq*3) | |
| key_pos_batched = key_pos.view(B * Nobj, 1, -1) # (B*Nobj, 1, Nq*3) for multi-point RoPE | |
| attn_out = self.cross_attentions[i]( | |
| query=q_in, # (B*Nobj, Nq, hidden_dim) | |
| key_value=kv_in, # (B*Nobj, 1 or Nobj, hidden_dim) | |
| pos_q=query_pos_batched, # (B*Nobj, Nq, 3) - individual anchor positions | |
| pos_k=key_pos_batched, # (B*Nobj, 1 or Nobj, Nq*3) - multi-point anchors | |
| attn_mask=kv_mask_for_cross_obj if self.cross_object_kv else None | |
| ) # (B*Nobj, Nq, hidden_dim) | |
| else: | |
| # Standard attention | |
| attn_out, _ = self.cross_attentions[i]( | |
| query=q_in, | |
| key=kv_in, | |
| value=kv_in | |
| ) # (B*Nobj, Nq, hidden_dim) | |
| # Residual + post norm | |
| attn_out = self.post_attn_norms[i](attn_out + query_proj) # (B*Nobj, Nq, hidden_dim) | |
| attended_features.append(attn_out) | |
| # === Cross-attention to state features (skipped when no_global_state) === | |
| if not self.no_global_state and selected_state_features is not None: | |
| state_feat = selected_state_features[i] # (B, N_state, D) | |
| state_feat_proj = self.state_scale_projections[i](state_feat) # (B, N_state, hidden_dim) | |
| # Expand state features for all anchors across all objects | |
| # state_feat_proj: (B, N_state, hidden_dim) -> (B*Nobj, N_state, hidden_dim) | |
| state_feat_expanded = state_feat_proj.unsqueeze(1).expand(B, Nobj, -1, -1).reshape(B * Nobj, -1, self.hidden_dim) | |
| # Norm | |
| state_kv_in = self.state_kv_in_norms[i](state_feat_expanded) if self._use_qk_norm else state_feat_expanded | |
| # Cross-attention: anchors attend to state | |
| if self.use_rope: | |
| if state_pos is None: | |
| raise ValueError("state_pos is required when using RoPE for state attention") | |
| # Expand state_pos for all objects: (B, N_state, 3) -> (B*Nobj, N_state, 3) | |
| state_pos_expanded = state_pos.unsqueeze(1).expand(B, Nobj, -1, -1).reshape(B * Nobj, -1, 3) | |
| state_attn_out = self.state_cross_attentions[i]( | |
| query=q_in, # (B*Nobj, Nq, hidden_dim) | |
| key_value=state_kv_in, # (B*Nobj, N_state, hidden_dim) | |
| pos_q=query_pos_batched, # (B*Nobj, Nq, 3) - anchor positions | |
| pos_k=state_pos_expanded, # (B*Nobj, N_state, 3) - state grid positions | |
| attn_mask=None | |
| ) # (B*Nobj, Nq, hidden_dim) | |
| else: | |
| state_attn_out, _ = self.state_cross_attentions[i]( | |
| query=q_in, | |
| key=state_kv_in, | |
| value=state_kv_in | |
| ) | |
| # Residual + post norm | |
| state_attn_out = self.state_post_attn_norms[i](state_attn_out + query_proj) | |
| attended_state_features.append(state_attn_out) | |
| else: | |
| if not self.no_global_state: | |
| # Without a global state input: append zeros to keep fusion dimensions consistent | |
| attended_state_features.append(torch.zeros_like(attended_features[-1])) | |
| # With no_global_state, attended_state_features stays empty | |
| # === Self-attention among anchors within each object === | |
| # With no_global_state, only object features feed the self-attention | |
| if self.no_global_state or len(attended_state_features) <= i: | |
| sa_input = attended_features[-1] # (B*Nobj, Nq, hidden_dim), object features only | |
| else: | |
| sa_input = attended_features[-1] + attended_state_features[-1] # (B*Nobj, Nq, hidden_dim) | |
| # Norm-first | |
| sa_input_normed = self.self_attn_in_norms[i](sa_input) # (B*Nobj, Nq, hidden_dim) | |
| # Self-attention: anchors attend to each other within the same object | |
| if self.use_rope: | |
| sa_out = self.self_attentions[i]( | |
| query=sa_input_normed, # (B*Nobj, Nq, hidden_dim) | |
| key_value=sa_input_normed, # (B*Nobj, Nq, hidden_dim) | |
| pos_q=query_pos_batched, # (B*Nobj, Nq, 3) - anchor positions | |
| pos_k=query_pos_batched, # (B*Nobj, Nq, 3) - same positions for self-attn | |
| attn_mask=None | |
| ) # (B*Nobj, Nq, hidden_dim) | |
| else: | |
| sa_out, _ = self.self_attentions[i]( | |
| query=sa_input_normed, | |
| key=sa_input_normed, | |
| value=sa_input_normed | |
| ) # (B*Nobj, Nq, hidden_dim) | |
| # Residual + post norm | |
| sa_out = self.self_attn_post_norms[i](sa_out + sa_input) # (B*Nobj, Nq, hidden_dim) | |
| # === Cross-object anchor self-attention === | |
| # Allows anchors from different objects to interact directly | |
| if self.cross_object_attention: | |
| # One-time runtime confirmation log | |
| if not self._cross_object_attention_logged: | |
| print(f"[Predictor Runtime] Cross-object anchor attention active: B={B}, Nobj={Nobj}, Nq={Nq}, total_anchors={Nobj*Nq}") | |
| self._cross_object_attention_logged = True | |
| # Reshape from per-object to global: (B*Nobj, Nq, D) -> (B, Nobj*Nq, D) | |
| sa_out_global = sa_out.view(B, Nobj * Nq, -1) # (B, Nobj*Nq, hidden_dim) | |
| # Reshape anchor positions: (B, Nobj, Nq, 3) -> (B, Nobj*Nq, 3) | |
| query_pos_global = query_pos_grouped.view(B, Nobj * Nq, 3) # (B, Nobj*Nq, 3) | |
| # Norm-first | |
| cross_obj_input = self.cross_obj_in_norms[i](sa_out_global) # (B, Nobj*Nq, hidden_dim) | |
| # Cross-object self-attention: all anchors attend to all anchors | |
| if self.use_rope: | |
| cross_obj_out = self.cross_obj_self_attentions[i]( | |
| query=cross_obj_input, # (B, Nobj*Nq, hidden_dim) | |
| key_value=cross_obj_input, # (B, Nobj*Nq, hidden_dim) | |
| pos_q=query_pos_global, # (B, Nobj*Nq, 3) - all anchor positions | |
| pos_k=query_pos_global, # (B, Nobj*Nq, 3) - same for self-attn | |
| attn_mask=anchor_mask_for_global # Mask padding anchors | |
| ) # (B, Nobj*Nq, hidden_dim) | |
| else: | |
| cross_obj_out, _ = self.cross_obj_self_attentions[i]( | |
| query=cross_obj_input, | |
| key=cross_obj_input, | |
| value=cross_obj_input | |
| ) # (B, Nobj*Nq, hidden_dim) | |
| # Residual + post norm | |
| cross_obj_out = self.cross_obj_post_norms[i](cross_obj_out + sa_out_global) # (B, Nobj*Nq, hidden_dim) | |
| # Reshape back to per-object: (B, Nobj*Nq, D) -> (B*Nobj, Nq, D) | |
| sa_out = cross_obj_out.view(B * Nobj, Nq, -1) # (B*Nobj, Nq, hidden_dim) | |
| # Replace this scale's object features with the enhanced version | |
| attended_features[-1] = sa_out | |
| # Concatenate multi-scale features for fusion | |
| # With no_global_state, only object features are concatenated | |
| if self.no_global_state: | |
| multi_scale_output = torch.cat(attended_features, dim=-1) | |
| # (B*Nobj, Nq, hidden_dim * num_scales) | |
| if not self._p1_no_state_logged: | |
| print(f"[Predictor] Fusion uses object features only: {multi_scale_output.shape[-1]}D") | |
| self._p1_no_state_logged = True | |
| else: | |
| multi_scale_output = torch.cat(attended_features + attended_state_features, dim=-1) | |
| # (B*Nobj, Nq, hidden_dim * num_scales * 2) | |
| # Add reference pose to fusion if enabled | |
| if self.use_ref_pose: | |
| # ref_pose_grouped: (B, Nobj, Nq, 3) -> (B*Nobj, Nq, 3) | |
| ref_pose_batched = ref_pose_grouped.view(B * Nobj, Nq, 3) | |
| ref_pose_features = self.ref_pose_proj(ref_pose_batched) # (B*Nobj, Nq, hidden_dim) | |
| multi_scale_output = torch.cat([multi_scale_output, ref_pose_features], dim=-1) | |
| # (B*Nobj, Nq, hidden_dim * num_scales + hidden_dim) | |
| # Final fusion and prediction (SwiGLU blocks) | |
| # All operations are per-anchor: (B*Nobj, Nq, dim) | |
| # Block 1 | |
| x = self.fusion_norm1(multi_scale_output) | |
| if hasattr(torch.nn.functional, 'silu'): | |
| x = self.fusion_drop(torch.nn.functional.silu(self.fusion_g1(x)) * self.fusion_w1(x)) | |
| else: | |
| x = self.fusion_drop(torch.sigmoid(self.fusion_g1(x)) * self.fusion_w1(x)) | |
| # Block 2 | |
| x = self.fusion_norm2(x) | |
| if hasattr(torch.nn.functional, 'silu'): | |
| x = self.fusion_drop(torch.nn.functional.silu(self.fusion_g2(x)) * self.fusion_w2(x)) | |
| else: | |
| x = self.fusion_drop(torch.sigmoid(self.fusion_g2(x)) * self.fusion_w2(x)) | |
| if getattr(self, 'fp32_output', False): | |
| # model.predictor.fp32_output: the acceleration read-out in fp32 also under bf16 autocast (training), as | |
| # in the fp32 evaluation rollout | |
| with torch.autocast(device_type=x.device.type, enabled=False): | |
| predictions = self.fusion_out(x.float()) # (B*Nobj, Nq, output_dim) | |
| else: | |
| predictions = self.fusion_out(x) # (B*Nobj, Nq, output_dim) | |
| # Reshape output to match input format | |
| if is_grouped: | |
| # Return grouped format: (B, Nobj, Nq, output_dim) | |
| predictions = predictions.view(B, Nobj, Nq, -1) | |
| else: | |
| # Return flat format: (B, Nobj*Nq, output_dim) | |
| predictions = predictions.view(B, M, -1) | |
| return predictions | |
| def predict_penetration_vertices( | |
| self, | |
| multi_scale_features: List[torch.Tensor], | |
| multi_scale_state_features: Optional[List[torch.Tensor]], | |
| pos_full: torch.Tensor # (B, Nobj, Nv, 3) | |
| ) -> torch.Tensor: | |
| """ | |
| Predict penetration sign for ALL vertices (vertex-level supervision). | |
| Returns penetration score in [-1, 1] where: | |
| - +1 indicates vertex is outside other objects (no penetration) | |
| - -1 indicates vertex is inside other objects (penetration) | |
| Args: | |
| multi_scale_features: List of object features from decoder layers (B, Nobj, D) | |
| multi_scale_state_features: List of state features from decoder layers (B, N_state, D) or None | |
| pos_full: Vertex positions (B, Nobj, Nv, 3) - AFTER Kabsch transform | |
| Returns: | |
| Penetration scores (B, Nobj, Nv, 1) with tanh activation, requires_grad=True for training | |
| """ | |
| # Use layer_index_mapping to get the correct position in the saved features list | |
| last_layer_idx = self.layer_indices[-1] | |
| mapped_idx = self.layer_index_mapping[last_layer_idx] | |
| obj_last = multi_scale_features[mapped_idx] # (B, Nobj, D) | |
| B, Nobj, _ = obj_last.shape | |
| obj_proj = self._pen_obj_proj(obj_last) # (B, Nobj, 32) | |
| if multi_scale_state_features is not None and len(multi_scale_state_features) > 0: | |
| state_last = multi_scale_state_features[mapped_idx] # (B, N_state, D) | |
| state_global = state_last.mean(dim=1) # (B, D) | |
| else: | |
| state_global = obj_last.mean(dim=1) * 0.0 # (B, D) | |
| state_proj = self._pen_state_proj(state_global) # (B, 32) | |
| state_proj = state_proj.unsqueeze(1).expand(-1, Nobj, -1) # (B, Nobj, 32) | |
| Nv = pos_full.shape[2] | |
| pos_proj = self._pen_pos_proj(pos_full) # (B, Nobj, Nv, 16) | |
| obj_proj_v = obj_proj.unsqueeze(2).expand(-1, -1, Nv, -1) # (B, Nobj, Nv, 32) | |
| state_proj_v = state_proj.unsqueeze(2).expand(-1, -1, Nv, -1) # (B, Nobj, Nv, 32) | |
| pen_feat = torch.cat([obj_proj_v, state_proj_v, pos_proj], dim=-1) # (B, Nobj, Nv, 80) | |
| pen_feat = self._pen_head_norm(pen_feat) | |
| pen = torch.tanh(self._pen_head(pen_feat)) # (B, Nobj, Nv, 1) | |
| return pen |