changh95 commited on
Commit
c27a3f0
·
verified ·
1 Parent(s): 47e84ce

Stage-2 release: DiT denoise as one persistent megakernel (snapshot 009de892, image dfb9c3889ab1)

Browse files

Replaces the Stage-1 (TTNN-op denoise) package with the megakernel release: image sha256:dfb9c3889ab17cc1d8187085b27e854d0311fb974d729efe88db9bfdce51869f (tt-model/gr00t-n1.5-3b-p150:dfb9c3889ab1), port snapshot 009de892c895 on tt-metal 668c290757, code/ digest d87906471090789d. `tt-model push --publish` uploaded code/, image/, tt_kernel_manifest.json, requirements.lock and the generated card and pruned the code/ and image/ files this bundle no longer ships; this commit restores the reviewed staging card (front matter: license other / nvidia-license, pipeline_tag robotics, base_model nvidia/GR00T-N1.5-3B, 13 tags; body = the tt-model.yaml card with the simplified quickstart bullets) and the authored files tt-model.yaml, SERVING.md, GPU_COMPARISON.md, LICENSE, LICENSE-NOTICE.md, requirements.lock and media/*. Card text per docs/publish/review-mk.md.

Files changed (6) hide show
  1. GPU_COMPARISON.md +80 -60
  2. LICENSE-NOTICE.md +1 -1
  3. README.md +17 -4
  4. SERVING.md +132 -84
  5. media/demo_actions.png +2 -2
  6. tt-model.yaml +54 -26
GPU_COMPARISON.md CHANGED
@@ -1,22 +1,23 @@
1
  # GR00T-N1.5-3B-p150 — Blackhole p150a vs RTX 5090 (same host, same weights, same observation)
2
 
3
- Dates: GPU rows 2026-09-14, p150a rows 2026-09-14/15. Facts only: every number is copied from the file named in its
4
- row. The two sides run **different code**: the GPU rows are NVIDIA's unmodified `Gr00tPolicy` (the official inference
5
- path, eager PyTorch); the p150a rows are this package's tt-nn port (Stage-1: TTNN ops replayed from four Metal traces).
6
- Read the caveats before comparing.
 
7
 
8
  ## What was run
9
 
10
  | | RTX 5090 | Blackhole p150a |
11
  |---|---|---|
12
- | Code | NVIDIA [Isaac-GR00T](https://github.com/NVIDIA/Isaac-GR00T) tag `n1.5-release` (`4af2b622`), `Gr00tPolicy.get_action`, `FourierGr1ArmsOnlyDataConfig`, unmodified | `code/models/experimental/gr00t` (frozen snapshot `5dc0517ee3b`), `Gr00tTT` with `TTPolicy(dtype_policy="mixed_dit", trace_layout="per_stage")`, served by `code/gr00t_p150/server/app.py` |
13
- | Weights | `nvidia/GR00T-N1.5-3B` @ `869830fc749c35f34771aa5209f923ac57e4564e`, bf16 shards from the HF cache | same snapshot; converted once into the device layout (bf16 everywhere, bfp8_b for the 16 DiT blocks' matmul weights) |
14
- | Precision | official path: `torch.autocast(bfloat16)` + flash-attention 2 (`flash_attention_2` is hard-coded in the vendored Eagle2.5 config) | bf16 activations; HiFi2 matmul fidelity in the backbone/adapter, bfp8_b DiT weights; fp32 host pre/post-processing |
15
  | Observation | the port's canonical golden sample: one 256×256 `ego_view` frame (GR1 sim `robot_sim.PickNPlace` traj 0 / step 100), GR1 state (7/7/6/6), instruction "pick the pear from the counter and place it in the plate", batch 1, fixed initial noise | identical inputs (`code/gr00t_p150/demo/n15/`); LLM sequence 296 tokens padded to the static 384 |
16
- | GPU / host | NVIDIA GeForce RTX 5090 (sm_120), driver 580.126.18; settled at P1, 2985 MHz SM / 13,801 MHz mem, 190–250 W during the loop | one p150a (11×10 compute grid, 8 DRAM banks), same host, `/dev/tenstorrent/0` |
17
  | Software | Python 3.10.20, torch 2.7.1+cu128, torchvision 0.22.1+cu128, transformers 4.51.3, flash-attn 2.8.3 (official prebuilt wheel; its kernels verified on sm_120 against SDPA to 1 bf16 ulp), cuDNN 9.7.1 | tt-metal main `668c2907575` (`v0.79.0-dev20260914-1`), torch 2.11.0+cpu; image: Python 3.12 |
18
- | Loop | `time_n15.py`: 3 rounds × (20 warm-ups + 20 timed calls), `torch.cuda.synchronize()` around every call, GPU otherwise idle; **best median of the 3 rounds** (the round medians agree within 0.4 ms) | `benchmarks/bench_e2e.py`: 10 warm-ups + 50 timed calls, host wall clock (`perf_counter`), trace replays enqueued non-blocking with one synchronize per call; a second 50-call pass with a synchronize after every trace gives the per-trace split; **median** (p90 in brackets) |
19
- | Source files | `docs/research/ref-env-n15.md` §6 (project research notes; timing JSON `golden/n15/timing.json`) — not shipped in this repo | `code/models/experimental/gr00t/benchmarks/results/e2e_stage1_n15.json` (shipped); served numbers from this image: `SERVING.md` §"Container validation" |
20
 
21
  ## Latency, batch 1 (ms)
22
 
@@ -38,84 +39,102 @@ An fp32 (no autocast) model-only run measured 64–100 ms across runs and was no
38
  `torch.compile` / TensorRT run was made for N1.5 (see caveats). NVIDIA's own N1.5 README quotes, for a single **H100**:
39
  VLM backbone 23.18 ms, action head 4 × 6.18 = 24.7 ms, full model 47.88 ms.
40
 
41
- ### Blackhole p150a, Stage-1 trace path (`e2e_stage1_n15.json`, `stages.<name>.value` / `.p90`)
42
 
43
- | stage | median ms (p90) |
44
- |---|---:|
45
- | **e2e** = host encode + upload + 4 traces + readback + host decode | **43.36** (44.08) |
46
- | device = upload + traces + readback | 41.68 (42.25) |
47
- | 4 trace replays, one synchronize | 37.83 (37.87) |
48
- | — trace `vision` (SigLIP tower + projector) | 8.71 |
49
- | — trace `llm` (Qwen3-1.7B[:12]) | 8.75 |
50
- | — trace `adapter` (VL-LN + 4 VL-SA blocks + K/V hoist for the DiT cross-attention + state encoder) | 4.47 |
51
- | — trace `denoise` (4 Euler steps × (action encoder + 16 DiT blocks + decoder)) | 16.01 |
52
- | upload (host inputs → device buffers, incl. synchronize) | 3.77 (4.33) |
53
- | readback of `action_pred` (D2H) | 0.09 |
54
- | host encode (`Observation` → `ModelInputs`: image chain, prompt + tokenizer, sin/cos state) | 1.52 (1.82) |
55
- | host decode (un-normalise) | 0.17 |
56
- | e2e of the split pass (synchronize after every trace) | 43.92 (44.68) |
57
-
58
- Same run re-executed on the frozen snapshot worktree before packaging: e2e 43.01, traces 37.82 (vision 8.70 · llm 8.73 ·
59
- adapter 4.44 · denoise 15.99) every trace median within 0.03 ms of the table above (`docs/publish/snapshot.md` §5.1).
60
-
61
- Served over HTTP by this image (`tt-model serve` `scripts/bench_http.py`, 10 warm-ups + 50 timed demo requests, quiet
62
- host): **device 42.43 ms (p90 43.18) · server-side 45.40 ms (p90 49.20)**, client wall 47.6 ms on the same host
63
- `SERVING.md` §4 has the three boots, incl. two under a heavily loaded host (device 44.3–44.5, server-side 53.6–58.1 ms
64
- medians) and the cold / warm boot times. The served device figure is 0.7 ms above `bench_e2e`'s device row (41.68 ms).
 
 
 
 
 
 
 
65
 
66
  ## Fidelity vs the fp32 reference (same observation, same initial noise)
67
 
68
  The fp32 reference is NVIDIA's own `Gr00tPolicy` run on CPU in fp32 (math SDPA). Both bf16 paths are measured
69
  against it; PCC in float64 over the flattened tensor.
70
 
71
- | tensor | NVIDIA bf16 + FA2 on the RTX 5090 (`ref-env-n15.md` §5.1) | p150a Stage-1 path (`tests/tt/results/test_e2e_untraced_taps_vs_golden_n15_20260914-230740.json`; that run records the port's `two` trace layout — the `per_stage` re-run on the frozen snapshot gives identical values, `SERVING.md` §2) | gate applied to the p150a (multi-sample rule: twice the worst deviation of NVIDIA's bf16 path over 9 observations) |
72
- |---|---:|---:|---:|
73
- | `backbone_features` (post final RMSNorm) | 0.999006 | 0.995863 | 0.995 |
74
- | `vl_self_attention_out` (= DiT `encoder_hidden_states`) | 0.997315 | 0.991189 | 0.991 |
75
- | `vit_post_layernorm_out` | 0.999000 | 0.995057 | 0.993 |
76
- | `action_pred_normalized` (1×16×32) | 0.999991 | 0.999980 (max\|d\| 0.0143) | 0.9999 (+ abs 0.027) |
77
- | `unnormalized_action.left_arm` / `right_arm` / `right_hand` | 0.99998 / 0.99998 / 0.99999 | 0.999947 / 0.999972 / 0.999994 (max\|d\| 0.021 / 0.017 / 0.029 rad) | 0.9999 (+ abs 0.050 / 0.038 / 0.058) |
78
- | `unnormalized_action.left_hand` (range 0.099 rad in this sample — PCC not meaningful) | 0.9525 | 0.943352 (max\|d\| 0.018 rad) | abs 0.046 only |
79
-
80
- Over the multi-sample set (9 observations, `golden-multisample.md` §4.1) NVIDIA's bf16 path itself drops to
81
- `backbone_features` 0.99771, `vl_self_attention_out` 0.99573 and `action_pred_normalized` 0.999987; the p150a is held
82
- to gates derived from those minima. All 58 rows of the p150a run pass; the final action chunk agrees with the fp32
83
- reference to PCC 0.99995 on every gated group.
 
 
 
 
 
84
 
85
  ## Comparison with matching definitions
86
 
87
  Ratio = RTX 5090 ms / p150a ms (> 1 means the p150a is faster).
88
 
89
- | row | RTX 5090 (official eager bf16 + FA2) | p150a (Stage-1 traces) | ratio |
90
- |---|---:|---:|---:|
91
- | end-to-end: `Gr00tPolicy.get_action` vs port e2e (both: host preprocessing + model + un-normalisation, batch 1) | 58.0 | 43.36 | **1.34** |
92
- | model only: `model.get_action` vs port device (upload + traces + readback) | 55.4 | 41.68 | **1.33** |
93
- | backbone: `EagleBackbone.forward` vs traces `vision` + `llm` | 20.8 | 17.46 | 1.19 |
94
- | action head: `ActionHead.get_action` (VL-SA + 4 denoising steps) vs traces `adapter` + `denoise` | 34.6 | 20.48 | 1.69 |
95
- | host preprocessing: `apply_transforms` vs port encode | 2.17 | 1.52 | 1.43 |
96
 
97
  Reading: at batch 1 the eager GPU path is kernel-launch bound (the action head is 60 % of its time at 1/3 of the FLOPs,
98
- ≈ 16 blocks × ~15 kernels per denoising step); the p150a's traces replay the same graph with no launch overhead, which is
99
- where the 1.7× on the head comes from, while the two backbones are within 20 %.
 
 
100
 
101
  ## Caveats — read before quoting a ratio
102
 
103
  1. **Eager GPU only.** No `torch.compile` or TensorRT run was made for N1.5. For N1.6 on the same GPU,
104
  `torch.compile(DiT.forward, mode="max-autotune")` cut the official e2e from 63.1 ms to 33.1 ms (`ref-env-n16.md` §5)
105
- and NVIDIA's N1.6 README lists 37 ms compiled on an RTX 5090; a compiled N1.5 would very likely be faster than this
106
- Stage-1 p150a path. The p150a's own next step (the persistent-megakernel denoise, Stage 2) is not in this image either.
107
  2. **Different code, similar precision class.** GPU: bf16 autocast + FA2 (NVIDIA's shipped path). p150a: bf16 with
108
- bfp8_b DiT weights and HiFi2 matmul fidelity. Both are compared against the same fp32 reference above; neither is fp32.
 
109
  3. **Stage boundaries differ slightly.** The GPU "backbone" includes `lm_head`; the p150a `adapter` trace holds the VL
110
  self-attention (GPU: inside the action head) plus the K/V hoist and state encoder. Only the e2e and model rows are
111
  like-for-like.
112
  4. **Loops differ** (3 × 20 timed calls best-median vs 50 timed calls median; `cuda.synchronize` per call vs one
113
  host-side synchronize per call). Both exclude HTTP/JSON framing. The GPU e2e includes a `deepcopy` of the observation
114
- (NVIDIA's code); the p150a e2e includes the 3.8 ms input upload.
115
  5. **Power / efficiency: not compared.** GPU draw during the loop was 190–250 W (idle not recorded in this run); p150a
116
  power was not measured.
117
  6. **Batch 1, one sample.** Timings are for the canonical observation; the port's traces are shape-locked, so any
118
  GR1 request with ≤ 102 text tokens takes the same device time (the sequence is padded to 384).
 
 
 
119
 
120
  ## Reproduce
121
 
@@ -124,7 +143,8 @@ where the 1.7× on the head comes from, while the two backbones are within 20 %.
124
  tt serve changh95/GR00T-N1.5-3B-p150
125
  python3 code/scripts/bench_http.py --url http://127.0.0.1:20000 --reps 50 --warmup 10 # server-side timing_ms + client wall
126
  # p150a, the port benchmark (needs a built tt-metal 668c2907575 tree, the pinned weights and the project's golden observation):
127
- python code/models/experimental/gr00t/benchmarks/bench_e2e.py --version n15 --out <dir>
 
128
  # RTX 5090: Isaac-GR00T n1.5-release + the environment of ref-env-n15.md §1.2; the timing script (tt_golden/time_n15.py)
129
  # lives in the project's research tree, not in this repo.
130
  ```
 
1
  # GR00T-N1.5-3B-p150 — Blackhole p150a vs RTX 5090 (same host, same weights, same observation)
2
 
3
+ Dates: GPU rows 2026-09-14, p150a rows 2026-09-18 (megakernel release; the Stage-1 rows of the 2026-09-14 release are kept
4
+ as the reference). Facts only: every number is copied from the file named in its row. The two sides run **different
5
+ code**: the GPU rows are NVIDIA's unmodified `Gr00tPolicy` (the official inference path, eager PyTorch); the p150a rows
6
+ are this package's tt-nn port (Stage 2: vision / LLM / adapter as TTNN ops replayed from three Metal traces, the DiT
7
+ denoise as one persistent megakernel `generic_op` in the fourth). Read the caveats before comparing.
8
 
9
  ## What was run
10
 
11
  | | RTX 5090 | Blackhole p150a |
12
  |---|---|---|
13
+ | Code | NVIDIA [Isaac-GR00T](https://github.com/NVIDIA/Isaac-GR00T) tag `n1.5-release` (`4af2b622`), `Gr00tPolicy.get_action`, `FourierGr1ArmsOnlyDataConfig`, unmodified | `code/models/experimental/gr00t` (frozen snapshot `009de892c895`), `Gr00tTT` with `TTPolicy(dtype_policy="mixed_dit", dit_backend="megakernel", mk_arena_dtype="bfp8_b", trace_layout="per_stage")`, device opened by `open_model_device(num_command_queues=2)`, served by `code/gr00t_p150/server/app.py`; Stage-1 reference rows: the same tree with `dit_backend="ttnn"` |
14
+ | Weights | `nvidia/GR00T-N1.5-3B` @ `869830fc749c35f34771aa5209f923ac57e4564e`, bf16 shards from the HF cache | same snapshot; converted once into the device layout (bf16 everywhere; the 16 DiT blocks' matmul weights bfp8_b, packed into the megakernel's DRAM arena — 496 MB streamed per denoising step) |
15
+ | Precision | official path: `torch.autocast(bfloat16)` + flash-attention 2 (`flash_attention_2` is hard-coded in the vendored Eagle2.5 config) | bf16 activations; HiFi2 matmul fidelity in the backbone / adapter and inside the megakernel's blocks (bf16 / HiFi4 tail ops), bfp8_b DiT weights; fp32 host pre/post-processing |
16
  | Observation | the port's canonical golden sample: one 256×256 `ego_view` frame (GR1 sim `robot_sim.PickNPlace` traj 0 / step 100), GR1 state (7/7/6/6), instruction "pick the pear from the counter and place it in the plate", batch 1, fixed initial noise | identical inputs (`code/gr00t_p150/demo/n15/`); LLM sequence 296 tokens padded to the static 384 |
17
+ | GPU / host | NVIDIA GeForce RTX 5090 (sm_120), driver 580.126.18; settled at P1, 2985 MHz SM / 13,801 MHz mem, 190–250 W during the loop | one p150a (11×10 compute grid, 8 DRAM banks; the megakernel uses 110 cores), same host, `/dev/tenstorrent/0` |
18
  | Software | Python 3.10.20, torch 2.7.1+cu128, torchvision 0.22.1+cu128, transformers 4.51.3, flash-attn 2.8.3 (official prebuilt wheel; its kernels verified on sm_120 against SDPA to 1 bf16 ulp), cuDNN 9.7.1 | tt-metal main `668c2907575` (`v0.79.0-dev20260914-1`), torch 2.11.0+cpu; image: Python 3.12 |
19
+ | Loop | `time_n15.py`: 3 rounds × (20 warm-ups + 20 timed calls), `torch.cuda.synchronize()` around every call, GPU otherwise idle; **best median of the 3 rounds** (the round medians agree within 0.4 ms) | `benchmarks/bench_e2e.py`: 10 warm-ups + 50 timed calls, host wall clock (`perf_counter`), input writes on CQ 1, trace replays enqueued non-blocking with one synchronize per call; a second 50-call pass with a synchronize after every trace gives the per-trace split; **median** (p90 in brackets) |
20
+ | Source files | `docs/research/ref-env-n15.md` §6 (project research notes; timing JSON `golden/n15/timing.json`) — not shipped in this repo | `code/models/experimental/gr00t/benchmarks/results/bench_e2e_n15_20260918-050550.json` (megakernel, the frozen snapshot), `bench_e2e_n15_20260918-032243.json` (same tree, `--dit-backend ttnn`), `e2e_stage1_n15.json` (the 2026-09-14 Stage-1 release) — all shipped; served numbers from this image: `SERVING.md` §4 |
21
 
22
  ## Latency, batch 1 (ms)
23
 
 
39
  `torch.compile` / TensorRT run was made for N1.5 (see caveats). NVIDIA's own N1.5 README quotes, for a single **H100**:
40
  VLM backbone 23.18 ms, action head 4 × 6.18 = 24.7 ms, full model 47.88 ms.
41
 
42
+ ### Blackhole p150a (`stages.<name>.value` / `.p90` of the named JSONs)
43
 
44
+ | stage | megakernel denoise (this release; `bench_e2e_n15_20260918-050550.json`) | same tree, Stage-1 TTNN denoise (`bench_e2e_n15_20260918-032243.json`) | Stage-1 release of 2026-09-14 (`e2e_stage1_n15.json`) |
45
+ |---|---:|---:|---:|
46
+ | **e2e** = host encode + upload + 4 traces + readback + host decode | **33.59** (33.91) | 40.38 (40.76) | 43.36 (44.08) |
47
+ | device = upload + traces + readback | 32.41 (32.61) | 39.14 (39.39) | 41.68 (42.25) |
48
+ | 4 trace replays, one synchronize | 31.12 (31.15) | 37.87 (37.91) | 37.83 (37.87) |
49
+ | — trace `vision` (SigLIP tower + projector) | 8.73 | 8.73 | 8.71 |
50
+ | — trace `llm` (Qwen3-1.7B[:12]) | 8.78 | 8.76 | 8.75 |
51
+ | — trace `adapter` (VL-LN + 4 VL-SA blocks + K/V hoist for the DiT cross-attention + state encoder) | 4.86 (intermediates in DRAM) | 4.48 | 4.47 |
52
+ | — trace `denoise` (4 Euler steps × (action encoder + 16 DiT blocks + decoder)) | **8.93** = 2.23 per step (one `generic_op`) | 16.02 | 16.01 |
53
+ | upload (host inputs → device buffers; CQ 1 in the 09-18 rows) | 1.25 (1.44) | 1.24 (1.41) | 3.77 (4.33) |
54
+ | readback of `action_pred` (D2H) | 0.05 | 0.07 | 0.09 |
55
+ | host encode (`Observation` → `ModelInputs`: image chain, prompt + tokenizer, sin/cos state) | 0.99 (1.22) | 1.05 (1.21) | 1.52 (1.82) |
56
+ | host decode (un-normalise) | 0.13 | 0.16 | 0.17 |
57
+ | e2e of the split pass (synchronize after every trace) | 34.16 (34.89) | 40.64 (40.96) | 43.92 (44.68) |
58
+
59
+ The megakernel column is the frozen snapshot's own run (`commit 009de892c895`); the same benchmark on the live tree at the
60
+ K4b checkpoint gave e2e 33.69 / device 32.48 / denoise 8.92 ms (`bench_e2e_n15_20260918-043024.json`, shipped) — every
61
+ device-side row within 0.1 ms. Against the same-tree TTNN denoise the megakernel saves **6.8 ms end-to-end (−16.8 %)**, all
62
+ of it in the denoise trace (16.028.93 ms, **−44 %**; per step 4.00 2.23 ms) minus the adapter's DRAM placement
63
+ (+0.38 ms). The megakernel alone, traced and replayed 20 times, runs the four steps in 8.86 ms
64
+ (`bench_mk_step_n15_20260918-035610.json`: 2.216 ms per step; the bfp8 streaming floor at the measured 414 GB/s would be
65
+ 1.20 ms per step, so the kernel is 1.85× off its bandwidth floor the block is compute- and gather-bound, not stream-bound).
66
+ The upload / host-encode differences between the 09-18 and 09-14 columns come from the port's 2-CQ input path and
67
+ device-side embedding gather added between the releases, not from the megakernel.
68
+
69
+ Served over HTTP by this image (`tt-model serve` → `scripts/bench_http.py`, 10 warm-ups + 50 timed demo requests, warm
70
+ boot): **device 32.5 ms (p90 32.82) · server-side 34.6 ms (p90 35.39)**,
71
+ client wall 36.3 ms on the same host — `SERVING.md` §4 has both boots and the cold / warm boot times. The Stage-1
72
+ image of 2026-09-14 served 42.43 ms device / 45.40 ms server-side on the same host.
73
 
74
  ## Fidelity vs the fp32 reference (same observation, same initial noise)
75
 
76
  The fp32 reference is NVIDIA's own `Gr00tPolicy` run on CPU in fp32 (math SDPA). Both bf16 paths are measured
77
  against it; PCC in float64 over the flattened tensor.
78
 
79
+ | tensor | NVIDIA bf16 + FA2 on the RTX 5090 (`ref-env-n15.md` §5.1) | p150a, megakernel denoise (`tests/tt/results/test_e2e_untraced_taps_vs_golden_n15_20260918-044858.json`, frozen snapshot) | p150a, Stage-1 release of 2026-09-14 (`test_e2e_untraced_taps_vs_golden_n15_20260914-230740.json` of the port project's `build-n15.md` §6 — JSON not shipped in this repo) | gate applied to the p150a (multi-sample rule: twice the worst deviation of NVIDIA's bf16 path over 9 observations) |
80
+ |---|---:|---:|---:|---:|
81
+ | `backbone_features` (post final RMSNorm) | 0.999006 | 0.995863 | 0.995863 | 0.995 |
82
+ | `vl_self_attention_out` (= DiT `encoder_hidden_states`) | 0.997315 | 0.991189 | 0.991189 | 0.991 |
83
+ | `vit_post_layernorm_out` | 0.999000 | 0.995057 | 0.995057 | 0.993 |
84
+ | `action_pred_normalized` (1×16×32) | 0.999991 | 0.999980 (max\|d\| 0.0137) | 0.999980 (max\|d\| 0.0143) | 0.9999 (+ abs 0.027) |
85
+ | `unnormalized_action.left_arm` / `right_arm` / `right_hand` | 0.99998 / 0.99998 / 0.99999 | 0.999944 / 0.999971 / 0.999993 (max\|d\| 0.023 / 0.017 / 0.029 rad) | 0.999947 / 0.999972 / 0.999994 (max\|d\| 0.021 / 0.017 / 0.029 rad) | 0.9999 (+ abs 0.050 / 0.038 / 0.058) |
86
+ | `unnormalized_action.left_hand` (range 0.099 rad in this sample — PCC not meaningful) | 0.9525 | 0.928345 (max\|d\| 0.030 rad) | 0.943352 (max\|d\| 0.018 rad) | abs 0.046 only |
87
+
88
+ The backbone and adapter rows are bit-identical between the two p150a columns (the megakernel does not touch them); the
89
+ action rows move by 3e-6 PCC. Megakernel vs the TTNN denoise on the same device and observation: `action_pred_normalized`
90
+ PCC 0.999997, per-step velocities 0.99998 (`test_mk_e2e_backends_agree_canonical_n15_20260918-042408.json`). Over the
91
+ multi-sample set (9 observations, `golden-multisample.md` §4.1) NVIDIA's bf16 path itself drops to `backbone_features`
92
+ 0.99771, `vl_self_attention_out` 0.99573 and `action_pred_normalized` 0.999987; the p150a is held to gates derived from
93
+ those minima. All 30 rows of the megakernel run pass (29 gated rows incl. the `left_hand` abs row + the two-runs-bit-equal
94
+ row; the Stage-1 path had 58 rows because its per-step DiT intermediates are observable and gated — inside one
95
+ `generic_op` they are not). On the 9 multi-sample observations the megakernel's gate outcomes equal the TTNN path's on 8;
96
+ the exception is `right_arm` on `t3_s300_seed2` at 0.999869 vs 0.9999 (`SERVING.md` §7).
97
 
98
  ## Comparison with matching definitions
99
 
100
  Ratio = RTX 5090 ms / p150a ms (> 1 means the p150a is faster).
101
 
102
+ | row | RTX 5090 (official eager bf16 + FA2) | p150a, megakernel denoise | ratio | p150a, Stage-1 release (2026-09-14) | ratio |
103
+ |---|---:|---:|---:|---:|---:|
104
+ | end-to-end: `Gr00tPolicy.get_action` vs port e2e (both: host preprocessing + model + un-normalisation, batch 1) | 58.0 | 33.59 | **1.73** | 43.36 | 1.34 |
105
+ | model only: `model.get_action` vs port device (upload + traces + readback) | 55.4 | 32.41 | **1.71** | 41.68 | 1.33 |
106
+ | backbone: `EagleBackbone.forward` vs traces `vision` + `llm` | 20.8 | 17.51 | 1.19 | 17.46 | 1.19 |
107
+ | action head: `ActionHead.get_action` (VL-SA + 4 denoising steps) vs traces `adapter` + `denoise` | 34.6 | 13.79 | **2.51** | 20.48 | 1.69 |
108
+ | host preprocessing: `apply_transforms` vs port encode | 2.17 | 0.99 | 2.19 | 1.52 | 1.43 |
109
 
110
  Reading: at batch 1 the eager GPU path is kernel-launch bound (the action head is 60 % of its time at 1/3 of the FLOPs,
111
+ ≈ 16 blocks × ~15 kernels per denoising step); the p150a replays the backbone graph with no launch overhead and runs the
112
+ whole head as one program whose weights stream from DRAM once per step, which is where the 2.5× on the head comes from,
113
+ while the two backbones are within 20 %. The backbone (vision + LLM, 17.5 ms) is now the largest block of the p150a's
114
+ 33.6 ms; the denoise is 27 %.
115
 
116
  ## Caveats — read before quoting a ratio
117
 
118
  1. **Eager GPU only.** No `torch.compile` or TensorRT run was made for N1.5. For N1.6 on the same GPU,
119
  `torch.compile(DiT.forward, mode="max-autotune")` cut the official e2e from 63.1 ms to 33.1 ms (`ref-env-n16.md` §5)
120
+ and NVIDIA's N1.6 README lists 37 ms compiled on an RTX 5090; a compiled N1.5 would very likely land near this
121
+ p150a figure rather than 1.7× behind it.
122
  2. **Different code, similar precision class.** GPU: bf16 autocast + FA2 (NVIDIA's shipped path). p150a: bf16 with
123
+ bfp8_b DiT weights (streamed by the megakernel) and HiFi2 matmul fidelity. Both are compared against the same fp32
124
+ reference above; neither is fp32.
125
  3. **Stage boundaries differ slightly.** The GPU "backbone" includes `lm_head`; the p150a `adapter` trace holds the VL
126
  self-attention (GPU: inside the action head) plus the K/V hoist and state encoder. Only the e2e and model rows are
127
  like-for-like.
128
  4. **Loops differ** (3 × 20 timed calls best-median vs 50 timed calls median; `cuda.synchronize` per call vs one
129
  host-side synchronize per call). Both exclude HTTP/JSON framing. The GPU e2e includes a `deepcopy` of the observation
130
+ (NVIDIA's code); the p150a e2e includes the 1.2 ms input upload.
131
  5. **Power / efficiency: not compared.** GPU draw during the loop was 190–250 W (idle not recorded in this run); p150a
132
  power was not measured.
133
  6. **Batch 1, one sample.** Timings are for the canonical observation; the port's traces are shape-locked, so any
134
  GR1 request with ≤ 102 text tokens takes the same device time (the sequence is padded to 384).
135
+ 7. **Megakernel disclosures.** The p150a rows of this release run the denoise with 64 KiB less allocatable L1 per core
136
+ and the adapter's intermediates in DRAM, expose no DiT intermediates for gating, and miss one multi-sample action gate
137
+ the Stage-1 path passes (`SERVING.md` §7). `GR00T_DIT_BACKEND=ttnn` reproduces the Stage-1 rows on the same image.
138
 
139
  ## Reproduce
140
 
 
143
  tt serve changh95/GR00T-N1.5-3B-p150
144
  python3 code/scripts/bench_http.py --url http://127.0.0.1:20000 --reps 50 --warmup 10 # server-side timing_ms + client wall
145
  # p150a, the port benchmark (needs a built tt-metal 668c2907575 tree, the pinned weights and the project's golden observation):
146
+ python code/models/experimental/gr00t/benchmarks/bench_e2e.py --version n15 --out <dir> # megakernel (default)
147
+ python code/models/experimental/gr00t/benchmarks/bench_e2e.py --version n15 --dit-backend ttnn --out <dir> # Stage-1 reference
148
  # RTX 5090: Isaac-GR00T n1.5-release + the environment of ref-env-n15.md §1.2; the timing script (tt_golden/time_n15.py)
149
  # lives in the project's research tree, not in this repo.
150
  ```
LICENSE-NOTICE.md CHANGED
@@ -8,7 +8,7 @@ two licences; nothing here grants more than its source does.
8
  | Model weights `nvidia/GR00T-N1.5-3B` @ `869830fc749c35f34771aa5209f923ac57e4564e` | **not included** — `tt-model serve` / `code/scripts/download_weights.sh` fetch the pinned snapshot into your HF cache | **NVIDIA License** (copy in [`LICENSE`](LICENSE); original: [`LICENSE` in the weights repo](https://huggingface.co/nvidia/GR00T-N1.5-3B/blob/main/LICENSE)) — use limited to research or evaluation (§3.3), redistribution only under the same licence with a copy included (§3.1), derivative works carry the same use limitation (§3.2) | NVIDIA |
9
  | Qwen2 tokenizer files (`vocab.json`, `merges.txt`, `tokenizer_config.json`, `special_tokens_map.json`, `added_tokens.json`) | `code/gr00t_p150/assets/tokenizer/n15/` | Apache-2.0 (copied from `gr00t/model/backbone/eagle2_hg_model/` of [NVIDIA/Isaac-GR00T](https://github.com/NVIDIA/Isaac-GR00T) at tag `n1.5-release`, an Apache-2.0 repository) | NVIDIA / Alibaba Qwen |
10
  | Demo frame and joint state (`media/demo_ego_view.png`, `media/demo_observation.json`, `code/gr00t_p150/demo/n15/`) | `media/`, `code/gr00t_p150/demo/n15/` | one step of the GR1 `robot_sim.PickNPlace` demo data shipped with Isaac-GR00T (`demo_data/`, Apache-2.0 repository); the reference actions in `expected.json` were computed from those inputs with the weights above | NVIDIA |
11
- | The tt-nn port (`code/models/experimental/gr00t/`), the policy server (`code/gr00t_p150/`), the scripts (`code/scripts/`) and the tt-metal files copied into `code/models/{common,tt_dit,demos}/` | `code/` | **Apache-2.0** (SPDX headers; © 2026 Tenstorrent). Because the port's device-layout weights and the served outputs are derivative works of the NVIDIA weights, running this package is bound by the NVIDIA License's non-commercial use limitation (§3.2/§3.3) | Tenstorrent |
12
  | The container image (`image/`) | `image/` | tt-metal / tt-nn (Apache-2.0), Ubuntu 22.04 base and the Python packages listed in `requirements.lock` under their own licences; **contains no weights** | Tenstorrent, upstream projects |
13
 
14
  Model card front matter therefore declares `license: other`, `license_name: nvidia-license`,
 
8
  | Model weights `nvidia/GR00T-N1.5-3B` @ `869830fc749c35f34771aa5209f923ac57e4564e` | **not included** — `tt-model serve` / `code/scripts/download_weights.sh` fetch the pinned snapshot into your HF cache | **NVIDIA License** (copy in [`LICENSE`](LICENSE); original: [`LICENSE` in the weights repo](https://huggingface.co/nvidia/GR00T-N1.5-3B/blob/main/LICENSE)) — use limited to research or evaluation (§3.3), redistribution only under the same licence with a copy included (§3.1), derivative works carry the same use limitation (§3.2) | NVIDIA |
9
  | Qwen2 tokenizer files (`vocab.json`, `merges.txt`, `tokenizer_config.json`, `special_tokens_map.json`, `added_tokens.json`) | `code/gr00t_p150/assets/tokenizer/n15/` | Apache-2.0 (copied from `gr00t/model/backbone/eagle2_hg_model/` of [NVIDIA/Isaac-GR00T](https://github.com/NVIDIA/Isaac-GR00T) at tag `n1.5-release`, an Apache-2.0 repository) | NVIDIA / Alibaba Qwen |
10
  | Demo frame and joint state (`media/demo_ego_view.png`, `media/demo_observation.json`, `code/gr00t_p150/demo/n15/`) | `media/`, `code/gr00t_p150/demo/n15/` | one step of the GR1 `robot_sim.PickNPlace` demo data shipped with Isaac-GR00T (`demo_data/`, Apache-2.0 repository); the reference actions in `expected.json` were computed from those inputs with the weights above | NVIDIA |
11
+ | The tt-nn port (`code/models/experimental/gr00t/`), the policy server (`code/gr00t_p150/`), the scripts (`code/scripts/`) and the tt-metal files copied into `code/models/{common,tt_dit,demos}/` (incl. the four `deepseek_v3_b1/unified_kernels/*.hpp` device headers the megakernel includes) | `code/` | **Apache-2.0** (SPDX headers; © 2026 Tenstorrent). Because the port's device-layout weights and the served outputs are derivative works of the NVIDIA weights, running this package is bound by the NVIDIA License's non-commercial use limitation (§3.2/§3.3) | Tenstorrent |
12
  | The container image (`image/`) | `image/` | tt-metal / tt-nn (Apache-2.0), Ubuntu 22.04 base and the Python packages listed in `requirements.lock` under their own licences; **contains no weights** | Tenstorrent, upstream projects |
13
 
14
  Model card front matter therefore declares `license: other`, `license_name: nvidia-license`,
README.md CHANGED
@@ -4,11 +4,24 @@ tags:
4
  - p150
5
  - tt-dit-server
6
  - tt-model-cache
7
- - tt-model-catalog
8
  - tt-model-container
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  ---
10
 
11
- # gr00t-n1.5-3b-p150
12
 
13
  NVIDIA Isaac GR00T N1.5 (3B vision-language-action policy) running on one Tenstorrent Blackhole p150a via tt-nn: one camera frame, proprioceptive state and a task instruction in, a 16-step chunk of GR1 arm and hand joint targets out. Stage-2 release: vision tower, LLM and VL adapter as TTNN ops replayed from Metal traces (bf16); the whole DiT denoise (4 steps × 16 blocks) as **one persistent megakernel** on 110 Tensix cores, streaming its bfp8 weights from a DRAM arena once per step. `GR00T_DIT_BACKEND=ttnn` selects the Stage-1 TTNN denoise.
14
  Weights: [nvidia/GR00T-N1.5-3B](https://huggingface.co/nvidia/GR00T-N1.5-3B) · Paper: [arXiv:2503.14734](https://arxiv.org/abs/2503.14734) · Upstream code: [NVIDIA/Isaac-GR00T](https://github.com/NVIDIA/Isaac-GR00T) (`n1.5-release`) · Port: `code/models/experimental/gr00t` and the server `code/gr00t_p150` in this repo
@@ -24,7 +37,8 @@ tt-model pull changh95/GR00T-N1.5-3B-p150 --with-weights
24
  tt-model serve changh95/GR00T-N1.5-3B-p150
25
  ```
26
 
27
- `pull --with-weights` downloads the Docker image and the [`nvidia/GR00T-N1.5-3B`](https://huggingface.co/nvidia/GR00T-N1.5-3B) weights at `869830fc749c35f34771aa5209f923ac57e4564e` (into your HF cache; they are not in the image). `serve` starts the model's own HTTP server on port 20000 (or the next free port, if that one is busy); the first start compiles kernels for your device, which takes several minutes, and the server is ready when it logs `Application startup complete`.
 
28
 
29
  ### Run with tt-cli
30
 
@@ -100,4 +114,3 @@ The exact sources the image was built from — `code/` in this repo is byte-iden
100
  | tt-metal | [`668c290757550588d0ce46b180c344a462a2aaf5`](https://github.com/tenstorrent/tt-metal/commit/668c290757550588d0ce46b180c344a462a2aaf5) |
101
  | `code/` digest | `d87906471090789d` (sha256, first 16 hex digits) |
102
  | built | 2026-09-17T20:23:50+00:00 by tt-model 0.1.0 |
103
-
 
4
  - p150
5
  - tt-dit-server
6
  - tt-model-cache
 
7
  - tt-model-container
8
+ - tenstorrent
9
+ - ttnn
10
+ - tt-metal
11
+ - tt-nn
12
+ - robotics
13
+ - vla
14
+ - gr00t
15
+ - tt-model-catalog
16
+ pipeline_tag: robotics
17
+ license: other
18
+ license_name: nvidia-license
19
+ license_link: https://huggingface.co/nvidia/GR00T-N1.5-3B/blob/main/LICENSE
20
+ base_model:
21
+ - nvidia/GR00T-N1.5-3B
22
  ---
23
 
24
+ # GR00T-N1.5-3B-p150
25
 
26
  NVIDIA Isaac GR00T N1.5 (3B vision-language-action policy) running on one Tenstorrent Blackhole p150a via tt-nn: one camera frame, proprioceptive state and a task instruction in, a 16-step chunk of GR1 arm and hand joint targets out. Stage-2 release: vision tower, LLM and VL adapter as TTNN ops replayed from Metal traces (bf16); the whole DiT denoise (4 steps × 16 blocks) as **one persistent megakernel** on 110 Tensix cores, streaming its bfp8 weights from a DRAM arena once per step. `GR00T_DIT_BACKEND=ttnn` selects the Stage-1 TTNN denoise.
27
  Weights: [nvidia/GR00T-N1.5-3B](https://huggingface.co/nvidia/GR00T-N1.5-3B) · Paper: [arXiv:2503.14734](https://arxiv.org/abs/2503.14734) · Upstream code: [NVIDIA/Isaac-GR00T](https://github.com/NVIDIA/Isaac-GR00T) (`n1.5-release`) · Port: `code/models/experimental/gr00t` and the server `code/gr00t_p150` in this repo
 
37
  tt-model serve changh95/GR00T-N1.5-3B-p150
38
  ```
39
 
40
+ - Weights [`nvidia/GR00T-N1.5-3B`](https://huggingface.co/nvidia/GR00T-N1.5-3B) at `869830fc749c` (5.45 GB) go to your HF cache; the image does not contain them.
41
+ - Serves on port 20000 (or the next free port); ready when the log says `Application startup complete`.
42
 
43
  ### Run with tt-cli
44
 
 
114
  | tt-metal | [`668c290757550588d0ce46b180c344a462a2aaf5`](https://github.com/tenstorrent/tt-metal/commit/668c290757550588d0ce46b180c344a462a2aaf5) |
115
  | `code/` digest | `d87906471090789d` (sha256, first 16 hex digits) |
116
  | built | 2026-09-17T20:23:50+00:00 by tt-model 0.1.0 |
 
SERVING.md CHANGED
@@ -3,17 +3,20 @@
3
  This repo is a **tt-model container package source**: `tt-model.yaml` + the code under `code/`. `tt-model package
4
  --container` builds the OCI image (tt-metal `668c2907575` built from source + the port + the HTTP stack),
5
  `tt-model serve` runs it against the card, `tt-model push` publishes it to this HF repo. The server is
6
- `code/gr00t_p150/server/app.py` (FastAPI, `kind: tt-dit-server`); what it runs on the device is the port's **Stage-1**
7
- path every op a TTNN op, captured once into four Metal traces (`vision`, `llm`, `adapter`, `denoise`) and replayed per
8
- request; the persistent-megakernel denoise (Stage 2) is not part of this release.
 
 
 
9
 
10
  | | |
11
  |---|---|
12
  | tt-metal tree the image is built from | `/home/deepgadget/experiments/gr00t/publish/tt-metal-668c2907575` — a clean local clone of tt-metal `main` @ `668c290757550588d0ce46b180c344a462a2aaf5` (`v0.79.0-dev20260914-1`), submodules populated, `git status` empty (the working clone `/home/deepgadget/experiments/gr00t/tt-metal` at the same commit has untracked dirs and would be recorded as dirty); torch pin 2.11.0 |
13
- | port code shipped | `code/models/experimental/gr00t/` = tt-metal ref `gr00t-p150-snapshot`, commit `5dc0517ee3bec090b7ead025001a343c2f63aa7b` (parent `668c290`; frozen 2026-09-14T23:36 KST), minus `__pycache__`, `.omc` tool state and 2,001 of the 2,004 per-test result JSONs (the three N1.5 e2e results the card cites and the `d0/d1/d2/mk_k1` summaries stay) |
14
  | weights | `nvidia/GR00T-N1.5-3B` @ `869830fc749c35f34771aa5209f923ac57e4564e`: `model-0000{1,2,3}-of-00003.safetensors` (5,448,327,040 B of bf16 tensors; 5,448,460,016 B on disk in 3 files), `model.safetensors.index.json`, `config.json`, `experiment_cfg/metadata.json`, `LICENSE` — a pinned pointer, never in the image |
15
- | app | `gr00t_p150.server.app:app` (uvicorn `--lifespan on`; the lifespan does weights → tokenizer → device → model → trace capture → warm-up → READY) |
16
- | device recipe | `models.experimental.gr00t.tt.device.open_gr00t_device(trace_region_size=64 MiB, l1_small_size=32768, device_id=0)`; asserts the p150a's 11×10 compute grid and 8 DRAM banks; `TTPolicy(dtype_policy="mixed_dit", trace_layout="per_stage")` |
17
  | serving shape | embodiment `gr1` (slot 24), layout `gr1`: 1 camera (`ego_view`) × 1 frame × 256 image tokens, ≤ 102 text tokens, LLM sequence padded to 384, state 7/7/6/6 → 16 × [7, 7, 6, 6] actions, batch 1 |
18
 
19
  Below, `$PUB=/home/deepgadget/experiments/gr00t/publish` (this repo is `$PUB/GR00T-N1.5-3B-p150`),
@@ -25,15 +28,17 @@ Below, `$PUB=/home/deepgadget/experiments/gr00t/publish` (this repo is `$PUB/GR0
25
  ```
26
  tt-model.yaml authoring manifest (schema 5.1) -- the whole build/serve interface, incl. the model card text
27
  SERVING.md this file
28
- GPU_COMPARISON.md RTX 5090 vs p150a, matching definitions and caveats
29
  LICENSE the NVIDIA License (weights); LICENSE-NOTICE.md says what is under which licence
30
  media/demo_ego_view.png the demo frame (256x256, GR1 sim robot_sim.PickNPlace traj 0 / step 100)
31
  media/demo_observation.json the matching raw joint state + instruction (request body minus the image)
32
- media/demo_actions.png served actions vs the fp32 reference for that request
33
  code/models/experimental/gr00t/ the tt-nn port (tt-metal tree layout): common/ (host pipeline: preprocessing, prompts,
34
- normalisation, checkpoint, weight plan), tt/ (device model, layers, traces, megakernel WIP),
 
35
  reference/ (fp32 torch reference), tests/, benchmarks/
36
- code/models/{common,tt_dit,demos}/ the 16 tt-metal files the port imports from the tree (source.code in tt-model.yaml)
 
37
  code/gr00t_p150/ the policy server: server/{app,schemas,smoke_test}.py, demo/n15/ (canonical demo
38
  observation + golden actions + noise), assets/tokenizer/n15/ (vendored Qwen2 tokenizer)
39
  code/scripts/ download_weights.sh, bench_http.py
@@ -44,8 +49,20 @@ tt_kernel_manifest.json, image/ written by `tt-model package`; uploaded by `tt
44
  The port keeps its tt-metal-tree absolute imports (`models.experimental.gr00t.*`, `models.tt_dit.*`,
45
  `models.common.*`): in the image the code lands at `/opt/tt-metal/<path>` with `PYTHONPATH=/opt/tt-metal`, and the
46
  image's tt-metal copy excludes `models/`, so everything under `models/` comes from this repo's `code/` (the port) and
47
- `source.code` (the 16 tree files). The server package `gr00t_p150` is shared with `changh95/GR00T-N1.6-3B-p150`; this
48
- repo ships only the N1.5 assets (`demo/n15`, `assets/tokenizer/n15`).
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
  ## 2. Run on the HOST for validation (no Docker)
51
 
@@ -63,8 +80,9 @@ export PYTHONPATH=$PWD/code:$TREE:$TREE/ttnn:$PUB/http-side # code/ FIRST s
63
  export TT_METAL_CACHE=$HOME/.cache/tt-metal-cache-gr00t # warm JIT kernels of the port's device sessions
64
  export HF_MODEL=nvidia/GR00T-N1.5-3B TT_WEIGHTS_REVISION=869830fc749c35f34771aa5209f923ac57e4564e
65
  export GR00T_VERSION=n15 TT_MESH_SHAPE=1x1 TT_DEVICE_ID=0 TT_METAL_VISIBLE_DEVICES=0
 
66
  export GR00T_TOKENIZER_DIR_N15=$PWD/code/gr00t_p150/assets/tokenizer/n15 # optional: the server picks the vendored dir itself
67
- export GR00T_TT_CACHE=$HOME/.cache/gr00t-tt # host .pt plan tier + .tensorbin device tier (~6.4 GB for N1.5)
68
 
69
  # import check, no device (what the image's verify.sh does):
70
  $TREE/python_env/bin/python -c "import gr00t_p150.server.app as a, sys; assert a.app and 'ttnn' not in sys.modules; print('ok')"
@@ -77,35 +95,40 @@ DEVICE_LOCK_TIMEOUT=14400 /home/deepgadget/experiments/gr00t/bin/with-device.sh
77
  ```
78
 
79
  Boot log landmarks (they drive `tt-model serve`'s checklist): `Loading weights: nvidia/GR00T-N1.5-3B @ 869830fc…` →
80
- `Tokenizer files: …` → `Opening device 0 (trace_region_size=67108864, l1_small_size=32768)` → `Loading pipeline:
81
- Gr00tTT.from_pretrained(n15, …)` → `Model built in N s (weights N s, 778 tensors, 3176 MB on device, cache path
82
- warm|cold)` `Warming up: capture per_stage traces on the n15 demo observation, then N traced predict(s)`
83
- `Warmup k/N: … ms (encode, device, decode)` `Warm-up fidelity vs golden fp32: actions PCC ` → `Warmup complete
84
- (capture s, first ms, steady ms; boot s) -- per_stage traces ['vision', 'llm', 'adapter', 'denoise']` → uvicorn
85
- `Application startup complete`. Any failure raises and uvicorn exits non-zero (no CPU fallback); the boot also **fails**
86
- if the warm-up actions' PCC vs the shipped golden is below `GR00T_WARMUP_PCC_MIN` (0.999). SIGTERM / Ctrl-C:
87
- `Releasing traces and device tensors` `Closing device`.
88
-
89
- Measured host boots (warm `~/.cache/gr00t-tt` and warm `TT_METAL_CACHE`; `$PUB/logs/server_n15_*.log`): first boot of a
90
- process 14.6 s to READY (model built 9.3 s incl. 9.2 s reading the `.tensorbin` tier, warm + capture 2.5 s, 5 traced
91
- warm-ups 44.4–44.8 ms); an immediately following boot 4.5 s (0.5 s model build with the tiers in the page cache); on the
92
- frozen snapshot worktree 5.2 s (`server_n15_snapshot_20260914-235713.log`). Kernels are rebuilt once for a new
93
- `TT_METAL_HOME` path (~42 s for the 355 kernels of the four traces, `docs/publish/snapshot.md` §2.3).
94
-
95
- Expected smoke line (host, snapshot worktree, `$PUB/logs/smoke_n15_snapshot_20260914-235713.log`):
 
 
 
 
 
96
 
97
  ```
98
- PASS GR00T-N1.5-3B-p150 n15: PCC(actions)=0.999982 max|d|=0.0289 PCC(action_pred_valid)=0.999976 repeat_maxdiff=0.0e+00
99
- seed_path_ok device_ms=42.95/42.16 total_ms=47.22/44.83 wall_ms=50
100
- per_group[left_arm=0.99995/0.0214 right_arm=0.99997/0.0172 left_hand=0.94335/0.0179 right_hand=0.99999/0.0289]
 
101
  ```
102
 
103
- The per-group PCC / max|d| equal the port's own device test (`code/models/experimental/gr00t/tests/tt/results/
104
- test_e2e_predict_actions_vs_golden_n15_20260914-230743.json`) to the printed precision; the traced policy is
105
- deterministic, so every run reproduces them bit-exactly. That JSON (and the two sibling `_2307*` JSONs the card cites)
106
- records the port's `two` trace layout, not the served `per_stage`; the same three tests re-run with `per_stage` on the
107
- frozen snapshot give identical values for every row (`docs/publish/snapshot.md` §4 of the port project), and the served
108
- `per_stage` actions equal the device test to the printed precision (§4).
109
 
110
  Offline overrides: `GR00T_WEIGHTS_DIR=<dir with the snapshot files>` (shimmed into a private hub cache so the port's
111
  `snapshot_dir()` resolves to it), `GR00T_TOKENIZER_DIR=<dir>`.
@@ -121,7 +144,7 @@ uv venv --python 3.12 $PUB/depcheck-n15 -q
121
  uv pip install --python $PUB/depcheck-n15/bin/python --dry-run torch==2.11.0 fastapi uvicorn "pydantic>=2" pillow "numpy>=1.24.4,<2" \
122
  safetensors huggingface_hub "transformers==5.12.1" "opencv-python-headless==4.8.1.78" "torchvision==0.26.0" pytest \
123
  --extra-index-url https://download.pytorch.org/whl/cpu --index-strategy unsafe-best-match
124
- # -> torch 2.11.0+cpu, torchvision 0.26.0+cpu, numpy 1.26.4, transformers 5.12.1, opencv-python-headless 4.8.1.78 (cp312 wheel), pytest 9.1.1 (logs/uv-dryrun-n15.log)
125
  ```
126
 
127
  ## 3. Package, serve, push (Docker)
@@ -152,8 +175,8 @@ $ROOT/.venv/bin/tt-model push $PUB/build/gr00t-n1.5-3b-p150 --publish
152
 
153
  `serve` pre-downloads the pinned files (a cache hit on this host), then `docker run --user 0:0 --device
154
  /dev/tenstorrent --ipc host` with `/dev/hugepages-1G`, `~/.cache/huggingface` at `/hf` (rw, `HF_HOME=/hf`) and
155
- `~/.cache/tt-model/gr00t-n1.5-3b-p150/{cache,weights,tensors}` at `/cache` (`TT_METAL_CACHE`, JIT kernels),
156
- `/weight-cache` (`TT_DIT_CACHE_DIR`; the port's weight tiers go to `/weight-cache/gr00t-tt`, ~6.4 GB) and
157
  `/tensor-cache`; it exports exactly `HF_MODEL=nvidia/GR00T-N1.5-3B`, `MESH_DEVICE=P150`, `TT_MESH_SHAPE=1x1` plus
158
  `serve.env` from `tt-model.yaml`, publishes the first free port from 20000 and waits ≤ 1800 s for `Application startup
159
  complete`. tt-cli users: `tt serve changh95/GR00T-N1.5-3B-p150` / `tt model stop changh95/GR00T-N1.5-3B-p150`
@@ -166,38 +189,41 @@ complete`. tt-cli users: `tt serve changh95/GR00T-N1.5-3B-p150` / `tt model stop
166
 
167
  ## 4. Container validation (the image built from this manifest)
168
 
169
- Image `tt-model/gr00t-n1.5-3b-p150:443501bd3b7a` (`sha256:443501bd3b7afce579bf743eeedd64809886653c29a9d7125614e8451cd61a7b`,
170
- 3.2 GB as 46 OCI blobs), built 2026-09-14 15:17–15:30 UTC from the manifest above (`tt-model package`: tt-metal C++ build
171
- with a warm ccache 7 min, Python 3.12 venv, all 17 `verify:` lines + the launcher's own passed inside the image;
172
- 13 min 17 s in total). Validated on the p150a on 2026-09-15 00:56–01:02 KST: three `tt-model serve` boots of this exact
173
- image, each followed by `smoke_test.py`, a 50-request `bench_http.py` and `tt-model stop`, all inside one device-lock hold
174
- per run (`publish/logs/validate-n15.run2.log` = boots 1–2, `validate-n15.run3.log` = boot 3; per-boot files
175
- `{serve,smoke,bench-http,container,info}-n15.{2,3}.*`; boot 1's per-file artefacts were overwritten by boot 3 and survive
176
- only inside `validate-n15.run2.log`).
 
177
 
178
  | boot | caches (`~/.cache/tt-model/gr00t-n1.5-3b-p150/`) | host load | `tt-model serve` → READY | container boot (lifespan) | smoke | `bench_http.py` (10 warm-up + 50): device / server-side median (p90) | `tt-model stop` |
179
  |---|---|---|---|---|---|---|---|
180
- | 1 (**cold**) | `weights/` and `cache/` empty before; 6.4 GB + 411 MB after | two concurrent tt-metal image builds + CPU tests (load avg 16–110) | **1 min 43 s** (103.6 s wall) | 100.9 s: device open 4 s `Model built in 8.9 s ( cache path cold)` (checkpoint host plan `.tensorbin`) → trace capture **87.3 s** incl. the JIT of the four traces' kernels (`riscv-tt-elf-g++` lines in the container log)3 warm-ups 50.9 / 45.8 / 101.5 ms | **PASS** — PCC(actions) 0.999982, max\|d\| 0.0289; PCC(`action_pred_valid`) 0.999976; device 46.96 / 42.21 ms, total 54.42 / 48.21 ms | 44.27 (55.35) / 53.59 (91.12) ms; encode median 3.2, p90 41 ms; client wall 58.1 (94.5) | clean SIGTERM shutdown 4.9 s |
181
- | 2 (warm, immediately after) | reused | same loaded host | 20.2 s (21.3 s wall) | 15.1 s: `Model built in 0.6 s (… cache path warm)`, capture 6.6 s, warm-ups 47.3 / 119.4 / 65.3 ms (encode spikes = host contention) | **PASS** — same fidelity; device 50.88 / 42.59 ms, total 67.44 / 45.45 ms | 44.51 (55.76) / 58.14 (96.86) ms; encode median 5.8, p90 38 ms; client wall 64.4 (104.2) | clean 3.1 s |
182
- | 3 (warm, quiet host) | reused | load avg ≈ 9, no other device or build work | **9.7 s** (10.0 s wall) | 7.6 s: model 0.6 s, capture 3.2 s, warm-ups 44.5 → 44.3 ms | **PASS** — same fidelity; device 42.99 / 42.19 ms, total 47.31 / 44.95 ms, client wall 50 ms | **42.43 (43.18) / 45.40 (49.20) ms**, min 41.66 / 44.46; decode 1.06, encode 1.63, decode_actions 0.14; client wall 47.6 (51.8) | clean 2.0 s |
183
-
184
- Fidelity is identical in every boot and identical to the host runs of §2 — the served `actions` JSON of the container is
185
- byte-for-byte the host run's (`smoke-n15.3.json` vs `smoke_n15_snapshot_20260914-235713.json`) — and equal to the
186
- port's device test `tests/tt/results/test_e2e_predict_actions_vs_golden_n15_20260914-230743.json` to the printed precision:
187
- `left_arm` 0.99995 / 0.0214, `right_arm` 0.99997 / 0.0172, `left_hand` 0.94335 / 0.0179 (abs-gated), `right_hand`
188
- 0.99999 / 0.0289. The boot-3 benchmark is the card's "served" row; the device median 42.4 ms is 0.7 ms above the port's
189
- own `bench_e2e` device figure (41.68 ms: upload + traces + readback) and the loaded-host runs show that the extra
190
- latency and the p90 tails sit in the host-side stages (`encode`), not on the device.
191
-
192
- `GET /info` inside the container reports `weights.snapshot_dir = /hf/hub/models--nvidia--GR00T-N1.5-3B/snapshots/869830fc…`
193
- (the mounted HF cache), `source.tt_metal = {commit 668c290…, ttnn_dist_version 0.65.2.dev10011}`,
194
- `source.port_snapshot_commit = 5dc0517ee3b…`, `device_facts` grid 11×10 / DRAM 8×1 (`info-n15.3.json`).
195
-
196
- **Device-sharing note for this host.** The first validation attempt (00:32–00:40 KST) ran while another session's
197
- container was still holding the card after that session's lock hold had ended; `bin/with-device.sh`'s probe hung and
198
- reset the card twice, then refused to run. Since then every device entry goes through
199
- `publish/scripts/run_validation_n15_when_free.sh`, which waits until `docker ps` shows no `tt-model-*` container and the
200
- lock is free. Use the manifest path with `tt-model stop` / `logs` for an un-pushed package (`tt-model stop
 
 
201
  changh95/GR00T-N1.5-3B-p150` only works after `pull`).
202
 
203
  ## 5. Request / response contract
@@ -205,7 +231,7 @@ changh95/GR00T-N1.5-3B-p150` only works after `pull`).
205
  | route | returns |
206
  |---|---|
207
  | `GET /health` | `{"status": "ok" \| "starting", "model": "GR00T-N1.5-3B-p150", "device": "blackhole:0"}` — 200 always; `ok` only after the warm-up |
208
- | `GET /info` | model / version / task / hardware / `stage` (`stage1-ttnn-traces` + megakernel note) / `weights` (repo, revision, snapshot dir actually loaded) / `source` (port snapshot commit, server version, tt-metal commit, ttnn dist version) / `inputs` (the full observation contract) / `outputs` / `noise.default_seed` / `limits` / `device_facts` (11×10 grid, 8 DRAM banks) / `warmup_latency_ms` / `warmup_fidelity_vs_golden` / `license` |
209
  | `GET /v1/models` | OpenAI-shaped stub `{"object": "list", "data": [{"id": "nvidia/GR00T-N1.5-3B", "object": "model", "owned_by": "changh95"}]}` so the tt-model ready card / `tt-model curl` do not 404; this is not a chat API |
210
  | `GET /demo` | `{"request": <ready-to-post demo body>, "expected": <fp32 reference actions + gates>}` |
211
  | `POST /predict` | one observation → one 16-step action chunk (below) |
@@ -231,45 +257,67 @@ Response (200):
231
  "normalized": false, "embodiment": "gr1", "embodiment_id": 24, "version": "n15", "model": "GR00T-N1.5-3B-p150", "layout": "gr1",
232
  "images": {"ego_view": {"frames": 1, "received_hw": [256, 256]}}, "seq_len": 296, "prompt_tokens": 14, "state_dtype": "float64",
233
  "noise_source": "seed" | "client", "seed": 0 | null,
234
- "timing_ms": {"decode": 1.5, "encode": 2.6, "device": 43.0, "decode_actions": 0.1, "total": 47.2}}
235
  ```
236
 
237
  `actions` are physical joint targets (radians) — `(y + 1) / 2 · (max − min) + min` with the checkpoint's
238
  `experiment_cfg/metadata.json` GR1 statistics — exactly what `Gr00tPolicy.get_action` returns. `timing_ms`: `decode` =
239
  base64 + image decode + validation; `encode` = the port's host preprocessing; `device` = `predict_normalized` (static-shape
240
- asserts, input writes, 4 × `execute_trace`, one blocking readback); `decode_actions` = un-normalisation; `total` = the
241
- whole handler. Errors: **400** structural problems (`{"detail": {"errors": [...], "inputs": {...}}}`), undecodable image,
242
- bad state / instruction / noise, other embodiment; **503** while starting; **500** `Type: text` on a device failure.
243
- Handlers are synchronous and serialised on one lock; batch 1.
244
 
245
  ## 6. Environment (read in the lifespan, never at import)
246
 
247
  | variable | container value (`serve.env`) | meaning |
248
  |---|---|---|
249
  | `HF_MODEL` / `TT_WEIGHTS_REVISION` | `nvidia/GR00T-N1.5-3B` (launcher) / `869830fc…` | must be the version's pinned repo and sha (both are asserted against the port's own `configs.HF_REPOS` / `HF_SNAPSHOT_SHAS`) |
250
- | `GR00T_VERSION` | `n15` | the server code is shared by both GR00T packages and never guesses |
251
  | `GR00T_EMBODIMENT` / `GR00T_LAYOUT` | `gr1` / `gr1` | the only device-validated layout of this release |
252
  | `GR00T_POLICY` / `GR00T_TRACE_LAYOUT` | `mixed_dit` / `per_stage` | `TTPolicy` knobs; `bf16` / `mixed` and `two` exist in the port but are not the benchmarked defaults |
253
- | `GR00T_TRACE_REGION_SIZE` / `GR00T_L1_SMALL_SIZE` | default 67108864 / 32768 | the port's validated `open_gr00t_device` values |
 
 
 
 
254
  | `GR00T_TOKENIZER_DIR_N15` | `/opt/tt-metal/gr00t_p150/assets/tokenizer/n15` | vendored Qwen2 tokenizer (not in the weights repo); `GR00T_TOKENIZER_DIR` overrides |
255
  | `GR00T_TT_CACHE` | `/weight-cache/gr00t-tt` | host `.pt` plan tier + `.tensorbin` device tier (persisted under `~/.cache/tt-model/gr00t-n1.5-3b-p150/weights`) |
256
  | `GR00T_PROJECT_ROOT` / `GR00T_GOLDEN_ROOT` | `/nonexistent` | the port's dev-box defaults; nothing on the serve path needs them |
257
- | `GR00T_WARMUP_RUNS` / `GR00T_WARMUP_PCC_MIN` | `3` / default 0.999 | traced predicts before READY; boot fails below the PCC floor vs the shipped golden |
258
  | `GR00T_DEFAULT_SEED`, `GR00T_TORCH_THREADS`, `GR00T_WEIGHTS_DIR` | unset | request default seed (0), torch intra-op threads, offline snapshot dir |
259
  | `TT_MESH_SHAPE`, `TT_DEVICE_ID`, `TT_METAL_VISIBLE_DEVICES` | `1x1` (launcher), `0`, `0` | one chip; any other mesh shape is refused |
260
 
261
  ## 7. Caveats
262
 
263
- - **Stage-1 path only.** TTNN ops in Metal traces; the persistent-megakernel denoise (K1 streaming kernel: 464 GB/s bf16 /
264
- 414 GB/s bfp8, `code/models/experimental/gr00t/tests/tt/results/mk_k1_summary.md`) is not integrated into `Gr00tTT` and not served.
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  - **Shape-locked traces.** One embodiment (`gr1`), one camera, one frame, ≤ 102 text tokens (LLM S padded to 384), batch 1.
266
  Other embodiments / cameras / longer instructions → 400 before anything reaches the device. Concurrent requests queue on one lock.
267
  - **Deterministic noise.** Actions are a deterministic function of (observation, seed | noise); the reference noise of the
268
- demo is not seed-reproducible, so the smoke test posts it as `noise`. Same observation + same noise → bit-identical actions.
 
269
  - **Base checkpoint.** Outputs are the GR1 sim data-config actions of the released N1.5 checkpoint; a real robot needs NVIDIA's post-training.
270
  - **First container boot is cold**: the port converts the 5.4 GB checkpoint into its `.pt` plan tier and the `.tensorbin`
271
- device tier under `/weight-cache/gr00t-tt` and JITs the kernels of the four traces into `/cache`; both persist under
272
- `~/.cache/tt-model/gr00t-n1.5-3b-p150/`, so later boots are seconds (measured values in §4).
 
 
 
 
273
  - **Python 3.12 image vs 3.10 tree venv.** Host runs are an approximation; the image is authoritative (its `verify.sh`
274
  imports the app and the port with no device; the served smoke in §4 is the proof).
275
  - **Weights licence.** `nvidia/GR00T-N1.5-3B` is under the NVIDIA License (non-commercial: research or evaluation use);
 
3
  This repo is a **tt-model container package source**: `tt-model.yaml` + the code under `code/`. `tt-model package
4
  --container` builds the OCI image (tt-metal `668c2907575` built from source + the port + the HTTP stack),
5
  `tt-model serve` runs it against the card, `tt-model push` publishes it to this HF repo. The server is
6
+ `code/gr00t_p150/server/app.py` (FastAPI, `kind: tt-dit-server`); what it runs on the device is the port's **Stage-2**
7
+ path: the vision tower, the LLM and the VL adapter as TTNN ops captured once into three Metal traces (`vision`, `llm`,
8
+ `adapter`) and, in the fourth trace (`denoise`), the whole DiT flow-matching head — action encoder, 16 DiT blocks,
9
+ decoder, 4 Euler steps — as **one persistent megakernel `generic_op`** (`TTPolicy.dit_backend="megakernel"`, bfp8_b
10
+ weight arena in DRAM; the port's default since 2026-09-18). `GR00T_DIT_BACKEND=ttnn` serves the Stage-1 TTNN denoise
11
+ (the 2026-09-14 release path) from the same image; both are reported by `GET /info -> stage`.
12
 
13
  | | |
14
  |---|---|
15
  | tt-metal tree the image is built from | `/home/deepgadget/experiments/gr00t/publish/tt-metal-668c2907575` — a clean local clone of tt-metal `main` @ `668c290757550588d0ce46b180c344a462a2aaf5` (`v0.79.0-dev20260914-1`), submodules populated, `git status` empty (the working clone `/home/deepgadget/experiments/gr00t/tt-metal` at the same commit has untracked dirs and would be recorded as dirty); torch pin 2.11.0 |
16
+ | port code shipped | `code/models/experimental/gr00t/` = tt-metal ref `gr00t-p150-snapshot-mk`, commit `009de892c895917ad6abe9a6c36518f00316ed4b` (parent `668c290`; frozen 2026-09-18T04:45 KST; megakernel tree `tt/megakernel/**` = the K4b-final checkpoint, sha256 listing `tests/tt/results/mk_tree_20260918-041439.sha256`, digest `7df61c35e9dc7857`), minus `__pycache__`, `.omc` tool state, the port's `generated/` runtime output and all but the 50 N1.5 result JSONs this repo cites (`tests/tt/results/`: the 13 `*_summary.md` + the K4b-checkpoint chain of 2026-09-18 04:08–04:37 KST + the 4 snapshot-validation JSONs; `benchmarks/results/`: 8 JSONs, §1) |
17
  | weights | `nvidia/GR00T-N1.5-3B` @ `869830fc749c35f34771aa5209f923ac57e4564e`: `model-0000{1,2,3}-of-00003.safetensors` (5,448,327,040 B of bf16 tensors; 5,448,460,016 B on disk in 3 files), `model.safetensors.index.json`, `config.json`, `experiment_cfg/metadata.json`, `LICENSE` — a pinned pointer, never in the image |
18
+ | app | `gr00t_p150.server.app:app` (uvicorn `--lifespan on`; the lifespan does weights → tokenizer → policy → device → model → trace capture → warm-up → READY) |
19
+ | device recipe | `models.experimental.gr00t.tt.model.open_model_device(policy, num_command_queues=2, trace_region_size=64 MiB, l1_small_size=32768, device_id=0)`: under the megakernel policy the worker L1 is opened 64 KiB smaller (`worker_l1_size` 1,461,248 − 65,536 = **1,395,712 B**; the kernel-config ring of the megakernel binaries, 136,192 B) and the VL adapter's intermediates are placed in DRAM; asserts the p150a's 11×10 compute grid and 8 DRAM banks; `TTPolicy(dtype_policy="mixed_dit", dit_backend="megakernel", mk_arena_dtype="bfp8_b", trace_layout="per_stage")` |
20
  | serving shape | embodiment `gr1` (slot 24), layout `gr1`: 1 camera (`ego_view`) × 1 frame × 256 image tokens, ≤ 102 text tokens, LLM sequence padded to 384, state 7/7/6/6 → 16 × [7, 7, 6, 6] actions, batch 1 |
21
 
22
  Below, `$PUB=/home/deepgadget/experiments/gr00t/publish` (this repo is `$PUB/GR00T-N1.5-3B-p150`),
 
28
  ```
29
  tt-model.yaml authoring manifest (schema 5.1) -- the whole build/serve interface, incl. the model card text
30
  SERVING.md this file
31
+ GPU_COMPARISON.md RTX 5090 vs p150a (megakernel and Stage-1 rows), matching definitions and caveats
32
  LICENSE the NVIDIA License (weights); LICENSE-NOTICE.md says what is under which licence
33
  media/demo_ego_view.png the demo frame (256x256, GR1 sim robot_sim.PickNPlace traj 0 / step 100)
34
  media/demo_observation.json the matching raw joint state + instruction (request body minus the image)
35
+ media/demo_actions.png served actions vs the fp32 reference for that request (rendered from this image's smoke response)
36
  code/models/experimental/gr00t/ the tt-nn port (tt-metal tree layout): common/ (host pipeline: preprocessing, prompts,
37
+ normalisation, checkpoint, weight plan), tt/ (device model, layers, traces; tt/megakernel/:
38
+ arena, core map, descriptors, dit_program + kernels/dit_kernel.cpp and ops/*.hpp),
39
  reference/ (fp32 torch reference), tests/, benchmarks/
40
+ code/models/{common,tt_dit,demos}/ the 20 tt-metal files the port needs from the tree (source.code in tt-model.yaml): 16 Python
41
+ modules + the 4 deepseek_v3_b1/unified_kernels/*.hpp headers the megakernel's kernel source includes
42
  code/gr00t_p150/ the policy server: server/{app,schemas,smoke_test}.py, demo/n15/ (canonical demo
43
  observation + golden actions + noise), assets/tokenizer/n15/ (vendored Qwen2 tokenizer)
44
  code/scripts/ download_weights.sh, bench_http.py
 
49
  The port keeps its tt-metal-tree absolute imports (`models.experimental.gr00t.*`, `models.tt_dit.*`,
50
  `models.common.*`): in the image the code lands at `/opt/tt-metal/<path>` with `PYTHONPATH=/opt/tt-metal`, and the
51
  image's tt-metal copy excludes `models/`, so everything under `models/` comes from this repo's `code/` (the port) and
52
+ `source.code` (the 20 tree files). The megakernel's kernel source is JIT-compiled at boot from
53
+ `/opt/tt-metal/models/experimental/gr00t/tt/megakernel/kernels/dit_kernel.cpp`; it includes four headers of the
54
+ tree's `models/demos/deepseek_v3_b1/unified_kernels/` (`kernel_op_api`, `kernel_utils`, `dataflow_utils`, `mcast`),
55
+ which is why those are in `source.code` (their own includes live under `tt_metal/`, which the image ships). The server
56
+ package `gr00t_p150` is shared with `changh95/GR00T-N1.6-3B-p150` and `changh95/GR00T-N1.7-3B-p150`; this repo ships
57
+ only the N1.5 assets (`demo/n15`, `assets/tokenizer/n15`).
58
+
59
+ Result JSONs shipped under `code/models/experimental/gr00t/benchmarks/results/` (the card's numbers ship with the code):
60
+ `bench_e2e_n15_20260918-050550.json` (the snapshot's own megakernel benchmark, `commit 009de892…`, the card's port row),
61
+ `bench_e2e_n15_20260918-043024.json` (the same benchmark on the live tree at the K4b checkpoint), `bench_e2e_n15_20260918-032243.json`
62
+ (the same tree with `--dit-backend ttnn`: the Stage-1 reference the −16.8 % is measured against), `bench_mk_step_n15_20260918-035610.json`
63
+ (the megakernel alone, 20 traced replays), `bench_load_n15_20260918-042919.json` (start-up cost with / without the ttnn DiT
64
+ weights), and the 2026-09-14 Stage-1 documents `e2e_stage1_n15.json`, `denoise_stage1_n15.json`, `stage_ops_n15.json`
65
+ (the previous release's card).
66
 
67
  ## 2. Run on the HOST for validation (no Docker)
68
 
 
80
  export TT_METAL_CACHE=$HOME/.cache/tt-metal-cache-gr00t # warm JIT kernels of the port's device sessions
81
  export HF_MODEL=nvidia/GR00T-N1.5-3B TT_WEIGHTS_REVISION=869830fc749c35f34771aa5209f923ac57e4564e
82
  export GR00T_VERSION=n15 TT_MESH_SHAPE=1x1 TT_DEVICE_ID=0 TT_METAL_VISIBLE_DEVICES=0
83
+ export GR00T_DIT_BACKEND=megakernel GR00T_MK_ARENA_DTYPE=bfp8_b GR00T_NUM_CQS=2 # = the image's serve.env (= the port's defaults)
84
  export GR00T_TOKENIZER_DIR_N15=$PWD/code/gr00t_p150/assets/tokenizer/n15 # optional: the server picks the vendored dir itself
85
+ export GR00T_TT_CACHE=$HOME/.cache/gr00t-tt # host .pt plan tier + .tensorbin device tier (~2.5 GB under the megakernel for N1.5; 6.4 GB on the Stage-1 path)
86
 
87
  # import check, no device (what the image's verify.sh does):
88
  $TREE/python_env/bin/python -c "import gr00t_p150.server.app as a, sys; assert a.app and 'ttnn' not in sys.modules; print('ok')"
 
95
  ```
96
 
97
  Boot log landmarks (they drive `tt-model serve`'s checklist): `Loading weights: nvidia/GR00T-N1.5-3B @ 869830fc…` →
98
+ `Tokenizer files: …` → `Opening device 0 (trace_region_size=67108864, l1_small_size=32768, num_command_queues=2,
99
+ dit_backend=megakernel, mk_arena_dtype=bfp8_b)` → `Loading pipeline: Gr00tTT.from_pretrained(n15, …)` → `Model built in
100
+ N s (weights N s, 499 tensors, 2652 MB on device, cache path warm|cold; dit_backend megakernel, arena bfp8_b, adapter_mem
101
+ DRAM, head {mk_arena_plan_pack_s, mk_upload_s, …}; ttnn DiT weights skipped: 279 tensors / 524 MB)` → `Warming up: capture
102
+ per_stage traces on the n15 demo observation, then N traced predict(s)` `Warmup k/N: ms (encode, device, decode)` →
103
+ `Warm-up fidelity vs golden fp32: actions PCC …` `Warmup complete (capture s, first ms, steady ms; boot s) -- per_stage
104
+ traces ['vision', 'llm', 'adapter', 'denoise']` uvicorn `Application startup complete`. Any failure raises and uvicorn
105
+ exits non-zero (no CPU fallback); the boot also **fails** if the warm-up actions' PCC vs the shipped golden is below
106
+ `GR00T_WARMUP_PCC_MIN` (0.999). SIGTERM / Ctrl-C: `Releasing traces and device tensors` → `Closing device`.
107
+
108
+ Under the megakernel the ttnn DiT / precompute / encoder tensors are not uploaded (499 device tensors, 2,652 MB, instead
109
+ of the Stage-1 path's 778 / 3,176 MB; `bench_load_n15_20260918-042919.json`); the bfp8_b weight arena the kernel streams
110
+ from (16 blocks, 496 MB per step) is packed on the host and uploaded once at model build (`mk_arena_plan_pack_s` ≈ 0.9 s,
111
+ `mk_upload_s` 1.0 s in the log line).
112
+
113
+ Measured host boot of the megakernel path with a **fresh, empty `TT_METAL_CACHE`** (every kernel JIT-compiled, incl. the
114
+ megakernel; warm `~/.cache/gr00t-tt` weight tiers; `GR00T_WARMUP_RUNS=5`; `$PUB/logs/server_n15_20260918-043232.log`,
115
+ `tests/tt/results/mk_default_summary.md` §7): `Model built in 2.5 s` (weights 0.4 s), `warm_and_capture` 31.4 s, boot to
116
+ READY **37.6 s**, 5 traced warm-ups 33.8 → 33.8 ms. Kernels are rebuilt once for a new `TT_METAL_HOME` path.
117
+
118
+ Expected smoke line (host, megakernel default, same log stamp; `$PUB/logs/smoke_n15_20260918-043232.log`):
119
 
120
  ```
121
+ info: … stage=stage2-megakernel dit_backend=megakernel arena=bfp8_b cqs=2
122
+ PASS GR00T-N1.5-3B-p150 n15: PCC(actions)=0.999980 max|d|=0.0296 PCC(action_pred_valid)=0.999976 repeat_maxdiff=0.0e+00
123
+ seed_path_ok device_ms=33.03/32.64 total_ms=37.02/35.0 wall_ms=40
124
+ per_group[left_arm=0.99994/0.0229 right_arm=0.99997/0.0172 left_hand=0.92834/0.0296 right_hand=0.99999/0.0289]
125
  ```
126
 
127
+ The per-group PCC / max|d| equal the port's own device test on the frozen snapshot
128
+ (`code/models/experimental/gr00t/tests/tt/results/test_e2e_predict_actions_vs_golden_n15_20260918-044904.json`, `commit
129
+ 009de892…`) to the printed precision; the traced policy is deterministic, so every run reproduces them bit-exactly. The
130
+ Stage-1 host smoke of the 2026-09-14 release (`smoke_n15_snapshot_20260914-235713.log`: PCC(actions) 0.999982, max|d|
131
+ 0.0289, device 42.95 / 42.16 ms) is the reference the megakernel figures are compared with in `GPU_COMPARISON.md`.
 
132
 
133
  Offline overrides: `GR00T_WEIGHTS_DIR=<dir with the snapshot files>` (shimmed into a private hub cache so the port's
134
  `snapshot_dir()` resolves to it), `GR00T_TOKENIZER_DIR=<dir>`.
 
144
  uv pip install --python $PUB/depcheck-n15/bin/python --dry-run torch==2.11.0 fastapi uvicorn "pydantic>=2" pillow "numpy>=1.24.4,<2" \
145
  safetensors huggingface_hub "transformers==5.12.1" "opencv-python-headless==4.8.1.78" "torchvision==0.26.0" pytest \
146
  --extra-index-url https://download.pytorch.org/whl/cpu --index-strategy unsafe-best-match
147
+ # -> torch 2.11.0+cpu, torchvision 0.26.0+cpu, numpy 1.26.4, transformers 5.12.1, opencv-python-headless 4.8.1.78 (cp312 wheel), pytest 9.1.1
148
  ```
149
 
150
  ## 3. Package, serve, push (Docker)
 
175
 
176
  `serve` pre-downloads the pinned files (a cache hit on this host), then `docker run --user 0:0 --device
177
  /dev/tenstorrent --ipc host` with `/dev/hugepages-1G`, `~/.cache/huggingface` at `/hf` (rw, `HF_HOME=/hf`) and
178
+ `~/.cache/tt-model/gr00t-n1.5-3b-p150/{cache,weights,tensors}` at `/cache` (`TT_METAL_CACHE`, JIT kernels incl. the
179
+ megakernel), `/weight-cache` (`TT_DIT_CACHE_DIR`; the port's weight tiers go to `/weight-cache/gr00t-tt`, ~2.5 GB under the megakernel, 6.4 GB on the Stage-1 path) and
180
  `/tensor-cache`; it exports exactly `HF_MODEL=nvidia/GR00T-N1.5-3B`, `MESH_DEVICE=P150`, `TT_MESH_SHAPE=1x1` plus
181
  `serve.env` from `tt-model.yaml`, publishes the first free port from 20000 and waits ≤ 1800 s for `Application startup
182
  complete`. tt-cli users: `tt serve changh95/GR00T-N1.5-3B-p150` / `tt model stop changh95/GR00T-N1.5-3B-p150`
 
189
 
190
  ## 4. Container validation (the image built from this manifest)
191
 
192
+ Image `tt-model/gr00t-n1.5-3b-p150:dfb9c3889ab1` (`sha256:dfb9c3889ab17cc1d8187085b27e854d0311fb974d729efe88db9bfdce51869f`, 3.2 GB (3,218,078,767 B in 46 content-addressed blobs)), built 2026-09-18 05:23–05:28 KST (`built.created_at` 2026-09-17T20:23:50+00:00) from the manifest above
193
+ (`tt-model package`: tt-metal C++ build with a warm ccache, Python 3.12 venv, all 25 `verify:` lines + the launcher's own passed
194
+ inside the image; 4 min 22 s with the two sibling GR00T builds sharing the host in total; `publish/logs/pkg-mk-n15.log`). Validated on the p150a on 2026-09-18 05:32–05:35 KST: two `tt-model serve`
195
+ boots of this exact image inside **one** device-lock hold (`publish/scripts/validate_image_mk_n15.sh` via
196
+ `run_validation_mk_n15_when_free.sh`, which enters the lock only when no `tt-model-*` container and no other process holds the
197
+ card), each followed by `smoke_test.py`, a 50-request `bench_http.py` and `tt-model stop` by manifest path; per-boot files
198
+ `publish/logs/{serve,container,info,smoke,bench-http}-mk-n15-c{1,2}-20260918-053217.*`, driver log `validate-mk-n15-20260918-053217.log`.
199
+ Before boot 1 the package's `~/.cache/tt-model/gr00t-n1.5-3b-p150/{cache,weights}` of the Stage-1 release were moved aside, so
200
+ boot 1 is the true cold boot of a fresh install (checkpoint → weight tiers, JIT of every kernel incl. the megakernel).
201
 
202
  | boot | caches (`~/.cache/tt-model/gr00t-n1.5-3b-p150/`) | host load | `tt-model serve` → READY | container boot (lifespan) | smoke | `bench_http.py` (10 warm-up + 50): device / server-side median (p90) | `tt-model stop` |
203
  |---|---|---|---|---|---|---|---|
204
+ | 1 (**cold**) | `weights/` and `cache/` empty before; 343 MB `cache/` + 2.5 GB `weights/` after | load avg 7.8 at start, 31 at the end (two sibling image builds on the host) | **2 min 14 s (134.7 s wall: container start 31.6 s + lifespan)** | 97.7 s: `Model built in 29.9 s (weights 12.8 s, arena plan + pack 14.3 s; cache path cold)` → trace capture incl. the JIT of every kernel and the megakernel 55.7 s 5 warm-ups 34.2 33.6 ms | **PASS** — PCC(actions) 0.999980, max\|d\| 0.0296; PCC(`action_pred_valid`) 0.999976; device 33.17 / 33.10 ms, total 36.90 / 35.56 ms | 32.59 (32.83) / 34.59 (35.05) ms; client wall 36.6 (37.2) | clean, 2.0 s |
205
+ | 2 (warm) | reused | load avg 31 (same builds) | **11.2 s** | 9.0 s: `Model built in 2.6 s (… cache path warm)`, capture 2.9 s, warm-ups 33.6 33.4 ms | **PASS** — same fidelity; device 33.51 / 33.14 ms, total 37.21 / 35.52 ms | **32.52 (32.82) / 34.62 (35.39) ms**, min 32.29 / 34.09; decode 1.02, encode 0.90, decode_actions 0.10; client wall 36.3 (37.3) | clean, 2.0 s |
206
+
207
+ Fidelity is identical in both boots and identical to the host run of §2 — the served `actions` JSON of the container is
208
+ byte-for-byte the host run's (`smoke-mk-n15-c2-20260918-053217.json` vs `smoke_n15_20260918-043232.json`) and equal to the
209
+ port's device test on the frozen snapshot `tests/tt/results/test_e2e_predict_actions_vs_golden_n15_20260918-044904.json` to
210
+ the printed precision: `left_arm` 0.99994 / 0.0229, `right_arm` 0.99997 / 0.0172, `left_hand` 0.92834 / 0.0296
211
+ (abs-gated), `right_hand` 0.99999 / 0.0289. The boot-2 benchmark is the card's "served" row; its device median is
212
+ 0.1 ms above the port's own `bench_e2e` device figure on the snapshot (32.41 ms: upload + traces + readback,
213
+ `bench_e2e_n15_20260918-050550.json`). The Stage-1 image of 2026-09-14 served 42.4 ms device / 45.4 ms server-side on
214
+ the same host (`build-n15.md` §5); the megakernel image serves 9.9 ms less device time per request.
215
+
216
+ `GET /info` inside the container reports `stage = {served: stage2-megakernel, dit_backend: megakernel, mk_arena_dtype:
217
+ bfp8_b, num_command_queues: 2, cq1_uploads: true, worker_l1_size: 1395712, adapter_mem: DRAM, weights: {499 tensors,
218
+ 2652.2 MB, skipped_ttnn_dit: {279, 523.9 MB}}, head_timing_s}`, `weights.snapshot_dir = /hf/hub/models--nvidia--GR00T-N1.5-3B/snapshots/869830fc…`
219
+ (the mounted HF cache), `source.tt_metal = {commit 668c290…}`, `source.port_snapshot_commit`, `device_facts` grid 11×10 /
220
+ DRAM 8×1 (`info-mk-n15-c2-20260918-053217.json`).
221
+
222
+ **Device-sharing note for this host.** The card is shared with other sessions; every device entry goes through
223
+ `bin/with-device.sh` (flock + a ttnn probe) and, for containers, through `publish/scripts/run_validation_mk_n15_when_free.sh`,
224
+ which waits until `docker ps` shows no `tt-model-*` container, `fuser /dev/tenstorrent/0` is empty and the lock is free
225
+ (a container left running after its lock hold ends would otherwise be reset by the next caller's probe — `build-n15.md`
226
+ §5.0). Use the manifest path with `tt-model stop` / `logs` for an un-pushed package (`tt-model stop
227
  changh95/GR00T-N1.5-3B-p150` only works after `pull`).
228
 
229
  ## 5. Request / response contract
 
231
  | route | returns |
232
  |---|---|
233
  | `GET /health` | `{"status": "ok" \| "starting", "model": "GR00T-N1.5-3B-p150", "device": "blackhole:0"}` — 200 always; `ok` only after the warm-up |
234
+ | `GET /info` | model / version / task / hardware / `stage` (`served`: `stage2-megakernel` \| `stage1-ttnn-traces`, `dit_backend`, `mk_arena_dtype`, `num_command_queues`, `cq1_uploads`, `worker_l1_size`, `adapter_mem`, `weights` incl. the skipped ttnn DiT set, `head_timing_s`, a note) / `policy` (the resolved `TTPolicy`) / `weights` (repo, revision, snapshot dir actually loaded) / `source` (port snapshot commit, server version, tt-metal commit, ttnn dist version) / `inputs` (the full observation contract) / `outputs` / `noise.default_seed` / `limits` / `device_facts` (11×10 grid, 8 DRAM banks) / `warmup_latency_ms` / `warmup_fidelity_vs_golden` / `license` |
235
  | `GET /v1/models` | OpenAI-shaped stub `{"object": "list", "data": [{"id": "nvidia/GR00T-N1.5-3B", "object": "model", "owned_by": "changh95"}]}` so the tt-model ready card / `tt-model curl` do not 404; this is not a chat API |
236
  | `GET /demo` | `{"request": <ready-to-post demo body>, "expected": <fp32 reference actions + gates>}` |
237
  | `POST /predict` | one observation → one 16-step action chunk (below) |
 
257
  "normalized": false, "embodiment": "gr1", "embodiment_id": 24, "version": "n15", "model": "GR00T-N1.5-3B-p150", "layout": "gr1",
258
  "images": {"ego_view": {"frames": 1, "received_hw": [256, 256]}}, "seq_len": 296, "prompt_tokens": 14, "state_dtype": "float64",
259
  "noise_source": "seed" | "client", "seed": 0 | null,
260
+ "timing_ms": {"decode": 1.47, "encode": 2.02, "device": 33.51, "decode_actions": 0.15, "total": 37.21}}
261
  ```
262
 
263
  `actions` are physical joint targets (radians) — `(y + 1) / 2 · (max − min) + min` with the checkpoint's
264
  `experiment_cfg/metadata.json` GR1 statistics — exactly what `Gr00tPolicy.get_action` returns. `timing_ms`: `decode` =
265
  base64 + image decode + validation; `encode` = the port's host preprocessing; `device` = `predict_normalized` (static-shape
266
+ asserts, input writes on CQ 1, 4 × `execute_trace` the fourth is the megakernel —, one blocking readback);
267
+ `decode_actions` = un-normalisation; `total` = the whole handler. Errors: **400** structural problems (`{"detail":
268
+ {"errors": [...], "inputs": {...}}}`), undecodable image, bad state / instruction / noise, other embodiment; **503** while
269
+ starting; **500** `Type: text` on a device failure. Handlers are synchronous and serialised on one lock; batch 1.
270
 
271
  ## 6. Environment (read in the lifespan, never at import)
272
 
273
  | variable | container value (`serve.env`) | meaning |
274
  |---|---|---|
275
  | `HF_MODEL` / `TT_WEIGHTS_REVISION` | `nvidia/GR00T-N1.5-3B` (launcher) / `869830fc…` | must be the version's pinned repo and sha (both are asserted against the port's own `configs.HF_REPOS` / `HF_SNAPSHOT_SHAS`) |
276
+ | `GR00T_VERSION` | `n15` | the server code is shared by the three GR00T packages and never guesses |
277
  | `GR00T_EMBODIMENT` / `GR00T_LAYOUT` | `gr1` / `gr1` | the only device-validated layout of this release |
278
  | `GR00T_POLICY` / `GR00T_TRACE_LAYOUT` | `mixed_dit` / `per_stage` | `TTPolicy` knobs; `bf16` / `mixed` and `two` exist in the port but are not the benchmarked defaults |
279
+ | `GR00T_DIT_BACKEND` | `megakernel` (= the port's default, pinned) | `TTPolicy.dit_backend`: `megakernel` = the denoise trace is one persistent `generic_op` (Stage 2); `ttnn` = the Stage-1 sequence of TTNN ops (for A/B; the device is then opened with the firmware `worker_l1_size` and the adapter's intermediates in L1). Reported in `/info -> stage` |
280
+ | `GR00T_MK_ARENA_DTYPE` | `bfp8_b` (= what `auto` resolves to for n15) | dtype of the megakernel's DRAM weight arena: `auto` / `bf16` / `bfp8_b`; the bf16 arena is 2.66 ms per step instead of 2.22 (`tests/tt/results/mk_k4b_summary.md` §0) |
281
+ | `GR00T_NUM_CQS` | `2` | command queues of `open_model_device`: `2` = per-call input writes on CQ 1 overlap the trace replays (the benchmarked path; bit-equal to `1` per `tests/tt/results/test_mk_2cq_bit_equality_n15_20260918-041502.json`) |
282
+ | `GR00T_TRACE_REGION_SIZE` / `GR00T_L1_SMALL_SIZE` | default 67108864 / 32768 | the port's validated `open_model_device` values |
283
+ | `GR00T_MK_L1_CUT_KIB` (port) | unset (= 64) | the worker-L1 cut the megakernel's kernel-config ring needs; read by the port's `open_model_device`, not by the server. Changing it invalidates the validated configuration |
284
  | `GR00T_TOKENIZER_DIR_N15` | `/opt/tt-metal/gr00t_p150/assets/tokenizer/n15` | vendored Qwen2 tokenizer (not in the weights repo); `GR00T_TOKENIZER_DIR` overrides |
285
  | `GR00T_TT_CACHE` | `/weight-cache/gr00t-tt` | host `.pt` plan tier + `.tensorbin` device tier (persisted under `~/.cache/tt-model/gr00t-n1.5-3b-p150/weights`) |
286
  | `GR00T_PROJECT_ROOT` / `GR00T_GOLDEN_ROOT` | `/nonexistent` | the port's dev-box defaults; nothing on the serve path needs them |
287
+ | `GR00T_WARMUP_RUNS` / `GR00T_WARMUP_PCC_MIN` | `5` / `0.999` | traced predicts before READY; boot fails below the PCC floor vs the shipped golden |
288
  | `GR00T_DEFAULT_SEED`, `GR00T_TORCH_THREADS`, `GR00T_WEIGHTS_DIR` | unset | request default seed (0), torch intra-op threads, offline snapshot dir |
289
  | `TT_MESH_SHAPE`, `TT_DEVICE_ID`, `TT_METAL_VISIBLE_DEVICES` | `1x1` (launcher), `0`, `0` | one chip; any other mesh shape is refused |
290
 
291
  ## 7. Caveats
292
 
293
+ - **Stage-2 path.** The denoise is one persistent megakernel `generic_op` (K4b-final kernel: bfp8 weights streamed from a
294
+ DRAM arena once per step, 110 Tensix cores, dual-NoC hub multicast, distributed LayerNorm, per-head flash SDPA split over
295
+ three cores for the long-key cross blocks; `tests/tt/results/mk_k4b_summary.md`, `mk_k5_summary.md`, `mk_default_summary.md`).
296
+ Consequences: (1) the device is opened with a 64 KiB smaller worker L1 (`worker_l1_size` 1,395,712 B) so the kernel-config
297
+ ring holds the ≈ 131.7 KB compute-core program (4.5 KB of headroom under the 136,192 B ring: any kernel growth must be
298
+ re-checked), and the VL adapter's intermediates are placed in DRAM (+0.38 ms on the `adapter` trace: 4.86 vs 4.48 ms);
299
+ (2) the DiT residual-stream intermediates (`dit_block{i}_out`, `dit_out[k]`) are not observable from the served model —
300
+ the untraced tap set is the backbone / adapter taps + `action_decoder_out[k]`, `pred_velocity[k]` and the action rows
301
+ (30 rows instead of the Stage-1 path's 58); (3) the e2e gain over the Stage-1 denoise on the same tree is −16.8 %
302
+ (40.38 → 33.59 ms; denoise trace 16.02 → 8.93 ms, −44 %), not the plan's −30 %.
303
+ - **Fidelity vs the Stage-1 path.** Gate outcomes are equal on 8 of the 9 N1.5 observations (the demo and 7 of the 8 multi-sample
304
+ goldens); the megakernel's only extra miss is `unnormalized_actions.right_arm` on `t3_s300_seed2` at PCC 0.999869 vs
305
+ the 0.9999 gate (max|d| 0.029 rad within its 0.038 tolerance; recorded as a known marginal row with floor 0.9998 in
306
+ `tests/tt/test_mk_e2e.py`; `tests/tt/results/test_mk_e2e_backends_agree_t3_s300_seed2_n15_20260918-042413.json`). On the
307
+ demo observation megakernel vs TTNN `action_pred_normalized` PCC is 0.999997 (`…_canonical_n15_20260918-042408.json`).
308
  - **Shape-locked traces.** One embodiment (`gr1`), one camera, one frame, ≤ 102 text tokens (LLM S padded to 384), batch 1.
309
  Other embodiments / cameras / longer instructions → 400 before anything reaches the device. Concurrent requests queue on one lock.
310
  - **Deterministic noise.** Actions are a deterministic function of (observation, seed | noise); the reference noise of the
311
+ demo is not seed-reproducible, so the smoke test posts it as `noise`. Same observation + same noise → bit-identical actions;
312
+ traced == untraced and 2 CQs == 1 CQ bit for bit.
313
  - **Base checkpoint.** Outputs are the GR1 sim data-config actions of the released N1.5 checkpoint; a real robot needs NVIDIA's post-training.
314
  - **First container boot is cold**: the port converts the 5.4 GB checkpoint into its `.pt` plan tier and the `.tensorbin`
315
+ device tier under `/weight-cache/gr00t-tt` and JITs every kernel of the four traces — incl. the megakernel — into `/cache`;
316
+ both persist under `~/.cache/tt-model/gr00t-n1.5-3b-p150/`, so later boots are seconds (measured values in §4). A cache
317
+ directory left by the Stage-1 image is reused (its kernels stay, the megakernel is added).
318
+ - **One megakernel model per process.** Building a second megakernel `Gr00tTT` after releasing the first on the same open
319
+ device hung the card on N1.6 during development (`mk_default_summary.md` §9); the server builds exactly one model per
320
+ process and never rebuilds it.
321
  - **Python 3.12 image vs 3.10 tree venv.** Host runs are an approximation; the image is authoritative (its `verify.sh`
322
  imports the app and the port with no device; the served smoke in §4 is the proof).
323
  - **Weights licence.** `nvidia/GR00T-N1.5-3B` is under the NVIDIA License (non-commercial: research or evaluation use);
media/demo_actions.png CHANGED

Git LFS Details

  • SHA256: b84e80a26ce9ad40756ec49bbd8c16bef3202210358337ee89e3a39ee3703699
  • Pointer size: 131 Bytes
  • Size of remote file: 114 kB

Git LFS Details

  • SHA256: 2068d137808904213c694cb7385c69aea79c6ada2d6b937dd9ec0bcfcefdaa8d
  • Pointer size: 131 Bytes
  • Size of remote file: 115 kB
tt-model.yaml CHANGED
@@ -1,5 +1,8 @@
1
  # SPDX-License-Identifier: Apache-2.0
2
- # tt-model-manager container manifest (schema 5.1) for NVIDIA Isaac GR00T N1.5-3B (Stage-1 TTNN path) on Blackhole.
 
 
 
3
  #
4
  # Run EVERY tt-model command from this directory: `source.tt_metal` and `extra_code[].root: code`
5
  # resolve against the process CWD, not against this file. Rootless Docker on this box needs
@@ -38,7 +41,7 @@ source:
38
  # three submodules populated and `git status --porcelain` empty, so the image is built from exactly the commit the
39
  # card links to (the working clone has untracked dirs and would be recorded as a dirty tree).
40
  tt_metal: /home/deepgadget/experiments/gr00t/publish/tt-metal-668c2907575
41
- # tt-metal-relative files the port imports from the tree OTHER than itself, with their import closure
42
  # (the image's tt-metal copy excludes models/, so everything under models/ must be listed here or in extra_code).
43
  code:
44
  - models/common/utility_functions.py # models.tt_dit.utils.matmul imports is_blackhole (needs pytest!)
@@ -56,13 +59,22 @@ source:
56
  - models/tt_dit/parallel/config.py
57
  - models/tt_dit/parallel/manager.py
58
  - models/tt_dit/encoders/qwen3vl/vision_qwen3vl.py # tt/qwen3vl_vision.py (lazy; not on the N1.5 serve path)
59
- - models/demos/deepseek_v3_b1/unified_kernel_descriptor.py # tt/megakernel/descriptors.py (lazy; Stage-2 work, not served)
 
 
 
 
 
 
 
 
 
60
  # This repo's own code (lands at /opt/tt-metal/<path>; PYTHONPATH=/opt/tt-metal).
61
  extra_code:
62
  - root: code
63
  paths:
64
- - models # code/models/experimental/gr00t: the frozen snapshot of the port (tt-metal ref gr00t-p150-snapshot, 5dc0517ee3b)
65
- - gr00t_p150 # the policy server, its smoke test, the demo observation and the vendored Qwen2 tokenizer files
66
  - scripts # download_weights.sh, bench_http.py
67
  ubuntu: "22.04"
68
  python: "3.12"
@@ -95,35 +107,47 @@ serve:
95
  GR00T_LAYOUT: "gr1" # static layout: 1 image x 256 tokens, L_max 102 text tokens, LLM sequence padded to 384
96
  GR00T_POLICY: "mixed_dit" # TTPolicy.dtype_policy: DiT block matmul weights bfp8_b, everything else bf16 (validated default)
97
  GR00T_TRACE_LAYOUT: "per_stage" # four Metal traces vision / llm / adapter / denoise (the benchmarked layout)
 
 
 
98
  GR00T_TOKENIZER_DIR_N15: "/opt/tt-metal/gr00t_p150/assets/tokenizer/n15"
99
  GR00T_PROJECT_ROOT: "/nonexistent" # the port's dev-box default for project-relative paths; nothing on the serve path needs it
100
  GR00T_GOLDEN_ROOT: "/nonexistent" # tests only (defensive)
101
  GR00T_TT_CACHE: "/weight-cache/gr00t-tt" # host plan (.pt) + device-layout (.tensorbin) weight tiers -> ~/.cache/tt-model/gr00t-n1.5-3b-p150/weights
102
- GR00T_WARMUP_RUNS: "3" # traced predict() calls after trace capture, before READY
 
103
 
104
  # Build-time assertions, run INSIDE the finished image as uid 1000, no device, no weights, no HF_MODEL.
105
  verify:
106
  - "import gr00t_p150.server.app as a; assert a.app; import sys; assert 'ttnn' not in sys.modules, 'server import must be side-effect free'"
107
  - "import models.experimental.gr00t.tt.model as m; assert m.Gr00tTT"
108
  - "from models.experimental.gr00t.common import preprocessing, normalization, prompts, checkpoint, weights, configs; assert configs.HF_SNAPSHOT_SHAS['n15'] == '869830fc749c35f34771aa5209f923ac57e4564e'"
 
 
 
 
 
 
 
109
  - "import pytest, models.common.utility_functions; import models.tt_dit.utils.matmul as mm; assert mm.get_matmul_config"
110
  - "import models.demos.deepseek_v3_b1.unified_kernel_descriptor as u; assert u.UnifiedKernelDescriptor"
111
  - "import models.tt_dit.encoders.qwen3vl.vision_qwen3vl"
112
- - "import models.experimental.gr00t.tt.layers, models.experimental.gr00t.tt.qwen3vl_vision, models.experimental.gr00t.tt.megakernel.descriptors"
 
113
  - "import safetensors, huggingface_hub, cv2, numpy, PIL; assert int(numpy.__version__.split('.')[0]) < 2, numpy.__version__"
114
  - "import transformers; assert transformers.__version__ == '5.12.1', transformers.__version__"
115
  - "import torchvision; assert torchvision.__version__.startswith('0.26.0'), torchvision.__version__"
116
  - "from pathlib import Path; d = Path('/opt/tt-metal/gr00t_p150/assets/tokenizer/n15'); assert all((d / f).is_file() for f in ('vocab.json', 'merges.txt', 'tokenizer_config.json', 'special_tokens_map.json', 'added_tokens.json')), sorted(p.name for p in d.iterdir())"
117
  - "import os; os.environ['GR00T_TOKENIZER_DIR_N15'] = '/opt/tt-metal/gr00t_p150/assets/tokenizer/n15'; from models.experimental.gr00t.common.prompts import get_tokenizer; t = get_tokenizer('n15'); assert t.convert_tokens_to_ids('<IMG_CONTEXT>') == 151669"
118
- - "from pathlib import Path; assert Path('/opt/tt-metal/models/experimental/gr00t/common/golden_tap_map.json').is_file()"
119
- - "from pathlib import Path; assert Path('/opt/tt-metal/gr00t_p150/server/smoke_test.py').is_file() and Path('/opt/tt-metal/scripts/bench_http.py').is_file()"
120
  - "from pathlib import Path; d = Path('/opt/tt-metal/gr00t_p150/demo/n15'); assert all((d / f).is_file() for f in ('observation.json', 'noise.json', 'expected.json', 'frames/ego_view_t0.png')), sorted(str(p) for p in d.rglob('*'))"
121
- - "import ttnn; assert all(hasattr(ttnn, a) for a in ('begin_trace_capture', 'end_trace_capture', 'execute_trace', 'release_trace', 'copy_host_to_device_tensor', 'MinimalMatmulConfig'))"
122
  - "from gr00t_p150.server.app import parse_mesh_shape as p; assert p('1x1') == p('(1, 1)') == p('1,1') == (1, 1)"
123
 
124
  card:
125
  description: |
126
- NVIDIA Isaac GR00T N1.5 (3B vision-language-action policy) running on one Tenstorrent Blackhole p150a via tt-nn: one camera frame, proprioceptive state and a task instruction in, a 16-step chunk of GR1 arm and hand joint targets out. Stage-1 release: TTNN ops replayed from Metal traces (DiT weights bfp8, everything else bf16); the persistent-megakernel denoise is in progress and not in this image.
127
  Weights: [nvidia/GR00T-N1.5-3B](https://huggingface.co/nvidia/GR00T-N1.5-3B) · Paper: [arXiv:2503.14734](https://arxiv.org/abs/2503.14734) · Upstream code: [NVIDIA/Isaac-GR00T](https://github.com/NVIDIA/Isaac-GR00T) (`n1.5-release`) · Port: `code/models/experimental/gr00t` and the server `code/gr00t_p150` in this repo
128
  quickstart: |
129
  ### Run with tt-cli
@@ -147,14 +171,15 @@ card:
147
  ### Response
148
 
149
  ```json
150
- {"actions": {"left_arm": [[0.0069, 0.2107, 0.0483, -1.8136, -0.1264, -0.0548, 0.0147], "... 16 steps"],
151
- "right_arm": [[-0.0177, -0.1731, 0.3702, -1.7429, 0.4968, -0.0567, 0.9849], "..."],
152
- "left_hand": [[0.0081, 0.0155, 0.0033, 0.0064, 0.0169, 0.0438], "..."],
153
- "right_hand": [[-1.4880, -1.4915, -1.4781, -1.4810, -2.9609, 2.9904], "..."]},
154
- "action_horizon": 16, "embodiment": "gr1", "seq_len": 296,
155
- "timing_ms": {"decode": 1.5, "encode": 2.6, "device": 43.0, "decode_actions": 0.1, "total": 47.2}}
156
  ```
157
 
 
158
  - `actions` are physical, un-normalised joint targets (radians), 16 steps × [7, 7, 6, 6] dims, as `Gr00tPolicy.get_action` returns them; `timing_ms.device` covers upload, the trace replays and the readback.
159
 
160
  ### Demo
@@ -167,20 +192,23 @@ card:
167
 
168
  | Metric | Value |
169
  |---|---:|
170
- | Action chunk vs the fp32 reference (same observation, same initial noise) | PCC `left_arm` 0.99995 · `right_arm` 0.99997 · `right_hand` 0.99999 · `left_hand` max\|d\| 0.018 rad (near-constant group) · normalised chunk 0.99998 |
171
- | Intermediate taps vs the fp32 reference (57 gated taps, from pixels) | 58/58 rows pass |
172
- | Traced vs untraced, determinism | bit-identical; 5 trace replays bit-identical |
173
- | Inference on p150a (warm, batch 1, median of 50) | **43.4 ms end-to-end** = host encode 1.5 + upload 3.8 + traces 37.8 (vision 8.7 · LLM 8.7 · adapter 4.5 · denoise 16.0) + readback and decode 0.3 |
174
- | Served over HTTP by this image (warm, batch 1) | 42.4 ms device · 45.4 ms server-side incl. PNG decode |
175
- | Same policy on an RTX 5090 (same host, NVIDIA's unmodified `Gr00tPolicy`, eager PyTorch bf16 + flash-attn 2) | 58.0 ms end-to-end (model 55.4) → p150a 1.34× faster |
 
 
176
 
177
  ### Caveats
178
 
179
- - Stage-1 path: TTNN ops in four Metal traces, DiT matmul weights bfp8, everything else bf16. The megakernel denoise (weight streaming measured at 464 GB/s bf16 / 414 GB/s bfp8) is not in this image.
 
180
  - One embodiment and layout: GR1 arms and hands, one `ego_view` camera, instruction ≤ 102 tokens; anything else is refused with 400. Batch 1.
181
- - Noise is seeded by default (seed 0) so the output is reproducible; NVIDIA's `Gr00tPolicy` draws fresh noise per call. Outputs are the base checkpoint's actions for the GR1 sim data config.
182
  - Not an OpenAI-compatible API; `GET /v1/models` is a stub.
183
- - First boot is cold (weight conversion + kernel JIT, READY after 1 min 43 s); later boots take 10–20 s. Validated on tt-metal `668c2907575`, single p150a only. Details: [`SERVING.md`](SERVING.md), [`GPU_COMPARISON.md`](GPU_COMPARISON.md).
184
 
185
  ### Licensing
186
 
 
1
  # SPDX-License-Identifier: Apache-2.0
2
+ # tt-model-manager container manifest (schema 5.1) for NVIDIA Isaac GR00T N1.5-3B on Blackhole -- Stage-2 release:
3
+ # the DiT denoise runs as ONE persistent megakernel generic_op (TTPolicy.dit_backend="megakernel", bfp8_b weight
4
+ # arena, the port's default since 2026-09-18); vision / llm / adapter stay TTNN ops in Metal traces. The Stage-1
5
+ # TTNN denoise is selectable with GR00T_DIT_BACKEND=ttnn.
6
  #
7
  # Run EVERY tt-model command from this directory: `source.tt_metal` and `extra_code[].root: code`
8
  # resolve against the process CWD, not against this file. Rootless Docker on this box needs
 
41
  # three submodules populated and `git status --porcelain` empty, so the image is built from exactly the commit the
42
  # card links to (the working clone has untracked dirs and would be recorded as a dirty tree).
43
  tt_metal: /home/deepgadget/experiments/gr00t/publish/tt-metal-668c2907575
44
+ # tt-metal-relative files the port needs from the tree OTHER than itself, with their import closure
45
  # (the image's tt-metal copy excludes models/, so everything under models/ must be listed here or in extra_code).
46
  code:
47
  - models/common/utility_functions.py # models.tt_dit.utils.matmul imports is_blackhole (needs pytest!)
 
59
  - models/tt_dit/parallel/config.py
60
  - models/tt_dit/parallel/manager.py
61
  - models/tt_dit/encoders/qwen3vl/vision_qwen3vl.py # tt/qwen3vl_vision.py (lazy; not on the N1.5 serve path)
62
+ - models/demos/deepseek_v3_b1/unified_kernel_descriptor.py # tt/megakernel/descriptors.py: the ProgramDescriptor builder of the megakernel (served)
63
+ # Device-side headers the megakernel's kernel source includes (JIT-compiled at boot from
64
+ # /opt/tt-metal/models/experimental/gr00t/tt/megakernel/kernels/dit_kernel.cpp -> ../../../../../demos/deepseek_v3_b1/unified_kernels/*.hpp
65
+ # and, from ops/allgather.hpp, "models/demos/deepseek_v3_b1/unified_kernels/mcast.hpp" through -I$TT_METAL_HOME).
66
+ # Their own includes (tt_metal/tools/profiler/kernel_profiler.hpp, api/compute/blank.h, api/dataflow/dataflow_api.h,
67
+ # api/compute/experimental/deepseek_compute_kernel_hw_startup.h) live under tt_metal/, which the image ships.
68
+ - models/demos/deepseek_v3_b1/unified_kernels/kernel_op_api.hpp
69
+ - models/demos/deepseek_v3_b1/unified_kernels/kernel_utils.hpp
70
+ - models/demos/deepseek_v3_b1/unified_kernels/dataflow_utils.hpp
71
+ - models/demos/deepseek_v3_b1/unified_kernels/mcast.hpp
72
  # This repo's own code (lands at /opt/tt-metal/<path>; PYTHONPATH=/opt/tt-metal).
73
  extra_code:
74
  - root: code
75
  paths:
76
+ - models # code/models/experimental/gr00t: the frozen megakernel-release snapshot of the port (tt-metal ref gr00t-p150-snapshot-mk, 009de892c895; K4b-final kernel tree 7df61c35e9dc7857)
77
+ - gr00t_p150 # the policy server (megakernel default, open_model_device), its smoke test, the demo observation and the vendored Qwen2 tokenizer files
78
  - scripts # download_weights.sh, bench_http.py
79
  ubuntu: "22.04"
80
  python: "3.12"
 
107
  GR00T_LAYOUT: "gr1" # static layout: 1 image x 256 tokens, L_max 102 text tokens, LLM sequence padded to 384
108
  GR00T_POLICY: "mixed_dit" # TTPolicy.dtype_policy: DiT block matmul weights bfp8_b, everything else bf16 (validated default)
109
  GR00T_TRACE_LAYOUT: "per_stage" # four Metal traces vision / llm / adapter / denoise (the benchmarked layout)
110
+ GR00T_DIT_BACKEND: "megakernel" # TTPolicy.dit_backend: the denoise trace = one persistent megakernel generic_op (the port's default; pinned). "ttnn" = the Stage-1 TTNN op sequence
111
+ GR00T_MK_ARENA_DTYPE: "bfp8_b" # the megakernel's DRAM weight arena (what "auto" resolves to for n15: MK_ARENA_DTYPE_DEFAULT); "bf16" selectable
112
+ GR00T_NUM_CQS: "2" # command queues of tt.model.open_model_device: per-call input writes on CQ 1 overlap the traces (bit-equal to 1 CQ, tests/tt/test_mk_2cq.py)
113
  GR00T_TOKENIZER_DIR_N15: "/opt/tt-metal/gr00t_p150/assets/tokenizer/n15"
114
  GR00T_PROJECT_ROOT: "/nonexistent" # the port's dev-box default for project-relative paths; nothing on the serve path needs it
115
  GR00T_GOLDEN_ROOT: "/nonexistent" # tests only (defensive)
116
  GR00T_TT_CACHE: "/weight-cache/gr00t-tt" # host plan (.pt) + device-layout (.tensorbin) weight tiers -> ~/.cache/tt-model/gr00t-n1.5-3b-p150/weights
117
+ GR00T_WARMUP_RUNS: "5" # traced predict() calls after trace capture, before READY (the megakernel-default smoke configuration)
118
+ GR00T_WARMUP_PCC_MIN: "0.999" # boot FAILS when the warm-up actions' PCC vs the shipped fp32 golden is below this
119
 
120
  # Build-time assertions, run INSIDE the finished image as uid 1000, no device, no weights, no HF_MODEL.
121
  verify:
122
  - "import gr00t_p150.server.app as a; assert a.app; import sys; assert 'ttnn' not in sys.modules, 'server import must be side-effect free'"
123
  - "import models.experimental.gr00t.tt.model as m; assert m.Gr00tTT"
124
  - "from models.experimental.gr00t.common import preprocessing, normalization, prompts, checkpoint, weights, configs; assert configs.HF_SNAPSHOT_SHAS['n15'] == '869830fc749c35f34771aa5209f923ac57e4564e'"
125
+ - "from models.experimental.gr00t.tt.policy import TTPolicy, DEFAULT_DIT_BACKEND, MK_ARENA_DTYPE_DEFAULT; p = TTPolicy(); assert p.dit_backend == 'megakernel' == DEFAULT_DIT_BACKEND and p.mk_arena_dtype == 'auto' and MK_ARENA_DTYPE_DEFAULT['n15'] == 'bfp8_b', (p.dit_backend, p.mk_arena_dtype)"
126
+ - "import os; os.environ['GR00T_DIT_BACKEND'] = 'ttnn'; from models.experimental.gr00t.tt.policy import TTPolicy; assert TTPolicy().with_env_overrides().dit_backend == 'ttnn' and TTPolicy().dit_backend == 'megakernel'"
127
+ - "from models.experimental.gr00t.tt.model import open_model_device, device_worker_l1_size, MK_L1_CUT_BYTES_DEFAULT, ADAPTER_MEM_BY_BACKEND; assert MK_L1_CUT_BYTES_DEFAULT == 65536 and ADAPTER_MEM_BY_BACKEND['megakernel'] == 'DRAM'"
128
+ - "import models.experimental.gr00t.tt.megakernel.dit_program, models.experimental.gr00t.tt.megakernel.arena, models.experimental.gr00t.tt.megakernel.core_map; from models.experimental.gr00t.tt.megakernel.descriptors import KERNEL_SOURCE, L1_USABLE_BYTES; from pathlib import Path; assert Path('/opt/tt-metal', KERNEL_SOURCE).is_file(), KERNEL_SOURCE"
129
+ - "from pathlib import Path; k = Path('/opt/tt-metal/models/experimental/gr00t/tt/megakernel/kernels'); assert all((k / 'ops' / f).is_file() for f in ('allgather.hpp', 'block_matmul.hpp', 'dist_layernorm.hpp', 'euler_head.hpp', 'head_sdpa.hpp', 'row_layernorm.hpp', 'weight_stream.hpp')), sorted(p.name for p in (k / 'ops').iterdir())"
130
+ - "from pathlib import Path; d = Path('/opt/tt-metal/models/demos/deepseek_v3_b1/unified_kernels'); assert all((d / f).is_file() for f in ('kernel_op_api.hpp', 'kernel_utils.hpp', 'dataflow_utils.hpp', 'mcast.hpp')), sorted(p.name for p in d.iterdir()) if d.is_dir() else 'missing dir'"
131
+ - "from pathlib import Path; assert Path('/opt/tt-metal/tt_metal/tools/profiler/kernel_profiler.hpp').is_file() and Path('/opt/tt-metal/tt_metal/hw/inc/api/compute/experimental/deepseek_compute_kernel_hw_startup.h').is_file() and Path('/opt/tt-metal/tt_metal/hw/inc/api/compute/blank.h').is_file()"
132
  - "import pytest, models.common.utility_functions; import models.tt_dit.utils.matmul as mm; assert mm.get_matmul_config"
133
  - "import models.demos.deepseek_v3_b1.unified_kernel_descriptor as u; assert u.UnifiedKernelDescriptor"
134
  - "import models.tt_dit.encoders.qwen3vl.vision_qwen3vl"
135
+ - "import models.experimental.gr00t.tt.layers, models.experimental.gr00t.tt.qwen3vl_vision, models.experimental.gr00t.tt.action_head"
136
+ - "from gr00t_p150.server.app import STAGE_SERVED, DEFAULT_NUM_CQS, DIT_BACKENDS; assert STAGE_SERVED['megakernel'] == 'stage2-megakernel' and STAGE_SERVED['ttnn'] == 'stage1-ttnn-traces' and DEFAULT_NUM_CQS == 2 and set(DIT_BACKENDS) == {'ttnn', 'megakernel'}"
137
  - "import safetensors, huggingface_hub, cv2, numpy, PIL; assert int(numpy.__version__.split('.')[0]) < 2, numpy.__version__"
138
  - "import transformers; assert transformers.__version__ == '5.12.1', transformers.__version__"
139
  - "import torchvision; assert torchvision.__version__.startswith('0.26.0'), torchvision.__version__"
140
  - "from pathlib import Path; d = Path('/opt/tt-metal/gr00t_p150/assets/tokenizer/n15'); assert all((d / f).is_file() for f in ('vocab.json', 'merges.txt', 'tokenizer_config.json', 'special_tokens_map.json', 'added_tokens.json')), sorted(p.name for p in d.iterdir())"
141
  - "import os; os.environ['GR00T_TOKENIZER_DIR_N15'] = '/opt/tt-metal/gr00t_p150/assets/tokenizer/n15'; from models.experimental.gr00t.common.prompts import get_tokenizer; t = get_tokenizer('n15'); assert t.convert_tokens_to_ids('<IMG_CONTEXT>') == 151669"
142
+ - "from pathlib import Path; assert Path('/opt/tt-metal/models/experimental/gr00t/common/golden_tap_map.json').is_file() and Path('/opt/tt-metal/models/experimental/gr00t/tests/tt/gates/gates_multi.json').is_file()"
143
+ - "from pathlib import Path; assert Path('/opt/tt-metal/gr00t_p150/server/smoke_test.py').is_file() and Path('/opt/tt-metal/scripts/bench_http.py').is_file() and Path('/opt/tt-metal/scripts/download_weights.sh').is_file()"
144
  - "from pathlib import Path; d = Path('/opt/tt-metal/gr00t_p150/demo/n15'); assert all((d / f).is_file() for f in ('observation.json', 'noise.json', 'expected.json', 'frames/ego_view_t0.png')), sorted(str(p) for p in d.rglob('*'))"
145
+ - "import ttnn; assert all(hasattr(ttnn, a) for a in ('begin_trace_capture', 'end_trace_capture', 'execute_trace', 'release_trace', 'copy_host_to_device_tensor', 'MinimalMatmulConfig', 'generic_op', 'ProgramDescriptor', 'ComputeConfigDescriptor', 'CBDescriptor', 'SemaphoreDescriptor', 'UnpackToDestMode'))"
146
  - "from gr00t_p150.server.app import parse_mesh_shape as p; assert p('1x1') == p('(1, 1)') == p('1,1') == (1, 1)"
147
 
148
  card:
149
  description: |
150
+ NVIDIA Isaac GR00T N1.5 (3B vision-language-action policy) running on one Tenstorrent Blackhole p150a via tt-nn: one camera frame, proprioceptive state and a task instruction in, a 16-step chunk of GR1 arm and hand joint targets out. Stage-2 release: vision tower, LLM and VL adapter as TTNN ops replayed from Metal traces (bf16); the whole DiT denoise (4 steps × 16 blocks) as **one persistent megakernel** on 110 Tensix cores, streaming its bfp8 weights from a DRAM arena once per step. `GR00T_DIT_BACKEND=ttnn` selects the Stage-1 TTNN denoise.
151
  Weights: [nvidia/GR00T-N1.5-3B](https://huggingface.co/nvidia/GR00T-N1.5-3B) · Paper: [arXiv:2503.14734](https://arxiv.org/abs/2503.14734) · Upstream code: [NVIDIA/Isaac-GR00T](https://github.com/NVIDIA/Isaac-GR00T) (`n1.5-release`) · Port: `code/models/experimental/gr00t` and the server `code/gr00t_p150` in this repo
152
  quickstart: |
153
  ### Run with tt-cli
 
171
  ### Response
172
 
173
  ```json
174
+ {"actions": {"left_arm": [[0.0032, 0.2093, 0.0567, -1.8143, -0.1239, -0.0563, 0.0177], "... 16 steps"],
175
+ "right_arm": [[-0.0157, -0.1731, 0.3713, -1.7457, 0.4968, -0.0574, 0.9908], "..."],
176
+ "left_hand": [[0.0081, 0.0121, 0.0042, 0.0064, 0.0169, 0.0380], "..."],
177
+ "right_hand": [[-1.4850, -1.4915, -1.4734, -1.4743, -2.9550, 2.9947], "..."]},
178
+ "action_horizon": 16, "embodiment": "gr1", "seq_len": 296, "noise_source": "client", "seed": null,
179
+ "timing_ms": {"decode": 1.47, "encode": 2.02, "device": 33.51, "decode_actions": 0.15, "total": 37.21}}
180
  ```
181
 
182
+ - The example is the demo request with the reference noise, as `GET /demo` returns it (`noise_source: "client"`, `seed: null`): the smoke response of this image's second (warm) boot, `timing_ms` included.
183
  - `actions` are physical, un-normalised joint targets (radians), 16 steps × [7, 7, 6, 6] dims, as `Gr00tPolicy.get_action` returns them; `timing_ms.device` covers upload, the trace replays and the readback.
184
 
185
  ### Demo
 
192
 
193
  | Metric | Value |
194
  |---|---:|
195
+ | Action chunk vs the fp32 reference (same observation, same initial noise) | PCC `left_arm` 0.99994 · `right_arm` 0.99997 · `right_hand` 0.99999 · `left_hand` max\|d\| 0.030 rad (near-constant group) · normalised chunk 0.99998 |
196
+ | Intermediate taps vs the fp32 reference (the 28 taps the megakernel path exposes, from pixels; DiT intermediates not observable inside the megakernel) | 30/30 rows pass (29 gated); tightest margin `vit_block_first` 0.99994 vs 0.9999 |
197
+ | Megakernel vs the Stage-1 TTNN denoise, same device | normalised chunk PCC 0.999997 |
198
+ | Traced vs untraced, determinism | bit-identical; 5 trace replays bit-identical; 2 CQs == 1 CQ |
199
+ | Inference on p150a (warm, batch 1, median of 50) | **33.6 ms end-to-end** = host encode 1.0 + upload 1.2 + traces 31.1 (vision 8.7 · LLM 8.8 · adapter 4.9 · denoise 8.9 = 2.2 per step) + readback and decode 0.2 |
200
+ | Same tree with the Stage-1 TTNN denoise (`GR00T_DIT_BACKEND=ttnn`) | 40.4 ms end-to-end (denoise 16.0) → megakernel −16.8 % end-to-end, −44 % denoise |
201
+ | Served over HTTP by this image (warm, batch 1) | 32.5 ms device · 34.6 ms server-side incl. PNG decode |
202
+ | Same policy on an RTX 5090 (same host, NVIDIA's unmodified `Gr00tPolicy`, eager PyTorch bf16 + flash-attn 2) | 58.0 ms end-to-end (model 55.4) → p150a 1.73× faster |
203
 
204
  ### Caveats
205
 
206
+ - Stage-2 path: one persistent megakernel runs the denoise (bfp8 weights streamed from a DRAM arena once per step, 110 cores, distributed LayerNorm, per-head flash SDPA); the rest is TTNN ops in three bf16 Metal traces. Its binaries (≈ 131.7 KB) fit 4.5 KB under the 136,192 B kernel-config ring, so the device runs with 64 KiB less L1 per core and the VL adapter's intermediates in DRAM (+0.4 ms); the DiT intermediates are not observable from the served model. `GR00T_DIT_BACKEND=ttnn` serves the Stage-1 denoise from the same image.
207
+ - Fidelity vs the Stage-1 path: gate outcomes equal on 8 of the 9 observations (the demo and 7 of the 8 multi-sample goldens); the one miss is `right_arm` on `t3_s300_seed2`, PCC 0.99987 vs the 0.9999 gate (the TTNN path passes it; the normalised chunk passes on both). The 16.8 % end-to-end gain is below the plan's 30 %.
208
  - One embodiment and layout: GR1 arms and hands, one `ego_view` camera, instruction ≤ 102 tokens; anything else is refused with 400. Batch 1.
209
+ - Noise is seeded by default (seed 0, the seed the fp32 reference was dumped with) so the output is reproducible; NVIDIA's `Gr00tPolicy` draws fresh noise per call. Outputs are the base checkpoint's actions for the GR1 sim data config.
210
  - Not an OpenAI-compatible API; `GET /v1/models` is a stub.
211
+ - First boot is cold (weight conversion + kernel JIT incl. the megakernel, READY after 2 min 14 s); later boots take about 11 s. Validated on tt-metal `668c2907575`, single p150a only. Details: [`SERVING.md`](SERVING.md), [`GPU_COMPARISON.md`](GPU_COMPARISON.md).
212
 
213
  ### Licensing
214