Skip to content

Add validated 60k DeepSeek V4 Flash LoRA training and serving - #84

Merged
pawalt merged 112 commits into
mainfrom
devin/1780686358-deepseek-v4-flash-sft
Jul 19, 2026
Merged

pawalt merged 112 commits into
mainfrom
devin/1780686358-deepseek-v4-flash-sft

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds one focused DeepSeek-V4-Flash workflow:

  • rank-64 LoRA SFT at 60,000 tokens with NeMo AutoModel on 16 x 8 H200 GPUs;
  • five real forward/backward/optimizer steps and a pipeline-sharded checkpoint;
  • strict conversion to a standard PEFT adapter;
  • adapter serving with vLLM 0.25.1 on 4 H200 GPUs using PP=4;
  • a base-versus-adapter GSM8K sanity slice and semantic retrieval at exactly 60,000 input tokens.

The training module now exposes only run_id for training, finalization, and validation. Topology, sequence length, LoRA shape, optimizer steps, checkpoint behavior, EFA eligibility, and memory limits are fixed to the validated recipe. Disposable smoke tests, transport probes, experimental backends, and inactive NVSHMEM controls were removed.

The failed Megatron/ms-swift experiment and its large DSv4 boundary rewrite remain excluded. None of the retained patches changes model forward functions, attention math, or autograd.

Validated training result

Dimension Validated value
Hardware 16 nodes x 8 H200 (128 GPUs)
TP / DP / PP / CP / EP 1 / 2 / 4 / 16 / 32
Sequence length 60,000
Global / local batch 8 / 4
Attention / MoE dispatch TileLang / UCCL-EP
LoRA rank 64, alpha 64
Targets wq_a, wq_b, wkv in all 43 layers
Optimizer steps 5
Host memory 128 MiB scheduler request, 256 GiB cgroup limit

Evidence:

  • Modal training app ap-2wSNiwan5bsesrgftETpAb
  • Run ID: dsv4-flash-h200-16n-cp16-60k-lora64-5step-20260716-203704
  • Every step processed 479,896 supervised label tokens globally, or 59,987 per sample.
  • Loss remained finite from 0.0010 to 0.0008; gradient norm remained finite and nonzero from 0.0017 to 0.0014.
  • Step times were approximately 413, 28, 23, 22, and 22 seconds. The first includes compilation and distributed initialization.
  • All four PP stages wrote their step-4 adapter shards.

The request was EFA-enabled and landed on Mellanox/RoCE capacity. The image contains runtime-selected UCCL extensions for both EFA and Mellanox.

Checkpoint result

The finalizer merges the PP shards, rejects missing, duplicate, unexpected, wrong-shaped, non-finite, or unchanged LoRA tensors, maps projection names to official Transformers names, and performs a PEFT load against a meta-device base model.

Validated artifact:

  • Path: /dsv4-flash-h200-16n-cp16-60k-lora64-5step-20260716-203704/epoch_0_step_4/model/adapter_model.safetensors
  • SHA-256: 93839c8de196c581bf76f4305906eeaa5d9e22bf7ac5d5fe073ddc3970bf6a4f
  • 258 tensors across all 43 transformer layers.
  • 129 logical LoRA modules, with every LoRA-B tensor nonzero.
  • Zero missing adapter keys and zero unexpected keys through PEFT.

The recipe intentionally omits AdamW state. This is a serving/export adapter, not an exact optimizer-state resume checkpoint.

End-to-end serving and eval

The serving path uses vLLM 0.25.1 and the native FP4/FP8 checkpoint.

  • Modal eval app ap-zfkYYZIHgVGwbYp8izzdPP
  • The exact adapter SHA above passed preflight and mapped 129 logical modules: 43 each for q_a_proj, kv_proj, and wq_b.
  • vLLM loaded all 46 base shards with native FP8/DeepGEMM, FP4/Marlin, FP8 MLA cache, and fused MoE LoRA.
  • /v1/models returned both deepseek-ai/DeepSeek-V4-Flash and deepseek-v4-flash-60k-lora.
  • On deterministic GSM8K indices 0, 50, ..., 550, base and adapter each scored 11/12 and their numeric predictions agreed on all 12 examples.
  • Both model IDs retrieved 7391846250 from near the beginning of a semantic prompt containing exactly 60,000 input tokens. Each request reported prompt_tokens: 60000 and completion_tokens: 32.

This synthetic five-step adapter is not expected to improve quality. The GSM8K result is a regression sanity check that the adapter loads and executes without obvious output collapse.

Serving uses PP=4 rather than TP. DeepSeek-V4 PP support shipped in vLLM #41694, while vLLM #48697 still tracks incorrect native FP4 MoE TP output on NVIDIA GPUs.

vLLM 0.25.1 and current main do not advertise DeepseekV4ForCausalLM as LoRA-capable or provide its packed Q/KV adapter mapping. The retained vLLM patch adds only that interface registration and PEFT-to-vLLM name mapping. A clean official config.json is downloaded outside the shared training cache so stale metadata cannot select an unquantized loader.

Retained patches

Patch Purpose
automodel_checkpoint_dequant.patch Pass FP4 checkpoint dequantization through the pinned AutoModel recipe API.
automodel_pp_peft_checkpoint.patch Write one PEFT shard per PP stage and allow optimizer-state omission.
automodel_composite_backend.patch Handle the cpu:gloo,cuda:nccl control-plane backend.
automodel_uccl_teardown.patch Free UCCL before distributed/CUDA teardown.
uccl_ipv6_oob.patch Add IPv6 OOB support for Modal's inter-node interface.
vllm_deepseek_v4_lora.patch Register DSv4 LoRA and map PEFT attention targets to fused vLLM modules.

Verification

  • Real five-step, 128-H200 training run at 60,000 tokens.
  • Real four-H200 base-versus-adapter eval and semantic 60,000-token retrieval.
  • The cleaned _recipe_yaml output is byte-for-byte identical to the successful run's exact invocation.
  • uv run --frozen ruff check deepseek-v4-flash-sft/automodel_modal_train.py
  • uv run --frozen ruff format --check deepseek-v4-flash-sft/automodel_modal_train.py
  • uv run --frozen pyright deepseek-v4-flash-sft/automodel_modal_train.py
  • uv run --frozen python -m py_compile deepseek-v4-flash-sft/automodel_modal_train.py
  • Modal CLI parsing checked for train, finalize, and serving validation; each exposes only required --run-id.
  • No secrets or local credentials are committed.

The cleanup head was not subjected to a second 128-GPU training run. Its generated training YAML is byte-identical to the successful configuration, and the orchestration changes remove inactive options rather than changing the model recipe.

Limits

  • Training uses synthetic chat data. This proves the systems path, not model quality or convergence.
  • The 12-example GSM8K slice is not a model-quality benchmark.
  • Serving was validated for correctness at 60k, not benchmarked for throughput or latency.
  • The pinned upstream commits and local patches should be replaced as their fixes ship upstream.

Outside contributors

Link to Devin session: https://modal.devinenterprise.com/sessions/27b14e1d226e47f0b14a07b0c108a7ff

Requested by: @pawalt


Open in Devin Review

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: peyton@modal.com <pawalt@hey.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: peyton@modal.com <pawalt@hey.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: peyton@modal.com <pawalt@hey.com>
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration Bot and others added 8 commits June 5, 2026 20:03
…ADME ordering

- transformers 4.57.4 → 5.10.2: first version with deepseek_v4 in
  CONFIG_MAPPING (model config.json has no auto_map, so trust_remote_code
  alone is insufficient)
- huggingface_hub 0.36.0 → 1.18.0: required by transformers 5.10.2
- README: move deepseek entry to alphabetical position

Co-Authored-By: peyton@modal.com <pawalt@hey.com>
…nflict

mcore-bridge 1.4.2 caps transformers<5.10.0 but the code is compatible with
5.10.2. Install it separately with --no-deps to bypass the version constraint.

Co-Authored-By: peyton@modal.com <pawalt@hey.com>
Keep the mcore/peft-compatible transformers version and register a minimal
DeepseekV4Config via sitecustomize so AutoConfig can parse DeepSeek-V4-Flash.

Co-Authored-By: peyton@modal.com <pawalt@hey.com>
Co-Authored-By: peyton@modal.com <pawalt@hey.com>
Co-Authored-By: peyton@modal.com <pawalt@hey.com>
Co-Authored-By: peyton@modal.com <pawalt@hey.com>
Co-Authored-By: peyton@modal.com <pawalt@hey.com>
Co-Authored-By: peyton@modal.com <pawalt@hey.com>
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration Bot and others added 3 commits June 5, 2026 20:44
Co-Authored-By: peyton@modal.com <pawalt@hey.com>
Co-Authored-By: peyton@modal.com <pawalt@hey.com>
Co-Authored-By: peyton@modal.com <pawalt@hey.com>
deepseek_v4 is registered natively in transformers 5.8.0+, so the two
registration patches (transformers_deepseek_v4_config /
transformers_deepseek_v4_meta_model) are no longer needed. Bump both
download_image and msswift_image to transformers==5.10.2 (within all
ms-swift/mcore-bridge/vLLM constraints) and delete the patches plus the
TRANSFORMERS_DSV4_PATCHES tuple. download_image no longer applies any
image patches. Patch count 9 -> 7.

Co-Authored-By: peyton@modal.com <pawalt@hey.com>
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration Bot and others added 10 commits June 12, 2026 01:51
deploy_and_eval_merged and eval_summarization had ~120 lines of
identical vLLM launch/health-wait/_chat/teardown boilerplate. Extract a
module-level _vllm_server(model_dir, max_model_len, gpu_memory_utilization,
label) context manager that owns the vLLM model view, startup wait,
error-log dumping, and teardown, and yields a chat(prompt, max_tokens,
timeout) callable. The two call sites now differ only in
gpu_memory_utilization (0.9 vs 0.92) and per-request timeout (120s vs
300s).

Co-Authored-By: peyton@modal.com <pawalt@hey.com>
- Remove VLLM_USE_V1=1 from vllm_image env; the knob does not exist in
  vLLM 0.22.1 (V1 is the only engine), so it was a no-op.
- Drop the torch==2.9.1 pin from download_image. Its functions only do
  snapshot_download / datasets / fast-tokenizer token counting, none of
  which need torch, so leaving it unpinned avoids a divergent torch.
- Strip the ablation scaffolding from deepseek_patches.py: PatchSpec now
  keeps only name/command/why/verify_command, and apply_image_patches no
  longer honors DSV4_DISABLED_PATCHES or emits Applying/Skipping echo
  layers. The registry documents the recipe; it is not an ablation
  harness.

Co-Authored-By: peyton@modal.com <pawalt@hey.com>
Move the CP-vs-memory table, the 'CP absorbs long-context memory'
rationale, the MLA module-name warning, and the known-limits note into a
new 'Long-context (60k) SFT' README section. Update the two code
comments that referenced PLAN_60K_WITHOUT_MEMORY_PATCHES.md and delete
the standalone planning doc.

Co-Authored-By: peyton@modal.com <pawalt@hey.com>
All six cleanup tasks are done; the planning doc is no longer needed.

Co-Authored-By: peyton@modal.com <pawalt@hey.com>
A parallel session pushed a smaller version of the planning-doc folding.
Keep the fuller Long-context (60k) SFT README section and the post-Task
1-4 patch registry; both versions delete CLEANUP_PLAN.md and
PLAN_60K_WITHOUT_MEMORY_PATCHES.md.

Co-Authored-By: peyton@modal.com <pawalt@hey.com>
transformers 5.10.2 requires huggingface_hub>=1.5.0,<2.0, so the
0.36.0 pin made both images unbuildable; bump to 1.19.0 (verified
compatible with datasets==3.1.0 and transformers==5.10.2).

The smoke test called tokenizer.apply_chat_template on the base model,
but DeepSeek-V4-Flash ships a plain PreTrainedTokenizerFast with no chat
template. transformers removed the implicit ChatML default in 4.44, so
that line raised on both 4.57.4 and 5.10.2 (i.e. it was a pre-existing
failure, not caused by the upgrade). Validate raw tokenization instead;
the training template comes from ms-swift and the serving template is
exercised in the eval path.

Co-Authored-By: peyton@modal.com <pawalt@hey.com>
The base modelscope image ships an older peft that does an unconditional
`from transformers import HybridCache`, which transformers 5.x removed,
breaking the megatron CLI import chain. peft 0.19.1 loads cleanly under
transformers 5.10.2 and satisfies ms-swift's peft<0.20 constraint.

Co-Authored-By: peyton@modal.com <pawalt@hey.com>
The base DeepSeek-V4-Flash tokenizer ships no chat template, so the
smoke test validates raw tokenization; update the README to match.

Co-Authored-By: peyton@modal.com <pawalt@hey.com>
peft 0.19.x raised its torchao floor to 0.16.0, which made LoRA dispatch
hard-fail against the base image's torchao 0.9.0 during training. peft
0.18.0 loads under transformers 5.10.2 (no top-level HybridCache import)
and keeps the older 0.4.0 torchao floor, so it works with the image's
existing torchao without forcing a torch/torchao bump. Still <0.20 per
ms-swift's constraint.

Co-Authored-By: peyton@modal.com <pawalt@hey.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

devin-ai-integration Bot and others added 4 commits June 12, 2026 04:13
The transformers 5.10.2 upgrade re-saves the merged checkpoint with the
new nested rope_parameters schema ({main, compress}); vLLM v0.22.1 hashes
the rope config in get_rope and raises 'unhashable type: dict'. Normalize
it back to the flat rope_scaling the model originally shipped in the
vLLM-compat config shim.

Also relaunch the vLLM server subprocess on the transient CUDA error 802
('system not yet initialized') seen on cold-started B200/NVSwitch nodes.

Co-Authored-By: peyton@modal.com <pawalt@hey.com>
The earlier claim that CP=4 'comfortably absorbs' the 60k activation
memory after dropping the detach-based memory patches is not borne out by
measured runs. Correct the README, the CP_SIZE comment, and the
long_context_loop docstring to state the open status:

- CP=4 on 4x8 B200 OOMs on the first forward step (~17.7 GiB/GPU short).
- CP=8 on 8x8 B200 closes most of the gap (~0.5 GiB/GPU short) but still
  OOMs (per-rank cost dominated by EP-sharded frozen base weights) and
  exposes a RoPE-CP correctness bug in megatron_rope_cp_shape_fix
  (cyclic rotary-cache repeat at CP>4 corrupts positions).
- Independent of the transformers upgrade: A/B at 4.57.4 and 5.10.2 OOMs
  identically; this is a pre-existing property of the patches-removed
  design. The validated path remains the 4k train_model/export recipe.

Full data and the open decision are recorded in PR #84.

Co-Authored-By: peyton@modal.com <pawalt@hey.com>
@pawalt pawalt changed the title Add DeepSeek V4 Flash SFT example Add validated 60k DeepSeek V4 Flash LoRA training and serving Jul 17, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

@pawalt

pawalt commented Jul 18, 2026

Copy link
Copy Markdown
Member

Validated the new 8-node default end to end on 8x8 H200s (64 GPUs).

  • Training topology: TP=1, DP=1, PP=4, CP=16, EP=16; EFA-enabled request landed on Mellanox/RoCE and selected the matching UCCL provider.
  • Sequence length: 60,000; global/local batch: 8/4; gradient accumulation: 2.
  • Completed all 5 optimizer steps. Final step: loss 0.0008, grad norm 0.0014, trainer memory 25.35 GiB, throughput 15,767.93 tokens/s.
  • Finalizer found all 4 PP shards, 258 adapter tensors across all 43 layers, 129 updated LoRA-B tensors, and zero missing/unexpected PEFT keys. Adapter SHA-256: 756747a4f470ca3d783353a12b58a74ebcc02574f9aea209b1d04bac422a6539.
  • vLLM 0.25.1 loaded that exact adapter on 4 H200s. Base and adapter each scored 11/12 on the fixed GSM8K smoke slice with 12/12 numeric prediction agreement. Both model IDs retrieved the expected value from an actual 60,000-token prompt (prompt_tokens=60000).

Training app: https://modal.com/apps/modal-labs/peyton-agents/ap-RQAgxZiwey6e8Xg2Zyw7On
Finalizer app: https://modal.com/apps/modal-labs/peyton-agents/ap-YrXs4ImD3jjg8kJ3ghbY3M
Serving/eval app: https://modal.com/apps/modal-labs/peyton-agents/ap-qWTFKVR3yw2W4j0Sw6B4pS

The training app stopped with zero tasks after checkpointing. UCCL emitted its known nonfatal cudaFree failed: invalid argument destroy-time warnings, but the function did not fail and the independently loaded/finalized checkpoint passed all structural and serving checks.

Code/docs for this topology are in e363adb355f325d042e70e79d1ee570b3390e2f0.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Open in Devin Review

Comment on lines +1115 to +1116
if not SERVE_RUN_ID:
raise RuntimeError("Set SERVE_RUN_ID to a finalized checkpoint run ID")

@devin-ai-integration devin-ai-integration Bot Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Serving deployment always fails to start because the selected run is never seen by the server

The run to serve is read from a shell environment value (os.environ.get("SERVE_RUN_ID") at deepseek-v4-flash-sft/modal_train.py:46) that only exists on the machine that runs the deploy command, so when the server process starts it finds nothing and refuses to run.
Impact: The documented serving command starts a server that immediately errors out instead of serving the adapter, so serving never comes up.

Why the deploy-time environment value is empty at runtime

SERVE_RUN_ID is evaluated once at module import (deepseek-v4-flash-sft/modal_train.py:46). The README instructs SERVE_RUN_ID=<run-id> uv run --frozen modal deploy ..., which sets the variable only in the local deploy process. Modal re-imports modal_train.py inside the serving container, where SERVE_RUN_ID is not present in the environment (it is not injected via the image .env in deepseek-v4-flash-sft/dsv4_images.py:168-173 nor via a Secret), so it resolves to None. serve() then hits if not SERVE_RUN_ID: raise RuntimeError(...) at deepseek-v4-flash-sft/modal_train.py:375-376 and the web server fails to start.

The train path avoids exactly this pitfall by passing expected_nodes=N_NODES as an explicit function argument (deepseek-v4-flash-sft/modal_train.py:394) and re-validating it remotely (modal_train.py:183-184), rather than relying on the remote container re-reading N_NODES. The same technique was not applied to SERVE_RUN_ID.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@pawalt

pawalt commented Jul 19, 2026

Copy link
Copy Markdown
Member

Cleanup is pushed in 777ade9e58463c3b2744ac7768a55da8beb6cc39.

  • Renamed the launcher to deepseek-v4-flash-sft/modal_train.py.
  • Reduced it from 1,496 to 410 lines by moving image construction to dsv4_images.py, checkpoint finalization to dsv4_checkpoint.py, and the AutoModel config to train_recipe.yaml.
  • Removed the GSM8K/60k evaluation harness, server retry/debug machinery, cgroup diagnostics, and the validate_lora_serving entrypoint.
  • Retained only the operational train, finalize, and protected serve workflows.
  • Moved all six integration patches under deepseek-v4-flash-sft/patches/ without content changes.
  • Rewrote the README around the current workflow and removed the Limits, validation-history, and superseded-experiment prose.

Validation:

The endpoint remains protected with requires_proxy_auth=True; no proxy token is stored in this checkout, so the deployment was not invoked over HTTP during this cleanup pass.

@pawalt pawalt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@pawalt
pawalt merged commit b26fbbc into main Jul 19, 2026
1 check passed
@pawalt
pawalt deleted the devin/1780686358-deepseek-v4-flash-sft branch July 19, 2026 17:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant