Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1d32953
feat(deepseek_v4): add DeepSeek V4 Flash model, state dict adapter, t…
khazic Apr 24, 2026
2ef75e9
feat(checkpoint): recognize F8_E8M0 / F8_E5M2 dtypes in HF storage ba…
khazic Apr 24, 2026
738174b
feat(pipelining): V4-aware pipeline_forward with hc_mult axis support
khazic Apr 24, 2026
c114d9a
fix(datasets): drop trust_remote_code for datasets>=4.0 in HellaSwag …
khazic Apr 24, 2026
1761026
feat(v4): wire DeepseekV4HashGate for first num_hash_layers layers (P…
khazic Apr 24, 2026
ee880f2
fix(v4): register HashGate.tid2eid as a buffer, not a Parameter
khazic Apr 24, 2026
42e47fa
fix(v4): apply inverse RoPE to attention output before wo_a
khazic Apr 24, 2026
72947af
fix(v4): drop YaRN mscale correction from attention softmax_scale
khazic Apr 24, 2026
0b32f96
feat(moe): clamped SwiGLU path for DeepSeek V4 routed experts
khazic Apr 24, 2026
fe2b2a8
feat(moe): add sqrtsoftplus branch to shared Gate.forward (DeepSeek V4)
khazic Apr 24, 2026
bb2fcad
feat(v4): wire attn_sink into DeepseekV4Attention forward
khazic Apr 24, 2026
3bc62af
feat(v4): pure-torch HC Sinkhorn mixing for Block forward
khazic Apr 24, 2026
b9ecb95
feat(v4): swap KAutomodel HC to HuggingFace transformers PR 45616 cla…
HuiyingLi Apr 25, 2026
cfebdce
feat(v4): swap attention to HF PR 45616 + add released-checkpoint com…
HuiyingLi Apr 25, 2026
e42168e
fix(dsv4): five layer-parity bugs found by per-tensor dump bisection
HuiyingLi Apr 25, 2026
126585e
chore(dsv4): remove DSV4_DEBUG_DUMP per-tensor instrumentation
HuiyingLi Apr 25, 2026
249d388
chore(dsv4): drop deepseek_v4_flash_validate.yaml from PR
HuiyingLi Apr 25, 2026
3fe3686
chore(datasets): drop trust_remote_code kwarg from HellaSwag
HuiyingLi Apr 25, 2026
e2de4de
chore(pipelining): drop dead V4-specific PP forward helpers
HuiyingLi Apr 25, 2026
02f88ff
chore(dsv4): rewrite hellaswag recipe to full-scale 16-node config
HuiyingLi Apr 25, 2026
837db44
chore(dsv4): point hellaswag recipe at HF deepseek-ai/DeepSeek-V4-Flash
HuiyingLi Apr 25, 2026
7a040e9
test: cover non-deepseek_v4 branch changes
HuiyingLi Apr 25, 2026
ab2d7a0
chore: fix ruff lint violations (CI lint failure on 7a040e93)
HuiyingLi Apr 25, 2026
11d0b1a
chore: apply ``ruff format`` to satisfy CI lint check on ab2d7a08
HuiyingLi Apr 25, 2026
9c5c3fa
fix(test): rewrite test_dsv4_layers.py against the current API
HuiyingLi Apr 27, 2026
6e33739
fix(test): align DSV4 tests with the post-port API + add doc card
HuiyingLi Apr 27, 2026
08c0d98
Merge remote-tracking branch 'origin/main' into feat/deepseek-v4-flash
HuiyingLi Apr 27, 2026
ec17113
docs: drop redundant deepseek-v4.md (dsv4-flash.md from main covers it)
HuiyingLi Apr 27, 2026
e8c8e06
fix(test): skip DSV4 forward smoke tests when CUDA is unavailable
HuiyingLi Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions examples/llm_finetune/deepseek_v4/deepseek_v4_flash_hellaswag.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# To run this recipe with 16 H100 nodes (128 GPUs)
# torchrun --nproc-per-node 8 examples/llm_finetune/deepseek_v4/deepseek_v4_flash_hellaswag.yaml --nproc-per-node 8


recipe: TrainFinetuneRecipeForNextTokenPrediction

seed: 1234

step_scheduler:
global_batch_size: 256
local_batch_size: 8
ckpt_every_steps: 500
val_every_steps: 500
num_epochs: 1
max_steps: 100

distributed:
strategy: fsdp2
tp_size: 1
cp_size: 1
pp_size: 4
ep_size: 32

sequence_parallel: false
activation_checkpointing: false

pipeline:
pp_schedule: interleaved1f1b
pp_microbatch_size: 1
layers_per_stage: 2
round_virtual_stages_to_pp_multiple: down
scale_grads_in_schedule: false
patch_inner_model: false
patch_causal_lm_model: false

moe:
reshard_after_forward: false
wrap_outer_model: false

dist_env:
backend: nccl
timeout_minutes: 30

model:
_target_: nemo_automodel.NeMoAutoModelForCausalLM.from_config
config:
_target_: nemo_automodel.components.models.deepseek_v4.config.DeepseekV4Config.from_pretrained
pretrained_model_name_or_path: deepseek-ai/DeepSeek-V4-Flash
name_or_path: deepseek-ai/DeepSeek-V4-Flash
num_nextn_predict_layers: 0
trust_remote_code: false
load_base_model: true
backend:
_target_: nemo_automodel.components.models.common.BackendConfig
attn: sdpa
linear: torch
rms_norm: torch_fp32
rope_fusion: false
dispatcher: torch
experts: torch_mm
enable_hf_state_dict_adapter: true
enable_fsdp_optimizations: true

checkpoint:
enabled: false
dequantize_base_checkpoint: true

loss_fn:
_target_: nemo_automodel.components.loss.masked_ce.MaskedCrossEntropy

dataset:
_target_: nemo_automodel.components.datasets.llm.hellaswag.HellaSwag
path_or_dataset: rowan/hellaswag
split: train
tokenizer:
_target_: transformers.AutoTokenizer.from_pretrained
pretrained_model_name_or_path: deepseek-ai/DeepSeek-V4-Flash

packed_sequence:
packed_sequence_size: 0

dataloader:
_target_: torchdata.stateful_dataloader.StatefulDataLoader
collate_fn:
_target_: nemo_automodel.components.datasets.utils.default_collater
pad_seq_len_divisible: 64
shuffle: true

validation_dataset:
_target_: nemo_automodel.components.datasets.llm.hellaswag.HellaSwag
path_or_dataset: rowan/hellaswag
split: validation
tokenizer:
_target_: transformers.AutoTokenizer.from_pretrained
pretrained_model_name_or_path: deepseek-ai/DeepSeek-V4-Flash

validation_dataloader:
_target_: torchdata.stateful_dataloader.StatefulDataLoader
collate_fn:
_target_: nemo_automodel.components.datasets.utils.default_collater
pad_seq_len_divisible: 64
shuffle: false
drop_last: true

optimizer:
_target_: torch.optim.AdamW
betas:
- 0.9
- 0.95
eps: 1e-8
lr: 1e-5
weight_decay: 0.1
5 changes: 5 additions & 0 deletions nemo_automodel/_transformers/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
"DeepseekV32ForCausalLM",
("nemo_automodel.components.models.deepseek_v32.model", "DeepseekV32ForCausalLM"),
),
(
"DeepseekV4ForCausalLM",
("nemo_automodel.components.models.deepseek_v4.model", "DeepseekV4ForCausalLM"),
),
(
"Glm4MoeForCausalLM",
("nemo_automodel.components.models.glm4_moe.model", "Glm4MoeForCausalLM"),
Expand Down Expand Up @@ -162,6 +166,7 @@
# AutoConfig.from_pretrained can resolve them without trust_remote_code.
_CUSTOM_CONFIG_REGISTRATIONS: Dict[str, Tuple[str, str]] = {
"baichuan": ("nemo_automodel.components.models.baichuan.configuration", "BaichuanConfig"),
"deepseek_v4": ("nemo_automodel.components.models.deepseek_v4.config", "DeepseekV4Config"),
"kimi_k25": ("nemo_automodel.components.models.kimi_k25_vl.model", "KimiK25VLConfig"),
"kimi_vl": ("nemo_automodel.components.models.kimivl.model", "KimiVLConfig"),
"llava_onevision": ("nemo_automodel.components.models.llava_onevision.model", "LlavaOneVisionConfig"),
Expand Down
2 changes: 2 additions & 0 deletions nemo_automodel/components/checkpoint/_backports/hf_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
"I64": torch.int64,
"BF16": torch.bfloat16,
"F8_E4M3": torch.float8_e4m3fn,
"F8_E5M2": torch.float8_e5m2,
"F8_E8M0": torch.float8_e8m0fnu,
}

HF_DCP_VERSION: float = 1.0
Expand Down
10 changes: 9 additions & 1 deletion nemo_automodel/components/checkpoint/checkpointing.py
Original file line number Diff line number Diff line change
Expand Up @@ -986,7 +986,15 @@ def _get_storage_reader(
Configured storage reader or None for other formats.
"""
if self.config.model_save_format == SerializationFormat.SAFETENSORS or is_init_step:
if key_mapping is None:
# The upstream HuggingFaceStorageReader delegates dtype decoding to
# safetensors.torch._TYPES, which does not yet recognize the FP8
# scale dtypes emitted by some quantized HF checkpoints (e.g.
# DeepSeek V4's F8_E8M0 scales → KeyError('F8_E8M0') inside
# read_metadata → DCP ends up with metadata=None on every rank).
# The in-tree backport's DTYPE_MAP was extended for F8_E8M0/F8_E5M2,
# so prefer it for base-model HF loads. Mid-training DCP loads may
# still use the faster upstream reader.
if key_mapping is None and not is_init_step:
try:
from torch.distributed.checkpoint.hf_storage import (
HuggingFaceStorageReader as _UpstreamHFReader,
Expand Down
4 changes: 1 addition & 3 deletions nemo_automodel/components/datasets/llm/hellaswag.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ def __init__(
tokenizer,
split="train",
num_samples_limit=None,
trust_remote_code=True,
pad_to_max_length=True,
):
"""Initialize the HellaSwag dataset wrapper.
Expand All @@ -44,7 +43,6 @@ def __init__(
tokenizer (PreTrainedTokenizer): The tokenizer used to process text.
split (str, optional): Dataset split to use (e.g., 'train', 'validation'). Defaults to 'train'.
num_samples_limit (int, optional): Maximum number of samples to load. Defaults to None.
trust_remote_code (bool, optional): Whether to trust remote code. Defaults to True.
pad_to_max_length (bool, optional): Whether to pad sequences to max length in the dataset.
If False, sequences will have variable lengths and padding will be handled by the collate function.
Defaults to True.
Expand All @@ -54,7 +52,7 @@ def __init__(
"""
if isinstance(num_samples_limit, int):
split = f"{split}[:{num_samples_limit}]"
raw_datasets = load_dataset(path_or_dataset, split=split, trust_remote_code=trust_remote_code)
raw_datasets = load_dataset(path_or_dataset, split=split)
processor = SFTSingleTurnPreprocessor(tokenizer)
processor.pad_to_max_length = pad_to_max_length
self.dataset = processor.process(raw_datasets, self)
Expand Down
43 changes: 40 additions & 3 deletions nemo_automodel/components/distributed/pipelining/functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,28 +284,51 @@ def _precompute_stage_shapes(
"""
hidden_size, vocab_size = _get_hidden_and_vocab_size(model_config)

# DeepSeek V4 preserves an extra hc_mult axis between blocks, so inter-stage
# hidden state is [mb, seq, hc_mult, dim] until the last (norm) stage folds
# it back to [mb, seq, dim].
is_v4 = (
getattr(model_config, "model_type", None) == "deepseek_v4"
or getattr(getattr(model_config, "text_config", None), "model_type", None) == "deepseek_v4"
)
hc_mult = int(getattr(model_config, "hc_mult", 1) or 1) if is_v4 else 1

for stage in stages:
# Infer the computation dtype from the stage's parameters
try:
model_dtype = next(stage.submod.parameters()).dtype
except StopIteration:
model_dtype = torch.bfloat16

inner_submod = getattr(stage.submod, "model", stage.submod)
stage_has_norm = getattr(inner_submod, "norm", None) is not None

# --- inputs_meta ---
if stage.is_first:
# First stage receives input_ids: [mb, seq_len] int64
stage.inputs_meta = (torch.empty(microbatch_size, seq_len, device="meta", dtype=torch.long),)
else:
# Non-first stages receive hidden_states: [mb, seq_len, hidden_size]
stage.inputs_meta = (torch.empty(microbatch_size, seq_len, hidden_size, device="meta", dtype=model_dtype),)
if hc_mult > 1:
stage.inputs_meta = (
torch.empty(microbatch_size, seq_len, hc_mult, hidden_size, device="meta", dtype=model_dtype),
)
else:
stage.inputs_meta = (
torch.empty(microbatch_size, seq_len, hidden_size, device="meta", dtype=model_dtype),
)

# --- outputs_meta ---
has_lm_head = hasattr(stage.submod, "lm_head") and stage.submod.lm_head is not None
if has_lm_head:
# Last stage with lm_head produces logits: [mb, seq_len, vocab_size]
outputs_meta = (torch.empty(microbatch_size, seq_len, vocab_size, device="meta", dtype=model_dtype),)
elif hc_mult > 1 and not stage_has_norm:
# V4 mid-pipeline: tensor still carries the hc_mult axis.
outputs_meta = (
torch.empty(microbatch_size, seq_len, hc_mult, hidden_size, device="meta", dtype=model_dtype),
)
else:
# Intermediate stages produce hidden_states: [mb, seq_len, hidden_size]
# Standard intermediate stage (or V4 final-norm stage without lm_head).
outputs_meta = (torch.empty(microbatch_size, seq_len, hidden_size, device="meta", dtype=model_dtype),)
stage._configure_outputs_meta(outputs_meta)

Expand Down Expand Up @@ -452,6 +475,12 @@ def split_model_into_stages(
else:
lm_head_fqn = "lm_head"

# DeepSeek V4: model carries an extra compressor-rotary module on every stage
# and an HC head on the last stage; both must survive PP module pruning.
is_v4_keep = getattr(getattr(model, "config", None), "model_type", None) == "deepseek_v4"
has_rotary_emb_compress = is_v4_keep and hasattr(text_model, "rotary_emb_compress")
has_hc_head = is_v4_keep and hasattr(text_model, "hc_head")

# Auto-generate module split if not provided
if module_names_per_stage is None:
module_names_per_stage = generate_hf_model_fqn_per_model_part(
Expand All @@ -466,6 +495,14 @@ def split_model_into_stages(
lm_head_fqn=lm_head_fqn,
)

# V4 post-processing: keep the compressor rotary on every stage and the
# HC head on the last stage so the V4 PP forward can run end-to-end.
if has_rotary_emb_compress:
for stage_modules in module_names_per_stage:
stage_modules.append(f"{layers_prefix}rotary_emb_compress")
if has_hc_head:
module_names_per_stage[-1].append(f"{layers_prefix}hc_head")

def _build_stage_from_modules(
stage_idx: int, module_names: list[str], num_stages: int
) -> tuple[PipelineStage, nn.Module]:
Expand Down
13 changes: 13 additions & 0 deletions nemo_automodel/components/models/deepseek_v4/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
Loading
Loading