Skip to content

Commit d9d98b9

Browse files
tomlifuclaude
andcommitted
perf(recipe): add an 8-GPU GB300 MXFP8 pretrain recipe for Qwen3.5-VL 35B-A3B
Add qwen35_vl_35b_a3b_pretrain_8gpu_gb300_fp8mx_library_config, an MXFP8 Qwen3.5-VL 35B-A3B pretraining recipe for 8 GB300 GPUs. It starts from qwen35_vl_35b_a3b_pretrain_16gpu_gb300_bf16_config, keeps its parallel layout, CUDA graphs, recompute, batch shape and real routing, and adds: - MXFP8 with FP8 parameter all-gather (bf16_with_mxfp8_mixed); - Transformer Engine's CuTe DSL fused grouped MLP: the op fuser, the 32-wide GLU interleave and the NVTE_CUTEDSL_FUSED_GROUPED_MLP gate, which only work together; - 32 flex-dispatcher SMs, with HybridEP's fused permute off: it requires identical dispatch, combine and preprocessing chunk sizes, and this recipe uses 64/128/64; - the HybridEP preprocessing SMs, dispatch/expert-wgrad overlap, dispatch and preprocessing chunk sizes and cuDNN normalization of the configuration it was tuned on, which the 16-GPU recipe does not carry over; - cuDNN GDN (gdn_kernel_backend="transformer_engine"), set only when Megatron-Core declares the field (NVIDIA/Megatron-LM#7583), with the same guard as _enable_gdn_conv_fusion. The name carries a _library suffix because qwen35_vl_35b_a3b_pretrain_8gpu_gb300_fp8mx_config is already a perf recipe. Transformer Engine reads NVTE_CUTEDSL_FUSED_GROUPED_MLP once, when it registers the fused MLP at import time, and run_recipe.py applied a library recipe's env_vars after that import, so the gate was ignored. Add the variable to IMPORT_TIME_RECIPE_ENV_VARS, so that a recipe declaring it goes through the existing re-exec bootstrap, as for NVTE_CPU_OFFLOAD_V1. Through run_recipe.py this also changes two existing GB200 library recipes that declare the gate and no other import-time variable: qwen35_text_35b_a3b_sft_long_context_16gpu_gb200_fp8mx_config, which enables the op fuser and the GLU interleave, now runs the fused MLP instead of the unfused grouped GEMM (not measured here), and qwen3_30b_a3b_pretrain_8gpu_gb200_fp8mx_config, which keeps the op fuser off, now re-executes once. Tests check the recipe's settings, the exact set of model fields and environment variables in which it differs from the 16-GPU recipe, that building it leaves the 16-GPU recipe unchanged, that cuDNN GDN is selected for a model config that declares the field and that no attribute is created on one without it, and that the bootstrap covers both import-time variables. Signed-off-by: tomlifu <lifuz@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 912c16e commit d9d98b9

6 files changed

Lines changed: 203 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: 71 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,77 @@ 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 env gate are all required. Transformer Engine reads the gate when it is
370+
# imported and registers the fusion only if it is set (scripts/training/run_recipe.py re-executes itself so the
371+
# gate is set before that import; other entry points must export it); without the gate or the interleave it
372+
# 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+
cfg.env_vars["NVTE_CUTEDSL_FUSED_GROUPED_MLP"] = 1
376+
377+
cfg.model.moe_flex_dispatcher_num_sms = 32
378+
# Off: HybridEP's fused permute requires identical dispatch, combine and preprocessing chunk sizes, and this
379+
# recipe uses 64/128/64 (set below and in the 16-GPU env). The tuned configuration ran this way too.
380+
cfg.model.moe_permute_fusion_into_hybridep = False
381+
382+
# The tuned configuration started from qwen35_vl_35b_a3b_pretrain_config and kept these values. The 16-GPU
383+
# recipe does not carry them over (it leaves the two model fields at Megatron-Core defaults, sets both chunk
384+
# sizes to 128 and omits the cuDNN normalization flags), and was never measured with MXFP8.
385+
cfg.model.moe_hybridep_num_sms_preprocessing = 16
386+
cfg.model.overlap_dispatch_backward_with_experts_wgrad = True
387+
cfg.env_vars["NUM_OF_TOKENS_PER_CHUNK_DISPATCH_API"] = 64
388+
cfg.env_vars["NUM_OF_TOKENS_PER_CHUNK_PREPROCESSING_API"] = 64
389+
cfg.env_vars["NVTE_NORM_BWD_USE_CUDNN"] = 1
390+
cfg.env_vars["NVTE_NORM_FWD_USE_CUDNN"] = 1
391+
392+
# cuDNN GDN (FROST engine: cuDNN frontend >= 1.29 and nvidia-cutlass-dsl >= 4.7): measured +12.0% to +19.4% end
393+
# to end at 35B-A3B on 16 GB300 GPUs (see _select_gdn_kernel_backend in perf_recipes/qwen_vl/common.py). Guarded
394+
# like _enable_gdn_conv_fusion: assigning a field Megatron-Core lacks would not raise, and the recipe would look
395+
# enabled while FLA keeps running.
396+
if hasattr(type(cfg.model), "gdn_kernel_backend") or hasattr(cfg.model, "gdn_kernel_backend"):
397+
cfg.model.gdn_kernel_backend = "transformer_engine"
398+
399+
return cfg
400+
401+
333402
__all__ = [
403+
"qwen35_vl_35b_a3b_pretrain_8gpu_gb300_fp8mx_library_config",
334404
"qwen35_vl_35b_a3b_pretrain_16gpu_gb300_bf16_config",
335405
"qwen35_vl_397b_a17b_pretrain_config",
336406
]

‎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/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)