# SPDX-License-Identifier: Apache-2.0 # tt-model-manager container manifest (schema 5.1) for NVIDIA Isaac GR00T N1.6-3B on Blackhole # (Stage-2: the DiT denoise as one persistent megakernel generic_op; vision / llm / adapter as TTNN traces). # # Run EVERY tt-model command from this directory: `source.tt_metal` and `extra_code[].root: code` resolve against the # process CWD, not against this file. `--out` must lie outside any git checkout (`package` deletes / first). # On the build host rootless Docker needs `source /home/deepgadget/experiments/tt-models/bin/docker-env.sh` first. # # tt-model package --container tt-model.yaml --out /home/deepgadget/experiments/gr00t/publish/build # minutes with the ccache mount, ~2 h cold; runs verify.sh # tt-model serve /home/deepgadget/experiments/gr00t/publish/build/gr00t-n1.6-3b-p150/tt_kernel_manifest.json # python code/gr00t_p150/server/smoke_test.py --url http://127.0.0.1:20000 # PASS/FAIL one-liner (prints the served stage) # tt-model stop /home/deepgadget/experiments/gr00t/publish/build/gr00t-n1.6-3b-p150/tt_kernel_manifest.json # org/name works only after pull/push # tt-model push /home/deepgadget/experiments/gr00t/publish/build/gr00t-n1.6-3b-p150 --publish # # The one Blackhole card of the build host is shared: `serve -> smoke -> bench -> stop` runs inside ONE bin/with-device.sh hold. schema: "5.1" repo: changh95/GR00T-N1.6-3B-p150 name: gr00t-n1.6-3b-p150 # A POINTER, pinned. `serve` pre-downloads exactly these files at this sha into the host HF cache (mounted at /hf); # the server resolves the same snapshot with snapshot_download(revision=TT_WEIGHTS_REVISION) and asserts that the # port's own common.configs.snapshot_dir("n16") points at it. The weights never enter the image. weights: repo: nvidia/GR00T-N1.6-3B revision: d0814e7ecb19202e7c8468b46098b0b7ef3a6d61 allow_patterns: - "model-*.safetensors" # 2 bf16 shards: 6,573,217,664 B of bf16 tensors, 6,573,374,552 B on disk - model.safetensors.index.json # LazyCheckpoint needs it (common/checkpoint.py) - config.json - processor_config.json # modality configs (common/normalization.py) - statistics.json # per-embodiment min/max statistics (common/normalization.py) - embodiment_id.json # tag -> slot cross-check (common/normalization.py) - LICENSE # NVIDIA License: keep it next to the weights it covers kind: tt-dit-server arch: blackhole source: # Clean shared clone of tt-metal main 668c2907575 (v0.79.0-dev20260914-1), the commit the port was validated on; # git status is empty there, so the card records a clean tree. The image builds tt-metal from this source. tt_metal: /home/deepgadget/experiments/gr00t/publish/tt-metal-668c2907575 # tt-metal-relative files the port imports from the tree OTHER than itself, with their import closure. code: - models/common/utility_functions.py # models.tt_dit.utils.matmul imports is_blackhole - models/tt_dit/utils/matmul.py # tt/layers.py: MinimalMatmul config rows (lazy) - models/tt_dit/utils/agmm_rules.py # lazy from matmul.py - models/tt_dit/utils/mmrs_rules.py # lazy from matmul.py - models/tt_dit/utils/tensor.py # layers/linear.py, layers/module.py, lora.py, parallel/manager.py - models/tt_dit/utils/progress.py # layers/module.py - models/tt_dit/utils/walltime.py # utils/progress.py - models/tt_dit/utils/substate.py # layers/module.py - models/tt_dit/layers/linear.py # encoders/qwen3vl/vision_qwen3vl.py (N1.7 tower; lazy, unused at run time here) - models/tt_dit/layers/module.py - models/tt_dit/layers/normalization.py - models/tt_dit/layers/lora.py # imported at the bottom of layers/linear.py - models/tt_dit/parallel/config.py - models/tt_dit/parallel/manager.py - models/tt_dit/encoders/qwen3vl/vision_qwen3vl.py # tt/qwen3vl_vision.py (lazy; N1.7 only) - models/demos/deepseek_v3_b1/unified_kernel_descriptor.py # tt/megakernel/descriptors.py (lazy) -- ON the serve path: the megakernel program descriptor # Device-code headers the megakernel's kernels include from the tree (tt/megakernel/kernels/dit_kernel.cpp # `#include "../../../../../demos/deepseek_v3_b1/unified_kernels/*.hpp"`, JIT-compiled at boot relative to TT_METAL_HOME): # the transitive closure inside models/ is exactly these four; their own includes are tt_metal hw/api headers the image ships. - models/demos/deepseek_v3_b1/unified_kernels/kernel_op_api.hpp - models/demos/deepseek_v3_b1/unified_kernels/kernel_utils.hpp - models/demos/deepseek_v3_b1/unified_kernels/dataflow_utils.hpp - models/demos/deepseek_v3_b1/unified_kernels/mcast.hpp # This repo's own code (lands at /opt/tt-metal/; PYTHONPATH=/opt/tt-metal). extra_code: - root: code paths: - models # code/models/experimental/gr00t: the frozen snapshot of the port (tt-metal ref gr00t-p150-snapshot-mk = commit 009de892c895, parent 668c2907575; megakernel tree 7df61c35e9dc7857) - gr00t_p150 # FastAPI server, schemas, smoke test, demo observation, vendored Qwen2 tokenizer files (n16) - scripts # download_weights.sh, bench_http.py ubuntu: "22.04" python: "3.12" runtime: app: gr00t_p150.server.app:app mesh_shape_env: TT_MESH_SHAPE # On top of the kind defaults (fastapi, uvicorn, pydantic>=2, pillow), torch==2.11.0+cpu (auto-pinned from the tree's # tt_metal/python_env/requirements-dev.txt) and ttnn's own deps. Every third-party module on the serve path, pinned # where the tree pins it. Resolved on Python 3.12 with uv (image build 2026-09-18, requirements.lock): numpy 1.26.4, # transformers 5.12.1, opencv-python-headless 4.8.1.78, safetensors 0.8.0, huggingface_hub 1.32.0, loguru 0.7.3, torch 2.11.0+cpu. # No torchvision: the N1.6 image chain is cv2 + PIL (torchvision is only on the N1.5 path). packages: - "numpy>=1.24.4,<2" - safetensors # common/checkpoint.py (module level) - huggingface_hub # server: snapshot_download(revision=..., local_files_only=True) first - transformers==5.12.1 # common/prompts.get_tokenizer (AutoTokenizer, lazy); tree pin - opencv-python-headless==4.8.1.78 # common/preprocessing.py imports cv2 at module level; tree pin (cp312 wheel exists) - loguru # models/common/utility_functions.py (via models.tt_dit.utils.matmul) serve: port: 20000 hardware: p150 mesh_device: P150 env: TT_WEIGHTS_REVISION: "d0814e7ecb19202e7c8468b46098b0b7ef3a6d61" # == weights.revision == configs.HF_SNAPSHOT_SHAS["n16"] TT_METAL_VISIBLE_DEVICES: "0" TT_DEVICE_ID: "0" GR00T_VERSION: "n16" # the server code is shared with GR00T-N1.5-3B-p150 / N1.7-3B-p150 and never guesses GR00T_EMBODIMENT: "gr1" # GR1 arms + hands + waist, embodiment slot 20; the only device-validated layout GR00T_LAYOUT: "gr1" # static layout: 1 camera x 81 image tokens, L_max 24 text tokens, S_pad 128 GR00T_POLICY: "mixed_dit" # TTPolicy.dtype_policy: DiT matmul weights bfp8_b, everything else bf16 (the validated default) GR00T_TRACE_LAYOUT: "per_stage" # four Metal traces: vision / llm / adapter / denoise (the benchmarked layout) GR00T_DIT_BACKEND: "megakernel" # TTPolicy.dit_backend: the DiT denoise as ONE persistent generic_op (Stage 2, the port's default # since 2026-09-18); "ttnn" serves the Stage-1 TTNN op sequence on the same image GR00T_MK_ARENA_DTYPE: "auto" # TTPolicy.mk_arena_dtype: the megakernel's DRAM weight arena; auto -> bfp8_b for n16 (the validated arena) GR00T_NUM_CQS: "2" # command queues of the device (open_model_device): 2 = the port's production path, per-call input # writes on CQ 1 overlapping the traces (bit-equal to 1 CQ, tests/tt/test_mk_2cq.py) GR00T_TOKENIZER_DIR: "/opt/tt-metal/gr00t_p150/assets/tokenizer/n16" # vendored Qwen2 tokenizer files (not in the weights repo) GR00T_PROJECT_ROOT: "/nonexistent" # kill the dev-box default the port's prompts.py would otherwise fall back to GR00T_GOLDEN_ROOT: "/nonexistent" # same for common/golden.py (tests only; defensive) GR00T_TT_CACHE: "/weight-cache/gr00t-tt" # host .pt plan tier + .tensorbin device tier (~2.6 GB under the megakernel; 7.9 GB on the Stage-1 path) -> ~/.cache/tt-model/gr00t-n1.6-3b-p150/weights GR00T_WARMUP_RUNS: "3" # traced predict() calls after trace capture, before READY GR00T_WARMUP_PCC_MIN: "0.999" # boot FAILS when the warm-up actions' PCC vs the shipped fp32 golden is below this (served: 0.99923) # Build-time assertions, run INSIDE the finished image as uid 1000, no device, no weights, no HF_MODEL. verify: - "import gr00t_p150.server.app as a; assert a.app" - "import sys, gr00t_p150.server.app; assert 'ttnn' not in sys.modules and not any(m.startswith('models.experimental.gr00t') for m in sys.modules), 'the server module must import without side effects'" - "import models.experimental.gr00t.tt.model as m; assert m.Gr00tTT" - "from models.experimental.gr00t.tt.policy import TTPolicy, DEFAULT_DIT_BACKEND; p = TTPolicy(); assert p.dit_backend == 'megakernel' == DEFAULT_DIT_BACKEND and p.mk_arena_dtype == 'auto' and p.mk_arena_dtype_for('n16') == 'bfp8_b', (p.dit_backend, p.mk_arena_dtype)" - "import models.experimental.gr00t.tt.megakernel.dit_program as d; from models.experimental.gr00t.tt.megakernel import arena, core_map, descriptors; assert d.DiTMegakernel and descriptors.KERNEL_SOURCE.endswith('tt/megakernel/kernels/dit_kernel.cpp')" - "import os; from pathlib import Path; from models.experimental.gr00t.tt.megakernel.descriptors import KERNEL_SOURCE; k = Path(os.environ.get('TT_METAL_HOME', '/opt/tt-metal')) / KERNEL_SOURCE; assert k.is_file() and len(list((k.parent / 'ops').glob('*.hpp'))) == 7, str(k)" - "import os, re; from pathlib import Path; from models.experimental.gr00t.tt.megakernel.descriptors import KERNEL_SOURCE; home = Path(os.environ.get('TT_METAL_HOME', '/opt/tt-metal')); k = home / KERNEL_SOURCE; incs = {m for f in [k, *(k.parent / 'ops').glob('*.hpp')] for m in re.findall(r'#include \"([^\"]*unified_kernels/[^\"]+)\"', f.read_text())}; assert incs, 'no unified_kernels includes found'; missing = [i for i in incs if not (k.parent / i).resolve().is_file() and not (home / i).is_file()]; assert not missing, missing" - "from models.experimental.gr00t.tt.model import open_model_device, device_worker_l1_size, worker_l1_size_for, MK_L1_CUT_BYTES_DEFAULT; from models.experimental.gr00t.tt.policy import TTPolicy; assert MK_L1_CUT_BYTES_DEFAULT == 65536 and worker_l1_size_for(TTPolicy()) is not None and worker_l1_size_for(TTPolicy(dit_backend='ttnn')) is None" - "from gr00t_p150.server.app import STAGE_SERVED, DEFAULT_NUM_CQS, DIT_BACKENDS, MK_ARENA_DTYPES; assert STAGE_SERVED['megakernel'] == 'stage2-megakernel' and STAGE_SERVED['ttnn'] == 'stage1-ttnn-traces' and DEFAULT_NUM_CQS == 2 and set(DIT_BACKENDS) == {'ttnn', 'megakernel'} and 'bfp8_b' in MK_ARENA_DTYPES" - "from models.experimental.gr00t.common import configs, preprocessing, normalization, prompts, checkpoint, weights; assert configs.HF_SNAPSHOT_SHAS['n16'] == 'd0814e7ecb19202e7c8468b46098b0b7ef3a6d61' and configs.HF_REPOS['n16'] == 'nvidia/GR00T-N1.6-3B'" - "import models.tt_dit.utils.matmul as mm; assert mm.get_matmul_config" - "import models.demos.deepseek_v3_b1.unified_kernel_descriptor as u; assert u.UnifiedKernelDescriptor" - "import safetensors, huggingface_hub, cv2, numpy, PIL, loguru; assert int(numpy.__version__.split('.')[0]) < 2, numpy.__version__; assert cv2.__version__ == '4.8.1', cv2.__version__" - "import transformers; assert transformers.__version__ == '5.12.1', transformers.__version__" - "from pathlib import Path; d = Path('/opt/tt-metal/gr00t_p150/assets/tokenizer/n16'); 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())" - "import os; os.environ['GR00T_TOKENIZER_DIR_N16'] = '/opt/tt-metal/gr00t_p150/assets/tokenizer/n16'; from models.experimental.gr00t.common.prompts import get_tokenizer; t = get_tokenizer('n16'); assert t.convert_tokens_to_ids('') == 151669 and t.convert_tokens_to_ids('') == 151670 and t.convert_tokens_to_ids('') == 151671" - "from pathlib import Path; d = Path('/opt/tt-metal/gr00t_p150/demo/n16'); assert all((d / f).is_file() for f in ('observation.json', 'noise.json', 'expected.json', 'frames/ego_view_bg_crop_pad_res256_freq20_t0.png')), sorted(str(p) for p in d.rglob('*'))" - "from gr00t_p150.server.app import load_demo_request; import numpy as np; r = load_demo_request('n16'); assert set(r['images']) == {'ego_view_bg_crop_pad_res256_freq20'} and np.asarray(r['noise']).squeeze().shape == (50, 128)" - "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()" - "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()" - "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'))" - "from gr00t_p150.server.app import parse_mesh_shape as p; assert p('1x1') == p('(1, 1)') == p('1,1') == (1, 1)" card: description: | NVIDIA Isaac GR00T N1.6 (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, hand and waist actions out. Stage-2 release: the whole DiT denoise (32 blocks × 4 flow-matching steps) runs as one persistent megakernel streaming bfp8 weights from a DRAM arena; vision, LLM and adapter are TTNN ops (bf16) replayed from Metal traces; `GR00T_DIT_BACKEND=ttnn` selects the Stage-1 denoise. Weights: [nvidia/GR00T-N1.6-3B](https://huggingface.co/nvidia/GR00T-N1.6-3B) · Paper: [arXiv:2503.14734](https://arxiv.org/abs/2503.14734) · Project page: [gr00t-n1_6](https://research.nvidia.com/labs/gear/gr00t-n1_6/) · Upstream code: [NVIDIA/Isaac-GR00T](https://github.com/NVIDIA/Isaac-GR00T) (`n1.6.1-release`) · Port: `code/models/experimental/gr00t` and the server `code/gr00t_p150` in this repo quickstart: | ### Run with tt-cli ```bash tt serve changh95/GR00T-N1.6-3B-p150 python3 - <<'EOF' import base64, json obs = json.load(open("media/demo_observation.json")) # GR1 demo step: raw state + instruction obs["images"] = {"ego_view_bg_crop_pad_res256_freq20": base64.b64encode(open("media/demo_ego_view.png", "rb").read()).decode()} json.dump(obs, open("req.json", "w")) EOF curl -s localhost:20000/predict -H 'Content-Type: application/json' -d @req.json tt model stop changh95/GR00T-N1.6-3B-p150 ``` - `POST /predict`: `images` (`{"ego_view_bg_crop_pad_res256_freq20": }`), `state` (`{group: [floats]}`: `left_arm` 7, `right_arm` 7, `left_hand` 6, `right_hand` 6, `waist` 3, raw joint values), `instruction` (≤ 24 tokens); optional `seed` (default 42) or `noise` (`[50, 128]`), `return_normalized`. - `GET /health`, `GET /info` (`stage` names the denoise backend), `GET /demo` (the shipped demo request and its golden actions). ### Response ```json {"actions": {"left_arm": [[-0.0289, 0.1453, 0.0224, -2.1383, -0.0165, -0.0225, -0.0428], "... 16 steps"], "right_arm": [[-0.0024, -0.1512, 0.0269, -2.1461, -0.0091, 0.0124, 0.0044], "..."], "left_hand": [[0.0135, -0.0092, -0.0047, 0.0070, 0.0455, 0.0281], "..."], "right_hand": [[-0.2253, -0.2421, -0.2345, -0.2311, -0.2338, 0.3385], "..."], "waist": [[0.0001, -0.0060, 0.0008], "..."]}, "action_horizon": 16, "embodiment": "gr1", "seq_len": 116, "seed": 42, "timing_ms": {"decode": 0.9, "encode": 1.4, "device": 39.6, "decode_actions": 0.2, "total": 42.1}} ``` - `actions` are physical, un-normalised joint targets, 16 steps × [7, 7, 6, 6, 3] dims, as `Gr00tPolicy.get_action` returns them for the GR1 data config (arm and hand groups are relative outputs composed onto the request's `state`, `waist` is absolute); `timing_ms.device` covers upload, the trace replays and the readback. ### Demo | Input (`media/demo_ego_view.png`, GR1 sim PickNPlace, 256×256) | Instruction | p150a actions vs the fp32 reference (`media/demo_actions_vs_golden.png`) | |:---:|:---|:---:| | ![](media/demo_ego_view.png) | `pick the pear from the counter and place it in the plate` | ![](media/demo_actions_vs_golden.png) | ### Accuracy and speed | Metric | Value | |---|---:| | Action chunk vs the fp32 reference (same observation, same initial noise) | PCC `left_arm` 0.99995 · `right_arm` 0.99990 · `right_hand` 0.99308 · `left_hand` / `waist` max\|d\| 0.059 / 0.013 (near-constant groups) · all 29 action dims 0.99923 (normalised valid slice 0.99922) | | Intermediate taps vs the fp32 reference (the 23 taps the megakernel path exposes, from pixels; DiT block internals are not observable inside the kernel) | 26 of 28 gated rows pass (29 of 31 rows incl. the determinism row and the two xfail info rows); 2 marginal misses on the DiT state-token row that no action reads (`action_decoder_out[k=1,2]` 0.999594 / 0.999598 vs gates 0.9997 / 0.99978), caused by bf16 error in the SigLIP2 tower; the Stage-1 card's `dit_out[k=1,2]` misses are now inside the kernel | | Megakernel vs the Stage-1 TTNN denoise, same device | `action_pred_normalized` PCC ≥ 0.999994 on every N1.6 reference sample | | Traced vs untraced, determinism | bit-identical (13 of 13 rows); 5 trace replays bit-identical; 2 CQs == 1 CQ | | Inference on p150a (warm, batch 1, median of 50) | **40.7 ms end-to-end** (p90 40.8) = host encode 1.4 + upload 1.4 + traces 37.8 (vision 10.8 · LLM 9.6 · adapter 2.2 · denoise 15.3 = 3.84 ms per step) + readback and decode 0.1 | | Served over HTTP by this image (warm, batch 1, 50 requests) | 39.6 ms device · 42.1 ms server-side incl. PNG decode (p90 42.5) | | Same policy on an RTX 5090 (same host, NVIDIA's unmodified `Gr00tPolicy`, eager PyTorch bf16 + flash-attn 2) | 63.1 ms end-to-end → p150a 1.55× faster; with `torch.compile` on the DiT the GPU reaches 33.1 ms → GPU 1.23× faster than this Stage-2 path | ### Caveats - Stage-2 path: the `denoise` trace is one persistent `generic_op` on all 110 Tensix cores (8 readers stream the 32-block weight set once per step from a 0.98 GB bfp8 DRAM arena to 96 compute cores; distributed LayerNorm, per-head flash SDPA, Euler update and decoder inside the kernel). Against the ttnn denoise on the same tree (e2e 56.2 ms, denoise 30.4) it saves 27.6 % / 49.6 %, short of the planned 30 % e2e; `GR00T_DIT_BACKEND=ttnn` selects that path. - Fidelity margin: the served chunk matches the fp32 reference at PCC 0.99923 (physical actions; 0.99922 on the normalised valid slice, where NVIDIA's own bf16 GPU run scores 0.99956), 2.3e-4 above the 0.999 boot self-check. - Costs of the megakernel: 64 KiB less allocatable L1 per core (the kernel-config ring is 136,192 B, the compute-core binaries take ≈ 131.7 KB of it), the VL adapter's intermediates in DRAM (+0.2 ms adapter trace), and no DiT-internal taps (10 head taps + the backbone taps remain gated). - One embodiment and layout: GR1 (embodiment slot 20), one `ego_view_bg_crop_pad_res256_freq20` camera, instruction ≤ 24 tokens; anything else is refused with 400. Batch 1. - Noise is seeded (seed 42, as the deployed policy); pass `seed` or `noise` for another draw. Outputs are the base checkpoint's actions for the GR1 sim data config. - Not an OpenAI-compatible API; `GET /v1/models` is a stub. - First boot is cold (weight conversion + kernel JIT incl. the megakernel, 58 s); later boots take 14 s. Validated on tt-metal `668c2907575`, single p150a only. Details: [`SERVING.md`](SERVING.md), [`GPU_COMPARISON.md`](GPU_COMPARISON.md). ### Licensing - Weights: [nvidia/GR00T-N1.6-3B](https://huggingface.co/nvidia/GR00T-N1.6-3B), **NVIDIA License** (non-commercial; copy in [`LICENSE`](LICENSE)). Not redistributed here. - Tokenizer files (`code/gr00t_p150/assets/tokenizer/n16/`) are from Isaac-GR00T `n1.6.1-release` under the same NVIDIA License. - Port and serving code (`code/`): Apache-2.0, © 2026 Tenstorrent, distributed under the same non-commercial limitation as a derivative of the weights.