Skip to content

feat(sglang): nvfp4 quantized rollout and refit - #3615

Open
Kh4L wants to merge 22 commits into
NVIDIA-NeMo:mainfrom
Kh4L:recut/sglang-nvfp4
Open

Kh4L wants to merge 22 commits into
NVIDIA-NeMo:mainfrom
Kh4L:recut/sglang-nvfp4

Conversation

@Kh4L

@Kh4L Kh4L commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Original work: #3426 by @xiuhu17. Re-cut at his request; his original authorship and sign-offs are preserved.

Adds NVFP4 SGLang rollouts and Megatron weight refit on top of #3614. The shared iterator expands weight/scale groups, preserves padded NVFP4 storage, refreshes graph-captured scales in place and rejects semantic refit failures. MoE and FP4 GEMM runner settings can explicitly select CuteDSL.

Startup requires a locally converted checkpoint with the expected MoE-only layout marker. Incompatible external ModelOpt NVFP4 layouts are rejected before serving starts.

Stack: #3613 has merged; #3614 remains the parent. The full diff against main includes MXFP8. Review the NVFP4 increment.

Head 035a6003f219f324ab2dc136e381ef95849f1b0f restacks on refreshed #3614 and main d48b2e0f3d7fc4891b418ae603c8e8fa71482b9c. Quantization modules are unchanged. Dependency files and submodule pins exactly match main, including Transformer Engine 2.18; this PR no longer introduces a separate Transformer Engine version bump.

Validation on the refreshed head:

  • 118 focused CPU tests pass, covering shared MXFP8/NVFP4 checkpoint handling, paired scales, layout rejection, backend forwarding, semantic update failures and cache-policy behavior.
  • Repository-pinned Ruff 0.9.9, import sorting, formatting and diff checks pass across the PR's Python files.
  • CPU tests use normal module imports on macOS, with project conftest/config disabled. This is supplementary coverage, not full CI or a fresh GPU run.

Historical GPU evidence: HSG job 6593575 completed successfully on 3 nodes / 12 GB200 GPUs at 736472843ba46eca4c16f127009452bdb148f090, with four optimizer steps and four NVFP4 broadcast refits. This predates the current main refresh and does not validate its dependency updates.

Current-head maintainer CI and review remain required. A maintainer must run /ok to test 035a6003f219f324ab2dc136e381ef95849f1b0f.

@copy-pr-bot

copy-pr-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@Kh4L
Kh4L marked this pull request as ready for review August 13, 2026 01:33
@Kh4L
Kh4L requested review from a team as code owners August 13, 2026 01:33
@Kh4L
Kh4L force-pushed the recut/sglang-nvfp4 branch 4 times, most recently from 6403701 to fbd2b31 Compare August 14, 2026 05:40
@Kh4L
Kh4L force-pushed the recut/sglang-nvfp4 branch from fbd2b31 to 4f56b14 Compare August 14, 2026 19:14

@Kh4L Kh4L left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review (/review-pr-team), per the contributor self-review policy

Scoped to this PR's own increment (35d910009..4f56b148a — the nvfp4 feature, the TE 2.17 bump, the lock regen, and my two rebase fixes). The three layers below have their own posted self-reviews and were not re-reviewed.

The headline: the lint gate is red and the two tests guarding the pair-completeness logic have never executed. pytest is used at test_megatron_sglang_weight_iterator.py:240,251 but never imported — ruff fails with F821 twice, and both tests error with NameError instead of running. Fixed in 6e6599998. Beyond that, the offline/online consistency I most expected to be wrong is actually correct, and the dependency bump is justified and clean — but there are five correctness issues in the new code and a checkpoint-compatibility gap that stops stock ModelOpt NVFP4 checkpoints from being recognized at all.

Fixed in 6e6599998

The missing import pytest. Verified rather than assumed: same harness, pre-fix file → 2 failed, 5 passed (NameError: name 'pytest' is not defined); post-fix → 7 passed. ruff check now clean across all 15 changed files. The two dead tests were the only coverage for _iter_nvfp4_hf_named_tensors' incomplete-pair and duplicate-role guards, so replacing either raise with a bare return was indistinguishable from the shipped state.

Provenance note, since I'd rather say it than have it found: this is my own miss. My commit 4f56b148a says these fixes were "surfaced by running the repo's own pre-commit against this layer" — I ran it against the production files and not the tests, which is exactly how an F821 in a test file survived a commit whose stated purpose was fixing an F821.

Left inline for your judgement

Seven items, in rough severity order. All are marked traced-vs-executed inline.

  1. A stranded gate/up half is detected only after part of the refit has already shipped. The completeness check sits at the end of the generator, but buckets are consumed lazily and transmitted as they're produced — so the engines are left holding a mix of new and stale weights with no rollback. The name-axis skip is correctly symmetric; this is the dtype/rank rejections, which have no pair-level counterpart.
  2. should_quantize_nvfp4 raises where all three of its sibling checks return False. The same expert tensor is fatal or harmless depending only on whether the user happened to list it in modules_to_not_convert.
  3. The converted checkpoint advertises FP8 KV-cache quantization it does not contain. SGLang reads kv_cache_scheme → attaches ModelOptFp8KVCacheMethod to every RadixAttention → finds no k_scale/v_scale → silently substitutes 1.0. Inert at kv_cache_dtype: auto, uncalibrated and unwarned otherwise. mxfp8 emits no KV metadata at all, so this is new here.
  4. Stock ModelOpt NVFP4 checkpoints are never recognized. is_existing_nvfp4_checkpoint reads only config.json, but real releases put the block in hf_quant_config.json — the file SGLang itself declares. Confirmed against nvidia/Qwen3-235B-A22B-NVFP4 and nvidia/DeepSeek-R1-0528-NVFP4: both have no quantization_config in config.json. The converter then tries to re-quantize an already-NVFP4 checkpoint and dies, after copying its metadata into the cache dir.
  5. exclude_modules entries are globs, matched with in. Real ModelOpt checkpoints ship "model.layers.0.self_attn*"; plain substring containment never matches, so those entries are silent no-ops for skipping and make validate_checkpoint_high_precision_layout pass vacuously.
  6. _contiguous_pair_view doesn't require canonical strides. A size-1 leading dim can carry any stride[0] and still report contiguous, which defeats both the stride proxy and the adjacency check — and unlike every other precondition failure it returns a wrong result rather than falling back to torch.cat.
  7. weight_scale_2 is re-derived from a local copy of TE's formula rather than read back from the quantizer. The copy is faithful today (I diffed it against TE's reference recipe, see below). But the block scales come from TE while the global scale comes from our reimplementation, and the two must agree exactly or every dequantized weight is off by a constant factor. Any upstream change — pow_2_scales, a different row_scaled_nvfp4 default, a tweak to the clamp — silently desynchronizes them, and no test would catch it.

The thing a smoke test has to prove

SGLang re-runs process_weights_after_loading on every quantized module after each refit (model_runner.end_weight_update → postprocess_weight). Its counterpart restore_weight dispatches to restore_weights_before_loading, and ModelOpt defines no such hook — so NVFP4 MoE re-finalizes over already-finalized state. Some transforms guard for this deliberately (the W13 deinterleave carries a sticky _w13_deinterleaved flag); the CuteDSL-v2 branch applies interleave_w13_halves unconditionally. Whether that corrupts depends on whether the refit fully overwrites both params first, and nothing here establishes it. The previously validated MoE hot-reload work ran on flashinfer_trtllm, which takes the other branch — so the CuteDSL path is unexercised. This needs ≥3 refits, not 1; a single-refit run cannot tell "correct" from "corrupts on re-finalize".

Verified good — recording these so nobody re-litigates them

  • Offline and online quantization selection agree. This is the property most likely to silently break an RL run, so I executed it rather than reasoning. The raw predicates do diverge — with extra_high_precision_layers_hf: ["down_proj"] the offline converter quantizes 0 tensors while the online iterator quantizes 8, because offline skips at MoE-container granularity and online at pair granularity. But _sync_checkpoint_ignore merges the checkpoint's container-level ignore list back into modules_to_not_convert, and the synchronizer re-reads that config fresh from the driver-side generation object every refit, so the mutation propagates. Replaying the merge with the real functions: 4/4 cases agree. No test pins this reconciliation, though — the two shipped tests each cover one side in isolation.
  • The TE bump is necessary, not incidental. nvfp4_use_4over6, nvfp4_e4m3_max and nvfp4_4over6_err_mode are all new in v2.17 (v2.16 adds only row_scaled_nvfp4), so the >=2.17 requirement in the docstring is exactly right. Tag v2.17 resolves to 2e559f06…, matching the lock; VERSION.txt says 2.17.0, matching the hand-declared metadata; v2.17.0 does not exist, so @v2.17 is the correct spelling. TE's requires-dist is unchanged between 2.15 and 2.17, so the 12-line lock diff is correct rather than suspicious. No second, now-stale TE pin exists anywhere in the repo. v2.16 is the only breaking release crossed and the repo uses none of what it removed. Moving from the release_v2.15 branch to the v2.17 tag is a real improvement — release_v2.17 has already drifted past its own release.
  • The Transformer Engine contract holds. Two things here could have been quietly catastrophic, so I checked both against upstream rather than trusting the shape asserts. _rowwise_scale_inv is a plain row-major block-scale matrix, not a swizzled/tiled one (with_gemm_swizzled_scales defaults False), so [:num_rows, :num_cols // 16] reads the region it intends to. And _amax_rowwise is a per-tensor scalar here, so amax_rowwise.reshape(-1)[0] is right: TE's reference recipe only reshapes the global amax to a per-row [m, 1, 1] under row_scaled_nvfp4, and this PR passes row_scaled_nvfp4=False. nvfp4_global_encode_scale_te also reproduces TE's formula exactly — same e4m3_max * 6.0 / amax, same finfo.max clamp, same zero→one guard, same reciprocal for the decode scale.
  • Cross-shard deferred-pair handling is correct and genuinely tested, and the emitted companion tensors have the right ranks and dtypes.

Two build notes worth a line in the PR description

  • TE v2.17.1 was available before this lock was regenerated. Published 2026-08-07; it fixes checkpoint "extra state" being unpickled for stateless recipes and adds an explicit env-var guard. A framework that loads checkpoints routinely probably wants it. Caveat: the fix introduces an opt-in env var, so it may not be purely drop-in.
  • NeMo-Automodel declares transformer-engine[pytorch]<=2.11.0, and NeMo-RL requests nemo-automodel[moe]. The global override-dependencies already overshot that ceiling at 2.15; this widens it from +4 to +6 minors. Pre-existing, not caused here, but crossing 2.16 makes it worth stating.

Tests and docs

No test exercises any real quantization math — every one monkeypatches the leaf quantizer. Three mutations survive the full suite undetected: nvfp4_global_decode_scale_te → constant 1.0; nvfp4_weight_e4m3_max → hardcoded 448 (ignoring NVTE_NVFP4_4OVER6* entirely); _contiguous_pair_view → always None. All three are pure CPU functions that need neither a GPU nor Transformer Engine, so this gap is closable today.

Relatedly, NVTE_NVFP4_4OVER6* is undeclared cross-process configuration that scales every emitted weight_scale_2: offline conversion runs on the driver, online refit in the Megatron worker, and nothing checks the two saw the same environment.

There is no documentation for SGLang rollout weight precision anywhere — not for nvfp4, and not for mxfp8 below it. docs/fp8.md is not the natural home (different backend, different mechanism); docs/guides/quantization-aware-rl.md is, and it already tabulates verified configurations. Which raises the point worth making explicitly: NeMo-RL already has a real-quant NVFP4 rollout lane — ModelOpt + vLLM, policy.generation.real_quant: true — with published convergence results on the very models this targets, including a 300-step Qwen3-30B-A3B W4A4 run and a Nano3 W4A16 run. That lane is vLLM-only so an SGLang implementation is legitimately needed, but this PR adds a second, independent NVFP4 packing implementation without saying how the two relate or why the existing one couldn't be reused. Its real_quant_ignore list is also the existing precedent for the skip-list design here. Separately, docs/guides/refit.md:26 now describes constraints that quantized SGLang violates.

Environment caveats

No GPU, and sglang/flashinfer/transformer_engine are not installed, so nothing involving their runtime behaviour was executed by anyone — SGLang and TE findings are traced against fetched upstream sources at the pinned revisions, and quoted inline. pytest cannot run the repo suite here (tests/unit/conftest.py imports ray), so test executions used a stubbed module loader; the counts above are real runs of the real test bodies. Nothing in this PR has run on hardware, which the PR body already says.

Generated by Claude Code

@Kh4L
Kh4L force-pushed the recut/sglang-nvfp4 branch 4 times, most recently from cc9dcb7 to f9c297c Compare August 18, 2026 19:24
@Kh4L
Kh4L requested a review from a team as a code owner August 18, 2026 19:24
@Kh4L
Kh4L force-pushed the recut/sglang-nvfp4 branch from f9c297c to ba5e9be Compare August 18, 2026 19:29
@github-actions github-actions Bot added the Documentation Improvements or additions to documentation label Aug 18, 2026
@Kh4L
Kh4L force-pushed the recut/sglang-nvfp4 branch from ba5e9be to 74c4836 Compare August 26, 2026 03:24
@Kh4L
Kh4L requested a review from a team as a code owner August 26, 2026 03:24
@Kh4L Kh4L added the CI:L2 Run doctests, unit tests, functional tests, and convergence tests label Aug 26, 2026
@Kh4L

Kh4L commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 74c4836

@Kh4L
Kh4L force-pushed the recut/sglang-nvfp4 branch from 74c4836 to cdd2097 Compare August 26, 2026 16:31
@Kh4L

Kh4L commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test cdd2097

@Kh4L

Kh4L commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 679ab0c

@Kh4L

Kh4L commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Current head 7d429c388bdaade042f6aa0e108abd289fddc6b1 is restacked on refreshed #3614 and adds the NVFP4 declaration to the failing runner-backend fixture. Compared with HSG-validated 736472843ba46eca4c16f127009452bdb148f090, only five SGLang unit-test fixtures changed, so job 6593575 remains applicable to the runtime tree. Fresh CI is triggered on the exact head.

@Kh4L

Kh4L commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test d98ccf0

xiuhu17 and others added 20 commits October 3, 2026 00:35
Enable scheme=mxfp8 under policy.generation.sglang_cfg.quantization:

- mxfp8_setup.py: offline HF->MXFP8 checkpoint conversion with cache
  fingerprinting (ensure_mxfp8_checkpoint), run before SGLang boot
- mxfp8_quantization_core.py: quantize_mxfp8 (flashinfer), dynamic
  skip-substring policy, scale-key helpers shared by offline conversion
  and online refit
- MegatronSGLangHfWeightIterator: quantize eligible tensors during the
  AutoBridge walk, emitting (weight, weight_scale_inv) pairs sized into
  post-transformation buckets
- wire target_precision/sglang_quantization_cfg through the megatron
  refit paths; SglangQuantizationConfig type + exemplar YAML block

Signed-off-by: zhihaow6 <zhihaow6@illinois.edu>
(cherry picked from commit d0cf391)
Same gate as the equivalent commit lower in this stack: the mxfp8 quantization
modules and their tests fail `ruff format --check` under the version pinned in
.pre-commit-config.yaml (v0.9.9), which breaks lint-check and therefore the
whole pipeline.

Signed-off-by: Serge Panev <spanev@nvidia.com>
…ts, pass ruff

Four self-review findings on the mxfp8 increment.

`SKIP_WEIGHT_SUBSTRINGS` held "router" and "mlp.gate.", which between them
miss most MoE router spellings. A top-k router is a discrete argmax, so
quantization error flips expert selection outright -- a rollout/trainer
policy divergence, which is the one failure an RL run cannot absorb.
Widened to `.gate.` plus `shared_expert_gate.`, which covers the class
rather than the families we happened to notice: `mlp.gate.` (Qwen3-MoE,
DeepSeek), `block_sparse_moe.gate.` (Mixtral), `mixer.gate.`
(NemotronH/nanov3) and Qwen2-MoE's separate `shared_expert_gate`. The
dots are load-bearing -- `gate_proj`, `gate_up_proj` and
`shared_expert.gate_proj` are ordinary MLP weights and stay quantized.

Worth stating why the narrow version of this fix was wrong: it enumerated
`block_sparse_moe.gate.` and `shared_expert_gate.`, which left
`backbone.layers.N.mixer.gate.weight` -- the router of
`NVIDIA-Nemotron-3-Nano-30B-A3B`, the model this work targets -- still
quantized. Verified against that checkpoint's real tensor names and
shapes, and the repo already draws the same line in
`DEFAULT_NVFP4_IGNORE` and the nanov3 recipe's `real_quant_ignore`.

`CONVERTER_VERSION` goes to 5 because of that skip-list change. The cache
directory hashes the config knobs via `_quantization_fingerprint`, but not
the module-level selection constants, so without the bump a checkpoint
converted under the old list would be reused with its router already
quantized -- while the refit, now skipping it, sends BF16 into a slot the
engine loaded as MXFP8.

`convert_mxfp8` opened `model.safetensors.index.json` unconditionally, so
any checkpoint whose BF16 weights fit in one file died with a bare
FileNotFoundError. That is every shipped SGLang recipe: all five run
Qwen2.5-Math-1.5B-Instruct, which ships a single `model.safetensors` and
no index. `_checkpoint_weight_names` in the same module already handles
both layouts; the converter now takes the same branch and derives its
shard list from the directory when there is no index.

`test_quantized_refit_requires_megatron_backend` called only the two
non-raising cases, so the branch the guard exists for was never
exercised -- neutering the function to a bare return left the whole suite
green. The rejecting case is now its own test.

Also: `ruff check` was failing on the increment (unused `Iterable` and
`pytest` imports, three import-ordering violations). The earlier
`chore(sglang): satisfy the repo-pinned ruff formatter` commit only
covered `ruff-format`, so `lint-check` -- which the whole pipeline
`needs` -- would still have failed. And two of the three new modules
carried a 2025 copyright header in a 2026 commit.

Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: zhihaow6 <zhihaow6@illinois.edu>
(cherry picked from commit b304253)
`test_megatron_sglang_weight_iterator.py` uses `pytest.raises` in the two
nvfp4 tests that cover the incomplete-pair and duplicate-role guards, but
the file never imports pytest. Two consequences, both bad:

`ruff` fails the lint gate with `F821 Undefined name 'pytest'` twice, so
`lint-check` is red on this layer.

More importantly the two tests never execute -- they raise `NameError`
before reaching `_iter_nvfp4_hf_named_tensors`, so they are the only
coverage for those two `raise` paths and neutering either one to a bare
`return` was indistinguishable from the shipped state. Measured with a
stubbed module loader: before, `2 failed, 5 passed`; after, `7 passed`.

Found by running `pre-commit` over the *test* files as well as the
production files -- my earlier fix on this layer only covered the latter,
which is how an F821 survived a commit whose purpose was fixing an F821.

Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
Keep GPU-only Transformer Engine conversion out of the Ray driver, require an explicitly marked NeMo-RL MoE-only checkpoint, and reject incompatible external ModelOpt layouts before SGLang startup.

Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
@Kh4L
Kh4L force-pushed the recut/sglang-nvfp4 branch from d98ccf0 to 31fd89a Compare October 4, 2026 07:07
@Kh4L

Kh4L commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 31fd89a

Kh4L added 2 commits October 28, 2026 10:46
Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI:L2 Run doctests, unit tests, functional tests, and convergence tests Documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants