File size: 12,456 Bytes
7cc5047 | 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 | # SPDX-License-Identifier: Apache-2.0
"""The CPU reference (``reference/``: ONNX initializers by node, every BN folded where the port folds it, torch fp32)
vs the deployed graph's oracles (PLAN.md 0.3 item 1; BUNDLE_CONVENTIONS.md section 10), on identical inputs (the
host pre-processing of ``host/``). ONNX Runtime cannot load ``bevfusion_lidar.onnx`` (its sparse convs are TensorRT
plugins, S:bevfusion:343-351), so the oracle is split as the SPEC prescribes (SPEC 6.2, PLAN.md 2.6 "CPU reference"):
- **sparse encoder** vs the research reference (``research/bevfusion/scripts/bevfusion_ref.py``: its own hashed
rulebooks and an ``index_add_`` scatter form with the BatchNorms unfolded, independent of this bundle's C13 gather
maps): every layer output (conv_input, both blocks of every level, the three strided convs, conv_out) and the
to-dense BEV at PCC >= 0.9999, the active sets of every level identical;
- **dense part before the data-dependent selection** vs ONNX Runtime on the deployed graph's plugin-free dense
sub-graph fed with the same BEV: every tap (SECOND convs and blocks, deblocks, neck, shared conv, heatmap head,
sigmoid heat, the local-max heatmap, the key position embedding) at PCC >= 0.9999;
- **the top-500 selection set-based**: >= 99 % of the set shared, and every ORT proposal with heat >= 0.1 (the
strong proposals) in the reference's set;
- **after it teacher-forced**: the reference run with ORT's own top-k indices reproduces every downstream tap
(query features / positions / heat, decoder layers, prediction heads, ``bbox_pred``, ``score``) at PCC >= 0.9999
and ``label_pred`` exactly;
- **end to end set-based**: the same detections after Autoware's post-processing (same label, centre within 1 cm,
score within 1e-4) and the same counts at every stage;
- the research goldens (the research reference end to end: numpy voxelizer, torch scatter encoder, ORT dense): the
same voxel tensors and BEV, the same heatmap, top-500 set and detections.
Cases: the shipped samples (PandaSet 019 f40, test.pcd), the Autoware sample-rosbag top scan #144 (the SPEC's frame;
research data, license unstated) and nuScenes mini_val scene-0103 key-frame 20 (sparse 32-beam; research only).
Runs in the research venv (onnxruntime + torch, CPU only; threads <= 4)::
cd bundles/bevfusion-p150 && OMP_NUM_THREADS=4 PYTHONPATH=code \\
/home/ubuntu/experiments/tt-models/tools/research-venv/bin/python -m pytest -q -p no:cacheprovider -s \\
code/tt_bevfusion/tests/test_reference_cpu.py
Skipped where onnxruntime, the weights or the research reference are absent.
"""
from __future__ import annotations
import json
import numpy as np
import pytest
pytest.importorskip("onnxruntime")
pytest.importorskip("onnx")
torch = pytest.importorskip("torch")
from tt_bevfusion.reference.model import SELECTION_TAPS # noqa: E402
from tt_bevfusion.reference.ort import DENSE_ORT_TAPS # noqa: E402
from tt_bevfusion.reference.pipeline import BEVFusionReference # noqa: E402
from tt_bevfusion.reference.weights import ENCODER_PLAN # noqa: E402
from tt_bevfusion.tests import _data # noqa: E402
from tt_bevfusion.ttaw.golden import TapRegistry # noqa: E402
from tt_bevfusion.ttaw.metrics import match_detections, pcc # noqa: E402
PCC_MIN = 0.9999 # PLAN.md 0.3 item 1
THREADS = 4
STRONG = 0.1 # strong proposals (heat >= 0.1) must all be selected
CASES = {
"pandaset_019_f40": ("sample", "pandaset_019_f40.npz"),
"test_pcd": ("sample", "test_pcd.npz"),
"rosbag_0144": ("research", "rosbag_0144"),
"ns0103_k20": ("research", "ns0103_k20"),
}
@pytest.fixture(scope="module")
def references():
w = _data.weights_dir()
torch.set_num_threads(THREADS)
return (BEVFusionReference(w, backend="torch", threads=THREADS),
BEVFusionReference(w, backend="hybrid", threads=THREADS, intermediates=True))
def _source(case):
kind, name = CASES[case]
return _data.sample_path(name) if kind == "sample" else _data.research_cloud(name)
_RUNS = {}
def _runs(references, case):
if case not in _RUNS:
rt, rh = references
src = _source(case)
tt, th = TapRegistry(), TapRegistry() # every tap (module and per layer)
a = rt.run(src, taps=tt)
b = rh.run(src, taps=th)
_RUNS.clear() # keep one case in memory
_RUNS[case] = (a, b, tt, th)
return _RUNS[case]
def _research_encoder_taps(R, inits, vfe, coors):
"""The research reference's sparse encoder (``SparseEncoderRef.forward``) layer by layer, with its own
rulebooks, scatter-form convs and unfolded BatchNorms: {tap name: (N, C) features} + the level coords."""
enc = R.SparseEncoderRef(inits)
P = enc.P
out, coords = {}, {}
x = torch.from_numpy(vfe.astype(np.float32))
c = torch.from_numpy(coors[:, ::-1].astype(np.int64).copy())
level = R.SparseLevel(c, enc.SPATIAL)
coords["l1"] = level.coords.numpy()
rb = R.subm_rulebook(level, (3, 3, 3), (1, 1, 1))
x = torch.relu(enc.bn(R.apply_rulebook(x, rb, enc.w[P + "conv_input.0.weight"], len(level.coords)),
P + "conv_input.1"))
out["sparse.conv_input"] = x
for stage, nblocks, down in enc.STAGES:
rb = R.subm_rulebook(level, (3, 3, 3), (1, 1, 1))
for blk in range(nblocks):
pre = f"{P}encoder_layers.encoder_layer{stage}.{blk}."
idn = x
y = torch.relu(enc.bn(R.apply_rulebook(x, rb, enc.w[pre + "conv1.weight"], len(level.coords)),
pre + "bn1"))
out[f"sparse.layer{stage}.{blk}.conv1"] = y
y = enc.bn(R.apply_rulebook(y, rb, enc.w[pre + "conv2.weight"], len(level.coords)), pre + "bn2")
x = torch.relu(y + idn)
out[f"sparse.layer{stage}.{blk}"] = x
if down is not None:
_, _, pad = down
pre = f"{P}encoder_layers.encoder_layer{stage}.2."
rb_d, level = R.strided_rulebook(level, (3, 3, 3), (2, 2, 2), pad)
x = torch.relu(enc.bn(R.apply_rulebook(x, rb_d, enc.w[pre + "0.weight"], len(level.coords)), pre + "1"))
out[f"sparse.down{stage}"] = x
coords[f"l{stage + 1}"] = level.coords.numpy()
rb_o, out_level = R.strided_rulebook(level, (1, 1, 3), (1, 1, 2), (0, 0, 0))
x = torch.relu(enc.bn(R.apply_rulebook(x, rb_o, enc.w[P + "conv_out.0.weight"], len(out_level.coords)),
P + "conv_out.1"))
out["sparse.conv_out"] = x
coords["out"] = out_level.coords.numpy()
return {k: v.numpy() for k, v in out.items()}, coords
@pytest.mark.parametrize("case", list(CASES))
def test_sparse_encoder_matches_the_research_reference(references, case):
rt, _ = references
a, _, tt, _ = _runs(references, case)
R = _data.research_module()
inits = R.load_initializers(str(_data.weights_dir() / _data.ONNX))
ref, coords = _research_encoder_taps(R, inits, a.prepared.vfe, a.prepared.coors)
plan = a.prepared.plan
for lvl, c in coords.items(): # identical active sets (the research's in key order)
mine = plan.levels[lvl].coords
assert len(mine) == len(c), lvl
if lvl != "l1":
assert np.array_equal(mine, c), lvl # both sort strided outputs by the (x, y, z) linear key
names = [name for name, *_ in ENCODER_PLAN]
worst = {name: pcc(tt[name], ref[name]) for name in names}
worst["sparse.bev"] = pcc(tt["sparse.bev"], R.SparseEncoderRef(inits).forward(a.prepared.vfe, a.prepared.coors))
print(case, plan.describe()["counts"], json.dumps({k: round(v, 10) for k, v in
sorted(worst.items(), key=lambda kv: kv[1])[:4]}))
bad = {k: v for k, v in worst.items() if not v >= PCC_MIN}
assert not bad, bad
@pytest.mark.parametrize("case", list(CASES))
def test_dense_taps_before_the_selection_match_onnxruntime(references, case):
a, b, tt, th = _runs(references, case)
assert np.array_equal(a.prepared.vfe, b.prepared.vfe)
names = [n for n in DENSE_ORT_TAPS if n not in SELECTION_TAPS]
assert set(names) <= set(th.names()) and set(names) <= set(tt.names())
worst = {}
for name in names:
x, y = tt[name], th[name]
assert x.size == y.size, name
worst[name] = pcc(x, y.reshape(x.shape))
print(case, json.dumps({k: round(v, 10) for k, v in sorted(worst.items(), key=lambda kv: kv[1])[:5]}))
bad = {k: v for k, v in worst.items() if not v >= PCC_MIN}
assert not bad, bad
@pytest.mark.parametrize("case", list(CASES))
def test_topk_selection_set_based(references, case):
_, _, tt, th = _runs(references, case)
mine, ref = tt["head.topk_indices"].ravel(), th["head.topk_indices"].ravel()
k = ref.size
shared = len(set(mine.tolist()) & set(ref.tolist()))
strong = ref[th["head.topk_values"].ravel() >= STRONG]
print(case, f"top-{k} shared {shared}, strong {len(strong)}, positional {np.mean(mine == ref):.4f}")
assert k == 500 and shared >= 0.99 * k
assert set(strong.tolist()) <= set(mine.tolist())
np.testing.assert_allclose(np.sort(tt["head.topk_values"].ravel()), np.sort(th["head.topk_values"].ravel()),
atol=1e-4)
@pytest.mark.parametrize("case", list(CASES))
def test_teacher_forced_decoder_matches_onnxruntime(references, case):
rt, _ = references
_, b, _, th = _runs(references, case)
tf = TapRegistry(include=SELECTION_TAPS)
rt.network(b.prepared, tf, topk_indices=th["head.topk_indices"])
assert np.array_equal(tf["head.topk_indices"], th["head.topk_indices"])
assert np.array_equal(tf["label_pred"], th["label_pred"])
worst = {}
for name in SELECTION_TAPS:
if name in ("head.topk_indices", "label_pred"):
continue
x, y = tf[name], th[name]
assert x.size == y.size, name
worst[name] = pcc(x, y.reshape(x.shape))
print(case, json.dumps({k: round(v, 10) for k, v in sorted(worst.items(), key=lambda kv: kv[1])[:5]}))
bad = {k: v for k, v in worst.items() if not v >= PCC_MIN}
assert not bad, bad
assert np.array_equal(tf["query.pos"], th["query.pos"]) # exact arithmetic query_pos
@pytest.mark.parametrize("case", list(CASES))
def test_detections_match_onnxruntime_set_based(references, case):
a, b, _, _ = _runs(references, case)
assert a.counts == b.counts
da, db = a.output, b.output
m = match_detections(da.boxes, da.label_ids, da.scores, db.boxes, db.label_ids, db.scores, max_dist=0.01)
assert m.recall == 1.0 and m.precision == 1.0, m.to_dict()
assert max(m.score_errors, default=0.0) < 1e-4
assert len(da) == len(db) and len(da) > 0
@pytest.mark.parametrize("case", ["rosbag_0144", "pandaset_019_f40", "ns0103_k20"])
def test_reproduces_the_research_goldens(references, case):
"""The research reference's goldens of the same frame (rosbag: ``golden_leveled/golden_lidar.npz``; public:
``public_data/golden/<id>_lidar.npz``): identical voxel tensors, the BEV (when stored), the sigmoid heatmap,
the top-500 set and the Autoware detections (labels after the remapper, centre within 1 cm, score within 1e-4)."""
a, _, tt, _ = _runs(references, case)
g = _data.research_golden("ps019_f40" if case == "pandaset_019_f40" else case)
prep = a.prepared
assert np.array_equal(prep.coors, g["coors"]) and np.array_equal(prep.voxel_set.num_points,
g["num_points_per_voxel"])
assert np.array_equal(prep.vfe, g["vfe"])
if "bev_lidar" in g.files:
assert pcc(tt["sparse.bev"], g["bev_lidar"]) >= PCC_MIN
if "heatmap" in g.files:
assert pcc(tt["head.heat"], g["heatmap"]) >= PCC_MIN
if "topk_indices" in g.files:
shared = len(set(tt["head.topk_indices"].ravel().tolist()) & set(g["topk_indices"].ravel().tolist()))
assert shared >= 495
det = g["detections"] # label (Autoware), score, x, y, z, l, w, h, yaw, vx, vy
out = a.output
m = match_detections(out.boxes, out.label_ids, out.scores, det[:, [2, 3, 4, 5, 6, 7, 8]], det[:, 0].astype(int),
det[:, 1], max_dist=0.01)
assert m.recall == 1.0 and m.precision == 1.0, m.to_dict()
assert max(m.score_errors, default=0.0) < 1e-4
|