#!/usr/bin/env python3 # SPDX-License-Identifier: Apache-2.0 """Stage breakdown of warm plans -- the numbers OPT_BASELINE.md / OPT_REPORT.md / the card quote. bin/devrun -t 900 -- python code/scripts/bench.py --iters 100 --json out.json bin/devrun -t 900 -- python code/scripts/bench.py --dispatch worker --num-cqs 2 --iters 100 --json out.json bin/devrun -t 900 -- python code/scripts/bench.py --input --input ... ``--input`` takes the planner tensors as an ``.npz`` with the 15 ``INPUT_SCHEMA`` names (the shipped samples) or with ``raw/`` keys (the research / public-data scene files); default: the shipped ``kashiwanoha_dense.npz``. Per input, ``ttaw.profiling.StageBench`` collects ``--iters`` warm iterations of each stage (p50 / p99 / mean / min): - ``load``: decoding the ``.npz`` + the ``INPUT_SCHEMA`` check (``model(inputs=)`` does it outside timing_ms); - ``host_pre``: the node's pre-processing (``host.prepare``: normalization, speed masks, encoder host features, decoder masks, the solver's initial state); - ``pack``: the 19 persistent trace inputs (``tt.inputs.plan_inputs``); - ``host_in``: their ttnn host tensors (fp32 / bf16 TILE, ``ttaw.tensors.to_host_tensor``); - ``h2d``: the upload into the persistent device inputs + device sync; - ``trace``: one replay of the ``plan`` trace + device sync (the device latency of one plan); - ``d2h``: the one packed readback (``final_x0`` + logits + the ego rows of the 11 iterates); - ``host_post``: the node's post-processing (``host.make_output``); - ``e2e``: ``model(inputs=)``, the in-process API call (schema check included); - ``e2e_path``: ``model(inputs=)`` (adds ``load``; schema-named ``.npz`` files only); - ``b2b``: back-to-back replays with no host work in between (device time per plan), ``--b2b-rounds`` rounds of ``--b2b-iters`` replays; ``plans_per_s`` = 1000 / median b2b. Also: ``model(...).timing_ms`` (preprocess / device / postprocess / total), the first call after ``from_pretrained`` and the load time (weights, build, warm-up + capture), AICLK / power / temperature sampled from sysfs during the timed loops (``ttaw.profiling.AiclkSampler``), the staged path checked bit for bit against ``model()``, the device configuration (dispatch, CQs, grid) and the numerics options in effect (``DIFFUSION_PLANNER_*`` knobs). Always quote the configuration line with the numbers. """ from __future__ import annotations import argparse import json import statistics import time from pathlib import Path from typing import Any, Dict, Optional import numpy as np from tt_diffusion_planner import DiffusionPlanner SAMPLE = Path(__file__).resolve().parents[1] / "tt_diffusion_planner" / "samples" / "kashiwanoha_dense.npz" def load_scene(path: str) -> Dict[str, Any]: """``{"name", "path", "arrays", "schema_npz"}``: the 15 raw tensors of a schema-named or ``raw/``-prefixed npz.""" from tt_diffusion_planner.reference import config as C p = Path(path) with np.load(p, allow_pickle=False) as z: files = set(z.files) if all(k in files for k in C.INPUT_NAMES): arrays, schema_npz = {k: np.array(z[k]) for k in C.INPUT_NAMES}, True elif all(f"raw/{k}" in files for k in C.INPUT_NAMES): arrays, schema_npz = {k: np.array(z[f"raw/{k}"]) for k in C.INPUT_NAMES}, False else: raise SystemExit(f"{p}: neither the INPUT_SCHEMA names nor raw/ keys") name = p.stem[len("golden_"):] if p.stem.startswith("golden_") else p.stem if p.parent.name not in ("samples", "ort"): name = f"{p.parent.name}/{name}" return {"name": name, "path": str(p), "arrays": arrays, "schema_npz": schema_npz} def counts(arrays: Dict[str, np.ndarray]) -> Dict[str, int]: """Valid entities of a scene (non-empty rows), for the report.""" def rows(a, axis): return int(np.any(np.abs(a) > 0, axis=axis).sum()) return {"neighbors": rows(arrays["neighbor_agents_past"][0], (1, 2)), "lanes": rows(arrays["lanes"][0], (1, 2)), "route_lanes": rows(arrays["route_lanes"][0], (1, 2)), "polygons": rows(arrays["polygons"][0], (1, 2)), "line_strings": rows(arrays["line_strings"][0], (1, 2))} def unpack(out: Dict[str, np.ndarray], tt: Any) -> Dict[str, Any]: """The packed readback -> the raw outputs of ``TtDiffusionPlanner.forward`` (same reshapes).""" from tt_diffusion_planner.reference import config as C from tt_diffusion_planner.tt import config as T final = tt.unpack(out) steps = out["ego_steps"].reshape(-1, T.STATE_COLS) return {"final_x0": final.reshape(C.MAX_NUM_AGENTS, C.OUTPUT_T + 1, C.POSE_DIM).astype(np.float32), "logit": out["logit"].reshape(-1)[:C.TURN_INDICATOR_OUTPUT_DIM].astype(np.float32), "denoising_steps": [s.reshape(1, C.OUTPUT_T + 1, C.POSE_DIM).astype(np.float32) for s in steps]} def bench_scene(model, scene: Dict[str, Any], a: argparse.Namespace) -> Dict[str, Any]: import ttnn from tt_diffusion_planner.host import pipeline as hp from tt_diffusion_planner.reference import config as C from tt_diffusion_planner.tt import inputs as I from tt_diffusion_planner.ttaw.io import load_named_arrays from tt_diffusion_planner.ttaw.profiling import AiclkSampler, StageBench, time_b2b from tt_diffusion_planner.ttaw.tensors import to_host_tensor runner, dev = model.runner, model.device arrays, params = scene["arrays"], model.validate_params({}) obs = model.normalization.observation bench = StageBench(f"diffusion-planner {scene['name']}") timing: Dict[str, list] = {} variant = model.tt.variant_for(hp.prepare(load_named_arrays(arrays, C.INPUT_SCHEMA), obs)) # COMPACT bucket sync = lambda: ttnn.synchronize_device(dev) # noqa: E731 for _ in range(a.warmup): ref = model(inputs=arrays) with AiclkSampler(chip=a.chip, interval_s=0.05) as clk: for _ in range(a.iters): # the in-process API call with bench.stage("e2e"): ref = model(inputs=arrays) for k, v in ref.timing_ms.items(): timing.setdefault(k, []).append(v) slots = {k: runner._slot(k, "input") for k in I.INPUT_SPECS} for _ in range(a.iters): # the same path, stage by stage if scene["schema_npz"]: with bench.stage("load"): raw = load_named_arrays(scene["path"], C.INPUT_SCHEMA) else: raw = load_named_arrays(arrays, C.INPUT_SCHEMA) with bench.stage("host_pre"): prep = hp.prepare(raw, obs) with bench.stage("pack"): packed = model.tt.filter_inputs(I.plan_inputs(prep)) # INPUT_TRIM (as model() does) with bench.stage("host_in"): host = {k: to_host_tensor(v, slots[k].dtype, slots[k].layout, shape=slots[k].shape) for k, v in packed.items()} with bench.stage("h2d"): runner.upload(host) sync() with bench.stage("trace"): runner.replay(variant) sync() with bench.stage("d2h"): out = runner.read(variant) with bench.stage("host_post"): res = model._postprocess(unpack(out, model.tt), prep, params) if scene["schema_npz"]: for _ in range(a.iters): with bench.stage("e2e_path"): model(inputs=scene["path"]) rounds = [time_b2b(lambda: runner.replay(variant), sync, n=a.b2b_iters, warmup=3) for _ in range(a.b2b_rounds)] for r in rounds: bench.add("b2b", r) if getattr(a, "dump", None): Path(a.dump).parent.mkdir(parents=True, exist_ok=True) flat: Dict[str, np.ndarray] = {} def walk(prefix, v): if isinstance(v, dict): for k, w in v.items(): walk(f"{prefix}{k}/", w) else: flat[prefix.rstrip("/") or "out"] = np.asarray(v) walk("", out) np.savez(f"{a.dump}.{scene['name'].replace('/', '_')}.npz", **flat) same = bool(np.array_equal(res.poses, ref.poses) and np.array_equal(res.predicted_agents, ref.predicted_agents) and res.turn_indicator["command"] == ref.turn_indicator["command"]) summary = bench.summary() b2b = statistics.median(rounds) return {"name": scene["name"], "path": scene["path"], "valid": counts(arrays), "variant": variant, "stages_ms": summary, "timing_ms": {k: {"p50": float(np.percentile(v, 50)), "p99": float(np.percentile(v, 99)), "min": float(min(v))} for k, v in timing.items()}, "b2b_rounds_ms": rounds, "plans_per_s_b2b": 1000.0 / b2b, "aiclk": clk.summary(), "staged_equals_model": same, "turn_command": int(ref.turn_indicator["command"]), "table": bench.table()} def main() -> None: ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) ap.add_argument("--iters", type=int, default=100) ap.add_argument("--warmup", type=int, default=5) ap.add_argument("--b2b-iters", type=int, default=50) ap.add_argument("--b2b-rounds", type=int, default=3) ap.add_argument("--input", action="append", default=None, help="scene .npz (repeatable)") ap.add_argument("--dispatch", default=None, choices=["eth", "worker"]) ap.add_argument("--num-cqs", type=int, default=None, choices=[1, 2]) ap.add_argument("--chip", type=int, default=0, help="sysfs chip index for the AICLK sampler") ap.add_argument("--tag", default="") ap.add_argument("--json") ap.add_argument("--dump", help="prefix: save each scene's raw packed readback as ..npz " "(bit-identity checks of structural rewrites)") a = ap.parse_args() from tt_diffusion_planner.reference.weights import find_weights_dir scenes = [load_scene(p) for p in (a.input or [str(SAMPLE)])] wd = find_weights_dir() # None: from_pretrained resolves the pinned HF snapshot t0 = time.perf_counter() with DiffusionPlanner.from_pretrained(dispatch=a.dispatch, num_command_queues=a.num_cqs, weights_dir=str(wd) if wd else None) as model: load_s = time.perf_counter() - t0 t1 = time.perf_counter() model(inputs=scenes[0]["arrays"]) # first call after from_pretrained (traces captured) first_ms = (time.perf_counter() - t1) * 1e3 info = model.tt.describe() res: Dict[str, Any] = { "tag": a.tag, "config": model.device_info, "iters": a.iters, "load_s": round(load_s, 2), "warmup_ms": {k: round(v, 1) for k, v in model.warmup_ms.items()}, "first_call_ms": round(first_ms, 2), "options": info["options"], "precision": info["precision"], "uploaded_mb": info["uploaded_mb"], "trace_buffers_mb": info["trace_buffers_mb"], "program_cache_entries": info["trace"].get("program_cache_entries"), "num_command_queues": info["trace"]["num_command_queues"], "scenes": {}} for scene in scenes: r = bench_scene(model, scene, a) res["scenes"][r["name"]] = r cfg = model.device_info print(f"\n## {r['name']} [{cfg.get('dispatch')} {res['num_command_queues']}CQ {cfg.get('grid')}] " f"valid {r['valid']} variant {r['variant']}") print(r["table"]) print("timing_ms p50:", {k: round(v["p50"], 3) for k, v in r["timing_ms"].items()}, f"| b2b rounds {[round(x, 3) for x in r['b2b_rounds_ms']]} ms -> {r['plans_per_s_b2b']:.2f} plans/s", f"| aiclk {r['aiclk'].get('aiclk_mhz')}", f"| check: staged == model() {r['staged_equals_model']}", flush=True) print(json.dumps({k: v for k, v in res.items() if k != "scenes"}, default=str)) if a.json: Path(a.json).parent.mkdir(parents=True, exist_ok=True) Path(a.json).write_text(json.dumps(res, indent=1, default=str) + "\n") if __name__ == "__main__": main()