Skip to content

Commit 85d8082

Browse files
Merge #2623 review updates into the GLM-5.3-Flash branch
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
2 parents bdc5ff2 + 370c11f commit 85d8082

4 files changed

Lines changed: 109 additions & 26 deletions

File tree

‎modelopt/torch/distill/plugins/megatron.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ def _sharded_state_dict(self, *args, **kwargs) -> "ShardedStateDict":
590590
# An MTP head left out of quantization is exempt from that: there is no quantization
591591
# error to recover there, and its CE materialises an fp32 [seq, vocab] tensor.
592592
skip_mtp_loss = _mtp_excluded_from_quantization(model)
593-
if skip_mtp_loss:
593+
if distill_cfg.skip_lm_loss and skip_mtp_loss:
594594
# Freeze the untrained MTP head: DDP's overlapped grad reduce asserts on params with no grad.
595595
for name, param in model.named_parameters():
596596
if "mtp" in name.split("."):

‎modelopt/torch/export/unified_export_megatron.py‎

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -333,11 +333,6 @@ def save_pretrained(
333333
# medusa_heads and eagle_module only exist in the last stage.
334334
is_last_stage_main_rank = pp_rank == pp_size - 1 and tp_rank == 0
335335
is_writer_rank = self._is_sidecar_writer_rank(is_last_stage_main_rank)
336-
# One writer per pipeline stage: other TP / DP / EP ranks hold the same layers (EP>1 ranks
337-
# hold no gathered experts at all), and writing them too would race on the same files.
338-
writes_layers = (
339-
tp_rank == 0 and get_data_parallel_rank() == 0 and get_expert_model_parallel_rank() == 0
340-
)
341336

342337
quantization_format = self._get_quantization_format(self.model)
343338
if self._any_rank_uses_iq_quantization():
@@ -357,6 +352,22 @@ def save_pretrained(
357352
"Megatron IQ1_S/IQ2_XS unified export currently requires pipeline model "
358353
"parallel size 1"
359354
)
355+
# SequentialMLP rules index experts by local position, so EP>1 would collide. Agreed across
356+
# ranks so that stages without such experts don't block in the collectives below.
357+
if (
358+
torch.distributed.is_initialized()
359+
and get_expert_model_parallel_world_size() > 1
360+
and self._any_rank(any(hasattr(m, "local_experts") for m in self.model.modules()))
361+
):
362+
raise NotImplementedError(
363+
"Export at expert parallel size > 1 needs grouped-GEMM experts; "
364+
"export SequentialMLP (--no_moe_grouped_gemm) checkpoints at EP=1."
365+
)
366+
# One writer per pipeline stage: other TP / DP / EP ranks hold the same layers (EP>1 ranks
367+
# hold no gathered experts at all), and writing them too would race on the same files.
368+
writes_layers = (
369+
tp_rank == 0 and get_data_parallel_rank() == 0 and get_expert_model_parallel_rank() == 0
370+
)
360371

361372
# Main export process
362373
layer_state_dicts = self.layer_state_dicts
@@ -800,15 +811,6 @@ def _get_transformer_layer_state_dict(self, layer, layer_id, is_mtp=False):
800811
layer.mlp.shared_experts.gate_weight, layer_id, is_mtp=is_mtp
801812
)
802813
if hasattr(layer.mlp.experts, "local_experts"):
803-
# SequentialMLP rules index experts by local position, so EP>1 would collide.
804-
if (
805-
torch.distributed.is_initialized()
806-
and get_expert_model_parallel_world_size() > 1
807-
):
808-
raise NotImplementedError(
809-
"Export at expert parallel size > 1 needs grouped-GEMM experts; "
810-
"export SequentialMLP (--no_moe_grouped_gemm) checkpoints at EP=1."
811-
)
812814
if not self.rules.get("use_packed_local_experts", False):
813815
for expert_id, expert in enumerate(layer.mlp.experts.local_experts):
814816
self.rules["local_experts.linear_fc1"](
@@ -1319,11 +1321,15 @@ def _any_rank_uses_iq_quantization(self) -> bool:
13191321
holding no IQ layer would skip the raise and then block in the next collective while its
13201322
peers exit. Agree across ranks first, mirroring ``_gather_exclude_modules``.
13211323
"""
1322-
local_uses_iq = uses_iq_quantization(self.model)
1324+
return self._any_rank(uses_iq_quantization(self.model))
1325+
1326+
@staticmethod
1327+
def _any_rank(local: bool) -> bool:
1328+
"""Whether ``local`` holds on any rank, so callers can raise everywhere or nowhere."""
13231329
if not torch.distributed.is_initialized():
1324-
return local_uses_iq
1330+
return local
13251331
per_rank = [None] * torch.distributed.get_world_size()
1326-
torch.distributed.all_gather_object(per_rank, local_uses_iq)
1332+
torch.distributed.all_gather_object(per_rank, local)
13271333
return any(per_rank)
13281334

13291335
def _get_quantization_format(self, module: torch.nn.Module):

‎tests/gpu_megatron/torch/distill/plugins/test_distill_megatron.py‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ def _test_topk_logits_kl_loss(top_k, rank, size):
216216
loss["kd_loss"].backward()
217217

218218

219-
def _test_skip_lm_loss_with_mtp(quantized, rank, size):
219+
def _test_skip_lm_loss_with_mtp(quantized, skip_lm_loss, rank, size):
220220
"""Test that skip_lm_loss only zeroes the main LM head, unless MTP is left out of quantization."""
221221
set_seed(SEED)
222222

@@ -264,7 +264,7 @@ def _test_skip_lm_loss_with_mtp(quantized, rank, size):
264264
student_model.decoder.weight_quantizer = TensorQuantizer()
265265

266266
distill_cfg = setup_distillation_config(
267-
config_or_path=DistillationConfig(skip_lm_loss=True),
267+
config_or_path=DistillationConfig(skip_lm_loss=skip_lm_loss),
268268
student_cfg=student_model.config,
269269
teacher_cfg=teacher_model.config,
270270
)
@@ -277,10 +277,11 @@ def _test_skip_lm_loss_with_mtp(quantized, rank, size):
277277
adjust_distillation_model_for_mcore(distillation_model, distill_cfg)
278278

279279
# An untrained MTP head is frozen so DDP doesn't wait for its grads.
280+
mtp_skipped = quantized and skip_lm_loss
280281
with distillation_model.hide_teacher_model():
281282
mtp_params = [p for n, p in distillation_model.named_parameters() if "mtp" in n.split(".")]
282283
assert mtp_params
283-
assert all(p.requires_grad != quantized for p in mtp_params)
284+
assert all(p.requires_grad != mtp_skipped for p in mtp_params)
284285

285286
# Intercept each call to compute_language_model_loss and record return values.
286287
recorded_losses = []
@@ -314,8 +315,8 @@ def _recording_loss(labels, logits):
314315
f"Expected {mtp_num_layers + 1} loss calls, got {len(recorded_losses)}"
315316
)
316317
for i, loss in enumerate(recorded_losses[:-1]):
317-
assert loss.any() != quantized, f"MTP head {i} loss should be zero iff MTP is skipped"
318-
assert not recorded_losses[-1].any(), "Main LM head loss should be zero with skip_lm_loss=True"
318+
assert loss.any() != mtp_skipped, f"MTP head {i} loss should be zero iff MTP is skipped"
319+
assert recorded_losses[-1].any() != skip_lm_loss, "Main LM head loss is zero iff skip_lm_loss"
319320

320321

321322
def test_logits_kl_loss(dist_workers):
@@ -328,10 +329,12 @@ def test_topk_logits_kl_loss(dist_workers, top_k: int = 5):
328329
dist_workers.run(partial(_test_topk_logits_kl_loss, top_k))
329330

330331

331-
@pytest.mark.parametrize("quantized", [False, True])
332-
def test_skip_lm_loss_with_mtp(dist_workers, quantized):
332+
@pytest.mark.parametrize(
333+
("quantized", "skip_lm_loss"), [(False, True), (True, True), (True, False)]
334+
)
335+
def test_skip_lm_loss_with_mtp(dist_workers, quantized, skip_lm_loss):
333336
"""Test that skip_lm_loss only zeroes the main LM head, unless MTP is left out of quantization."""
334-
dist_workers.run(partial(_test_skip_lm_loss_with_mtp, quantized))
337+
dist_workers.run(partial(_test_skip_lm_loss_with_mtp, quantized, skip_lm_loss))
335338

336339

337340
def test_mtp_excluded_from_quantization():

‎tests/gpu_megatron/torch/export/test_unified_export_megatron.py‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from _test_utils.torch.megatron.models import get_mcore_gpt_model, get_mcore_hybrid_model
3131
from _test_utils.torch.megatron.utils import get_forward
3232
from _test_utils.torch.transformers_models import (
33+
create_tiny_deepseek_v3_dir,
3334
create_tiny_llama_dir,
3435
create_tiny_nemotron_dir,
3536
create_tiny_nemotron_h_dir,
@@ -394,6 +395,51 @@ def test_megatron_iq_export_rejects_tensor_parallelism(qformat):
394395
exporter.save_pretrained("unused", "unused")
395396

396397

398+
def test_megatron_export_rejects_sequential_experts_at_ep():
399+
"""SequentialMLP experts are numbered by local position, so EP>1 export must refuse them."""
400+
experts = torch.nn.Module()
401+
experts.local_experts = torch.nn.ModuleList()
402+
exporter = object.__new__(GPTModelExporter)
403+
exporter.model = torch.nn.Sequential(experts)
404+
405+
with (
406+
patch.object(exporter, "_is_sidecar_writer_rank", return_value=False),
407+
patch.object(GPTModelExporter, "_any_rank", staticmethod(lambda local: local)),
408+
patch.object(uem.torch.distributed, "is_initialized", return_value=True),
409+
patch.object(uem, "get_expert_model_parallel_world_size", return_value=2),
410+
patch.object(uem, "get_pipeline_model_parallel_rank", return_value=0),
411+
patch.object(uem, "get_pipeline_model_parallel_world_size", return_value=1),
412+
patch.object(uem, "get_tensor_model_parallel_rank", return_value=0),
413+
pytest.raises(NotImplementedError, match="grouped-GEMM experts"),
414+
):
415+
exporter.save_pretrained("unused", "unused")
416+
417+
418+
def _test_mla_export_keeps_hf_head_dim(model_dir, rank, size):
419+
model = get_mcore_gpt_model(
420+
tensor_model_parallel_size=size,
421+
pipeline_model_parallel_size=1,
422+
initialize_megatron=True,
423+
num_layers=2,
424+
hidden_size=128,
425+
num_attention_heads=2,
426+
vocab_size=128,
427+
max_sequence_length=128,
428+
transformer_impl="transformer_engine",
429+
multi_latent_attention=True,
430+
).cuda()
431+
hf_head_dim = transformers.AutoConfig.from_pretrained(model_dir).head_dim
432+
# kv_channels is MLA's V head dim, which the exporter used to write as HF's head_dim.
433+
assert model.config.kv_channels != hf_head_dim
434+
exporter = GPTModelExporter(model, str(model_dir))
435+
assert exporter._hf_text_config.head_dim == hf_head_dim
436+
437+
438+
def test_mla_export_keeps_hf_head_dim(dist_workers_size_1, tmp_path):
439+
model_dir = create_tiny_deepseek_v3_dir(tmp_path)
440+
dist_workers_size_1.run(partial(_test_mla_export_keeps_hf_head_dim, model_dir))
441+
442+
397443
@pytest.mark.parametrize("qformat", IQ_FORMAT_NAMES)
398444
def test_megatron_iq_export_rejects_pipeline_parallelism(qformat):
399445
"""IQ packing requires PP=1 so the fused-MoE rejection reaches every rank.
@@ -424,6 +470,27 @@ def test_megatron_iq_export_rejects_pipeline_parallelism(qformat):
424470
exporter.save_pretrained("unused", "unused")
425471

426472

473+
def _verify_exported_metadata(export_dir: Path, model_type, quant_config, extra_module):
474+
# The router bias decides expert routing: FP32 like Megatron's buffer and HF's checkpoints.
475+
bias_dtypes = set()
476+
for shard in export_dir.glob("*.safetensors"):
477+
with safe_open(str(shard), framework="pt") as f:
478+
for key in f.keys(): # noqa: SIM118 - safe_open is not iterable
479+
if key.endswith(("expert_bias", "e_score_correction_bias")):
480+
bias_dtypes.add(f.get_slice(key).get_dtype())
481+
assert bias_dtypes <= {"F32"}, bias_dtypes
482+
if model_type == "qwen3_moe":
483+
assert bias_dtypes
484+
if model_type == "qwen3vl" and quant_config:
485+
# The vision tower is copied through unquantized.
486+
quant = json.loads((export_dir / "hf_quant_config.json").read_text())["quantization"]
487+
assert "model.visual*" in quant["exclude_modules"]
488+
if model_type == "llama" and quant_config is None and extra_module is None:
489+
# The source generation config (top_p without do_sample) is copied through as-is.
490+
generation_config = json.loads((export_dir / "generation_config.json").read_text())
491+
assert generation_config["top_p"] == 0.5
492+
493+
427494
def _test_unified_export_megatron(
428495
tmp_path,
429496
model_type,
@@ -585,6 +652,9 @@ def _test_unified_export_megatron(
585652
if quant_config:
586653
_verify_model_quant_config(tmp_export_dir, quant_config, kv_cache_quant_cfg)
587654

655+
if rank == 0:
656+
_verify_exported_metadata(tmp_export_dir, model_type, quant_config, extra_module)
657+
588658
if rank == 0 and extra_module is None:
589659
# Names / shapes only: these Megatron weights are random, not loaded from model_dir.
590660
allow_missing = ()
@@ -650,6 +720,10 @@ def test_unified_export_megatron(
650720
):
651721
if model_type == "llama":
652722
model_dir = create_tiny_llama_dir(tmp_path)
723+
# Valid to load, but rejected by GenerationConfig.save_pretrained's validation.
724+
(model_dir / "generation_config.json").write_text(
725+
json.dumps({"top_p": 0.5, "do_sample": False})
726+
)
653727
elif model_type == "qwen3vl":
654728
model_dir = create_tiny_qwen3vl_dir(tmp_path)
655729
elif model_type == "nemotron":

0 commit comments

Comments
 (0)