File size: 11,851 Bytes
ddaad99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Write the CPU fp32 goldens of the TT port with the bundle's own reference (``tt_bevformer.reference``, port form:
host geometry, rebatched SCA, rotation by gather) and cross-check them against the research goldens.

Large goldens (internal validation data, never shipped) go to ``research/bevformer/goldens/`` (``--out``):

==============================================  ==================================================================
``pandaset_019/f30.npz``, ``f35.npz``, ``f40``  the shipped sample as a stateful sequence (reset at f30): outputs,
                                                ``bev_embed``, inputs, host geometry; f35 (history + ego motion)
                                                also every per-module tap and the network input ``image``
``sample0/f1.npz``, ``f2``, ``f3``              the bevdet_vendor nuScenes key-frame (PLAN.md 2.11 detection
                                                gates on frames 1-2; frame 3 = +2 m / +3 deg synthetic motion);
                                                f1 also the image, backbone.layer4, neck and transformer taps
``nuscenes_scene-0103/kf01.npz``                mini_val, teacher-forced with the public golden kf00 bev_embed;
                                                image + transformer taps (in-domain, real 4.3 m / -1.0 deg motion)
==============================================  ==================================================================

Taps above 20 M elements (backbone.layer1-3) are stored as float16 (their PCC against float32 changes by < 1e-7);
everything else float32; ``__meta__`` records the dtypes, the generator, the bundle commit, the weights digest and
the PCCs against the research goldens. Small goldens (PandaSet-derived, CC BY 4.0) go to the bundle:
``staging_samples_pandaset/tests/goldens/pandaset_019_f30-40.json`` (detections, counts, PCCs; git-ignored until the PandaSet sample ships),
``pandaset_019_f30-40_last_layer.npz`` (last-layer logits / boxes) and the smoke test's stored reference
``staging_samples_pandaset/samples/pandaset_019_f30.reference.json`` (``to_dict()`` of a fresh-stream request of frame 30).

    cd bundles/bevformer-p150 && OMP_NUM_THREADS=4 PYTHONPATH=$PWD/code \\
        /home/ubuntu/experiments/tt-models/tools/research-venv/bin/python code/scripts/make_goldens.py --threads 4
"""
from __future__ import annotations

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

import numpy as np

HERE = Path(__file__).resolve().parent
CODE = HERE.parent
sys.path.insert(0, str(CODE))

from tt_bevformer import __version__  # noqa: E402
from tt_bevformer.host import postprocess as hpost  # noqa: E402
from tt_bevformer.host.inputs import load_frame  # noqa: E402
from tt_bevformer.reference.pipeline import BEVFormerReference  # noqa: E402
from tt_bevformer.tests import paths  # noqa: E402
from tt_bevformer.ttaw.golden import save_goldens  # noqa: E402
from tt_bevformer.ttaw.metrics import pcc  # noqa: E402

FP16_ABOVE = 20_000_000
OUTS = ("bev_embed", "outputs_classes", "outputs_coords")
GEOMETRY = ("ref_cam", "sca_rows", "sca_counts", "sca_gather", "sca_scale", "shift", "rot_index")
ALL_TAPS = ["backbone.layer1", "backbone.layer2", "backbone.layer3", "backbone.layer4", "neck", "enc.*", "ref_cam",
            "encoder.*", "decoder.*", "head.*"]
TRANSFORMER_TAPS = ["backbone.layer4", "neck", "enc.*", "ref_cam", "encoder.layer0.*", "encoder.layer0",
                    "encoder.layer1", "encoder.layer2", "decoder.*", "head.*"]


def _git_head(path: Path) -> str:
    """``<short sha>`` of the bundle, with ``+dirty`` when the working tree differs from it."""
    try:
        head = subprocess.run(["git", "-C", str(path), "rev-parse", "--short", "HEAD"], capture_output=True,
                              text=True, check=True).stdout.strip()
        dirty = subprocess.run(["git", "-C", str(path), "status", "--porcelain", "--untracked-files=no"],
                               capture_output=True, text=True, check=True).stdout.strip()
        return head + ("+dirty" if dirty else "")
    except (OSError, subprocess.CalledProcessError):
        return "unknown"


def _select(taps, patterns):
    return {k: v for k, v in taps.items() if any(fnmatch.fnmatch(k, p) for p in patterns)}


def _pack(res, taps_patterns, with_image, meta):
    """Golden tensors of one frame + the dtype record."""
    t = {name: getattr(res, name) for name in OUTS}
    for k, v in res.inputs.items():
        if k == "image" and not with_image:
            continue
        if k == "prev_bev":
            meta["prev_bev_sha256"] = hashlib.sha256(np.ascontiguousarray(v).tobytes()).hexdigest()
            continue
        t[f"input.{k}"] = v
    for k in GEOMETRY:
        t[f"geometry.{k}"] = res.geometry[k]
    dtypes = {}
    for k, v in _select(res.taps, taps_patterns).items():
        arr = np.asarray(v)
        if arr.size > FP16_ABOVE:
            arr = arr.astype(np.float16)
        t[f"tap.{k}"] = arr
        dtypes[k] = str(arr.dtype)
    meta["tap_dtypes"] = dtypes
    return t


def _compare(res, golden_path: Path, report: dict, key: str) -> None:
    if golden_path is None or not Path(golden_path).is_file():
        return
    out = {}
    with np.load(golden_path) as g:
        for name in OUTS:
            for gk in (name, f"{key}_{name}"):
                if gk in g.files:
                    out[name] = pcc(getattr(res, name), g[gk])
        for k in g.files:
            if k.startswith("tap_") and k[4:] in res.taps:
                out["tap." + k[4:]] = pcc(res.taps[k[4:]].reshape(g[k].shape), g[k])
    report[key] = out
    print(f"  {key}: " + ", ".join(f"{k} {v:.10f}" for k, v in out.items()), flush=True)


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--out", type=Path, default=paths.golden_dir())
    ap.add_argument("--threads", type=int, default=4)
    ap.add_argument("--only", nargs="*", default=["pandaset", "sample0", "nuscenes"])
    a = ap.parse_args()
    import torch

    torch.set_num_threads(a.threads)
    wpath = paths.weights_file()
    if wpath is None:
        raise SystemExit("weights not found (BEVFORMER_WEIGHTS_DIR)")
    ref = BEVFormerReference(wpath)
    base_meta = {"generator": "bundles/bevformer-p150/code/scripts/make_goldens.py", "bundle_version": __version__,
                 "bundle_commit": _git_head(CODE.parent), "weights": str(wpath),
                 "weights_sha256": ref.weights.sha256, "reference_form": "port (BEVFormerNet.forward_port)",
                 "fp16_taps_above_elements": FP16_ABOVE}
    report: dict = {}
    t_all = time.time()

    if "pandaset" in a.only:
        pub = paths.public_data()
        ref.reset()
        small = {"frames": [], "doc": "PandaSet 019 f30-40 (shipped sample, CC BY 4.0 + PandaSet Dataset Terms): "
                                      "CPU fp32 reference (port form), stateful sequence reset at f30; Autoware "
                                      "post-processing (score > 0.2)"}
        last = {}
        for f in (30, 35, 40):
            t0 = time.time()
            fr = load_frame(paths.PANDASET_SAMPLES / f"pandaset_019_f{f}.json")
            res = ref.step(fr, port=True, taps=(f == 35), workers=4)
            meta = dict(base_meta, frame=f, sequence="pandaset_019_f30-40", reset=bool(fr.stream["reset"]))
            g = pub / "pandaset" / "goldens" / "ps019_f30-40" / f"f{f}.npz" if pub else None
            _compare(res, g, report, f"pandaset_019/f{f}")
            meta["pcc_vs_research"] = report.get(f"pandaset_019/f{f}", {})
            save_goldens(a.out / "pandaset_019" / f"f{f}.npz", _pack(res, ALL_TAPS if f == 35 else [], f == 35, meta),
                         meta, compress=True)
            dets = res.detections
            small["frames"].append({"frame": f, "use_prev_bev": float(res.inputs["use_prev_bev"][0]),
                                    "n_detections": len(dets), "pcc_vs_research": meta["pcc_vs_research"],
                                    "detections": [{k: (round(v, 5) if isinstance(v, float) else v)
                                                    for k, v in d.items()} for d in dets.to_records()]})
            last[f"f{f}_outputs_classes"] = res.outputs_classes[-1, 0]
            last[f"f{f}_outputs_coords"] = res.outputs_coords[-1, 0]
            if f == 30:
                body = hpost.to_output(dets, model="bevformer-p150").to_dict()
                (paths.PANDASET_SAMPLES / "pandaset_019_f30.reference.json").write_text(json.dumps(body, indent=1) + "\n")
            print(f"pandaset f{f}: {len(dets)} detections, {time.time() - t0:.0f} s", flush=True)
        paths.PANDASET_GOLDENS.mkdir(parents=True, exist_ok=True)
        (paths.PANDASET_GOLDENS / "pandaset_019_f30-40.json").write_text(json.dumps(small, indent=1) + "\n")
        np.savez_compressed(paths.PANDASET_GOLDENS / "pandaset_019_f30-40_last_layer.npz", **last)

    if "sample0" in a.only:
        g0 = paths.research_file("golden", "sample0_golden.npz")
        if g0 is not None:
            with np.load(g0) as g:
                gs = {k: g[k] for k in g.files}
            prev = gs["f1_prev_bev"]
            for f in (1, 2, 3):
                t0 = time.time()
                res = ref.run_frame(gs["image"], prev, gs[f"f{f}_use_prev_bev"], gs[f"f{f}_can_bus"], gs["lidar2img"],
                                    port=True, taps=(f == 1))
                meta = dict(base_meta, frame=f, sequence="sample0 (bevdet_vendor nuScenes key-frame; internal only)")
                _compare(res, g0, report, f"f{f}")
                report[f"sample0/f{f}"] = report.pop(f"f{f}")
                meta["pcc_vs_research"] = report[f"sample0/f{f}"]
                meta["n_detections"] = len(res.detections)
                save_goldens(a.out / "sample0" / f"f{f}.npz",
                             _pack(res, TRANSFORMER_TAPS if f == 1 else [], f == 1, meta), meta, compress=True)
                prev = res.bev_embed
                print(f"sample0 f{f}: {len(res.detections)} detections, {time.time() - t0:.0f} s", flush=True)

    if "nuscenes" in a.only and paths.public_data() is not None:
        P = paths.import_public_common()
        pub = paths.public_data()
        t0 = time.time()
        x = P.load_frame_inputs(str(pub / "nuscenes" / "inputs" / "scene-0103" / "kf01.npz"))
        with np.load(pub / "nuscenes" / "goldens" / "scene-0103" / "kf00.npz") as g:
            prev = g["bev_embed"]
        res = ref.run_frame(x["image"], prev, x["use_prev_bev"], x["can_bus"], x["lidar2img"], port=True, taps=True)
        meta = dict(base_meta, frame="kf01", sequence="nuScenes mini_val scene-0103 (CC BY-NC-SA 4.0; internal only)",
                    teacher_forced="prev_bev = public-data golden kf00 bev_embed")
        _compare(res, pub / "nuscenes" / "goldens" / "scene-0103" / "kf01.npz", report, "nuscenes_scene-0103/kf01")
        meta["pcc_vs_research"] = report["nuscenes_scene-0103/kf01"]
        save_goldens(a.out / "nuscenes_scene-0103" / "kf01.npz", _pack(res, TRANSFORMER_TAPS, True, meta), meta,
                     compress=True)
        print(f"nuscenes kf01: {len(res.detections)} detections, {time.time() - t0:.0f} s", flush=True)

    a.out.mkdir(parents=True, exist_ok=True)
    worst = min((v for r in report.values() for v in r.values()), default=float("nan"))
    (a.out / "report.json").write_text(json.dumps({"meta": base_meta, "pcc_vs_research": report,
                                                   "worst_pcc": worst, "seconds": time.time() - t_all}, indent=1))
    print(f"done in {time.time() - t_all:.0f} s; worst PCC vs research {worst:.10f}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())