Repository navigation
feat(quant): export quantized GatedDeltaNet input projections to HF - #6714
Conversation
save_hf_pretrained_modelopt now splits a quantized GatedDeltaNet in_proj (Qwen3.5) into HF's in_proj_qkv / in_proj_z with their scales, taking the rows from the mapping's own split. The beta / alpha gates are written in BF16 and listed in ignore, as ModelOpt's export_mcore_gpt_to_hf does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
|
Automatic Claude reviews have been retired. To request a pull-request review, post a comment containing: Add |
|
/review |
svcnvidia-nemo-ci
left a comment
There was a problem hiding this comment.
Reviewed all four changed files and traced the affected GDN mappings, quantization-spec planning, export-hook lifecycle, and export callers. No verified findings at light review depth.
Static inspection only; tests were not executed.
Suggested test cases
test_gdn_state_follows_the_in_proj_layout_and_keeps_gates_unquantized[1]: verify exact QKV/Z row recovery and explicit unquantized B/A states at TP1.test_gdn_state_follows_the_in_proj_layout_and_keeps_gates_unquantized[2]: retain the same assertions for the TP2-packed layout.test_export_hook_passes_unquantized_outputs_through: retain the original output name and tensor.test_quantized_export_is_independent_of_parallelism[qwen3_5-nvfp4-parallelisms3]: verify the exact ignore set, packed QKV/scales, and TP2/PP2 equality against TP1.test_quantized_export_is_independent_of_parallelism[qwen3-nvfp4-parallelisms0]: preserve TP2/PP2 checkpoint equality.test_quantized_export_is_independent_of_parallelism[qwen3-fp8-parallelisms1]: preserve FP8 output dtype and TP2 checkpoint equality.test_quantized_export_is_independent_of_parallelism[qwen3_moe-mixed-parallelisms2]: preserve router exclusions and EP2/ETP2/PP2 checkpoint equality.- No perf tests impacted.
Note: formal approval is not available for this request, so this review is posted as a comment only.
Review generated by codex. This was a full review. Request a full review with /review reset=true.
|
/ok to test cf6f1cd |
|
Why do we leave the alpha/beta projections in BF16 here? ModelOpt fake-quantizes the entire fused Megatron |
|
Good catch. The gates stay in BF16 so the exported checkpoint matches what ModelOpt produces for HF PTQ and what serving expects:
You're right that this leaves a mismatch: Megatron fake-quantizes those rows, and the export doesn't. Its effect is limited:
The proper fix is for ModelOpt's Megatron GDN support to leave those rows unquantized during fake quant, so simulation matches export. That's a ModelOpt change, so I'll track it there as a follow-up rather than in this PR. |
…ant (#3124) ### What does this PR do? Type of change: Bug fix Megatron-Core fuses GatedDeltaNet's `[q, k, v, z, beta, alpha]` into one `in_proj` (a `TELayerNormColumnParallelLinear`) behind one weight and one input quantizer, so Megatron fake quant quantizes the beta / alpha gate rows. Every export keeps them in BF16: - `export_mcore_gpt_to_hf`; - Megatron-Bridge's `save_hf_pretrained_modelopt` (NVIDIA-NeMo/Megatron-Bridge#6714); - HF PTQ, whose recipes disable `*linear_attn.in_proj_a*` / `*in_proj_b*`, since vLLM's `in_proj_ba` is too narrow for FP4 kernels. PTQ evaluation and QAD / QARL of Qwen3.5 / Qwen3.6 therefore trained against quantization error on rows that deployment doesn't have. This was raised in NVIDIA-NeMo/Megatron-Bridge#6714 (comment). `_QuantGatedDeltaNet` now marks the gate rows of its `in_proj`, by name from `in_proj_split_names` / `in_proj_split_sections`, local to the TP rank. For those rows: - **Forward:** the quantized TE LayerNormLinear computes the output from the unquantized weight and the unquantized LayerNorm output, i.e. what deployment runs. The quantizers' pre-quant scale and rotation still apply, so an AWQ / SmoothQuant scale the stored weight compensates for stays correct (`apply_without_qdq` applies a quantizer's transforms without its QDQ). - **Backward:** the weight gradient of those rows comes from the unquantized input, and the input gradient from their unquantized weight. With no pre-quant scale or rotation on `in_proj`'s quantizers, both match BF16; with them, see the known limitation below. - **`fold_weight`** and the vLLM fake-quant exporter (`export_mcore_gpt_to_hf_vllm_fq`) keep the rows unquantized, so `--vllm_fakequant_export` and the library export match too. The `qkv` / `z` rows are unchanged, and the fake-quant weight cache still hits, since nothing is modified in place. Other `in_proj` layer types (non-TE) warn and stay fully quantized. Not changed: weight calibration (`max` runs `weight_only_quantize` on the whole weight) still includes the gate rows in the `in_proj` weight amax. Both exporters use that same amax for `in_proj_qkv` / `in_proj_z`, so fake quant and export stay consistent. Excluding the rows would need a calibration-wide change; GPTQ writes back through `iter_weights_for_calibration`. Why mark rows in the fused GEMM rather than the alternatives: - **Splitting `in_proj` into quantized and BF16 linears** would change Megatron-Core's GatedDeltaNet module structure, its sharded checkpoint layout and the TP / CP split of `in_proj`, in Megatron-Core or in a ModelOpt fork of the module. - **The quantizer enable / disable mechanisms** act on a whole quantizer, i.e. on all of `in_proj`; there is no per-row switch. Computing the few gate rows separately inside the existing GEMM patch keeps the module and checkpoints unchanged and costs one small extra GEMM. Known limitation (pre-existing, every row): the TE LayerNormLinear backward does not apply the input quantizer's pre-quant scale / rotation derivatives in dgrad, nor weight-quantizer transform derivatives in wgrad. The gate rows follow the same convention; a fix for all rows is tracked in OMNIML-6311. ### Usage No change to how models are quantized: it applies whenever a Megatron-Core GatedDeltaNet model is. Adds the helper `modelopt.torch.quantization.utils.apply_without_qdq`. ### Testing - New `test_in_proj_keeps_gates_unquantized` and `test_in_proj_keeps_gates_unquantized_tp2` (TP1 and TP2), with FP8 W8A8 on `in_proj`: - the gate rows of the output equal the unquantized output, while the other rows don't; - with a loss on the gate rows only, the input and gate-weight gradients match BF16; - `VllmFqGPTModelExporter` writes the gate weights unquantized; - with a power-of-two pre-quant input scale and the weight divided by it, the gate output is unchanged; - `fold_weight` keeps the gate weights. Each check fails without its fix: on `main`, 21 / 32 gate outputs differ; without the backward fix, the gate-weight gradient; without the exporter change, the exported gate rows; without the input transforms, the scaled-input gate output. The input-gradient check guards the dgrad path, which already used the unquantized gate weight. - New `test_mcore_vllm_export_gdn` runs `export_mcore_gpt_to_hf_vllm_fq` on a tiny Qwen3.5-VL. The model is CPU-offloaded, and its `in_proj` weight quantizer has a weight-only `rotate_back`, which only runs on CUDA. - The written `in_proj_b` / `in_proj_a` weights equal the Megatron rows with only that transform applied. - They have no quantizers in `quantizer_state.pth` / `quant_recipe.yaml`, while `in_proj_qkv` / `in_proj_z` keep their input quantizers. - It fails without the exporter fix, and with the gate rows outside `_fakequant_weight`'s CUDA staging (`Expected x.is_cuda()`). - It runs at TP1, like the other Qwen3.5 export tests. - `tests/gpu_megatron/torch/quantization/plugins/test_megatron_gated_delta_net.py`, `test_transformer_engine.py`, `tests/gpu_megatron/torch/export/test_unified_export_megatron.py`, `test_quantized_mbridge_export.py` on 2× RTX 6000 Ada in `nemo:26.10.rc9` (fla-core 0.5.1), with two Megatron-Core versions: - the Megatron-Bridge pin of this repo's CI, 0652ea7: 129 passed, 4 skipped, 1 failed; - Megatron-Bridge#6714 (Megatron-LM a296523510, as in Megatron-Bridge main e42042fa8): the same. The failure, `test_matches_megatron_exporter[llama-nvfp4]`, fails the same way on `main` in that container: TE's fused RoPE has no kernel for this GPU. - On the latest commit: `test_megatron_gated_delta_net.py` and `test_vllm_fakequant_megatron_export.py`, 11 passed, in the same container with the CI Megatron-Bridge pin. - Cost at Qwen3.6-35B-A3B `in_proj` size (2048 → 12352, 64 gate rows, 8192 tokens, FP8 W8A8 fwd + bwd): the gate GEMM and weight concat add about 0.07 ms per call, within run-to-run noise of the ~10 ms total. Peak memory is unchanged. ### Before your PR is "*Ready for review*" - Is this change backward compatible?: ✅ (numerics of the gate rows change to match the exported checkpoint) - If you copied code from any other sources or added a new PIP dependency, did you follow guidance in `CONTRIBUTING.md`: N/A - Did you write any new necessary tests?: ✅ - Did you update [Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?: ✅ - Did you get Claude approval on this PR?: ❌ ### Additional Information Requested for the Qwen3.6 QAD / QARL studies. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Bug Fixes** * Fixed fake quantization of Gated DeltaNet so beta and alpha gates remain in BF16 during evaluation, quantization-aware distillation, weight folding, and vLLM export. This keeps gate values and outputs consistent with the unquantized reference. * Other input-projection layers continue to be quantized; a warning is shown when their gates are also quantized. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
What does this PR do ?
save_hf_pretrained_modelopt/export_hf_weights_modelopt(#6657, #5812) now export a quantized GatedDeltaNetin_proj(Qwen3.5 / Qwen3.6) instead of raisingNotImplementedErrorforGDNLinearMappingSeparate. The output matches ModelOpt'sexport_mcore_gpt_to_hfbit for bit.Part of #4804 (PR 4 of its plan, the Qwen3.6-35B-A3B pilot); tracked as OMNIML-5977.
Changelog
in_proj_qkvandin_proj_zare exported packed with their scales. Their rows come from the mapping's own split (split_gdn_linear_weights+_split_gdn_grouped_to_separate) applied to row indices, so the scales follow the same TP layout as the weights.in_proj_bandin_proj_a(the beta / alpha gates) are written in BF16 and listed inquantization_config.ignore. Megatron fuses them into the one quantizedin_proj, so a recipe can't exclude them with a quantizer pattern; ModelOpt's exporter keeps them in BF16 the same way.Nonethrough unchanged, instead of failing.Tests
in_projbuilt withmerge_gdn_linear_weightsat TP1 and TP2, and leaves the gates unquantized; the export hook passes unquantized outputs through. Both fail without this change.test_modelopt_hf_export.pyadds a toy Qwen3.5 (one GatedDeltaNet and one full-attention layer) in NVFP4: TP2 and PP2 write the TP1 checkpoint bit for bit, andignorelists exactly the embedding, LM head,conv1d,in_proj_aandin_proj_b. The export script keepsconv1dunquantized, as ModelOpt's recipes do.Run in
nvcr.io/nvidian/nemo:26.10.rc9with this branch overlaid:tests/unit_tests/models/test_modelopt_utils.py(78 passed) andtest_modelopt_hf_export.py(4 passed, including the new Qwen3.5 case).tests/gpu_megatron/torch/export/test_quantized_mbridge_export.py(Test Megatron-Bridge quantized HF export against the Megatron exporter NVIDIA/Model-Optimizer#3066) extended to a tiny Qwen3.5-VL-MoE with every linear layer quantized, NVFP4 and FP8: the bridge export at TP2 matchesexport_mcore_gpt_to_hfat TP1 bit for bit (weights, scales,config.json,quantization_config; FP8input_scalewithin 2% at TP2). Once this merges, I'll bump ModelOpt CI's Megatron-Bridge commit and switch that test's Qwen3.5-VL case from NVFP4 MLP-only to full NVFP4.Not covered here: KV-cache export (waits for the nvidia-modelopt 0.49.0rc0 pin), Qwen3-Next's
GDNLinearMapping, and Mamba (MambaInProjMapping, OMNIML-5978).Before your PR is "Ready for review"
Pre checks:
Additional Information
🤖 Generated with Claude Code