File size: 14,099 Bytes
68a3286 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | # SPDX-License-Identifier: Apache-2.0
"""TT-friendly equivalent forms of the deployed graph, proven on the CPU before device time is spent (the reference
bundles' ``test_fused_host.py`` discipline, REFERENCE_PATTERNS.md 2.12). Each one is exact in real arithmetic (most
are exact in float32 too, as noted) and differs from :mod:`.model` only by float32 summation order;
``tests/test_reference_rewrites_host.py`` checks them against the literal modules with the real weights.
| form | replaces | why (PLAN.md 2.5) |
|---|---|---|
| :func:`pfn_tile_form` | PFN layer 1 on ``concat[h, tile(max h)]`` | ``relu(max_slots(h @ W1a) + max_slots(h) @ W1b + b1)``: no (N, 20, 64) concat, the per-pillar term added after the slot max (exact: ``relu(. + c)`` is monotonic) |
| :func:`pad_pillar_slots` | 20 point slots per pillar | 32 slots (one tile row block per pillar): slots 20-31 repeat slot 0, so both slot maxima are unchanged (max is idempotent; zero rows there would add ``relu(b0)`` to full pillars) |
| :func:`k_split_weights` | ``shared_conv(concat[d0, d1, d2])`` | ``sum_i conv(d_i, W_i)``: the 56 MB concat never exists (PR:P14, S:transfusion:276) |
| :func:`deblock0_as_linear` | Conv 2x2 stride 2 (deblock0) | space-to-depth + linear 256 -> 128 (S:transfusion:271), in case ``conv2d`` loses at 384^2 |
| :func:`deconv_as_linear` | ConvTranspose 2x2 s2 (deblock2) | linear + depth-to-space (the k = 2 device form is ``conv_transpose2d``, PR:P5; this is its oracle) |
| :func:`local_max_masks` | MaxPool p0 + zero border + equality | C22's masked chain ``heat * (m_pass + (heat == maxpool3x3_p1(heat)) * m_eq)`` (exact, PR:P6) |
| :func:`query_pos_from_index` | ``bev_pos`` gather | ``(p % 192 + 0.5, p // 192 + 0.5)`` in float32 (bit-exact; never a bf16 table: S:transfusion:241) |
| class table (``HeadParams.class_table``) | Conv1d of a one-hot | a (5, 128) row gather (bit-exact) |
| :func:`cross_attention_kv_tables` | ``(lidar + pe(bev_pos)) @ Wk + bk`` | ``lidar @ Wk + CK`` with the constant ``CK = pe(bev_pos) @ Wk + bk`` (same for V; S:transfusion:279) |
| :func:`padded_head_attention` | 8 heads x 16 | heads zero-padded to 32 in the projection weights (``ttaw.ops.attention.pad_head_columns`` / ``pad_head_rows``) with ``scale = 1/4`` (SDPA rejects D = 16; exact: PR:P7) |
| :func:`merged_prediction_head_weights` | six Conv1d 128 -> 64 + ReLU -> C heads | one 128 -> 384 + ReLU and a block-diagonal 384 -> 15 (padded to 32) |
numpy weight transforms; torch only inside the ``*_torch`` forwards (imported lazily).
"""
from __future__ import annotations
from typing import Dict, List, Sequence, Tuple
import numpy as np
from ..ttaw.ops.attention import pad_head_columns, pad_head_rows
from .weights import AttentionParams, HeadParams, Layer
__all__ = ["pfn_tile_form", "pad_pillar_slots", "PILLAR_TILE_SLOTS", "PFN_IN_PADDED", "k_split_weights",
"k_split_torch", "deblock0_as_linear", "space_to_depth_nhwc", "deconv_as_linear", "depth_to_space_nhwc",
"local_max_masks", "local_max_masked_torch", "query_pos_from_index", "cross_attention_kv_tables",
"padded_head_attention", "pad_head_dim_rows", "merged_prediction_head_weights", "merged_heads_torch"]
PILLAR_TILE_SLOTS = 32 # one 32-row tile per pillar on the device
PFN_IN_PADDED = 16 # 11 decorated features zero-padded to 16 (32-byte bf16 rows)
def _t(a):
"""float32 torch tensor owning a copy (the ONNX-derived arrays may be read-only)."""
import torch
return torch.from_numpy(np.array(a, dtype=np.float32, copy=True))
# --------------------------------------------------------------------------------------------------------- PFN
def pad_pillar_slots(features: np.ndarray, slots: int = PILLAR_TILE_SLOTS, width: int = PFN_IN_PADDED) -> np.ndarray:
"""(P, 20, F) decorated features -> (P, slots, width): features zero-padded to ``width`` columns, slots
``20..slots-1`` copies of slot 0 (a real point of every kept pillar)."""
f = np.asarray(features, dtype=np.float32)
P, K, F = f.shape
if slots < K or width < F:
raise ValueError(f"cannot pad ({K}, {F}) to ({slots}, {width})")
out = np.zeros((P, slots, width), dtype=np.float32)
out[:, :K, :F] = f
out[:, K:, :F] = f[:, :1, :]
return out
def pfn_tile_form(features, w0, b0, w1a, w1b, b1):
"""The device form of the PFN (torch tensors): ``h = relu(x @ w0 + b0)``, ``m = max_slots h``,
``y = relu(max_slots(h @ w1a) + m @ w1b + b1)`` -> (P, 64). ``x`` may carry padded columns (zero rows in ``w0``)
and repeated slots (:func:`pad_pillar_slots`)."""
import torch
h = torch.relu(features @ w0 + b0) # (P, S, 32)
m = h.amax(dim=1) # (P, 32): padded slots included
z = (h @ w1a).amax(dim=1) # (P, 64): the max taken before the per-pillar term
return torch.relu(z + m @ w1b + b1)
# ---------------------------------------------------------------------------------------------------- neck / head
def k_split_weights(shared: Layer, split: Sequence[int]) -> List[np.ndarray]:
"""The shared conv weight cut along input channels into the deblocks' channel groups:
``conv(concat[d_i], W) == sum_i conv(d_i, W_i)``."""
if sum(split) != shared.in_channels:
raise ValueError(f"split {list(split)} does not cover {shared.in_channels} input channels")
bounds = np.cumsum([0] + list(split))
return [np.ascontiguousarray(shared.weight[:, a:b]) for a, b in zip(bounds[:-1], bounds[1:])]
def k_split_torch(deblocks, shared: Layer):
"""``sum_i conv(d_i, W_i) + b`` (no activation: the TransFusion shared conv has none), torch NCHW."""
import torch.nn.functional as F
parts = k_split_weights(shared, [int(d.shape[1]) for d in deblocks])
acc = None
for d, w in zip(deblocks, parts):
y = F.conv2d(d, _t(w), None, stride=shared.stride, padding=shared.padding)
acc = y if acc is None else acc + y
return acc + _t(shared.bias)[None, :, None, None]
def space_to_depth_nhwc(x: np.ndarray, s: int) -> np.ndarray:
"""(H, W, C) -> (H/s, W/s, s*s*C), channel ``(a * s + b) * C + c`` = pixel ``(s*i + a, s*j + b)``."""
H, W, C = x.shape
return np.asarray(x).reshape(H // s, s, W // s, s, C).transpose(0, 2, 1, 3, 4).reshape(H // s, W // s, s * s * C)
def deblock0_as_linear(layer: Layer) -> Tuple[np.ndarray, np.ndarray]:
"""Conv k x k stride k (no padding) as ``space_to_depth(x) @ Wl + b``: ``Wl[(a * k + b) * Cin + ci, co] =
W[co, ci, a, b]``."""
if layer.kind != "conv" or layer.kernel != layer.stride or layer.padding != 0:
raise ValueError("deblock0_as_linear needs a conv with kernel == stride and no padding")
w = layer.weight # (Cout, Cin, k, k)
cout, cin, k, _ = w.shape
wl = np.ascontiguousarray(w.transpose(2, 3, 1, 0).reshape(k * k * cin, cout), dtype=np.float32)
return wl, layer.bias.astype(np.float32)
def deconv_as_linear(layer: Layer) -> Tuple[np.ndarray, np.ndarray]:
"""ConvTranspose with kernel == stride s (no padding) as a linear ``Cin -> s*s*Cout`` on NHWC rows: column
``(a * s + b) * Cout + co`` holds ``W[ci, co, a, b]``, bias tiled the same way."""
if layer.kind != "conv_transpose" or layer.kernel != layer.stride or layer.padding != 0:
raise ValueError("deconv_as_linear needs a ConvTranspose with kernel == stride and no padding")
w = layer.weight # (Cin, Cout, s, s)
cin, cout, s, _ = w.shape
wl = np.ascontiguousarray(w.transpose(0, 2, 3, 1).reshape(cin, s * s * cout), dtype=np.float32)
return wl, np.tile(layer.bias, s * s).astype(np.float32)
def depth_to_space_nhwc(y: np.ndarray, H: int, W: int, s: int, cout: int) -> np.ndarray:
"""Rows ``(i, j)`` of ``y`` (H*W, s*s*Cout) -> the (H*s, W*s, Cout) map: ``out[s*i + a, s*j + b, co] =
y[i*W + j, (a*s + b)*Cout + co]``."""
return np.asarray(y).reshape(H, W, s, s, cout).transpose(0, 2, 1, 3, 4).reshape(H * s, W * s, cout)
# ----------------------------------------------------------------------------------------------- local max (C22)
def local_max_masks(height: int, width: int, classes: int, kernel: int = 3) -> Tuple[np.ndarray, np.ndarray]:
"""The constant masks of C22's chain for TransFusion's rule (``MaxPool`` k3 s1 **p0** + zero border, every class):
``m_eq`` = 1 on interior cells (the pooled window is fully inside), 0 on the ``kernel // 2`` border (always
suppressed); ``m_pass`` = 0 everywhere (no class bypasses the local max). (classes, H, W) float32."""
r = kernel // 2
m_eq = np.zeros((classes, height, width), np.float32)
m_eq[:, r:height - r, r:width - r] = 1.0
return np.zeros_like(m_eq), m_eq
def local_max_masked_torch(heat, m_pass, m_eq, kernel: int = 3):
"""``heat * (m_pass + (heat == maxpool_k_s1_p(k//2)(heat)) * m_eq)`` (torch NCHW; the pool's padding is -inf)."""
import torch.nn.functional as F
pooled = F.max_pool2d(heat, kernel, 1, kernel // 2)
return heat * (_t(m_pass)[None] + (heat == pooled).to(heat.dtype) * _t(m_eq)[None])
# ------------------------------------------------------------------------------------------------- query / decoder
def query_pos_from_index(pos: np.ndarray, feat_w: int) -> np.ndarray:
"""(K,) flat BEV cells -> (K, 2) float32 ``(x + 0.5, y + 0.5)`` (exact: integers below 2^24 plus 0.5)."""
p = np.asarray(pos, dtype=np.int64)
return np.stack([(p % feat_w).astype(np.float32) + np.float32(0.5),
(p // feat_w).astype(np.float32) + np.float32(0.5)], axis=-1)
def cross_attention_kv_tables(attn: AttentionParams, key_pos_embed: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
"""``CK = pe(bev_pos) @ Wk + bk`` and ``CV = pe(bev_pos) @ Wv + bv`` (36864, 128), float64 then one rounding:
``K = lidar^T @ Wk + CK``, ``V = lidar^T @ Wv + CV`` (the 0.6 GMAC position branch leaves the per-frame graph)."""
kpe = np.asarray(key_pos_embed, dtype=np.float64)
ck = kpe @ attn.wk.astype(np.float64) + attn.bk.astype(np.float64)
cv = kpe @ attn.wv.astype(np.float64) + attn.bv.astype(np.float64)
return ck.astype(np.float32), cv.astype(np.float32)
def pad_head_dim_rows(table: np.ndarray, num_heads: int, head_dim: int, padded: int) -> np.ndarray:
"""(L, H*D) -> (L, H*padded) with each head's columns zero-padded (a constant K / V table in the padded layout)."""
t = np.asarray(table, dtype=np.float32).reshape(-1, num_heads, head_dim)
out = np.zeros((t.shape[0], num_heads, padded), np.float32)
out[..., :head_dim] = t
return out.reshape(t.shape[0], num_heads * padded)
def padded_head_attention(q_in, k_in, v_in, attn: AttentionParams, *, k_const=None, v_const=None,
padded: int = 32):
"""Multi-head attention with the head dim zero-padded to ``padded`` in the weights (torch, (L, 128) inputs):
``softmax(scale * Qp Kp^T) Vp`` per head with ``scale = 1 / q_divisor`` (the logical ``1/sqrt(16)``), then the
row-padded output projection. ``k_const`` / ``v_const`` (L_k, 128): the constant K / V tables
(:func:`cross_attention_kv_tables`), whose bias they include (then ``k_in @ Wk`` alone is projected)."""
import torch
H, D = attn.num_heads, attn.head_dim
wq, bq = pad_head_columns(attn.wq, attn.bq, H, D, padded)
wk, bk = pad_head_columns(attn.wk, attn.bk, H, D, padded)
wv, bv = pad_head_columns(attn.wv, attn.bv, H, D, padded)
wo = pad_head_rows(attn.wo, H, D, padded)
q = q_in @ _t(wq) + _t(bq)
if k_const is None:
k = k_in @ _t(wk) + _t(bk)
v = v_in @ _t(wv) + _t(bv)
else:
k = k_in @ _t(wk) + _t(pad_head_dim_rows(k_const, H, D, padded))
v = v_in @ _t(wv) + _t(pad_head_dim_rows(v_const, H, D, padded))
lq, lk = q.shape[0], k.shape[0]
qh = q.reshape(lq, H, padded).transpose(0, 1)
kh = k.reshape(lk, H, padded).transpose(0, 1)
vh = v.reshape(lk, H, padded).transpose(0, 1)
o = torch.softmax((qh @ kh.transpose(1, 2)) * attn.scale, dim=-1) @ vh # (H, Lq, padded)
return o.transpose(0, 1).reshape(lq, H * padded) @ _t(wo) + _t(attn.bo)
# --------------------------------------------------------------------------------------------- prediction heads
def merged_prediction_head_weights(head: HeadParams, *, pad_to: int = 32
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, List[Tuple[str, int, int]]]:
"""The six prediction heads as two matmuls on the (K, 128) decoder rows: ``hidden = relu(x @ Wh + bh)`` with
``Wh`` (128, 6*64), and ``out = hidden @ Wo + bo`` with the block-diagonal ``Wo`` (6*64, 15 -> ``pad_to``).
Returns (Wh, bh, Wo, bo, slices); ``slices`` lists ``(name, start, stop)`` of each head's output channels."""
wh = np.concatenate([hid.weight[:, :, 0].T for _, hid, _ in head.prediction_heads], axis=1).astype(np.float32)
bh = np.concatenate([hid.bias for _, hid, _ in head.prediction_heads]).astype(np.float32)
rows = sum(hid.out_channels for _, hid, _ in head.prediction_heads)
cols = sum(fin.out_channels for _, _, fin in head.prediction_heads)
width = -(-cols // pad_to) * pad_to
wo = np.zeros((rows, width), np.float32)
bo = np.zeros(width, np.float32)
slices, r, c = [], 0, 0
for name, hid, fin in head.prediction_heads:
wo[r:r + hid.out_channels, c:c + fin.out_channels] = fin.weight[:, :, 0].T
bo[c:c + fin.out_channels] = fin.bias
slices.append((name, c, c + fin.out_channels))
r += hid.out_channels
c += fin.out_channels
return wh, bh, wo, bo, slices
def merged_heads_torch(x, merged) -> Dict[str, object]:
"""Apply :func:`merged_prediction_head_weights` to the (K, 128) decoder output -> {name: (1, C, K)}."""
import torch
wh, bh, wo, bo, slices = merged
y = torch.relu(x @ _t(wh) + _t(bh)) @ _t(wo) + _t(bo) # (K, 32)
return {name: y[:, a:b].T[None] for name, a, b in slices}
|