Skip to content

Add GLM-5.3-Flash and GLM-5.2 support to Megatron-Bridge PTQ and HF export - #2539

Draft
kevalmorabia97 wants to merge 5 commits into
mainfrom
kmorabia/mbridge-glm53-flash
Draft

kevalmorabia97 wants to merge 5 commits into
mainfrom
kmorabia/mbridge-glm53-flash

Conversation

@kevalmorabia97

@kevalmorabia97 kevalmorabia97 commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Type of change: new feature

Adds GLM-5.3-Flash (glm5_next) support to examples/megatron_bridge, covering both PTQ (quantize.py) and unified HF export (export_quantized_megatron_to_hf.py). It uses the recipe behind the published nvidia/GLM-5.3-Flash-NVFP4, which was produced with hf_ptq.py, so the Megatron path now produces a checkpoint with the same quantization layout.

GLM-5.3-Flash differs from GLM-5 / glm_moe_dsa:

  • Every HF decoder layer becomes two Megatron physical layers, each wrapped in an mHC hyper-connection.
  • Attention alternates 3:1 between KDA linear attention and NoPE MLA with the DSA kpool indexer.
  • It adds an MTP layer and a vision tower.

Quantization

  • Recipe (models/zai-org/GLM-5.3-Flash/ptq/nvfp4_experts_dense_mlp-kv_fp8_cast): the dense-MLP patterns only matched HF names, so on Megatron the layer 0-2 MLPs stayed BF16. Added the mlp.linear_fc1/fc2 patterns and a *mtp* disable (Megatron-Bridge builds the MTP layer, HF does not). Neither matches anything in the HF model, so hf_ptq.py is unchanged.
  • FP8 KV cache: the Megatron plugin only added k/v_bmm quantizers to TEDotProductAttention, so on DSA layers kv_fp8_cast did nothing. DSAttention now registers with the same quant module, which tolerates value=None (the absorbed-MLA path). This also covers other DSA models on Megatron.

Export (new plugins/mcore_glm.py plus exporter support)

  • mHC wrappers are unwrapped and written as hc_{attn,ffn}_{fn,base,scale}.
  • Each attention/MLP pair of physical layers folds back into one HF layer.
  • KDA's fused q|k|v in_proj and conv1d are split into HF's tensors. The split code is now shared with Qwen3.5's GatedDeltaNet.
  • The DSA indexer and KV-cache settings are exported.
  • The MTP layer is written under the decoder's names, with eh_proj rebuilt from Megatron's split e_proj / h_proj.
  • Registering the mapping also lets quantize.py use grouped-GEMM experts for this model.

GLM-5 / GLM-5.2 export (GlmMoeDsaForCausalLM, supported by the stock Megatron-Bridge in the 26.10 container)

  • Mapping built from the DeepSeek MLA/MoE rules plus the DSA indexer and KV-cache settings. The indexer export now skips the kpool tensors when a model has none.
  • Megatron-Bridge's GLM-5 bridge doesn't build the MTP layer (mtp_num_layers = None), so the exporter copies it from the source checkpoint. The copy is dequantized from FP8 and excluded as model.layers.<N>.*, the same way nvidia/GLM-5.2-NVFP4 ships it in BF16. It also handles depth-pruned exports.

Expert-parallel export: export_quantized_megatron_to_hf.py --ep_size N. Grouped-GEMM experts are gathered across the EP group on export, so the whole MoE no longer has to fit on one GPU. The experts are gathered only to EP rank 0, and one TP/DP/EP rank per pipeline stage writes the shards. Previously every EP rank held and wrote every expert, so host memory scaled with EP, ranks raced on the same files, and a full-size GLM-5.3-Flash export at EP4 was OOM-killed. For GLM-5.3-Flash this is the only way to shard the export, because mHC rules out pipeline parallelism. SequentialMLP experts (--no_moe_grouped_gemm) are numbered by local position, so export now raises for them at EP>1 instead of writing colliding expert IDs.

Exporter fixes that also affect other architectures

  • Experts-only grouped-GEMM exports were not marked as quantized. Such exports (e.g. nvfp4_experts_only-*) wrote packed NVFP4 experts but no hf_quant_config.json and no quantization_config, so they would be served as unquantized weights. The cause: weight_attr_names() returned nothing for TEGroupedLinear (its weights are weight0..N behind one GroupedQuantizer), which was already a documented known gap. It now reports weight.
  • expert_bias is exported as FP32 instead of the export dtype. Megatron holds it as an FP32 buffer, and HF checkpoints (DeepSeek, GLM) ship e_score_correction_bias as F32; it decides expert routing.
  • The copied-through vision tower is now listed in exclude_modules (model.visual*), as hf_ptq.py already does, so deployments don't treat it as quantized.
  • head_dim is no longer overwritten with kv_channels for MLA models. For MLA, kv_channels is the V head dim, not HF's head_dim.
  • generation_config.json is written without re-validation. Transformers 5.17 refuses to re-save GLM-5.3-Flash's shipped config (top_p without do_sample).

Usage

torchrun --nproc_per_node 8 examples/megatron_bridge/quantize.py \
    --hf_model_name_or_path zai-org/GLM-5.3-Flash \
    --recipe models/zai-org/GLM-5.3-Flash/ptq/nvfp4_experts_dense_mlp-kv_fp8_cast \
    --ep_size 8 --skip_generate --export_megatron_path /tmp/GLM-5.3-Flash-NVFP4-megatron

# Keep --ep_size <= GPUs per node (see Testing). mHC does not support pipeline parallelism in Megatron-Core yet, so shard the export by EP instead.
torchrun --nproc_per_node 8 examples/megatron_bridge/export_quantized_megatron_to_hf.py \
    --hf_model_name_or_path zai-org/GLM-5.3-Flash \
    --megatron_path /tmp/GLM-5.3-Flash-NVFP4-megatron \
    --export_unified_hf_path /tmp/GLM-5.3-Flash-NVFP4 --ep_size 8

Testing

Environment: the NeMo 26.10.rc2 container. Its Megatron-LM and Megatron-Bridge don't support GLM-5.3-Flash yet, so these unmerged PRs were cherry-picked on top. They apply cleanly to rc2's checkouts.

End-to-end runs on a tiny GLM-5.3-Flash: 4 HF layers (3 KDA + 1 DSA) built from the real config, with random weights. All norms and small tensors are randomized too, so swapped tensors would show up. Each run is quantize → export, compared against the source and the published checkpoint:

  • Tensor names: identical to nvidia/GLM-5.3-Flash-NVFP4, with layer and expert indices normalized, including the MTP layer and every scale tensor.
  • Unquantized tensors: all 197 are bit-exact against the source. hc_*_base/scale, A_log, dt_bias and e_score_correction_bias are F32, as in the real source checkpoint.
  • NVFP4 tensors: all 75 dequantize closest to their own source weight, so there are no gate/up or expert-index swaps.
  • Quantization config: NVFP4, group_size 16 and kv_cache_quant_algo: FP8 match the published checkpoint.
  • config.json: identical to the source apart from quantization_config.
  • Parallelism and layouts: covers both grouped-GEMM and SequentialMLP experts, and checkpoints quantized at TP=1, TP=2 and EP=2.

GLM-5.2 on the stock 26.10.rc2 container (no Megatron-LM / Megatron-Bridge patches), GB300: a tiny GLM-5.2 built from the real config (3 layers + MTP, randomized norms), quantized with general/ptq/nvfp4_experts_only-kv_fp8_cast and then exported:

  • Tensor names are identical to the source.
  • All 105 unquantized tensors, including the copied MTP layer, are bit-exact.
  • The 48 expert weights are NVFP4, with NVFP4 / FP8 KV cache / group size 16.
  • config.json is identical to the source, and e_score_correction_bias is F32.
  • The stock GLM-5 bridge uses the HybridEP token dispatcher, which has no kernels for sm_89, so this can't run on an RTX 6000 Ada. On GB300, quantize.py's post-quantization generation check fails with a HybridEP in-place autograd error after the checkpoint is saved, so run with --skip_generate.

EP export: a TP2-quantized and an EP2-quantized toy GLM-5.3-Flash checkpoint, each exported at EP2, are bit-identical to the EP1 exports: all 497 tensors, hf_quant_config.json and config.json.

Unit and GPU tests:

  • tests/unit/recipe/test_glm_5_3_recipe.py (new Megatron-names case).
  • New export tests in test_mcore_export_mappings.py and test_unified_export_megatron.py (KDA split, mHC).
  • The full tests/gpu_megatron/torch/export suite (96 passed), including new mocked tests for the GLM-5 MTP copy (FP8 dequantize, depth-pruned remap).
  • tests/examples/megatron_bridge/test_quantize_export.py now quantizes with NVFP4 instead of FP8, and exports nemotron_h at --ep_size <num_gpus> from its TP-quantized checkpoint; 3/3 passed on 2 GPUs.
  • tests/unit/torch/export/test_get_quantization.py: new grouped-experts format and IQ detection tests, which fail without the weight_attr_names fix. tests/unit/torch/quantization: 1133 passed; test_dbrx also fails on main in this container.
  • The Megatron KV-cache quantization tests in tests/gpu_megatron/torch/quantization/plugins/test_megatron.py.

Full-size GLM-5.3-Flash on GB300 (oci-jhb, 4 GPUs per node, from the real FP8-blockwise zai-org/GLM-5.3-Flash):

  • Quantize: 4 nodes, --ep_size 4 (experts within each node, data parallel across nodes), cnn_nemotron_v2_mix, 1024 samples, sequence length 4096. Calibration took about 4 minutes and the job about 15. The 600 GB Megatron checkpoint has no NaN, Inf or zero amax. Quantized: routed experts (42 MoE layers) and the dense MLP (layers 0-2) in NVFP4, FP8 KV on the 11 DSA layers; MTP stays BF16.
  • Export: 1 node, --ep_size 4, 39 minutes, 191 GB output. Checked against nvidia/GLM-5.3-Flash-NVFP4 from safetensors headers:
    • Tensor names: identical set (147,661 names).
    • Quant config: NVFP4, group size 16, kv_cache_quant_algo: FP8, matching.
    • Unquantized tensors: no shape changes vs the source, and the F32 tensors stay F32.
    • MTP: exported BF16 from the live model.
    • config.json: equal to the source apart from quantization_config and fields transformers 5.17 derives on save.
    • Memory: peak host RSS about 413 GiB on the writer rank.
  • Known issue, not fixed here: with EP spanning nodes (--ep_size 16 on 4 nodes), calibration hits NaN at an expert input quantizer on the first batch. The same EP16 layout without quantizers is NaN-free across all 16 ranks, and EP4 with quantizers is clean, so the cause is still being investigated. Keep --ep_size ≤ GPUs per node.

Not tested:

  • Serving: no vLLM release here supports glm5_next yet (0.26.0 does not), so the export isn't deployed or evaluated.

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ Exports for other architectures change in four ways: expert_bias is now F32, VLM exports list the vision tower in exclude_modules, MLA configs keep the source head_dim, and experts-only grouped-GEMM exports are now marked as quantized.
  • 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

Draft until the Megatron-LM and Megatron-Bridge PRs above land. GLM-5 / GLM-5.2 need neither.

🤖 Generated with Claude Code

Quantize GLM-5.3-Flash (glm5_next) with examples/megatron_bridge to the
recipe behind nvidia/GLM-5.3-Flash-NVFP4 and export it as a unified HF
checkpoint:

- Recipe: match Megatron dense-MLP names and keep the MTP layer BF16.
- Quantization: register DSAttention for FP8 KV-cache quantization.
- Export: add the glm5_next mapping (mHC hyper-connections, KDA,
  NoPE-MLA with the DSA indexer, MTP with split e/h projections).
- Exporter fixes: keep expert_bias FP32, exclude the passthrough vision
  tower, skip the head_dim override for MLA, and pass the generation
  config through unvalidated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
@copy-pr-bot

copy-pr-bot Bot commented Sep 23, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://NVIDIA.github.io/Model-Optimizer/pr-preview/pr-2539/

Built to branch gh-pages at 2026-09-24 17:02 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@codecov

codecov Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 8.39416% with 251 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.72%. Comparing base (a21411a) to head (7da2a2c).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
modelopt/torch/export/unified_export_megatron.py 7.29% 127 Missing ⚠️
modelopt/torch/quantization/plugins/megatron.py 0.00% 124 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2539      +/-   ##
==========================================
- Coverage   68.89%   68.72%   -0.18%     
==========================================
  Files         605      608       +3     
  Lines       67063    68908    +1845     
==========================================
+ Hits        46204    47354    +1150     
- Misses      20859    21554     +695     
Flag Coverage Δ
unit 58.70% <8.39%> (+0.27%) ⬆️

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.

- export_quantized_megatron_to_hf.py: add --ep_size. Grouped-GEMM experts
  are already gathered across the EP group on export; SequentialMLP
  experts are indexed locally, so export raises for them at EP>1.
- Add the GLM-5 / GLM-5.2 (glm_moe_dsa) export mapping. Megatron-Bridge
  does not build its MTP layer, so it is copied from the source checkpoint
  (dequantized, excluded from quantization) like the released NVFP4 ones.
- weight_attr_names now reports TEGroupedLinear, so an experts-only
  quantized grouped-GEMM MoE is detected as quantized; its export used to
  omit hf_quant_config.json and quantization_config.
- test_quantize_export: quantize with NVFP4 and export nemotron_h at EP.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
@kevalmorabia97 kevalmorabia97 changed the title Add GLM-5.3-Flash support to Megatron-Bridge PTQ and HF export Add GLM-5.3-Flash and GLM-5.2 support to Megatron-Bridge PTQ and HF export Sep 24, 2026
At EP>1 every EP rank all-gathered every expert and wrote the same shards,
so host memory scaled with EP and ranks raced on the same files. A
full-size GLM-5.3-Flash export at EP4 was OOM-killed (~260 GB RSS per
rank). Gather experts to EP rank 0 only, and write shards from one
TP/DP/EP rank per pipeline stage.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
@kevalmorabia97

Copy link
Copy Markdown
Collaborator Author

/claude review

@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 (gpt-6-astra) — DM the bot to share feedback.

Changes requested: Hub-ID exports silently omit GLM-5 MTP weights, and folded-layer verification mishandles depth-pruned GLM-5.3 exports.

Needs action:

  • Fix Hub-ID MTP passthrough in unified_export_megatron.py, including a mocked Hub regression test; see inline comment.
  • Fix the export self-check to use HF decoder depth after layer folding; test a pruned GLM-5.3 model.
  • Restore FP8 integration coverage in test_quantize_export.py, or identify equivalent retained coverage and justify its removal.
  • Add DSAttention regression coverage for conversion, tensor/None values, and checkpoint round-trip in test_megatron.py.
  • Document the circular-import reason for the local GroupedQuantizer import in core_utils.py, or move it to module scope.

No action needed:

  • The design extends existing DeepSeek/Qwen mappings and shared slicing rather than adding a competing export system; the PR explains the layout and memory constraints.
  • Switching the Nemotron integration case to EP is justified by the new export capability; removing FP8 coverage is not explained.

Comment thread modelopt/torch/export/unified_export_megatron.py
Comment thread modelopt/torch/export/unified_export_megatron.py
Comment thread tests/examples/megatron_bridge/test_quantize_export.py Outdated
Comment thread modelopt/torch/quantization/plugins/megatron.py Outdated
Comment thread modelopt/torch/quantization/utils/core_utils.py
Comment thread modelopt/torch/quantization/plugins/megatron.py Outdated
Comment thread examples/megatron_bridge/export_quantized_megatron_to_hf.py Outdated
@claude

claude Bot commented Sep 24, 2026

Copy link
Copy Markdown

Claude review summary

Findings: 0 CRITICAL · 1 IMPORTANT · 1 SUGGESTION

Severity Area Location
IMPORTANT Export modelopt/torch/quantization/plugins/megatron.py:1055-1057 — uncalibrated v_bmm_quantizer on the DSA absorbed-MLA path suppresses both KV scales while still declaring kv_cache_quant_algo: FP8
SUGGESTION Docs examples/megatron_bridge/export_quantized_megatron_to_hf.py:90 (+ module docstring) — stale "only Pipeline parallelism is supported" now that --ep_size exists

Most impactful finding

Registering DSAttention in _core_attention_classes is what makes KV-cache quantization reachable on DSA layers for the first time, and on those layers the absorbed-MLA path passes value=None. v_bmm_quantizer is therefore never invoked, its amax stays None, and _self_attention_scaling (unified_export_megatron.py:2039) gates both writes behind all(s is not None for s in kv_scales) — so neither k_scale nor v_scale is emitted, while the two lines that follow still set self.kv_cache_dtype from get_kv_cache_dtype(module) and stamp kv_cache_quant_algo: FP8 into hf_quant_config.json. The GLM recipe in this PR is shielded because the kv_fp8_cast unit sets use_constant_amax: true, but the plain kv_fp8 unit is calibrated — and tests/examples/megatron_bridge/test_quantize_export.py now switches to general/ptq/nvfp4_default-kv_fp8. Details and two candidate fixes are in the inline thread.

What I verified and cleared

  • Rule-book plumbing for the new boolean flags — fold_attn_mlp_layer_pairs / mtp_in_decoder_layers survive both with_language_model_prefix (non-CustomModuleMapping values pass through unchanged) and _populate_rule_book (isinstance(v, (CustomModuleMapping, bool))), so the self.rules.get(...) reads work.
  • Layer-count arithmetic — _src_num_hidden_layers is captured before num_hidden_layers //= 2, so _copy_decoder_mtp_layers_from_pretrained resolves source vs. destination MTP indices correctly. save_safetensors_by_layer_index still indexes all config.num_layers physical shards, so the folded 2-physical-to-1-HF mapping produces a complete model.safetensors.index.json.
  • New EP writer gate — writes_layers = tp_rank == 0 and dp_rank == 0 and ep_rank == 0 combined with passing {} on other ranks is safe: save_safetensors_by_layer_index tolerates an empty dict, its torch.distributed.barrier() plus rank-0 index assembly still sees every per-layer meta file, and both the vision-tower merge (is_first_stage_main_rank) and the MTP merge (is_last_stage_main_rank) land on ranks that satisfy the gate — nothing is silently dropped. The gather_object-to-EP-rank-0 switch in _grouped_mlp_slicing matches.
  • exclude_modules ordering — vision_passthrough_prefixes is assigned before the exclude_modules comprehension that consumes it.
  • weight_attr_names GroupedQuantizer branch (core_utils.py) — traced every consumer: uses_iq_quantization, get_quantization_format and get_quant_config never dereference module.weight, and the one consumer that does (iter_weights_for_calibration) is overridden by _QuantTEGroupedLinear (plugins/transformer_engine.py:240). Safe, and it is the right fix for experts-only grouped-GEMM models previously reporting no format.
  • Mode/state composability — QuantModuleRegistry.register accepts a multi-entry dict, and the _QuantTEDotProductAttention to _QuantCoreAttention rename does not change the registry key string for TEDotProductAttention, so existing modelopt_state still restores. The DSAttention import is properly guarded for older Megatron-Core, as is HyperConnectionHybridLayer.
  • _split_fused_projection extraction — behavior-preserving for the existing GatedDeltaNet path; keep_bf16_names=("in_proj_a", "in_proj_b") reproduces the previous set exactly.
  • New NotImplementedError in _get_dsa_indexer_state_dict — not a regression for existing archs: deepseek_causal_lm_export is registered only for DeepseekV2ForCausalLM / DeepseekV3ForCausalLM, neither of which has core_attention.indexer, so the new branch is unreachable for them.

Non-blocking note

_get_fused_norm_weight (pre-existing code, hence no inline thread) returns (None, None) both when a module has no fused norm and when it has one but the arch defines no matching rule. _glm5_next_causal_lm_export defines fused_pre_mlp_layernorm but neither fused_input_layernorm nor fused_norm; if the Megatron KDA spec fuses the input norm into in_proj — as it does for GatedDeltaNet, the case that fallback chain exists for — those weights would be dropped without a warning. I could not confirm the KDA spec from this repo, and test_glm5_next_names_match_released_checkpoint only checks rule-to-name in one direction, so a missing rule is invisible to it. Worth a sanity check against a real export before merge.

Risk assessment

Moderate. The structural work is careful and well-decomposed, and the new unit tests cover the genuinely tricky mechanics: KDA fused-QKV/conv1d splitting, mHC tensor naming, decoder-resident MTP copying, and grouped-experts format detection. The documented backward-compatibility deltas — MLA head_dim no longer overwritten, expert_bias now FP32, vision tower added to exclude_modules, experts-only grouped-GEMM models now reporting a quantization format — are all intentional and called out in the PR body. Residual risk is concentrated in the DSA KV-cache path above and in the 104 new uncovered lines flagged by codecov, most of which are the multi-rank EP/PP export paths that only a live distributed run exercises.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude review found 1 IMPORTANT issue (0 CRITICAL), so this is a comment review rather than an approval.

IMPORTANT (Export) — modelopt/torch/quantization/plugins/megatron.py:1055-1057: registering DSAttention makes KV-cache quantization reachable on DSA layers for the first time, but the absorbed-MLA path passes value=None, so v_bmm_quantizer is never invoked and its amax stays None. _self_attention_scaling then gates both writes behind all(s is not None for s in kv_scales) and emits neither k_scale nor v_scale, while still setting self.kv_cache_dtype from get_kv_cache_dtype(module) — producing a checkpoint that advertises kv_cache_quant_algo: FP8 with no scale tensors on those layers. The GLM recipe here is shielded only because kv_fp8_cast sets use_constant_amax: true; the plain, calibrated kv_fp8 unit is not, and the example test in this PR now uses general/ptq/nvfp4_default-kv_fp8. Suggested fixes (mirror K statistics onto V for the absorbed path, or emit scales independently and fail loudly on a missing one) are in the inline thread.

SUGGESTION — stale "only Pipeline parallelism is supported" comment and module docstring in examples/megatron_bridge/export_quantized_megatron_to_hf.py now that --ep_size exists. Non-blocking.

The rest of the change held up under review: the boolean rule flags survive with_language_model_prefix and _populate_rule_book, the folded layer-pair index arithmetic and MTP source/destination indices are correct, the new EP writer gate drops nothing (vision-tower and MTP merges both land on ep0/dp0 ranks and save_safetensors_by_layer_index tolerates an empty dict), the weight_attr_names GroupedQuantizer branch is safe for every consumer, the _QuantCoreAttention rename preserves registry keys so modelopt_state still restores, and _split_fused_projection is behavior-preserving for GatedDeltaNet. A full breakdown is in the summary comment.

kevalmorabia97 and others added 2 commits September 24, 2026 06:36
…heck

- DSAttention (absorbed MLA) passes value=None: calibrate V on the shared
  KV latent so a calibrated FP8 KV cache also exports a v_scale.
- Copy GLM-5 decoder-layer MTP from a Hub-ID source by downloading only
  the shards that hold it.
- Export self-check: compare against the HF decoder depth (folded models
  have 2x physical layers) and check the MTP layer at its remapped index.
- test_quantize_export keeps an FP8 case; add DSAttention KV-cache,
  depth-pruned self-check and Hub MTP tests; document the local import;
  refresh the export script's parallelism notes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
…op HAS_TE

torch's state_dict()/load_state_dict() route ``_extra_state`` only through
classes that override get/set_extra_state. TEDotProductAttention does,
DSAttention does not, so its quantizer state (including the KV amax) was
never saved and a restored model ran the KV cache with dynamic scales.
Register DSAttention through a subclass that routes its extra state to the
quantizer state handlers. The DSA KV test now round-trips a torch-dist
checkpoint.

Transformer Engine is always present in Megatron environments, so import it
unconditionally like the NAS / prune plugins, and drop the HAS_TE guard.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
@kevalmorabia97

Copy link
Copy Markdown
Collaborator Author

/claude review

Comment on lines +1075 to +1091

if HAS_DSA:

@QuantModuleRegistry.register({DSAttention: "megatron_DSAttention"})
class _QuantDSAttention(_QuantCoreAttention):
"""DSAttention with KV-cache quantization.

torch's state_dict() / load_state_dict() route ``_extra_state`` only through classes that
override these; TEDotProductAttention does, DSAttention does not, so without them the
quantizer state (including amax) was dropped from checkpoints.
"""

def get_extra_state(self):
return quant_module_get_extra_state(self)

def set_extra_state(self, state):
quant_module_set_extra_state(self, state)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[SUGGESTION] This fixes the symptom per-class; the cause is in register_modelopt_extra_state_callbacks, and the same silent drop stays latent for every other registered Megatron module whose base class lacks these methods.

register_modelopt_extra_state_callbacks (modelopt/torch/opt/plugins/megatron.py:136-141) binds the hooks as instance attributes:

module.get_extra_state = types.MethodType(_modelopt_get_extra_state, module)

but PyTorch gates extra-state entirely on a class-level check (getattr(self.__class__, "get_extra_state", Module.get_extra_state) is not Module.get_extra_state) before it ever looks the attribute up on the instance. So the instance binding only has an effect when the wrapped class already overrides get_extra_state/set_extra_state — TE's DotProductAttention does, DSAttention doesn't, which is exactly the bug this block works around.

Two consequences worth noting:

  1. Because the instance attribute shadows the class method (megatron_replace_quant_module_hook binds it on the pre-conversion module, and DynamicModule conversion preserves __dict__), _QuantDSAttention.get_extra_state is essentially never called — it exists only to flip torch's class check to True. That's non-obvious enough that a future reader may "clean up" the apparently-redundant override and silently reintroduce the amax loss. Worth saying so in the docstring if the per-class shape is kept.
  2. A repo-wide grep finds no other class-level get_extra_state/set_extra_state in modelopt/torch/, so any future QuantModuleRegistry.register on a Megatron module whose base lacks them (e.g. native non-TE ColumnParallelLinear/RowParallelLinear in a build without TE) will hit the same silent quantizer-state drop — with no error, just missing amax after a checkpoint round-trip.

Suggested fix, in the spirit of CONTRIBUTING's "fix the bug cause, not the side effect": have the helper install the hooks on the type instead of the instance, so the class check passes for every registered module without per-class boilerplate. Since conversion already gives each converted module a fresh dynamic class, something like the following in register_modelopt_extra_state_callbacks is safe:

cls = type(module)
if cls.get_extra_state is torch.nn.Module.get_extra_state:
    cls.get_extra_state = _modelopt_get_extra_state

(_modelopt_get_extra_state's zero-arg super() relies on __class__ from its defining module, so if it moves onto a type keep it defined where it is and reference it, rather than re-defining it inside the class body.) If that turns out to be too invasive for this PR, an assert/warning in the helper when the class check would fail would at least make the next occurrence loud instead of silent.

Comment on lines +1044 to +1049
query = self.q_bmm_quantizer(query)
if value is None:
# Absorbed MLA (DSAttention) passes value=None: the key is the KV latent that both K
# and V are read from, so calibrate V on it too (output unused) to export a V scale.
self.v_bmm_quantizer(key)
else:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[SUGGESTION] The discarded self.v_bmm_quantizer(key) runs on every forward, not just during calibration.

Calibrating V on the KV latent is the right call for absorbed MLA, and it's what unblocks _self_attention_scaling's all(s is not None for s in kv_scales) gate so both k_scale and v_scale get written. But once amax is collected, this call still does a full fake quant-dequant of the latent tensor whose result is thrown away — per DSA layer, per forward, for the rest of the model's life (QAT steps, eval, any post-calibration forward). With fake_quant enabled that's an extra quantize+dequantize over a [seq, batch, kv_lora_rank] tensor per layer.

Two ways to avoid it:

  • Gate on the stats-collection pass, e.g. only call it while the quantizer is collecting (getattr(self.v_bmm_quantizer, "_if_calib", False), the same predicate transformer_engine.py:54 uses), or
  • Skip the extra forward entirely and copy K's amax into V once at the end of calibration (in modelopt_post_restore or a small calibration hook) — they are provably identical here, and tests/gpu_megatron/torch/quantization/plugins/test_megatron.py's torch.equal(v_bmm_quantizer.amax, k_bmm_quantizer.amax) assertion would still hold.

The second is cheaper but moves logic away from the point where the invariant is visible; the first is a one-line change and keeps the explanation next to the code. Either is fine — the current form is correct, just pays a permanent cost for a calibration-only need.

@claude

claude Bot commented Sep 24, 2026

Copy link
Copy Markdown

Claude review — re-review at 7da2a2c6

Counts: 0 CRITICAL · 0 IMPORTANT · 2 SUGGESTION

Full review (bare /claude review, no scope given). 16 files, +1060/-~300; focus was cross-file dataflow, mode/state composition, export key/name compatibility, and algorithm-level correctness rather than style (CodeRabbit's lane).

Both findings from my previous round are resolved

  • (was IMPORTANT) DSA v_bmm_quantizer was never invoked, so its amax stayed None. _self_attention_scaling gates both k_scale and v_scale behind all(s is not None for s in kv_scales), so a kv_cache_quant_algo: FP8 model exported with neither scale while still advertising FP8 KV cache in hf_quant_config.json. Now fixed at megatron.py:1044-1049: when value is None (absorbed MLA), V is calibrated on the KV latent that both K and V are read from. The new test_dsa_kv_cache_quant pins v_bmm_quantizer.amax == k_bmm_quantizer.amax and round-trips through sharded_state_dict_test_helper.
  • (was SUGGESTION) Stale "only Pipeline parallelism is supported for export" comment/docstring in examples/megatron_bridge/export_quantized_megatron_to_hf.py now reflects --ep_size.

What I verified this round

  • modelopt_state compatibility of the _QuantTEDotProductAttention → _QuantCoreAttention rename. The QuantModuleRegistry key string ("TEDotProductAttention") is unchanged, so existing checkpoints still restore. No dangling references to the old class name.
  • _extra_state plumbing. _QuantDSAttention now satisfies torch's class-level extra-state check, and _QuantCoreAttention.sharded_state_dict forwards the whole state_dict() (now including _extra_state) into make_sharded_tensors_for_checkpoint, which already special-cases *_extra_state — the same path TE's own _extra_state travels. Root cause and latent scope are in an inline SUGGESTION.
  • HAS_TE removal. Deliberately not flagged: nas/plugins/megatron.py, prune/plugins/mcore_minitron.py and speculative/plugins/megatron_eagle.py already import TE unconditionally, so this matches repo convention even though import_plugin swallows ModuleNotFoundError.
  • weight_attr_names GroupedQuantizer branch (utils/core_utils.py). Only _QuantTEGroupedLinear creates a GroupedQuantizer, and it overrides iter_weights_for_calibration — the sole consumer that dereferences getattr(self, weight_name). quant_utils.py:1796 only uses hasattr. The new yield "weight" for a module with no .weight cannot reach a dereference. Safe.
  • Folded attn/MLP layer pairs (fold_attn_mlp_layer_pairs). No target-name collision: the attention physical layer (2N) writes input_layernorm, the MLP physical layer (2N+1) has input_layernorm=IdentityOp and writes pre_mlp_layernorm → post_attention_layernorm. head_dim is correctly left alone for MLA configs.
  • _verify_exported_keys MTP remap. Traced _src_num_hidden_layers (captured pre-mutation) against hf_depth for four cases — unpruned, depth-pruned, folded, folded+pruned. The mtp_id = src_layer - _src_num_hidden_layers plus 0 <= mtp_id < num_mtp filter correctly drops pruned-away source layers while remapping the source MTP index onto hf_depth + mtp_id. Matches test_verify_exported_keys_depth_pruned_with_decoder_mtp.
  • EP-rank-0 gather_object switch in _grouped_mlp_slicing. Non-writer EP ranks now pass None; consumers iterate gathered_bytes or (). Metadata (layer_config_dict, exclude_modules) still reaches the writer via _gather_layer_config_dict/_gather_exclude_modules, so dropping the all-gather does not lose the quant config. writes_layers = tp_rank == 0 and dp_rank == 0 and ep_rank == 0 coincides with the gather destination for the EP layouts I traced (ep=N with world=N, and ep=2 with world=4).
  • _download_hub_shards. The index-driven shard subset keeps _scale_inv keys in the same shard as their weight (shared layer prefix), and the EntryNotFoundError fallback to model.safetensors yields a directory _read_checkpoint_keys (2326-2337) can read. test_mtp_state_dict_copies_decoder_mtp_layers_from_hub asserts only the MTP shard is requested.
  • Recipe YAML. *mtp* is ordered after the kv_fp8_cast import so it also disables the MTP layer's *_bmm_quantizer, keeping MTP fully BF16; neither the new *mlp.linear_fc1* nor *mtp* pattern matches anything in the HF module tree, so hf_ptq.py behavior is unchanged.

Risk assessment

Low-to-moderate, concentrated in the multi-rank export paths. The single-rank logic is well covered by the six new/extended tests, and the PR body's bit-exactness check of all 197 unquantized tensors against the released checkpoint is strong evidence for the GLM-5.3 name mappings. The residual risk is the ~114 uncovered patch lines codecov reports, which sit almost entirely in the PP/EP fan-out (writes_layers gating, the new gather_object destination arithmetic, the SequentialMLP EP>1 NotImplementedError guard) — correct by inspection, but only the pp/ep parametrizations in tests/examples/megatron_bridge/test_quantize_export.py exercise them, and only at small world sizes. Worth one manual multi-node PP x EP export before this is relied on for a production GLM-5.3 release artifact.

Neither SUGGESTION blocks merge.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude review passed — no blocking issues found. LGTM

kevalmorabia97 pushed a commit that referenced this pull request Sep 25, 2026
…y PR (#2547)

## Summary

`partial-install (torch)` has been failing on **every** PR since
2026-09-24 — including PRs whose branches predate the breakage — and
because it is a *collection* error rather than a test failure, it aborts
the entire run:

```
ImportError while importing test module '.../tests/unit/torch/speculative/plugins/test_hf_streaming_dataset.py'
E   ModuleNotFoundError: No module named 'httpx'
collected 2243 items / 1 error / 45 skipped
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
```

`unit-pr-required-check` aggregates it, so nothing currently merges on a
fresh run.

## What happened

**No code changed.**
`modelopt/torch/speculative/plugins/hf_streaming_dataset.py` has
imported `httpx` at module scope since #1509 (2026-06-02), and `httpx`
has never appeared in `pyproject.toml`. It arrived only transitively:
`dev-test` → `timm` → `huggingface_hub` → `httpx`.

**huggingface_hub 2.0.0**, published **2026-09-24T12:01:21Z**, replaced
`httpx<1,>=0.23.0` with the separate **`httpx2<3,>=2.0.0`**
distribution. Different package name, so `httpx` stopped being installed
and the chain disappeared.

The boundary is exact — every run *created* before that timestamp
passes, every one after fails:

| PR | run created | result |
|---|---|---|
| #2536 / #2535 | 09-23 22:02 | pass |
| #2500 | 09-23 23:45 | pass — **merged 09-24 20:01 on this stale-green
result** |
| *hub 1.33.0 (still requires httpx)* | *09-24 09:49* | |
| **hub 2.0.0 published** | **09-24 12:01** | ← |
| #2539 | 09-24 16:57 | fail |
| #2544 | 09-24 18:32 | fail |
| #2216 | 09-25 11:58 | fail |

#2500 merging afterwards is not a counterexample: GitHub does not re-run
checks at merge time, so it merged on a result from ~20 hours earlier.
That is also why this went unnoticed.

## The changes

### 1. Declare `httpx` in the `hf` extra

`httpx` is not incidental to streaming — it is the only transport:

- every fetch is HTTP: `POST /v1/completions` to the vLLM serve plus
`GET /meta` and `/desc` against the connector's sidecar, all through
`httpx.Client`;
- there is no non-HTTP path — the base `StreamingDataset._fetch` is an
abstract seam and `EagleVllmStreamingDataset._fetch` is its only
implementation;
- no other HTTP library appears in the module (`requests` / `urllib` /
`aiohttp`: zero hits, and `requests` is not declared either);
- even the retry predicate is built from it: `_TRANSIENT_FETCH_ERRORS =
(httpx.HTTPError, OSError)`.

It belongs in `hf` rather than in the core `dependencies`: the same
module needs `transformers.trainer_pt_utils` at module scope, so one
extra already gates the whole file, and a core install has no use for an
HTTP client. The bound matches the 0.x API the code uses — `httpx` has
no 1.0 release, and 2.x is a different distribution.

This is the part that stops it recurring. `[hf]` currently gets `httpx`
only because `datasets` happens to require it — the same accident with a
different supplier, one release away from repeating.

### 2. Acquire `httpx` in the test through the existing skip guard

The test file already intends to skip where the extra is absent — it has
`pytest.importorskip("transformers")` and a comment explaining why, and
`transformers` is absent in this job too. It broke only because `import
httpx` sat **five lines above** that guard, where a missing module ends
collection instead of skipping one file.

## Verification

- With everything installed: **18 passed**, no behaviour change.
- The import-order property is checked with an AST walk over the
module's top-level statements: no `hf`-extra-only import precedes the
first `importorskip` (which is now line 41).
- A faithful local reproduction was attempted and abandoned honestly:
hiding `httpx` locally also breaks `huggingface_hub` 1.28, which
`modelopt.torch.opt.plugins.huggingface` imports, so the local failure
is not the CI one. CI is the oracle for that half — this PR's own
`partial-install (torch)` run is the check that matters.

## Scope

Two files, five lines of declaration and four of test import order.
Deliberately not folded into any feature PR: it blocks the whole repo,
and burying a repo-wide fix inside unrelated work is how these stay
invisible.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Optional Hugging Face installations now include `httpx`, supporting
features that require HTTP communication without requiring it for all
installations.
* **Tests**
* Hugging Face streaming dataset tests now skip when `httpx` is
unavailable, allowing the remaining test suite to be collected and run
without it.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants