changh95's picture
Add files using upload-large-folder tool
0c2d7e9 verified
Raw History Blame Contribute Delete
41 kB
# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
# SPDX-License-Identifier: Apache-2.0
"""End-to-end traced latency of ``Gr00tTT`` on one golden observation (WP-06; IMPLEMENTATION_PLAN.md §4, §6, §7.1).
Per call (plan §3.1): host ``encode`` -> upload of the per-call inputs -> trace replays (``per_stage``: vision, llm,
adapter, denoise; ``two``: A, B) -> D2H of ``action_pred`` -> host ``decode``. After ``--warmup`` (10) untimed calls,
``--reps`` (50) timed calls give the **median / p90** of the end-to-end time and of every component, plus a second
pass that synchronises after every trace for the **per-trace split** (the split pass serialises the replays, so its
sum is >= the pipelined device time of the main pass; both are recorded). Host encode / decode are timed on the
same observation.
Main pass = the production path ``tracer.call(mi, noise)`` (``TraceExecutor.replay``: static-shape asserts, host
inputs, writes -- on CQ 1 overlapping the traces when the device has two command queues -- and every trace, blocking
on the last one). Its ``upload`` row is the executor's ``last_replay_upload_s`` (call start to the first
``execute_trace``: host preparation + the writes that gate the first trace), ``host_prepare`` the host-input part of
it, ``traces`` the rest of the call. Split pass = ``tracer.upload`` (every input written on the trace CQ, serialised)
+ one synchronised replay per trace; its per-tensor accounting (``executor.upload_stats``: bytes, host tilize ms,
copy ms) is reported as ``upload/<input>`` rows and ``upload/total`` (docs/plan/n17-perf.md).
``--num-cqs`` (default 2) opens the device with that many command queues; ``--cq1-uploads auto|on|off`` and
``--embed-gather device|host`` are forwarded to ``Gr00tTT``; ``--profile-encode`` adds the per-step host-encode
profile (``meta.encode_profile``; also in ``--dry-run``). ``--dit-backend ttnn|megakernel`` (WP-K5) selects the
denoise backend (``TTPolicy.dit_backend``; default = the policy default, the **megakernel** since 2026-09-18; the
device is opened through ``tt.model.open_model_device`` so the megakernel gets its ``worker_l1_size``) and
``--mk-arena-dtype auto|bf16|bfp8_b`` the megakernel's weight arena; ``meta.dit_backend`` / ``meta.mk_arena_dtype`` /
``meta.megakernel`` / ``meta.weights`` (tensors and bytes ``TTWeights`` uploaded, what the megakernel skipped) record
what ran. ``--load-ttnn-dit-weights`` keeps the Stage-1 DiT upload under the megakernel (startup A/B).
The model comes from ``tt.model.Gr00tTT`` (WP-14). Until that module exists the import is guarded: ``--dry-run``
validates ``--version`` / ``--policy`` / ``--trace-layout`` / ``--layout`` / ``--embodiment`` against ``common.configs``
and ``tt.policy.TTPolicy``, resolves the golden sample, times the **host** encode / decode when the golden observation
and the HF snapshot are present (CPU only, no ttnn), and writes the JSON with ``dry_run: true``; without ``--dry-run``
a missing ``tt.model`` raises ``ImportError`` (no silent fallback).
Contract expected from ``Gr00tTT`` (plan §2.2 rows ``tt/model.py`` / ``tt/tracer.py``; checked at run time by
:func:`resolve_stage_replayer`, which raises ``AttributeError`` naming every missing member):
* ``Gr00tTT.from_pretrained(version, embodiment=None, layout_name=None, policy=TTPolicy(), device=None,
cache_root=None)``,
* ``model.tracer`` (a ``TraceExecutor``) with ``stage_names`` (execution order; ``("vision", "llm", "adapter",
"denoise")`` or ``("A", "B")``), ``upload(mi, noise)`` (refresh the persistent per-call input buffers),
``replay(stage)`` (enqueue one trace, non-blocking), ``synchronize()`` and ``read_action_pred() -> torch.Tensor
[1, 1, 64, A_pad]`` (D2H).
Output ``benchmarks/results/bench_e2e_<version>_<ts>.json`` (schema ``gr00t-tt-perf/1`` of ``bench_stage_ops``):
``stages`` = ``e2e`` (median ms, host included), ``device`` (upload -> D2H), ``trace/<stage>`` (split pass),
``upload``, ``d2h``, ``host_encode``, ``host_decode`` -- all ``metric: ms`` medians; p90 / min live next to them.
Run::
DEVICE_LOCK_TIMEOUT=14400 /home/deepgadget/experiments/gr00t/bin/with-device.sh \\
python -m models.experimental.gr00t.benchmarks.bench_e2e --version n16 --policy mixed_dit --trace-layout per_stage
python -m models.experimental.gr00t.benchmarks.bench_e2e --version n15 --dry-run
"""
from __future__ import annotations
import argparse
import statistics
import sys
import time
from pathlib import Path
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple
from models.experimental.gr00t.benchmarks import bench_stage_ops as SO
from models.experimental.gr00t.benchmarks import mb1_common as MB
from models.experimental.gr00t.tt import device as D
from models.experimental.gr00t.tt.policy import DEFAULT_DIT_BACKEND, DIT_BACKENDS, MK_ARENA_DTYPES # torch-free
BENCH_NAME = "bench_e2e"
VERSIONS = SO.VERSIONS
DEFAULT_WARMUP = 10
DEFAULT_REPS = 50
DEFAULT_HOST_REPS = 20
TRACE_LAYOUTS: Tuple[str, ...] = ("per_stage", "two")
POLICIES: Tuple[str, ...] = ("mixed_dit", "bf16", "mixed")
# plan §4: trace names per layout; the denoise trace is the last one of either layout
STAGE_NAMES: Dict[str, Tuple[str, ...]] = {"per_stage": ("vision", "llm", "adapter", "denoise"), "two": ("A", "B")}
REPLAYER_MEMBERS: Tuple[str, ...] = ("stage_names", "upload", "replay", "synchronize", "read_action_pred")
#: the production-path members the main pass needs on top of the split-pass contract (``tt.model.TraceReplayer``)
PRODUCTION_MEMBERS: Tuple[str, ...] = ("call", "upload_stats")
EMBED_GATHERS: Tuple[str, ...] = ("device", "host")
CQ1_MODES: Tuple[str, ...] = ("auto", "on", "off")
# --------------------------------------------------------------------------------------------------------------------
# Config / policy / golden resolution (CPU; shared with bench_denoise)
# --------------------------------------------------------------------------------------------------------------------
def resolve_setup(
version: str,
*,
policy: str = "mixed_dit",
trace_layout: str = "per_stage",
layout_name: Optional[str] = None,
embodiment: Optional[str] = None,
dit_backend: str = DEFAULT_DIT_BACKEND,
mk_arena_dtype: str = "auto",
) -> Dict[str, Any]:
"""Validate the CLI choices against ``common.configs`` / ``tt.policy`` and return the resolved facts
(``cfg``, ``layout``, ``policy`` object, ``embodiment_tag`` / ``embodiment_id``, expected trace names)."""
from models.experimental.gr00t.common.configs import get_config
from models.experimental.gr00t.tt.policy import TTPolicy
if version not in VERSIONS:
raise ValueError(f"version must be one of {VERSIONS}, got {version!r}")
if trace_layout not in TRACE_LAYOUTS:
raise ValueError(f"trace_layout must be one of {TRACE_LAYOUTS}, got {trace_layout!r}")
cfg = get_config(version)
layout = cfg.canonical_layout if layout_name is None else cfg.layout(layout_name)
tag = embodiment if embodiment is not None else layout.embodiment_tag
emb_id = cfg.embodiment_id(tag) # raises on an unknown tag
pol = TTPolicy(
dtype_policy=policy, trace_layout=trace_layout, dit_backend=dit_backend, mk_arena_dtype=mk_arena_dtype
) # raises on unknown values
return {
"cfg": cfg,
"layout": layout,
"policy": pol,
"embodiment_tag": tag,
"embodiment_id": emb_id,
"stage_names": STAGE_NAMES[trace_layout],
"meta": {
"version": version,
"layout_name": layout.name,
"embodiment_tag": tag,
"embodiment_id": emb_id,
"s_pad_llm": layout.s_pad_llm,
"s_pad_vl": layout.s_pad_vl,
"n_images": layout.n_images,
"dit_m_pad": layout.dit_m_pad,
"noise_shape": list(layout.noise_shape),
"policy": pol.to_dict(),
"dit_backend": pol.dit_backend,
"mk_arena_dtype": pol.mk_arena_dtype_for(version) if pol.dit_backend == "megakernel" else None,
"expected_stage_names": list(STAGE_NAMES[trace_layout]),
},
}
def golden_available(version: str, sample: str = "canonical") -> Tuple[bool, str]:
"""``(ok, reason)`` -- the golden sample with its ``obs/`` and the pinned HF snapshot (tokenizer) are present."""
from models.experimental.gr00t.common.configs import snapshot_dir
from models.experimental.gr00t.common.golden import GoldenSet, golden_root
if not golden_root().is_dir():
return False, f"golden root {golden_root()} not present"
try:
gs = GoldenSet(version, sample)
except (KeyError, FileNotFoundError) as e:
return False, f"golden sample {sample!r} not present ({e})"
from models.experimental.gr00t.reference.model import golden_obs_dir
try:
golden_obs_dir(gs)
except FileNotFoundError as e:
return False, str(e)
try:
snapshot_dir(version)
except FileNotFoundError as e:
return False, f"HF snapshot missing ({e})"
return True, ""
def load_golden_inputs(
version: str, sample: str, embodiment: Optional[str], layout_name: Optional[str]
) -> Dict[str, Any]:
"""``{gs, obs, mi}`` of a golden sample through ``reference.model.load_golden_sample`` + ``preprocessing.encode``
(host only)."""
from models.experimental.gr00t.common.preprocessing import encode
from models.experimental.gr00t.reference.model import load_golden_sample
gs, obs = load_golden_sample(version, sample)
mi = encode(version, obs, embodiment=embodiment, layout_name=layout_name)
return {"gs": gs, "obs": obs, "mi": mi}
def make_noise(layout: Any, seed: int) -> Any:
"""Deterministic ``initial_noise`` of the layout's ``noise_shape`` (``[1, H, A_max]``, fp32)."""
import torch
g = torch.Generator().manual_seed(int(seed))
return torch.randn(tuple(layout.noise_shape), generator=g)
# --------------------------------------------------------------------------------------------------------------------
# Model loading and the stage-replayer contract (device)
# --------------------------------------------------------------------------------------------------------------------
def import_gr00t_tt() -> Any:
"""``tt.model.Gr00tTT`` or ``None`` when WP-14 has not landed (callers decide: dry-run vs ImportError)."""
try:
from models.experimental.gr00t.tt import model as M # noqa: WPS433 - optional upstream module
except ImportError:
return None
return getattr(M, "Gr00tTT", None)
def load_model(
setup: Mapping[str, Any],
device: Any,
cache_root: Optional[Path] = None,
*,
embed_gather: str = "device",
cq1_uploads: Optional[bool] = None,
load_ttnn_dit_weights: Optional[bool] = None,
) -> Any:
Gr00tTT = import_gr00t_tt()
if Gr00tTT is None:
raise ImportError(
"tt.model.Gr00tTT is not importable (WP-14 pending); use --dry-run, or land tt/model.py with "
"Gr00tTT.from_pretrained(version, embodiment, layout_name, policy, device, cache_root)"
)
if embed_gather not in EMBED_GATHERS:
raise ValueError(f"embed_gather must be one of {EMBED_GATHERS}, got {embed_gather!r}")
kw: Dict[str, Any] = dict(
embodiment=setup["embodiment_tag"],
layout_name=setup["layout"].name,
policy=setup["policy"],
device=device,
embed_gather=embed_gather,
cq1_uploads=cq1_uploads,
)
if load_ttnn_dit_weights is not None:
kw["load_ttnn_dit_weights"] = bool(load_ttnn_dit_weights)
if cache_root is not None:
kw["cache_root"] = cache_root
return Gr00tTT.from_pretrained(setup["cfg"].version, **kw)
def resolve_cq1(mode: str) -> Optional[bool]:
"""``"auto"`` -> ``None`` (``Gr00tTT`` enables the CQ-1 writes iff the device has two command queues),
``"on"`` / ``"off"`` -> ``True`` / ``False``."""
if mode not in CQ1_MODES:
raise ValueError(f"cq1_uploads must be one of {CQ1_MODES}, got {mode!r}")
return None if mode == "auto" else mode == "on"
def resolve_stage_replayer(model: Any, expected_stages: Sequence[str]) -> Any:
"""``model.tracer`` satisfying the module-docstring contract; raises ``AttributeError`` listing missing members
and ``ValueError`` when its ``stage_names`` are not the plan §4 names of the layout."""
tracer = getattr(model, "tracer", None)
if tracer is None:
raise AttributeError("Gr00tTT has no `tracer` (plan §2.2 TraceExecutor); the benchmarks need it")
missing = [m for m in REPLAYER_MEMBERS if not hasattr(tracer, m)]
if missing:
raise AttributeError(f"Gr00tTT.tracer lacks {missing}; required contract: {REPLAYER_MEMBERS}")
names = tuple(str(s) for s in tracer.stage_names)
if names != tuple(expected_stages):
raise ValueError(f"tracer.stage_names {names} != expected {tuple(expected_stages)} for this trace layout")
return tracer
def _worker_l1_size(device: Any) -> Optional[int]:
"""``tt.model.device_worker_l1_size`` (the allocatable L1 the device was opened with; recorded in ``meta``)."""
try:
from models.experimental.gr00t.tt.model import device_worker_l1_size
except ImportError:
return None
return int(device_worker_l1_size(device))
def denoise_stage_name(stage_names: Sequence[str]) -> str:
"""The trace that holds the 4-step denoise: the last one of either layout (``denoise`` / ``B``)."""
if not stage_names:
raise ValueError("empty stage list")
return str(stage_names[-1])
# --------------------------------------------------------------------------------------------------------------------
# Timing helpers
# --------------------------------------------------------------------------------------------------------------------
def _stats_ms(samples: Sequence[float]) -> Dict[str, Any]:
if not samples:
raise ValueError("no samples")
xs = sorted(float(x) for x in samples)
n = len(xs)
p90 = xs[min(n - 1, int(round(0.9 * (n - 1))))]
return {
"metric": "ms",
"value": round(statistics.median(xs), 4),
"p90": round(p90, 4),
"min": round(xs[0], 4),
"max": round(xs[-1], 4),
"mean": round(statistics.fmean(xs), 4),
"n": n,
}
def time_host(fn: Callable[[], Any], reps: int, warmup: int = 1) -> Dict[str, Any]:
"""Wall-clock ms of ``reps`` calls of ``fn`` after ``warmup`` untimed calls."""
if reps <= 0:
raise ValueError("reps must be positive")
for _ in range(warmup):
fn()
out: List[float] = []
for _ in range(reps):
t0 = time.perf_counter()
fn()
out.append((time.perf_counter() - t0) * 1e3)
return _stats_ms(out)
def host_timings(version: str, inputs: Mapping[str, Any], layout: Any, host_reps: int) -> Dict[str, Dict[str, Any]]:
"""Median / p90 ms of ``common.preprocessing.encode`` and ``common.normalization.decode`` (plan §3.1 host per
call; f2 §1 measured 1.3 / 1.5 / 7.7 ms encode)."""
import torch
from models.experimental.gr00t.common.normalization import decode
from models.experimental.gr00t.common.preprocessing import encode
obs, mi = inputs["obs"], inputs["mi"]
pred = torch.zeros(tuple(layout.noise_shape), dtype=torch.float32)
return {
"host_encode": time_host(
lambda: encode(version, obs, embodiment=mi.embodiment_tag, layout_name=layout.name), host_reps
),
"host_decode": time_host(lambda: decode(version, pred, obs, embodiment=mi.embodiment_tag), host_reps),
}
def encode_profile(version: str, inputs: Mapping[str, Any], layout: Any, reps: int = 30) -> Dict[str, Dict[str, Any]]:
"""Per-step host-encode profile of one observation (median ms of ``reps`` calls after a warm-up): the version's
image encoder and its parts, the prompt (cached and uncached tokenization), the state, the static layout, the
RoPE / vision tables (cached and uncached) and the whole ``encode`` (docs/plan/n17-perf.md). CPU only."""
from models.experimental.gr00t.common import preprocessing as P
from models.experimental.gr00t.common import prompts as PR
from models.experimental.gr00t.common.configs import get_config
from models.experimental.gr00t.common.normalization import encode_state, get_embodiment_spec
obs, mi = inputs["obs"], inputs["mi"]
cfg = get_config(version)
spec = get_embodiment_spec(version, mi.embodiment_tag)
frames = [obs.images[cam] for cam in spec.video_keys]
encoders = {"n15": P.n15_encode_images, "n16": P.n16_encode_images, "n17": P.n17_encode_images}
enc_images = encoders[version]
vis = enc_images(frames, spec)
tp = PR.encode_prompt(version, obs.instruction, vis.tokens_per_image)
out: Dict[str, Dict[str, Any]] = {}
out["images"] = time_host(lambda: enc_images(frames, spec), reps)
if version == "n17":
out["images/albumentations"] = time_host(
lambda: [
P.albumentations_eval_transform(
fr, spec.shortest_image_edge, spec.crop_fraction, spec.letter_box_transform
)
for f in frames
for fr in f
],
reps,
)
out["images/pixel_patches"] = time_host(lambda: P.qwen2vl_pixel_patches(vis.images_uint8), reps)
out["images/transformers_processor"] = time_host(
lambda: P.get_qwen2vl_image_processor()(images=[im for im in vis.images_uint8], return_tensors="pt"), reps
)
out["prompt/cached"] = time_host(lambda: PR.encode_prompt(version, obs.instruction, vis.tokens_per_image), reps)
out["prompt/uncached"] = time_host(lambda: PR.tokenize_prompt(version, tp.text, vis.tokens_per_image), reps)
out["state"] = time_host(lambda: encode_state(spec, obs.state), reps)
lay = PR.select_layout(cfg, vis.n_images, vis.grid_thw, vis.tokens_per_image[0], tp.n_text, layout.name)
out["slot_layout"] = time_host(lambda: PR.build_slot_layout(cfg, tp, lay), reps)
slots = PR.build_slot_layout(cfg, tp, lay)
inv_dtype = PR.LLM_ROPE_INV_FREQ_DTYPE[version]
if version == "n17":
grid = vis.image_grid_thw
ids = slots.input_ids_padded[0]
out["rope/cached"] = time_host(
lambda: PR.mrope_tables_cached(
tuple(ids.tolist()),
cfg.prompt.image_token_id,
PR.grid_key(grid),
cfg.vision.merge_size,
cfg.llm.head_dim,
cfg.llm.rope_theta,
tuple(cfg.llm.mrope_section),
inv_dtype,
),
reps,
)
def mrope_uncached() -> Any:
pos3 = PR.mrope_position_ids(ids, cfg.prompt.image_token_id, grid, cfg.vision.merge_size)
return PR.mrope_cos_sin(pos3, cfg.llm.head_dim, cfg.llm.rope_theta, cfg.llm.mrope_section, inv_dtype)
out["rope/uncached"] = time_host(mrope_uncached, reps)
out["vision_tables/cached"] = time_host(
lambda: PR.vision_tables_cached(
PR.grid_key(grid),
cfg.vision.pos_grid,
cfg.vision.merge_size,
cfg.vision.head_dim,
cfg.vision.rope_theta,
),
reps,
)
out["vision_tables/uncached"] = time_host(
lambda: (
PR.vision_bilinear_indices_and_weights(grid, cfg.vision.pos_grid, cfg.vision.merge_size),
PR.vision_rope_cos_sin(grid, cfg.vision.head_dim, cfg.vision.merge_size, cfg.vision.rope_theta),
PR.vision_cu_seqlens(grid),
),
reps,
)
else:
out["rope"] = time_host(
lambda: PR.rope_cos_sin_1d(slots.position_ids[0], cfg.llm.head_dim, cfg.llm.rope_theta, inv_dtype), reps
)
out["encode"] = time_host(
lambda: P.encode(version, obs, embodiment=mi.embodiment_tag, layout_name=layout.name), reps
)
return out
# --------------------------------------------------------------------------------------------------------------------
# Device run
# --------------------------------------------------------------------------------------------------------------------
def run(
device: Any,
*,
version: str,
policy: str = "mixed_dit",
trace_layout: str = "per_stage",
sample: str = "canonical",
embodiment: Optional[str] = None,
layout_name: Optional[str] = None,
warmup: int = DEFAULT_WARMUP,
reps: int = DEFAULT_REPS,
host_reps: int = DEFAULT_HOST_REPS,
noise_seed: int = 0,
cache_root: Optional[Path] = None,
verbose: bool = True,
embed_gather: str = "device",
cq1_uploads: Optional[bool] = None,
profile_encode: bool = False,
dit_backend: str = DEFAULT_DIT_BACKEND,
mk_arena_dtype: str = "auto",
load_ttnn_dit_weights: Optional[bool] = None,
) -> Dict[str, Any]:
"""Measure the traced end-to-end latency and its split on an open device; returns the perf payload (not written).
Main pass = production path (``tracer.call``), split pass = serialised writes + per-trace sync (module docstring).
"""
import torch
from models.experimental.gr00t.common.normalization import decode
from models.experimental.gr00t.common.preprocessing import encode
if warmup < 0 or reps <= 0:
raise ValueError("warmup must be >= 0 and reps > 0")
setup = resolve_setup(
version,
policy=policy,
trace_layout=trace_layout,
layout_name=layout_name,
embodiment=embodiment,
dit_backend=dit_backend,
mk_arena_dtype=mk_arena_dtype,
)
ok, reason = golden_available(version, sample)
if not ok:
raise FileNotFoundError(f"{version}/{sample}: {reason}")
inputs = load_golden_inputs(version, sample, setup["embodiment_tag"], setup["layout"].name)
obs, mi = inputs["obs"], inputs["mi"]
noise = make_noise(setup["layout"], noise_seed)
t_load = time.perf_counter()
model = load_model(
setup,
device,
cache_root,
embed_gather=embed_gather,
cq1_uploads=cq1_uploads,
load_ttnn_dit_weights=load_ttnn_dit_weights,
)
load_s = time.perf_counter() - t_load
tracer = resolve_stage_replayer(model, setup["stage_names"])
missing = [m for m in PRODUCTION_MEMBERS if not hasattr(tracer, m)]
if missing:
raise AttributeError(f"Gr00tTT.tracer lacks the production-path members {missing} (tt.model.TraceReplayer)")
stages = list(tracer.stage_names)
executor = model.executor
per_tensor: List[Dict[str, Dict[str, float]]] = []
def one_call(split: bool) -> Dict[str, float]:
t: Dict[str, float] = {}
t0 = time.perf_counter()
mi_call = encode(version, obs, embodiment=setup["embodiment_tag"], layout_name=setup["layout"].name)
t1 = time.perf_counter()
if split:
tracer.upload(mi_call, noise)
tracer.synchronize()
t2 = time.perf_counter()
per_tensor.append(tracer.upload_stats)
for st in stages:
ts = time.perf_counter()
tracer.replay(st)
tracer.synchronize()
t[f"trace/{st}"] = (time.perf_counter() - ts) * 1e3
tracer.synchronize()
t3 = time.perf_counter()
t["host_prepare"] = model.timing["last_host_inputs_s"] * 1e3
else:
tracer.call(mi_call, noise) # blocks on the last trace
t3 = time.perf_counter()
t2 = t1 + executor.timing["last_replay_upload_s"]
t["host_prepare"] = executor.timing["last_replay_prepare_s"] * 1e3
pred = tracer.read_action_pred()
t4 = time.perf_counter()
pred_norm = pred.float().reshape(pred.shape[-2], pred.shape[-1])[
: setup["layout"].noise_shape[1], : setup["layout"].noise_shape[2]
]
decode(version, pred_norm.unsqueeze(0), obs, embodiment=setup["embodiment_tag"])
t5 = time.perf_counter()
t.update(
host_encode=(t1 - t0) * 1e3,
upload=(t2 - t1) * 1e3,
traces=(t3 - t2) * 1e3,
d2h=(t4 - t3) * 1e3,
host_decode=(t5 - t4) * 1e3,
device=(t4 - t1) * 1e3,
e2e=(t5 - t0) * 1e3,
)
return t
for _ in range(warmup):
one_call(split=False)
main_pass = [one_call(split=False) for _ in range(reps)]
split_pass = [one_call(split=True) for _ in range(reps)]
keys_main = ("e2e", "device", "traces", "upload", "host_prepare", "d2h", "host_encode", "host_decode")
summary: Dict[str, Dict[str, Any]] = {k: _stats_ms([c[k] for c in main_pass]) for k in keys_main}
for st in stages:
summary[f"trace/{st}"] = _stats_ms([c[f"trace/{st}"] for c in split_pass])
summary["e2e_split_pass"] = _stats_ms([c["e2e"] for c in split_pass])
summary["upload_split_pass"] = _stats_ms([c["upload"] for c in split_pass])
summary["upload_split_pass"]["note"] = "tracer.upload (+sync): host inputs + every write serialised on the trace CQ"
summary.update(upload_rows(per_tensor))
summary["host_decode"]["note"] = "decode of the traced action_pred (valid rows / dims sliced on host)"
summary["upload"]["note"] = "production path: call start -> first execute_trace (host inputs + gating writes)"
facts = D.device_facts(device)
facts.update(MB.device_report(__import__("ttnn"), device))
ex_desc = executor.describe()
meta = dict(
setup["meta"],
shape_key=str(mi.shape_key),
n_text=mi.n_text,
seq_len=mi.seq_len,
model_load_s=round(load_s, 2),
stage_names=stages,
pred_shape=list(torch.as_tensor(tracer.read_action_pred()).shape),
embed_gather=getattr(model, "embed_gather", None),
cq1_uploads=ex_desc.get("cq1_uploads"),
num_command_queues=int(executor.backend.num_command_queues(device)),
prepare_asserts=ex_desc.get("prepare_asserts"),
per_call_inputs=[s.describe() for s in executor.plan().inputs],
per_call_bytes=ex_desc.get("plan", {}).get("per_call_bytes"),
vit_fork=getattr(getattr(model.backbone, "tower", None), "fork", None),
dit_backend=model.policy.dit_backend,
mk_arena_dtype=model.recipe.mk_arena_dtype,
megakernel=model.head.describe().get("megakernel"),
worker_l1_size=_worker_l1_size(device),
head_timing={k: v for k, v in model.head.timing.items()},
model_timing={k: v for k, v in model.timing.items() if k.endswith("_s")},
weights={
"n_tensors": len(model.W),
"device_bytes": float(model.W.device_bytes),
"load_path": model.W.load_stats.get("path"),
"load_s": model.timing.get("weights_load_s"),
"skipped": dict(getattr(model, "weights_skipped", {})),
},
)
if profile_encode:
meta["encode_profile"] = encode_profile(version, inputs, setup["layout"])
payload = SO.perf_payload(
BENCH_NAME,
version,
dry_run=False,
args={
"version": version,
"policy": policy,
"trace_layout": trace_layout,
"sample": sample,
"embodiment": setup["embodiment_tag"],
"layout": setup["layout"].name,
"warmup": warmup,
"reps": reps,
"host_reps": host_reps,
"noise_seed": noise_seed,
"embed_gather": embed_gather,
"cq1_uploads": cq1_uploads,
"dit_backend": dit_backend,
"mk_arena_dtype": mk_arena_dtype,
"load_ttnn_dit_weights": load_ttnn_dit_weights,
},
method={
"main_pass": f"{warmup} warm-up calls, then {reps} calls: encode -> tracer.call (TraceExecutor.replay: "
"asserts, host inputs, writes, every trace; blocking on the last) -> D2H -> decode; medians / p90 of each "
"component (device = call start..D2H; upload = call start..first execute_trace; traces = the rest)",
"split_pass": f"{reps} more calls: tracer.upload (+sync) -> one synchronised replay per trace -> "
"trace/<stage> medians and the per-tensor upload/<input> rows (bytes, host tilize ms, copy ms)",
"clock": "host wall clock (time.perf_counter); trace replays enqueued non-blocking",
},
stages=summary,
rows=[dict(c, pass_="main", i=i) for i, c in enumerate(main_pass)]
+ [dict(c, pass_="split", i=i) for i, c in enumerate(split_pass)],
device=facts,
meta=meta,
)
if verbose:
print_summary(payload)
return payload
def upload_rows(per_tensor: Sequence[Mapping[str, Mapping[str, float]]]) -> Dict[str, Dict[str, Any]]:
"""``upload/<input>`` stage rows (median ms of the serialised write; ``host_ms`` = from_torch / tilize,
``copy_ms`` = copy_host_to_device_tensor enqueue, ``bytes``) and ``upload/total`` from the split pass'
``executor.upload_stats`` snapshots."""
if not per_tensor:
return {}
names = list(per_tensor[0])
out: Dict[str, Dict[str, Any]] = {}
for name in names:
rows = [s[name] for s in per_tensor if name in s]
st = _stats_ms([r["total_s"] * 1e3 for r in rows])
st["host_ms"] = round(statistics.median([r["host_s"] * 1e3 for r in rows]), 4)
st["copy_ms"] = round(statistics.median([r["copy_s"] * 1e3 for r in rows]), 4)
st["bytes"] = int(rows[0]["bytes"])
out[f"upload/{name}"] = st
tot = _stats_ms([sum(r["total_s"] for r in s.values()) * 1e3 for s in per_tensor])
tot["bytes"] = int(sum(int(s["bytes"]) for s in per_tensor[0].values()))
tot["note"] = "sum of the serialised per-tensor writes (host tilize + copy enqueue) of one call"
out["upload/total"] = tot
return out
# --------------------------------------------------------------------------------------------------------------------
# Dry run
# --------------------------------------------------------------------------------------------------------------------
def dry_run(
*,
version: str,
policy: str = "mixed_dit",
trace_layout: str = "per_stage",
sample: str = "canonical",
embodiment: Optional[str] = None,
layout_name: Optional[str] = None,
host_reps: int = DEFAULT_HOST_REPS,
verbose: bool = True,
profile_encode: bool = False,
dit_backend: str = DEFAULT_DIT_BACKEND,
mk_arena_dtype: str = "auto",
) -> Dict[str, Any]:
"""Validate the setup and time the host side (encode / decode) when the golden data is present; no device."""
setup = resolve_setup(
version,
policy=policy,
trace_layout=trace_layout,
layout_name=layout_name,
embodiment=embodiment,
dit_backend=dit_backend,
mk_arena_dtype=mk_arena_dtype,
)
stages: Dict[str, Dict[str, Any]] = {}
meta = dict(setup["meta"])
meta["gr00t_tt_importable"] = import_gr00t_tt() is not None
ok, reason = golden_available(version, sample)
meta["golden_sample"] = sample
if ok:
inputs = load_golden_inputs(version, sample, setup["embodiment_tag"], setup["layout"].name)
mi = inputs["mi"]
meta.update(shape_key=str(mi.shape_key), n_text=mi.n_text, seq_len=mi.seq_len, n_images=mi.n_images)
stages.update(host_timings(version, inputs, setup["layout"], host_reps))
if profile_encode:
prof = encode_profile(version, inputs, setup["layout"], reps=max(host_reps, 5))
meta["encode_profile"] = prof
stages.update({f"encode/{k}": v for k, v in prof.items()})
else:
meta["host_timing_skipped"] = reason
for st in setup["stage_names"]:
stages[f"trace/{st}"] = {"metric": "ms", "value": None, "note": "dry run: not measured"}
payload = SO.perf_payload(
BENCH_NAME,
version,
dry_run=True,
args={
"version": version,
"policy": policy,
"trace_layout": trace_layout,
"sample": sample,
"embodiment": setup["embodiment_tag"],
"layout": setup["layout"].name,
"host_reps": host_reps,
"dit_backend": dit_backend,
"mk_arena_dtype": mk_arena_dtype,
},
method={"host": f"{host_reps} host calls after 1 warm-up (encode / decode), median / p90 ms; no device"},
stages=stages,
rows=[],
meta=meta,
)
if verbose:
print_summary(payload)
return payload
def print_summary(payload: Mapping[str, Any]) -> None:
rows = []
for k, s in payload["stages"].items():
rows.append(
{
"stage": k,
"median_ms": s.get("value"),
"p90_ms": s.get("p90"),
"min_ms": s.get("min"),
"host_ms": s.get("host_ms"),
"copy_ms": s.get("copy_ms"),
"bytes": s.get("bytes"),
"n": s.get("n"),
"note": s.get("note"),
}
)
MB.print_table(
rows,
["stage", "median_ms", "p90_ms", "min_ms", "host_ms", "copy_ms", "bytes", "n", "note"],
title=f"{payload['bench']} {payload['version']} ({'DRY RUN' if payload['dry_run'] else 'measured'})",
)
m = payload["meta"]
print(
" layout {l} | embodiment {e} | S_pad {s} | shape_key {sk} | policy {p} | dit_backend {b} (arena {a}) | "
"stages {st} | embed_gather {g} | cq1_uploads {c} ({n} CQ){skip}".format(
l=m.get("layout_name"),
e=m.get("embodiment_tag"),
s=m.get("s_pad_llm"),
sk=m.get("shape_key", "-"),
p=(m.get("policy") or {}).get("dtype_policy"),
b=m.get("dit_backend", (m.get("policy") or {}).get("dit_backend")),
a=m.get("mk_arena_dtype", "-"),
st=m.get("stage_names", m.get("expected_stage_names")),
g=m.get("embed_gather", "-"),
c=m.get("cq1_uploads", "-"),
n=m.get("num_command_queues", "-"),
skip=f" | host timing skipped: {m['host_timing_skipped']}" if m.get("host_timing_skipped") else "",
)
)
# --------------------------------------------------------------------------------------------------------------------
# CLI
# --------------------------------------------------------------------------------------------------------------------
def build_parser() -> argparse.ArgumentParser:
ap = argparse.ArgumentParser(
description=__doc__.split("\n\n")[0], formatter_class=argparse.RawDescriptionHelpFormatter
)
ap.add_argument("--version", required=True, choices=VERSIONS)
ap.add_argument("--policy", default="mixed_dit", choices=POLICIES, help="TTPolicy.dtype_policy")
ap.add_argument(
"--dit-backend",
default=DEFAULT_DIT_BACKEND,
choices=DIT_BACKENDS,
help=f"TTPolicy.dit_backend: Stage-1 ttnn ops | megakernel (default: the policy default, {DEFAULT_DIT_BACKEND})",
)
ap.add_argument(
"--load-ttnn-dit-weights",
action="store_true",
help="keep the Stage-1 DiT / precompute / encoder upload under the megakernel backend (startup A/B; "
"default: the megakernel skips them, tt.model.megakernel_weight_names)",
)
ap.add_argument(
"--mk-arena-dtype",
default="auto",
choices=MK_ARENA_DTYPES,
help="TTPolicy.mk_arena_dtype: the megakernel's weight arena (auto = the per-version default)",
)
ap.add_argument(
"--trace-layout", default="per_stage", choices=TRACE_LAYOUTS, help="TTPolicy.trace_layout (plan §4)"
)
ap.add_argument("--sample", default="canonical", help="golden sample name")
ap.add_argument("--embodiment", default=None, help="embodiment tag (default: the layout's)")
ap.add_argument("--layout", default=None, help="static layout name (default: the version's canonical layout)")
ap.add_argument("--warmup", type=int, default=DEFAULT_WARMUP)
ap.add_argument("--reps", type=int, default=DEFAULT_REPS)
ap.add_argument("--host-reps", type=int, default=DEFAULT_HOST_REPS)
ap.add_argument("--noise-seed", type=int, default=0)
ap.add_argument("--cache-root", default=None, help="TTWeights cache root (default: the plan cache)")
ap.add_argument("--out", default=str(SO.RESULTS_DIR))
ap.add_argument("--dry-run", action="store_true", help="validate the setup, time the host side only; no device")
ap.add_argument("--trace-region-mb", type=int, default=64)
ap.add_argument("--num-cqs", type=int, default=2, choices=(1, 2), help="command queues the device is opened with")
ap.add_argument("--cq1-uploads", default="auto", choices=CQ1_MODES, help="per-call input writes on CQ 1")
ap.add_argument("--embed-gather", default="device", choices=EMBED_GATHERS, help="Gr00tTT text-row gather")
ap.add_argument("--profile-encode", action="store_true", help="add the per-step host-encode profile to meta")
ap.add_argument(
"--torch-threads", type=int, default=None, help="torch.set_num_threads for the host side (experiment knob)"
)
ap.add_argument(
"--cv2-threads", type=int, default=None, help="cv2.setNumThreads for the host side (experiment knob)"
)
return ap
def set_host_threads(torch_threads: Optional[int], cv2_threads: Optional[int]) -> Dict[str, int]:
"""Process-level thread counts of the host libraries (n17-perf.md open issue: 16 torch + 32 cv2 threads make
the small host ops 1.5-2x slower next to the ttnn worker threads); returns the values in force."""
import cv2
import torch
if torch_threads is not None:
torch.set_num_threads(int(torch_threads))
if cv2_threads is not None:
cv2.setNumThreads(int(cv2_threads))
return {"torch_threads": int(torch.get_num_threads()), "cv2_threads": int(cv2.getNumThreads())}
def main(argv: Optional[Sequence[str]] = None) -> int:
args = build_parser().parse_args(argv)
threads = set_host_threads(args.torch_threads, args.cv2_threads)
if args.dry_run:
payload = dry_run(
version=args.version,
policy=args.policy,
trace_layout=args.trace_layout,
sample=args.sample,
embodiment=args.embodiment,
layout_name=args.layout,
host_reps=args.host_reps,
profile_encode=args.profile_encode,
dit_backend=args.dit_backend,
mk_arena_dtype=args.mk_arena_dtype,
)
SO.write_perf_json(payload, Path(args.out))
return 0
if import_gr00t_tt() is None:
raise ImportError("tt.model.Gr00tTT is not importable (WP-14 pending) -- run with --dry-run")
SO.ensure_tt_metal_cache()
from models.experimental.gr00t.tt import model as M
from models.experimental.gr00t.tt.policy import TTPolicy
device = M.open_model_device(
TTPolicy(
dtype_policy=args.policy,
trace_layout=args.trace_layout,
dit_backend=args.dit_backend,
mk_arena_dtype=args.mk_arena_dtype,
),
trace_region_size=args.trace_region_mb << 20,
l1_small_size=D.DEFAULT_L1_SMALL_SIZE,
num_command_queues=args.num_cqs,
)
try:
payload = run(
device,
version=args.version,
policy=args.policy,
trace_layout=args.trace_layout,
sample=args.sample,
embodiment=args.embodiment,
layout_name=args.layout,
warmup=args.warmup,
reps=args.reps,
host_reps=args.host_reps,
noise_seed=args.noise_seed,
cache_root=Path(args.cache_root) if args.cache_root else None,
embed_gather=args.embed_gather,
cq1_uploads=resolve_cq1(args.cq1_uploads),
profile_encode=args.profile_encode,
dit_backend=args.dit_backend,
mk_arena_dtype=args.mk_arena_dtype,
load_ttnn_dit_weights=True if args.load_ttnn_dit_weights else None,
)
finally:
D.close_gr00t_device(device)
payload["meta"]["host_threads"] = threads
SO.write_perf_json(payload, Path(args.out))
return 0
__all__ = [
"BENCH_NAME",
"CQ1_MODES",
"DIT_BACKENDS",
"MK_ARENA_DTYPES",
"EMBED_GATHERS",
"POLICIES",
"PRODUCTION_MEMBERS",
"REPLAYER_MEMBERS",
"STAGE_NAMES",
"TRACE_LAYOUTS",
"denoise_stage_name",
"dry_run",
"encode_profile",
"golden_available",
"host_timings",
"import_gr00t_tt",
"load_golden_inputs",
"load_model",
"make_noise",
"resolve_cq1",
"resolve_setup",
"resolve_stage_replayer",
"run",
"time_host",
"upload_rows",
]
if __name__ == "__main__":
raise SystemExit(main())