File size: 11,468 Bytes
aec3f16 | 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 | # SPDX-License-Identifier: Apache-2.0
"""The CPU reference without onnxruntime (host suite: tt-env, research venv): architecture, weights loaders, the
fp32 forward against the stored goldens, teacher-forced partial runs and the stored smoke references.
TT_VISIBLE_DEVICES=none python -m pytest -q code/tt_sceneseg/tests/test_reference_host.py
The weights are found without downloading (``reference.assets``: ``SCENESEG_WEIGHTS_DIR``, the staged
``weights_upload/``, the workspace download); tests that need them skip elsewhere. The goldens were written in the
research venv (torch 2.14) by ``code/scripts/ref_golden.py``; this suite may run on another torch (tt-env: 2.11).
"""
from __future__ import annotations
import json
import numpy as np
import pytest
from tt_sceneseg.reference import assets, config
from tt_sceneseg.reference.weights import ARCH, ARCH_BY_KEY, WeightsError, load_weights, param_count
from tt_sceneseg.tests.research import GOLDENS, LARGE_GOLDENS, SAMPLE, SAMPLES
ONNX = assets.onnx_path(required=False)
needs_onnx = pytest.mark.skipif(ONNX is None, reason="SceneSeg_FP32.onnx not found (set SCENESEG_WEIGHTS_DIR)")
def _macs(spec, hw):
"""MACs of one layer at its input resolution ``hw``; returns (macs, output hw)."""
h, w = hw
if spec.kind == "linear":
return spec.cin * spec.cout, hw
if spec.kind == "conv_transpose":
return spec.cin * spec.cout * spec.k * spec.k * h * w, (h * spec.stride, w * spec.stride)
oh, ow = (h + 2 * spec.pad - spec.k) // spec.stride + 1, (w + 2 * spec.pad - spec.k) // spec.stride + 1
return spec.cout * oh * ow * (spec.cin // spec.groups) * spec.k * spec.k, (oh, ow)
def test_architecture_counts():
"""108 layers (100 Conv, 5 ConvTranspose, 3 Gemm), 16 depthwise, 48,341,495 parameters and 183.514 GMAC at
batch 1 (research/sceneseg/SPEC.md 4.1, verify_onnx.json)."""
assert len(ARCH) == 108 and len(ARCH_BY_KEY) == 108
kinds = [s.kind for s in ARCH]
assert (kinds.count("conv"), kinds.count("conv_transpose"), kinds.count("linear")) == (100, 5, 3)
dw = [s for s in ARCH if s.depthwise]
assert len(dw) == 16 and sorted({(s.k, s.stride) for s in dw}) == [(3, 1), (3, 2), (5, 1), (5, 2)]
params = sum(int(np.prod(s.weight_shape)) + s.cout for s in ARCH)
assert params == 48_341_495
# MACs: walk the layers at their resolutions (backbone strides, context 10x20, decoder up-sampling)
res = {"Backbone": (320, 640)}
total = 0
hw = (320, 640)
for s in ARCH:
if s.key.startswith("Backbone"):
if s.key.endswith((".fc1", ".fc2")):
m, _ = _macs(s, (1, 1))
else:
m, hw = _macs(s, hw)
res["Backbone"] = hw
elif s.kind == "linear":
m, _ = _macs(s, (1, 1))
elif s.key.startswith("SceneContext"):
m, _ = _macs(s, config.CONTEXT_HW)
hw = config.CONTEXT_HW
elif s.kind == "conv_transpose":
m, hw = _macs(s, hw)
else:
m, _ = _macs(s, hw)
total += m
assert res["Backbone"] == (10, 20) and hw == (320, 640)
assert total == pytest.approx(183.514e9, rel=1e-5)
@needs_onnx
def test_onnx_weights():
w = load_weights(ONNX)
assert len(w) == 108 and param_count(w) == 48_341_495
assert all(lw.folded_bn is None for lw in w.values()) # BN was folded at export
assert all(lw.weight.dtype == np.float32 and lw.weight.flags["C_CONTIGUOUS"] for lw in w.values())
sd = assets.state_dict_path(required=False)
if sd is not None: # the .pth / safetensors cross-check
from tt_sceneseg.reference.weights import load_state_dict_weights
p = load_state_dict_weights(sd)
for k, lw in w.items():
ref = p[k]
assert np.abs(lw.weight - ref.weight).max() <= 2e-7 * max(np.abs(ref.weight).max(), 1.0), k
assert np.abs(lw.bias - ref.bias).max() <= 1e-5, k
if lw.spec.bn is None: # unfolded layers: the same numbers
np.testing.assert_array_equal(lw.weight, ref.weight)
np.testing.assert_array_equal(lw.bias, ref.bias)
def test_loader_refuses_other_files(tmp_path, monkeypatch):
bad = tmp_path / config.ONNX_FILE
bad.write_bytes(b"not an onnx file")
with pytest.raises(Exception):
load_weights(bad)
if ONNX is not None: # another export: refused by its sha256
monkeypatch.setattr(config, "ONNX_SHA256", "0" * 64)
with pytest.raises(WeightsError, match="sha256"):
load_weights(ONNX)
assert len(load_weights(ONNX, check_sha256=False)) == len(ARCH) # ... unless asked not to check
def _small_golden(name="highway_normal_1"):
path = GOLDENS / f"{name}.npz"
if not path.is_file():
pytest.skip("tests/goldens not generated (code/scripts/ref_golden.py)")
return np.load(path)
@needs_onnx
@pytest.mark.parametrize("order", config.CHANNEL_ORDERS)
def test_forward_matches_stored_goldens(order):
"""The fp32 forward on the shipped sample against the stored goldens of the research venv run: logits at the
sampled pixels PCC >= 0.99999, class-map agreement >= 0.9999, tap statistics within 1e-4 relative."""
torch = pytest.importorskip("torch")
from tt_sceneseg.host.postprocess import class_map_from_logits
from tt_sceneseg.host.preprocess import prepare_image
from tt_sceneseg.reference.model import SceneSegReference
from tt_sceneseg.ttaw.golden import TapRegistry
from tt_sceneseg.ttaw.metrics import pcc
torch.set_num_threads(4)
g = _small_golden()
meta = json.loads((GOLDENS / "highway_normal_1.json").read_text())
x = prepare_image(str(SAMPLE), channel_order=order).network_input()
taps = TapRegistry(exclude=config.LARGE_TAPS)
logits = SceneSegReference.from_onnx(ONNX).run_numpy(x, taps=taps)["logits"]
yx = g["sample_yx"]
assert pcc(logits[0][:, yx[0], yx[1]], g[f"{order}.logits_samples"]) >= 0.99999
assert (class_map_from_logits(logits) == g[f"{order}.class_map"]).mean() >= 0.9999
for name, st in meta["variants"][order]["taps"].items():
if name in taps.names():
v = taps[name]
assert list(v.shape) == st["shape"], name
scale = max(st["absmax"], 1e-6)
assert abs(float(v.mean()) - st["mean"]) <= 1e-4 * scale and abs(float(v.std()) - st["std"]) <= 1e-4 * scale
@needs_onnx
def test_teacher_forced_runs_equal_the_full_forward():
"""``run_from`` (the inputs of a TT module case are reference taps) reproduces the full forward."""
pytest.importorskip("torch")
from tt_sceneseg.host.preprocess import prepare_image
from tt_sceneseg.reference.model import SceneSegReference
from tt_sceneseg.ttaw.golden import TapRegistry
ref = SceneSegReference.from_onnx(ONNX)
taps = TapRegistry(include=("l0", "l2", "l3", "l4", "l8", "context", "neck", "logits"))
full = ref.run_numpy(prepare_image(str(SAMPLE)).network_input(), taps=taps)["logits"]
t = taps.to_dict()
for start in ("context", "neck", "head"):
got = ref.run_from(start, t)["logits"]
np.testing.assert_allclose(got, full, rtol=0, atol=1e-4, err_msg=start)
with pytest.raises(ValueError):
ref.run_from("backbone", t)
@needs_onnx
@pytest.mark.parametrize("order", config.CHANNEL_ORDERS)
def test_pipeline_equals_stored_smoke_reference(order):
"""``samples/highway_normal_1.<variant>.reference.json`` (the container smoke's reference) is what the
reference pipeline returns: the same published mask and class map (agreement >= 0.9999 across torch builds)."""
pytest.importorskip("torch")
from tt_sceneseg.reference.pipeline import ReferencePipeline
from tt_sceneseg.ttaw.server import smoke
path = SAMPLES / f"highway_normal_1.{order}.reference.json"
if not path.is_file():
pytest.skip("stored reference not generated (code/scripts/ref_golden.py)")
stored = json.loads(path.read_text())
body = ReferencePipeline(ONNX, order, threads=4).reference_body(str(SAMPLE))
metrics, fails = smoke.compare_with_reference(body, stored, gates={"min_label_agreement": 0.9999})
assert fails == [], fails
assert set(metrics) == {"mask", "class_map"}
assert body["meta"]["channel_order"] == order and body["class_names"] == list(config.CLASS_NAMES)
def test_large_goldens_index():
"""research/sceneseg/goldens/index.json (written with the goldens): the module taps of the gate samples exist
with the recorded sha256 (sizes only checked here; the device tests read them)."""
idx = LARGE_GOLDENS / "index.json"
if not idx.is_file():
pytest.skip("research/sceneseg/goldens not generated")
meta = json.loads(idx.read_text())
for rel, rec in meta["files"].items():
p = LARGE_GOLDENS / rel
assert p.is_file() and p.stat().st_size == rec["bytes"], rel
def test_comma10k_goldens_reproduce_the_spec():
"""The comma10k ground-truth metric (tests/sceneseg_eval.py) on the stored fp32 class maps (shipped copies in
tests/goldens/comma10k/): the per-image IoUs of
the index, and the SPEC section 3 means (training ROI crop: foreground 0.729 bgr / 0.765 rgb, road 0.907 /
0.919): the FP32 side of the PLAN 2.2 comma10k gate."""
from tt_sceneseg.tests.sceneseg_eval import COMMA10K_GOLDENS, comma10k_ious
rec = json.loads((COMMA10K_GOLDENS / "index.json").read_text()) # shipped since the release docs
for name, r in rec["images"].items():
with np.load(COMMA10K_GOLDENS / f"{name[:4]}.npz") as z:
for order in config.CHANNEL_ORDERS:
assert comma10k_ious(z[f"cls_{order}"], z["gt"]) == pytest.approx(r[order], nan_ok=True), (name, order)
spec = {"bgr": (0.729, 0.907), "rgb": (0.765, 0.919)}
for order, (fg, road) in spec.items():
assert rec["mean"][order]["foreground"] == pytest.approx(fg, abs=1e-3)
assert rec["mean"][order]["road"] == pytest.approx(road, abs=1e-3)
def test_shipped_comma10k_matches_the_gate_inputs():
"""The comma10k images, masks and fp32 goldens shipped since the release docs are byte-identical to the ones the
frozen comma10k gate was set on (sha256 of research/sceneseg/goldens/index.json, recorded with the goldens), and
the workspace copies (when present) still match."""
import hashlib
from tt_sceneseg.tests.sceneseg_eval import COMMA10K_GOLDENS, comma10k_samples
def sha(p):
return hashlib.sha256(p.read_bytes()).hexdigest()
rec = json.loads((COMMA10K_GOLDENS / "index.json").read_text())
samples = comma10k_samples()
assert sorted(samples) == sorted(rec["images"])
for name, (img, mask) in samples.items():
assert sha(img) == rec["images"][name]["image_sha256"], name
assert sha(mask) == rec["images"][name]["mask_sha256"], name
for fname, f in rec["files"].items():
assert sha(COMMA10K_GOLDENS / fname) == f["sha256"], fname
idx = LARGE_GOLDENS / "index.json"
if idx.is_file():
large = json.loads(idx.read_text())
assert large["comma10k"]["images"] == rec["images"] and large["comma10k"]["mean"] == rec["mean"]
for fname, f in rec["files"].items():
assert large["files"][f"comma10k/{fname}"] == f, fname
|