Repository navigation
[Fix] [NVIDIA] modelopt: Keep SM12x FP8 batch-invariant in deterministic mode - #44861
yibol9768-alt wants to merge 3 commits into
Conversation
117a332 to
ff5adcd
Compare
…tic mode Co-authored-by: OpenAI Codex Signed-off-by: Yibo Liu <240359763+yibol9768-alt@users.noreply.github.com>
ff5adcd to
f1cae93
Compare
Keep N/K reduction tiles fixed while using row tiles suited to decode and prefill. Cover biased strided BF16/FP16 inputs under CUDA Graph replay. Co-authored-by: OpenAI Codex Signed-off-by: Yibo Liu <240359763+yibol9768-alt@users.noreply.github.com>
…roject#44861, part 5a) Port the deterministic-gated half of PR sgl-project#44861 (net diff of the two upstream commits f1cae93 + 5f90880 from the fork-point 9c7b338, excluding kernels/ops/gemm/fp8_kernel.py, which lands separately as part 5b). Why this stack wants it: our ModelOptFp8LinearMethod.apply() carried ZERO batch-invariance code, so with a ModelOpt FP8 (or mixed NVFP4) checkpoint on SM12x the same rows score differently in prefill vs decode. That is the exact class of divergence the prefill-vs-decode KL consistency probe measures, so the NVFP4 arm could never be checked with --enable-deterministic-inference. Landing (3 files, +120/-1, `git apply -3` from the squashed net diff -- all three hunks applied CLEANLY, zero conflicts): * fp8_utils.py: new apply_fp8_linear_deterministic() (+33), placed exactly at the PR's anchor (between use_aiter_bpreshuffle_gemm() and apply_fp8_linear_bmm_flashinfer()). It quantizes with the static per-tensor scale and calls triton_scaled_mm() with BLOCK_SIZE_N=64 / BLOCK_SIZE_K=128 PINNED and only BLOCK_SIZE_M tiered on the row count (16 / 32 / 128), so the fp32 accumulation order of every output element is independent of batch size: rows are bit-identical between a full prefill and a shorter suffix batch (the PR's own test asserts rtol=atol=0). Our in-tree triton_scaled_mm already honours caller-supplied tile sizes when use_heuristic=False (the same entry point our tuned-channelwise FP8 path uses at fp8_utils.py:2433/:2438), and static_quant_fp8(..., repeat_scale= False) keeps x_scale per-tensor, so nothing in the helper's argument contract depends on M. * modelopt_quant.py: import get_exec alongside get_platform (:68) and `apply_fp8_linear_deterministic`, and insert the `use_sm120_fp8 and get_exec().deterministic.enable_deterministic_inference` branch in apply() between the Marlin branch and the SM12x try_sm120_fp8_linear facade -- i.e. deterministic mode deliberately gives up the small-M fast path in exchange for a fixed reduction schedule. Anchors verified in our tree: self.use_sm120_fp8 is set at :559 (cuda_capability[0] == 12) and get_exec().deterministic.enable_deterministic_inference is the established read path here (20+ in-tree users, e.g. fp8.py:1267 and moe_runner/triton_utils/fused_moe_triton_config.py:72). * test/registered/kernels/ops/gemm/test_sm120_fp8_linear.py: the PR's 14 new tests (+74), byte-identical (zero drift on that file). NO extra gating needed from us: the file already ends its import block with `if not (torch.cuda.is_available() and get_device_capability() == (12, 0)): pytest.skip(..., allow_module_level=True)`, so on this CPU-only box it collects as `1 skipped` both before and after (verified). This box IS SM120, so those 14 tests will actually run in a GPU window -> recorded as a GPU debt (cache-suffix bit-equality + CUDA-graph replay with bias and a strided input). Fork survival: fp8_utils.py (+41/-163 drift) and modelopt_quant.py (+210/-61 drift) both took the PR hunks at their own anchors; the SM12x facade branch, sm120_fp8_alpha precompute, _can_use_flashinfer_bmm/use_flashinfer_bmm path and the entry-190 Marlin/pure-FP8 gates are untouched (hunks are additive). Behaviour with --enable-deterministic-inference OFF (prod fp8 arm) is unchanged by this commit: the new branch is gated on the flag. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> (cherry picked from commit 5f90880)
…art 5b) The second half of PR sgl-project#44861, landed as its own commit because it is NOT gated on --enable-deterministic-inference and therefore touches a shared kernel. kernels/ops/gemm/fp8_kernel.py::scaled_mm_kernel is launched from exactly one place, triton_scaled_mm() (:1112), which has three in-tree callers, all in srt/layers/quantization/fp8_utils.py: the new apply_fp8_linear_deterministic() (2275) and the two tuned-channelwise w8a8 paths (2466 / 2471). Why the explicit `other=0.0` matters: `tl.load(ptr, mask=...)` without `other` leaves masked lanes "don't care" per Triton semantics, and those lanes are fed straight into `tl.dot(a, b, accumulator)`. A masked-out ROW or COLUMN only pollutes outputs that are discarded anyway, but the K-tail (masks_k) pollutes VALID outputs: every masked k lane contributes a_garbage * b_garbage to the fp32 accumulator of every row in the tile, and an uninitialised e4m3 register can even hold a NaN pattern. Pinning the masked value to 0.0 makes the reduction over a ragged K tail exact and reproducible, which is precisely what the deterministic helper needs (it fixes BLOCK_SIZE_K=128, so any K that is not a multiple of 128 hits the tail -- e.g. the PR's own 96x80 case), and it also matches what every other reduction kernel in this file does. Does any prod behaviour change? No, on two independent grounds: * The masked lanes only exist when K % BLOCK_SIZE_K != 0 (or N % BLOCK_SIZE_N != 0, which only affects discarded columns). The tuned-channelwise configs in-tree use BLOCK_SIZE_K in {64,128,256,512,1024} against K in {4096,12288} (python/sglang/kernels/ops/gemm/configs/*fp8_w8a8_channelwise.json), all of which divide exactly, so those launches mask nothing and compile to the same arithmetic. Prod's fp8 arm additionally pins --fp8-gemm-backend cutlass and the only channelwise JSONs present are for NVIDIA_L40S, so get_w8a8_channelwise_fp8_config() returns None on this device family today and the triton branch is not even taken. * Everything else that reaches this kernel is inside the new deterministic branch from the previous commit (flag OFF => unchanged). So for the fp8 prod arm this is a no-op-by-construction change; it becomes load-bearing the moment a ragged-K shape runs through triton_scaled_mm (the deterministic NVFP4/FP8 arm, or any future tuned config with a non-dividing K). Landing: `git apply -3` of the net-diff file selection, CLEAN; zero drift -- the pre/post blob ids (ff32551..ff7c9f5) are byte-identical to upstream's. Its tests live in test/registered/kernels/ops/gemm/test_sm120_fp8_linear.py, which already module-level-skips off SM120 (verified `1 skipped`). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> (cherry picked from commit f1cae93)
|
On a second SM12x part, an RTX PRO 6000 Max-Q (SM120) with nvidia/Qwen3-14B-FP8, head 5f90880 makes cold and prefix-cache-hit selected-token logprobs identical in all 12 comparisons, where main differs in all 12 (max abs delta 0.5 to 2.375). The CUDA graph cost matches the 5090 numbers the body gave for that head at batch 1 and is higher at batch 4. All of this is for 5f90880; I haven't run the current three-stage head 6a7695d.
The comparisons are the body's cases (prefixes 128/1024/4096 at batches 1/4/2, two repetitions) in one eager and one graph boot per arm. Times are medians of five, and the ranges don't overlap in any graph cell. In eager mode PR / main runs from 0.89 to 1.055, but those boots shared the card with another user's job; the graph boots had it alone. The PR's Setup: the body's engine settings (deterministic inference, Triton attention, TP1, BF16), fp8 KV, the PR diff applied to main 4fc86f7. Logs and scripts: https://gist.github.com/waynehacking8/e8474d2b9449f657396df5a040566e16 |
Use three pipeline stages instead of two without changing the N/K tiles, row tiles, accumulation/scaling order, or SM12x deterministic dispatch. On one RTX 5090 with the complete official SGLang nightly, the existing 137-case SM120 suite passes and 288 additional tensor comparisons are bit-exact to the two-stage path. Qwen3-8B-FP8 preserves all 128 complete answer texts/token sequences and 18 paired cache comparisons. ABBA graph boots reduce the fixed-work matrix medians by 2.4-10.3%; eager times are effectively unchanged. Local results are bounded by the single device/model and do not replace upstream CI. Co-authored-by: OpenAI Codex Signed-off-by: Yibo Liu <240359763+yibol9768-alt@users.noreply.github.com>
|
Validated on a hybrid GDN model, which the description leaves out of scope: it fixes this case. Setup. One RTX PRO 6000 (SM120), TP=1. Image Result,
A prefill-only probe (cold, prefix hit with a 37-token suffix, cold again; 101 tokens) gives -6.1011090279 all three times with the PR. Where the difference came from on this model (diagnosed independently before finding this PR, by dumping per-layer tensors for the hit tokens, cold vs hit):
Not measured: throughput of the deterministic Triton path on this model. |
Motivation
On SM12x, ModelOpt static-FP8 linears can change reduction schedules between cold prefill and a shorter prefix-cache-hit batch despite deterministic inference being enabled. The same input sequence then receives different scores.
Related to #41351's ModelOpt path. Credit to
furedericca-labfor the diagnosis and initial fallback, andtwu3202for identifying the small-M gap. This does not claim to resolve every hybrid/GDN issue in that thread.Modifications
Accuracy Tests
Validated 2026-10-22 on one RTX 5090, 400W, TP=1, using the complete official SGLang
0.5.23.dev20261020+g55b2aef7adnightly: PyTorch 2.14.1, Triton 3.8.0, FlashInfer 0.7.2, sglang-kernel0.4.10 and its official Rust radix cache. Complete source files are installed before each process starts; normal package imports are used, with no function/AST injection. This is official-nightly validation, not a latest-main rebuild or upstream CI.Model:
nvidia/Qwen3-8B-FP8@2cebc4c89e25abc17668c81b01dceaf3d8b914d5, BF16 activations, automatic FP8 KV, deterministic mode and Triton attention.The 128 questions reuse previous data and test preservation of the existing PR's outputs. They are not an independent quality evaluation, an accuracy gain, or evidence of statistical equivalence to upstream. Earlier upstream/candidate checks remain disclosed:15/16→16/16 on a16-question nightly subset, and119/128→117/128 on the older mixed runtime. See the prior nightly results and historical128-question results.
The unmodified nightly FP8 path still shows nonzero cold/cache selected-logprob deltas in the fresh Graph control. An independent RTX PRO 6000 Max-Q / Qwen3-14B-FP8 check also corroborates the fix at prior head
5f90880cc2; it predates this stage-only refinement. The original hybrid TP=2 model, SM121 and torch.compile have not been tested locally.Raw paired results, full generated outputs, tests, source snapshots, scripts and hashes.
Speed Tests and Profiling
The isolated probe identifies GEMM as the main component cost in these cases; increasing pipeline depth is faster for several projection shapes while all 64 explored cases remain exact. These cache-evicted microbenchmarks are diagnostic; the end-to-end results below determine the performance claim.
32 forced output tokens per request, one warmup and five measured samples per cell per boot, cache flushed before requests, passive cooling to<=55C outside timing. PR-arm Graph boot order is A(two stages), B(three stages), B, A. Table medians pool ten samples per PR arm; per-boot medians, ranges and every sample are retained. The upstream Graph reference and each eager arm have one boot/five samples. Graph capture covers decode 1/2/4 and prefill 4/128/256/1024/2048/4096/8192; unsupported partial prefills may fall back to eager.
The stage-only update reduces this Graph matrix's pooled median wall times by 2.4–10.3%, most consistently on the short prompts. The 2048/1 cell varies between boots: current medians0.5285/0.5895s, candidate0.4982/0.5434s. Eager differences are within about 0.5%, so no meaningful eager speedup is claimed.
Against the single fresh upstream Graph reference, the three-stage path ranges from 2.1% lower to9.2% higher wall time. This does not eliminate the determinism/cost tradeoff or establish a general speedup. Earlier 5.7–25.8% nightly and 5–20% mixed-runtime Graph costs describe the prior two-stage measurements and remain in their historical evidence. All ten successful jobs stayed below 72 C, peaking at 66C with the 400W limit unchanged.
Full samples, boot medians and measurement limits.
Duplicate check: inspected #34429 (small-M dispatch), #42865 (block/MXFP8), #38921 (SM89 prequantized inputs) and #44076 (GDN chunk alignment); these do not implement this static per-tensor deterministic path. Original pinned diff audit.
AI assistance was used for diagnosis, implementation, validation and this description. The human contributor reviewed the original fix and authorized this performance investigation and PR update.
Checklist
Review and Merge Process
Maintainer review and upstream CI follow the normal project process. Local validation and independent reproductions do not constitute upstream CI or maintainer approval.
CI States
Latest PR Test (Base): ❌ Run #39746823806
Latest PR Test (Extra): ❌ Run #39746823720
Latest PR Test (AMD ROCm 10): ❌ Run #39746823779