Skip to content

Commit 3283995

Browse files
authored
feat(recipes): modernize DeepSeek V4 library configs (#5554)
Signed-off-by: Chen Cui <chcui@nvidia.com>
1 parent 99b8dae commit 3283995

22 files changed

Lines changed: 1220 additions & 323 deletions

File tree

‎.dev.commit‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2f2f8ebaef90f4d62a18a43808398223b1fc5809
1+
8a77f4a9b78cd58dee862f234cc5cc84e47311be

‎examples/model_verification_cards/deepseek-v4-flash/card.yaml‎

Lines changed: 243 additions & 153 deletions
Large diffs are not rendered by default.

‎examples/models/deepseek_v4/README.md‎

Lines changed: 175 additions & 129 deletions
Large diffs are not rendered by default.

‎scripts/inference/setup_inference.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,12 @@ def _build_parser() -> argparse.ArgumentParser:
7575
"--gpus_per_node",
7676
type=int,
7777
dest="gpus_per_node",
78-
help="GPUs and inference tasks per node.",
78+
help="GPUs allocated per node.",
79+
)
80+
execution.add_argument(
81+
"--tasks-per-node",
82+
type=int,
83+
help="Inference tasks per node; defaults to one task per GPU.",
7984
)
8085
execution.add_argument("--cpus-per-task", type=int, help="CPUs allocated to each inference task.")
8186
execution.add_argument("--mem", help="Optional Slurm memory request, such as 64G.")
@@ -170,6 +175,8 @@ def _validate_args(args: argparse.Namespace) -> None:
170175
raise ValueError("--nodes must be at least 1.")
171176
if args.gpus_per_node is None or args.gpus_per_node < 1:
172177
raise ValueError("--gpus-per-node is required and must be at least 1.")
178+
if args.tasks_per_node is not None and not 1 <= args.tasks_per_node <= args.gpus_per_node:
179+
raise ValueError("--tasks-per-node must be between 1 and --gpus-per-node.")
173180
if args.cpus_per_task is not None and args.cpus_per_task < 1:
174181
raise ValueError("--cpus-per-task must be at least 1.")
175182
if any(not value.strip() for value in args.srun_args):
@@ -201,7 +208,7 @@ def _build_executor(args: argparse.Namespace, env_names: list[str], mounts: list
201208
account=args.account,
202209
partition=args.partition,
203210
nodes=args.nodes,
204-
ntasks_per_node=args.gpus_per_node,
211+
ntasks_per_node=args.tasks_per_node or args.gpus_per_node,
205212
cpus_per_task=args.cpus_per_task,
206213
mem=args.mem,
207214
exclusive=True if args.exclusive else None,

‎scripts/training/run_recipe.py‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@
132132
TrainMode = Literal["pretrain", "finetune"]
133133

134134

135+
IMPORT_TIME_RECIPE_ENV_VARS = frozenset({"NVTE_CPU_OFFLOAD_V1"})
136+
137+
135138
COMMON_OVERRIDE_FIELDS = (
136139
("max_steps", "train.train_iters"),
137140
("global_batch_size", "train.global_batch_size"),
@@ -420,6 +423,12 @@ def _selected_recipe_name(args: argparse.Namespace) -> str:
420423
return args.recipe or f"{args.model}_{recipe_task(args.mode)}_config"
421424

422425

426+
def _requires_recipe_environment_bootstrap(recipe: ConfigContainer) -> bool:
427+
"""Return whether a recipe declares an unset import-time environment variable."""
428+
recipe_env = getattr(recipe, "env_vars", None) or {}
429+
return any(name in recipe_env and name not in os.environ for name in IMPORT_TIME_RECIPE_ENV_VARS)
430+
431+
423432
def _load_selected_recipe(args: argparse.Namespace) -> ConfigContainer:
424433
"""Load the requested recipe by its complete name or model-derived library name."""
425434
peft_scheme = args.mode if args.mode in {"lora", "dora"} else None
@@ -496,17 +505,19 @@ def main(argv: list[str] | None = None) -> None:
496505
world_size=benchmark_world_size,
497506
)
498507
configuration_mode = _train_mode(args.mode)
499-
500-
if benchmark_metadata is not None:
508+
if benchmark_metadata is not None or _requires_recipe_environment_bootstrap(recipe):
501509
recipe = bootstrap_recipe_environment(
502510
recipe,
503511
script_path=str(Path(__file__).resolve()),
504512
argv=list(argv) if argv is not None else sys.argv[1:],
505513
)
514+
else:
515+
recipe = apply_runtime_environment(recipe)
516+
517+
if benchmark_metadata is not None:
506518
execution_mode = "pretrain"
507519
step_mode = benchmark_metadata.task
508520
else:
509-
recipe = apply_runtime_environment(recipe)
510521
execution_mode = configuration_mode
511522
step_mode = configuration_mode
512523

‎skills/create-model-verification-card/scripts/verify_hf_inference.py‎

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,27 @@ def _parse_args() -> argparse.Namespace:
4949
help="Pass enable_thinking=False to the tokenizer chat template.",
5050
)
5151
parser.add_argument("--device", default="cuda", help="Torch device used for inference.")
52+
parser.add_argument(
53+
"--device-map",
54+
choices=("auto", "balanced", "balanced_low_0", "sequential"),
55+
help="Optional Hugging Face device-map strategy for sharded model loading.",
56+
)
5257
parser.add_argument(
5358
"--dtype",
5459
choices=("bfloat16", "float16", "float32"),
5560
default="bfloat16",
5661
help="Model loading dtype.",
5762
)
63+
parser.add_argument(
64+
"--autocast",
65+
action="store_true",
66+
help="Run generation under autocast using the model loading dtype.",
67+
)
68+
parser.add_argument(
69+
"--require-gpu-only",
70+
action="store_true",
71+
help="Fail if any model shard is placed on CPU or disk.",
72+
)
5873
args = parser.parse_args()
5974
if args.max_new_tokens <= 0:
6075
parser.error("--max-new-tokens must be positive")
@@ -122,6 +137,23 @@ def _validate_loading_info(loading_info: dict[str, Any]) -> None:
122137
raise RuntimeError(f"Exported Hugging Face checkpoint did not reload strictly: {details}")
123138

124139

140+
def _validate_gpu_only_placement(model: Any) -> None:
141+
"""Require every model shard to be resident on CUDA devices."""
142+
device_map = getattr(model, "hf_device_map", None)
143+
if device_map is None:
144+
if getattr(model.device, "type", str(model.device).split(":", 1)[0]) != "cuda":
145+
raise RuntimeError(f"Model is not GPU-only: device={model.device}")
146+
return
147+
148+
non_gpu = {
149+
name: str(device)
150+
for name, device in device_map.items()
151+
if not isinstance(device, int) and str(device).split(":", 1)[0] != "cuda"
152+
}
153+
if non_gpu:
154+
raise RuntimeError(f"Model has non-GPU placements: {non_gpu}")
155+
156+
125157
def _load_runtime(args: argparse.Namespace) -> tuple[Any, Any, Any]:
126158
"""Load torch, the selected HF auto-model, and its tokenizer or processor."""
127159
import torch
@@ -137,27 +169,53 @@ def _load_runtime(args: argparse.Namespace) -> tuple[Any, Any, Any]:
137169

138170
processor = AutoTokenizer.from_pretrained(args.hf_model, trust_remote_code=args.trust_remote_code)
139171
model_cls = AutoModelForCausalLM
140-
model, loading_info = model_cls.from_pretrained(
141-
args.hf_model,
142-
dtype=dtype,
143-
trust_remote_code=args.trust_remote_code,
144-
output_loading_info=True,
145-
)
172+
model_kwargs = {
173+
"dtype": dtype,
174+
"trust_remote_code": args.trust_remote_code,
175+
"output_loading_info": True,
176+
}
177+
if args.device_map:
178+
model_kwargs["device_map"] = args.device_map
179+
model, loading_info = model_cls.from_pretrained(args.hf_model, **model_kwargs)
146180
_validate_loading_info(loading_info)
147-
model = model.to(args.device).eval()
181+
if not args.device_map:
182+
model = model.to(args.device)
183+
model = model.eval()
184+
if args.require_gpu_only:
185+
_validate_gpu_only_placement(model)
186+
LOG.info("Strict HF reload complete (%d model modules)", sum(1 for _ in model.modules()))
148187
return torch, model, processor
149188

150189

190+
def _model_input_device(model: Any) -> Any:
191+
"""Return the device holding the model's input embeddings."""
192+
get_input_embeddings = getattr(model, "get_input_embeddings", None)
193+
if get_input_embeddings is not None:
194+
embeddings = get_input_embeddings()
195+
weight = getattr(embeddings, "weight", None)
196+
if weight is not None and getattr(weight.device, "type", None) != "meta":
197+
return weight.device
198+
return model.device
199+
200+
151201
def main() -> int:
152202
"""Run one bounded greedy generation and print its completion."""
153203
args = _parse_args()
154204
torch, model, processor = _load_runtime(args)
155205
tokenizer = getattr(processor, "tokenizer", processor)
156-
inputs = _prepare_inputs(processor, args).to(model.device)
206+
input_device = _model_input_device(model)
207+
inputs = _prepare_inputs(processor, args).to(input_device)
157208
prompt_length = inputs["input_ids"].shape[1]
158209
pad_token_id = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else tokenizer.eos_token_id
159210

160-
with torch.inference_mode():
211+
with (
212+
torch.inference_mode(),
213+
torch.autocast(
214+
device_type=torch.device(input_device).type,
215+
dtype=getattr(torch, args.dtype),
216+
enabled=args.autocast,
217+
),
218+
):
161219
output = model.generate(
162220
**inputs,
163221
do_sample=False,

‎skills/nemo-mbridge-recipe-recommender/references/recipe-index.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ All recipes live under `src/megatron/bridge/recipes/`. Each function returns a
7474
| `deepseek_v2_pretrain_config` | Pretrain | 1 | 4 | 32 | 128 |
7575
| `deepseek_v3_pretrain_config` | Pretrain | 2 | 16 | 64 | 2048 |
7676
| `deepseek_v3_pretrain_config_32nodes` | Pretrain | 2 | 8 | 32 | 256 |
77+
| `deepseek_v4_flash_pretrain_64gpu_gb200_fp8mx_library_config` | Pretrain | 1 | 4 | 16 | 64 (GB200; VPP4; natural unlimited capacity) |
78+
| `deepseek_v4_flash_sft_openmath_thinking_packed_gb200_config` | SFT | 1 | 4 | 8 | 32 (GB200; offline packed) |
79+
| `deepseek_v4_flash_peft_openmath_thinking_packed_config` | PEFT | 1 | 4 | 8 | 32 (offline packed) |
80+
| `deepseek_v4_flash_peft_openmath_thinking_packed_gb200_config` | PEFT | 1 | 4 | 8 | 32 (GB200; offline packed) |
7781

7882
### GLM-4.5
7983

‎src/megatron/bridge/models/deepseek/deepseek_v4_bridge.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -944,11 +944,10 @@ def maybe_modify_converted_hf_weight(
944944
) -> Dict[str, torch.Tensor]:
945945
"""Recreate DSv4 quantized weight/scale pairs expected by the source shard index.
946946
947+
Legacy indexer scorer names are restored before selecting the export dtype.
947948
When ``task.weight_dtype`` is set, skip requantization and return the weights
948949
unchanged — the generic export path casts the dtype.
949950
"""
950-
if task.weight_dtype is not None:
951-
return converted_weights_dict
952951
native_scorer_key = next(
953952
(
954953
key
@@ -962,6 +961,8 @@ def maybe_modify_converted_hf_weight(
962961
if legacy_key in hf_state_dict:
963962
converted_weights_dict = dict(converted_weights_dict)
964963
converted_weights_dict[legacy_key] = converted_weights_dict.pop(native_scorer_key)
964+
if task.weight_dtype is not None:
965+
return converted_weights_dict
965966
return quantization_utils.requantize_hf_weight_scale_pairs(
966967
converted_weights_dict,
967968
hf_state_dict,

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
# DeepSeek V4
3535
from .deepseek_v4 import (
3636
deepseek_v4_flash_no_mtp_sft_config,
37+
deepseek_v4_flash_peft_openmath_thinking_packed_config,
3738
deepseek_v4_flash_pretrain_config,
3839
deepseek_v4_flash_pretrain_gb200_config,
3940
deepseek_v4_flash_pretrain_muon_config,
@@ -47,9 +48,12 @@
4748
set_deepseek_v4_pipeline_model_parallel_layout,
4849
)
4950
from .gb200.deepseek_v4 import (
51+
deepseek_v4_flash_peft_openmath_thinking_packed_gb200_config,
5052
deepseek_v4_flash_pretrain_64gpu_gb200_bf16_config,
5153
deepseek_v4_flash_pretrain_64gpu_gb200_bf16_muon_config,
5254
deepseek_v4_flash_pretrain_64gpu_gb200_fp8mx_config,
55+
deepseek_v4_flash_pretrain_64gpu_gb200_fp8mx_library_config,
56+
deepseek_v4_flash_sft_openmath_thinking_packed_gb200_config,
5357
)
5458
from .gb300.deepseek_v4 import (
5559
deepseek_v4_pro_pretrain_32gpu_gb300_bf16_config,
@@ -66,6 +70,8 @@
6670
"deepseek_v3_pretrain_config_32nodes",
6771
"set_deepseek_v3_pipeline_model_parallel_layout",
6872
# DeepSeek V4
73+
"deepseek_v4_flash_peft_openmath_thinking_packed_config",
74+
"deepseek_v4_flash_peft_openmath_thinking_packed_gb200_config",
6975
"deepseek_v4_flash_pretrain_config",
7076
"deepseek_v4_flash_pretrain_gb200_config",
7177
"deepseek_v4_flash_pretrain_muon_config",
@@ -80,6 +86,8 @@
8086
"deepseek_v4_flash_pretrain_64gpu_gb200_bf16_config",
8187
"deepseek_v4_flash_pretrain_64gpu_gb200_bf16_muon_config",
8288
"deepseek_v4_flash_pretrain_64gpu_gb200_fp8mx_config",
89+
"deepseek_v4_flash_pretrain_64gpu_gb200_fp8mx_library_config",
90+
"deepseek_v4_flash_sft_openmath_thinking_packed_gb200_config",
8391
"deepseek_v4_pro_pretrain_32gpu_gb300_bf16_config",
8492
"deepseek_v4_pro_pretrain_32gpu_gb300_fp8mx_config",
8593
"set_deepseek_v4_pipeline_model_parallel_layout",

‎src/megatron/bridge/recipes/deepseek/deepseek_v4.py‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
# ruff: noqa: F401
15-
"""Compatibility aliases for legacy recipe names."""
15+
"""Hardware-agnostic DeepSeek V4 recipes and compatibility aliases."""
1616

1717
from __future__ import annotations
1818

1919
from megatron.bridge.models.deepseek.deepseek_v4_bridge import (
2020
set_deepseek_v4_pipeline_model_parallel_layout,
2121
)
22+
from megatron.bridge.peft.lora import LoRA
2223
from megatron.bridge.recipes.deepseek.gb200.deepseek_v4 import (
2324
deepseek_v4_flash_pretrain_64gpu_gb200_bf16_config,
2425
deepseek_v4_flash_pretrain_64gpu_gb200_bf16_muon_config,
@@ -66,6 +67,7 @@
6667

6768
__all__ = [
6869
"deepseek_v4_flash_no_mtp_sft_config",
70+
"deepseek_v4_flash_peft_openmath_thinking_packed_config",
6971
"deepseek_v4_flash_pretrain_config",
7072
"deepseek_v4_flash_pretrain_muon_config",
7173
"deepseek_v4_flash_pretrain_mxfp8_config",
@@ -85,6 +87,34 @@
8587
]
8688

8789

90+
_DEEPSEEK_V4_LORA_TARGET_MODULES = [
91+
"linear_q_down_proj",
92+
"linear_q_up_proj",
93+
"linear_kv_proj",
94+
"linear_proj",
95+
"linear_fc1",
96+
"linear_fc2",
97+
]
98+
99+
100+
def _apply_deepseek_v4_lora(cfg: ConfigContainer) -> None:
101+
"""Apply the DeepSeek V4 LoRA convergence contract to an SFT config."""
102+
cfg.peft = LoRA(
103+
target_modules=list(_DEEPSEEK_V4_LORA_TARGET_MODULES),
104+
dim=32,
105+
alpha=32,
106+
dropout=0.0,
107+
share_expert_adapters=False,
108+
)
109+
cfg.optimizer.lr = 1.0e-4
110+
cfg.optimizer.min_lr = 0.0
111+
# MCore's unified recompute path cannot replay the frozen grouped-expert graph.
112+
cfg.model.recompute_granularity = None
113+
cfg.model.recompute_modules = None
114+
cfg.model.recompute_method = None
115+
cfg.model.recompute_num_layers = None
116+
117+
88118
def deepseek_v4_flash_sft_openmath_thinking_packed_config() -> ConfigContainer:
89119
"""DSv4 Flash SFT on OpenMathInstruct-2 with thinking channel and offline-packed sequences.
90120
@@ -93,6 +123,9 @@ def deepseek_v4_flash_sft_openmath_thinking_packed_config() -> ConfigContainer:
93123
Pre-pack data with ``prepare_gpt_sft_packed_data.py`` before running SFT.
94124
When using CP>1, pass ``model.cp_partition_mode=contiguous`` (required for DSv4 CSA
95125
attention) and ``pad_seq_to_mult=4`` to ensure divisibility by cp_size.
126+
127+
For GB200-optimized training with HybridEP dispatcher and DSA kernel fusion,
128+
use ``deepseek_v4_flash_sft_openmath_thinking_packed_gb200_config`` instead.
96129
"""
97130
cfg = deepseek_v4_flash_sft_config()
98131
# DSv4 hybrid attention requires contiguous CP partition when CP > 1;
@@ -104,3 +137,15 @@ def deepseek_v4_flash_sft_openmath_thinking_packed_config() -> ConfigContainer:
104137
pad_seq_to_mult=2 * cfg.model.context_parallel_size,
105138
)
106139
return cfg
140+
141+
142+
def deepseek_v4_flash_peft_openmath_thinking_packed_config() -> ConfigContainer:
143+
"""DSv4 Flash LoRA on packed OpenMathInstruct-2 thinking data.
144+
145+
The attention targets follow the DeepSeek MLA projection layout. Both shared
146+
and routed MLP projections are adapted; grouped routed experts use one adapter
147+
per local expert to match the verl training layout.
148+
"""
149+
cfg = deepseek_v4_flash_sft_openmath_thinking_packed_config()
150+
_apply_deepseek_v4_lora(cfg)
151+
return cfg

0 commit comments

Comments
 (0)