Skip to content

Commit 7bbe5de

Browse files
authored
feat(recipe): add Qwen3.5 text MXFP8 long-context SFT (#6300)
Signed-off-by: Chen Cui <chcui@nvidia.com>
1 parent ea66fc4 commit 7bbe5de

6 files changed

Lines changed: 295 additions & 6 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from .gb200.qwen35 import (
2222
qwen35_text_9b_pretrain_8gpu_gb200_bf16_config,
2323
qwen35_text_35b_a3b_pretrain_8gpu_gb200_bf16_config,
24+
qwen35_text_35b_a3b_sft_long_context_16gpu_gb200_fp8mx_config,
2425
)
2526

2627
# Qwen2 models
@@ -175,4 +176,5 @@
175176
"qwen35_text_9b_pretrain_8gpu_gb200_bf16_config",
176177
"qwen35_text_35b_a3b_pretrain_config",
177178
"qwen35_text_35b_a3b_pretrain_8gpu_gb200_bf16_config",
179+
"qwen35_text_35b_a3b_sft_long_context_16gpu_gb200_fp8mx_config",
178180
]

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@
1818
from megatron.bridge.recipes.qwen.gb200.qwen35 import (
1919
qwen35_text_9b_pretrain_8gpu_gb200_bf16_config,
2020
qwen35_text_35b_a3b_pretrain_8gpu_gb200_bf16_config,
21+
qwen35_text_35b_a3b_sft_long_context_16gpu_gb200_fp8mx_config,
2122
)
2223

2324

2425
__all__ = [
2526
"qwen3_30b_a3b_pretrain_8gpu_gb200_fp8mx_config",
2627
"qwen35_text_9b_pretrain_8gpu_gb200_bf16_config",
2728
"qwen35_text_35b_a3b_pretrain_8gpu_gb200_bf16_config",
29+
"qwen35_text_35b_a3b_sft_long_context_16gpu_gb200_fp8mx_config",
2830
]

‎src/megatron/bridge/recipes/qwen/gb200/qwen35.py‎

Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,139 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
"""GB200 text-only pretraining recipes for Qwen3.5 dense and MoE models."""
15+
"""GB200 text-only pretraining and SFT recipes for Qwen3.5 models."""
1616

1717
from __future__ import annotations
1818

19+
from copy import deepcopy
20+
1921
import torch
2022
from transformers import AutoConfig
2123

2224
from megatron.bridge import AutoBridge
23-
from megatron.bridge.recipes.common import _pretrain_common
25+
from megatron.bridge.recipes.common import _pretrain_common, _sft_common
26+
from megatron.bridge.recipes.utils.dataset_utils import default_coderforge_config
27+
from megatron.bridge.recipes.utils.environment_utils import COMMON_RECIPE_ENV_VARS
28+
from megatron.bridge.recipes.utils.optimizer_utils import distributed_fused_adam_with_cosine_annealing
2429
from megatron.bridge.training.comm_overlap import CommOverlapConfig
2530
from megatron.bridge.training.config import ConfigContainer
26-
from megatron.bridge.training.mixed_precision import bf16_mixed
31+
from megatron.bridge.training.mixed_precision import bf16_mixed, bf16_with_mxfp8_mixed
2732

2833

2934
_QWEN35_9B_BASE = "Qwen/Qwen3.5-9B-Base"
3035
_QWEN35_35B_A3B_BASE = "Qwen/Qwen3.5-35B-A3B-Base"
36+
_QWEN35_35B_A3B_INSTRUCT_REVISION = "59d61f3ce65a6d9863b86d2e96597125219dc754" # pragma: allowlist secret
37+
_CODERFORGE_REVISION = "060fca96cf723b2ebab3181e9e59fafd273df3cb" # pragma: allowlist secret
38+
39+
40+
def qwen35_text_35b_a3b_sft_long_context_16gpu_gb200_fp8mx_config(
41+
seq_length: int = 131072,
42+
*,
43+
hf_path: str = "Qwen/Qwen3.5-35B-A3B",
44+
hf_revision: str | None = _QWEN35_35B_A3B_INSTRUCT_REVISION,
45+
) -> ConfigContainer:
46+
"""MXFP8 text SFT with packed chat data, TP1/CP8/EP16 and one MTP layer.
47+
48+
Set a pretrained text checkpoint before training. The default CoderForge
49+
source is normalized and packed by the dataset builder. Model config and
50+
tokenizer share a pinned revision; use ``hf_revision=None`` for an extracted
51+
local causal-LM checkpoint. No vision model is constructed.
52+
53+
Args:
54+
seq_length: Packed token budget, a positive multiple of 16 for CP8.
55+
hf_path: HF model ID or extracted local text checkpoint.
56+
hf_revision: Matching model/tokenizer revision, or None for local inputs.
57+
58+
Returns:
59+
Training configuration with MXFP8 parameter gather and CuTeDSL grouped MLP.
60+
"""
61+
if seq_length <= 0 or seq_length % 16:
62+
raise ValueError("seq_length must be positive and divisible by 16 for CP8.")
63+
cfg = _sft_common()
64+
hf_config = AutoConfig.from_pretrained(hf_path, revision=hf_revision)
65+
text_config = deepcopy(getattr(hf_config, "text_config", hf_config))
66+
if text_config.model_type != "qwen3_5_moe_text":
67+
raise ValueError("Expected a Qwen3.5 MoE text configuration.")
68+
if seq_length > text_config.max_position_embeddings:
69+
raise ValueError("seq_length exceeds the text model's max_position_embeddings.")
70+
if hasattr(hf_config, "text_config"):
71+
text_config.tie_word_embeddings = hf_config.tie_word_embeddings
72+
text_config.architectures = ["Qwen3_5MoeForCausalLM"]
73+
cfg.model = AutoBridge.from_hf_config(text_config).to_megatron_provider(load_weights=False)
74+
cfg.tokenizer.tokenizer_model = hf_path
75+
cfg.tokenizer.hf_tokenizer_kwargs = {"revision": hf_revision}
76+
77+
cfg.model.seq_length = seq_length
78+
cfg.model.pipeline_dtype = torch.bfloat16
79+
cfg.model.context_parallel_size = 8
80+
cfg.model.expert_model_parallel_size = 16
81+
cfg.model.expert_tensor_parallel_size = 1
82+
cfg.model.mtp_num_layers = 1
83+
cfg.model.bias_activation_fusion = True
84+
cfg.model.cross_entropy_fusion_impl = "te"
85+
cfg.model.calculate_per_token_loss = True
86+
cfg.model.recompute_granularity = "selective"
87+
cfg.model.recompute_modules = ["gdn_norm_out", "moe"]
88+
cfg.model.moe_token_dispatcher_type = "flex"
89+
cfg.model.moe_flex_dispatcher_backend = "hybridep"
90+
cfg.model.moe_flex_dispatcher_num_sms = 32
91+
cfg.model.moe_hybridep_pad_uneven_dispatch_inputs = True
92+
cfg.model.moe_router_fusion = True
93+
cfg.model.moe_use_grouped_tensor = True
94+
cfg.model.use_transformer_engine_op_fuser = True
95+
cfg.model.moe_mlp_glu_interleave_size = 32
96+
cfg.model.moe_single_grouped_weight = True
97+
98+
# Pad each runtime segment to 2*CP, not each conversation to seq_length.
99+
# Physical and logical cu_seqlens remain distinct for attention and MTP.
100+
cfg.dataset = default_coderforge_config(
101+
seq_length=seq_length,
102+
enable_offline_packing=True,
103+
pad_seq_to_mult=16,
104+
)
105+
cfg.dataset.dataset_kwargs = {"pad_to_max_length": True}
106+
cfg.dataset.hf_dataset.load_kwargs = {"revision": _CODERFORGE_REVISION}
107+
cfg.dataset.do_validation = True
108+
cfg.dataset.hf_validation_proportion = 0.05
109+
# Preserve the original long-context recipe's sampling and worker lifecycle.
110+
cfg.dataset.seed = 1234
111+
cfg.dataset.persistent_workers = True
112+
cfg.train.train_iters = 500
113+
cfg.train.global_batch_size = 32
114+
cfg.train.manual_gc = True
115+
cfg.train.manual_gc_interval = 100
116+
cfg.train.manual_gc_eval = 100
117+
cfg.rng.seed = 1234
118+
cfg.validation.eval_interval = 50
119+
cfg.validation.eval_iters = 8
120+
# Preserve the VL baseline's LR schedule rather than silently retuning SFT.
121+
cfg.optimizer, cfg.scheduler = distributed_fused_adam_with_cosine_annealing(
122+
lr_warmup_iters=200, lr_decay_iters=300000, max_lr=2e-5, min_lr=2e-6
123+
)
124+
cfg.optimizer.overlap_param_gather = True
125+
cfg.mixed_precision = bf16_with_mxfp8_mixed()
126+
cfg.checkpoint.load_main_params_from_ckpt = True
127+
cfg.checkpoint.load_optim = False
128+
cfg.checkpoint.load_rng = False
129+
cfg.ddp.use_distributed_optimizer = True
130+
cfg.ddp.data_parallel_sharding_strategy = "optim_grads_params"
131+
cfg.ddp.overlap_grad_reduce = True
132+
cfg.ddp.overlap_param_gather = True
133+
cfg.env_vars = {
134+
**COMMON_RECIPE_ENV_VARS,
135+
"CUDA_DEVICE_MAX_CONNECTIONS": 1,
136+
"NVTE_BWD_LAYERNORM_SM_MARGIN": 20,
137+
"NVTE_FWD_LAYERNORM_SM_MARGIN": 20,
138+
"NUM_OF_HYBRID_EP_RANKS_PER_NVLINK_DOMAIN": 16,
139+
"NUM_OF_TOKENS_PER_CHUNK_COMBINE_API": 128,
140+
"NVLINK_DOMAIN_SIZE": 72,
141+
"USE_MNNVL": 1,
142+
"NVTE_CUTEDSL_FUSED_GROUPED_MLP": 1,
143+
"CUDNN_FE_GROUPED_GEMM_DYNAMIC_MNKL": 1,
144+
"NVTE_GROUPED_LINEAR_SINGLE_PARAM": 1,
145+
}
146+
cfg.checkpoint.load = None
147+
return cfg
31148

32149

33150
def qwen35_text_9b_pretrain_8gpu_gb200_bf16_config() -> ConfigContainer:

‎tests/unit_tests/recipes/recipe_test_utils.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,14 @@ def patch_recipe_construction_dependencies(monkeypatch: pytest.MonkeyPatch) -> N
261261

262262
def load_offline_auto_config(*args: object, **kwargs: object) -> SimpleNamespace:
263263
del args, kwargs
264-
return SimpleNamespace(text_config=SimpleNamespace(architectures=None))
264+
return SimpleNamespace(
265+
tie_word_embeddings=False,
266+
text_config=SimpleNamespace(
267+
architectures=None,
268+
model_type="qwen3_5_moe_text",
269+
max_position_embeddings=262144,
270+
),
271+
)
265272

266273
monkeypatch.setattr(
267274
AutoConfig,
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
2+
3+
import importlib
4+
from copy import deepcopy
5+
from pathlib import Path
6+
from types import SimpleNamespace
7+
8+
import pytest
9+
import torch
10+
from transformers import AutoConfig
11+
12+
from megatron.bridge.recipes.qwen.gb200 import qwen35
13+
14+
15+
pytestmark = pytest.mark.unit
16+
_MXFP8 = qwen35.qwen35_text_35b_a3b_sft_long_context_16gpu_gb200_fp8mx_config
17+
18+
19+
@pytest.fixture
20+
def hf_config(monkeypatch):
21+
config = AutoConfig.for_model(
22+
"qwen3_5_moe",
23+
text_config={
24+
"num_hidden_layers": 4,
25+
"hidden_size": 512,
26+
"num_attention_heads": 4,
27+
"num_key_value_heads": 4,
28+
"head_dim": 128,
29+
"max_position_embeddings": 262144,
30+
"num_experts": 32,
31+
"num_experts_per_tok": 2,
32+
"moe_intermediate_size": 128,
33+
"shared_expert_intermediate_size": 128,
34+
"linear_num_key_heads": 8,
35+
"linear_num_value_heads": 8,
36+
"linear_key_head_dim": 128,
37+
"linear_value_head_dim": 128,
38+
"tie_word_embeddings": True,
39+
},
40+
tie_word_embeddings=False,
41+
)
42+
calls = []
43+
44+
def load(path, **kwargs):
45+
calls.append((path, kwargs))
46+
return config
47+
48+
monkeypatch.setattr(qwen35.AutoConfig, "from_pretrained", load)
49+
return config, calls
50+
51+
52+
def test_text_long_context_validates_and_preserves_inputs(hf_config, monkeypatch):
53+
config, calls = hf_config
54+
original = deepcopy(config.to_dict())
55+
training_module = importlib.import_module("megatron.bridge.training.config")
56+
monkeypatch.setattr(training_module, "get_world_size_safe", lambda: 16)
57+
monkeypatch.setattr(
58+
torch.cuda, "get_device_properties", lambda index: SimpleNamespace(major=10, name="NVIDIA GB200")
59+
)
60+
61+
cfg = _MXFP8()
62+
cfg.validate()
63+
64+
assert config.to_dict() == original
65+
assert calls == [("Qwen/Qwen3.5-35B-A3B", {"revision": qwen35._QWEN35_35B_A3B_INSTRUCT_REVISION})]
66+
assert cfg.tokenizer.hf_tokenizer_kwargs["revision"] == calls[0][1]["revision"]
67+
assert cfg.model.share_embeddings_and_output_weights is False
68+
assert cfg.model.mtp_num_layers == 1
69+
assert cfg.model.mtp_loss_scaling_factor == 0.1
70+
assert cfg.model.context_parallel_size == 8
71+
assert cfg.model.expert_model_parallel_size == 16
72+
assert cfg.model.tensor_model_parallel_size == 1
73+
assert cfg.model.sequence_parallel is False
74+
assert cfg.model.recompute_modules == ["gdn_norm_out", "moe"]
75+
assert cfg.dataset.hf_dataset.dataset_name == "coderforge"
76+
assert cfg.dataset.hf_dataset.load_kwargs == {"revision": qwen35._CODERFORGE_REVISION}
77+
assert cfg.dataset.dataset_root is None
78+
assert cfg.dataset.hf_validation_proportion == 0.05
79+
assert cfg.dataset.offline_packing_specs.pad_seq_to_mult == 16
80+
assert cfg.dataset.offline_packing_specs.packed_sequence_size == 131072
81+
assert cfg.dataset.dataset_kwargs["pad_to_max_length"] is True
82+
assert cfg.dataset.seed == 1234
83+
assert cfg.dataset.persistent_workers is True
84+
assert cfg.train.global_batch_size == 32
85+
assert cfg.train.micro_batch_size == 1
86+
assert cfg.train.train_iters == 500
87+
assert cfg.scheduler.lr_warmup_iters == 200
88+
assert cfg.scheduler.lr_decay_iters == 300000
89+
assert cfg.ddp.grad_reduce_in_fp32 is True
90+
91+
92+
def test_verified_mxfp8_kernel_and_parameter_settings(hf_config):
93+
cfg = _MXFP8()
94+
assert cfg.mixed_precision.fp8_recipe == "mxfp8"
95+
assert cfg.mixed_precision.fp8_param_gather is True
96+
assert cfg.mixed_precision.reuse_grad_buf_for_mxfp8_param_ag is True
97+
assert cfg.checkpoint.load_main_params_from_ckpt is True
98+
assert cfg.checkpoint.load_optim is False
99+
assert cfg.checkpoint.load_rng is False
100+
assert cfg.model.moe_use_grouped_tensor is True
101+
assert cfg.model.use_transformer_engine_op_fuser is True
102+
assert cfg.model.moe_mlp_glu_interleave_size == 32
103+
assert cfg.model.moe_single_grouped_weight is True
104+
assert cfg.model.moe_single_grouped_bias is False
105+
assert cfg.env_vars["NUM_OF_HYBRID_EP_RANKS_PER_NVLINK_DOMAIN"] == 16
106+
assert cfg.env_vars["NVTE_CUTEDSL_FUSED_GROUPED_MLP"] == 1
107+
assert cfg.env_vars["CUDNN_FE_GROUPED_GEMM_DYNAMIC_MNKL"] == 1
108+
assert cfg.env_vars["NVTE_GROUPED_LINEAR_SINGLE_PARAM"] == 1
109+
110+
111+
@pytest.mark.parametrize("seq_length", [0, -16, 15, 131073])
112+
def test_invalid_sequence_length_fails_before_hub_access(hf_config, seq_length):
113+
_, calls = hf_config
114+
with pytest.raises(ValueError, match="positive and divisible by 16"):
115+
_MXFP8(seq_length)
116+
assert calls == []
117+
118+
119+
def test_local_text_config_uses_matching_tokenizer(hf_config, monkeypatch):
120+
config, _ = hf_config
121+
monkeypatch.setattr(qwen35.AutoConfig, "from_pretrained", lambda *_args, **_kwargs: config.text_config)
122+
123+
cfg = _MXFP8(1024, hf_path="local-text-checkpoint", hf_revision=None)
124+
125+
assert cfg.tokenizer.tokenizer_model == "local-text-checkpoint"
126+
assert cfg.tokenizer.hf_tokenizer_kwargs == {"revision": None}
127+
assert cfg.model.seq_length == 1024
128+
assert cfg.dataset.offline_packing_specs.packed_sequence_size == 1024
129+
130+
131+
def test_wrong_model_type_is_rejected(hf_config):
132+
config, _ = hf_config
133+
config.text_config.model_type = "qwen3_5_text"
134+
with pytest.raises(ValueError, match="Expected a Qwen3.5 MoE"):
135+
_MXFP8()
136+
137+
138+
@pytest.mark.parametrize("seq_length", [16, 32768, 65536, 131072])
139+
def test_aligned_context_lengths(hf_config, seq_length):
140+
cfg = _MXFP8(seq_length)
141+
assert cfg.model.seq_length == cfg.dataset.seq_length == seq_length
142+
assert cfg.dataset.offline_packing_specs.packed_sequence_size == seq_length
143+
144+
145+
def test_context_exceeding_model_limit_is_rejected(hf_config):
146+
config, _ = hf_config
147+
with pytest.raises(ValueError, match="max_position_embeddings"):
148+
_MXFP8(config.text_config.max_position_embeddings + 16)
149+
150+
151+
def test_public_runner_finds_unique_text_recipe(hf_config, monkeypatch):
152+
monkeypatch.syspath_prepend(str(Path(__file__).resolve().parents[3] / "scripts/training"))
153+
runner = importlib.import_module("recipe_runner")
154+
assert runner.find_library_recipe(_MXFP8.__name__) is _MXFP8
155+
assert runner.find_benchmark_recipe(_MXFP8.__name__) is None
156+
cfg = runner.load_recipe(_MXFP8.__name__)
157+
assert cfg.model.mtp_num_layers == 1
158+
assert cfg.dataset.hf_dataset.dataset_name == "coderforge"

‎tests/unit_tests/recipes/test_qwen_recipes.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,16 +97,19 @@ def to_megatron_provider(self, load_weights: bool = False):
9797

9898
class _FakeTextConfig:
9999
architectures = None
100+
model_type = "qwen3_5_moe_text"
101+
max_position_embeddings = 262144
100102

101103

102104
class _FakeRootConfig:
103105
text_config = _FakeTextConfig()
106+
tie_word_embeddings = False
104107

105108

106109
class _FakeAutoConfig:
107110
@staticmethod
108-
def from_pretrained(hf_path: str):
109-
# Ignore hf_path; return a unified config with a nested text config.
111+
def from_pretrained(hf_path: str, *, revision: str | None = None):
112+
# Match the pinned-config API without downloading a model configuration.
110113
return _FakeRootConfig()
111114

112115

0 commit comments

Comments
 (0)