File size: 15,686 Bytes
ddaad99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55474c5
 
ddaad99
55474c5
 
 
ddaad99
 
 
55474c5
 
 
 
 
 
ddaad99
 
 
 
 
 
 
 
 
 
 
 
 
55474c5
 
 
ddaad99
 
55474c5
 
 
 
 
 
 
 
 
 
 
ddaad99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55474c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# SPDX-License-Identifier: Apache-2.0
"""Host tests of the API's host half (``BEVFormer._build_host`` / ``_prepare``, no device, no weights): the runner
inputs a frame of the shipped sample produces equal the golden network inputs and host geometry (``make_goldens.py``,
port form), the stream history semantics (new id / reset -> ``use_prev_bev = 0``), the per-calibration cache, the
SCA-capacity refusal (HTTP 400) and the can_bus layout knob."""
from __future__ import annotations

import numpy as np
import pytest

from tt_bevformer import io as tio
from tt_bevformer.api import BEVFormer
from tt_bevformer.reference import config as C
from tt_bevformer.tests import paths

SAMPLES = [paths.PANDASET_SAMPLES / f"pandaset_019_f{f}.json" for f in paths.SAMPLE_FRAMES]


def _model(**compile_params) -> BEVFormer:
    m = BEVFormer.__new__(BEVFormer)
    m.compile_params = dict(compile_params)
    m._build_host()
    return m


def _kw(i: int, sid: str = "s", reset=None):
    paths.need_pandaset()
    from tt_bevformer.host.inputs import load_sample

    kw = load_sample(SAMPLES[i])
    st = dict(kw["stream"], id=sid)
    if reset is not None:
        st["reset"] = reset
    kw["stream"] = st
    return kw


@pytest.mark.parametrize("pre", ["numpy", "numba", "bf16", "numpy-s2d", "numba-s2d", "bf16-s2d"])
def test_frame_inputs_equal_the_golden_inputs(pre):
    """f30 (reset) then f35 as one stream: can_bus, use_prev_bev, the rotation map, the TSA shift and every
    per-calibration table equal what the golden run fed the port-form reference (both host pre-processing paths,
    BEVFORMER_HOST_PRE: the network input bit for bit)."""
    from tt_bevformer.host import fast
    from tt_bevformer.tt.model import calibration_inputs
    from tt_bevformer.tt.params import tsa_shift_rows

    from tt_bevformer.tt.params import stem_s2d_image

    if pre.startswith(("numba", "bf16")) and not fast.available():
        pytest.skip("numba is not installed")
    s2d = pre.endswith("-s2d")
    m = _model(HOST_PRE=pre.split("-")[0], STEM="s2d" if s2d else "plain", UPLOAD_PIPE=False, STEM_IN="kin")
    for i, f in ((0, 30), (1, 35)):
        p = m._prepare(**_kw(i, reset=(i == 0)))
        gp = paths.golden_dir() / "pandaset_019" / f"f{f}.npz"
        if not gp.is_file():
            pytest.skip(f"{gp} missing")
        with np.load(gp) as z:
            g = {k: z[k] for k in z.files if k != "__meta__"}
        up = p["upload"]
        np.testing.assert_array_equal(up["params"]["can_bus"].reshape(18), g["input.can_bus"])
        assert up["params"]["use"] == float(g["input.use_prev_bev"][0]) == (0.0 if i == 0 else 1.0)
        np.testing.assert_array_equal(up["inputs"]["rot_index"].reshape(-1), g["geometry.rot_index"])
        np.testing.assert_array_equal(up["params"]["tsa_shift"], tsa_shift_rows(g["geometry.shift"],
                                                                                 up["params"]["use"]))
        from tt_bevformer.tt.model import img_shape

        assert up["inputs"]["img"].shape == img_shape("s2d" if s2d else "plain", m.knobs.UPLOAD == "rows")
        if "input.image" in g:
            want = np.ascontiguousarray(g["input.image"].reshape(C.NUM_CAMS, 3, *C.IMG_HW).transpose(0, 2, 3, 1))
            if s2d:
                want = stem_s2d_image(want)
            got = up["inputs"]["img"]
            if pre == "bf16":                    # bf16 rows: the golden input cast as the upload casts it
                import torch

                got = got.reshape(want.shape)
                want = torch.from_numpy(np.ascontiguousarray(want, np.float32)).to(torch.bfloat16)
                assert got.dtype == torch.bfloat16 and torch.equal(got.view(torch.int16), want.view(torch.int16))
            else:
                np.testing.assert_array_equal(got.reshape(want.shape), want)
        geo = {k[len("geometry."):]: v for k, v in g.items() if k.startswith("geometry.")}
        want_cal = calibration_inputs(geo)
        for k, v in p["geometry"]["inputs"].items():
            np.testing.assert_array_equal(v, want_cal[k], err_msg=k)


def test_stream_history_semantics():
    m = _model()
    assert m._prepare(**_kw(0, "a"))["fresh"]                         # new id
    p = m._prepare(**_kw(1, "a", reset=False))
    assert not p["fresh"] and p["use_prev_bev"][0] == 1.0
    assert m._prepare(**_kw(2, "a", reset=True))["fresh"]             # explicit reset
    assert m._prepare(**_kw(1, "b", reset=False))["fresh"]            # another id starts fresh
    with pytest.raises(tio.InputError):
        m._prepare(**dict(_kw(1, "a"), stream={"id": "a", "reset": "yes"}))


def test_calibration_cache():
    m = _model()
    a = m._prepare(**_kw(0))["geometry"]
    b = m._prepare(**_kw(0))["geometry"]
    assert a is b                                                     # same lidar2img -> same tables
    c = m._prepare(**_kw(1))["geometry"]                              # per-frame compensated calibration
    assert c["key"] != a["key"]


def test_sca_overflow_is_a_client_error(monkeypatch):
    from tt_bevformer.host import geometry as hgeo

    def overflow(*_a, **_k):
        raise hgeo.SCAOverflow("a camera sees 7000 BEV queries")

    monkeypatch.setattr(hgeo, "sca_tables", overflow)
    with pytest.raises(tio.InputError, match="calibration not supported"):
        _model()._prepare(**_kw(0))


def test_can_bus_layout_knob():
    t = _model()._prepare(**_kw(0))["can_bus"]
    a = _model(can_bus_layout="autoware")._prepare(**_kw(0))["can_bus"]
    np.testing.assert_array_equal(t[7:10], a[13:16])                  # accel <-> velocity slots
    np.testing.assert_array_equal(t[13:16], a[7:10])
    np.testing.assert_array_equal(t[10:13], a[10:13])


def test_knobs_defaults():
    from tt_bevformer.api import KNOBS

    k = KNOBS.read(env={})
    assert k.PRECISION == "HiFi2+fp32+l1acc:w=bf16" and k.MAX_STREAMS == 4 and k.SHIFT_MODE == "surrogate"
    assert k.CAN_BUS_LAYOUT == "training" and k.MSDA == "kernel" and k.DCN == "kernel" and k.DCN_OFFSET_L1
    assert k.MSDA_PX and k.MSDA_VALUE == "tile" and k.HOST_PRE == "bf16" and k.BACKBONE_MM == "exact2"
    assert k.UPLOAD == "rows" and k.STEM == "plain" and not k.MSDA_SKIP and k.STEM_IN == "kpre"
    assert k.MSDA_LOC == "compact" and k.MSDA_SOFTMAX == "kernel" and k.SCA_GATHER == "tile" and k.BEV_MM == "fast" and k.DEC_ATTN == "sdpa"
    assert k.PRECISION_RULES.endswith("img_backbone.layer3.*=HiFi2+fp32+l1acc:w=bfp8") and k.RES3_NEXT and k.ACT8 and k.ACT8_L3
    assert k.DCN_IM2COL == "l1" and k.DCN_SCALE == "full" and k.BACKBONE_S2 == "prod" and k.S2_SKIP and k.MSDA_FRAC16 and k.DCN_SAMPLE == "bilinear" and k.SCA_COPY == "kernel" and k.DEC_VALUE == "fused" and k.BACKBONE_C3 == "l1_2"
    # D20 round 2 (all bit-identical data movement)
    assert k.MMRES_MCAST and k.MSDA_MK_MCAST and k.DCN_GLUE == "kernel" and k.RES3_RM == "on" and k.DCN_OUT == "l1"
    assert k.PAIRMAP == "v2" and k.ENC_L1 == "all" and k.NECK_L1 and k.DCN_SUM == "mul" and not k.DCN_PAIRS
    assert k.HOST_MALLOC == "keep" and k.RES_L1 and k.OUTPUTS == "tile" and k.MSDA_PAIRS == "all"
    assert k.VALUES_DIRECT and k.SCA_VALUE == "l1" and k.HOST_STAGING and k.PIPELINE == 1


def test_upload_pipe_uploads_every_camera_bit_exact():
    """BEVFORMER_UPLOAD_PIPE: _prepare leaves the images pending (no img input), _forward's camera pipeline uploads
    img0 .. img5 = the bf16 rows of HOST_PRE=bf16, each exactly once, bit for bit."""
    import torch

    from tt_bevformer.host import fast
    from tt_bevformer.tt.model import CAM_ROWS_SHAPE

    if not fast.available():
        pytest.skip("numba is not installed")
    m = _model(HOST_PRE="bf16", UPLOAD_PIPE=True, STEM_IN="kin")
    p = m._prepare(**_kw(0, reset=True))
    assert p["pending_images"] is not None and not any(k.startswith("img") for k in p["upload"]["inputs"])
    want = fast.preprocess_rows_bf16(p["pending_images"], rgb=True, threads=2).copy()

    class _Runner:
        def __init__(self):
            self.got = {}

        def upload(self, inputs=None, params=None):
            for k, v in (inputs or {}).items():
                assert k not in self.got
                self.got[k] = v.clone()

    m.runner = _Runner()
    m._upload_cameras(p["pending_images"])
    assert sorted(m.runner.got) == [f"img{i}" for i in range(C.NUM_CAMS)]
    for i in range(C.NUM_CAMS):
        t = m.runner.got[f"img{i}"]
        assert t.dtype == torch.bfloat16 and tuple(t.shape) == CAM_ROWS_SHAPE
        assert np.array_equal(t.view(torch.int16).numpy().view(np.uint16).reshape(want[i].shape), want[i])
    assert not _model(HOST_PRE="numba", UPLOAD_PIPE=True, STEM_IN="kin")._upload_pipe()
    assert not _model(UPLOAD_PIPE=True)._upload_pipe()                 # the default STEM_IN=kpre uploads uint8


def test_kpre_uploads_the_node_images():
    """BEVFORMER_STEM_IN=kpre: the frame's inputs are the six uint8 node images (img0 .. img5, [1, 1, 900, 4800]),
    the node resize of the 1920x1080 sample equal to the numpy node transport; no float rows on the host."""
    import torch

    from tt_bevformer.host import fast
    from tt_bevformer.host import preprocess as hp
    from tt_bevformer.tt.model import NODE_U8_SHAPE

    if not fast.available():
        pytest.skip("numba is not installed")
    m = _model(STEM_IN="kpre")
    assert m._kpre() and not m._upload_pipe()
    kw = _kw(0, reset=True)
    p = m._prepare(**kw)
    ins = p["upload"]["inputs"]
    assert "img" not in ins and sorted(k for k in ins if k.startswith("img")) == [f"img{i}" for i in range(6)]
    from tt_bevformer.host.inputs import prepare_request

    frame = prepare_request(kw["images"], kw.get("calibration"), kw.get("stream"), kw.get("ego"), bgr=True)
    for i in range(C.NUM_CAMS):
        t = ins[f"img{i}"]
        assert t.dtype == torch.uint8 and tuple(t.shape) == NODE_U8_SHAPE
        want = hp.node_transport(frame.images_bgr[i])[:, :, ::-1]          # BGR -> the RGB the kernel reads
        assert np.array_equal(t.numpy().reshape(want.shape), want)


@pytest.mark.parametrize("mode", [1, 2])
def test_pipeline_orders_frames_and_survives_failures(mode, monkeypatch):
    """BEVFORMER_PIPELINE: concurrent calls take the device stage in the order their host half finished, the CQ1
    camera upload of a frame (mode 2) comes after the previous frame's image replay was enqueued, the stream half
    runs in the device stage, and a frame that fails in any stage gives up its turn (no deadlock)."""
    import random
    import threading
    import time
    import types

    m = _model(PIPELINE=mode)
    assert m.pipelined and m.pipeline == mode
    m._lock = threading.RLock()
    m._closed = False
    log, pre_order = [], []
    log_lock = threading.Lock()

    def note(*ev):
        with log_lock:
            log.append(ev)

    def prepare_host(points=None, **kw):
        time.sleep(random.uniform(0, 0.004))
        i = kw["stream"]["i"]
        pre_order.append(i)
        if i == 7:
            raise tio.InputError("bad frame 7")
        return {"i": i, "node_u8": None}

    monkeypatch.setattr(m, "_prepare_host", prepare_host)
    monkeypatch.setattr(m, "_host_images", lambda host: {"img0": host["i"]})

    def upload(imgs):
        note("upload", imgs["img0"])
        if imgs["img0"] == 11:
            raise RuntimeError("upload 11")
        return ("ev", imgs["img0"])

    monkeypatch.setattr(m, "_upload_images_cq1", upload)

    busy = []

    def device_stage(host, imgs, ev, ticket):
        i = host["i"]
        assert (ev == ("ev", i)) if mode == 2 else ev is None
        busy.append(i)
        overlap.extend(busy if len(busy) > 1 else [])
        note("image", i)
        with m._cv:                                  # what _device_stage's enqueue section implies
            m._img_turn = max(m._img_turn, m._ticket_of[i])
            m._cv.notify_all()
        time.sleep(random.uniform(0, 0.003))
        if i == 10:
            time.sleep(0.03)                         # frame 11 fails its CQ1 upload while 10 is still on the device
        busy.remove(i)
        if i == 13:
            raise RuntimeError("device 13")
        note("done", i)
        return {"i": i}, {"timing_ms": {}}

    overlap = []
    monkeypatch.setattr(m, "_device_stage", device_stage)
    m._ticket_of = {}
    orig_host = m._prepare_host

    def tracked(points=None, **kw):
        h = orig_host(points, **kw)
        m._ticket_of[h["i"]] = m._ticket
        return h

    monkeypatch.setattr(m, "_prepare_host", tracked)
    monkeypatch.setattr(m, "_postprocess", lambda raw, prep, p: types.SimpleNamespace(timing_ms={}))
    errors = {}

    def call(i):
        try:
            m(images=None, stream={"i": i})
        except Exception as e:  # noqa: BLE001
            errors[i] = type(e).__name__

    threads = [threading.Thread(target=call, args=(i,)) for i in range(24)]
    for t in threads:
        t.start()
    for t in threads:
        t.join(10)
    assert not any(t.is_alive() for t in threads), "pipeline deadlocked"
    assert not overlap, f"device stages overlapped: {overlap}"   # a failed frame must not skip the turn order
    assert errors.get(7) == "InputError" and errors.get(13) == "RuntimeError"
    if mode == 2:
        assert errors.get(11) == "RuntimeError"
    ok = [i for i in pre_order if i not in errors]
    assert [e[1] for e in log if e[0] == "done"] == [i for i in ok]      # device order == host-half order
    if mode == 2:                                    # frame k's upload after frame k - 1's image replay
        pos = {e: n for n, e in enumerate(log)}
        seq = [i for i in pre_order if i != 7]
        for a, b in zip(seq, seq[1:]):
            if ("image", a) in pos:
                assert pos[("upload", b)] > pos[("image", a)]
    assert m._turn == m._ticket and m._img_turn == m._ticket - 1


def test_tt_model_kwargs_cover_every_device_option():
    """``api.tt_model_kwargs`` (shared by the API and the device PCC gate fixture) sets every TtBEVFormer option
    except the test / runtime-only ones, so a new device knob cannot silently fall back to the constructor default in
    the gates (verification 2026-10-13 FAIL-2)."""
    import inspect

    from tt_bevformer.api import KNOBS, tt_model_kwargs
    from tt_bevformer.tt.model import TtBEVFormer

    sig = inspect.signature(TtBEVFormer.__init__)
    options = {n for n, p in sig.parameters.items() if p.kind == p.KEYWORD_ONLY}
    kw = tt_model_kwargs(KNOBS.read())
    assert set(kw) <= options
    assert options - set(kw) == {"num_command_queues", "debug", "image_taps"}
    assert kw["stem_in"] == ("kin" if str(KNOBS.read().STEM_IN) == "kpre" else str(KNOBS.read().STEM_IN))
    assert tt_model_kwargs(KNOBS.read(), stem_in="kpre")["stem_in"] == "kpre"


def test_precision_rules_resolve(monkeypatch):
    """BEVFORMER_PRECISION_RULES (PLAN D20): first matching glob wins, unmatched modules keep the default."""
    from tt_bevformer.tt import kconf

    monkeypatch.setattr(kconf, "PREC_RULES", "*.conv_offset=HiFi4+fp32+l1acc:w=fp32;"
                                             "img_backbone.layer3.*=LoFi+fp32+l1acc:w=bfp8")
    assert kconf.prec("img_backbone.layer3.0.conv2.conv_offset", "x") == "HiFi4+fp32+l1acc:w=fp32"
    assert kconf.prec("img_backbone.layer3.4.conv1", "x") == "LoFi+fp32+l1acc:w=bfp8"
    assert kconf.prec("encoder.layers.0.ffn.0", "x") == "x"
    for off in ("", "none"):
        monkeypatch.setattr(kconf, "PREC_RULES", off)
        assert kconf.prec("img_backbone.layer3.4.conv1", "x") == "x"
    monkeypatch.setattr(kconf, "PREC_RULES", "broken")
    with pytest.raises(ValueError):
        kconf.prec("a", "x")