File size: 10,422 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
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
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Goldens of frnet-p150 from the independent fp32 CPU reference (research venv: torch, onnx, onnxruntime; CPU only).

    cd bundles/frnet-p150 && OMP_NUM_THREADS=4 PYTHONPATH=code \\
        /home/ubuntu/experiments/tt-models/tools/research-venv/bin/python code/scripts/make_goldens.py --set all

Sets (output under ``research/frnet/goldens`` unless ``--out``; never shipped, except ``host``):

- ``taps``: the PandaSet 019 f40 sample (staged) in both variants through the bundle's host pre-processing and the
  torch fp32 reference with every module tap (``reference/weights.py`` ``MODULE_TAPS`` + the per-voxel max of every
  point branch, ``backbone.segmax0-4``) -> ``<variant>/pandaset_019_40.taps.npz`` (+ the four network inputs as
  ``input.*`` and ONNX Runtime's ``pred_probs`` as ``ort.pred_probs``) and a ``.json`` with each tap's shape,
  sha256 and its PCC / max |diff| against the same tensor of the deployed graph under ONNX Runtime (intermediates);
- ``frames``: every catalogued frame (``tests/_data.py`` ``FRAMES``: 16 OT128 + 4 QT128 public goldens, the VLS-128
  x > 0 halves) through the torch reference vs the stored ORT ``pred_probs``: PCC, max |diff|, argmax agreement (raw
  rows and all rows) and the ORT top-2 margin of every disagreeing point -> ``manifest.json``;
- ``host``: ``code/tt_frnet/tests/goldens/preprocess.json`` -- sha256 of the four network inputs that the RESEARCH
  pre-processing reference (``research/frnet/scripts/frnet_preproc_ref.py``) produces for the PandaSet 019 f40 sample (with
  its crop transform) and for the research synthetic scans, so the host tests can check the bundle's
  pre-processing where the research tree is absent.
"""
from __future__ import annotations

import argparse
import hashlib
import json
import sys
import time
from pathlib import Path

import numpy as np

HERE = Path(__file__).resolve()
CODE = HERE.parents[1]
sys.path.insert(0, str(CODE))

WS = Path("/home/ubuntu/experiments/tt-models")
RES = WS / "research" / "frnet"
STEM = "pandaset_019_40"


def sha(a: np.ndarray) -> str:
    return hashlib.sha256(np.ascontiguousarray(a).tobytes()).hexdigest()


def pcc64(a: np.ndarray, b: np.ndarray) -> float:
    from tt_frnet.ttaw.metrics import pcc

    return float(pcc(a, b))


def make_taps(out: Path, weights_dir: str, threads: int) -> None:
    import torch

    from tt_frnet.reference.ort import OrtFRNet
    from tt_frnet.reference.pipeline import FRNetReference
    from tt_frnet.reference.weights import MODULE_TAPS
    from tt_frnet.ttaw.golden import TapRegistry

    torch.set_num_threads(threads)
    patterns = list(MODULE_TAPS) + ["backbone.segmax*"]
    for variant in ("ot128", "qt128"):
        t0 = time.time()
        ref = FRNetReference(weights_dir, variant, backend="torch", threads=threads)
        taps = TapRegistry(include=patterns)
        staged = CODE.parent / "staging_samples_pandaset"              # git-ignored (tests/_data.py)
        T = json.loads((staged / "calib" / f"{STEM}.json").read_text())["T_base_link_from_sensor"]
        run = ref.run(staged / "samples" / f"{STEM}.npz", calibration={"T_base_link_from_sensor": T}, taps=taps)
        t_torch = time.time() - t0
        inp = run.prepared.inputs
        ort_taps = TapRegistry(include=patterns)
        ort = OrtFRNet(weights_dir, variant, threads=threads, intermediates=True, tap_patterns=patterns,
                       weights=ref.weights)
        probs_ort = ort(inp.points, inp.coors, inp.voxel_coors, inp.inverse_map, taps=ort_taps)
        del ort
        gold = np.load(RES / "public_data" / "golden" / f"{STEM}_{variant}.npz", allow_pickle=False)
        if not np.array_equal(probs_ort, gold["autoware_pred_probs"]):
            sys.exit(f"{variant}: ORT pred_probs differ from the research golden (onnxruntime version?)")
        meta = {"frame": STEM, "variant": variant, "reference": "tt_frnet.reference.model (torch fp32)",
                "onnx_sha256": ref.weights.onnx_sha256, "torch": torch.__version__, "threads": threads,
                "N": inp.num_points, "N_raw": inp.num_points_raw, "M": inp.num_voxels,
                "torch_s": round(t_torch, 1), "taps": {}}
        arrays = {f"input.{k}": v for k, v in inp.onnx_feeds().items()}
        arrays["ort.pred_probs"] = probs_ort
        for name in taps.names():
            v = np.ascontiguousarray(taps[name], np.float32)
            arrays[name] = v
            entry = {"shape": list(v.shape), "sha256": sha(v)}
            if name in ort_taps:
                o = np.asarray(ort_taps[name], np.float32)
                entry.update(pcc_vs_ort=pcc64(v, o), max_abs_vs_ort=float(np.abs(v - o).max()))
            meta["taps"][name] = entry
        worst = min((e["pcc_vs_ort"], k) for k, e in meta["taps"].items() if "pcc_vs_ort" in e)
        meta["worst_tap_vs_ort"] = {"tap": worst[1], "pcc": worst[0]}
        d = out / variant
        d.mkdir(parents=True, exist_ok=True)
        path = d / f"{STEM}.taps.npz"
        np.savez(path, **arrays)
        meta["file_sha256"] = hashlib.sha256(path.read_bytes()).hexdigest()
        meta["bytes"] = path.stat().st_size
        (d / f"{STEM}.taps.json").write_text(json.dumps(meta, indent=1) + "\n")
        print(f"taps {variant}: {len(taps.names())} taps, {path.stat().st_size / 1e9:.2f} GB, worst vs ORT "
              f"{worst[1]} {worst[0]:.12f}, {time.time() - t0:.0f} s", flush=True)
        del taps, ort_taps, arrays


def make_frames(out: Path, weights_dir: str, threads: int) -> None:
    import torch

    from tt_frnet.reference.model import FRNetTorch
    from tt_frnet.reference.weights import load_weights
    from tt_frnet.tests import _data

    torch.set_num_threads(threads)
    nets = {}
    manifest = {"what": "torch fp32 reference vs the ORT pred_probs stored in each research golden",
                "torch": torch.__version__, "threads": threads, "frames": {}}
    for name, (rel, prefix, variant) in _data.FRAMES.items():
        path = RES / rel
        if not path.is_file():
            print(f"{name}: missing {rel}", flush=True)
            continue
        if variant not in nets:
            nets[variant] = FRNetTorch(load_weights(weights_dir, variant))
        inputs, ort_probs, g = _data.load_frame(name)
        t0 = time.time()
        probs = nets[variant](inputs.points, inputs.coors, inputs.voxel_coors, inputs.inverse_map)
        dt = time.time() - t0
        n_raw = inputs.num_points_raw
        a, b = probs.argmax(1), ort_probs.argmax(1)
        diff = np.flatnonzero(a != b)
        top2 = np.sort(ort_probs[diff], axis=1)[:, -2:] if len(diff) else np.zeros((0, 2), np.float32)
        counts = np.bincount(inputs.inverse_map)
        manifest["frames"][name] = {
            "golden": f"research/frnet/{rel}", "variant": variant, "N": inputs.num_points, "N_raw": n_raw,
            "M": inputs.num_voxels, "max_points_per_cell": int(counts.max()),
            "pcc": pcc64(probs, ort_probs), "max_abs": float(np.abs(probs - ort_probs).max()),
            "argmax_agreement": float((a == b).mean()), "argmax_agreement_raw": float((a[:n_raw] == b[:n_raw]).mean()),
            "disagreements": int(len(diff)),
            "disagreement_ort_margin_max": float((top2[:, 1] - top2[:, 0]).max()) if len(diff) else 0.0,
            "torch_s": round(dt, 1)}
        print(f"{name}: N {inputs.num_points} M {inputs.num_voxels} pcc {manifest['frames'][name]['pcc']:.12f} "
              f"max|d| {manifest['frames'][name]['max_abs']:.2e} argmax {manifest['frames'][name]['argmax_agreement']:.6f}"
              f" ({len(diff)} flips) {dt:.1f} s", flush=True)
    out.mkdir(parents=True, exist_ok=True)
    (out / "manifest.json").write_text(json.dumps(manifest, indent=1) + "\n")


def make_host(weights_dir: str) -> None:
    from tt_frnet.reference.config import FRNetConfig

    sys.path.insert(0, str(RES / "scripts"))
    import frnet_preproc_ref as R  # noqa: E402  (workspace research code, numpy only)

    out = {"what": "sha256 of the four network inputs produced by research/frnet/scripts/frnet_preproc_ref.py "
                   "(variant 'autoware'); the bundle's host pre-processing must reproduce them",
           "cases": {}}
    pub = RES / "public_data"
    for variant in ("ot128", "qt128"):
        cfg = FRNetConfig.from_model_dir(weights_dir, variant)
        z = np.load(pub / "inputs" / f"{STEM}.npz", allow_pickle=False)
        T = z["T_base_link_sensor"]
        crop12 = np.concatenate([T[:3, :3].reshape(-1), T[:3, 3]]).astype(np.float32)
        cases = {f"{STEM}.{variant}": (z["xyzi"], crop12, cfg.crop_bounds)}
        pts, _ = R.synthetic_scan(R.CFGS[variant], n_cols=600 if variant == "qt128" else 1200, seed=0)
        cases[f"synthetic_seed0.{variant}"] = (pts, None, None)
        for name, (xyzi, crop, bounds) in cases.items():
            pre = R.autoware_preprocess(xyzi, R.CFGS[variant], sensor_to_ref_12=crop, crop_bounds=bounds,
                                        variant="autoware")
            out["cases"][name] = {"N0": int(len(xyzi)), "N_raw": int(pre["num_points_raw"]),
                                  "N": int(pre["num_points"]), "M": int(pre["num_unique"]),
                                  "input_sha256": sha(np.ascontiguousarray(xyzi, np.float32)),
                                  **{k: sha(pre[k]) for k in ("points", "coors", "voxel_coors", "inverse_map")}}
            print(f"host {name}: N {pre['num_points']} M {pre['num_unique']}", flush=True)
    path = CODE / "tt_frnet" / "tests" / "goldens" / "preprocess.json"
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(out, indent=1) + "\n")


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    ap.add_argument("--set", choices=("taps", "frames", "host", "all"), default="all")
    ap.add_argument("--out", type=Path, default=RES / "goldens")
    ap.add_argument("--weights-dir", default=str(WS / "assets" / "frnet" / "hf_lidar_frnet"))
    ap.add_argument("--threads", type=int, default=4)
    a = ap.parse_args()
    if a.set in ("host", "all"):
        make_host(a.weights_dir)
    if a.set in ("frames", "all"):
        make_frames(a.out, a.weights_dir, a.threads)
    if a.set in ("taps", "all"):
        make_taps(a.out, a.weights_dir, a.threads)
    return 0


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