# MIT License # # Copyright (c) Authors of # "PRoPE: Projective Positional Encoding for Multiview Transformers" # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # How to use: # # 1. Easiest way (fast): # attn = PropeDotProductAttention(...) # o = attn(q, k, v, viewmats, Ks) # # 2. More flexible way (fast): # attn = PropeDotProductAttention(...) # attn._precompute_and_cache_apply_fns(viewmats, Ks) # q = attn._apply_to_q(q) # k = attn._apply_to_kv(k) # v = attn._apply_to_kv(v) # o = F.scaled_dot_product_attention(q, k, v, **kwargs) # o = attn._apply_to_o(o) # # 3. The most flexible way (but slower because repeated computation of RoPE coefficients): # o = prope_dot_product_attention(q, k, v, ...) from functools import partial from typing import Callable, Optional, Tuple, List from einops import rearrange import torch import torch.nn.functional as F def prope_attention(self_attn, x, cam_emb): q = self_attn.norm_q(self_attn.q(x)) k = self_attn.norm_k(self_attn.k(x)) v = self_attn.v(x) # TODO: Hope gradients don't explode! x_prop_self_attn = prope_dot_product_attention(self_attn.attn, q, k, v, **cam_emb) x_prop_self_attn = self_attn.o(x_prop_self_attn) return x_prop_self_attn def prope_dot_product_attention( attn_module: Callable, q: torch.Tensor, # (batch, num_heads, seqlen, head_dim) k: torch.Tensor, # (batch, num_heads, seqlen, head_dim) v: torch.Tensor, # (batch, num_heads, seqlen, head_dim) *, viewmats: torch.Tensor, # (batch, cameras, 4, 4) Ks: Optional[torch.Tensor], # (batch, cameras, 3, 3) patches_x: int, # How many patches wide is each image? patches_y: int, # How many patches tall is each image? image_width: int, # Width of the image. Used to normalize intrinsics. image_height: int, # Height of the image. Used to normalize intrinsics. coeffs_x: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, coeffs_y: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, ) -> torch.Tensor: """Similar to torch.nn.functional.scaled_dot_product_attention, but applies PRoPE-style positional encoding. Currently, we assume that the sequence length is equal to: cameras * patches_x * patches_y And token ordering allows the `(seqlen,)` axis to be reshaped into `(cameras, patches_x, patches_y)`. """ # We're going to assume self-attention: all inputs are the same shape. q = rearrange(q, "b s (n d) -> b n s d", n=attn_module.num_heads) k = rearrange(k, "b s (n d) -> b n s d", n=attn_module.num_heads) v = rearrange(v, "b s (n d) -> b n s d", n=attn_module.num_heads) (batch, num_heads, seqlen, head_dim) = q.shape cameras = viewmats.shape[1] assert q.shape == k.shape == v.shape assert viewmats.shape == (batch, cameras, 4, 4) assert Ks is None or Ks.shape == (batch, cameras, 3, 3) assert seqlen == cameras * patches_x * patches_y apply_fn_q, apply_fn_kv, apply_fn_o = _prepare_apply_fns( head_dim=head_dim, viewmats=viewmats, Ks=Ks, patches_x=patches_x, patches_y=patches_y, image_width=image_width, image_height=image_height, coeffs_x=coeffs_x, coeffs_y=coeffs_y, ) # print("\n\n\nqkv dtypes before:", q.dtype, k.dtype, v.dtype) q = rearrange(apply_fn_q(q), "b n s d -> b s (n d)") k = rearrange(apply_fn_kv(k), "b n s d -> b s (n d)") v = rearrange(apply_fn_kv(v), "b n s d -> b s (n d)") # print("\n\n\nqkv dtypes after:", q.dtype, k.dtype, v.dtype) out = attn_module(q, k, v) out = rearrange(out, "b s (n d) -> b n s d", n=attn_module.num_heads) out = apply_fn_o(out) assert out.shape == (batch, num_heads, seqlen, head_dim) out = rearrange(out, "b n s d -> b s (n d)") return out def _prepare_apply_fns( head_dim: int, # Q/K/V will have this last dimension viewmats: torch.Tensor, # (batch, cameras, 4, 4) Ks: Optional[torch.Tensor], # (batch, cameras, 3, 3) patches_x: int, # How many patches wide is each image? patches_y: int, # How many patches tall is each image? image_width: int, # Width of the image. Used to normalize intrinsics. image_height: int, # Height of the image. Used to normalize intrinsics. coeffs_x: Optional[torch.Tensor] = None, coeffs_y: Optional[torch.Tensor] = None, ) -> Tuple[ Callable[[torch.Tensor], torch.Tensor], Callable[[torch.Tensor], torch.Tensor], Callable[[torch.Tensor], torch.Tensor], ]: """Prepare transforms for PRoPE-style positional encoding.""" dtype = viewmats.dtype device = viewmats.device (batch, cameras, _, _) = viewmats.shape # Normalize camera intrinsics. if Ks is not None: Ks_norm = torch.zeros_like(Ks) Ks_norm[..., 0, 0] = Ks[..., 0, 0] / image_width Ks_norm[..., 1, 1] = Ks[..., 1, 1] / image_height Ks_norm[..., 0, 2] = Ks[..., 0, 2] / image_width - 0.5 Ks_norm[..., 1, 2] = Ks[..., 1, 2] / image_height - 0.5 Ks_norm[..., 2, 2] = 1.0 del Ks # Compute the camera projection matrices we use in PRoPE. # - K is an `image<-camera` transform. # - viewmats is a `camera<-world` transform. # - P = lift(K) @ viewmats is an `image<-world` transform. P = torch.einsum("...ij,...jk->...ik", _lift_K(Ks_norm), viewmats) P_T = P.transpose(-1, -2) P_inv = torch.einsum( "...ij,...jk->...ik", _invert_SE3(viewmats), _lift_K(_invert_K(Ks_norm)), ) else: # GTA formula. P is `camera<-world` transform. P = viewmats P_T = P.transpose(-1, -2) P_inv = _invert_SE3(viewmats) assert P.shape == P_inv.shape == (batch, cameras, 4, 4) # Precompute cos/sin terms for RoPE. We use tiles/repeats for 'row-major' # broadcasting. if coeffs_x is None: coeffs_x = _rope_precompute_coeffs( torch.tile(torch.arange(patches_x, dtype=dtype, device=device), (patches_y * cameras,)), freq_base=100.0, freq_scale=1.0, feat_dim=head_dim // 4, ) if coeffs_y is None: coeffs_y = _rope_precompute_coeffs( torch.tile( torch.repeat_interleave( torch.arange(patches_y, dtype=dtype, device=device), patches_x ), (cameras,), ), freq_base=100.0, freq_scale=1.0, feat_dim=head_dim // 4, ) # Block-diagonal transforms to the inputs and outputs of the attention operator. assert head_dim % 4 == 0 transforms_q = [ (partial(_apply_tiled_projmat, matrix=P_T), head_dim // 2), (partial(_rope_apply_coeffs, coeffs=coeffs_x), head_dim // 4), (partial(_rope_apply_coeffs, coeffs=coeffs_y), head_dim // 4), ] transforms_kv = [ (partial(_apply_tiled_projmat, matrix=P_inv), head_dim // 2), (partial(_rope_apply_coeffs, coeffs=coeffs_x), head_dim // 4), (partial(_rope_apply_coeffs, coeffs=coeffs_y), head_dim // 4), ] transforms_o = [ (partial(_apply_tiled_projmat, matrix=P), head_dim // 2), (partial(_rope_apply_coeffs, coeffs=coeffs_x, inverse=True), head_dim // 4), (partial(_rope_apply_coeffs, coeffs=coeffs_y, inverse=True), head_dim // 4), ] apply_fn_q = partial(_apply_block_diagonal, func_size_pairs=transforms_q) apply_fn_kv = partial(_apply_block_diagonal, func_size_pairs=transforms_kv) apply_fn_o = partial(_apply_block_diagonal, func_size_pairs=transforms_o) return apply_fn_q, apply_fn_kv, apply_fn_o def _apply_tiled_projmat( feats: torch.Tensor, # (batch, num_heads, seqlen, feat_dim) matrix: torch.Tensor, # (batch, cameras, D, D) ) -> torch.Tensor: """Apply projection matrix to features.""" # - seqlen => (cameras, patches_x * patches_y) # - feat_dim => (feat_dim // 4, 4) (batch, num_heads, seqlen, feat_dim) = feats.shape cameras = matrix.shape[1] assert seqlen > cameras and seqlen % cameras == 0 D = matrix.shape[-1] assert matrix.shape == (batch, cameras, D, D) assert feat_dim % D == 0 return torch.einsum( "bcij,bncpkj->bncpki", matrix, feats.reshape((batch, num_heads, cameras, -1, feat_dim // D, D)), ).reshape(feats.shape) def _rope_precompute_coeffs( positions: torch.Tensor, # (seqlen,) freq_base: float, freq_scale: float, feat_dim: int, ) -> Tuple[torch.Tensor, torch.Tensor]: """Precompute RoPE coefficients.""" assert len(positions.shape) == 1 assert feat_dim % 2 == 0 num_freqs = feat_dim // 2 freqs = freq_scale * ( freq_base ** ( -torch.arange(num_freqs, dtype=positions.dtype, device=positions.device)[None, None, None, :] / num_freqs ) ) angles = positions[None, None, :, None] * freqs # Shape should be: `(batch, num_heads, seqlen, num_freqs)`; we're # broadcasting across `batch` and `num_heads`. assert angles.shape == (1, 1, positions.shape[0], num_freqs) return torch.cos(angles), torch.sin(angles) def _rope_apply_coeffs( feats: torch.Tensor, # (batch, num_heads, seqlen, feat_dim) coeffs: Tuple[torch.Tensor, torch.Tensor], inverse: bool = False, ) -> torch.Tensor: """Apply RoPE coefficients to features. We adopt a 'split' ordering convention. (in contrast to 'interleaved')""" cos, sin = coeffs # We allow (cos, sin) to be either with shape (1, 1, seqlen, feat_dim // 2), # or (1, 1, seqlen_per_image, feat_dim // 2) and we repeat it to # match the shape of feats. if cos.shape[2] != feats.shape[2]: n_repeats = feats.shape[2] // cos.shape[2] cos = cos.repeat(1, 1, n_repeats, 1) sin = sin.repeat(1, 1, n_repeats, 1) assert len(feats.shape) == len(cos.shape) == len(sin.shape) == 4 assert cos.shape[-1] == sin.shape[-1] == feats.shape[-1] // 2 x_in = feats[..., : feats.shape[-1] // 2] y_in = feats[..., feats.shape[-1] // 2 :] return torch.cat( ( [cos * x_in + sin * y_in, -sin * x_in + cos * y_in] if not inverse else [cos * x_in - sin * y_in, sin * x_in + cos * y_in] ), dim=-1, ) def _apply_block_diagonal( feats: torch.Tensor, # (..., dim) func_size_pairs: List[Tuple[Callable[[torch.Tensor], torch.Tensor], int]], ) -> torch.Tensor: """Apply a block-diagonal function to an input array. Each function is specified as a tuple with form: ((Tensor) -> Tensor, int) Where the integer is the size of the input to the function. """ funcs, block_sizes = zip(*func_size_pairs) assert feats.shape[-1] == sum(block_sizes) x_blocks = torch.split(feats, block_sizes, dim=-1) out = torch.cat( [f(x_block) for f, x_block in zip(funcs, x_blocks)], dim=-1, ) assert out.shape == feats.shape, "Input/output shapes should match." return out def _invert_SE3(transforms: torch.Tensor) -> torch.Tensor: """Invert a 4x4 SE(3) matrix.""" assert transforms.shape[-2:] == (4, 4) Rinv = transforms[..., :3, :3].transpose(-1, -2) out = torch.zeros_like(transforms) out[..., :3, :3] = Rinv out[..., :3, 3] = -torch.einsum("...ij,...j->...i", Rinv, transforms[..., :3, 3]) out[..., 3, 3] = 1.0 return out def _lift_K(Ks: torch.Tensor) -> torch.Tensor: """Lift 3x3 matrices to homogeneous 4x4 matrices.""" assert Ks.shape[-2:] == (3, 3) out = torch.zeros(Ks.shape[:-2] + (4, 4), dtype=Ks.dtype, device=Ks.device) out[..., :3, :3] = Ks out[..., 3, 3] = 1.0 return out def _invert_K(Ks: torch.Tensor) -> torch.Tensor: """Invert 3x3 intrinsics matrices. Assumes no skew.""" assert Ks.shape[-2:] == (3, 3) out = torch.zeros_like(Ks) out[..., 0, 0] = 1.0 / Ks[..., 0, 0] out[..., 1, 1] = 1.0 / Ks[..., 1, 1] out[..., 0, 2] = -Ks[..., 0, 2] / Ks[..., 0, 0] out[..., 1, 2] = -Ks[..., 1, 2] / Ks[..., 1, 1] out[..., 2, 2] = 1.0 return out