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}