frnet-p150 / code /tt_frnet /tests /test_reference_cpu.py
changh95's picture
tt-model push frnet-p150 (container)
fe51a63 verified
Raw History Blame Contribute Delete
7.34 kB
# 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