Repository navigation
feat(training): add DPO training with offline-scored reference model - #6092
Merged
Merged
Conversation
…turn Signed-off-by: Josephasafg <ajgard7@gmail.com>
…g_data_loader Signed-off-by: Josephasafg <ajgard7@gmail.com>
…tifacts Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com> # Conflicts: # tests/unit_tests/recipes/qwen/test_qwen3_recipes.py
cuichenx
reviewed
Oct 12, 2026
Josephasafg
marked this pull request as ready for review
October 12, 2026 07:46
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
…alidate Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Contributor
Author
|
@cuichenx Thanks for the great comments. I've updated the PR. PTAL. Thanks! |
Contributor
Author
@cuichenx Ive added an L1 test as requested |
Contributor
|
/ok to test 74f8998 |
Contributor
|
Automatic Claude reviews have been retired. To request a pull-request review, post a comment containing: Add |
…ings - Add the NVIDIA Apache-2.0 header to the seven new DPO test files (fixes copyright-check). - Qwen3-30B-A3B DPO recipe: the scorer now takes --recipe, not --tp/--ep/--etp; note that CE fusion matches because the scorer builds from the recipe. - DPODatasetConfig.pad_seq_length_to_mult: derived in ConfigContainer.validate, not dpo_train. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Chen Cui <chcui@nvidia.com>
Contributor
|
/ok to test 59570d8 |
Contributor
Author
|
@cuichenx Thanks! Seems like two unrelated tests are failing |
cuichenx
approved these changes
Oct 14, 2026
3 of 5 tasks
This branch was successfully deployed
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.
What does this PR do ?
Adds DPO (Direct Preference Optimization) as a training mode. The reference model's log-probabilities are scored once, offline, and read back during training, so the trainer holds one model and runs at SFT memory and step time.
Changelog
--mode dpoinscripts/training/run_recipe.py, backed bydpo_train()intraining/dpo_train.py: validates the run config and the reference artifact, then hands off to the existingfinetuneloop.data/datasets/preference*.pyanddata/builders/dpo.py. Rows are TRL-style chat pairs from an HF dataset or a JSONL file. Chosen and rejected rows of a pair always land in the same micro batch, next to each other.training/dpo.pyandtraining/dpo_step.py: sigmoid preference loss, optional SFT term, metrics for preference loss, accuracy, margin and chosen/rejected rewards.scripts/dpo/score_reference_logprobs.py, the offline scorer. You run it with the same TP, EP and ETP you'll train with. Extra GPUs beyond that become data-parallel replicas, and rank 0 collects the results and writes the artifact.qwen3_30b_a3b_dpo_config(TP=4, PP=2, EP=4, ETP=1, SP).scripts/dpo/README.mdand a DPO section inscripts/training/README.md.Why the reference is scored offline
The usual DPO trainer keeps a frozen copy of the reference model and runs it every step - a second set of resident weights, an extra forward per step, and the reference is stuck with the policy's parallel layout
Instead, the scorer runs the reference over the dataset once and writes
ref_logprobs.jsonl(chosen and rejected completion logprob sums per row) andscoring_metadata.json(dataset, split, tokenizer, sequence length, Parallelism type, prompt key). At startup the trainer compares that metadata against its own config and refuses to run on a mismatch. Without the check a stale or mis-scored artifact does not crash anything, the margins could possibly be wrong.Pipeline parallelism is not recorded: it only decides which rank runs a layer. Scoring at PP=2 and PP=1 gave the same results over sums on all 8,320 pairs; TP=4 vs TP=8 differed on every pair, so TP is recorded.
The artifact stores plain logprob sums with no beta applied, so one scoring run covers every learning rate and beta you try afterwards.
flowchart LR DS[("preference pairs<br/>HF dataset or JSONL")] subgraph once["scorer, run once"] REF["reference model"] ART[/"ref_logprobs.jsonl<br/>chosen / rejected logprob sums per row"/] META[/"scoring_metadata.json<br/>dataset, split, tokenizer,<br/>seq len, TP / SP"/] REF --> ART REF --> META end subgraph train["trainer, every run"] CHK{"metadata matches<br/>run config?"} LOADER["pair loader<br/>chosen + rejected adjacent"] POL["policy model"] LOSS["DPO loss<br/>preference loss, accuracy,<br/>margin, rewards"] STEP["backward + optimizer step<br/>finetune loop"] CHK -->|"yes"| LOADER CHK -->|"no"| STOP["error at startup"] LOADER --> POL --> LOSS --> STEP LOADER -->|"reference sums"| LOSS end DS --> REF DS -->|"same rows, same order"| LOADER ART -->|"joined by row index"| LOADER META --> CHKUsage
Results
Qwen/Qwen3-30B-A3B with
qwen3_30b_a3b_dpo_configat its defaults, 2 nodes x 8 H100 80GB.HuggingFaceH4/ultrafeedback_binarizedtrain_prefs, 8,192 pairs, one epoch. Validation on 128 pairs oftest_prefsevery 32 stepsStep 1 preference loss is 0.691, i.e. ln 2, as it should be when the policy starts from the reference.
W&B curves from the run above, validation split. The model learns to prefer chosen over rejected
Training preference loss per 32-step window: 0.681, 0.652, 0.636, 0.634.
Accuracy plateaus at 0.68-0.70, in line with published DPO results on UltraFeedback.
Tests
Unit tests, no GPU:
tests/unit_tests/data/builders/test_dpo.py,data/datasets/test_preference.py,data/datasets/test_preference_tokenization.py,training/test_dpo.py,training/test_dpo_step.py,training/test_dpo_train.py,scripts/dpo/test_score_reference_logprobs.py, plus cases intraining/test_config.py,training/utils/test_pg_utils.py,recipes/qwen/test_qwen3_recipes.pyand thescripts/trainingrunner tests.