bevformer-p150 / code /tt_bevformer /tests /test_api_host.py
changh95's picture
tt-model push bevformer-p150 (container)
55474c5 verified
Raw History Blame Contribute Delete
15.7 kB
# 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")