feat: SFT from a HF dataset through the RL stack (dataset_sft) - #3233
Draft
mikasenghaas wants to merge 7 commits into
Draft
feat: SFT from a HF dataset through the RL stack (dataset_sft)#3233mikasenghaas wants to merge 7 commits into
mikasenghaas wants to merge 7 commits into
Conversation
A run-level algorithm (type = "dataset_sft") that trains SFT straight
from a static HF dataset while keeping the RL stack's orchestration:
the orchestrator renders dataset examples into ce-routed training
samples and ships them through the regular pack/transport path, the
dispatcher serves online evals against the live policy, and the trainer
runs unchanged (algorithm-blind ce loss component).
Train sources take no effect and must be empty; eval sources stay
optional. Example rendering is shared with the native SFT trainer via
render_example, extracted from SFTDataset._process. The data position
({epoch, cursor}) round-trips through the orchestrator checkpoint.
Includes a dev-sized reverse-text config (sft-rl.toml) mirroring
sft.toml for side-by-side comparison.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A production-scale counterpart to the reverse-text sft/sft-rl pair: GLM-4.5-Air (106B A12B) on INTELLECT-3-SFT at 131k context, cp=4 ulysses + muon on 4 H200 nodes, with an identical 1,048,576 tokens/step budget on both entrypoints so the two paths are directly comparable. - sft.toml — native `uv run sft`, batch_size 8 x seq_len 131072 - sft-rl.toml — `uv run rl` + dataset_sft, token_batch_size 1048576, plus one inference node running AIME'25 evals every 10 steps against the live policy Cache paths are user-scoped (/tmp/mika-*): the bare /tmp/.triton-cache and /tmp/.vllm-cache used by the other configs in this directory are a PermissionError on any node where a different user created them first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Shared /tmp cache dirs (.triton-cache, .vllm-cache, .flashinfer-cache) are a startup PermissionError for every user except whoever created them first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
next_batch() accumulated whole samples until num_tokens crossed token_batch_size, so a budget that is an exact multiple of seq_len x dp — the natural choice — overshot into ceil = dp+1 minimum FFD bins every step, which the packer rounds up to 2 x dp by splitting: every rank ran two half-full micro-batches instead of one full row, doubling per-micro-batch overhead (cp all-gathers, MoE dispatch, activation offload cycles). Measured on GLM-4.5-Air at 131k/cp=4/dp=8: all 20 steps shipped 8.00-8.31 x seq_len tokens -> 16 micro-batches, +65% forward+backward vs the native SFT trainer on the same data. The first sample that would cross the budget is now held back and leads the next step's batch; the orchestrator checkpoint position rolls back to that example so a resume re-renders it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…leanly The launch script's `wait -n` tore a node down as soon as its FIRST background process exited, even cleanly. Both directions of that race are real: at max_steps the trainer (exit 0) killed the orchestrator mid-drain of final-step evals (observed: step-20 AIME eval killed at 9/30), and a dataset_sft orchestrator that finishes shipping early (exit 0) killed the trainer mid-step (observed at step 5 with an ablated broadcast cadence), leaving peers hung in NCCL and the allocation held by the idle inference node. Teardown now waits until every principal process on the node — torchrun and the orchestrator — has exited (loop over `wait -n -p`); any non-zero exit still tears down immediately so --kill-on-bad-exit can reap peers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The trainer broadcast weights to inference after every step, whether anything consumed them or not — for dataset_sft without a pending eval that is pure overhead (~7s/step for GLM-4.5-Air at 131k on 4 nodes). The orchestrator knows exactly which policy versions inference will consume, so it stamps the request on the work itself: ``MicroBatch.sync_weights`` marks the steps whose resulting weights are needed, and the trainer broadcasts exactly those (filesystem broadcast still writes every version for resume). Rollout training marks every step except the final TARGET_LAG+1 (moving the trainer's old final-step skip to the producer); dataset_sft marks steps where an eval fires next or evals are queued/in flight — in-flight evals sample the live policy, so long-running inference keeps receiving fresh weights at the trainer's cadence, mid-generation. Pacing previously rode on those per-step broadcasts (the ship hold waited on policy versions). It now rides on consumption acks: receivers push ``ack|<rank>|<step>`` on the ZMQ control channel (the READY barrier socket) after each step, and the dataset ship hold waits on the acked floor instead. The filesystem transport keeps no-op pacing — batches persist on disk. A mark is a promise that the broadcast finds its receiving side up: shutdown holds until the last requested sync lands (``await_final_weight_sync``), since the trainer may reach a marked step long after the last eval finished and a mid-broadcast orchestrator exit strands it in the NCCL rendezvous. Validated on GLM-4.5-Air/INTELLECT-3-SFT at 131k (10 steps, AIME'25 eval every 5): broadcasts landed at exactly v0-v5 (startup + eval-active steps), v9-v10 (eval at 10), none in between; the shutdown hold released 0.2s after the trainer's final broadcast. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
First-fit-decreasing minimizes bin count but places each sample into the first bin with room, co-locating the longest samples: with few bins per DP rank the hottest rank's attention cost (quadratic in sample length) gates every FSDP step. Measured on GLM-4.5-Air/INTELLECT-3-SFT at 131k with one bin per rank, per-rank cost imbalance (max/mean sum len^2) was 2.2-3.4x and the trainer ran 31% slower per step than the native SFT trainer on the same data. Packing now tries a cost-balanced variant at the bin count the token total forces anyway (opened up front, each sample placed longest-first into the cheapest compatible bin, scored by the existing additive bin_cost) and keeps it unless it needs more rounded-up bins than first-fit — never more bins, strictly better balance. On the same workload imbalance drops to a median 1.8x and the trainer matches the native SFT trainer's step time (34.6s vs 36.1s median; first-fit ran 47.2s with per-step broadcasts, 40.7s without). Rollout batches benefit the same way whenever the forced bin rounding leaves few bins per rank; with many bins per rank the downstream cost-balanced distribution already evens things out and the packing choice matters less. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
POC: SFT from a static HF dataset through the RL training stack, keeping the orchestrator + inference server for online evals.
[orchestrator.algo] type = "dataset_sft": carries anSFTDatasetConfig(name,subsets/splits/probabilities/stopping_strategy,shuffle,seed,loss_mask) — the same example schema and knobs as the native SFT trainer.[[orchestrator.train.source]]is a config error); eval sources stay optional;dataset_sftis rejected as a per-source algorithm.DatasetSFTSourcerenders dataset examples through the policy renderer into ce-routedTrainingSamples (ce_weightson the loss mask,rl_weightszeroed, dummy logprobs/temperatures). Adataset_loopruns beside the main loop: it packs and ships batches through the regular packer/transport path, while the dispatcher serves evals only. Token batching takes the largest whole-sample prefix withintoken_batch_size(the first overflowing sample carries to the next step). The data position ({epoch, cursor}, per-epoch reshuffle) round-trips through the orchestrator checkpoint.messages/prompt+completionresolution, tools, role loss masking) is extracted fromSFTDataset._processinto a sharedrender_example, so both SFT paths tokenize identically.Three pieces of shared infrastructure fell out of chasing the RL trainer's step time down to the native SFT trainer's (ablation details under Verification):
MicroBatch.sync_weightsmarks the steps whose weights are needed, and the trainer broadcasts exactly those. Rollout training marks every step except the finalTARGET_LAG+1(the trainer's old final-step skip, moved to the producer);dataset_sftmarks eval-trigger steps and any step shipped while evals are queued or in flight — in-flight evals sample the live policy, so long-running inference keeps receiving fresh weights at the trainer's cadence, mid-generation. Producer pacing moves from policy versions to consumption acks on the ZMQ control channel (the READY-barrier socket), and shutdown holds until the last requested sync lands so the trainer is never stranded mid-NCCL-rendezvous.packed_samples_into_micro_bs). First-fit-decreasing minimizes bins but co-locates the longest samples; with few bins per DP rank the hottest rank's quadratic attention cost gates every FSDP step. Packing now tries a cost-balanced variant at the bin count the token total forces anyway (each sample placed longest-first into the cheapest compatible bin, scored by the existing additivebin_cost) and keeps it unless it needs more rounded-up bins than first-fit — never more bins, strictly better balance. Rollout batches benefit the same way whenever forced bin rounding leaves few bins per rank.multi_node_rl.sbatch.j2).wait -ntore a node down when its first background process exited, even cleanly — atmax_stepsthe trainer's exit killed the orchestrator mid-drain of final-step evals, and adataset_sftorchestrator finishing its ship loop early killed the trainer mid-step. Teardown now waits until every principal process (torchrun, orchestrator) has exited cleanly; any non-zero exit still tears down immediately.Adds
configs/basic/reverse-text/sft-rl.toml(dev-sized counterpart ofsft.toml, same token budget per step) andexamples/advanced/glm-4.5-air/{sft,sft-rl}.toml(GLM-4.5-Air on INTELLECT-3-SFT at 131k context, cp=4 ulysses + muon on 4 H200 nodes, identical 1,048,576 tokens/step on both entrypoints).Verification
Reverse-text (Qwen3-0.6B, 100 steps × ~131k tokens, lr 2e-5, one training GPU each):
uv run sft(1 GPU)uv run rl+dataset_sft(1 GPU + 1 vLLM)nll/mean)GLM-4.5-Air at 131k (INTELLECT-3-SFT
swe_swiss+am_if+toucan_tool, 4×8×H200 training, cp=4 ulysses, muon, 1,048,576 tokens/step). The step-time ablation that motivated the design (same batches, broadcast-free medians):uv run sftdataset_sft, per-step broadcast + first-fit packingBroadcast cost and packing imbalance were the whole gap: measured per-rank attention cost (max/mean Σ len²) is first-fit 2.68, native stream-order rows 1.95, cost-balanced 1.64 — which is why the balanced RL trainer edges out the native trainer per step. The loss-pipeline extras cost nothing measurable and stay.
End-to-end on-demand broadcast validation (GLM-4.5-Air 131k, 10 steps, AIME'25 eval every 5 steps): broadcasts landed at exactly v0 (startup), v1–v3 and v5 (batches shipped while evals were in flight — mid-generation weight updates at the trainer's cadence), v4 and v9 (eval-trigger steps), v10 (mid-final-eval); none on the idle steps 6–8. The shutdown hold released 0.2 s after the trainer's final broadcast, and all four training nodes tore down cleanly on their own.
🤖 Generated with Claude Code