# SPDX-License-Identifier: Apache-2.0 # tt-model-manager container manifest (schema 5.1) for NVIDIA Isaac GR00T N1.6-3B on Blackhole (Stage-1 TTNN trace path). # # 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 # ~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 # 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 -> 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 - 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; Stage-2 work, not served) # 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 commit 5dc0517e, parent 668c2907575) - 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 (dry-run 2026-09-15): numpy 1.26.4, transformers 5.12.1, # opencv-python-headless 4.8.1.78, safetensors 0.8.0, huggingface_hub 1.31.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 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 (validated Stage-1 default) GR00T_TRACE_LAYOUT: "per_stage" # four Metal traces: vision / llm / adapter / denoise (the benchmarked layout) 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 (~8 GB) -> ~/.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 # 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.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'))" - "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: SigLIP2 ViT (252², 81 tokens per image) → 16-layer Qwen3-1.7B backbone → 32-block alternating cross/self-attention AdaLN DiT, 4 Euler flow-matching steps) running on one Tenstorrent Blackhole p150a via tt-nn: one camera frame + proprioceptive state + a task instruction in, a 16-step chunk of GR1 arm / hand / waist actions out. Stage-1 release: every op is a TTNN op replayed from four Metal traces (DiT matmul weights bfp8, everything else bf16); the persistent-megakernel denoise is in progress and is not in this image. Weights: [nvidia/GR00T-N1.6-3B](https://huggingface.co/nvidia/GR00T-N1.6-3B) · Paper: [arXiv:2503.14734](https://arxiv.org/abs/2503.14734) (GR00T N1 report) · Project page: [research.nvidia.com/labs/gear/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` in this repo (tt-metal `models/experimental/gr00t`, snapshot `5dc0517e`) + `code/gr00t_p150` (server) quickstart: | ### Run with tt-cli ```bash tt serve changh95/GR00T-N1.6-3B-p150 python - <<'EOF' import base64, json obs = json.load(open("media/demo_observation.json")) # GR1 PickNPlace 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` (`{camera key: base64 PNG/JPEG}` — exactly one camera, `ego_view_bg_crop_pad_res256_freq20`, for the GR1 embodiment; any size, letterboxed / cropped / resized to 252×252 server-side by the reference eval chain), `state` (`{group: [floats]}`: `left_arm` 7, `right_arm` 7, `left_hand` 6, `right_hand` 6, `waist` 3 — raw joint values), `instruction` (task text, ≤ 24 BPE tokens in this layout); optional `embodiment` (`gr1`, the only layout in this image), `seed` (initial flow-matching noise; default = the deployed policy's fixed seed 42 → deterministic), `noise` (explicit `[50, 128]` initial noise), `state_dtype` (`float32` default), `return_normalized`. - `GET /health`, `GET /info` (full contract, device facts, warm-up fidelity), `GET /demo` (the shipped demo request + its golden actions). ### Response ```json {"actions": {"left_arm": [[-0.0289, 0.1453, 0.0224, -2.1383, -0.0165, -0.0260, -0.0414], "... 16 steps × 7"], "right_arm": [[-0.0024, -0.1502, 0.0280, -2.1461, -0.0091, 0.0132, 0.0044], "..."], "left_hand": [[0.0097, -0.0177, 0.0041, 0.0078, 0.0539, 0.0281], "..."], "right_hand": [[-0.2339, -0.2466, -0.2431, -0.2311, -0.2423, 0.3333], "..."], "waist": [[-0.0010, -0.0060, 0.0008], "..."]}, "action_horizon": 16, "action_keys": ["left_arm", "right_arm", "left_hand", "right_hand", "waist"], "action_dims": {"left_arm": 7, "right_arm": 7, "left_hand": 6, "right_hand": 6, "waist": 3}, "normalized": false, "embodiment": "gr1", "embodiment_id": 20, "version": "n16", "model": "GR00T-N1.6-3B-p150", "layout": "gr1", "images": {"ego_view_bg_crop_pad_res256_freq20": {"frames": 1, "received_hw": [256, 256]}}, "seq_len": 116, "prompt_tokens": 12, "state_dtype": "float32", "noise_source": "seed", "seed": 42, "timing_ms": {"decode": 0.8, "encode": 1.5, "device": 56.5, "decode_actions": 0.1, "total": 58.9}} ``` - `actions` are **physical, un-normalised** joint targets, 16 steps × [7, 7, 6, 6, 3] dims, exactly what `Gr00tPolicy.get_action` returns for the GR1 data config: the arm and hand groups are the checkpoint's RELATIVE outputs already composed onto the request's `state` (per-step min/max un-normalisation + the raw last state, clipped), `waist` is absolute. `seq_len` is the LLM prompt length the request produced (116 for the demo; padded to 128 on device). - `timing_ms`: `decode` = base64 + PNG decode + validation, `encode` = the port's host preprocessing (image chain, prompt + tokenizer, sin/cos state), `device` = input upload + the four trace replays + one blocking readback, `decode_actions` = un-normalisation, `total` = handler wall time. ### Demo | Input (`media/demo_ego_view.png`, GR1 sim PickNPlace, 256×256) | Instruction | |:---:|:---| | ![](media/demo_ego_view.png) | `pick the pear from the counter and place it in the plate` | Served action chunk vs the fp32 reference for this observation (same initial noise), one panel per action dimension: `media/demo_actions_vs_golden.png`. ![](media/demo_actions_vs_golden.png) ### Accuracy and speed | Metric | Value | |---|---:| | Action-chunk PCC vs the fp32 golden (official `Gr00tPolicy` code on CPU, same observation, same noise) | `left_arm` 0.99995 (gate 0.9999) · `right_arm` 0.99991 (0.9998) · `right_hand` 0.99312 (0.99); `left_hand` 0.801 and `waist` 0.772 reported-only (near-constant groups, golden range < 0.25 — max\|d\| 0.059 / 0.017 within tolerance); all 29 action dims concatenated **0.999209**, `action_pred_valid` 0.99921 (gate 0.998) | | Intermediate taps vs golden (45 canonical taps, untraced, from pixels) | **52 of 56 gated rows pass; 4 marginal misses** (57 of 61 rows incl. the determinism row and the four xfail info rows), all on DiT state-token row 0 (which no action reads): `dit_out[k=1]` 0.999556 vs gate 0.99961, `dit_out[k=2]` 0.999496 vs 0.99964, `action_decoder_out[k=1]` 0.999579 vs 0.9997, `action_decoder_out[k=2]` 0.999556 vs 0.99978 (excluding row 0: 0.99984 / 0.99984 / 0.99991 / 0.99992). Root cause: bf16 HiFi2 SigLIP2 tower error propagation (`vit_block_last` 0.99276), not the DiT — `test_tt_action_head` passes on golden inputs | | Traced == untraced, determinism, stale state | bit-identical (13/13 rows; 5 replays + 1 traced-after-untraced; A→B→A→B alternation passes) | | Inference on p150a, `bench_e2e` (warm, batch 1, one 256² frame → 252², S 116→128, median of 50) | **59.4 ms end-to-end** (p90 60.1) = host encode 2.0 + upload 3.8 + 4 traces 53.3 (vision 11.5 · LLM 9.5 · adapter 2.0 · denoise 30.4) + D2H 0.1 + decode 0.2 | | Served over HTTP from this image (container, warm, 50 requests after 5 warm-ups, seed path, quiet host) | **56.8 ms device** (p90 57.3, min 56.5, max 57.6) · **59.4 ms server-side total** incl. PNG decode (p90 59.8) · 61.0 ms client wall on localhost (p90 61.6) | | Same policy on an RTX 5090 (same host, NVIDIA's unmodified code, eager PyTorch bf16 + flash-attn 2, batch 1) | `get_action` e2e 63.1 ms (backbone 16.8, action head 36.2) → **p150a 1.06× faster e2e**; with `torch.compile(DiT, max-autotune)` the GPU reaches 33.1 ms (head 13.5) → **GPU 1.8× faster** than this Stage-1 path; NVIDIA's published RTX 5090 numbers: eager 58 ms, compiled 37 ms | ### Caveats - **Stage-1 path.** Vision / LLM / adapter / denoise are TTNN ops captured in four Metal traces; DiT block matmul weights are bfp8_b, everything else bf16 (`mixed_dit`). The persistent-megakernel denoise (Stage 2) is at rung K1 — weight streaming measured at 464 GB/s bf16 / 414 GB/s bfp8 over the full 32-block N1.6 weight set — and is **not** in this image. - **N1.6 fidelity margin.** The served chunk reproduces the fp32 golden at PCC 0.99921 on the normalised valid slice (NVIDIA's own bf16 GPU run scores 0.99956 there), 2e-4 above the 0.999 boot / smoke gate; the four intermediate-tap misses above are disclosed verbatim. N1.5 (`changh95/GR00T-N1.5-3B-p150`) passes every tap. - **One embodiment, one layout.** The image is built for GR1 (`gr1`, embodiment slot 20, one `ego_view_bg_crop_pad_res256_freq20` camera, instruction ≤ 24 BPE tokens → LLM sequence padded to 128); other embodiments / cameras / longer instructions are refused (400). Batch 1; requests are serialised on one lock. - **Noise is seeded** (seed 42, as the deployed policy); pass `seed` or `noise` for a different draw. Outputs are the base checkpoint's actions for the GR1 sim data config — a real robot needs NVIDIA's post-training. - Not an OpenAI-compatible API; `GET /v1/models` is a stub so the tt-model ready card does not 404. - Validated on tt-metal main `668c2907575` (`v0.79.0-dev20260914-1`), single p150a only. First boot converts the checkpoint to device layout and compiles every kernel (137 s cold, see SERVING.md) into `~/.cache/tt-model/gr00t-n1.6-3b-p150/{weights,cache}` (~8 GB); later boots take 13–21 s (13 s with the weight tier still in the host page cache, 21 s an hour later). - GPU comparison: RTX 5090 rows (2026-09-14) are NVIDIA's unmodified `Gr00tPolicy` path (`Isaac-GR00T` `n1.6.1-release`, PyTorch eager bf16 + flash-attn 2, `cudnn.benchmark` on, deterministic mode off), medians of 20 timed iterations after 5 warm-ups (`torch.cuda.synchronize()` around every call), GPU otherwise idle; the p150a row is the Stage-1 trace path incl. upload / readback. p150a power was not measured, so no efficiency comparison is made. Full table: [`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: research or evaluation use only; verbatim copy in [`LICENSE`](LICENSE)). Not redistributed here; fetched into your HF cache by `tt-model serve` / `scripts/download_weights.sh`. - Tokenizer files under `code/gr00t_p150/assets/tokenizer/n16/` (Qwen2 BPE + Eagle special tokens) are copied from [NVIDIA/Isaac-GR00T `n1.6.1-release`](https://github.com/NVIDIA/Isaac-GR00T) (`gr00t/model/modules/nvidia/Eagle-Block2A-2B-v2`), whose `LICENSE` is the same NVIDIA License. - Port and serving code (`code/models/experimental/gr00t`, `code/gr00t_p150`, `code/scripts`): Apache-2.0 (SPDX headers), © 2026 Tenstorrent; distributed here under the same non-commercial use limitation as the weights (NVIDIA License §3.2). tt-metal (`code/models/common`, `code/models/tt_dit`, `code/models/demos`, and the image): Apache-2.0. - Demo frame and state (`media/`, `code/gr00t_p150/demo/n16`): one GR1 PickNPlace step from the Isaac-GR00T demo data; the fp32 / bf16 reference actions shipped beside it were computed with NVIDIA's code on the pinned weights.