File size: 7,175 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 | # SPDX-License-Identifier: Apache-2.0
"""Host tests of the API wiring (no device): identity and runtime parameters, the host half of the hooks
(``_build_host``, ``_prepare``, ``_postprocess``) with the fp32 torch reference network in the device's place, against
the stored CPU reference of the shipped samples (compared with the container smoke's own comparator,
``ttaw.server.smoke``); the ``sampling`` input keyword; a frame Autoware skips; the server's ``sampling`` field.
TT_VISIBLE_DEVICES=none python -m pytest -q -p fake_ttnn_plugin code/tt_transfusion/tests/test_api_host.py
"""
from __future__ import annotations
import base64
import json
import numpy as np
import pytest
from tt_transfusion import io as tio
from tt_transfusion.api import TransFusion
from tt_transfusion.host.postprocess import RUNTIME_PARAMS
from tt_transfusion.reference.config import NODE_PARAMS
from tt_transfusion.tests import _data
from tt_transfusion.ttaw.decode import AUTOWARE_LABELS
from tt_transfusion.ttaw.server import smoke
def test_identity_and_runtime_params():
assert tuple(TransFusion.VARIANTS) == ("default",) and TransFusion.DEFAULT_VARIANT == "default"
assert tuple(TransFusion.LABELS) == AUTOWARE_LABELS and TransFusion.LABELS[7] == "PEDESTRIAN"
assert TransFusion.RUNTIME_PARAMS is RUNTIME_PARAMS and TransFusion.EXTRA_INPUTS == ("sampling",)
assert TransFusion.DEFAULT_REVISION == "8f07eb93038bfd62e35d1f78e56a6b3fb76b12aa"
p = TransFusion.validate_params({})
assert p["score_threshold"] == 0.1 and p["remap_classes"] is True and p["max_detections"] is None
for k in ("circle_nms_dist_threshold", "iou_nms_threshold", "iou_nms_search_distance_2d"):
assert p[k] == NODE_PARAMS[k] # Autoware's defaults
assert TransFusion.validate_params({"score_threshold": "0.5"})["score_threshold"] == 0.5
for bad in ({"score_threshold": 1.5}, {"remap_classes": 1}, {"max_detections": 0}, {"iou_nms_threshold": 1.5},
{"sampling": "clean"}): # sampling is an input keyword, not a runtime param
with pytest.raises(tio.InputError):
TransFusion.validate_params(bad)
def _host_model() -> TransFusion:
m = TransFusion.__new__(TransFusion) # no device: the host half only
m.weights_path = _data.weights_dir()
m.variant = "default"
m._build_host()
return m
@pytest.fixture(scope="module")
def net():
torch = pytest.importorskip("torch")
pytest.importorskip("onnx")
from tt_transfusion.reference.model import TransFusionTorch
from tt_transfusion.reference.weights import load_weights
torch.set_num_threads(2)
return TransFusionTorch(load_weights(_data.weights_dir()), threads=2)
@pytest.mark.parametrize("sample", ["test_pcd", "pandaset_019_f40"])
def test_hooks_reproduce_the_stored_reference(net, sample):
m = _host_model()
prep = m._prepare(points=_data.sample_path(f"{sample}.npz"))
raw = net.forward(prep.features, prep.coords, prep.num_pillars)
out = m._postprocess(raw, prep, m.validate_params({}))
body = out.to_dict()
ref = json.loads(_data.sample_path(f"{sample}.reference.json").read_text())
metrics, fails = smoke.compare_with_reference(body, ref, gates={"max_center_dist": 0.01, "min_recall": 1.0,
"min_precision": 1.0, "max_score_err": 1e-4})
assert fails == [], (fails, metrics)
assert body["meta"]["counts"] == ref["meta"]["counts"] and body["meta"]["variant"] == "default"
assert body["meta"]["num_pillars"] == ref["meta"]["num_pillars"]
assert len(body["meta"]["orientation_availability"]) == body["num_detections"] == len(body["meta"]["proposal"])
def test_prepare_contract():
m = _host_model()
with pytest.raises(tio.InputError):
m._prepare(points=None)
pts = np.random.default_rng(0).uniform(-40, 40, (2000, 4)).astype(np.float32)
with pytest.raises(tio.InputError, match="point cloud only"):
m._prepare(points=pts, images=[{"camera": "front"}])
with pytest.raises(tio.InputError, match="sampling"):
m._prepare(points=pts, sampling="random")
clean = m._prepare(points=pts)
assert clean.sampling == {"mode": "clean", "offset": 0, "voxelizer_points": 2000}
assert clean.skipped is None and clean.autoware_would_skip == "too_few_voxels"
def test_a_frame_autoware_skips_gives_no_objects():
"""``autoware_compat`` + fewer than 5,000 pillars: no network run (``_forward`` returns None without touching a
device), an empty output carrying the reason."""
m = _host_model()
pts = np.random.default_rng(1).uniform(-40, 40, (2000, 4)).astype(np.float32)
prep = m._prepare(points=pts, sampling={"mode": "autoware_compat", "offset": 7})
assert prep.skipped == "too_few_voxels"
raw = m._forward(prep)
out = m._postprocess(raw, prep, m.validate_params({}))
body = out.to_dict()
assert raw is None and body["num_detections"] == 0 and body["meta"]["skipped"] == "too_few_voxels"
assert body["meta"]["sampling"]["mode"] == "autoware_compat"
def test_server_accepts_the_sampling_field(monkeypatch):
pytest.importorskip("fastapi")
from fastapi.testclient import TestClient
from tt_transfusion.server import app as server
from tt_transfusion.tests.stubs import StubModel
seen = {}
class Recorder(StubModel):
def __call__(self, points=None, **kw):
seen.update(kw)
return super().__call__(points, **kw)
monkeypatch.setenv("TT_MESH_SHAPE", "1x1")
monkeypatch.setattr(server.app.state.ttaw, "model_factory", Recorder)
pts = np.random.default_rng(0).normal(size=(10, 4)).astype(np.float32)
data = base64.b64encode(pts.tobytes()).decode()
with TestClient(server.app) as c:
r = c.post("/predict", json={"points": {"format": "bin", "data": data},
"sampling": {"mode": "autoware_compat", "offset": 5}})
assert r.status_code == 200, r.text
assert seen["sampling"] == {"mode": "autoware_compat", "offset": 5}
assert c.post("/predict", json={"points": {"format": "bin", "data": data}, "samplin": "x"}).status_code == 422
def test_a_frame_without_pillars_runs(net):
"""No point inside the range: zero pillars, an all-zero canvas, the network still runs (Autoware would skip the
frame: reported as ``autoware_would_skip``) and the post-processing returns a valid, possibly empty, output."""
m = _host_model()
pts = np.array([[200.0, 0.0, 0.0, 10.0], [0.0, 0.0, 9.0, 10.0]], np.float32)
prep = m._prepare(points=pts)
assert prep.num_pillars == 0 and prep.autoware_would_skip == "too_few_voxels"
assert (prep.canvas_index == prep.features.shape[0]).all() and not prep.features.any()
raw = net.forward(prep.features, prep.coords, prep.num_pillars)
assert raw["cls_score0"].shape == (1, 5, 500) and np.isfinite(raw["bbox_pred0"]).all()
body = m._postprocess(raw, prep, m.validate_params({})).to_dict()
assert body["meta"]["num_pillars"] == 0 and body["num_detections"] == len(body["meta"]["proposal"])
|