Source code for pimm.models.volt.volt_v1m1

"""Volt-v1m1: Sonata-compatible Volt encoder backbone.

A flat (non-hierarchical) sparse-conv-tokenized transformer that satisfies
Sonata's `(point) -> point` backbone contract by emitting a *coarse* Point
at token resolution (one row per 5x5x5 voxel patch) with a
`pooling_parent`/`pooling_inverse` chain — the same shape PT-v3m2's
`SerializedPooling` produces for its hierarchical stages.

    point in:  feat, coord, origin_coord, offset (-> batch), grid_size,
               optional `mask` (per-point bool)
    point out: token-resolution Point with
                 feat / coord / origin_coord / batch / grid_coord
                 pooling_parent (the input Point)
                 pooling_inverse (per-input-point parent-token id)

The forward pipeline is `stem -> mask injection -> strided tokenizer ->
RoPE transformer blocks -> per-token Point construction`. The stem
(1x1 SubMConv3d into `stem_channels`) provides a per-point post-projection
slot where a learnable mask token can replace masked-point features in
the same way PT-v3m2's `Embedding` module does — keeping mask injection
in learned embedding space rather than mixing it into the 4-dim
physical-units input.

Why coarse output: Sonata's `match_neighbour` runs KNN over the returned
Point. Per-input-point broadcast tokens make matched pairs *byte-identical
within a patch*, which collapses the contrastive loss to its trivial
fixed point. Returning at token resolution gives every matched pair
distinct features. The `PretrainEvaluator` walks the `pooling_parent`
chain back up to per-point features for linear probing.

Pair with Sonata using `up_cast_level=0` and `head_in_channels=embed_dim`.
"""

from __future__ import annotations

from typing import Optional

import spconv.pytorch as spconv
import torch
import torch.nn as nn
import torch_scatter
from torch.nn.init import trunc_normal_

from pimm.models.builder import MODELS
from pimm.models.modules import PointModel
from pimm.models.utils.structure import Point
from pimm.models.volt.layers import (
    Block,
    RoPE,
    build_point_to_token,
    sort_tokens_by_batch,
)
from pimm.utils.logger import get_logger

logger = get_logger(__name__)


[docs] @MODELS.register_module("Volt-v1m1") class VoltBackbone(PointModel): """Flat Volt encoder for Sonata pretraining.""" def __init__( self, in_channels: int = 4, stem_channels: int = 32, embed_dim: int = 384, depth: int = 12, num_heads: int = 6, mlp_ratio: float = 4.0, init_values: Optional[float] = None, qk_norm: bool = True, drop_path: float = 0.3, increase_drop_path: bool = True, stride: int = 5, kernel_size: int = 5, mask_token: bool = True, final_norm: bool = True, rope_max_grid_size: tuple = (1024, 1024, 1024), rope_freq_split: tuple = (11, 11, 10), ): super().__init__() self.stride = stride self.kernel_size = kernel_size self.embed_dim = embed_dim self.stem = spconv.SubMConv3d( in_channels, stem_channels, kernel_size=1, bias=True, indice_key="volt_stem", ) if mask_token: self.point_mask_token = nn.Parameter(torch.zeros(stem_channels)) trunc_normal_(self.point_mask_token, std=0.02) else: self.point_mask_token = None self.tokenizer = spconv.SparseConv3d( stem_channels, embed_dim, kernel_size=kernel_size, stride=stride, bias=True, indice_key="embedding", ) if increase_drop_path: dp = torch.linspace(0, drop_path, depth).tolist() else: dp = [drop_path] * depth self.blocks = nn.Sequential( *[ Block( dim=embed_dim, num_heads=num_heads, mlp_ratio=mlp_ratio, init_values=init_values, qk_norm=qk_norm, drop_path=dp[i], act_layer=nn.GELU, norm_layer=nn.LayerNorm, ) for i in range(depth) ] ) self.norm = nn.LayerNorm(embed_dim) if final_norm else nn.Identity() h_dim = embed_dim // num_heads assert sum(rope_freq_split) * 2 == h_dim, ( f"rope_freq_split {tuple(rope_freq_split)} sums to {sum(rope_freq_split)}, " f"but h_dim//2 = {h_dim // 2} for embed_dim={embed_dim}, num_heads={num_heads}. " f"Adjust rope_freq_split so that sum(..)*2 == embed_dim // num_heads." ) self.pos_enc = RoPE( freq_split=tuple(rope_freq_split), max_grid_size=tuple(rope_max_grid_size), ) self.apply(self._init_weights) logger.info( f"Volt-v1m1: in_channels={in_channels}, stem_channels={stem_channels}, " f"embed_dim={embed_dim}, depth={depth}, num_heads={num_heads}, " f"stride={stride}, kernel_size={kernel_size}, mask_token={mask_token}, " f"final_norm={final_norm}" ) @staticmethod def _init_weights(module): if isinstance(module, nn.Linear): trunc_normal_(module.weight, std=0.02) if module.bias is not None: nn.init.zeros_(module.bias) elif hasattr(module, "init_weights"): module.init_weights() @staticmethod def _compute_cu_seqlens(batch_indices: torch.Tensor): counts = torch.bincount(batch_indices) cu = torch.zeros( counts.numel() + 1, dtype=torch.int32, device=batch_indices.device ) cu[1:] = torch.cumsum(counts.to(torch.int32), dim=0) max_seqlen = int(counts.max().item()) if counts.numel() else 0 return cu, max_seqlen
[docs] def forward(self, point): if not isinstance(point, Point): point = Point(point) if "grid_coord" not in point.keys(): assert {"coord", "grid_size"}.issubset(point.keys()), ( "Volt-v1m1 requires either `grid_coord` or both `coord` and " "`grid_size` on the input Point." ) point["grid_coord"] = torch.div( point.coord - point.coord.min(0)[0], point.grid_size, rounding_mode="trunc", ).int() grid_coord = point.grid_coord batch = point.batch sparse_shape = torch.add(torch.max(grid_coord, dim=0).values, 96).tolist() indices = torch.cat( [batch.unsqueeze(-1).int(), grid_coord.int()], dim=1 ).contiguous() x = spconv.SparseConvTensor( features=point.feat, indices=indices, spatial_shape=sparse_shape, batch_size=int(batch[-1].item()) + 1, ) x = self.stem(x) if ( self.point_mask_token is not None and "mask" in point.keys() and point.mask is not None ): mask = point.mask x = x.replace_feature( torch.where( mask.unsqueeze(-1), self.point_mask_token.to(x.features.dtype), x.features, ) ) x = self.tokenizer(x) token_features, token_indices = sort_tokens_by_batch( x.features, x.indices.long() ) cu, max_seq = self._compute_cu_seqlens(token_indices[:, 0]) freqs = self.pos_enc.compute_axial_cis_efficient(token_indices[:, 1:]) for blk in self.blocks: token_features = blk(token_features, freqs, cu, max_seq) token_features = self.norm(token_features) point_to_token = build_point_to_token( grid_coord.long(), batch.long(), token_indices, self.stride ) # Build the coarse (token-resolution) Point. Mirrors the # PT-v3m2 SerializedPooling output shape: per-cluster feat/coord # plus a pooling_parent/pooling_inverse chain so the evaluator # can broadcast back to per-point features for linear probing. T = token_features.shape[0] coarse_dict = dict( feat=token_features, coord=torch_scatter.scatter_mean( point.coord, point_to_token, dim=0, dim_size=T ), grid_coord=token_indices[:, 1:].int(), batch=token_indices[:, 0].long(), grid_size=( point.grid_size * self.stride if "grid_size" in point.keys() else None ), pooling_parent=point, pooling_inverse=point_to_token, ) if "origin_coord" in point.keys(): coarse_dict["origin_coord"] = torch_scatter.scatter_mean( point.origin_coord, point_to_token, dim=0, dim_size=T ) # Propagate per-input-point classification labels (e.g. for # OnlineLinearProbe / PretrainEvaluator) by picking the first # input point in each token — matches PT-v3m2's `head_indices` # semantics (point_transformer_v3m2_sonata.py:498,526). if "segment_motif" in point.keys() or "segment" in point.keys(): input_idx = torch.arange(point.feat.shape[0], device=point.feat.device) head_indices, _ = torch_scatter.scatter_min( input_idx, point_to_token, dim=0, dim_size=T ) for label_key in ("segment_motif", "segment"): if label_key in point.keys(): coarse_dict[label_key] = point[label_key][head_indices] # Drop None-valued keys so addict-style Point doesn't store them. coarse_dict = {k: v for k, v in coarse_dict.items() if v is not None} return Point(coarse_dict)