Skip to content

Commit d3fcda9

Browse files
authored
feat(recipe): add Qwen3.5 VL long-context GB200 SFT recipes (#6324)
Signed-off-by: Chen Cui <chcui@nvidia.com>
1 parent 67f1920 commit d3fcda9

4 files changed

Lines changed: 275 additions & 4 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
qwen35_vl_27b_pretrain_16gpu_gb200_bf16_mock_config,
1818
qwen35_vl_35b_a3b_peft_8gpu_gb200_bf16_functional_config,
1919
qwen35_vl_35b_a3b_sft_8gpu_gb200_bf16_functional_config,
20+
qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_bf16_config,
21+
qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_fp8mx_config,
2022
)
2123

2224
# Qwen3.5/Qwen3.6-VL GB300 models
@@ -97,6 +99,8 @@
9799
# Qwen3.5-VL SFT configs — MoE
98100
"qwen35_vl_35b_a3b_sft_config",
99101
"qwen35_vl_35b_a3b_sft_8gpu_gb200_bf16_functional_config",
102+
"qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_bf16_config",
103+
"qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_fp8mx_config",
100104
"qwen35_vl_35b_a3b_fsdp_sft_config",
101105
"qwen35_vl_122b_a10b_sft_config",
102106
"qwen35_vl_397b_a17b_sft_config",

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@
1616
qwen35_vl_27b_pretrain_16gpu_gb200_bf16_mock_config,
1717
qwen35_vl_35b_a3b_peft_8gpu_gb200_bf16_functional_config,
1818
qwen35_vl_35b_a3b_sft_8gpu_gb200_bf16_functional_config,
19+
qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_bf16_config,
20+
qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_fp8mx_config,
1921
)
2022

2123

2224
__all__ = [
2325
"qwen35_vl_27b_pretrain_16gpu_gb200_bf16_mock_config",
2426
"qwen35_vl_35b_a3b_peft_8gpu_gb200_bf16_functional_config",
2527
"qwen35_vl_35b_a3b_sft_8gpu_gb200_bf16_functional_config",
28+
"qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_bf16_config",
29+
"qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_fp8mx_config",
2630
]

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

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
from __future__ import annotations
1818

19+
from megatron.bridge import AutoBridge
20+
from megatron.bridge.data.builders import EnergonDatasetConfig, QwenVLEnergonTaskEncoderConfig
21+
from megatron.bridge.recipes.common import _sft_common_vlm
1922
from megatron.bridge.recipes.qwen_vl.h100.qwen35_vl import (
2023
qwen35_vl_27b_pretrain_16gpu_h100_bf16_mock_config,
2124
qwen35_vl_35b_a3b_peft_4gpu_h100_bf16_config,
@@ -24,7 +27,7 @@
2427
from megatron.bridge.recipes.utils.environment_utils import COMMON_RECIPE_ENV_VARS
2528
from megatron.bridge.training.comm_overlap import CommOverlapConfig
2629
from megatron.bridge.training.config import ConfigContainer
27-
from megatron.bridge.training.mixed_precision import get_mixed_precision_config
30+
from megatron.bridge.training.mixed_precision import bf16_mixed, bf16_with_mxfp8_mixed, get_mixed_precision_config
2831
from megatron.bridge.utils.cuda_graph import set_cuda_graph_modules
2932

3033

@@ -212,6 +215,132 @@ def qwen35_vl_35b_a3b_sft_8gpu_gb200_bf16_functional_config() -> ConfigContainer
212215
return cfg
213216

214217

218+
_QWEN35_35B_A3B_REVISION = "59d61f3ce65a6d9863b86d2e96597125219dc754" # pragma: allowlist secret
219+
220+
221+
def _qwen35_vl_35b_a3b_long_context_common() -> ConfigContainer:
222+
"""Build the shared 32-GB200 long-context SFT configuration."""
223+
cfg = _sft_common_vlm()
224+
hf_path = "Qwen/Qwen3.5-35B-A3B"
225+
cfg.model = AutoBridge.from_hf_pretrained(hf_path, revision=_QWEN35_35B_A3B_REVISION).to_megatron_provider(
226+
load_weights=False
227+
)
228+
cfg.model.calculate_per_token_loss = True
229+
cfg.ddp.average_in_collective = False
230+
231+
cfg.model.mtp_num_layers = 1
232+
cfg.model.bias_activation_fusion = True
233+
cfg.model.cuda_graph_scope = []
234+
cfg.model.moe_router_fusion = True
235+
cfg.model.offload_modules = None
236+
cfg.model.seq_length = 131072
237+
cfg.model.tensor_model_parallel_size = 2
238+
cfg.model.context_parallel_size = 8
239+
cfg.model.expert_model_parallel_size = 16
240+
cfg.model.expert_tensor_parallel_size = 1
241+
cfg.model.sequence_parallel = True
242+
cfg.model.moe_token_dispatcher_type = "flex"
243+
cfg.model.moe_flex_dispatcher_backend = "hybridep"
244+
cfg.model.moe_flex_dispatcher_num_sms = 32
245+
cfg.model.moe_hybridep_pad_uneven_dispatch_inputs = True
246+
247+
cfg.model.cross_entropy_fusion_impl = "te"
248+
cfg.model.recompute_granularity = "selective"
249+
cfg.model.recompute_modules = ["gdn_norm_out", "moe"]
250+
cfg.model.vision_recompute_granularity = "full"
251+
cfg.model.vision_recompute_method = "uniform"
252+
cfg.model.vision_recompute_num_layers = 1
253+
254+
cfg.train.micro_batch_size = 1
255+
# Nemotron Image Training v3, clevr_2 subset, prepared as Energon shards.
256+
# Native packing combines samples into one physical 128K microbatch.
257+
cfg.dataset = EnergonDatasetConfig(
258+
seq_length=cfg.model.seq_length,
259+
micro_batch_size=cfg.train.micro_batch_size,
260+
num_workers=1,
261+
shuffle_buffer_size=2,
262+
packing_buffer_size=8,
263+
dataset_kwargs={"image_decode_spec": "pilrgb"},
264+
task_encoder=QwenVLEnergonTaskEncoderConfig(
265+
hf_processor_path=hf_path,
266+
hf_processor_revision=_QWEN35_35B_A3B_REVISION,
267+
max_pixels=401408,
268+
max_num_images=4,
269+
max_visual_tokens=2048,
270+
),
271+
)
272+
273+
cfg.optimizer.lr = 2e-5
274+
cfg.optimizer.min_lr = 2e-6
275+
cfg.scheduler.lr_warmup_iters = 200
276+
cfg.scheduler.lr_decay_iters = 300000
277+
cfg.scheduler.lr_wsd_decay_iters = 300000
278+
cfg.comm_overlap = CommOverlapConfig(
279+
tp_comm_overlap=False,
280+
overlap_grad_reduce=False,
281+
overlap_param_gather=False,
282+
overlap_param_gather_with_optimizer_step=False,
283+
overlap_moe_expert_parallel_comm=False,
284+
delay_wgrad_compute=False,
285+
)
286+
cfg.env_vars = {
287+
**COMMON_RECIPE_ENV_VARS,
288+
"CUDA_DEVICE_MAX_CONNECTIONS": 1,
289+
"NVTE_BWD_LAYERNORM_SM_MARGIN": 20,
290+
"NVTE_FWD_LAYERNORM_SM_MARGIN": 20,
291+
# Both language and vision towers use automatic TE backend selection.
292+
"NVTE_FUSED_ATTN": 1,
293+
"NVTE_FLASH_ATTN": 1,
294+
"NVTE_UNFUSED_ATTN": 1,
295+
"NCCL_GRAPH_REGISTER": 0,
296+
"NCCL_NVLS_ENABLE": 0,
297+
"NUM_OF_HYBRID_EP_RANKS_PER_NVLINK_DOMAIN": 16,
298+
"NUM_OF_TOKENS_PER_CHUNK_COMBINE_API": 128,
299+
"NVLINK_DOMAIN_SIZE": 72,
300+
"TORCH_NCCL_AVOID_RECORD_STREAMS": 1,
301+
"USE_MNNVL": 1,
302+
}
303+
return cfg
304+
305+
306+
def qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_bf16_config() -> ConfigContainer:
307+
"""Return 128K BF16 Qwen3.5-VL SFT with TP2/CP8/EP16 on 32 GB200 GPUs.
308+
309+
Override with ``"dataset.path=$CLEVR2_ENERGON"`` to select the prepared
310+
Nemotron Image Training v3 ``clevr_2`` Energon shards, and supply a
311+
pretrained checkpoint.
312+
313+
Prepare the shards with `prepare_nemotron_image_v3.py
314+
<https://github.com/NVIDIA-NeMo/Megatron-Bridge/blob/main/tutorials/data/energon/prepare_nemotron_image_v3.py>`_
315+
using ``--subsets clevr_2 --skip-source-integrity-check`` after verifying
316+
the source revision; the built-in integrity manifest covers only ``turing``.
317+
Native Energon packing uses one physical microbatch per 128K pack.
318+
"""
319+
cfg = _qwen35_vl_35b_a3b_long_context_common()
320+
cfg.mixed_precision = bf16_mixed()
321+
return cfg
322+
323+
324+
def qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_fp8mx_config() -> ConfigContainer:
325+
"""Return packed CLEVR2 128K MXFP8 SFT with BF16 parameter communication.
326+
327+
Override with ``"dataset.path=$CLEVR2_ENERGON"`` to select the prepared
328+
Nemotron Image Training v3 ``clevr_2`` Energon shards, and supply a
329+
pretrained checkpoint.
330+
331+
Prepare the shards with `prepare_nemotron_image_v3.py
332+
<https://github.com/NVIDIA-NeMo/Megatron-Bridge/blob/main/tutorials/data/energon/prepare_nemotron_image_v3.py>`_
333+
using ``--subsets clevr_2 --skip-source-integrity-check`` after verifying
334+
the source revision; the built-in integrity manifest covers only ``turing``.
335+
Native Energon packing uses one physical microbatch per 128K pack.
336+
"""
337+
cfg = _qwen35_vl_35b_a3b_long_context_common()
338+
cfg.mixed_precision = bf16_with_mxfp8_mixed()
339+
cfg.mixed_precision.fp8_param_gather = False
340+
cfg.mixed_precision.reuse_grad_buf_for_mxfp8_param_ag = False
341+
return cfg
342+
343+
215344
def qwen35_vl_35b_a3b_peft_8gpu_gb200_bf16_functional_config() -> ConfigContainer:
216345
"""Return shared Qwen3.5/Qwen3.6-VL 35B-A3B LoRA for eight GB200 GPUs."""
217346
cfg = qwen35_vl_35b_a3b_peft_4gpu_h100_bf16_config()
@@ -223,4 +352,6 @@ def qwen35_vl_35b_a3b_peft_8gpu_gb200_bf16_functional_config() -> ConfigContaine
223352
"qwen35_vl_27b_pretrain_16gpu_gb200_bf16_mock_config",
224353
"qwen35_vl_35b_a3b_peft_8gpu_gb200_bf16_functional_config",
225354
"qwen35_vl_35b_a3b_sft_8gpu_gb200_bf16_functional_config",
355+
"qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_bf16_config",
356+
"qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_fp8mx_config",
226357
]

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

Lines changed: 135 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import importlib
2424
import inspect
25+
from types import SimpleNamespace
2526
from typing import Callable
2627

2728
import pytest
@@ -108,6 +109,8 @@
108109
_QWEN35_VL_GB200_FUNCS = [
109110
_qwen35_vl_gb200_module.qwen35_vl_27b_pretrain_16gpu_gb200_bf16_mock_config,
110111
_qwen35_vl_gb200_module.qwen35_vl_35b_a3b_sft_8gpu_gb200_bf16_functional_config,
112+
_qwen35_vl_gb200_module.qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_bf16_config,
113+
_qwen35_vl_gb200_module.qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_fp8mx_config,
111114
_qwen35_vl_gb200_module.qwen35_vl_35b_a3b_peft_8gpu_gb200_bf16_functional_config,
112115
]
113116

@@ -148,7 +151,7 @@ class _FakeAutoBridge:
148151
"""Fake AutoBridge for testing."""
149152

150153
@staticmethod
151-
def from_hf_pretrained(hf_path: str):
154+
def from_hf_pretrained(hf_path: str, *, revision: str | None = None):
152155
return _FakeAutoBridge()
153156

154157
def to_megatron_provider(self, load_weights: bool = False):
@@ -441,7 +444,7 @@ def test_qwen35_vl_27b_peft_lora_defaults(monkeypatch: pytest.MonkeyPatch):
441444

442445
def test_qwen35_vl_397b_a17b_pretrain_64gpu_gb300_defaults(monkeypatch: pytest.MonkeyPatch):
443446
"""The 64-GB300 library pretrain recipe should own the measured execution policy."""
444-
patch_recipe_module_global(monkeypatch, _qwen35_vl_gb300_module, "AutoBridge", _FakeAutoBridge)
447+
patch_recipe_module_global(monkeypatch, _qwen35_vl_h100_module, "AutoBridge", _FakeAutoBridge)
445448

446449
cfg = _qwen35_vl_gb300_module.qwen35_vl_397b_a17b_pretrain_config()
447450

@@ -472,7 +475,7 @@ def test_qwen35_vl_397b_a17b_pretrain_64gpu_gb300_defaults(monkeypatch: pytest.M
472475

473476
def test_qwen35_vl_35b_a3b_pretrain_16gpu_gb300_defaults(monkeypatch: pytest.MonkeyPatch):
474477
"""The 16-GB300 library pretrain recipe should own the measured execution policy."""
475-
patch_recipe_module_global(monkeypatch, _qwen35_vl_gb300_module, "AutoBridge", _FakeAutoBridge)
478+
patch_recipe_module_global(monkeypatch, _qwen35_vl_h100_module, "AutoBridge", _FakeAutoBridge)
476479

477480
cfg = _qwen35_vl_gb300_module.qwen35_vl_35b_a3b_pretrain_16gpu_gb300_bf16_config()
478481

@@ -659,6 +662,83 @@ def test_qwen35_vl_35b_a3b_long_context_sft_defaults(monkeypatch: pytest.MonkeyP
659662
assert cfg.ddp.average_in_collective is False
660663

661664

665+
def test_qwen35_vl_35b_a3b_gb200_long_context_precision_pair(monkeypatch: pytest.MonkeyPatch):
666+
"""The GB200 BF16 and MXFP8 recipes should share one 128K execution topology."""
667+
from megatron.bridge.data.builders import EnergonDatasetConfig, QwenVLEnergonTaskEncoderConfig
668+
from megatron.bridge.models.qwen_vl.qwen35_vl_provider import Qwen35VLMoEModelProvider
669+
670+
monkeypatch.setattr(
671+
_FakeAutoBridge,
672+
"to_megatron_provider",
673+
lambda self, load_weights=False: Qwen35VLMoEModelProvider(
674+
num_layers=40, hidden_size=2048, num_attention_heads=16, bias_activation_fusion=False
675+
),
676+
)
677+
patch_recipe_module_global(monkeypatch, _qwen35_vl_gb200_module, "AutoBridge", _FakeAutoBridge)
678+
679+
bf16_cfg = _qwen35_vl_gb200_module.qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_bf16_config()
680+
fp8mx_cfg = _qwen35_vl_gb200_module.qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_fp8mx_config()
681+
682+
for cfg in (bf16_cfg, fp8mx_cfg):
683+
_assert_basic_config(cfg)
684+
assert cfg.model.seq_length == 131072
685+
assert cfg.model.bias_activation_fusion is True
686+
assert cfg.model.tensor_model_parallel_size == 2
687+
assert cfg.model.pipeline_model_parallel_size == 1
688+
assert cfg.model.pipeline_dtype is None
689+
assert cfg.model.virtual_pipeline_model_parallel_size is None
690+
assert cfg.model.context_parallel_size == 8
691+
assert cfg.model.expert_model_parallel_size == 16
692+
assert cfg.model.expert_tensor_parallel_size == 1
693+
assert cfg.model.sequence_parallel is True
694+
assert cfg.model.moe_token_dispatcher_type == "flex"
695+
assert cfg.model.moe_flex_dispatcher_backend == "hybridep"
696+
assert cfg.model.moe_flex_dispatcher_num_sms == 32
697+
assert cfg.model.moe_hybridep_pad_uneven_dispatch_inputs is True
698+
assert cfg.model.gdn_pre_gated_delta_rule_fusion is False
699+
assert cfg.model.cross_entropy_fusion_impl == "te"
700+
assert cfg.model.recompute_granularity == "selective"
701+
assert cfg.model.recompute_modules == ["gdn_norm_out", "moe"]
702+
assert cfg.model.recompute_method is None
703+
assert cfg.model.recompute_num_layers is None
704+
assert cfg.model.vision_recompute_granularity == "full"
705+
assert cfg.model.vision_recompute_method == "uniform"
706+
assert cfg.model.vision_recompute_num_layers == 1
707+
assert cfg.model.vision_recompute_modules is None
708+
assert cfg.model.attention_backend.name == "auto"
709+
for variable in ("NVTE_FUSED_ATTN", "NVTE_FLASH_ATTN", "NVTE_UNFUSED_ATTN"):
710+
assert cfg.env_vars[variable] == 1
711+
assert cfg.train.global_batch_size == 32
712+
assert cfg.train.micro_batch_size == 1
713+
assert cfg.dataset.seq_length == 131072
714+
assert cfg.dataset.enable_in_batch_packing is False
715+
assert cfg.dataset.defer_in_batch_packing_to_step is False
716+
assert isinstance(cfg.dataset, EnergonDatasetConfig)
717+
assert cfg.dataset.path is None # Caller supplies prepared CLEVR2 shards.
718+
assert cfg.dataset.micro_batch_size == 1
719+
assert cfg.dataset.num_workers == 1
720+
assert cfg.dataset.shuffle_buffer_size == 2
721+
assert cfg.dataset.packing_buffer_size == 8
722+
assert cfg.dataset.dataset_kwargs == {"image_decode_spec": "pilrgb"}
723+
encoder = cfg.dataset.task_encoder
724+
assert isinstance(encoder, QwenVLEnergonTaskEncoderConfig)
725+
assert encoder.hf_processor_path == "Qwen/Qwen3.5-35B-A3B"
726+
assert encoder.hf_processor_revision == _qwen35_vl_gb200_module._QWEN35_35B_A3B_REVISION
727+
assert encoder.min_pixels == 200704
728+
assert encoder.max_pixels == 401408
729+
assert encoder.max_num_images == 4
730+
assert encoder.max_visual_tokens == 2048
731+
assert cfg.mixed_precision.grad_reduce_in_fp32 is True
732+
assert cfg.ddp.grad_reduce_in_fp32 is True
733+
assert cfg.env_vars["CUDA_DEVICE_MAX_CONNECTIONS"] == 1
734+
assert cfg.env_vars["NUM_OF_HYBRID_EP_RANKS_PER_NVLINK_DOMAIN"] == 16
735+
736+
assert bf16_cfg.mixed_precision.fp8 is None
737+
assert fp8mx_cfg.mixed_precision.fp8_recipe == "mxfp8"
738+
assert fp8mx_cfg.mixed_precision.fp8_param_gather is False
739+
assert fp8mx_cfg.mixed_precision.reuse_grad_buf_for_mxfp8_param_ag is False
740+
741+
662742
def test_qwen35_vl_35b_a3b_fsdp_sft_defaults(monkeypatch: pytest.MonkeyPatch):
663743
"""35B-A3B FSDP SFT should have FSDP-specific parallelism and settings."""
664744
patch_recipe_module_global(monkeypatch, _qwen35_vl_module, "AutoBridge", _FakeAutoBridge)
@@ -1453,3 +1533,55 @@ def test_qwen35_vl_gdn_conv_fusion_skipped_on_older_core(monkeypatch: pytest.Mon
14531533
cfg = _qwen35_vl_module.qwen35_vl_9b_pretrain_mock_config()
14541534

14551535
assert not hasattr(cfg.model, "gdn_pre_gated_delta_rule_fusion")
1536+
1537+
1538+
@pytest.mark.parametrize(
1539+
"recipe_func",
1540+
[
1541+
_qwen35_vl_gb200_module.qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_bf16_config,
1542+
_qwen35_vl_gb200_module.qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_fp8mx_config,
1543+
],
1544+
)
1545+
def test_gb200_long_context_validates_with_real_provider(monkeypatch, recipe_func):
1546+
from megatron.bridge.models.qwen_vl.qwen35_vl_provider import Qwen35VLMoEModelProvider
1547+
1548+
monkeypatch.setattr(
1549+
torch.cuda, "get_device_properties", lambda index: SimpleNamespace(major=10, name="NVIDIA GB200")
1550+
)
1551+
1552+
config_module = importlib.import_module("megatron.bridge.training.config")
1553+
monkeypatch.setattr(config_module, "get_world_size_safe", lambda: 32)
1554+
monkeypatch.setattr(
1555+
_FakeAutoBridge,
1556+
"to_megatron_provider",
1557+
lambda self, load_weights=False: Qwen35VLMoEModelProvider(
1558+
num_layers=40, hidden_size=2048, num_attention_heads=16
1559+
),
1560+
)
1561+
patch_recipe_module_global(monkeypatch, _qwen35_vl_gb200_module, "AutoBridge", _FakeAutoBridge)
1562+
config = recipe_func()
1563+
1564+
config.dataset.path = "prepared-clevr2-energon"
1565+
config.dataset.validate()
1566+
config.validate()
1567+
assert config.dataset.pad_to_max_length is True # Derived for HybridEP.
1568+
1569+
assert config.train.global_batch_size % (2 * config.train.micro_batch_size) == 0
1570+
config.dataset.enable_in_batch_packing = True
1571+
with pytest.raises(ValueError, match="mutually exclusive"):
1572+
config.dataset.validate()
1573+
1574+
1575+
def test_gb200_long_context_pins_model_and_processor_together(monkeypatch):
1576+
calls = []
1577+
1578+
def from_hf_pretrained(hf_path, **kwargs):
1579+
calls.append((hf_path, kwargs))
1580+
return _FakeAutoBridge()
1581+
1582+
monkeypatch.setattr(_FakeAutoBridge, "from_hf_pretrained", from_hf_pretrained)
1583+
patch_recipe_module_global(monkeypatch, _qwen35_vl_gb200_module, "AutoBridge", _FakeAutoBridge)
1584+
recipe = _qwen35_vl_gb200_module.qwen35_vl_35b_a3b_sft_long_context_32gpu_gb200_bf16_config
1585+
cfg = recipe()
1586+
encoder = cfg.dataset.task_encoder
1587+
assert calls == [(encoder.hf_processor_path, {"revision": encoder.hf_processor_revision})]

0 commit comments

Comments
 (0)