Source code for pimm.models.point_transformer_v3.point_transformer_v3m8

"""Point Transformer V3 with 3D RoPE, bottleneck CPE, and QK-Norm. Mostly based on Utonia's
(arXiv:2603.03283) PTv3 implementation:

https://github.com/Pointcept/Pointcept/blob/main/pointcept/models/point_transformer_v3/point_transformer_v3m3_utonia.py

Registered as both PT-v3m4 and PT-v3m8.
"""

from spconv import constants

constants.SPCONV_ALLOW_TF32 = True
import spconv.pytorch as spconv
import torch
import torch.nn as nn
import torch_scatter
import pointops
from addict import Dict
from timm.layers import DropPath
from torch.nn.init import trunc_normal_

from pimm.models.builder import MODELS
from pimm.models.modules import PointModule, PointSequential
from pimm.models.utils.misc import offset2bincount
from pimm.models.utils.structure import Point

try:
    from flash_attn import flash_attn_varlen_func
except ImportError:
    flash_attn_varlen_func = None


class Point3DRoPE(nn.Module):
    """3D rotary position embedding for serialized point cloud attention.

    Splits each head into 3 equal sub-vectors (x, y, z) and applies 1D RoPE
    per axis. head_dim must be divisible by 6 (3 axes * 2 for rotation pairs).

    Optional coordinate augmentation during training (DINOv3-style):
      - jitter: axis-wise multiplicative perturbation, j = exp(U(-log γ, log γ)^3)
      - rescale: isotropic multiplicative perturbation, r = exp(U(-log η, log η))
    """

    def __init__(self, head_dim, base=10000, jitter_degree=None, rescale_degree=None):
        super().__init__()
        assert (
            head_dim % 6 == 0
        ), f"head_dim must be divisible by 6 for 3D RoPE, got {head_dim}"
        self.head_dim = head_dim
        self.chunk_dim = head_dim // 3
        self.base = base
        self.jitter_degree = jitter_degree
        self.rescale_degree = rescale_degree

        inv_freq = 1.0 / (
            self.base ** (torch.arange(0, self.chunk_dim, 2).float() / self.chunk_dim)
        )
        self.register_buffer("inv_freq", inv_freq)

    @torch.no_grad()
    def _augment_coords(self, xyz):
        if not self.training:
            return xyz

        if self.jitter_degree is not None and self.jitter_degree > 1:
            log_gamma = torch.tensor(
                self.jitter_degree, device=xyz.device, dtype=xyz.dtype
            ).log()
            eps_j = torch.empty(3, device=xyz.device, dtype=xyz.dtype).uniform_(
                -log_gamma, log_gamma
            )
            xyz = xyz * eps_j.exp()

        if self.rescale_degree is not None and self.rescale_degree > 1:
            log_eta = torch.tensor(
                self.rescale_degree, device=xyz.device, dtype=xyz.dtype
            ).log()
            eps_s = torch.empty(1, device=xyz.device, dtype=xyz.dtype).uniform_(
                -log_eta, log_eta
            )
            xyz = xyz * eps_s.exp()

        return xyz

    def _compute_cos_sin(self, xyz):
        """xyz: (..., 3) -> cos, sin each (..., head_dim)."""
        x, y, z = xyz[..., 0:1], xyz[..., 1:2], xyz[..., 2:3]

        emb_x = x * self.inv_freq
        emb_y = y * self.inv_freq
        emb_z = z * self.inv_freq

        emb_x = torch.cat((emb_x, emb_x), dim=-1)
        emb_y = torch.cat((emb_y, emb_y), dim=-1)
        emb_z = torch.cat((emb_z, emb_z), dim=-1)

        emb = torch.cat((emb_x, emb_y, emb_z), dim=-1)
        return emb.cos(), emb.sin()

    @staticmethod
    def _rotate_half(x):
        x1, x2 = x.chunk(2, dim=-1)
        return torch.cat((-x2, x1), dim=-1)

    def forward(self, q, k, xyz):
        """Apply 3D RoPE to q and k.

        q, k: (..., H, [K,] D) with head dim at position 1
        xyz:  (matching leading dims, [K,] 3)
        """
        dtype = q.dtype
        xyz = self._augment_coords(xyz)
        cos, sin = self._compute_cos_sin(xyz)

        # broadcast across heads (always dim 1)
        cos = cos.unsqueeze(1)
        sin = sin.unsqueeze(1)

        q_chunks = q.split(self.chunk_dim, dim=-1)
        k_chunks = k.split(self.chunk_dim, dim=-1)
        cos_chunks = cos.split(self.chunk_dim, dim=-1)
        sin_chunks = sin.split(self.chunk_dim, dim=-1)

        q_out, k_out = [], []
        for i in range(3):
            q_out.append(
                q_chunks[i] * cos_chunks[i]
                + self._rotate_half(q_chunks[i]) * sin_chunks[i]
            )
            k_out.append(
                k_chunks[i] * cos_chunks[i]
                + self._rotate_half(k_chunks[i]) * sin_chunks[i]
            )
        return torch.cat(q_out, dim=-1).to(dtype), torch.cat(k_out, dim=-1).to(dtype)


class LayerScale(nn.Module):
    def __init__(
        self,
        dim: int,
        init_values: float = 1e-5,
        inplace: bool = False,
    ) -> None:
        super().__init__()
        self.inplace = inplace
        self.gamma = nn.Parameter(init_values * torch.ones(dim))

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return x.mul_(self.gamma) if self.inplace else x * self.gamma


class RPE(torch.nn.Module):
    def __init__(self, patch_size, num_heads):
        super().__init__()
        self.patch_size = patch_size
        self.num_heads = num_heads
        self.pos_bnd = int((4 * patch_size) ** (1 / 3) * 2)
        self.rpe_num = 2 * self.pos_bnd + 1
        self.rpe_table = torch.nn.Parameter(torch.zeros(3 * self.rpe_num, num_heads))
        torch.nn.init.trunc_normal_(self.rpe_table, std=0.02)

    def forward(self, coord):
        idx = (
            coord.clamp(-self.pos_bnd, self.pos_bnd)  # clamp into bnd
            + self.pos_bnd  # relative position to positive index
            + torch.arange(3, device=coord.device) * self.rpe_num  # x, y, z stride
        )
        out = self.rpe_table.index_select(0, idx.reshape(-1))
        out = out.view(idx.shape + (-1,)).sum(3)
        out = out.permute(0, 3, 1, 2)  # (N, K, K, H) -> (N, H, K, K)
        return out


class SerializedAttention(PointModule):
    def __init__(
        self,
        channels,
        num_heads,
        patch_size,
        qk_norm=False,
        qkv_bias=True,
        qk_scale=None,
        attn_drop=0.0,
        proj_drop=0.0,
        order_index=0,
        enable_rpe=False,
        enable_flash=True,
        upcast_attention=True,
        upcast_softmax=True,
        rope_base=10,
        rope_jitter=None,
        rope_rescale=None,
    ):
        super().__init__()
        assert channels % num_heads == 0
        self.channels = channels
        self.num_heads = num_heads
        head_dim = int(channels // num_heads)
        self.scale = qk_scale or head_dim**-0.5
        self.order_index = order_index
        self.upcast_attention = upcast_attention
        self.upcast_softmax = upcast_softmax
        self.enable_rpe = enable_rpe
        self.enable_flash = enable_flash
        if enable_flash:
            assert (
                enable_rpe is False
            ), "Set enable_rpe to False when enable Flash Attention"
            assert (
                upcast_attention is False
            ), "Set upcast_attention to False when enable Flash Attention"
            assert (
                upcast_softmax is False
            ), "Set upcast_softmax to False when enable Flash Attention"
            self.patch_size = patch_size
            self.attn_drop = attn_drop
        else:
            self.patch_size_max = patch_size
            self.patch_size = 0
            self.attn_drop = torch.nn.Dropout(attn_drop)

        if qk_norm:
            self.q_norm = nn.LayerNorm(head_dim, eps=1e-6)
            self.k_norm = nn.LayerNorm(head_dim, eps=1e-6)
        else:
            self.q_norm = None
            self.k_norm = None

        self.qkv = torch.nn.Linear(channels, channels * 3, bias=qkv_bias)
        self.proj = torch.nn.Linear(channels, channels)
        self.proj_drop = torch.nn.Dropout(proj_drop)
        self.softmax = torch.nn.Softmax(dim=-1)
        self.rpe = RPE(patch_size, num_heads) if self.enable_rpe else None
        self.rope = Point3DRoPE(
            head_dim,
            rope_base,
            jitter_degree=rope_jitter,
            rescale_degree=rope_rescale,
        )

    @torch.no_grad()
    def get_rel_pos(self, point, order):
        K = self.patch_size
        rel_pos_key = f"rel_pos_{self.order_index}"
        if rel_pos_key not in point.keys():
            grid_coord = point.grid_coord[order]
            grid_coord = grid_coord.reshape(-1, K, 3)
            point[rel_pos_key] = grid_coord.unsqueeze(2) - grid_coord.unsqueeze(1)
        return point[rel_pos_key]

    @torch.no_grad()
    def get_padding_and_inverse(self, point):
        pad_key = "pad"
        unpad_key = "unpad"
        cu_seqlens_key = "cu_seqlens_key"
        if (
            pad_key not in point.keys()
            or unpad_key not in point.keys()
            or cu_seqlens_key not in point.keys()
        ):
            if self.patch_size == -1:
                cu_seqlens = torch.cat([point.offset.new_zeros(1), point.offset]).int()
                point[pad_key] = None
                point[unpad_key] = None
                point[cu_seqlens_key] = cu_seqlens
                return point[pad_key], point[unpad_key], point[cu_seqlens_key]

            offset = point.offset
            bincount = offset2bincount(offset)
            bincount_pad = (
                torch.div(
                    bincount + self.patch_size - 1,
                    self.patch_size,
                    rounding_mode="trunc",
                )
                * self.patch_size
            )
            # only pad point when num of points larger than patch_size
            mask_pad = bincount > self.patch_size
            bincount_pad = ~mask_pad * bincount + mask_pad * bincount_pad
            _offset = nn.functional.pad(offset, (1, 0))
            _offset_pad = nn.functional.pad(torch.cumsum(bincount_pad, dim=0), (1, 0))
            pad = torch.arange(_offset_pad[-1], device=offset.device)
            unpad = torch.arange(_offset[-1], device=offset.device)
            cu_seqlens = []
            for i in range(len(offset)):
                unpad[_offset[i] : _offset[i + 1]] += _offset_pad[i] - _offset[i]
                if bincount[i] != bincount_pad[i]:
                    pad[
                        _offset_pad[i + 1]
                        - self.patch_size
                        + (bincount[i] % self.patch_size) : _offset_pad[i + 1]
                    ] = pad[
                        _offset_pad[i + 1]
                        - 2 * self.patch_size
                        + (bincount[i] % self.patch_size) : _offset_pad[i + 1]
                        - self.patch_size
                    ]
                pad[_offset_pad[i] : _offset_pad[i + 1]] -= _offset_pad[i] - _offset[i]
                cu_seqlens.append(
                    torch.arange(
                        _offset_pad[i],
                        _offset_pad[i + 1],
                        step=self.patch_size,
                        dtype=torch.int32,
                        device=offset.device,
                    )
                )
            point[pad_key] = pad
            point[unpad_key] = unpad
            point[cu_seqlens_key] = nn.functional.pad(
                torch.concat(cu_seqlens), (0, 1), value=_offset_pad[-1]
            )
        return point[pad_key], point[unpad_key], point[cu_seqlens_key]

    def forward(self, point, return_attn=False):
        if not self.enable_flash:
            self.patch_size = min(
                offset2bincount(point.offset).min().tolist(), self.patch_size_max
            )

        H = self.num_heads
        K = self.patch_size
        C = self.channels

        pad, unpad, cu_seqlens = self.get_padding_and_inverse(point)
        max_seqlen = cu_seqlens[-1] if self.patch_size == -1 else self.patch_size

        order = point.serialized_order[self.order_index]
        if pad is not None:
            order = order[pad]
            inverse = unpad[point.serialized_inverse[self.order_index]]

        qkv = self.qkv(point.feat)[order]
        rope_coord = point.coord[order].clone()

        if not self.enable_flash:
            # (N', K, 3, H, C') => (3, N', H, K, C')
            q, k, v = (
                qkv.reshape(-1, K, 3, H, C // H).permute(2, 0, 3, 1, 4).unbind(dim=0)
            )

            # apply QK norm before RoPE!
            if self.q_norm:
                q = self.q_norm(q)
            if self.k_norm:
                k = self.k_norm(k)

            # q: (P, H, K, D), rope_coord: (N_pad, 3) -> (P, K, 3)
            q, k = self.rope(q, k, rope_coord.reshape(-1, K, 3))

            if self.upcast_attention:
                q = q.float()
                k = k.float()
            attn = (q * self.scale) @ k.transpose(-2, -1)
            if self.enable_rpe:
                attn = attn + self.rpe(self.get_rel_pos(point, order))
            if self.upcast_softmax:
                attn = attn.float()
            attn = self.softmax(attn)
            attn = self.attn_drop(attn).to(qkv.dtype)
            feat = (attn @ v).transpose(1, 2).reshape(-1, C)
        else:
            # split q/k/v so we can apply RoPE before flash attention
            qkv_bf = qkv.to(torch.bfloat16).reshape(-1, 3, H, C // H)
            q, k, v = qkv_bf.unbind(dim=1)  # each (N_pad, H, D)
            if self.q_norm:  # before rope!
                q = self.q_norm(q)
            if self.k_norm:
                k = self.k_norm(k)
            q, k = self.rope(q, k, rope_coord)  # rope_coord: (N_pad, 3)
            feat = flash_attn_varlen_func(
                q, k, v,
                cu_seqlens_q=cu_seqlens,
                cu_seqlens_k=cu_seqlens,
                max_seqlen_q=max_seqlen,
                max_seqlen_k=max_seqlen,
                dropout_p=self.attn_drop if self.training else 0,
                softmax_scale=self.scale,
            ).reshape(-1, C)
            feat = feat.to(qkv.dtype)

        if pad is not None:
            feat = feat[inverse]

        feat = self.proj(feat)
        feat = self.proj_drop(feat)
        point.feat = feat
        return point


class MLP(nn.Module):
    def __init__(
        self,
        in_channels,
        hidden_channels=None,
        out_channels=None,
        act_layer=nn.GELU,
        drop=0.0,
    ):
        super().__init__()
        out_channels = out_channels or in_channels
        hidden_channels = hidden_channels or in_channels
        self.fc1 = nn.Linear(in_channels, hidden_channels)
        self.act = act_layer()
        self.fc2 = nn.Linear(hidden_channels, out_channels)
        self.drop = nn.Dropout(drop)

    def forward(self, x):
        x = self.fc1(x)
        x = self.act(x)
        x = self.drop(x)
        x = self.fc2(x)
        x = self.drop(x)
        return x


class BottleneckCPE(PointModule):
    """CPE with an optional channel bottleneck around the sparse convolution.

    Equal input and CPE widths skip the input projection, producing the former
    full-width CPE path: sparse convolution, output projection, normalization.
    """

    def __init__(self, channels, cpe_channels, cpe_indice_key, norm_layer=nn.LayerNorm):
        super().__init__()
        self.down = (
            nn.Identity()
            if channels == cpe_channels
            else nn.Linear(channels, cpe_channels)
        )
        self.conv = spconv.SubMConv3d(
            cpe_channels,
            cpe_channels,
            kernel_size=3,
            bias=True,
            indice_key=cpe_indice_key,
        )
        self.up = nn.Linear(cpe_channels, channels)
        self.norm = norm_layer(channels)

    def forward(self, point: Point):
        point.feat = self.down(point.feat)
        point.sparse_conv_feat = point.sparse_conv_feat.replace_feature(point.feat)
        point.sparse_conv_feat = self.conv(point.sparse_conv_feat)
        point.feat = point.sparse_conv_feat.features
        point.feat = self.up(point.feat)
        point.feat = self.norm(point.feat)
        return point


class Block(PointModule):
    def __init__(
        self,
        channels,
        num_heads,
        patch_size=48,
        mlp_ratio=4.0,
        qk_norm=False,
        qkv_bias=True,
        qk_scale=None,
        attn_drop=0.0,
        proj_drop=0.0,
        drop_path=0.0,
        layer_scale=None,
        norm_layer=nn.LayerNorm,
        act_layer=nn.GELU,
        pre_norm=True,
        order_index=0,
        cpe_indice_key=None,
        enable_rpe=False,
        enable_flash=True,
        upcast_attention=True,
        upcast_softmax=True,
        enable_cpe=True,
        cpe_channels=96,
        rope_base=10,
        rope_jitter=None,
        rope_rescale=None,
    ):
        super().__init__()
        self.channels = channels
        self.pre_norm = pre_norm

        if enable_cpe:
            self.cpe = BottleneckCPE(
                channels=channels,
                cpe_channels=cpe_channels,
                cpe_indice_key=cpe_indice_key,
                norm_layer=norm_layer,
            )
        else:
            self.cpe = None

        self.norm1 = PointSequential(norm_layer(channels))
        self.ls1 = PointSequential(
            LayerScale(channels, init_values=layer_scale)
            if layer_scale is not None
            else nn.Identity()
        )
        self.attn = SerializedAttention(
            channels=channels,
            patch_size=patch_size,
            num_heads=num_heads,
            qk_norm=qk_norm,
            qkv_bias=qkv_bias,
            qk_scale=qk_scale,
            attn_drop=attn_drop,
            proj_drop=proj_drop,
            order_index=order_index,
            enable_rpe=enable_rpe,
            enable_flash=enable_flash,
            upcast_attention=upcast_attention,
            upcast_softmax=upcast_softmax,
            rope_base=rope_base,
            rope_jitter=rope_jitter,
            rope_rescale=rope_rescale,
        )
        self.norm2 = PointSequential(norm_layer(channels))
        self.ls2 = PointSequential(
            LayerScale(channels, init_values=layer_scale)
            if layer_scale is not None
            else nn.Identity()
        )
        self.mlp = PointSequential(
            MLP(
                in_channels=channels,
                hidden_channels=int(channels * mlp_ratio),
                out_channels=channels,
                act_layer=act_layer,
                drop=proj_drop,
            )
        )
        self.drop_path = PointSequential(
            DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
        )

    def forward(self, point: Point):
        if self.cpe is not None:
            with torch.autocast(device_type="cuda", enabled=False):
                orig_dtype = point.feat.dtype

                # => float32
                point.feat = point.feat.to(dtype=torch.float32)
                point.sparse_conv_feat = point.sparse_conv_feat.replace_feature(
                    point.sparse_conv_feat.features.to(torch.float32)
                )

                # perform fwd
                shortcut = point.feat
                point = self.cpe(point)
                point.feat = shortcut + point.feat

                # => original dtype
                point.feat = point.feat.to(orig_dtype)
        shortcut = point.feat

        if self.pre_norm:
            point = self.norm1(point)
        point = self.drop_path(self.ls1(self.attn(point)))
        point.feat = shortcut + point.feat
        if not self.pre_norm:
            point = self.norm1(point)

        shortcut = point.feat
        if self.pre_norm:
            point = self.norm2(point)
        point = self.drop_path(self.ls2(self.mlp(point)))
        point.feat = shortcut + point.feat
        if not self.pre_norm:
            point = self.norm2(point)
        point.sparse_conv_feat = point.sparse_conv_feat.replace_feature(point.feat)
        return point


class GridPooling(PointModule):
    def __init__(
        self,
        in_channels,
        out_channels,
        stride=2,
        norm_layer=None,
        act_layer=None,
        reduce="max",
        shuffle_orders=True,
        traceable=True,  # record parent and cluster
    ):
        super().__init__()
        self.in_channels = in_channels
        self.out_channels = out_channels

        self.stride = stride
        assert reduce in ["sum", "mean", "min", "max"]
        self.reduce = reduce
        self.shuffle_orders = shuffle_orders
        self.traceable = traceable

        self.proj = nn.Linear(in_channels, out_channels)
        if norm_layer is not None:
            self.norm = PointSequential(norm_layer(out_channels))
        if act_layer is not None:
            self.act = PointSequential(act_layer())

    def forward(self, point: Point):
        if "grid_coord" in point.keys():
            grid_coord = point.grid_coord
        elif {"coord", "grid_size"}.issubset(point.keys()):
            grid_coord = torch.div(
                point.coord - point.coord.min(0)[0],
                point.grid_size,
                rounding_mode="trunc",
            ).int()
        else:
            raise AssertionError(
                "[gird_coord] or [coord, grid_size] should be include in the Point"
            )
        grid_coord = torch.div(grid_coord, self.stride, rounding_mode="trunc")
        grid_coord = grid_coord | point.batch.view(-1, 1) << 48
        grid_coord, cluster, counts = torch.unique(
            grid_coord,
            sorted=True,
            return_inverse=True,
            return_counts=True,
            dim=0,
        )
        grid_coord = grid_coord & ((1 << 48) - 1)
        # indices of point sorted by cluster, for torch_scatter.segment_csr
        _, indices = torch.sort(cluster)
        # index pointer for sorted point, for torch_scatter.segment_csr
        idx_ptr = torch.cat([counts.new_zeros(1), torch.cumsum(counts, dim=0)])
        # head_indices of each cluster, for reduce attr e.g. code, batch
        head_indices = indices[idx_ptr[:-1]]
        point_dict = Dict(
            feat=torch_scatter.segment_csr(
                self.proj(point.feat)[indices], idx_ptr, reduce=self.reduce
            ),
            coord=torch_scatter.segment_csr(
                point.coord[indices], idx_ptr, reduce="mean"
            ),
            grid_coord=grid_coord,
            batch=point.batch[head_indices],
        )
        if "origin_coord" in point.keys():
            point_dict["origin_coord"] = torch_scatter.segment_csr(
                point.origin_coord[indices], idx_ptr, reduce="mean"
            )
        if "condition" in point.keys():
            point_dict["condition"] = point.condition
        if "context" in point.keys():
            point_dict["context"] = point.context
        if "name" in point.keys():
            point_dict["name"] = point.name
        if "split" in point.keys():
            point_dict["split"] = point.split
        if "color" in point.keys():
            point_dict["color"] = torch_scatter.segment_csr(
                point.color[indices], idx_ptr, reduce="mean"
            )
        if "segment_motif" in point.keys():
            point_dict["segment_motif"] = point.segment_motif[head_indices]
        if "grid_size" in point.keys():
            point_dict["grid_size"] = point.grid_size * self.stride

        if self.traceable:
            point_dict["pooling_inverse"] = cluster
            point_dict["pooling_parent"] = point
        order = point.order
        point = Point(point_dict)
        if self.norm is not None:
            point = self.norm(point)
        if self.act is not None:
            point = self.act(point)
        point.serialization(order=order, shuffle_orders=self.shuffle_orders)
        point.sparsify()
        return point


class GridUnpooling(PointModule):
    def __init__(
        self,
        in_channels,
        skip_channels,
        out_channels,
        norm_layer=None,
        act_layer=None,
        traceable=False,  # record parent and cluster
    ):
        super().__init__()
        self.proj = PointSequential(nn.Linear(in_channels, out_channels))
        self.proj_skip = PointSequential(nn.Linear(skip_channels, out_channels))

        if norm_layer is not None:
            self.proj.add(norm_layer(out_channels))
            self.proj_skip.add(norm_layer(out_channels))

        if act_layer is not None:
            self.proj.add(act_layer())
            self.proj_skip.add(act_layer())

        self.traceable = traceable

    def forward(self, point):
        assert "pooling_parent" in point.keys()
        assert "pooling_inverse" in point.keys()
        parent = point.pop("pooling_parent")
        inverse = point.pooling_inverse
        feat = point.feat

        parent = self.proj_skip(parent)
        parent.feat = parent.feat + self.proj(point).feat[inverse]
        parent.sparse_conv_feat = parent.sparse_conv_feat.replace_feature(parent.feat)

        if self.traceable:
            point.feat = feat
            parent["unpooling_parent"] = point
            parent["unpooling_inverse"] = inverse
        return parent


class Embedding(PointModule):
    def __init__(
        self,
        in_channels,
        embed_channels,
        norm_layer=None,
        act_layer=None,
        mask_token=False,
    ):
        super().__init__()
        self.in_channels = in_channels
        self.embed_channels = embed_channels

        self.stem = PointSequential(linear=nn.Linear(in_channels, embed_channels))
        if norm_layer is not None:
            self.stem.add(norm_layer(embed_channels), name="norm")
        if act_layer is not None:
            self.stem.add(act_layer(), name="act")

        if mask_token:
            self.mask_token = nn.Parameter(torch.zeros(1, embed_channels))
        else:
            self.mask_token = None

    def forward(self, point: Point):
        point = self.stem(point)
        if "mask" in point.keys():
            point.feat = torch.where(
                point.mask.unsqueeze(-1),
                self.mask_token.to(point.feat.dtype),
                point.feat,
            )
        return point


[docs] @MODELS.register_module(["PT-v3m4", "PT-v3m8"]) class PointTransformerV3(PointModule): """Point Transformer V3 with 3D RoPE and bottleneck CPE.""" def __init__( self, in_channels=6, order=("z", "z-trans"), stride=(2, 2, 2, 2), enc_depths=(2, 2, 2, 6, 2), enc_channels=(32, 64, 128, 256, 512), enc_num_head=(2, 4, 8, 16, 32), enc_patch_size=(48, 48, 48, 48, 48), dec_depths=(2, 2, 2, 2), dec_channels=(64, 64, 128, 256), dec_num_head=(4, 4, 8, 16), dec_patch_size=(48, 48, 48, 48), mlp_ratio=4, qk_norm=False, qkv_bias=True, qk_scale=None, attn_drop=0.0, proj_drop=0.0, drop_path=0.3, layer_scale=None, pre_norm=True, shuffle_orders=True, enable_rpe=False, enable_flash=True, enable_cpe=True, upcast_attention=False, upcast_softmax=False, traceable=False, mask_token=False, enc_mode=False, freeze_encoder=False, cpe_first_layer_only=False, enc_cpe_channels=(96, 96, 96, 96, 96), dec_cpe_channels=(96, 96, 96, 96), embedding_act_layer=True, add_iso_features=False, iso_k=8, iso_r=None, rope_base=10, rope_jitter=None, rope_rescale=None, ): super().__init__() self.num_stages = len(enc_depths) self.order = [order] if isinstance(order, str) else order self.shuffle_orders = shuffle_orders self.enc_mode = enc_mode self.freeze_encoder = freeze_encoder self.embedding_act_layer = embedding_act_layer self.add_iso_features = add_iso_features self.iso_k = iso_k self.iso_r = iso_r self.rope_base = rope_base self.rope_jitter = rope_jitter self.rope_rescale = rope_rescale assert self.num_stages == len(stride) + 1 assert self.num_stages == len(enc_depths) assert self.num_stages == len(enc_channels) assert self.num_stages == len(enc_num_head) assert self.num_stages == len(enc_patch_size) assert self.enc_mode or self.num_stages == len(dec_depths) + 1 assert self.enc_mode or self.num_stages == len(dec_channels) + 1 assert self.enc_mode or self.num_stages == len(dec_num_head) + 1 assert self.enc_mode or self.num_stages == len(dec_patch_size) + 1 # normalization layer ln_layer = nn.LayerNorm # activation layers act_layer = nn.GELU self.embedding = Embedding( in_channels=in_channels + (1 if self.add_iso_features else 0), embed_channels=enc_channels[0], norm_layer=ln_layer, act_layer=act_layer if self.embedding_act_layer else None, mask_token=mask_token, ) # encoder enc_drop_path = [ x.item() for x in torch.linspace(0, drop_path, sum(enc_depths)) ] self.enc = PointSequential() for s in range(self.num_stages): enc_drop_path_ = enc_drop_path[ sum(enc_depths[:s]) : sum(enc_depths[: s + 1]) ] enc = PointSequential() if s > 0: enc.add( GridPooling( in_channels=enc_channels[s - 1], out_channels=enc_channels[s], stride=stride[s - 1], norm_layer=ln_layer, act_layer=act_layer, ), name="down", ) for i in range(enc_depths[s]): enc.add( Block( channels=enc_channels[s], num_heads=enc_num_head[s], patch_size=enc_patch_size[s], mlp_ratio=mlp_ratio, qk_norm=qk_norm, qkv_bias=qkv_bias, qk_scale=qk_scale, attn_drop=attn_drop, proj_drop=proj_drop, drop_path=enc_drop_path_[i], layer_scale=layer_scale, norm_layer=ln_layer, act_layer=act_layer, pre_norm=pre_norm, order_index=i % len(self.order), cpe_indice_key=f"stage{s}", enable_rpe=enable_rpe, enable_flash=enable_flash, upcast_attention=upcast_attention, upcast_softmax=upcast_softmax, enable_cpe=(False if cpe_first_layer_only and i != 0 else True) and enable_cpe, cpe_channels=enc_cpe_channels[s], rope_base=rope_base, rope_jitter=rope_jitter, rope_rescale=rope_rescale, ), name=f"block{i}", ) if len(enc) != 0: self.enc.add(module=enc, name=f"enc{s}") # decoder if not self.enc_mode: dec_drop_path = [ x.item() for x in torch.linspace(0, drop_path, sum(dec_depths)) ] self.dec = PointSequential() dec_channels = list(dec_channels) + [enc_channels[-1]] for s in reversed(range(self.num_stages - 1)): dec_drop_path_ = dec_drop_path[ sum(dec_depths[:s]) : sum(dec_depths[: s + 1]) ] dec_drop_path_.reverse() dec = PointSequential() dec.add( GridUnpooling( in_channels=dec_channels[s + 1], skip_channels=enc_channels[s], out_channels=dec_channels[s], norm_layer=ln_layer, act_layer=act_layer, traceable=traceable, ), name="up", ) for i in range(dec_depths[s]): dec.add( Block( channels=dec_channels[s], num_heads=dec_num_head[s], patch_size=dec_patch_size[s], mlp_ratio=mlp_ratio, qk_norm=qk_norm, qkv_bias=qkv_bias, qk_scale=qk_scale, attn_drop=attn_drop, proj_drop=proj_drop, drop_path=dec_drop_path_[i], layer_scale=layer_scale, norm_layer=ln_layer, act_layer=act_layer, pre_norm=pre_norm, order_index=i % len(self.order), cpe_indice_key=f"stage{s}", enable_rpe=enable_rpe, enable_flash=enable_flash, upcast_attention=upcast_attention, upcast_softmax=upcast_softmax, enable_cpe=( False if cpe_first_layer_only and i != 0 else True ) and enable_cpe, cpe_channels=dec_cpe_channels[s], rope_base=rope_base, rope_jitter=rope_jitter, rope_rescale=rope_rescale, ), name=f"block{i}", ) self.dec.add(module=dec, name=f"dec{s}") if self.freeze_encoder: for p in self.embedding.parameters(): p.requires_grad = False for p in self.enc.parameters(): p.requires_grad = False self.apply(self._init_weights) @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 isinstance(module, spconv.SubMConv3d): trunc_normal_(module.weight, std=0.02) if module.bias is not None: nn.init.zeros_(module.bias) elif isinstance(module, nn.LayerNorm): nn.init.ones_(module.weight) nn.init.zeros_(module.bias)
[docs] def forward(self, data_dict): point = Point(data_dict) point = self.embedding(point) point.serialization(order=self.order, shuffle_orders=self.shuffle_orders) point.sparsify() point = self.enc(point) if not self.enc_mode: point = self.dec(point) return point