File size: 14,730 Bytes
fe51a63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# SPDX-License-Identifier: Apache-2.0
"""The TT device dataflow of FRNet (PORT_LOG.md "trace layout"), run on the CPU in fp32 and proven equal to the
literal graph (``reference/model.py``) by ``tests/test_reference_rewrites_host.py``. Each rewrite is exact in real
arithmetic; in float32 they differ only by summation order:

- **points sorted by cell and padded** to ``N_cap`` (``host/preprocess.py`` ``FramePlan``): the network is
  permutation-equivariant in the points; padded rows form one trailing sentinel segment whose results nothing reads;
- **host features**: the 8-d decoration and ``pre_norm`` in float64, one rounding (``decorate_points``);
- **segmented max** (6 sites: ``ScatterElements(max)``) as the exact **log-step** form of PLAN.md 1.3 (the K1
  kernel's stock-op fallback): round ``r`` takes ``x = max(x, x[idx_r])`` with the host shift table ``idx_r``,
  then one gather of each segment's last row (``seg_last``) -> ``(M_cap, C)``; :func:`segmax_logstep`;
- **frustum2pixel** (6 ``ScatterND``) as a gather: ``image[pixel] = table[pix2vox_s[pixel]]`` from the voxel table
  with one appended **zero row** (index ``M_cap``) for empty pixels -- ``ttaw.ops.gather.scatter_rows`` semantics
  (C19); duplicates at s > 1 resolved on the host (last voxel wins);
- **pixel2point** as a gather of NHWC rows (``point2pix_s``);
- **concat-free convs**: ``conv(concat(a, b))`` = ``conv_a(a) + conv_b(b)`` for the fusion convs (256 = 128 + 128)
  and the 5-way K-split ``fuse_layer1`` (640 = 5 x 128, probe P14: 8.3 vs 14.2 ms);
- **concat-free point layers**: ``linear(concat(a, b))`` = ``a @ W_a + b @ W_b + bias`` (point stem, point fusion,
  ``point_fuse_layer1``), which also lets the two halves keep different dtypes;
- **align-corners bilinear** as interpolation matrices ``A_h X A_w^T`` (C18 ``interp_matrix``, float64 -> float32).

:class:`Emulation` optionally rounds layer operands the way the device stores them (bf16, or the TF32-like
mantissa of the device's fp32 matmul, probe P12) -- a first-order CPU model to size the fp32 islands of the
port before the device runs (``code/scripts/precision_study.py``); it models no math-fidelity phases or tile
layouts, so its numbers are indicative only.
"""
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Dict, List, Optional, Sequence

import numpy as np
import torch
import torch.nn.functional as F

from ..host.preprocess import FramePlan, segmax_rounds, segmax_shift_tables
from ..ttaw.golden import NULL_TAPS, TapRegistry
from ..ttaw.ops.upsample import interp_matrix
from .model import TorchLayers, _t
from .weights import Conv, FRNetWeights, Linear

__all__ = ["Emulation", "round_mode", "segmax_logstep", "FRNetPlanTorch", "plan_taps_to_literal"]

MODES = ("fp32", "bf16", "tf32")


def round_mode(x: torch.Tensor, mode: str) -> torch.Tensor:
    """``fp32``: unchanged; ``bf16``: round to nearest even bfloat16; ``tf32``: truncate to 10 mantissa bits (the
    probe P12 truncation model of the device's fp32 matmul operands; the device measured 1.4-1.75x its error)."""
    if mode == "fp32":
        return x
    if mode == "bf16":
        return x.to(torch.bfloat16).to(torch.float32)
    if mode == "tf32":
        return (x.contiguous().view(torch.int32) & ~0x1FFF).view(torch.float32)
    raise ValueError(f"unknown rounding mode {mode!r}; one of {MODES}")


@dataclass(frozen=True)
class Emulation:
    """Operand rounding per site (``fp32`` = exact float32). ``conv_act`` / ``conv_weight``: inputs and weights
    (+ bias) of the 53 convs; ``point_act`` / ``point_weight``: inputs and weights of the 17 point / voxel linears;
    ``gather``: rows produced by the embedding gathers (frustum2pixel, pixel2point: ``ttnn.embedding`` tables are
    bf16-only, so ``bf16`` is the stock-op device); ``upsample``: the decoder's interpolation operands. ``overrides``:
    per-layer modes by layer name (e.g. ``{"head.logits": ("tf32", "fp32")}``: (act, weight))."""

    conv_act: str = "fp32"
    conv_weight: str = "fp32"
    point_act: str = "fp32"
    point_weight: str = "fp32"
    gather: str = "fp32"
    upsample: str = "fp32"
    overrides: Dict[str, tuple] = field(default_factory=dict)

    @classmethod
    def parse(cls, spec: str) -> "Emulation":
        """``"conv_act=bf16,point_act=tf32,gather=bf16"`` (unset sites stay fp32)."""
        kw = {}
        for part in filter(None, (p.strip() for p in spec.split(","))):
            k, _, v = part.partition("=")
            if k not in cls.__dataclass_fields__ or k == "overrides" or v not in MODES:
                raise ValueError(f"bad emulation item {part!r}")
            kw[k] = v
        return cls(**kw)

    @property
    def exact(self) -> bool:
        return all(getattr(self, k) == "fp32" for k in ("conv_act", "conv_weight", "point_act", "point_weight",
                                                       "gather", "upsample")) and not self.overrides

    def label(self) -> str:
        items = [f"{k}={getattr(self, k)}" for k in ("conv_act", "conv_weight", "point_act", "point_weight",
                                                      "gather", "upsample") if getattr(self, k) != "fp32"]
        items += [f"{k}:{v}" for k, v in sorted(self.overrides.items())]
        return ",".join(items) or "fp32"


def segmax_logstep(x: torch.Tensor, plan: FramePlan, rounds: Optional[int] = None) -> torch.Tensor:
    """Per-voxel max of the sorted, padded rows ``x`` ``(N_cap, C)`` -> ``(M_cap, C)``: ``rounds`` (default: just
    enough for the frame's longest segment) log-step rounds over the host shift tables, then a gather of every
    segment's last row (PLAN.md 1.3). Exact (max is order-independent)."""
    rounds = segmax_rounds(plan.max_segment_length) if rounds is None else int(rounds)
    for idx in segmax_shift_tables(plan.seg_id, rounds):
        x = torch.maximum(x, x[_t(idx.astype(np.int64))])
    return x[_t(plan.seg_last.astype(np.int64))]


def _with_zero_row(table: torch.Tensor) -> torch.Tensor:
    return torch.cat([table, table.new_zeros(1, table.shape[1])], 0)


def _nhwc(x: torch.Tensor) -> torch.Tensor:
    _, c, h, w = x.shape
    return x.permute(0, 2, 3, 1).reshape(h * w, c)


def _nchw(rows: torch.Tensor, h: int, w: int) -> torch.Tensor:
    return rows.reshape(1, h, w, -1).permute(0, 3, 1, 2).contiguous()


class FRNetPlanTorch:
    """``FRNetPlanTorch(weights)(plan, taps)`` -> ``pred_probs`` of the sorted, padded rows ``(N_cap, 27)``
    (``plan.unsort`` gives the network-input order). Taps use the names of ``reference/model.py``; point taps are in
    sorted padded order, voxel taps have ``M_cap`` rows (:func:`plan_taps_to_literal` aligns them)."""

    def __init__(self, weights: FRNetWeights, *, emulation: Optional[Emulation] = None,
                 rounds: Optional[int] = None):
        self.weights = weights
        self.layers = TorchLayers(weights)
        self.emu = emulation or Emulation()
        self.rounds = rounds
        H, W = weights.frustum_hw
        self._interp = {}
        for st in [1] + [s.stride for s in weights.stages]:
            ah = interp_matrix(H // st, H, mode="linear", align_corners=True)
            aw = interp_matrix(W // st, W, mode="linear", align_corners=True)
            self._interp[st] = (_t(ah.astype(np.float32)), _t(aw.astype(np.float32)))
        # K-split weights of the concat-consuming layers
        self._split_lin = {}
        for lin, parts in ((weights.point_stem, (128, 256)), (weights.point_fuse[0], (128,) * 5),
                           *((st.point_fusion, (128, 128)) for st in weights.stages)):
            w_t, b_t, _ = self.layers.lin[lin.name]
            if w_t.shape[0] != sum(parts):
                raise ValueError(f"{lin.name}: {w_t.shape[0]} inputs, expected {sum(parts)}")
            self._split_lin[lin.name] = (list(torch.split(w_t, list(parts), 0)), b_t)
        self._split_conv = {}
        for conv, parts in ((weights.fusion_stem, (128, 128)), (weights.fuse_layer1, (128,) * 5),
                            *((st.pixel_fusion, (128, 128)) for st in weights.stages)):
            w_t, b_t, stride, pad, act = self.layers.conv[conv.name]
            if w_t.shape[1] != sum(parts):
                raise ValueError(f"{conv.name}: {w_t.shape[1]} inputs, expected {sum(parts)}")
            self._split_conv[conv.name] = (list(torch.split(w_t, list(parts), 1)), b_t, stride, pad, act)

    # ---- rounded layer applications ---------------------------------------------------------------------------
    def _modes(self, name: str, act: str, weight: str) -> tuple:
        return self.emu.overrides.get(name, (act, weight))

    def linear(self, parts: Sequence[torch.Tensor], layer: Linear) -> torch.Tensor:
        act_m, w_m = self._modes(layer.name, self.emu.point_act, self.emu.point_weight)
        if layer.name in self._split_lin:
            ws, b = self._split_lin[layer.name]
        else:
            w, b, _ = self.layers.lin[layer.name]
            ws = [w]
        if len(parts) != len(ws):
            raise ValueError(f"{layer.name}: {len(parts)} input parts for {len(ws)} weight parts")
        y = round_mode(b, w_m).expand(parts[0].shape[0], -1).clone()
        for x, w in zip(parts, ws):
            y = y + round_mode(x, act_m) @ round_mode(w, w_m)
        return F.relu(y) if layer.relu else y

    def conv(self, parts: Sequence[torch.Tensor], layer: Conv) -> torch.Tensor:
        act_m, w_m = self._modes(layer.name, self.emu.conv_act, self.emu.conv_weight)
        if layer.name in self._split_conv:
            ws, b, stride, pad, act = self._split_conv[layer.name]
        else:
            w, b, stride, pad, act = self.layers.conv[layer.name]
            ws = [w]
        if len(parts) != len(ws):
            raise ValueError(f"{layer.name}: {len(parts)} input parts for {len(ws)} weight parts")
        y = None
        for i, (x, w) in enumerate(zip(parts, ws)):
            part = F.conv2d(round_mode(x, act_m), round_mode(w, w_m), round_mode(b, w_m) if i == 0 else None,
                            stride=stride, padding=pad)
            y = part if y is None else y + part
        if act == "hardswish":
            y = F.hardswish(y)
        elif act == "sigmoid":
            y = torch.sigmoid(y)
        return y

    def gather(self, table: torch.Tensor, index: np.ndarray) -> torch.Tensor:
        return round_mode(table, self.emu.gather)[_t(index.astype(np.int64))]

    def upsample(self, x: torch.Tensor, stride: int) -> torch.Tensor:
        if stride == 1:
            return x
        ah, aw = self._interp[stride]
        m = self.emu.upsample
        x = round_mode(x, m)
        return torch.einsum("hi,bcij,wj->bchw", round_mode(ah, m), x, round_mode(aw, m))

    # ---- the device graph -------------------------------------------------------------------------------------
    def __call__(self, plan: FramePlan, taps: TapRegistry = NULL_TAPS) -> np.ndarray:
        return self.forward(plan, taps)

    @torch.no_grad()
    def forward(self, plan: FramePlan, taps: TapRegistry = NULL_TAPS) -> np.ndarray:
        Wt = self.weights
        H, W = plan.frustum_hw
        if (H, W) != Wt.frustum_hw:
            raise ValueError(f"plan grid {plan.frustum_hw} != weights grid {Wt.frustum_hw}")

        def tap(name: str, value: torch.Tensor) -> torch.Tensor:
            if taps.wants(name):
                taps.tap(name, value.numpy())
            return value

        def to_image(table: torch.Tensor, s: int) -> torch.Tensor:
            return _nchw(self.gather(_with_zero_row(table), plan.pix2vox[s]), H // s, W // s)

        def to_points(img: torch.Tensor, s: int) -> torch.Tensor:
            return self.gather(_nhwc(img), plan.point2pix[s])

        x = tap("encoder.input", _t(plan.features))
        pf: List[torch.Tensor] = []
        for i, lin in enumerate(Wt.ffe):
            x = self.linear([x], lin)
            pf.append(tap(f"encoder.pf{i}", x))
        vmax = tap("encoder.voxel_max", segmax_logstep(x, plan, self.rounds))
        vfeat = tap("encoder.voxel_feats", self.linear([vmax], Wt.compression))
        img = tap("backbone.frustum", to_image(vfeat, 1))
        for c in Wt.stem:
            img = self.conv([img], c)
        tap("backbone.stem", img)
        p = tap("backbone.point_stem", self.linear([to_points(img, 1), pf[-1]], Wt.point_stem))
        seg = tap("backbone.segmax0", segmax_logstep(p, plan, self.rounds))
        x = tap("backbone.fusion_stem", self.conv([to_image(seg, 1), img], Wt.fusion_stem))
        maps, pts_out = [x], [p]
        for st in Wt.stages:
            li, s = st.index, st.stride
            for bi, (c1, c2, ds) in enumerate(st.blocks):
                y = self.conv([self.conv([x], c1)], c2)
                idt = self.conv([x], ds) if ds is not None else x
                x = tap(f"backbone.layer{li}.block{bi}", F.hardswish(y + idt))
            tap(f"backbone.layer{li}", x)
            p = tap(f"backbone.point_fusion{li}", self.linear([to_points(x, s), p], st.point_fusion))
            seg = tap(f"backbone.segmax{li}", segmax_logstep(p, plan, self.rounds))
            fuse = tap(f"backbone.pixel_fusion{li}", self.conv([to_image(seg, s), x], st.pixel_fusion))
            att = tap(f"backbone.attention{li}", self.conv([self.conv([fuse], st.attention[0])], st.attention[1]))
            x = tap(f"backbone.stage{li}", fuse * att + x)
            maps.append(x)
            pts_out.append(p)
        strides = [1] + [st.stride for st in Wt.stages]
        ups = [tap(f"decoder.up{k}", self.upsample(mp, s)) for k, (mp, s) in enumerate(zip(maps, strides))]
        y = tap("decoder.fuse1", self.conv(ups, Wt.fuse_layer1))
        y = tap("decoder.fuse2", self.conv([y], Wt.fuse_layer2))
        q = tap("decoder.point_fuse1", self.linear(pts_out, Wt.point_fuse[0]))
        q = tap("decoder.point_fuse2", self.linear([q], Wt.point_fuse[1]))
        h = to_points(y, 1)
        for i, (lin, skip) in enumerate(zip(Wt.head_mlps, (q, pf[2], pf[1], pf[0]))):
            h = tap(f"head.mlp{i}", self.linear([h], lin) + skip)
        logits = tap("head.logits", self.linear([h], Wt.conv_seg))
        return tap("pred_probs", torch.softmax(logits, -1)).numpy()


def plan_taps_to_literal(name: str, value: np.ndarray, plan: FramePlan) -> np.ndarray:
    """A tap of :class:`FRNetPlanTorch` in the layout of ``reference/model.py``: point tensors (``N_cap`` rows)
    unsorted to the network-input order, voxel tensors (``M_cap`` rows) cut to ``M``, maps unchanged."""
    v = np.asarray(value)
    if v.ndim == 2 and v.shape[0] == plan.n_cap:
        return plan.unsort(v)
    if v.ndim == 2 and v.shape[0] == plan.m_cap:
        return v[: plan.m]
    return v