Skip to content

Commit 6dd02c4

Browse files
tomlifuclaude
andauthored
perf(recipe): add an 8-GPU GB300 MXFP8 pretrain recipe for Qwen3.5-VL 35B-A3B (#6413)
Signed-off-by: tomlifu <lifuz@nvidia.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 1234500 commit 6dd02c4

7 files changed

Lines changed: 230 additions & 8 deletions

File tree

‎scripts/training/run_recipe.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,11 @@
132132
TrainMode = Literal["pretrain", "finetune"]
133133

134134

135-
IMPORT_TIME_RECIPE_ENV_VARS = frozenset({"NVTE_CPU_OFFLOAD_V1"})
135+
# Recipe environment variables that are read when a library is imported, before this script applies the recipe
136+
# environment. A library recipe that declares one of them (and the user has not set it) is re-executed with the
137+
# recipe environment in place. Transformer Engine reads NVTE_CUTEDSL_FUSED_GROUPED_MLP once, when it registers the
138+
# CuTe DSL fused grouped MLP at import time; set any later, it is ignored and the unfused grouped GEMM runs.
139+
IMPORT_TIME_RECIPE_ENV_VARS = frozenset({"NVTE_CPU_OFFLOAD_V1", "NVTE_CUTEDSL_FUSED_GROUPED_MLP"})
136140

137141

138142
COMMON_OVERRIDE_FIELDS = (

‎src/megatron/bridge/recipes/qwen_vl/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
# Qwen3.5/Qwen3.6-VL GB300 models
2525
from .gb300.qwen35_vl import (
26+
qwen35_vl_35b_a3b_pretrain_8gpu_gb300_fp8mx_library_config,
2627
qwen35_vl_35b_a3b_pretrain_16gpu_gb300_bf16_config,
2728
qwen35_vl_397b_a17b_pretrain_config,
2829
)
@@ -85,6 +86,7 @@
8586
"qwen35_vl_9b_pretrain_mock_config",
8687
"qwen35_vl_27b_pretrain_16gpu_gb200_bf16_mock_config",
8788
"qwen35_vl_35b_a3b_pretrain_config",
89+
"qwen35_vl_35b_a3b_pretrain_8gpu_gb300_fp8mx_library_config",
8890
"qwen35_vl_35b_a3b_pretrain_16gpu_gb300_bf16_config",
8991
"qwen35_vl_397b_a17b_pretrain_config",
9092
"qwen35_vl_35b_a3b_pretrain_mock_config",

‎src/megatron/bridge/recipes/qwen_vl/gb300/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313
# limitations under the License.
1414

1515
from megatron.bridge.recipes.qwen_vl.gb300.qwen35_vl import (
16+
qwen35_vl_35b_a3b_pretrain_8gpu_gb300_fp8mx_library_config,
1617
qwen35_vl_35b_a3b_pretrain_16gpu_gb300_bf16_config,
1718
qwen35_vl_397b_a17b_pretrain_config,
1819
)
1920

2021

2122
__all__ = [
23+
"qwen35_vl_35b_a3b_pretrain_8gpu_gb300_fp8mx_library_config",
2224
"qwen35_vl_35b_a3b_pretrain_16gpu_gb300_bf16_config",
2325
"qwen35_vl_397b_a17b_pretrain_config",
2426
]

‎src/megatron/bridge/recipes/qwen_vl/gb300/qwen35_vl.py‎

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from megatron.bridge.recipes.utils.environment_utils import COMMON_RECIPE_ENV_VARS
2626
from megatron.bridge.training.comm_overlap import CommOverlapConfig
2727
from megatron.bridge.training.config import ConfigContainer
28-
from megatron.bridge.training.mixed_precision import bf16_mixed
28+
from megatron.bridge.training.mixed_precision import bf16_mixed, bf16_with_mxfp8_mixed
2929
from megatron.bridge.utils.cuda_graph import set_cuda_graph_modules
3030

3131

@@ -330,7 +330,94 @@ def qwen35_vl_35b_a3b_pretrain_16gpu_gb300_bf16_config() -> ConfigContainer:
330330
return cfg
331331

332332

333+
def qwen35_vl_35b_a3b_pretrain_8gpu_gb300_fp8mx_library_config() -> ConfigContainer:
334+
"""Return the MXFP8 full-pretraining config for Qwen3.5/Qwen3.6-VL 35B-A3B on 8 GB300 GPUs.
335+
336+
Starts from :func:`qwen35_vl_35b_a3b_pretrain_16gpu_gb300_bf16_config` and keeps its parallel layout (EP 4,
337+
TP/PP/CP 1), CUDA graphs, recompute, batch shape (micro batch 4, global batch 2048) and real routing. On top it
338+
sets MXFP8, Transformer Engine's CuTe DSL fused grouped MLP, cuDNN GDN, and the HybridEP, dispatcher and
339+
normalization settings of the configuration it was tuned on. With TP, PP and CP at 1, data parallelism is 8 on
340+
8 GPUs (expert data parallelism 2), so each rank runs 64 micro batches per step.
341+
342+
Like the 16-GPU recipe, it defaults to the mock VLM dataset. The measured runs used an Energon dataset
343+
(``--dataset energon dataset.path=...``) with ``dataset.task_encoder.max_pixels=200704``, and the vision CUDA graph
344+
cap inherited from the 16-GPU recipe (3712 patches per micro batch) was measured with that image budget at micro
345+
batch 4. Keep both, or re-measure the cap: a larger image budget can exceed it and stop training with an error.
346+
347+
cuDNN GDN is selected only on a Megatron-Core that declares ``gdn_kernel_backend`` (NVIDIA/Megatron-LM#7583);
348+
otherwise the GatedDeltaNet layers keep running on FLA.
349+
350+
Transformer Engine reads the fused-MLP gate ``NVTE_CUTEDSL_FUSED_GROUPED_MLP`` once, when it is imported.
351+
``scripts/training/run_recipe.py``, which ``train.sh`` runs, sets it in time by re-executing itself; with any
352+
other entry point, export it before Python starts, or Transformer Engine silently runs the unfused grouped GEMM.
353+
354+
The name carries a ``_library`` suffix because ``qwen35_vl_35b_a3b_pretrain_8gpu_gb300_fp8mx_config`` is
355+
already a perf recipe (a mock-data benchmark with EP 8 and global batch 512), and bare recipe lookup keeps
356+
resolving that name to it.
357+
358+
Measured on GB300 with real DataComp data and real routing (steps 101-150, one run per arm, submitted together),
359+
a launch configuration close to this recipe reached 11.3% more TFLOP/s per GPU on 8 GPUs than on 16 at the same
360+
global batch size. The two runs differ in data-parallel size and in micro batches per rank (64 vs 32), and one
361+
run per arm does not separate these effects, so no cause is claimed.
362+
"""
363+
cfg = qwen35_vl_35b_a3b_pretrain_16gpu_gb300_bf16_config()
364+
365+
cfg.mixed_precision = bf16_with_mxfp8_mixed()
366+
cfg.mixed_precision.grad_reduce_in_fp32 = False
367+
368+
# CuTe DSL fused grouped MLP (FC1 + SwiGLU + FC2 for MXFP8 on GPUs with compute capability 10.x). The op fuser,
369+
# the 32-wide GLU interleave and the NVTE_CUTEDSL_FUSED_GROUPED_MLP env gate (cfg.env_vars below) are all
370+
# required. Transformer Engine reads the gate when it is imported and registers the fusion only if it is set
371+
# (scripts/training/run_recipe.py re-executes itself so the gate is set before that import; other entry points
372+
# must export it); without the gate or the interleave it silently runs the unfused grouped GEMM.
373+
cfg.model.use_transformer_engine_op_fuser = True
374+
cfg.model.moe_mlp_glu_interleave_size = 32
375+
376+
cfg.model.moe_flex_dispatcher_num_sms = 32
377+
# Off: HybridEP's fused permute requires identical dispatch, combine and preprocessing chunk sizes, and this
378+
# recipe uses 64/128/64 (cfg.env_vars below). The tuned configuration ran this way too.
379+
cfg.model.moe_permute_fusion_into_hybridep = False
380+
381+
# The tuned configuration started from qwen35_vl_35b_a3b_pretrain_config and kept these values. The 16-GPU
382+
# recipe does not carry them over (it leaves the two model fields at Megatron-Core defaults, sets the dispatch
383+
# and preprocessing chunk sizes to 128 and omits the cuDNN normalization flags), and was never measured with
384+
# MXFP8.
385+
cfg.model.moe_hybridep_num_sms_preprocessing = 16
386+
cfg.model.overlap_dispatch_backward_with_experts_wgrad = True
387+
388+
# cuDNN GDN (FROST engine: cuDNN frontend >= 1.29 and nvidia-cutlass-dsl >= 4.7): measured +12.0% to +19.4% end
389+
# to end at 35B-A3B on 16 GB300 GPUs (see _select_gdn_kernel_backend in perf_recipes/qwen_vl/common.py). Guarded
390+
# like _enable_gdn_conv_fusion: assigning a field Megatron-Core lacks would not raise, and the recipe would look
391+
# enabled while FLA keeps running.
392+
if hasattr(type(cfg.model), "gdn_kernel_backend") or hasattr(cfg.model, "gdn_kernel_backend"):
393+
cfg.model.gdn_kernel_backend = "transformer_engine"
394+
395+
# The complete environment, declared here rather than inherited from the 16-GPU recipe. It differs from that
396+
# recipe in the fused-MLP gate, the dispatch and preprocessing chunk sizes (64 instead of 128) and the cuDNN
397+
# normalization flags.
398+
cfg.env_vars = {
399+
**COMMON_RECIPE_ENV_VARS,
400+
"CUDA_DEVICE_MAX_CONNECTIONS": 32,
401+
# Must equal expert_model_parallel_size or HybridEP splits the NVLink
402+
# domain wrongly. Unset does NOT auto-detect.
403+
"NUM_OF_HYBRID_EP_RANKS_PER_NVLINK_DOMAIN": 4,
404+
"NUM_OF_TOKENS_PER_CHUNK_COMBINE_API": 128,
405+
"NUM_OF_TOKENS_PER_CHUNK_DISPATCH_API": 64,
406+
"NUM_OF_TOKENS_PER_CHUNK_PREPROCESSING_API": 64,
407+
"NVLINK_DOMAIN_SIZE": 72,
408+
"USE_MNNVL": 1,
409+
# 0, as in the 16-GPU recipe: the LayerNorm SM margin follows the communication share (see there).
410+
"NVTE_BWD_LAYERNORM_SM_MARGIN": 0,
411+
"NVTE_FWD_LAYERNORM_SM_MARGIN": 0,
412+
"NVTE_NORM_BWD_USE_CUDNN": 1,
413+
"NVTE_NORM_FWD_USE_CUDNN": 1,
414+
"NVTE_CUTEDSL_FUSED_GROUPED_MLP": 1,
415+
}
416+
return cfg
417+
418+
333419
__all__ = [
420+
"qwen35_vl_35b_a3b_pretrain_8gpu_gb300_fp8mx_library_config",
334421
"qwen35_vl_35b_a3b_pretrain_16gpu_gb300_bf16_config",
335422
"qwen35_vl_397b_a17b_pretrain_config",
336423
]

‎tests/unit_tests/recipes/qwen_vl/test_qwen35_vl_recipes.py‎

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
]
116116

117117
_QWEN35_VL_GB300_FUNCS = [
118+
_qwen35_vl_gb300_module.qwen35_vl_35b_a3b_pretrain_8gpu_gb300_fp8mx_library_config,
118119
_qwen35_vl_gb300_module.qwen35_vl_35b_a3b_pretrain_16gpu_gb300_bf16_config,
119120
_qwen35_vl_gb300_module.qwen35_vl_397b_a17b_pretrain_config,
120121
]
@@ -514,6 +515,120 @@ def test_qwen35_vl_35b_a3b_pretrain_16gpu_gb300_defaults(monkeypatch: pytest.Mon
514515
assert cfg.checkpoint.pretrained_checkpoint is None
515516

516517

518+
def test_qwen35_vl_35b_a3b_pretrain_8gpu_gb300_fp8mx_library_defaults(monkeypatch: pytest.MonkeyPatch):
519+
"""The 8-GB300 MXFP8 recipe adds the measured precision, kernel and dispatcher settings to the 16-GPU recipe."""
520+
patch_recipe_module_global(monkeypatch, _qwen35_vl_h100_module, "AutoBridge", _FakeAutoBridge)
521+
522+
cfg = _qwen35_vl_gb300_module.qwen35_vl_35b_a3b_pretrain_8gpu_gb300_fp8mx_library_config()
523+
base = _qwen35_vl_gb300_module.qwen35_vl_35b_a3b_pretrain_16gpu_gb300_bf16_config()
524+
525+
_assert_basic_config(cfg)
526+
# Same parallel layout, CUDA graphs, recompute, batch shape and routing as the 16-GPU recipe.
527+
assert cfg.model.expert_model_parallel_size == 4
528+
assert cfg.env_vars["NUM_OF_HYBRID_EP_RANKS_PER_NVLINK_DOMAIN"] == 4
529+
assert cfg.train.global_batch_size == base.train.global_batch_size == 2048
530+
assert cfg.train.micro_batch_size == base.train.micro_batch_size == 4
531+
assert cfg.model.max_vision_cuda_graph_seq_length == 3712
532+
assert cuda_graph_module_names(cfg.model) == ["attn", "moe_router", "moe_preprocess"]
533+
assert cfg.model.recompute_modules == ["core_attn", "gdn_norm_out"]
534+
assert cfg.model.moe_router_force_load_balancing is False
535+
# MXFP8 with FP8 parameter all-gather.
536+
assert cfg.mixed_precision.fp8 == "e4m3"
537+
assert cfg.mixed_precision.fp8_recipe == "mxfp8"
538+
assert cfg.mixed_precision.fp8_param_gather is True
539+
assert cfg.mixed_precision.reuse_grad_buf_for_mxfp8_param_ag is True
540+
assert cfg.mixed_precision.grad_reduce_in_fp32 is False
541+
# CuTe DSL fused grouped MLP needs all three settings together.
542+
assert cfg.model.use_transformer_engine_op_fuser is True
543+
assert cfg.model.moe_mlp_glu_interleave_size == 32
544+
assert cfg.env_vars["NVTE_CUTEDSL_FUSED_GROUPED_MLP"] == 1
545+
assert cfg.model.moe_flex_dispatcher_num_sms == 32
546+
assert cfg.model.moe_permute_fusion_into_hybridep is False
547+
assert cfg.model.moe_hybridep_num_sms_preprocessing == 16
548+
assert cfg.model.overlap_dispatch_backward_with_experts_wgrad is True
549+
assert cfg.env_vars["NUM_OF_TOKENS_PER_CHUNK_COMBINE_API"] == 128
550+
assert cfg.env_vars["NUM_OF_TOKENS_PER_CHUNK_DISPATCH_API"] == 64
551+
assert cfg.env_vars["NUM_OF_TOKENS_PER_CHUNK_PREPROCESSING_API"] == 64
552+
assert cfg.env_vars["NVTE_NORM_FWD_USE_CUDNN"] == 1
553+
assert cfg.env_vars["NVTE_NORM_BWD_USE_CUDNN"] == 1
554+
# Building the 8-GPU recipe must not leak the env gate into the 16-GPU recipe.
555+
assert "NVTE_CUTEDSL_FUSED_GROUPED_MLP" not in base.env_vars
556+
557+
# These are the only model-field and environment-variable differences from the 16-GPU recipe; an extra model
558+
# field or environment variable fails here. Mixed precision is checked above on five fields only, and other
559+
# sections, such as train, optimizer, ddp and comm_overlap, are not compared.
560+
changed_model_fields = {
561+
name
562+
for name in vars(cfg.model).keys() | vars(base.model).keys()
563+
if getattr(cfg.model, name, None) != getattr(base.model, name, None)
564+
}
565+
assert changed_model_fields == {
566+
"use_transformer_engine_op_fuser",
567+
"moe_mlp_glu_interleave_size",
568+
"moe_flex_dispatcher_num_sms",
569+
"moe_permute_fusion_into_hybridep",
570+
"moe_hybridep_num_sms_preprocessing",
571+
"overlap_dispatch_backward_with_experts_wgrad",
572+
}
573+
changed_env_vars = {
574+
name
575+
for name in cfg.env_vars.keys() | base.env_vars.keys()
576+
if cfg.env_vars.get(name) != base.env_vars.get(name)
577+
}
578+
assert changed_env_vars == {
579+
"NVTE_CUTEDSL_FUSED_GROUPED_MLP",
580+
"NUM_OF_TOKENS_PER_CHUNK_DISPATCH_API",
581+
"NUM_OF_TOKENS_PER_CHUNK_PREPROCESSING_API",
582+
"NVTE_NORM_FWD_USE_CUDNN",
583+
"NVTE_NORM_BWD_USE_CUDNN",
584+
}
585+
586+
587+
class _FakeModelCfgWithGdnKernelBackend(_FakeModelCfg):
588+
"""Model config from a Megatron-Core that declares gdn_kernel_backend (NVIDIA/Megatron-LM#7583)."""
589+
590+
# Megatron-Core's default; a class attribute, as on the real dataclass.
591+
gdn_kernel_backend = "fla"
592+
593+
594+
class _FakeAutoBridgeWithGdnKernelBackend(_FakeAutoBridge):
595+
"""AutoBridge yielding a provider that declares gdn_kernel_backend."""
596+
597+
@staticmethod
598+
def from_hf_pretrained(hf_path: str, *, revision: str | None = None):
599+
return _FakeAutoBridgeWithGdnKernelBackend()
600+
601+
def to_megatron_provider(self, load_weights: bool = False):
602+
return _FakeModelCfgWithGdnKernelBackend()
603+
604+
605+
def test_qwen35_vl_35b_a3b_pretrain_8gpu_gb300_fp8mx_library_selects_cudnn_gdn(monkeypatch: pytest.MonkeyPatch):
606+
"""If Megatron-Core has gdn_kernel_backend, the 8-GPU recipe selects cuDNN GDN; the 16-GPU one keeps FLA."""
607+
patch_recipe_module_global(monkeypatch, _qwen35_vl_h100_module, "AutoBridge", _FakeAutoBridgeWithGdnKernelBackend)
608+
609+
cfg = _qwen35_vl_gb300_module.qwen35_vl_35b_a3b_pretrain_8gpu_gb300_fp8mx_library_config()
610+
base = _qwen35_vl_gb300_module.qwen35_vl_35b_a3b_pretrain_16gpu_gb300_bf16_config()
611+
612+
assert cfg.model.gdn_kernel_backend == "transformer_engine"
613+
# The 16-GPU BF16 recipe keeps Megatron-Core's default.
614+
assert base.model.gdn_kernel_backend == "fla"
615+
616+
617+
def test_qwen35_vl_35b_a3b_pretrain_8gpu_gb300_fp8mx_library_no_phantom_gdn_kernel_backend(
618+
monkeypatch: pytest.MonkeyPatch,
619+
):
620+
"""On a Megatron-Core without gdn_kernel_backend the recipe must not invent the attribute.
621+
622+
Assigning an unknown field would not raise: it would create an unused attribute, so the recipe would look as if it
623+
selected cuDNN GDN while the layers keep running on FLA.
624+
"""
625+
patch_recipe_module_global(monkeypatch, _qwen35_vl_h100_module, "AutoBridge", _FakeAutoBridge)
626+
627+
cfg = _qwen35_vl_gb300_module.qwen35_vl_35b_a3b_pretrain_8gpu_gb300_fp8mx_library_config()
628+
629+
assert not hasattr(cfg.model, "gdn_kernel_backend")
630+
631+
517632
def test_qwen35_vl_35b_a3b_pretrain_16gpu_h100_defaults(monkeypatch: pytest.MonkeyPatch):
518633
"""The 16-H100 library pretrain recipe should own the measured execution policy."""
519634
patch_recipe_module_global(monkeypatch, _qwen35_vl_h100_module, "AutoBridge", _FakeAutoBridge)

‎tests/unit_tests/recipes/test_recipe_environment_defaults.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,16 @@ def test_explicit_recipe_environment_invariants():
210210
"NVTE_FWD_LAYERNORM_SM_MARGIN": 0,
211211
},
212212
),
213+
(
214+
"qwen_vl/gb300/qwen35_vl.py",
215+
"qwen35_vl_35b_a3b_pretrain_8gpu_gb300_fp8mx_library_config",
216+
{
217+
"NUM_OF_HYBRID_EP_RANKS_PER_NVLINK_DOMAIN": 4,
218+
"NUM_OF_TOKENS_PER_CHUNK_DISPATCH_API": 64,
219+
"NUM_OF_TOKENS_PER_CHUNK_PREPROCESSING_API": 64,
220+
"NVTE_CUTEDSL_FUSED_GROUPED_MLP": 1,
221+
},
222+
),
213223
(
214224
"nemotronh_multimodal/h100/nemotron_35_super_vl.py",
215225
"nemotron_35_super_vl_sft_64gpu_h100_bf16_config",

‎tests/unit_tests/scripts/training/test_run_recipe.py‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -496,10 +496,11 @@ def test_library_only_canonical_name_applies_runtime_environment_without_restart
496496
handles.recipe_runner.load_forward_step.assert_called_once_with("llm_step", mode="pretrain")
497497

498498

499-
def test_library_recipe_bootstraps_unset_import_time_environment(monkeypatch):
499+
@pytest.mark.parametrize("env_name", ["NVTE_CPU_OFFLOAD_V1", "NVTE_CUTEDSL_FUSED_GROUPED_MLP"])
500+
def test_library_recipe_bootstraps_unset_import_time_environment(monkeypatch, env_name):
500501
module, handles = _load_module()
501-
monkeypatch.delenv("NVTE_CPU_OFFLOAD_V1", raising=False)
502-
config = SimpleNamespace(env_vars={"NVTE_CPU_OFFLOAD_V1": 1})
502+
monkeypatch.delenv(env_name, raising=False)
503+
config = SimpleNamespace(env_vars={env_name: 1})
503504
handles.recipe_runner.load_recipe.return_value = config
504505

505506
module.main(["--recipe", "deepseek_v4_flash_pretrain_config", "--mode", "pretrain"])
@@ -511,10 +512,11 @@ def test_library_recipe_bootstraps_unset_import_time_environment(monkeypatch):
511512
handles.recipe_runner.apply_runtime_environment.assert_not_called()
512513

513514

514-
def test_library_recipe_preserves_explicit_import_time_environment(monkeypatch):
515+
@pytest.mark.parametrize("env_name", ["NVTE_CPU_OFFLOAD_V1", "NVTE_CUTEDSL_FUSED_GROUPED_MLP"])
516+
def test_library_recipe_preserves_explicit_import_time_environment(monkeypatch, env_name):
515517
module, handles = _load_module()
516-
monkeypatch.setenv("NVTE_CPU_OFFLOAD_V1", "0")
517-
config = SimpleNamespace(env_vars={"NVTE_CPU_OFFLOAD_V1": 1})
518+
monkeypatch.setenv(env_name, "0")
519+
config = SimpleNamespace(env_vars={env_name: 1})
518520
handles.recipe_runner.load_recipe.return_value = config
519521

520522
module.main(["--recipe", "deepseek_v4_flash_pretrain_config", "--mode", "pretrain"])

0 commit comments

Comments
 (0)