Skip to content

Fix KV-cache scales dropped on Qwen Megatron-Core HF export - #2332

Merged
kevalmorabia97 merged 1 commit into
mainfrom
fix/qwen-kv-cache-export-scales
Sep 4, 2026
Merged

Fix KV-cache scales dropped on Qwen Megatron-Core HF export#2332
kevalmorabia97 merged 1 commit into
mainfrom
fix/qwen-kv-cache-export-scales

Conversation

@kevalmorabia97

@kevalmorabia97 kevalmorabia97 commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Type of change: Bug fix

Megatron-Core → HuggingFace export silently dropped KV-cache quantization for every Qwen
architecture. A checkpoint calibrated with an FP8 (or NVFP4) KV cache exported with
kv_cache_quant_algo unset, so the served model used an unquantized KV cache while the recipe
and the Megatron checkpoint both said otherwise. Nothing warned.

_GPTModelExporter only emits KV-cache state for layers whose architecture mapping defines a
core_attention rule:

# modelopt/torch/export/unified_export_megatron.py
if hasattr(layer.self_attention, "core_attention") and "core_attention" in self.rules:
    self.rules["core_attention"](layer.self_attention.core_attention, layer_id, is_mtp=is_mtp)

SelfAttentionScaling was wired in mcore_llama.py and mcore_nemotron.py but never in
mcore_qwen.py, so _self_attention_scaling never ran for Qwen: self.kv_cache_dtype stayed
unset and _gather_kv_cache_dtype() returned None.

Adding the rule to qwen3_causal_lm_export and qwen25_causal_lm_export covers all six Qwen
architectures — qwen3vl_causal_lm_export and qwen3_5_vl_causal_lm_export derive from
qwen3_causal_lm_export through with_language_model_prefix, which rewrites the prefix to
model.language_model.layers.{}.self_attn. automatically. GatedDeltaNet linear-attention layers
have no core_attention submodule, so the existing hasattr guard skips them.

Known remaining gap, not addressed here: deepseek_causal_lm_export,
gptoss_causal_lm_export and llama4_causal_lm_export are missing the same rule. I could not
validate those end to end, and DeepSeek's MLA uses different KV projection names, so they need
their own change rather than a copy of this one.

Usage

No API or flag change. The mapping now resolves for every Qwen architecture:

from modelopt.torch.export.plugins.mcore_common import all_mcore_hf_export_mapping

rule = all_mcore_hf_export_mapping["Qwen3_5MoeForConditionalGeneration"]["core_attention"]
print(rule.target_name_or_prefix)   # model.language_model.layers.{}.self_attn.

Testing

New tests/gpu_megatron/torch/export/plugins/test_mcore_export_mappings.py (9 cases). It needs
no GPU but imports mcore_common, so it sits beside test_moe_layout_choice.py, which is the
same shape. Confirmed the guard actually fires — reverting only mcore_qwen.py gives
6 failed / 3 passed (the Llama and Nemotron controls pass either way); with the fix,
9 passed.

End to end on a GB200 node in nvcr.io/nvidia/nemo:26.08: quantized Qwen/Qwen3.5-0.8B with a
W4A16-NVFP4 MLP / FP8-attention / kv_fp8_cast recipe via
examples/megatron_bridge/quantize.py, then export_quantized_megatron_to_hf.py.

exported hf_quant_config.json before after released nvidia/Qwen3.6-35B-A3B-NVFP4
kv_cache_quant_algo None FP8 FP8
k_scale / v_scale tensors 0 0 0

The absent scale tensors are correct for kv_fp8_cast: use_constant_amax pins the amax to the
E4M3 maxbound, so export_amax() returns nothing for get_scaling_factor and the runtime uses
the implicit 1.0 scale, while get_kv_cache_dtype still reports FP8 from num_bits. The
released checkpoint has exactly this shape, which is what the "after" column was checked
against.

The rest of the exported layer map is unchanged by this PR and was spot-checked against the
released checkpoint: NVFP4 W4A16 on the MLP projections, FP8 on
linear_attn.{in_proj_qkv,in_proj_z,out_proj} and self_attn.{q,k,v,o}_proj, with
in_proj_a / in_proj_b / conv1d / mtp.* excluded.

pre-commit run --files ... passes on all changed files.

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅
  • 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?: ✅
  • Did you get Claude approval on this PR?: ❌

Additional Information

Found while reproducing the nvidia/Qwen3.6-35B-A3B-NVFP4 recipe through
examples/megatron_bridge/ rather than examples/hf_ptq/. Labeled cherry-pick-0.47.0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Fixed Qwen checkpoint exports so calibrated FP8/NVFP4 KV-cache scales are preserved.
    • Exported checkpoints now retain the correct KV-cache quantization settings, preventing unintentionally unquantized KV-cache serving.
    • Improved KV-cache quantization mapping support across Qwen, Llama, Nemotron, and Qwen VLM exports.

_GPTModelExporter only emits KV-cache state for layers whose export mapping
defines a `core_attention` rule. SelfAttentionScaling was wired for Llama and
Nemotron but never for Qwen, so `_self_attention_scaling` never ran:
`kv_cache_quant_algo` was left unset and a checkpoint calibrated with an FP8 or
NVFP4 KV cache silently exported and served an unquantized one.

Add the rule to `qwen3_causal_lm_export` and `qwen25_causal_lm_export`. That
covers all six Qwen architectures, since `qwen3vl_causal_lm_export` and
`qwen3_5_vl_causal_lm_export` derive from the former through
`with_language_model_prefix`. GatedDeltaNet linear-attention layers have no
`core_attention` submodule and are skipped by the existing hasattr guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
@kevalmorabia97
kevalmorabia97 requested review from a team as code owners September 4, 2026 11:18
@kevalmorabia97 kevalmorabia97 added the cherry-pick-0.47.0 Upcoming release label Sep 4, 2026
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 877638a8-0986-4809-b36b-2b272ab316bf

📥 Commits

Reviewing files that changed from the base of the PR and between c56959c and 4fa50d6.

📒 Files selected for processing (3)
  • CHANGELOG.rst
  • modelopt/torch/export/plugins/mcore_qwen.py
  • tests/gpu_megatron/torch/export/plugins/test_mcore_export_mappings.py

Included review availability: Your plan provides up to 12 included reviews per hour; 11 remain after this review.


📝 Walkthrough

Walkthrough

The change adds SelfAttentionScaling mappings for Qwen 3 and Qwen 2.5 Megatron-Core exports. Tests verify KV-cache mapping coverage across supported architectures. The changelog records preservation of quantization scales and the quantization algorithm.

Changes

Qwen KV-cache export

Layer / File(s) Summary
Add Qwen KV-cache scaling mappings
modelopt/torch/export/plugins/mcore_qwen.py, CHANGELOG.rst
Qwen 3 and Qwen 2.5 mappings now apply SelfAttentionScaling to core_attention. The changelog records the KV-cache export fix.
Validate export mapping coverage
tests/gpu_megatron/torch/export/plugins/test_mcore_export_mappings.py
Parametrized tests verify the required core_attention, self_attention_scaling, and attention-prefix mappings across Llama, Nemotron, Qwen, and Qwen VLM architectures.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Merge Risk: ⚪ Minimal · up to 4fa50

Qwen Megatron-Core exports now retain KV-cache quantization metadata rather than silently serving calibrated caches unquantized. The mapping coverage validates the supported Qwen and derived vision-language architectures, with no remaining merge-blocking risk identified.

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 2 files. (1 skipped: 1 … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preserving KV-cache scales during Qwen Megatron-Core to Hugging Face export.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Anti-Patterns ✅ Passed PASS. The commit changes only static Qwen export mappings, a mapping test, and the changelog. The added Python lines contain no torch.load(..., weights_only=False), `numpy.load(..., allow_pickle=Tru…
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 2 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/qwen-kv-cache-export-scales

Comment @coderabbitai help to get the list of available commands.

@cjluo-nv cjluo-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot review (claude-opus-5) — DM the bot to share feedback.

Small, well-scoped bug fix: adds the missing core_attention: SelfAttentionScaling(...) rule to qwen3_causal_lm_export and qwen25_causal_lm_export, mirroring exactly what mcore_llama.py already does. I verified the mechanism against the code:

  • _GPTModelExporter._get_transformer_layer_state_dict only invokes self.rules["core_attention"] when the rule exists, so Qwen was indeed silently skipping _self_attention_scaling and leaving kv_cache_dtype unset — the described root cause checks out.
  • The VLM claim holds: with_language_model_prefix reconstructs each CustomModuleMapping via type(m)(...), so qwen3vl_causal_lm_export / qwen3_5_vl_causal_lm_export inherit the rule with the model.language_model.layers.{}.self_attn. prefix the test asserts.
  • No regression for non-KV-quantized Qwen exports: get_kv_cache_scaling_factor returns [None, None] when the bmm quantizers are absent/disabled (get_scaling_factor short-circuits on is_enabled), and get_kv_cache_dtype returns QUANTIZATION_NONE, so no stray k_scale/v_scale tensors and no kv_cache_quant_algo field appear. GatedDeltaNet layers take the linear_attn branch and never reach the rule.
  • No import-side change is needed, and _verify_exported_keys only checks source-minus-exported, so the extra scale tensors are harmless.

Test is config-level (tests/gpu_megatron/torch/export/plugins/test_mcore_export_mappings.py), the same shape as the neighbouring test_moe_layout_choice.py, which is appropriate since the bug itself is config-level; it fails without the fix per the PR description. CHANGELOG updated. Licensing: only the new test file's header, which matches LICENSE_HEADER verbatim — no license concern.

One non-blocking observation for the owner: the new test pins NemotronForCausalLMbackbone.layers.{}.mixer., but every other rule in nemotron_causal_lm_export uses model.layers.{}.... (the backbone.../mixer. naming belongs to Nemotron-H). That pre-existing prefix looks like a copy-paste from the Nemotron-H mapping and the test now enshrines it; worth a separate look/fix rather than blocking this PR.

@kevalmorabia97
kevalmorabia97 enabled auto-merge (squash) September 4, 2026 11:23
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-04 12:28 UTC

@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.80%. Comparing base (c56959c) to head (4fa50d6).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2332      +/-   ##
==========================================
- Coverage   79.31%   78.80%   -0.51%     
==========================================
  Files         527      527              
  Lines       61482    61482              
==========================================
- Hits        48765    48453     -312     
- Misses      12717    13029     +312     
Flag Coverage Δ
examples-diffusers 20.58% <ø> (ø)
examples-gpt-oss 13.17% <ø> (ø)
examples-llm_distill 13.24% <ø> (-0.01%) ⬇️
examples-llm_eval 16.96% <ø> (ø)
examples-llm_qat 17.44% <ø> (-0.01%) ⬇️
examples-llm_sparsity 15.78% <ø> (ø)
examples-megatron_bridge 26.25% <ø> (-0.12%) ⬇️
examples-specdec_bench 12.92% <ø> (ø)
examples-speculative_decoding 17.38% <ø> (-0.07%) ⬇️
examples-torch_onnx 21.67% <ø> (ø)
examples-torch_trt 14.96% <ø> (ø)
gpu 58.71% <ø> (-0.70%) ⬇️
regression 14.81% <ø> (+0.07%) ⬆️
unit 55.86% <ø> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kevalmorabia97
kevalmorabia97 merged commit f13a796 into main Sep 4, 2026
55 of 56 checks passed
@kevalmorabia97
kevalmorabia97 deleted the fix/qwen-kv-cache-export-scales branch September 4, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cherry-pick-0.47.0 Upcoming release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants