Skip to content

feat: dsv4f sft fork (do not merge) - #3692

Draft
mikasenghaas wants to merge 9 commits into
mainfrom
feat/dsv4f-sft
Draft

mikasenghaas wants to merge 9 commits into
mainfrom
feat/dsv4f-sft

Conversation

@mikasenghaas

@mikasenghaas mikasenghaas commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

Note

This PR is not meant to be merged. The branch is the prime-rl fork that our DeepSeek V4 Flash SFT runs pin. It stays on top of main and carries only the fixes that are not upstream yet. We rebase it onto main on our own schedule. When a fix lands upstream, we drop it from this branch.

Last synced with main at d5f29c072 (vLLM 0.30.0) on 2026-09-30. Earlier pinned heads are tagged dsv4f-sft/2026-09-27, dsv4f-sft/2026-09-28, and dsv4f-sft/2026-09-29.

Summary

The branch carries these commits on top of main:

  • DeepSeek V4 attention-sink loading: loads the attention sinks through vLLM's weight loaders. Before this fix, a layerwise weight update silently discarded the new sink values, so inference kept serving the boot values. vLLM 0.30.0 still writes the sinks with a bare slice copy.

  • Abort stale requests on pause: /pause accepts a mode and defaults to abort. The orchestrator pauses with abort instead of keep, so orphaned requests cannot block a weight update or resume on old-policy KV state.

  • inference.fp8_ue8m0_weight_scales: quantizes online block-FP8 weights with power-of-two block scales. Checkpoints that were dequantized from FP8 then convert back to FP8 exactly.

  • SwiGLU clamp on the Triton FP8 fast path: vLLM's fused silu_and_mul_per_block_quant path ignores DeepSeek V4's swiglu_limit. The patch routes clamped models to the clamped activation path.

  • inference.fp8_stochastic_weight_rounding: rounds online FP8 weights stochastically. The served weight then equals the BF16 trainer weight in expectation, so it tracks updates smaller than one FP8 bin. Includes unit tests.

  • Local vLLM and renderer plugins: inference.vllm_plugins lists module:function or path.py:function targets. prime-rl calls each target in every vLLM process, for example to register a local tokenizer mode. SFT accepts renderers plugin configs (renderers#165), so a local renderer can train without an upstream release.

  • [[data.source]] for SFT: each source is a single (dataset, subset, split), with an optional weight, columns, and renderer table. data.columns maps dataset columns onto messages, prompt, completion, tools, and per-sample renderer fields (for example renderer = { depth = "task_depth" }). Renderer arguments apply in this order: [renderer], then the source's renderer table, then non-null per-sample columns. The legacy name/subsets/splits/probabilities fields resolve to the same sources with the same interleave order and metric names, so resumed runs keep their sample order. Sources load only their mapped columns. Columns whose types differ between sources are stored as JSON so the sources can interleave.

  • Numpy interleave indices: load_sft_dataset builds interleave_datasets' probability-weighted indices with vectorized numpy instead of datasets' per-draw Python loop. The indices are identical, since Generator.choice with p is a searchsorted over rng.random, so resumed runs keep their order. A 45-subset all_exhausted mix (21M rows) goes from about 15 minutes per rank to seconds.

  • Pad SFT packs with their own tokens: CatDataset padded packs with token 0. Identical pad tokens get identical hidden states, so the MoE router sends every pad token of a pack to the same experts, and the expert-parallel rank that hosts them receives the whole pad run. Pads now repeat the pack's own tokens (text packs only). They stay loss-masked and come after every real token, so the loss is unchanged.

Verification

  • Ran ruff check and ruff format --check on each commit.
  • After the rebase onto vLLM 0.30.0: applied every shared vLLM patch in one process, and ran pytest tests/unit/inference tests/unit/train/sft tests/unit/test_configs.py tests/unit/train/models/test_fp8_utils.py: 315 passed, 15 skipped.
  • Ran pytest tests/unit/inference tests/unit/orchestrator/test_clients.py: 33 passed.
  • Ran pytest tests/unit/test_configs.py tests/unit/train/sft: 252 passed, including 13 new source tests.
  • Ran pytest tests/unit/train/sft: 82 passed, including exact index equality with interleave_datasets for both stopping strategies and pad-token content.
  • Measured MoE routing on the exact packs of a DSV4-Flash SFT step that stalled a 256K run (EP16). With token-0 padding, one EP group's pack pair was 73% padding, and some EP ranks received up to 6.2x their balanced load (one rank 6.1x in the last layer). With content padding on the same packs, the worst rank got 2.1x and the loss was identical. The rerun passed that step without the memory-ceiling stall, with expert imbalance (max_vio) about halved and about 10% more throughput.
  • Loaded a private 44-subset SFT dataset as [[data.source]] tables with a local renderer plugin. The per-sample renderer column was set on all 4,268 rendered rows.

🤖 Generated with Claude Code

garrett361 and others added 9 commits September 30, 2026 16:28
…ight loaders

DeepseekV4Model.load_weights writes the attention sinks with a bare
params_dict[name][:n].copy_(narrow_weight) instead of going through
param.weight_loader. Layerwise reload moves a layer's tensors to meta and
wraps each loader to buffer the incoming tensor, so that copy lands in a
meta tensor and is discarded silently: load_numel stays 0, finalize
restores the boot value with only a warning, and the loader still does
loaded_params.add(name), so a named_parameters() - loaded_params diff
cannot see the loss either. Attention sinks are trainable, so every weight
update keeps serving the sinks the server booted with.

Padding this rank's heads back up with -inf, the parameter's own init
value, makes it an ordinary full-parameter load through the existing
loader contract. Still required on vLLM 0.29.0 (model.py:1546).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…FP8 weight scales

Promotes the PRIME_DIAG_UE8M0_WEIGHTS diagnostic to a config field. For
dequantized FP8 releases the weights already sit on the e4m3 grid under
power-of-two block scales, so quantizing with those scales is exact where
amax / 448 injects full e4m3 rounding error. Measured against a bf16 reference
on 203k SWE tokens, combined with VLLM_USE_DEEP_GEMM_E8M0=1 it gives the lowest
mismatch of every FP8 configuration tried (KL 3.5e-3 vs 5.6e-3 for fp32 scales,
IPO masked fraction 0.0000 vs 0.0002) and removes the decode-step glitch tokens.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ast path

vLLM 0.29.0's TritonExperts.apply fuses SiLU-and-mul with the fp8 block
quantization through ops.silu_and_mul_per_block_quant, which has no clamp
argument, so DeepSeek V4 Flash's swiglu_limit of 10 is dropped for every
batch below 128 tokens when VLLM_USE_DEEP_GEMM_E8M0 is off. Tokens whose
gate or up pre-activation exceeds the limit get a wrong expert output; this
produced the deterministic glitch-token argmaxes seen in the FP8 RL runs.
The patch routes apply to the clamped activation branch whenever a clamp is
configured and leaves models without a clamp on the fast path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ne FP8 weights

Round-to-nearest pins a weight that starts on the e4m3 grid until the trainer
has moved it half a bin, about 1000 steps at lr 1e-6, so the served policy
stays at step 0 while the trainer drifts. Rounding each weight to its far
neighbour with probability equal to the fractional distance makes the served
weight unbiased and lets it track sub-bin updates in expectation. On-grid
input rounds to nearest, so the checkpoint quantizes to identical bytes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add `inference.vllm_plugins`: callables that run in every vLLM process
before the model and tokenizer load, given as `package.module:function`
or `path/to/file.py:function`. They register local tokenizer modes or
parsers without a vLLM package. They run last in the shared vLLM patch
hook, because vLLM swallows plugin errors and a failing plugin would
otherwise skip the patches after it.

Let the SFT per-row `reasoning_effort` override work with renderers
plugin configs, and pin renderers to the plugin support branch
(PrimeIntellect-ai/renderers#165).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Let SFT read several datasets, each a single (dataset, subset, split),
with its own sampling weight, column names, and renderer settings.
Renderer arguments resolve in order: [renderer], then the source's
renderer table, then per-sample columns mapped via
data.columns.renderer, so a dataset column like "task_depth" can feed
the renderer field "depth".

Legacy name/subsets/splits/probabilities resolve to the same sources,
with the same interleave seed and progress metric names, so existing
runs keep their sample order on resume. Sources now load only their
mapped columns, and columns whose types differ between sources are
stored as JSON so that mixed datasets interleave.

Bump renderers for the plugin template-kwarg helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
datasets builds interleave indices with probabilities in a Python loop,
one draw at a time, which took about 10 minutes per rank for the 21M
all_exhausted rows of a 45-subset mixture. Build the same indices with
vectorized numpy draws. Generator.choice with p is a searchsorted over
rng.random, so one long draw reproduces its batches of 1000 exactly,
and resumed runs keep their sample order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Packs were padded with token 0. Identical pad tokens get identical
hidden states, so the MoE router sends every pad token of a pack to the
same experts, and the expert-parallel rank hosting them receives the
whole pad run. On DeepSeek-V4-Flash SFT at 256K, a pack pair that was
73% padding pushed that rank to the memory ceiling, where it stalled
until the NCCL watchdog killed the job.

Repeating the pack's own tokens routes padding like real text. Pads
stay loss-masked and follow every real token, so the loss is unchanged.
Multimodal packs keep token 0 so no placeholder id escapes its item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

2 participants