feat(speculative): add DeepSeek V4 DSpark drafter and V4-Flash training - #2866
Conversation
DSpark draft whose backbone is DeepSeek V4 attention (Q-LoRA, single shared K=V latent, grouped O-LoRA, interleaved partial RoPE with output inverse-RoPE, attention sink), dense and non-causal over [context | noise-block] with the DFlash mask. Mirrors draft_qwen3 control flow; FFN is a dense SwiGLU for now. Signed-off-by: khazic <khazzz1c@gmail.com>
…der and tests Register DeepseekV4ForCausalLM -> DeepseekV4DSparkModel in the DSpark registry, add build_deepseek_v4_draft_config (shrinks depth, disables DSA/hash/MTP, adds DSpark fields, uses the SDPA dense-mask path), and a CPU unit-test mirroring the Qwen3 DSpark tests (forward shapes, three-term loss + backprop with frozen embed/lm_head, Markov head variants, config builder, registry, inv_freq fp32 pin). Signed-off-by: khazic <khazzz1c@gmail.com>
…apture DeepSeek V4 decoder layers emit hc_mult parallel residual copies, so a forward hook on an intermediate target-feature layer captures a 4D [B,S,hc_mult,H] tensor. Collapse it to [B,S,H] via the model's learned hc_head (matching V4's own final collapse), falling back to the stream mean. Non-HC targets (3D) pass through. Signed-off-by: khazic <khazzz1c@gmail.com>
V4's RMSNorm factory builds norms in bf16 while Linear/Embedding layers are fp32, so a freshly built draft had mixed dtypes. FSDP2 fully_shard requires a uniform original parameter dtype (it raises before any later cast), so cast the draft to fp32 at the end of __init__, matching the Qwen3 draft's single-dtype build. The recipe still casts to the bf16 compute dtype afterwards. Signed-off-by: khazic <khazzz1c@gmail.com>
- rotary _apply uses the repo's snapshot/restore fp32 idiom (Fp32Safe rotary) instead of re-deriving the inv_freq closed form; - merge the shared K=V latent into one wkv+kv_norm projection over [ctx|noise] (one GEMM + one RMSNorm instead of two, same FLOPs); - drop the unreachable flex_attention mask branch (the V4 draft is always dense eager-with-sink and a flex BlockMask cannot feed it); - collapse intermediate HC streams with their mean, decoupling the target wrapper from V4's final hc_head (which is trained for the last-layer stream). Signed-off-by: khazic <khazzz1c@gmail.com>
…Spark recipe train_dspark.py: detect deepseek_v4 targets (via PretrainedConfig.get_config_dict, since HF AutoConfig may not register the type) and load the full V4-Flash model as a frozen target through the V4 finetune recipe's EP/FSDP distributed path (create_distributed_setup_from_config + NeMoAutoModelForCausalLM.from_config + load_base_model, FP8 dequant via the state-dict adapter). Gather the EP/FSDP-sharded embed_tokens / lm_head (full_tensor all-gather) to seed the draft, and shard the draft over the world group as before. Qwen3 / Gemma4 branches are unchanged. Add examples/speculative/dspark/deepseek_v4_flash_dspark.yaml aligned to the official V4-Flash-DSpark hyperparams (block_size=5, target_layer_ids=[40,41,42], markov_rank=256, mask_token_id=128799), with an 8xA800 ep_size=8 layout. Signed-off-by: khazic <khazzz1c@gmail.com>
|
@HuiyingLi this PR adds DSpark drafter support for DeepSeek V4 plus a full V4-Flash training recipe, and it needs validation on 8 GPUs (I could only verify the components on 1 to 4 GPUs). Thanks in advance for testing. Here is how to run it. 1. Unit tests (CPU, ~30s) confirm the draft model, config builder, registry, and the Expect 2. Full V4-Flash DSpark training (8x80GB): The yaml loads the full Key assumptions to validate on 8 GPUs (the ones I could not test):
The draft itself runs the dense SDPA mask path (not tilelang/flex). Already verified on fewer GPUs: a single-card real V4-Flash load producing finite hidden states, and 4-GPU FSDP2 draft training with decreasing loss. Happy to fix anything that comes up. |
… and guide Add a fast smoke config (deepseek_v4_flash_smoke.yaml) and a one-shot end-to-end smoke runner (run_deepseek_v4_flash_smoke.sh) that exercise the real recipe path (full V4-Flash target, ep_size=8, tilelang, DeepEP dispatcher), shrunk to reach a training step in minutes, plus a README covering data prep, the smoke run, full training, and the validation status. The smoke asserts a finite, non-increasing three-term loss and lists the 5 EP sanity points a CPU test cannot cover. Signed-off-by: khazic <khazzz1c@gmail.com>
|
@HuiyingLi this PR now ships a one-shot smoke runner and a README, so you can validate the full V4-Flash path on your DeepEP box without piecing commands together. Prereqs: DeepEP (or HybridEP) installed, 8x at least 80 GB GPUs, and a local DeepSeek-V4-Flash checkpoint. The V4 MoE token dispatcher needs DeepEP; without it 1. Smoke first (a few minutes). This exercises the real EP path (full target, ep_size=8, tilelang, DeepEP) shrunk to reach a training step quickly, and asserts a finite, non-increasing loss: TARGET=/path/to/DeepSeek-V4-Flash bash tests/functional_tests/speculative/run_deepseek_v4_flash_smoke.shIt runs the CPU unit tests, builds 64 tiny rows, then an 8-GPU training smoke. If it prints 2. Full training. Regenerate Open-PerfectBlend responses with V4-Flash first (teacher-forced; see torchrun --standalone --nproc_per_node=8 -m nemo_automodel.recipes.llm.train_dspark -c examples/speculative/dspark/deepseek_v4_flash_dspark.yamlPer-step metrics (loss, ce_loss, l1_loss, confidence_loss, lr, mem) land in What is and is not verified. The 9 CPU unit tests pass and I reviewed the draft + recipe code, but I do not have a DeepEP box, so the full distributed target load and cross-shard hidden capture are not yet validated. When you run the smoke, please eyeball these 5 points:
Thanks! |
|
@khazic I ran the smoke on 8x H100 80 GB with DeepEP/HybridEP installed and a complete local DeepSeek-V4-Flash checkpoint. Results
To validate the remainder of the path, I added a local diagnostic option that changes recipe_args:
target_num_hidden_layers: 4
target_layer_ids: [1, 2, 3]All target layers Missing tokenizer chat templateThe V4-Flash tokenizer in this checkpoint has no I resolved this locally by allowing from nemo_automodel.components.datasets.llm.formatting_utils import _resolve_chat_template
self.tokenizer = NeMoAutoTokenizer.from_pretrained(target_path, trust_remote_code=trust_remote_code)
chat_template = recipe_cfg.get("chat_template", None)
if chat_template is not None:
self.tokenizer.chat_template = _resolve_chat_template(str(chat_template))For the smoke data I used a minimal template that marks only assistant content as the generation/loss span: chat_template: |-
{% for message in messages %}
{% if message['role'] == 'assistant' %}{% generation %}assistant:
{{ message['content'] }}{% endgeneration %}
{% else %}{{ message['role'] }}:
{{ message['content'] }}
{% endif %}
{% endfor %}This produced 38 non-padding tokens and 25 assistant loss tokens for the first generated row. Four-layer executionWith the PR's Using the model's Torch fallback via Peak logged memory was 16.06 GiB/rank. So the four-layer EP/hidden-capture/draft-training path works with eager attention; the original full-target load and the TileLang JIT path remain blockers in this environment. These were local diagnostic changes only. |
…d to end
The full 43-layer V4-Flash target dequantizes its FP4 routed experts to bf16
(~500 GiB), so ep_size=8 on one 8x80GB node OOMs in the expert dequant at load,
before the first forward (a resident-weight peak; seq_length / activation
checkpointing cannot help). Ship a config that actually fits plus the recipe
knobs the path needs:
- deepseek_v4_flash_dspark.yaml: default to a 2-node ep_size=16 layout
(~32 GiB/rank experts) with multi-node torchrun instructions, and correct the
prior (wrong) "fits on 8x80GB" memory note.
- recipe_args.chat_template: the V4-Flash tokenizer ships no chat template, so
messages-format data could not be rendered. Apply an inline / file template to
the tokenizer, or fail fast when none is available. The configs carry a
DeepSeek-style default whose {% generation %} block marks the assistant span as
the loss region.
- recipe_args.target_num_hidden_layers: a diagnostic knob that loads only the
first N target layers so the whole EP / hidden-capture / draft-training path
runs on a single node (CI / smoke). A draft trained this way is a pipeline
check, not a usable drafter.
- target_attn_backend defaults to eager (reliable everywhere); tilelang stays an
opt-in speedup.
- smoke config + runner reduced to 4 layers with eager attention so the smoke
passes on one 8x80GB node (the full target would OOM there).
- README rewritten with an honest memory table and the multi-node launch.
- unit tests for the new recipe helpers.
Signed-off-by: khazic <khazzz1c@gmail.com>
|
Thanks for the 8x H100 run, @HuiyingLi. Your findings all map to fixes I just pushed. The short version: the full 43-layer target genuinely does not fit on one 8x80GB node, so full training is now a multi-node config, and the two patches you wrote by hand are now supported recipe options. Why the full-target load OOMs (and why seq_length / activation checkpointing can't help). V4-Flash stores its 256 routed experts in FP4, but this path dequantizes them to bf16 for the grouped-expert GEMM, a 4x expansion. The routed experts alone are ~500 GiB in bf16, so at Full training is now multi-node. Your two local patches are now first-class options:
Attention backend defaults to eager. Since TileLang JIT-failed in your env ( CPU unit tests pass (the original draft/config/registry suite plus new tests for the two recipe helpers). The one thing I still cannot run myself is the full multi-node load and cross-shard hidden capture at |
Signed-off-by: HuiyingLi <willwin.lee@gmail.com>
|
@khazic Experimental setup/status update — this is not a training-success claim. I pushed commit
Experimental setup
Current execution statusThe distributed load/forward/backward path and checkpoint write/ W&B: https://wandb.ai/Nemo-automodel/deepseek-v4-dspark/runs/awf06tzq The initial pilot loss is unstable: approximately I do not consider this setup validated or successful yet. |
…r warmup TrainDSparkRecipe hardcoded torch.optim.AdamW directly on the model's native parameters, so any optimizer._target_ (e.g. TE FusedAdam) plus precision knobs like master_weights/exp_avg_dtype/store_param_remainders were silently ignored -- training ran with bf16 Adam moments instead of the fp32-master setup the config asked for, a known source of instability. Route optimizer construction through the shared build_optimizer()/OptimizerConfig system instead, defaulting to plain AdamW when no _target_ is set so existing Qwen3/Gemma4/V4 configs are unaffected. Also floor warmup_steps at a minimum of 20 optimizer steps. warmup_steps was a pure warmup_ratio * total_optim_steps, which collapses to a handful of steps on short/small-dataset runs (e.g. 4 steps for a 100-step run at the default warmup_ratio=0.04), dropping a freshly-initialized draft to near-peak LR almost immediately -- a reliable trigger for an early loss spike. An explicit warmup_ratio<=0 (the smoke config's intentional "no warmup") still opts out of the floor. Signed-off-by: khazic <khazzz1c@gmail.com>
|
@khazic Follow-up validation on current PR head Integration note on the optimizer fixBefore running, I found one real- target = opt_cfg.get_as_string("_target_", "torch.optim.AdamW")
kwargs.pop("_target_", None)I also added a runtime optimizer-state log. The exact YAML -> Common experiment setup
2 nodes / EP16 resultSlurm job W&B: https://wandb.ai/Nemo-automodel/deepseek-v4-dspark/runs/70iomuua 4 nodes / EP32 resultSlurm job
Minimum train loss was 2.8590 at step 96; mean train loss over steps 91-100 was 2.8932. Final step components were CE 9.2785, TV 1.8548, confidence 0.2095. There was no early loss spike in this setup. W&B: https://wandb.ai/Nemo-automodel/deepseek-v4-dspark/runs/jv7t32yq The final step-100 checkpoint includes model/optimizer/RNG state and a consolidated HF safetensors export at: This validates the updated optimizer path and 4-node distributed execution for the short regenerated-data pilot. The objective still needs evaluation at full data scale before interpreting this as drafter quality/convergence. |
…bility codecov/patch was failing (68.79% vs the 80% target / 70% floor): setup()'s optimizer construction and wandb init call sites were new, uncovered lines, since setup() needs a full distributed environment that CPU unit tests can't exercise. Pull the same logic into two small functions setup() now calls: - _build_dspark_optimizer: thin wrapper around build_optimizer(); needs no distributed environment for a non-pipelined single-part model, so it's testable with a plain CPU nn.Module. - _init_dspark_wandb: the is_main / block-presence / enable gating, testable by patching init_wandb_run / suppress_wandb_log_messages. Also fixes a real bug caught while adding these tests: _resolve_dspark_optimizer_spec was forcing AdamW's betas/weight_decay defaults onto ANY _target_, which would break an explicit target with no betas kwarg (e.g. plain SGD). Those defaults now only apply in the no-_target_ (plain AdamW) case. Signed-off-by: khazic <khazzz1c@gmail.com>
|
@HuiyingLi CI status update after the loss-spike fix (optimizer/warmup, commit 6e40b02) and the codecov fix:
I don't have permission to re-run the failed job ( |
Resolves conflicts from PR NVIDIA-NeMo#2866 (DeepSeek V4 DSpark) landing on main after this branch was cut. The DSpark recipe now dispatches three target families side by side: - target load: a three-way branch (DeepSeek V4 from_config FP8-dequant / MiniMax M3 from_pretrained distributed_setup / Qwen3-Gemma4 CausalLM), keying off _read_target_model_type. - draft build: build_deepseek_v4/minimax_m3/gemma4_draft_config share the one _DraftArgs bundle behind is_deepseek_v4/is_minimax_m3/is_gemma4. - embed/head init: the sharded-DTensor gather now covers both V4 and M3. - target.py: V4's Hyper-Connection stream collapse and the M3 multimodal generate_batch kwargs (filter_forward_kwargs) coexist. - dropped the duplicate _gather_full_weight_module the M3 branch had added (V4's identical one is now upstream) and its now-unused _to_full_tensor import. - test_train_dspark_helpers.py: kept V4's helper tests and appended the _extract_mm_kwargs tests. Signed-off-by: khazic <khazzz1c@gmail.com>
|
Hi, this is a great work. We wonder is there any open sourced checkpoint trained with full Opend-PerfectBlend corpus of this dense dspark draft model? And how about the performance of this model compared to the open sourced dsv4 dspark draft model (moe + mhc) in terms of accept rate or other metrics? |
What
Adds a DSpark speculative-decoding drafter for DeepSeek V4 targets (Qwen3 and Gemma4 are already supported), plus the recipe wiring to train it against a full V4-Flash target, a one-shot smoke runner, and docs.
Components (CPU unit-tested, 9 passed)
draft_deepseek_v4.py:DeepseekV4DSparkModelwith a V4 attention backbone (Q-LoRA, single shared K=V latent, grouped O-LoRA, interleaved partial RoPE with an output inverse-RoPE, attention sink), a plain pre-norm decoder + dense SwiGLU MLP, and an fp32inv_freqpin. Mirrors the Qwen3 DSpark draft's control flow.DeepseekV4ForCausalLM -> DeepseekV4DSparkModel.build_deepseek_v4_draft_config: derives the draft config from a V4 target config (shrink depth, disable DSA / hash routing / MTP, add the DSpark fields, SDPA dense-mask path).target.py:HFDSparkTargetModelcollapses V4's 4D Hyper-Connection streams (intermediate feature layers) to 3D via the stream mean; the final-norm output is already 3D.inv_freqfp32 pin). Re-run after the review cleanups: 9 passed.Additionally validated on an A800 with a layer-pruned (not full) V4-Flash target: GPU/CPU forward / loss / backward finite, 4-GPU FSDP2 draft training (loss decreases, all-reduce), and real FP8 e4m3 + FP4 expert + e8m0 block-scale weights dequantizing to bf16 with finite hidden states.
Full V4-Flash training recipe (needs DeepEP + 8-GPU validation)
train_dspark.py: adeepseek_v4target branch that loads the full V4-Flash model as a frozen target through the V4 finetune recipe's EP/FSDP distributed path (FP8 dequant via the state-dict adapter), seeds the draft from the gatheredembed_tokens/lm_head, and shards the draft over the world group. The Qwen3 / Gemma4 branches are unchanged.examples/speculative/dspark/deepseek_v4_flash_dspark.yaml: aligned to the official V4-Flash-DSpark hyperparameters (block_size=5, target_layer_ids=[40,41,42], markov_rank=256, mask_token_id=128799), now a multi-node (ep_size=16+) layout; see the file header for whyep_size=8on one 8x80GB node OOMs at load.The MoE token dispatcher (
hybridep/deepep) requires DeepEP installed; without itsetup()raisesImportError: HybridEP is not installed. So this path needs a DeepEP-equipped box.Smoke test + docs (added)
examples/speculative/dspark/deepseek_v4_flash_smoke.yaml: the same real EP path (full target, ep_size=8, DeepEP) shrunk (short seq, few anchors, 1 epoch,target_num_hidden_layersreduced) to reach a training step in minutes on one node.tests/functional_tests/speculative/run_deepseek_v4_flash_smoke.sh: one shot that runs the CPU unit tests, builds tiny smoke data, runs an 8-GPU training smoke, asserts a finite and non-increasing loss, and prints the EP sanity points.examples/speculative/dspark/README_deepseek_v4_flash.md: data prep, the smoke run, full training, and validation status.Validation status
Verified: the 9 CPU unit tests (re-run, 9 passed), the draft / recipe code correctness (static review), and, thanks to @HuiyingLi, the full distributed path end to end: an 8x H100 smoke (reduced target layers) and a 4-node / 32-GPU pilot (full 43-layer target,
ep_size=32, HybridEP) exercising the distributed target load, hidden-state capture, draft training, and checkpoint save/LATESTrestore. See the PR comments below for the detailed findings from those runs (missing chat template on the V4-Flash tokenizer, the full-target OOM atep_size=8root-caused to FP4->bf16 expert dequant and fixed by moving to a multi-node layout, and a TileLang JIT failure withtarget_attn_backend: eagernow the default fallback).Loss-spike investigation and fix: the 4-node pilot showed unstable early loss (
8.94 -> 14.81 -> 100.26 -> 30.55over the first 4 steps, see comments below). Root-caused to two pre-existing issues in the sharedTrainDSparkRecipescaffolding (also used by the Qwen3 / Gemma4 branches, not specific to this PR's V4 code):torch.optim.AdamWdirectly on the model's native (bf16) parameters, so anyoptimizer._target_/master_weights/master_weight_dtype/exp_avg_dtype/exp_avg_sq_dtype/store_param_remaindersoverride was silently ignored; training ran with bf16 Adam moments instead of the fp32-master setup the config asked for.warmup_steps = warmup_ratio * total_optim_stepscollapses to a handful of steps on short / small-dataset runs (4 steps for a 100-step run at the defaultwarmup_ratio=0.04), dropping the freshly-initialized draft to near-peak LR almost immediately.Fixed: optimizer construction now routes through the shared
build_optimizer()/OptimizerConfigsystem (defaults to plain AdamW when no_target_is set, so existing Qwen3 / Gemma4 / V4 configs are unaffected), andwarmup_stepsis floored at 20 optimizer steps unlesswarmup_ratio<=0(an explicit opt-out, e.g. the smoke config keeps its intentional no-warmup behavior).Re-validated by @HuiyingLi on the same 4-node / 32x H100 layout, with TE FusedAdam +
master_weights: true+ fp32 master/moments,lr=1e-5, on the 10,240-row regenerated dataset (5 epochs / 100 optimizer steps):No spike;
train/lrholds steady at1e-5andtrain/confidence_lossconverges quickly.train/tv_lossstays in the 1.85-2.0 range across this short (100-step, 10k-row) run; likely just insufficient training volume for the block_size=5 / 129280-vocab objective at this scale rather than a correctness issue, worth re-checking once training is scaled to the full corpus / more epochs.Signed-off-by: khazic khazzz1c@gmail.com