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")
|