GROOT N1.7 update: Thor NVFP4 + FA4 performance tier - #163
Merged
Merged
Conversation
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.
LiangSu8899
force-pushed
the
groot-n17-update
branch
from
August 5, 2026 06:27
fdb34ae to
006fb8f
Compare
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.
LiangSu8899
force-pushed
the
groot-n17-update
branch
from
August 5, 2026 06:28
006fb8f to
44f5e1d
Compare
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.7LIBERO 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 reallibero_10observation through the official preprocessing path (JetPack 7.2, MAXN):use_fp4=True)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-3Bcheckpoint 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_layersprecision 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
S_kvcolumns, so the per-layer full-buffer-inflogits 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.pack_gqa, VL-self-attn) run the vendored FlashAttention-4 (CuTe-DSL) forward, falling back to the existing fmha/cuBLAS chain when thethor-fa4runtime deps are absent.Shared-code changes
Only two files outside
groot_n17carry behavior:flash_rt/api.py—use_fp4=Truewithconfig="groot_n17"on Thor routes to the new frontend and falls back to the FP8 tier whenflash_rt_fp4is unavailable. Pi0.5'suse_fp4routing is untouched.flash_rt/hardware/thor/fa4_backend.py— the Thor chip string now follows the installednvidia-cutlass-dsl(4.4.x only acceptssm_110a; 4.5+ needs thesm_101aalias). Previously the loader always requestedsm_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) joinflash_rt_kernels, but only on SM100-class builds — CMake adds them underENABLE_SM100_CUTLASSand definesFLASHRT_HAVE_THOR_VLA_KERNELS, and their bindings incsrc/bindings.cppare 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
csrckernels 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 atS_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 optionalflash_rt_fp4extension.tests/test_groot_n17_thor_fp4_routing.py— 5 contracts on the publicload_modelbehavior the tier adds:use_fp4=Trueselects the NVFP4 frontend, a missing or NVFP4-lessflash_rt_fp4falls back to FP8,use_fp4withuse_fp16is 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_noiseneeds a newertransformersfordenormalize_action; it fails the same way onmain).Reproduce
The A/B/A above is three runs of the same command with the tier flipped;
--ref-out/--ref-incarry the FP8 output forward so the FP4 run reportsthe cross-tier action cosine, and
--min-cosineturns it into a gate:tests/_helpers/groot_n17/capture_aux_multi.py --views Ncaptures 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, anddocs/benchmark_comparison.mdrecord the tier, its flag, the precision ladder, and the measured numbers with the harness definition.docs/benchmark_comparison.mdalso 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.