File size: 7,337 Bytes
fe51a63 | 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 | # SPDX-License-Identifier: Apache-2.0
"""The CPU reference (``reference/``: ONNX initializers by node, BN folded where the port folds it, torch fp32) vs
ONNX Runtime on the deployed ONNX as Autoware binds it (PLAN.md 0.3 item 1; BUNDLE_CONVENTIONS.md section 10):
- FRNet has no data-dependent selection inside the network (no top-k, no NMS): every module tap
(``weights.MODULE_TAPS``: encoder point features and voxel max, frustum image, stem, point stem, fusion stem, the
four stages' blocks / point fusion / pixel fusion / attention / outputs, decoder, head, logits, ``pred_probs``)
and the opt-in taps (per-voxel max of every point branch, the five up-sampled decoder inputs) at
**PCC >= 0.9999** against the same tensor of the deployed graph (ORT exposes it as an extra graph output);
- after the selection, i.e. the per-point argmax of the post-processing: **set-based**, the class ids agree on
>= 99.99 % of the points and every disagreement is a near-tie of the ORT probabilities (top-2 margin < 1e-4);
- cases: the staged PandaSet sample through the request path (both variants), the hardest public frame for bf16 (nuScenes
0916 kf 20, OT128) and the densest-cell frame (VLS-128 x > 0 half, QT128: up to 60 points per cell);
- ORT itself reproduces the research goldens' ``pred_probs`` bit for bit (deterministic CPU EP), and the TT dataflow
(``rewrites.py``) is held to the same tap gates against ORT on the OT128 sample and the VLS-128 QT128 frame.
Runs in the research venv (onnxruntime + torch, CPU only; keep threads <= 4 on the shared host)::
cd bundles/frnet-p150 && OMP_NUM_THREADS=4 PYTHONPATH=code \\
/home/ubuntu/experiments/tt-models/tools/research-venv/bin/python -m pytest -q -p no:cacheprovider \\
code/tt_frnet/tests/test_reference_cpu.py
Skipped where onnxruntime or the weights 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_frnet.host.preprocess import build_plan # noqa: E402
from tt_frnet.reference.model import FRNetTorch # noqa: E402
from tt_frnet.reference.ort import OrtFRNet # noqa: E402
from tt_frnet.reference.pipeline import FRNetReference # noqa: E402
from tt_frnet.reference.rewrites import FRNetPlanTorch, plan_taps_to_literal # noqa: E402
from tt_frnet.reference.weights import MODULE_TAPS, load_weights # noqa: E402
from tt_frnet.tests import _data # noqa: E402
from tt_frnet.ttaw.golden import TapRegistry # noqa: E402
from tt_frnet.ttaw.metrics import pcc # noqa: E402
PCC_MIN = 0.9999 # PLAN.md 0.3 item 1
AGREE_MIN = 0.9999 # set-based class ids after the argmax
TIE = 1e-4 # a disagreement must be an ORT near-tie
THREADS = 4
PATTERNS = list(MODULE_TAPS) + ["backbone.segmax*", "decoder.up*"]
@pytest.fixture(scope="module")
def weights():
w = _data.weights_dir()
torch.set_num_threads(THREADS)
return {v: load_weights(w, v) for v in ("ot128", "qt128")}
def _inputs(case, weights):
"""``(NetworkInputs, research golden pred_probs or None)`` of a case: ``sample.<variant>`` goes through the
request path (staged PandaSet sample + its calibration), a catalogued frame is read from its research golden."""
kind, _, variant = case.rpartition(".")
if kind == "sample":
_data.need_pandaset()
ref = FRNetReference(_data.weights_dir(), variant, backend="torch", threads=THREADS, weights=weights[variant])
prep = ref.pre(_data.SAMPLE, _data.SAMPLE_CALIB)
assert prep.skipped is None
_, gold_probs, _ = _data.load_frame(f"{_data.SAMPLE_STEM}.{variant}")
return prep.inputs, gold_probs
inputs, gold_probs, _ = _data.load_frame(case)
return inputs, gold_probs
CASES = ["sample.ot128", "sample.qt128"] + _data.HARD_FRAMES
@pytest.fixture(scope="module", params=CASES)
def run(request, weights):
"""One case's torch and ORT outputs with every tap (module-scoped and parametrized: pytest runs every test of a
case together and drops the case before the next one; OT128 taps are ~2.5 GB per side)."""
case = request.param
variant = case.rpartition(".")[2]
inputs, gold = _inputs(case, weights)
tt, to = TapRegistry(include=PATTERNS), TapRegistry(include=PATTERNS)
probs_t = FRNetTorch(weights[variant])(inputs.points, inputs.coors, inputs.voxel_coors, inputs.inverse_map,
taps=tt)
ort = OrtFRNet(_data.weights_dir(), variant, threads=THREADS, intermediates=True, tap_patterns=PATTERNS,
weights=weights[variant])
probs_o = ort(inputs.points, inputs.coors, inputs.voxel_coors, inputs.inverse_map, taps=to)
del ort
yield case, inputs, gold, probs_t, probs_o, tt, to
tt.clear()
to.clear()
def test_taps_match_onnxruntime(run):
case, inputs, gold, probs_t, probs_o, tt, to = run
names = to.names()
assert set(MODULE_TAPS) <= set(names) and {f"backbone.segmax{i}" for i in range(5)} <= set(names)
worst = {}
for name in names:
x, y = tt[name], to[name]
assert x.shape == y.shape, name
worst[name] = pcc(x, y)
print(case, json.dumps({k: round(v, 12) 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
assert np.abs(probs_t - probs_o).max() < 1e-4
def test_class_ids_set_based_after_the_argmax(run):
case, inputs, gold, probs_t, probs_o, _, _ = run
a, b = probs_t.argmax(1), probs_o.argmax(1)
assert (a == b).mean() >= AGREE_MIN
flips = np.flatnonzero(a != b)
if len(flips):
top2 = np.sort(probs_o[flips], axis=1)[:, -2:]
assert (top2[:, 1] - top2[:, 0] < TIE).all(), "a disagreement that is not a near-tie"
def test_onnxruntime_reproduces_the_research_golden(run):
"""Deterministic CPU EP: the same inputs give the research golden's pred_probs bit for bit."""
_, _, gold, _, probs_o, _, _ = run
assert gold is not None and np.array_equal(probs_o, gold)
def test_tt_dataflow_matches_onnxruntime(run, weights):
"""``rewrites.py`` (sorted + padded points, gather-form scatters, log-step max, K-split, interpolation
matrices) against the deployed graph at the same tap gates (OT128 sample and the VLS-128 QT128 frame, whose
cells hold up to 60 points: 6 log-step rounds)."""
case, inputs, _, _, probs_o, _, to = run
if case not in ("sample.ot128", "vls128_frame000_fronthalf.qt128"):
pytest.skip("the TT dataflow is compared on the OT128 sample and the densest-cell frame")
variant = case.rpartition(".")[2]
w = weights[variant]
cfg = FRNetReference(_data.weights_dir(), variant, backend="torch", weights=w).config
plan = build_plan(inputs, cfg, pre_norm=(w.pre_norm_scale, w.pre_norm_shift))
tr = TapRegistry(include=PATTERNS)
out = plan.unsort(FRNetPlanTorch(w)(plan, taps=tr))
worst = min((pcc(plan_taps_to_literal(n, tr[n], plan), to[n]), n) for n in to.names())
print(case, "plan vs ORT worst tap", worst, "rounds", plan.stats()["segmax_rounds"])
assert worst[0] >= PCC_MIN, worst
assert np.abs(out - probs_o).max() < 1e-4 and (out.argmax(1) == probs_o.argmax(1)).mean() >= AGREE_MIN
|