Repository navigation
Conversation
6403701 to
fbd2b31
Compare
fbd2b31 to
4f56b14
Compare
Kh4L
left a comment
There was a problem hiding this comment.
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.
- 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.
should_quantize_nvfp4raises where all three of its sibling checks returnFalse. The same expert tensor is fatal or harmless depending only on whether the user happened to list it inmodules_to_not_convert.- The converted checkpoint advertises FP8 KV-cache quantization it does not contain. SGLang reads
kv_cache_scheme→ attachesModelOptFp8KVCacheMethodto everyRadixAttention→ finds nok_scale/v_scale→ silently substitutes 1.0. Inert atkv_cache_dtype: auto, uncalibrated and unwarned otherwise. mxfp8 emits no KV metadata at all, so this is new here. - Stock ModelOpt NVFP4 checkpoints are never recognized.
is_existing_nvfp4_checkpointreads onlyconfig.json, but real releases put the block inhf_quant_config.json— the file SGLang itself declares. Confirmed againstnvidia/Qwen3-235B-A22B-NVFP4andnvidia/DeepSeek-R1-0528-NVFP4: both have noquantization_configinconfig.json. The converter then tries to re-quantize an already-NVFP4 checkpoint and dies, after copying its metadata into the cache dir. exclude_modulesentries are globs, matched within. Real ModelOpt checkpoints ship"model.layers.0.self_attn*"; plain substring containment never matches, so those entries are silent no-ops for skipping and makevalidate_checkpoint_high_precision_layoutpass vacuously._contiguous_pair_viewdoesn't require canonical strides. A size-1 leading dim can carry anystride[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 totorch.cat.weight_scale_2is 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 differentrow_scaled_nvfp4default, 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_ignoremerges the checkpoint's container-levelignorelist back intomodules_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_maxandnvfp4_4over6_err_modeare all new in v2.17 (v2.16 adds onlyrow_scaled_nvfp4), so the>=2.17requirement in the docstring is exactly right. Tagv2.17resolves to2e559f06…, matching the lock;VERSION.txtsays2.17.0, matching the hand-declared metadata;v2.17.0does not exist, so@v2.17is the correct spelling. TE'srequires-distis 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 therelease_v2.15branch to thev2.17tag is a real improvement —release_v2.17has 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_invis a plain row-major block-scale matrix, not a swizzled/tiled one (with_gemm_swizzled_scalesdefaultsFalse), so[:num_rows, :num_cols // 16]reads the region it intends to. And_amax_rowwiseis a per-tensor scalar here, soamax_rowwise.reshape(-1)[0]is right: TE's reference recipe only reshapes the global amax to a per-row[m, 1, 1]underrow_scaled_nvfp4, and this PR passesrow_scaled_nvfp4=False.nvfp4_global_encode_scale_tealso reproduces TE's formula exactly — samee4m3_max * 6.0 / amax, samefinfo.maxclamp, 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 requestsnemo-automodel[moe]. The globaloverride-dependenciesalready 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
cc9dcb7 to
f9c297c
Compare
f9c297c to
ba5e9be
Compare
ba5e9be to
74c4836
Compare
|
/ok to test 74c4836 |
74c4836 to
cdd2097
Compare
|
/ok to test cdd2097 |
|
/ok to test 679ab0c |
|
Current head |
7d429c3 to
d98ccf0
Compare
|
/ok to test d98ccf0 |
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>
d98ccf0 to
31fd89a
Compare
|
/ok to test 31fd89a |
Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
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
035a6003f219f324ab2dc136e381ef95849f1b0frestacks on refreshed #3614 and maind48b2e0f3d7fc4891b418ae603c8e8fa71482b9c. 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:
Historical GPU evidence: HSG job
6593575completed successfully on 3 nodes / 12 GB200 GPUs at736472843ba46eca4c16f127009452bdb148f090, 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.