Skip to content

Commit 07b65bb

Browse files
Merge remote-tracking branch 'upstream/main' into fix-torch-jit-script-method-py314-warning
2 parents e0b03f9 + 3151ed3 commit 07b65bb

34 files changed

Lines changed: 4813 additions & 3451 deletions

docs/source/distillation_trainer.md

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -54,34 +54,15 @@ trainer.save_model()
5454

5555
## Usage tips
5656

57-
The [`experimental.distillation.DistillationTrainer`] needs three key parameters set via [`experimental.distillation.DistillationConfig`]:
57+
The [`experimental.distillation.DistillationTrainer`] needs two key parameters set via [`experimental.distillation.DistillationConfig`]:
5858

5959
* `lmbda`: controls the student data fraction, i.e., the proportion of on-policy student-generated outputs. When `lmbda=0.0`, training is fully off-policy (dataset completions only). When `lmbda=1.0`, training is fully on-policy (student generates all completions). For values in between, each gradient accumulation slice is randomly assigned as on- or off-policy based on `lmbda`.
6060
* `beta`: controls the interpolation in the Generalized Jensen-Shannon Divergence. When `beta=0.0` the loss approximates forward KL divergence, while `beta=1.0` approximates reverse KL divergence. Values in between interpolate.
61-
* `distillation_objective`: selects the training objective. Use `"jsd"` for the generalized JSD/KL objective, or `"iw_opd"` for Importance-Weighted On-Policy Distillation, which reweights sampled-token reverse-KL policy-gradient updates by prefix teacher-student agreement. IW-OPD requires `lmbda=1.0`.
62-
* `loss_top_k`: number of top tokens to use for the KL/JSD loss. Set to `0` for exact full-vocabulary computation (local teacher only), or `> 0` for a top-k approximation. See more about top-k with external teacher server below.
6361

6462
### On-policy vs. off-policy
6563

6664
Setting `lmbda=1.0` (fully on-policy) generally outperforms off-policy distillation because the student learns from its own mistakes rather than imitating trajectories it may never produce. The generation buffer ensures on-policy training stays efficient: prompts across gradient accumulation steps are batched into a single vLLM call.
6765

68-
### Importance-Weighted On-Policy Distillation
69-
70-
Set `distillation_objective="iw_opd"` to use Importance-Weighted On-Policy Distillation from [On the Position Bias of On-Policy Distillation](https://huggingface.co/papers/2606.22600). IW-OPD computes a sampled-token OPD advantage from teacher and student log-probabilities, then upweights earlier tokens and downweights later tokens according to accumulated teacher-student drift.
71-
72-
```python
73-
config = DistillationConfig(
74-
output_dir="distilled-model",
75-
distillation_objective="iw_opd",
76-
iw_opd_gamma=0.5,
77-
lmbda=1.0,
78-
)
79-
```
80-
81-
IW-OPD is only available for fully on-policy training. It is incompatible with `use_liger_kernel=True`; when
82-
`use_vllm=True`, set `vllm_sync_frequency=1`. The vLLM path requests sampled-token rollout log-probabilities and
83-
uses them as the detached IW-OPD rollout policy log-probs.
84-
8566
### Using an external teacher server
8667

8768
For teachers that do not fit on training GPUs (e.g., 100B+ parameters), host the teacher on a separate vLLM server and set `use_teacher_server=True` with `teacher_model_server_url`:

docs/source/paper_index.md

Lines changed: 9 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -684,37 +684,6 @@ training_args = GRPOConfig(
684684
)
685685
```
686686

687-
688-
### Rethinking the Trust Region in LLM Reinforcement Learning
689-
690-
**📜 Paper**: https://huggingface.co/papers/2602.04879
691-
692-
DPPO replaces PPO/GRPO's heuristic ratio-clipping with a principled trust region based on direct policy divergence estimates. PPO-style clipping masks tokens based on the probability ratio π/μ, which over-penalizes low-probability tokens and under-penalizes high-probability ones. DPPO instead masks based on direct approximations of policy divergence (TV or KL), ensuring updates stay within a theoretically grounded trust region. Four divergence approximations are supported: `binary_tv`, `binary_kl`, `topk_tv`, and `topk_kl`.
693-
694-
```python
695-
from trl.experimental.dppo import DPPOConfig, DPPOTrainer
696-
697-
training_args = DPPOConfig(
698-
divergence_type="binary_tv", # divergence approximation
699-
divergence_topk=20, # K for top-K divergence modes (Section 7 / Appendix G.2 of the paper)
700-
epsilon=0.15, # δ_low threshold (Appendix F of the paper)
701-
epsilon_high=0.15, # δ_high threshold (Appendix F of the paper)
702-
clip_ratio_c=20.0, # IS ratio upper bound C (Section 5.4 of the paper)
703-
beta=0.0, # KL regularization coefficient
704-
use_vllm=True,
705-
)
706-
707-
trainer = DPPOTrainer(
708-
model="your-model",
709-
reward_funcs=[...],
710-
args=training_args,
711-
train_dataset=dataset,
712-
)
713-
trainer.train()
714-
```
715-
716-
The official code [sail-sg/Stable-RL](https://github.com/sail-sg/Stable-RL)
717-
718687
## Optimal Advantage Regression
719688

720689
Papers relating to the [`experimental.a2po.A2POTrainer`].
@@ -1665,37 +1634,37 @@ Papers relating to training a student model with the help of a teacher model.
16651634

16661635
**📜 Paper**: https://huggingface.co/papers/2306.13649
16671636

1668-
Introduces Generalized Knowledge Distillation (GKD), which addresses distribution mismatch in KD for auto-regressive models by training the student on its own generated outputs with teacher feedback, instead of a fixed set of sequences. GKD supports flexible loss functions (e.g. beyond KL when the student cannot match the teacher) and integrates with RL fine-tuning (RLHF). The paper reports results on summarization, translation, arithmetic reasoning, and instruction-tuning. Used in TRL via [`experimental.distillation.DistillationTrainer`] and [`experimental.gkd.GKDTrainer`]. To reproduce the paper's setting, use this configuration:
1637+
Introduces Generalized Knowledge Distillation (GKD), which addresses distribution mismatch in KD for auto-regressive models by training the student on its own generated outputs with teacher feedback, instead of a fixed set of sequences. GKD supports flexible loss functions (e.g. beyond KL when the student cannot match the teacher) and integrates with RL fine-tuning (RLHF). The paper reports results on summarization, translation, arithmetic reasoning, and instruction-tuning. Used in TRL via [`experimental.gkd.GKDTrainer`], which exposes the paper's on/off-policy mixing (`lmbda`). [`experimental.distillation.DistillationTrainer`] implements the same generalized-JSD objective for the always-on-policy case. To reproduce the paper's setting, use this configuration:
16691638

16701639
```python
1671-
from trl.experimental.distillation import DistillationConfig
1640+
from trl.experimental.gkd import GKDConfig
16721641

16731642
# XSum summarization task (Table A.1 of the paper)
1674-
training_args = DistillationConfig(
1643+
training_args = GKDConfig(
16751644
lmbda=0.5, # λ student data fraction (Section 3 of the paper)
16761645
beta=0.5, # β Generalized JSD interpolation, 0=KL, 1=reverse KL (Section 3 of the paper)
16771646
temperature=1.0, # student training temperature (Appendix A of the paper)
16781647
max_steps=40000, # training steps (Table A.1 of the paper)
16791648
learning_rate=3e-4, # learning rate (Table A.1 of the paper)
16801649
per_device_train_batch_size=32, # batch size (Table A.1 of the paper)
16811650
warmup_steps=2000, # warm-up steps (Table A.1 of the paper)
1682-
max_completion_length=64, # max output tokens (Table A.1 of the paper)
1651+
max_new_tokens=64, # max output tokens (Table A.1 of the paper)
16831652
)
16841653
```
16851654

16861655
### On the Position Bias of On-Policy Distillation
16871656

16881657
**📜 Paper**: https://huggingface.co/papers/2606.22600
16891658

1690-
Introduces Importance-Weighted On-Policy Distillation (IW-OPD), which addresses the position bias in OPD by reweighting sampled-token distillation updates according to accumulated teacher-student prefix discrepancy. Early tokens keep larger weights, while later tokens after high drift are downweighted. Used in TRL via [`experimental.distillation.DistillationTrainer`] with `distillation_objective="iw_opd"`.
1659+
Introduces Importance-Weighted On-Policy Distillation (IW-OPD), which addresses the position bias in OPD by reweighting sampled-token distillation updates according to accumulated teacher-student prefix discrepancy. Early tokens keep larger weights, while later tokens after high drift are downweighted. Used in TRL via [`experimental.iw_opd.IWOPDTrainer`] with `distillation_objective="iw_opd"`.
16911660

1692-
The paper reports its main experiments with a verl PPO trainer and vLLM rollouts. `DistillationTrainer` exposes the matching distillation and rollout settings below; PPO-specific settings from the paper such as clipping range `0.2`, dual-clip constant `3.0`, PPO epochs, entropy coefficient, KL reward penalty, auxiliary KL, and rollout importance correction are not `DistillationConfig` parameters.
1661+
The paper optimizes IW-OPD with a clipped policy-gradient setup (verl) and vLLM rollouts. `IWOPDTrainer` exposes the matching distillation and rollout settings below; policy-optimization settings from the paper such as clipping range `0.2`, dual-clip constant `3.0`, inner PPO epochs, entropy coefficient, KL reward penalty, auxiliary KL, and rollout importance correction are not `IWOPDConfig` parameters.
16931662

16941663
```python
1695-
from trl.experimental.distillation import DistillationConfig
1664+
from trl.experimental.iw_opd import IWOPDConfig
16961665

1697-
# Table 6 and Algorithm 1 of the paper, mapped to DistillationConfig where available.
1698-
training_args = DistillationConfig(
1666+
# Table 6 and Algorithm 1 of the paper, mapped to IWOPDConfig where available.
1667+
training_args = IWOPDConfig(
16991668
distillation_objective="iw_opd",
17001669
iw_opd_gamma=0.5, # γ amplification, Algorithm 1 and Appendix C.3
17011670
lmbda=1.0, # fully on-policy rollouts

docs/source/vllm_integration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This document will guide you through the process of using vLLM with TRL for faster generation in online methods like GRPO and Online DPO. We first summarize a tl;dr on how to use vLLM with TRL, and then we will go into the details of how it works under the hood.
44

55
> [!WARNING]
6-
> TRL currently only supports vLLM versions from `0.16.0` to `0.24.0`. Please ensure you have a version in this range installed to avoid compatibility issues.
6+
> TRL currently only supports vLLM versions from `0.17.0` to `0.25.1`. Please ensure you have a version in this range installed to avoid compatibility issues.
77
88
> [!TIP]
99
> The following trainers currently support generation with vLLM:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ test = [
8080
"pytest"
8181
]
8282
vllm = [
83-
"vllm>=0.16.0,<=0.24.0",
83+
"vllm>=0.17.0,<=0.25.1",
8484
"fastapi",
8585
"pydantic",
8686
"aiohttp>=3.13.3",

tests/distributed/test_distributed.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,45 @@ def test_dpo(self, config, get_config_path):
148148
)
149149
# fmt: on
150150

151+
@pytest.mark.parametrize(
152+
"config",
153+
[
154+
"ddp",
155+
pytest.param(
156+
"zero2",
157+
marks=pytest.mark.xfail(
158+
Version(transformers.__version__) == Version("5.1.0"),
159+
reason="Upstream incompatibility: deepspeed and transformers==5.1.0 (see transformers#43780)",
160+
),
161+
),
162+
pytest.param(
163+
"zero3",
164+
marks=pytest.mark.xfail(
165+
Version(transformers.__version__) == Version("5.1.0"),
166+
reason="Upstream incompatibility: deepspeed and transformers==5.1.0 (see transformers#43780)",
167+
),
168+
),
169+
],
170+
)
171+
def test_dpo_precompute_ref_log_probs(self, config, get_config_path):
172+
# `--eval_strategy epoch` passes an eval dataset, so reference log-probs are precomputed for both the train and
173+
# eval splits (two passes), which is what previously broke multi-GPU precompute (fingerprint cache mismatch, and
174+
# a corrupted ZeRO-3 parameter coordinator from re-initializing DeepSpeed on the policy model per pass).
175+
# fmt: off
176+
run_command(
177+
[
178+
"accelerate", "launch", "--config_file", get_config_path(config), "trl/scripts/dpo.py",
179+
"--output_dir", self.tmp_dir,
180+
"--model_name_or_path", "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5",
181+
"--dataset_name", "trl-internal-testing/zen",
182+
"--dataset_config", "standard_preference",
183+
"--precompute_ref_log_probs",
184+
"--eval_strategy", "epoch",
185+
],
186+
os.environ.copy(),
187+
)
188+
# fmt: on
189+
151190
@require_liger_kernel
152191
@pytest.mark.parametrize(
153192
"config",

tests/experimental/test_async_grpo_trainer.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,17 @@ def dummy_reward_func(completions, **kwargs):
5656
class _StubRolloutWorker:
5757
"""Minimal rollout worker stub for testing the trainer in isolation."""
5858

59-
def __init__(self, tokenizer, dataset, num_generations: int = 8, samples_per_weight_sync: int = 10):
59+
def __init__(
60+
self, tokenizer, dataset, num_generations: int = 8, samples_per_weight_sync: int = 10, fork_k: int = 1
61+
):
6062
self.rollout_buffer = queue.Queue()
6163
self._samples_per_weight_sync = samples_per_weight_sync
6264
self._model_version = 0
65+
self._fork_k = fork_k
6366
self._sample_iter = self._make_sample_iter(tokenizer, dataset, num_generations)
6467

6568
def _make_sample_iter(self, tokenizer, dataset, num_generations):
66-
for row in itertools.cycle(dataset):
69+
for group_id, row in enumerate(itertools.cycle(dataset)):
6770
completions = [
6871
[{"role": "assistant", "content": f"{row['completion'][0]['content']} {idx}"}]
6972
for idx in range(num_generations)
@@ -79,16 +82,22 @@ def _make_sample_iter(self, tokenizer, dataset, num_generations):
7982
advantages = (rewards - rewards.mean()) / rewards.std()
8083
for idx in range(num_generations):
8184
completion_ids = prompt_completion_ids[idx][len(prompt_ids) :]
82-
yield RolloutSample(
85+
sample = RolloutSample(
8386
prompt=row["prompt"],
8487
completion=completions[idx],
8588
input_ids=prompt_ids + completion_ids,
8689
completion_mask=[0] * len(prompt_ids) + [1] * len(completion_ids),
8790
old_log_probs=[0.0] * len(prompt_ids) + [-0.5] * len(completion_ids),
8891
advantage=float(advantages[idx]),
8992
model_version=self._model_version,
93+
group_id=group_id,
9094
metrics={"reward": float(rewards[idx]), "reward_std": float(rewards.std())},
9195
)
96+
# fork_k rows per generation, all sharing this group_id: mimics message-mode forking a
97+
# conversation into several training rows. Only the shared group_id matters for the epoch
98+
# count, so identical duplicates are enough here (row shapes are covered by the reconciler tests).
99+
for _ in range(self._fork_k):
100+
yield sample
92101

93102
def _fill_queue(self):
94103
for _ in range(self._samples_per_weight_sync):
@@ -326,6 +335,7 @@ def _rollout_sample(length: int, advantage: float = 0.0, reward: float = 0.0) ->
326335
"completion_mask": [0] + [1] * (length - 1),
327336
"old_log_probs": [0.0] * length,
328337
"advantage": advantage,
338+
"group_id": 0,
329339
"metrics": {"reward": reward},
330340
}
331341

@@ -653,6 +663,7 @@ def _group(completions_sequences, completions_ids):
653663
tool_call_counts=[0] * n,
654664
tool_failure_counts=[0] * n,
655665
model_version=7,
666+
group_id=0,
656667
env_rewards=[None] * n,
657668
)
658669

@@ -722,3 +733,53 @@ def maybe_none(completions, **kwargs):
722733
assert math.isnan(samples[0].metrics["reward"])
723734
assert samples[1].advantage == 0.0 # only one scorable row -> zero-centered
724735
assert samples[1].metrics["reward"] == 2.0
736+
737+
738+
@pytest.mark.skipif(
739+
not is_ampere_or_newer() and torch_device != "xpu",
740+
reason="Flash Attention 2 requires Ampere or newer GPU, or XPU",
741+
)
742+
class TestEpochStop(TrlTestCase):
743+
"""`num_train_epochs` stops after N full passes over the PROMPTS, counted as distinct group_ids.
744+
745+
The point of the fix is that this is fork-independent: a conversation that message-mode forks into several training
746+
rows still counts as one prompt-group, so the number of epochs does not depend on the fork rate (only the number of
747+
optimizer steps does). Driven end-to-end through the real trainer (stub worker + real forward/backward), which is
748+
what actually regresses if the wiring breaks.
749+
"""
750+
751+
def _train(self, fork_k, num_train_epochs=2):
752+
model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5"
753+
dataset = load_dataset("trl-internal-testing/zen", "conversational_prompt_completion", split="train")
754+
args = AsyncGRPOConfig(
755+
output_dir=self.tmp_dir,
756+
num_train_epochs=num_train_epochs, # epoch-driven: no explicit max_steps
757+
per_device_train_batch_size=3,
758+
num_generations=3,
759+
max_completion_length=8,
760+
token_budget=-1, # FixedCountBatcher: deterministic samples/step, so groups accrue predictably
761+
vllm_server_timeout=5.0,
762+
report_to="none",
763+
)
764+
worker = _StubRolloutWorker(AutoTokenizer.from_pretrained(model_id), dataset, num_generations=3, fork_k=fork_k)
765+
trainer = AsyncGRPOTrainer(
766+
model=model_id, reward_funcs=dummy_reward_func, args=args, train_dataset=dataset, rollout_worker=worker
767+
)
768+
trainer.train()
769+
return trainer, len(dataset)
770+
771+
def test_epoch_stop_is_fork_independent(self):
772+
no_fork, num_prompts = self._train(fork_k=1)
773+
forked, _ = self._train(fork_k=3)
774+
775+
# Both stop after num_train_epochs=2 full passes over the prompts, i.e. ~2 x num_prompts distinct
776+
# groups, plus at most one micro-batch of overshoot — regardless of the fork rate. (HF's own
777+
# state.epoch is meaningless here: it's global_step/max_steps over an infinite IterableDataset,
778+
# so we judge epochs by distinct prompt-groups trained, which is what the callback targets.)
779+
for trainer in (no_fork, forked):
780+
assert 2 * num_prompts <= len(trainer._trained_groups) < 3 * num_prompts
781+
782+
# Forks add rows, hence fixed-size optimizer steps: 3x rows per conversation must take strictly more
783+
# steps for the same 2 epochs. If forks leaked into the epoch count, the forked run would instead
784+
# stop in FEWER prompt-passes (the pre-fix bug).
785+
assert forked.state.global_step > no_fork.state.global_step

0 commit comments

Comments
 (0)