Download code/tt_transfusion/reference/rewrites.py from changh95/transfusion-p150: direct link, hf CLI and curl.
- Browser
- Download file 14.1 kB
-
https://huggingface.co/changh95/transfusion-p150/resolve/main/code/tt_transfusion/reference/rewrites.py
- Command line
-
hf download hf://changh95/transfusion-p150/code/tt_transfusion/reference/rewrites.py
-
curl -L -o rewrites.py https://huggingface.co/changh95/transfusion-p150/resolve/main/code/tt_transfusion/reference/rewrites.py
14.1 kB
| # 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} | |