File size: 18,545 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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Goldens of the fp32 CPU reference (``tt_sceneseg.reference``) for the TT port's tests. CPU only (no device).

    OMP_NUM_THREADS=4 tools/research-venv/bin/python -I bundles/sceneseg-p150/code/scripts/ref_golden.py

Writes, for both channel orders (``bgr`` deployed, ``rgb`` training order):

- SMALL, shipped with the bundle (``code/tt_sceneseg/tests/goldens/``): per shipped sample (the 9 tutorial images)
  ``<stem>.npz`` = the 320x640 class map, the published mask at the source size and the 3 logits at 4096 fixed pixels
  (``sample_yx``), and ``<stem>.json`` = statistics (class fractions, decision margins; every tap's shape / mean / std
  / absmax / sha256 for ``highway_normal_1``).
- the container smoke's stored references, ``code/tt_sceneseg/samples/highway_normal_1.<order>.reference.json``
  (``ReferencePipeline.reference_body``: the ``/predict`` body without timings).
- LARGE, never shipped (``research/sceneseg/goldens/``): ``taps/<id>.<order>.npz`` = the module taps of
  ``config.MODULE_TAPS`` (fp32; what the TT module gates compare and teacher-force) for the gate samples, and
  ``public/<id>.npz`` = logits (fp32) and class maps of both orders for every converted public frame of
  research/sceneseg/public_data (PandaSet, nuScenes, comma10k, tutorial), plus ``index.json`` (sizes, sha256, the
  agreement with the research ORT goldens, versions).

The inputs are built with the node's exact pre-processing (``host.preprocess``); public frames start from the
converter's lossless decode (``inputs/<id>/source_bgr.png``). Nothing downloaded is executed (``-I``; weights are
read as data by ``reference.weights``).
"""
from __future__ import annotations

import argparse
import datetime as dt
import hashlib
import json
import sys
import time
from pathlib import Path

BUNDLE = Path(__file__).resolve().parents[2]
WS = BUNDLE.parents[1]
sys.path.insert(0, str(BUNDLE / "code"))

import numpy as np  # noqa: E402

from tt_sceneseg.reference import assets, config  # noqa: E402

PKG = BUNDLE / "code" / "tt_sceneseg"
SMALL = PKG / "tests" / "goldens"
SAMPLES = PKG / "samples"
LARGE = WS / "research" / "sceneseg" / "goldens"
PUBLIC = WS / "research" / "sceneseg" / "public_data"
TUTORIAL = ("highway_normal_1", "highway_normal_2", "highway_normal_3", "highway_normal_4", "highway_normal_5",
            "highway_rain_1", "highway_rain_2", "highway_rain_3", "highway_snow")
SMOKE_SAMPLE = "highway_normal_1"
TAP_SAMPLES = (("sample_highway_normal_1", None), ("ps019_f40_front_camera", ("bgr",)))   # (id, orders or None=all)
N_SAMPLES = 4096


def sha256_bytes(b: bytes) -> str:
    return hashlib.sha256(b).hexdigest()


def sha256_file(p: Path) -> str:
    h = hashlib.sha256()
    with open(p, "rb") as f:
        for block in iter(lambda: f.read(1 << 20), b""):
            h.update(block)
    return h.hexdigest()


def sha256_array(a: np.ndarray) -> str:
    a = np.ascontiguousarray(a)
    return sha256_bytes(f"{a.dtype.str}{a.shape}".encode() + a.tobytes())


def stats(a: np.ndarray) -> dict:
    a64 = np.asarray(a, np.float64)
    return {"shape": list(a.shape), "mean": float(a64.mean()), "std": float(a64.std()),
            "absmax": float(np.abs(a64).max()), "sha256": sha256_array(np.asarray(a, np.float32))}


def margin_stats(logits: np.ndarray) -> dict:
    """How many pixels a small logit change can flip (the pixels a bf16 port is likely to change)."""
    lg = np.sort(logits[0].astype(np.float64), axis=0)
    gap = lg[-1] - lg[-2]
    cls = np.argmax(logits[0], axis=0)
    out = {f"frac_within_{t}": float((gap < t).mean()) for t in (0.05, 0.25, 1.0)}
    fg = cls == config.FOREGROUND_CLASS
    out["fg_frac_within_0.25"] = float((gap[fg] < 0.25).mean()) if fg.any() else 0.0
    return out


def class_fractions(cls: np.ndarray) -> dict:
    return {config.CLASS_NAMES[c]: float((cls == c).mean()) for c in range(config.NUM_CLASSES)}


def load_bgr_png(path: Path) -> np.ndarray:
    from PIL import Image

    with Image.open(path) as im:
        return np.ascontiguousarray(np.asarray(im.convert("RGB"))[:, :, ::-1])


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    ap.add_argument("--threads", type=int, default=4)
    ap.add_argument("--skip-public", action="store_true", help="no research/sceneseg/goldens/public")
    ap.add_argument("--skip-taps", action="store_true", help="no research/sceneseg/goldens/taps")
    ap.add_argument("--only", choices=("comma10k",), help="only this part (the index is updated in place)")
    a = ap.parse_args()
    import torch

    torch.set_num_threads(a.threads)
    from tt_sceneseg.host.postprocess import postprocess
    from tt_sceneseg.host.preprocess import prepare_bgr, prepare_image
    from tt_sceneseg.reference.model import SceneSegReference
    from tt_sceneseg.reference.pipeline import ReferencePipeline
    from tt_sceneseg.ttaw.golden import TapRegistry
    from tt_sceneseg.ttaw.metrics import pcc

    onnx = assets.onnx_path()
    t0 = time.perf_counter()
    ref = SceneSegReference.from_onnx(onnx)
    versions = {"torch": torch.__version__, "numpy": np.__version__, "python": sys.version.split()[0]}
    common = {"onnx": config.ONNX_FILE, "onnx_sha256": config.ONNX_SHA256, "versions": versions,
              "written": dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds"),
              "writer": "code/scripts/ref_golden.py (fp32 CPU reference, tt_sceneseg.reference)"}
    print(f"reference loaded in {time.perf_counter() - t0:.1f} s; torch {torch.__version__}, {a.threads} threads",
          flush=True)
    rng = np.random.default_rng(0)
    sample_yx = np.stack([rng.integers(0, config.INPUT_HW[0], N_SAMPLES),
                          rng.integers(0, config.INPUT_HW[1], N_SAMPLES)])
    SMALL.mkdir(parents=True, exist_ok=True)
    LARGE.mkdir(parents=True, exist_ok=True)
    index = {"description": "Large goldens of the fp32 CPU reference of sceneseg-p150 (never shipped)", **common,
             "files": {}, "public": {}}
    if a.only:
        if (LARGE / "index.json").is_file():
            index = json.loads((LARGE / "index.json").read_text())
        comma10k_goldens(ref, index, common)
        (LARGE / "index.json").write_text(json.dumps(index, indent=1) + "\n")
        print(f"done in {time.perf_counter() - t0:.0f} s", flush=True)
        return 0
    logits_cache = {}

    # ---- the shipped samples: small goldens, smoke references, module taps of the smoke sample ----------------
    for stem in TUTORIAL:
        path = SAMPLES / f"{stem}.png"
        arrays = {"sample_yx": sample_yx.astype(np.int32)}
        meta = {**common, "sample": f"samples/{stem}.png", "sample_sha256": sha256_file(path), "variants": {}}
        for order in config.CHANNEL_ORDERS:
            t1 = time.perf_counter()
            prep = prepare_image(str(path), channel_order=order)
            want_taps = stem == SMOKE_SAMPLE and not a.skip_taps
            taps = TapRegistry(exclude=config.LARGE_TAPS) if stem == SMOKE_SAMPLE else None
            logits = ref.run_numpy(prep.network_input(), taps=taps)["logits"]
            res = postprocess(prep.src_hw, logits=logits)
            logits_cache[(stem, order)] = logits
            arrays[f"{order}.class_map"] = res.class_map
            arrays[f"{order}.mask"] = res.mask
            arrays[f"{order}.logits_samples"] = logits[0][:, sample_yx[0], sample_yx[1]].astype(np.float32)
            v = {"src_hw": list(prep.src_hw), "input_u8_sha256": sha256_array(prep.u8_bgr),
                 "network_input_sha256": sha256_array(prep.network_input()), "logits": stats(logits),
                 "class_fraction_320x640": class_fractions(res.class_map),
                 "foreground_fraction_src": res.foreground_fraction, "margins": margin_stats(logits),
                 "seconds": round(time.perf_counter() - t1, 1)}
            if taps is not None:
                v["taps"] = {name: stats(val) for name, val in taps.to_dict().items()}
                if want_taps:
                    out = LARGE / "taps" / f"sample_{stem}.{order}.npz"
                    out.parent.mkdir(parents=True, exist_ok=True)
                    np.savez(out, **{k: taps[k] for k in config.MODULE_TAPS})
                    index["files"][str(out.relative_to(LARGE))] = {"bytes": out.stat().st_size,
                                                                   "sha256": sha256_file(out),
                                                                   "taps": list(config.MODULE_TAPS)}
            meta["variants"][order] = v
            print(f"{stem} {order}: fg {res.foreground_fraction:.4f} {v['seconds']} s", flush=True)
        np.savez_compressed(SMALL / f"{stem}.npz", **arrays)
        (SMALL / f"{stem}.json").write_text(json.dumps(meta, indent=1) + "\n")

    # the container smoke's stored references (the /predict body of the CPU reference)
    for order in config.CHANNEL_ORDERS:
        pipe = ReferencePipeline(onnx, order, threads=a.threads, model=ref)
        body = pipe.reference_body(str(SAMPLES / f"{SMOKE_SAMPLE}.png"))
        (SAMPLES / f"{SMOKE_SAMPLE}.{order}.reference.json").write_text(json.dumps(body, indent=1) + "\n")
        print(f"reference body {order}: {body['class_counts']}", flush=True)

    # ---- the public frames: logits + class maps of both orders (large), module taps of the gate frames --------
    if not a.skip_public and (PUBLIC / "inputs").is_dir():
        pub = LARGE / "public"
        pub.mkdir(parents=True, exist_ok=True)
        tap_orders = dict(TAP_SAMPLES)
        for d in sorted(p for p in (PUBLIC / "inputs").iterdir() if (p / "source_bgr.png").is_file()):
            fid = d.name
            bgr = load_bgr_png(d / "source_bgr.png")
            arrays, rec = {}, {"orders": {}}
            for order in config.CHANNEL_ORDERS:
                t1 = time.perf_counter()
                prep = prepare_bgr(bgr, channel_order=order)
                stem = fid[4:] if fid.startswith("tut_") else None
                want_taps = (not a.skip_taps and fid in tap_orders and (tap_orders[fid] is None
                                                                        or order in tap_orders[fid]))
                taps = TapRegistry(include=config.MODULE_TAPS) if want_taps else None
                if stem is not None and (stem, order) in logits_cache and taps is None:
                    logits = logits_cache[(stem, order)]
                    same = sha256_array(prepare_image(str(SAMPLES / f"{stem}.png"), channel_order=order).u8_bgr)
                    if same != sha256_array(prep.u8_bgr):
                        raise SystemExit(f"{fid}: the converter's decode differs from the shipped sample's")
                else:
                    logits = ref.run_numpy(prep.network_input(), taps=taps)["logits"]
                res = postprocess(prep.src_hw, logits=logits)
                arrays[f"logits_{order}"] = logits.astype(np.float32)
                arrays[f"cls_{order}"] = res.class_map
                r = {"fg_fraction_src": res.foreground_fraction, "class_fraction_320x640":
                     class_fractions(res.class_map), "margins": margin_stats(logits)}
                gold = PUBLIC / "golden" / fid / "ort_fp32.npz"
                if gold.is_file():
                    with np.load(gold) as z:
                        r["vs_research_ort"] = {"logits_pcc": pcc(logits, z[f"logits_{order}"]),
                                                "logits_max_abs": float(np.abs(logits - z[f"logits_{order}"]).max()),
                                                "class_map_agreement":
                                                    float((res.class_map == z[f"cls_{order}"]).mean())}
                if taps is not None:
                    out = LARGE / "taps" / f"{fid}.{order}.npz"
                    out.parent.mkdir(parents=True, exist_ok=True)
                    np.savez(out, **{k: taps[k] for k in config.MODULE_TAPS})
                    index["files"][str(out.relative_to(LARGE))] = {"bytes": out.stat().st_size,
                                                                   "sha256": sha256_file(out),
                                                                   "taps": list(config.MODULE_TAPS)}
                r["seconds"] = round(time.perf_counter() - t1, 1)
                rec["orders"][order] = r
                print(f"{fid} {order}: fg {res.foreground_fraction:.4f} {r.get('vs_research_ort')} {r['seconds']} s",
                      flush=True)
            meta = json.loads((d / "meta.json").read_text()) if (d / "meta.json").is_file() else {}
            rec["dataset"] = meta.get("dataset")
            rec["may_ship_in_bundle"] = meta.get("may_ship_in_bundle")
            rec["source_hw"] = list(bgr.shape[:2])
            out = pub / f"{fid}.npz"
            np.savez_compressed(out, **arrays)
            index["files"][str(out.relative_to(LARGE))] = {"bytes": out.stat().st_size, "sha256": sha256_file(out)}
            index["public"][fid] = rec
    comma10k_goldens(ref, index, common)
    if index["files"]:
        worst = [(fid, o, r["vs_research_ort"]) for fid, rec in index["public"].items()
                 for o, r in rec["orders"].items() if "vs_research_ort" in r]
        if worst:
            index["vs_research_ort_min"] = {
                "logits_pcc": min(w[2]["logits_pcc"] for w in worst),
                "class_map_agreement": min(w[2]["class_map_agreement"] for w in worst)}
        (LARGE / "index.json").write_text(json.dumps(index, indent=1) + "\n")
    (SMALL / "README.md").write_text(SMALL_README)
    print(f"done in {time.perf_counter() - t0:.0f} s", flush=True)
    return 0


def comma10k_goldens(ref, index: dict, common: dict) -> None:
    """The PLAN.md 2.2 comma10k gate's FP32 side: the six comma10k images of the SPEC (MIT; 5 of them SceneSeg
    training images; shipped in samples/comma10k/) with the training ROI crop, both orders: class maps (research/sceneseg/goldens/comma10k/) and
    per-image IoU against the ground truth (index ``comma10k``; SPEC section 3: foreground 0.729 bgr / 0.765 rgb)."""
    from tt_sceneseg.host.postprocess import postprocess
    from tt_sceneseg.host.preprocess import prepare_bgr
    from tt_sceneseg.tests.sceneseg_eval import comma10k_gt, comma10k_ious, comma10k_samples, load_rgb, roi70

    samples = comma10k_samples()
    if not samples:
        print("comma10k images not found: skipped", flush=True)
        return
    out_dir = LARGE / "comma10k"
    out_dir.mkdir(parents=True, exist_ok=True)
    rec = {"protocol": "training ROI crop (top 70 %, last column dropped), class map resized to the crop with "
                       "nearest neighbour, per-image IoU, mean over the images", "images": {}, "mean": {}}
    for name, (img_path, mask_path) in samples.items():
        crop = roi70(load_rgb(img_path)[:, :, ::-1])
        gt = roi70(comma10k_gt(load_rgb(mask_path)))
        arrays, r = {"gt": gt}, {"crop_hw": list(crop.shape[:2]), "image_sha256": sha256_file(img_path),
                                 "mask_sha256": sha256_file(mask_path)}
        for order in config.CHANNEL_ORDERS:
            prep = prepare_bgr(crop, channel_order=order)
            logits = ref.run_numpy(prep.network_input())["logits"]
            res = postprocess(prep.src_hw, logits=logits)
            arrays[f"cls_{order}"] = res.class_map
            r[order] = comma10k_ious(res.class_map, gt)
        np.savez_compressed(out_dir / f"{name[:4]}.npz", **arrays)
        index["files"][f"comma10k/{name[:4]}.npz"] = {"bytes": (out_dir / f"{name[:4]}.npz").stat().st_size,
                                                      "sha256": sha256_file(out_dir / f"{name[:4]}.npz")}
        rec["images"][name] = r
        print(f"comma10k {name[:4]}: {r}", flush=True)
    for order in config.CHANNEL_ORDERS:
        rec["mean"][order] = {c: float(np.nanmean([r[order][c] for r in rec["images"].values()]))
                              for c in config.CLASS_NAMES}
    index["comma10k"] = rec
    print(f"comma10k mean: {rec['mean']}", flush=True)
    # the shipped copy (tests/goldens/comma10k/, since the release docs): the class maps + GT and this record with
    # the files' sha256, so the comma10k gate and test_reference_host run outside the workspace
    small = SMALL / "comma10k"
    small.mkdir(parents=True, exist_ok=True)
    files = {}
    for name in rec["images"]:
        src = out_dir / f"{name[:4]}.npz"
        (small / src.name).write_bytes(src.read_bytes())
        files[src.name] = index["files"][f"comma10k/{src.name}"]
    (small / "index.json").write_text(json.dumps({**rec, "files": files}, indent=1, sort_keys=True) + "\n")


SMALL_README = """# Small goldens of the fp32 CPU reference (shipped)

Written by `code/scripts/ref_golden.py` (research venv, CPU) with `tt_sceneseg.reference` on the 9 shipped samples,
both channel orders (`bgr` deployed, `rgb` training order), and on the six shipped comma10k images (`comma10k/`). The large ones (module taps of the gate samples, logits of
the public frames) stay in the workspace under `research/sceneseg/goldens/` and are never shipped.

| file | content |
|---|---|
| `<stem>.npz` | `sample_yx` int32 (2, 4096) fixed pixels; per order `<order>.class_map` uint8 (320, 640) (the node's argmax, lowest-index ties), `<order>.mask` uint8 at the source size (the published mono8 mask), `<order>.logits_samples` float32 (3, 4096) at `sample_yx` |
| `comma10k/<id>.npz` | the six shipped comma10k images (training ROI crop): `gt` uint8 at the crop's size (the comma10k mask mapped to background / foreground / road like `process_comma10k.py`), `cls_bgr` / `cls_rgb` uint8 (320, 640) fp32 class maps |
| `comma10k/index.json` | the protocol, per-image and mean ground-truth IoUs of the fp32 reference per order (the FP32 side of the comma10k gate), the images' and masks' sha256 and every `.npz`'s sha256 |
| `<stem>.json` | sha256 of the sample, the uint8 squash and the network input; logits statistics; class fractions; decision margins (fraction of pixels within 0.05 / 0.25 / 1.0 logit of a class flip); for `highway_normal_1` every tap's shape / mean / std / absmax / sha256 |

Tests: `tests/test_reference_host.py` (the fp32 forward on another torch build vs these),
`tests/test_host_postprocess.py` (the class decision of the stored logits samples). The TT gates compare with the
CPU reference, never with a TT run.
"""


if __name__ == "__main__":
    sys.exit(main())