Skip to content

GROOT N1.7 update: Thor NVFP4 + FA4 performance tier - #163

Merged
LiangSu8899 merged 16 commits into
mainfrom
groot-n17-update
Aug 5, 2026
Merged

LiangSu8899 merged 16 commits into
mainfrom
groot-n17-update

Conversation

@LiangSu8899

@LiangSu8899 LiangSu8899 commented Aug 5, 2026

Copy link
Copy Markdown
Member

Summary

Adds an opt-in NVFP4 + FA4 performance tier for GROOT N1.7 on Jetson AGX Thor (SM110). The default FP8 path is unchanged.

Measured on the GR00T-N1.7 LIBERO fine-tune (libero_10), one camera, 4 denoising steps, batch 1, medians over 20 iterations after 5 warmup iterations, on a fixture captured from a real libero_10 observation through the official preprocessing path (JetPack 7.2, MAXN):

Tier Backbone Action head E2E Frequency
FP8 (default, unchanged) 11.0 ms 25.8 ms 36.8 ms 27.2 Hz
NVFP4 + FA4 (use_fp4=True) 8.4 ms 15.2 ms 23.7 ms 42.3 Hz

Run as one same-session A/B/A: FP8 36.76 ms → NVFP4 23.65 ms → FP8 36.85 ms. Action cosine between the tiers on that fixture is 1.000000.

On the base GR00T-N1.7-3B checkpoint with a 2-view fixture (T=40, same protocol): FP8 50.2 ms (20 Hz) → NVFP4 29.9 ms (33 Hz), also A/B/A (51.6 / 29.9 / 50.2), action cosine 0.99994.

CUDA-graph replays on the new tier are bit-identical (the masked-softmax attention also removes a long-standing source of replay nondeterminism in the FP8 tier's logits padding). On an 8-sample aux set captured from the base checkpoint, 7 of 8 samples hold action cosine >= 0.9993 against the FP8 tier with a worst sample of 0.9976 — deployments that are accuracy-critical should validate on the target task rather than on cosine alone, or stay on the FP8 tier.

An earlier revision of this branch carried a dit_fp8_layers precision ladder. It was removed before review: with every DiT layer marked for FP8 the captured graph still produced the all-NVFP4 result, so the option did not do what it documented. The last commit removes the knob, its per-layer gate, and the benchmark flag rather than shipping unverified behavior.

What is in the tier

  • NVFP4 DiT action head — every DiT GEMM (fused self-attn QKV, cross-attn Q, attention output, both FFN projections) runs as a block-scaled NVFP4 GEMM with fused bf16 bias / bias+residual / bias+tanh-GELU+FP4-output epilogues. At M=41 the DiT is weight-bandwidth-bound, so halving weight bytes is the dominant win. Activation quantization is dynamic per-16-element (no calibration pass).
  • NVFP4 cross-KV projections — the 32 per-frame K/V GEMMs quarter their weight traffic.
  • Fused norm→FP4 / norm→FP8 front-ends — AdaLN and the pre-FFN LayerNorm emit FP4 directly (bit-identical to the two-step chain); the ViT / VL-self-attn LayerNorms emit FP8 directly, dropping the fp16 intermediate's write and read-back.
  • Vectorized backbone small kernels — 16-byte-load rewrites of the per-head RMSNorms, LayerNorms, static FP8 quantize, split-half RoPE, GQA head expand, and residual add. The scalar originals issue 2–4 B accesses and sit far under the LPDDR5X floor. Element math matches; RoPE / quantize / head-expand / residual are bit-exact.
  • Masked-softmax MHA — the softmax touches only the valid S_kv columns, so the per-layer full-buffer -inf logits pre-fill disappears; the DiT self-attention also reads the fused QKV GEMM output in place (token stride 3D), dropping three split copies per layer.
  • FA4 attention — the three backbone MHA sites (ViT multi-view, LLM causal GQA with pack_gqa, VL-self-attn) run the vendored FlashAttention-4 (CuTe-DSL) forward, falling back to the existing fmha/cuBLAS chain when the thor-fa4 runtime deps are absent.

Shared-code changes

Only two files outside groot_n17 carry behavior:

  • flash_rt/api.pyuse_fp4=True with config="groot_n17" on Thor routes to the new frontend and falls back to the FP8 tier when flash_rt_fp4 is unavailable. Pi0.5's use_fp4 routing is untouched.
  • flash_rt/hardware/thor/fa4_backend.py — the Thor chip string now follows the installed nvidia-cutlass-dsl (4.4.x only accepts sm_110a; 4.5+ needs the sm_101a alias). Previously the loader always requested sm_101a, so on 4.4.x FA4 failed to compile and every caller silently fell back to fmha. Call sites: LingBot (flash_rt/models/lingbot/kernel_ops.py) and Cosmos3-Edge (flash_rt/models/cosmos3_edge/layer_ref.py) — both keep their existing behavior on 4.5+ and now get the working FA4 path on 4.4.x.

The shared build surface also changes, in gated form: two new compilation units (csrc/kernels/attention_mha_masked.cu, csrc/kernels/vec_fp16_backbone.cu) join flash_rt_kernels, but only on SM100-class builds — CMake adds them under ENABLE_SM100_CUTLASS and defines FLASHRT_HAVE_THOR_VLA_KERNELS, and their bindings in csrc/bindings.cpp are guarded by the same define, so source and binding drop together on SM8x. The NVFP4 frontend probes for those bindings up front and fails naming the build flag rather than dying inside graph capture.

Everything else is additive: new csrc kernels with new symbols, a new frontend subclass, and gated branches in the N1.7 pipeline.

Tests

tests/test_groot_n17_thor_fp4_kernels.py — 16 contracts covering the NVFP4 GEMM epilogues against a torch reference, the bf16 activation quantizer, the fused DiT norms (bit-exact vs the two-step chain), the vectorized backbone helpers, and masked-softmax MHA against the pre-filled variant with a poisoned logits scratch. The masked softmax is additionally pinned at S_kv = 1024 / 1025 / 2048 in both dtypes against torch SDPA, since 1024 is where its register-tiled path hands over to the multi-pass one. Skips cleanly without CUDA or the optional flash_rt_fp4 extension.

tests/test_groot_n17_thor_fp4_routing.py — 5 contracts on the public load_model behavior the tier adds: use_fp4=True selects the NVFP4 frontend, a missing or NVFP4-less flash_rt_fp4 falls back to FP8, use_fp4 with use_fp16 is rejected, and the default route is unchanged. Frontends are stubbed, so these need no GPU or checkpoint.

Regression run on Thor (tests/test_groot_n17_*, test_thor_groot_attn_backend, test_build_inventory, test_load_model_use_fp8_kwarg): 156 passed, 36 skipped, 1 pre-existing environment failure (test_e2e_action_cosines_with_hf_noise needs a newer transformers for denormalize_action; it fails the same way on main).

Reproduce

The A/B/A above is three runs of the same command with the tier flipped;
--ref-out / --ref-in carry the FP8 output forward so the FP4 run reports
the cross-tier action cosine, and --min-cosine turns it into a gate:

# A: FP8 tier, save its actions as the precision reference
python benchmarks/groot_n17_thor_latency.py \
    --ckpt <n17-libero-checkpoint-dir> --aux <1-camera-aux-fixture.pt> \
    --tier fp8 --views 1 --embodiment libero_sim \
    --warmup 5 --iters 20 --ref-out fp8_actions.pt

# B: NVFP4 tier, compared against it (exits non-zero below the threshold)
python benchmarks/groot_n17_thor_latency.py \
    --ckpt <n17-libero-checkpoint-dir> --aux <1-camera-aux-fixture.pt> \
    --tier fp4 --views 1 --embodiment libero_sim \
    --warmup 5 --iters 20 --ref-in fp8_actions.pt --min-cosine 0.999

# A': FP8 again, to show the pair is not regime drift
python benchmarks/groot_n17_thor_latency.py \
    --ckpt <n17-libero-checkpoint-dir> --aux <1-camera-aux-fixture.pt> \
    --tier fp8 --views 1 --embodiment libero_sim --warmup 5 --iters 20

tests/_helpers/groot_n17/capture_aux_multi.py --views N captures a fixture with a restricted camera count, which is how the one-camera fixture above was produced.

Base

Rebased onto current main (after #159 / #160 / #161). The overlap with those is docs and registration only — README.md, docs/stable_api.md, CMakeLists.txt, flash_rt/api.py — and the rebase was clean. #160's RTX backbone-graph work is untouched; this PR only refreshes the GROOT N1.7 benchmark table's RTX 5090 row to the 16.61 ms full-graph number that #160 produced, since that table still carried the pre-graph 22 ms.

Docs

README.md, USAGE.md, docs/stable_api.md, and docs/benchmark_comparison.md record the tier, its flag, the precision ladder, and the measured numbers with the harness definition. docs/benchmark_comparison.md also carries NVIDIA's published TensorRT reference table for the same model family on the same board, alongside the camera-count-matched FlashRT rows and the 2-view base-checkpoint rows, each with its harness stated.

Run every DiT GEMM (fused self-attn QKV, cross-attn Q, attention output
projection, both FFN projections) as a block-scaled NVFP4 GEMM with
fused bf16 bias epilogues. At M=41 the DiT is weight-bandwidth-bound,
so halving the weight bytes against the FP8 tier is the dominant win;
the fused epilogues (bias, bias+residual, bias+GELU+fp4out) and the
fused norm->fp4 front-ends also remove most per-layer elementwise
launches.

New kernels (additive, SM100/SM110):
- cutlass_fp4_gemm_bias_bf16 / _bias_res_bf16 / _bias_gelu_fp4out_bf16:
  NVFP4 GEMMs with per-column bf16 bias, optional fused residual, and a
  fused tanh-GELU + FP4/SFA output feeding the next NVFP4 GEMM.
- quantize_fp4_dynamic_sfa_bf16_vec: vectorized bf16 -> NVFP4 + SFA.
- ada_layer_norm_fp4_sfa_bf16 / layer_norm_no_affine_fp4_sfa_bf16:
  fused DiT norms emitting FP4 + SFA directly (bit-identical to the
  two-step norm + quantize chain).

The FP4 tier is opt-in via GrootN17TorchFrontendThorFP4 (FP8 backbone
unchanged); activation quantization is dynamic per-16-element, so no
calibration pass is needed. Thor: e2e 51.4 -> 40.7 ms (action graph
27.6 -> 17.2 ms), action cosine 0.99995 vs the FP8 tier, N1.7 suite
green.
The 32 per-frame cross-KV GEMMs carry ~200 MB of bf16 weights per frame
and are weight-bandwidth-bound at M = |text| / |image| rows. Quantize
the K/V projection weights to NVFP4 (per-16 MSE block scales), quantize
the two gathered source activations once per frame (shared across all
16 cross layers), and fuse the bias adds into the GEMM epilogues.

Thor FP4 tier: e2e 40.7 -> 40.1 ms (action graph 17.2 -> 16.5 ms),
action cosine unchanged (0.99994 vs the FP8 tier). Default FP8 tier
untouched.
The FP8 backbone's non-GEMM time is dominated by small per-layer
kernels issuing 2-4B accesses (memory-latency-bound at 50-100 GB/s):
the per-head q/k RMSNorms (thousands of 128-wide rows through a
block-per-row kernel), the ViT LayerNorms, the static FP8 quantizes,
split-half RoPE, and the GQA head expand.

Add 16-byte-load rewrites as new symbols (rms_norm_fp16_vec with a
warp-per-row small-dim path, layer_norm_fp16_vec,
rope_rotate_half_fp16_vec, quantize_fp8_static_fp16_vec,
gpu_repeat_interleave_heads_vec) and route the ViT/LLM forwards through
them behind a dims flag, enabled by the FP4 frontend. Element math
matches the scalar kernels; rope/quantize/expand are bit-exact.

Thor FP4 tier: e2e 40.1 -> 36.5 ms (backbone graph 23.5 -> 19.8 ms),
action cosine 0.99995 vs the FP8 tier (unchanged). Scalar kernels and
the default FP8 tier untouched.
Two attention-path cuts on the vectorized tier:

- Masked-softmax MHA variants (attention_mha_fp16_masked /
  attention_mha_bf16_masked): the softmax reads and writes only the
  valid S_kv columns, so the per-layer full-buffer -inf logits pre-fill
  (a DRAM sweep per attention call) disappears. The causal fp16 kernel
  already masks by position internally, so its pre-fill is skipped
  outright. This also removes the long-standing graph-replay
  nondeterminism from reading uninitialized logits padding — replay
  determinism is now exactly 1.0.

- The DiT self-attention sites read the fused QKV GEMM output in place
  (token stride 3D) via a qkv_token_stride argument on the masked bf16
  variant, dropping the three per-layer split copies.

Also route the VL-self-attn / vlln norms and quantizes through the
vectorized tier. Thor FP4 tier: e2e 36.5 -> 35.6 ms, action cosine
unchanged (0.99994), determinism 1.0. Opt-in via the FP4 frontend;
default FP8 tier unchanged.
- load_model(config='groot_n17', hardware='thor', use_fp4=True) routes
  to GrootN17TorchFrontendThorFP4 (falls back to the FP8 tier when the
  flash_rt_fp4 extension is unavailable); incompatible with use_fp16.
- dit_fp8_layers=(...) keeps selected DiT layers on the calibrated FP8
  path. On the 8-sample reference set the all-FP4 default holds
  worst-sample action cosine ~0.998 vs the FP8 tier (others ~0.9999);
  the first DiT layers are the sensitive ones, and
  dit_fp8_layers=(0, 1, 2, 3) lifts the worst sample to ~0.9998 for
  about +2 ms. The cross-KV FP4 stage gains an ablation switch.
- USAGE.md documents the tier: same-session A/B/A on Thor is
  49.8/36.5/50.0 ms (1.37x), N1.7 suite green.
Route the three backbone attention sites through the vendored
FlashAttention-4 (CuTe-DSL) forward on the FP4 tier:

- LLM: causal GQA-native (pack_gqa) — one kernel replaces the
  QK^T / causal-softmax / PV cublas chain plus both K/V head-expand
  copies per layer.
- ViT: multi-view batched non-causal FA4 (views as the batch axis),
  ~2x the vendored fmha kernel at this shape.
- VL-self-attn: non-causal FA4.

The FA4 output feeds the o_proj quantize directly (no extra copy);
capture-safe under the backbone CUDA graph. Falls back to the existing
fmha/cublas chain when the FA4 runtime deps are missing.

Also fix the FA4 loader's Thor chip string to match the installed
nvidia-cutlass-dsl (4.4.x only accepts sm_110a; 4.5+ needs the sm_101a
alias).

Thor FP4 tier: e2e 36.5 -> 31.1 ms (backbone graph 19.8 -> 15.0 ms),
action cosine unchanged (0.99994), determinism 1.0.
- tests/test_groot_n17_thor_fp4_kernels.py: 13 contracts covering the
  NVFP4 GEMM epilogues (bias / bias+residual / bias+GELU+FP4-out) against
  a torch reference, the bf16 activation quantizer, the fused DiT norms
  (bit-exact vs the two-step norm-then-quantize chain), the vectorized
  backbone helpers (rope / quantize / head-expand bit-exact, norms within
  fp16 rounding), and masked-softmax MHA against the pre-filled variant
  with a poisoned logits scratch. Skips cleanly without CUDA or the
  optional flash_rt_fp4 extension.
- benchmarks/groot_n17_thor_latency.py: wall-clock per-frame
  image->action benchmark reporting the backbone / action-head split as
  a median over N iterations after warmup, with an optional
  action-cosine check against a saved reference.
- tests/_helpers/groot_n17/capture_aux_multi.py gains --views so a
  single-camera fixture can be captured for camera-count-matched runs.
- Docs: README, USAGE, docs/stable_api.md and docs/benchmark_comparison.md
  record the tier, its flag, and the measured numbers.
…ec tier

The ViT and VL-self-attn pre-attention / pre-FFN LayerNorms feed only
FP8 GEMMs, so the vectorized tier emits fp8 directly from the norm
kernel (layer_norm_fp8_static_fp16_vec) instead of writing the fp16
intermediate and re-reading it in a separate quantize launch.

Thor FP4 tier: e2e 30.9 -> 30.4 ms, action cosine unchanged
(0.99994), determinism 1.0.
…benchmark

fe.calibrate() requires the prompt state (it re-bakes act scales over
the baked alphas), so the --calib-aux path must run after set_prompt.
Verified both tiers end to end on Thor with the documented protocol
(median of 20 after 5 warmup): FP8 49.9 ms / 20.0 Hz, NVFP4+FA4
30.4 ms / 32.9 Hz, action cosine 0.99994, replay determinism 1.0.
Two remaining small-kernel inefficiencies on the vectorized tier:

- The residual adds ran two elements per thread; add a 16-byte-load
  variant (residual_add_fp16_vec) and route the ViT / LLM / VL-self-attn
  residuals and the DeepStack injection through it.
- The block-per-row norms launched a fixed 256-thread CTA, so a
  1024-wide row (128 16-byte vectors) left half the block idle through
  both reduction passes. Size the CTA to the row instead.

Thor FP4 tier: e2e 30.4 -> 30.0 ms (backbone graph 14.8 -> 14.3 ms),
action cosine unchanged (0.99994), determinism 1.0, kernel contracts
green.
Same-session A/B/A on Jetson AGX Thor (JetPack 7.2, MAXN), real 2-view
fixture, T=40, 4 denoising steps, batch 1, medians over 20 iterations
after warmup: FP8 51.6 ms, NVFP4+FA4 29.9 ms, FP8 50.2 ms (1.70x,
33 Hz), action cosine 0.99994 against the FP8 tier, bit-identical
graph replays. Includes the reproduction command.
The dit_fp8_layers option was meant to keep selected DiT layers on the
calibrated FP8 path, but it never took effect: with every layer marked
for FP8 the captured graph still produced the all-NVFP4 result (action
cosine 0.99759 against the FP8 tier, identical to the all-FP4 default),
so the per-layer gate in dit_forward was not reached at replay time.

Rather than ship a knob whose documented behavior the implementation
does not provide, remove the option, its per-layer gate, and the
benchmark flag. The tier's precision is what the all-NVFP4 path
measures: on the 8-sample reference set 7 of 8 samples hold action
cosine >= 0.9993 against the FP8 tier, with a worst sample of 0.9976.
Docs now state that instead of the ladder.

No performance change (e2e 29.95 ms, action cosine 0.99994,
determinism 1.0); N1.7 suite unchanged.
With the precision ladder gone, the FP8 self-attention branches can no
longer see the FP4 tier's in-place fused-QKV slots, so their split-copy
guards were dead. Restore the unconditional copies there and keep the
guard only on the FP4 branch that owns the flag.

Verified on Thor: FP4 tier e2e 29.90 ms with action cosine 0.99994 and
determinism 1.0; FP8 tier 50.03 ms and bit-identical to the pre-change
baseline output (cosine 1.000000, max diff 0.0).
Measured on the GR00T-N1.7 LIBERO fine-tune (libero_10) with one
camera, 4 denoising steps, batch 1, medians over 20 iterations after
5 warmup iterations — the same harness definition NVIDIA publishes for
their TensorRT numbers on this board. Fixture captured from a real
libero_10 observation through the official preprocessing path.

Same-session A/B/A on Jetson AGX Thor (JetPack 7.2, MAXN):
FP8 36.76 ms -> NVFP4+FA4 23.65 ms -> FP8 36.85 ms, i.e. 36.8 ms
(27 Hz) vs 23.7 ms (42 Hz). Action cosine between the tiers on that
fixture is 1.000000 and graph replays are bit-identical on both.

The 2-view base-checkpoint rows are kept for reference and now carry
their harness explicitly, so the two are not mistaken for one another.
The benchmark table's RTX 5090 row now reads 16.6 ms (2-view base
checkpoint, backbone graph replayed via infer(aux=...)) instead of the
pre-graph 22 ms. USAGE's per-stage RTX table is labeled as the eager
backbone path it measures, with the full-graph E2E stated alongside it,
so the two no longer read as contradicting each other.
…pers

Review findings on the masked-softmax MHA and its build placement:

- S_kv above 1024 was silently wrong. The register-tiled softmax caps at
  SMM_MAX_COLS columns and the dispatch never instantiated more than 32
  iterations, so columns past 1024 were neither read nor written while
  the binding accepted any length (S_q=1, S_kv=1025, NH=1, HD=16 scored
  cosine 0.646 against torch SDPA). Rows wider than the tiled path's
  reach now run a multi-pass kernel that holds no per-column registers,
  so any S_kv is correct: the same case now scores 0.9999998 (fp16) and
  0.9999836 (bf16). Pinned at S_kv = 1024 / 1025 / 2048 in both dtypes.

- attention_mha_masked.cu and vec_fp16_backbone.cu were compiled into
  the shared flash_rt_kernels module on every architecture, though their
  element math targets SM100-class memory behaviour and only the GROOT
  N1.7 Thor path calls them. They now build under ENABLE_SM100_CUTLASS
  with FLASHRT_HAVE_THOR_VLA_KERNELS, and their bindings are guarded by
  the same define so source and binding drop together elsewhere. The
  NVFP4 frontend probes for the bindings and fails naming the build flag
  instead of dying inside graph capture.

- Add load_model routing contracts for the tier (selection, fallback
  when flash_rt_fp4 is absent or reports no NVFP4, rejection of
  use_fp4 with use_fp16, unchanged default). Frontends are stubbed, so
  they need no GPU or checkpoint.

- benchmarks/groot_n17_thor_latency.py gains --min-cosine so the A/B
  precision check can gate rather than only print.

Thor, unchanged from before the fix: FP4 tier 29.99 ms, FP8 tier
50.13 ms and still bit-identical to the baseline output (cosine
1.000000, max diff 0). N1.7 suite 164 passed, 36 skipped.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant