Technical Documentation: huggingface/trl
βΉοΈ Provenance: Multi-Tier Ingestion Β· CodeWiki Reference
Overview (`README.md`)
TRL - Transformers Reinforcement Learning
A comprehensive library to post-train foundation models
π What's New
π Training on AMD GPUs: TRL's trainers now run on AMD Instinct GPUs through PyTorch's ROCm build.
π₯ Beam: Fine-tune Beam with SFTTrainer on a single node of 8 B300s. See the LoRA example.
π₯ Muse Spark Open: Fine-tune Muse Spark Open with SFTTrainer, sharded across your GPUs at load time. See the 8-node LoRA example.
π Training on AWS Trainium: TRL's trainers now run on AWS Trainium through TorchNeuron, PyTorch's native Neuron backend.
Overview
TRL is a cutting-edge library designed for post-training foundation models using advanced techniques like Supervised Fine-Tuning (SFT), Group Relative Policy Optimization (GRPO), and Direct Preference Optimization (DPO). Built on top of the π€ Transformers ecosystem, TRL supports a variety of model architectures and modalities, and can be scaled-up across various hardware setups.
Highlights
Trainers: Various fine-tuning methods are easily accessible via trainers like
SFTTrainer,GRPOTrainer,DPOTrainer,KTOTrainerand more.Efficient and scalable:
- Leverages π€ Accelerate to scale from single GPU to multi-node clusters using methods like DDP and DeepSpeed.
- Full integration with π€ PEFT enables training on large models with modest hardware via quantization and LoRA/QLoRA.
- Integrates π¦₯ Unsloth for accelerating training using optimized kernels.
Command Line Interface (CLI): A simple interface lets you fine-tune with models without needing to write code.
Installation
Python Package
Install the library using pip:
pip install trlFrom source
If you want to use the latest features before an official release, you can install TRL from source:
pip install git+https://github.com/huggingface/trl.gitRepository
If you want to use the examples you can clone the repository with the following command:
git clone https://github.com/huggingface/trl.gitQuick Start
For more flexibility and control over training, TRL provides dedicated trainer classes to post-train language models or PEFT adapters on a custom dataset. Each trainer in TRL is a light wrapper around the π€ Transformers trainer and natively supports distributed training methods like DDP, DeepSpeed ZeRO, and FSDP.
`SFTTrainer`
Here is a basic example of how to use the SFTTrainer:
from trl import SFTTrainer
from datasets import load_dataset
dataset = load_dataset("trl-lib/Capybara", split="train")
trainer = SFTTrainer(
model="Qwen/Qwen2.5-0.5B",
train_dataset=dataset,
)
trainer.train()`GRPOTrainer`
GRPOTrainer implements the Group Relative Policy Optimization (GRPO) algorithm that is more memory-efficient than PPO and was used to train Deepseek AI's R1.
from datasets import load_dataset
from trl import GRPOTrainer
from trl.rewards import accuracy_reward
dataset = load_dataset("trl-lib/DeepMath-103K", split="train")
trainer = GRPOTrainer(
model="Qwen/Qwen2.5-0.5B-Instruct",
reward_funcs=accuracy_reward,
train_dataset=dataset,
)
trainer.train()[!NOTE]
For reasoning models, use thereasoning_accuracy_reward()function for better results.
`DPOTrainer`
DPOTrainer implements the popular Direct Preference Optimization (DPO) algorithm that was used to post-train Llama 3 and many other models. Here is a basic example of how to use the DPOTrainer:
from datasets import load_dataset
from trl import DPOTrainer
dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train")
trainer = DPOTrainer(
model="Qwen/Qwen3-0.6B",
train_dataset=dataset,
)
trainer.train()`KTOTrainer`
KTOTrainer implements the Kahneman-Tversky Optimization (KTO) algorithm, which aligns models from simple binary (desirable / undesirable) feedback rather than paired preferences. Here is a basic example of how to use the KTOTrainer:
from datasets import load_dataset
from trl import KTOTrainer
dataset = load_dataset("trl-lib/kto-mix-14k", split="train")
trainer = KTOTrainer(
model="Qwen/Qwen3-0.6B",
train_dataset=dataset,
)
trainer.train()`RewardTrainer`
Here is a basic example of how to use the RewardTrainer:
from trl import RewardTrainer
from datasets import load_dataset
dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train")
trainer = RewardTrainer(
model="Qwen/Qwen2.5-0.5B-Instruct",
train_dataset=dataset,
)
trainer.train()Command Line Interface (CLI)
You can use the TRL Command Line Interface (CLI) to quickly get started with post-training methods like Supervised Fine-Tuning (SFT) or Direct Preference Optimization (DPO):
SFT:
trl sft --model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name trl-lib/Capybara \
--output_dir Qwen2.5-0.5B-SFTDPO:
trl dpo --model_name_or_path Qwen/Qwen2.5-0.5B-Instruct \
--dataset_name argilla/Capybara-Preferences \
--output_dir Qwen2.5-0.5B-DPOKTO:
trl kto --model_name_or_path Qwen/Qwen2.5-0.5B-Instruct \
--dataset_name trl-lib/kto-mix-14k \
--output_dir Qwen2.5-0.5B-KTORead more about CLI in the relevant documentation section or use --help for more details.
Development
If you want to contribute to trl or customize it to your needs make sure to read the contribution guide and make sure you make a dev install:
git clone https://github.com/huggingface/trl.git
cd trl/
pip install -e .[dev]Experimental
A minimal incubation area is available under trl.experimental for unstable / fast-evolving features. Anything there may change or be removed in any release without notice.
Example:
from trl.experimental.new_trainer import NewTrainerRead more in the Experimental docs.
Citation
@software{vonwerra2020trl,
title = {{TRL: Transformers Reinforcement Learning}},
author = {von Werra, Leandro and Belkada, Younes and Tunstall, Lewis and Beeching, Edward and Thrush, Tristan and Lambert, Nathan and Huang, Shengyi and Rasul, Kashif and GallouΓ©dec, Quentin},
license = {Apache-2.0},
url = {https://github.com/huggingface/trl},
year = {2020}
}License
This repository's source code is available under the Apache-2.0 License.
Overview (`examples/README.md`)
Examples
Each example lives in its own self-contained folder. The full index and layout conventions are documented at https://huggingface.co/docs/trl/example_overview.
Adding a new example
- Name the folder after the method and what makes the example distinctive: the task (
grpo_wordle), the model (sft_gpt_oss), or the technique (grpo_qlora). Load folder-local assets relative to the script (Path(__file__).parent / ...), not the working directory. - An example tells a story. A bare single-trainer training script is not an example: stable trainers already have one as a CLI command (
trl/scripts), and every trainer's documentation page has a runnable snippet. - Scripts declare their dependencies in a
# /// scriptheader and document the exact run command(s) in the module docstring (python examples/<folder>/<script>.py ...), including multi-GPU variants where relevant. - Add a row to the Index table in
docs/source/example_overview.md, with a Colab badge if the example has a notebook that runs on free Colab.
Overview (`examples/async_grpo_harbor/README.md`)
AsyncGRPO with Harbor
An installed agent runs each task through OpenEnv. TRL trains on the engine's captured token IDs,
logprobs, and loss masks, then updates the same vLLM instance.
Local setup
Use Python 3.12 and this TRL checkout. The typed capture API was merged in
OpenEnv #1280. Install the pinned upstream revision:
git clone https://github.com/huggingface/OpenEnv.git OpenEnv
git -C OpenEnv checkout --detach 86a180ede21e044f7929b9a7783ad83aa67d83a3
pip install -e . trackio datasets ./OpenEnv
export PYTHONPATH="$PWD/OpenEnv/envs${PYTHONPATH:+:$PYTHONPATH}"The source path is required because this revision's environment wheel does not packageharbor_env.harness. Both the core package and the harness above use the same revision.
Run the OpenEnv server with its sandbox credentials, then start vLLM and the trainer using the commands
in async_grpo_harbor.py. The server additionally needs pip install './OpenEnv[harbor]';
the trainer can use an existing server. Use the same model and vLLM endpoint for rollouts and weight updates.
Set the server's MAX_CONCURRENT_ENVS to at least --max-inflight + 1 (9 for the defaults); the factory
keeps one connection for task metadata. The Jobs launcher sets this capacity automatically.
Training saves a checkpoint every 50 steps, keeps the latest three, and saves the final model and
tokenizer in the output directory on successful completion, including runs shorter than 50 steps.
Hugging Face Jobs
launcher.py starts the server, vLLM, and trainer on one two-GPU job. It installs the pinned
OpenEnv runtime and checks out the matching harness source. Pass HF_TOKEN and the sandbox credential
as job secrets. A bucket mounted at /data preserves checkpoints and logs.
Use --train-script-url with the raw URL of the TRL revision being tested before the example is merged.
Use --tunnel-check-s 0 to disable tunnel supervision. A server restart interrupts existing sessions;
subsequent sessions receive the new proxy URL.
Capture contract
- OpenEnv supplies a validated
TrainingTracewith required masks. Prompt and completion IDs come
from the inference engine; TRL does not re-tokenize captured prompts or select agent turns. loss_maskcovers prompt plus completion. Prompt positions are zero; completion positions may mix
zeros and ones. TRL maps the completion span to its genericTurnRecord.output_mask.- For example, prompt
[10, 11], completion[12, 13, 14], and mask[0, 0, 1, 0, 1]train on tokens12and14, while retaining13as context. - The worker constructs the session factory with its sampling policy. Pass a callable such as
partial(HarborSessionFactory, server_url, ...); do not configure sampling separately on the factory.
OpenEnv applies the policy to captured requests. - Invalid captures stop the worker. Transport failures remain unscorable; an agent timeout retains
valid captured turns and the verifier's score.
Execution status and structured tool outcomes are follow-up OpenEnv API work. Today, TRL recognizes
timeouts through TimeoutError and reconstructs conversation and tool diagnostics from captures.
Set agent turn and time budgets on the session factory; the worker only forwards sampling.
Rewritten histories can produce multiple training rows. Token retention does not guarantee equal
rollout weighting or a suitable memory budget for every harness.
Run the packaged contract and reconciliation tests with this checkout on PYTHONPATH:
python -m pytest tests/experimental/test_openenv_tito.py \
tests/experimental/test_async_grpo_trainer.py::TestReconciler -qOverview (`scripts/generate_tiny_models/README.md`)
Tiny model generation
This directory contains one script per tiny model used by the TRL test suite. Each script builds a random-weight, minimally-sized model on top of a real tokenizer/processor and pushes it to the trl-internal-testing organization on the Hub.
Layout
generate_tiny_models/
βββ _common.py # shared helpers (push_to_hub, smoke_test, print_config_diff, ...)
βββ for_causal_lm/ # *ForCausalLM + GPT-2 LM head + small/PEFT variants
βββ for_sequence_classification/ # *ForSequenceClassification (reward models)
βββ for_conditional_generation/ # *ForConditionalGeneration (VLMs + T5 + Bart encoder-decoder)
βββ for_block_diffusion/ # *ForBlockDiffusion (DiffusionGemma)Running
From the repo root, invoke a script by its module path:
python -m scripts.generate_tiny_models.for_causal_lm.qwen3_for_causal_lmEach script:
- Checks that the installed
transformersversion matches the one pinned in the script (fails otherwise). - Calls
set_seedto seed the RNGs that model construction draws from. - Builds the tiny model with random weights.
- Runs
smoke_testβ a minimal forward pass to catch config misspecification and NaNs. - Runs
check_dtype_patternβ reads the reference safetensors header via the Hub API and flags any tensor whose dtype diverges from the reference (catches e.g. fp32 norms kept inside a bf16 checkpoint). - Runs
print_config_diffβ prints every flat-key difference between the reference Hub config and the tiny model's config (for debugging scale-downs). - Pushes the model, tokenizer/processor, generation config, and model card to the Hub in a single commit.
If the repo already exists on the Hub, the push is skipped by default. Pass --create-pr to open a PR against the existing repo instead:
python -m scripts.generate_tiny_models.for_causal_lm.qwen3_for_causal_lm --create-prDirect overwrites of main aren't supported β update via --create-pr and merge the PR on the Hub.
Version pinning
Each script calls check_transformers_version(...), which enforces:
max(version that introduced the model, TRL's transformers floor)By default β check_transformers_version() with no argument β the expected version is the transformers>= floor read from pyproject.toml (currently 4.56.2). Scripts for models introduced after the floor pass an explicit pin, e.g. check_transformers_version("4.57.0") for Qwen3-VL or check_transformers_version("5.6.0") for Gemma4. The check is an exact match via packaging.version.Version; install the matching version before running.
Why exact? transformers is backward-compatible (a checkpoint saved by X loads on any β₯ X) but not forward-compatible. TRL CI runs against the floor, so tiny models must be saved with the oldest version that supports them β any newer save risks using config fields the floor can't parse. The exact-match check prevents accidental drift.
Seeding
Every script calls set_seed() right after the version check, before the model is built. The shared SEED lives in _common.py.
This matters because HfApi.create_commit drops files that are unchanged, so a re-push of an unmodified tiny model should be a no-op and open no PR. Without a seed the weights differ on every run, so model.safetensors always changed and every regeneration produced a diff whether or not anything real had changed. Seeding also makes a config-only change produce a config-only Hub diff.
The call has to come before the model is constructed rather than before init_weights_tiny_model: most scripts never call that helper, and by then the randomness has already happened inside the model's own initialization.
Note that the seed fixes the RNG stream, not the library that produces it. check_transformers_version pins transformers exactly, but nn.init belongs to torch, which is pinned nowhere, so a torch upgrade can still change the weights.
Adding a new tiny model
- Pick the right subfolder based on the model class suffix (
ForCausalLM,ForSequenceClassification,ForConditionalGeneration). - Copy an existing script with the closest shape and adapt it β reference model id, config class, model class, special kwargs.
- If the model requires a release newer than the TRL floor, pass it explicitly:
check_transformers_version("X.Y.Z"). Otherwise leave the call argument-less to default to the floor. - Keep the
set_seed()call that the copied script already has, directly after the version check. - Run it. Inspect the
[smoke_test]and[config_diff]output before letting it push.
Overview (`tests/invariant/README.md`)
Training invariant tests
Catches silent training bugs that don't fail unit tests but shift the training trajectory. Runs on real models, opt-in only.
How it works
Configs are grouped into equivalence classes: configs in the same class must produce the same trajectory (e.g. PDB=1ΓGAS=8 must equal PDB=8ΓGAS=1, FA2 must equal eager). Each class has one canonical config (the first one) that owns the class's reference snapshot. Every config in the class β canonical included β is asserted to match the saved reference. This catches both invariant breakage (a non-canonical config drifting away from the canonical's pinned trajectory) and numerical regressions (the canonical itself drifting from its committed snapshot across versions).
Recording the references is a separate concern from testing them, so it's a separate entry point (python -m tests.invariant.test_invariant).
Each config is a trl <method> CLI invocation with a fixed set of args. The harness shells out (subprocess.run(["trl", method, ...])), the CLI runs end to end, writes trainer_state.json to its --output_dir, and the harness parses the log_history into a Trajectory.
This means the suite tests the actual user-facing entry point, not the Python API. Catches CLI-only bugs (arg parsing, defaults, dispatch) for free. Distributed runs are an additive change: prepend accelerate launch --config_file <strategy>.yaml to the same command.
Scope (initial)
- Trainers:
trl sft,trl dpo - Model:
Qwen/Qwen2.5-0.5B-Instruct(pinned revision) - Equivalence classes:
sft:sft_default(canonical),sft_pdb1_gas8(gradient accumulation),sft_attn_fa2_kernels(FA2 via kernels)dpo:dpo_default(canonical),dpo_pdb1_gas8(gradient accumulation)
- Single GPU, fp32, fixed seed, ~50 optimizer steps.
Other axes (sharding, DDP, more trainers) are deferred and will be additive.
Trajectory
Per optimizer step: loss, grad_norm. One JSON per equivalence class in references/ (sft.json, dpo.json):
{
"config": {"name": "sft_default", "method": "sft", "args": {...}},
"env": {"accelerate": "...", "torch": "...", "transformers": "...", "trl": "...", "gpu": "H100-80GB"},
"steps": [{"step": 1, "loss": 1.234, "grad_norm": 0.567}, ...]
}Comparison
Scalar series with absolute tolerance + zero-mean-residual. The residual check is what flags bugs like GAS-dropping β they show up as a one-sided systematic shift in the loss curve, not as point-wise outliers.
Hardware
Reference snapshots are recorded on H100 80GB, except sft_fa2.json (H200). The recording hardware is in each snapshot's env block.
Running
# test
pytest tests/invariant/ -m invariant
# record references
python -m tests.invariant.test_invariant # all classes
python -m tests.invariant.test_invariant sft # one classSnapshot updates must be justified in the PR description.
Overview (`trl/chat_templates/README.md`)
Chat Templates
Jinja2 chat templates stored here serve two purposes:
- Identity comparison: detecting which model is being used (by comparing
tokenizer.chat_templateagainst known templates) to add the appropriate response schema (add_response_schema) or swap in a training template (get_training_chat_template). - Training patches: modified templates that fix training-specific issues (prefix-preservation for GRPO,
{% generation %}markers for SFT assistant-only loss).
Why prefix-preserving? The GRPO tool call loop extracts tool response formatting tokens by comparing tokenizations with and without tool messages appended (_get_tool_suffix_ids). This requires the chat template to be prefix-preserving: appending messages must not change how earlier messages are rendered.
Why generation-tagged? SFT with assistant_only_loss=True requires the chat template to include {% generation %} / {% endgeneration %} markers around assistant output, so return_assistant_tokens_mask=True can produce correct masks. Most model templates don't include these markers natively.
Why _v2? Identity comparison is exact, so when a model repo changes its chat template, the new revision is stored next to the original as <name>_v2.jinja (then _v3, ...) and both are recognized. The suffix only orders revisions as they are added; each section below says where the revision comes from.
How is drift detected? A weekly CI job runs scripts/check_chat_templates.py, which fails when one of its reference Hub repos ships a chat template that is not stored here. When adding a template for a new model, add the repo it comes from to REPOS in that script.
Original templates
Used for identity comparison only.
`cohere.jinja`
Original Cohere chat template (as shipped by CohereLabs/aya-expanse-8b).
`cohere2.jinja`
Original Cohere2 chat template (as shipped by CohereLabs/tiny-aya-earth).
`deepseek_r1_distill.jinja`
Original DeepSeek-R1-Distill chat template, shipped byte-identically by every distill checkpoint (both the Qwen and the Llama ones). It prefills the assistant turn with <ο½Assistantο½>\n, so the model's output starts inside the reasoning block; response parsing uses deepseek_r1_distill_template. Assistant turns are re-rendered without their reasoning (the template drops everything up to and including ``), and tool_calls are silently dropped whenever content is set, so the template does not support tool calling.
`deepseekv3.jinja`
Original DeepSeek-V3 chat template.
`diffusion_gemma.jinja`
Original DiffusionGemma chat template (as shipped by google/diffusiongemma-26B-A4B-it).
`gemma.jinja`
Original Gemma chat template. Used by both Gemma (v1) and Gemma2, which ship identical templates.
`gemma3.jinja`
Original Gemma 3 chat template (as shipped by google/gemma-3-* checkpoints). Unlike gemma.jinja, this template supports system messages and renders multimodal content blocks (text and image) for VLM training.
`gemma4.jinja`
Original Gemma 4 chat template (as shipped by google/gemma-4-E2B-it and the other checkpoints in that generation, at 6b7e72c). Renders reasoning (<|channel>thought\n...<channel|>), assistant tool_calls (<|tool_call>call:<name>{...}<tool_call|> blocks), and multimodal content blocks. Response parsing uses gemma4_template.
`gemma4_v2.jinja`
Gemma 4 chat template as shipped by google/gemma-4-E2B-it at 4742fe8: the revision the model was released with. Unlike gemma4.jinja, it does not render reasoning in assistant turns that carry tool_calls. Response parsing reuses gemma4_template.
`gemma4_v3.jinja`
Gemma 4 chat template as shipped by google/gemma-4-E2B-it at b446025: the revision between the release and gemma4.jinja. Unlike gemma4_v2.jinja, it renders reasoning in assistant turns that carry tool_calls. Response parsing reuses gemma4_template.
`gemma4_v4.jinja`
Gemma 4 chat template as shipped by google/gemma-4-E2B-it at 905e84b: the revision after gemma4.jinja. Differs from it only in emitting <|image|>, <|audio|> and <|video|> placeholders for multimodal tool responses. Response parsing reuses gemma4_template.
`gemma4_v5.jinja`
Gemma 4 chat template as shipped by google/gemma-4-E2B-it at 8995379: the current revision. Unlike the earlier ones, it also renders the reasoning of a final assistant turn. Response parsing reuses gemma4_template.
`glm4moe.jinja`
Original GLM-4-MoE chat template.
`gptoss.jinja`
Original GPT-OSS chat template.
`idefics3.jinja`
Original Idefics3 chat template (as shipped by HuggingFaceM4/Idefics3-8B-Llama3). Does not support tool calling.
`lfm2.jinja`
Original LFM2 chat template (as shipped by LiquidAI/LFM2-* checkpoints). ChatML-style. Renders tools into the system prompt wrapped in <|tool_list_start|> / <|tool_list_end|>, and wraps tool message content in <|tool_response_start|> / <|tool_response_end|>. It never reads message['tool_calls'] though, so assistant tool calls are silently dropped: the model is trained to emit <|tool_call_start|> / <|tool_call_end|> as plain text inside content.
`lfm2_v2.jinja`
Later revision of the LFM2 chat template (as shipped by LiquidAI/LFM2-1.2B since 40f3da0). Unlike lfm2.jinja, it renders assistant tool_calls, in the same <|tool_call_start|>[name(key=value, ...)]<|tool_call_end|> format as lfm2_2_5.jinja, and tolerates a missing content alongside them. Response parsing reuses lfm2_2_5_template.
`lfm2_2_5.jinja`
Original LFM2.5 chat template (as shipped by LiquidAI/LFM2.5-230M and the other checkpoints in that generation). Unlike lfm2.jinja, it renders assistant tool_calls β as a single <|tool_call_start|>[name(key=value, ...)]<|tool_call_end|> block holding a comma-separated list of Python-style calls β supports a `` block (read off message.thinking), and already carries {% generation %} markers, so no training patch is needed. Response parsing uses lfm2_2_5_template.
`lfm2_2_5_v2.jinja`
Later revision of the LFM2.5 chat template (as shipped by LiquidAI/LFM2.5-230M since 13a5383). Differs from lfm2_2_5.jinja only in reading the `` block off message.thinking, falling back to message.reasoning and message.reasoning_content, and rendering it only when it is a non-empty string. Response parsing reuses lfm2_2_5_template.
`lfm2_2_5_v3.jinja`
Later LFM2.5 chat template (as shipped by LiquidAI/LFM2.5-24B-A2B and LiquidAI/LFM2.5-2.6B). Same tool-call format as lfm2_2_5_v2.jinja, with two rendering differences: the generation prompt pre-fills ``, so the model's output starts inside the reasoning block, and string arguments are escaped (\\, \', \n, \r) while lists render as JSON. Already carries {% generation %} markers, so no training patch is needed. Response parsing uses lfm2_2_5_v3_template.
`lfm2_2_5_vl.jinja`
Original LFM2.5-VL chat template (as shipped by LiquidAI/LFM2.5-VL-3B). Same rendering as lfm2_2_5.jinja β including the {% generation %} markers, so no training patch is needed β plus multimodal content blocks (each image item renders as ``) and more defensive handling of non-string content and flat tool_calls. Response parsing reuses lfm2_2_5_template.
`llama3.jinja`
Original Llama 3 chat template.
`llama3_1.jinja` / `llama3_2.jinja`
Original Llama 3.1 / 3.2 chat templates. Both render tool calls as a single bare JSON object using the key parameters (instead of arguments) and support at most one tool call per assistant turn.
`llava_next.jinja`
Original Llava-Next chat template (as shipped by llava-hf/llava-v1.6-mistral-7b-hf). Renders multimodal content blocks in the LLaVA / Mistral [INST] ... [/INST] format. Does not support tool calling.
`muse_glimmer.jinja`
Original Muse Glimmer chat template (as shipped by meta-models/Muse-Glimmer-30B). Renders the ATEM protocol: one <|start|>assistant to=<recipient><|message|>... block per channel, where the recipient is self for reasoning, a tool name for tool calls (<atem:function_calls> / <atem:invoke> / <atem:parameter> XML), and user for content. The checkpoint ships its own response_template, so add_response_schema leaves it alone.
`nemotron_3_nano.jinja`
Original Nemotron Nano chat template (as shipped by nvidia/NVIDIA-Nemotron-3-Nano-* checkpoints). Renders tool calls in the same Hermes-style <function=...> / `` format as Qwen3.5, so it reuses qwen3_5_schema for response parsing.
`nemotron_3_super.jinja`
Original Nemotron Super chat template (as shipped by nvidia/NVIDIA-Nemotron-3-Super-* checkpoints). Same as nemotron_3_nano.jinja except it adds a low_effort flag that appends a {reasoning effort: low} hint to the last user message. Tool calls use the same Hermes-style format, so it also reuses qwen3_5_schema for response parsing.
`nemotron_3_ultra.jinja`
Original Nemotron Ultra chat template (as shipped by nvidia/NVIDIA-Nemotron-3-Ultra-* checkpoints). Same as nemotron_3_nano.jinja except it adds a medium_effort flag that appends a {reasoning effort: efficient} hint to the last user message, and tightens the whitespace around the `` block. Tool calls use the same Hermes-style format, so it also reuses qwen3_5_schema for response parsing.
`nemotron_3_5_lightning.jinja`
Original Nemotron 3.5 Lightning chat template (as shipped by nvidia/NVIDIA-Nemotron-3.5-Lightning-* checkpoints). Same as nemotron_3_ultra.jinja except it drops the medium_effort flag. Tool calls use the same Hermes-style format, so it also reuses qwen3_5_schema for response parsing.
`phi3.jinja`
Original Phi-3 chat template.
`phi3_5.jinja`
Original Phi-3.5 chat template.
`qwen2_5.jinja`
Original Qwen2.5 chat template.
`qwen2_5_vl.jinja`
Original Qwen2.5-VL chat template. Also matches Qwen2-VL, which ships a byte-identical template. Does not support tool calling.
`qwen3.jinja`
Original Qwen3 chat template.
`qwen3_instruct_2507.jinja`
Original Qwen3-Instruct-2507 chat template (as shipped by Qwen/Qwen3-4B-Instruct-2507). A simpler Qwen3 variant: unlike qwen3.jinja, it has no reasoning_content / `` handling, no multi_step_tool tracking and no enable_thinking flag. Response parsing uses qwen3_template.
`qwen3_vl.jinja`
Original Qwen3-VL chat template. Unlike text-only Qwen3, this template is already prefix-preserving (no conditional thinking blocks), so no training patch is needed.
`qwen3_5_think.jinja` / `qwen3_5_nothink.jinja`
Original Qwen3.5 chat templates. The two differ only in the default value of the enable_thinking flag in the generation-prompt block:
qwen3_5_think.jinjaβ defaults to thinking enabled. Shipped by Qwen3.5-4B and larger.qwen3_5_nothink.jinjaβ defaults to thinking disabled. Shipped by Qwen3.5-2B and smaller.
`qwen3_6.jinja`
Original Qwen3.6 chat template (shared across Qwen3.6-27B, Qwen3.6-35B-A3B, and their FP8 variants). Differs from qwen3_5_think.jinja by adding a preserve_thinking flag and tweaking how non-string tool-call argument values are stringified.
`qwen3_8.jinja`
Original Qwen3.8 chat template (as shipped by Qwen/Qwen3.8-27B and its FP8 variant). Differs from qwen3_6.jinja by adding a reasoning_effort flag (xhigh β the default β medium or low) that prepends a reasoning-effort instruction to the system prompt, and by defaulting preserve_thinking to enabled. Tool calls use the same Hermes-style format, so it also reuses qwen3_5_schema for response parsing.
`smolvlm.jinja`
Original SmolVLM chat template. Also matches SmolVLM2, which ships a byte-identical template. Does not support tool calling.
Training templates
Patched templates that fix training-specific issues. Swapped in at init when tools are enabled (GRPO) or when assistant_only_loss=True (SFT).
`cohere_training.jinja`
Patched Cohere template. Diff vs cohere.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`cohere2_training.jinja`
Patched Cohere2 template. Diff vs cohere2.jinja:
Move the trailing <|END_OF_TURN_TOKEN|> from after the role-dispatch {% endif %} into each role branch, so it can be wrapped together with the assistant content. Wrap the assistant branch (<|START_RESPONSE|>...<|END_RESPONSE|><|END_OF_TURN_TOKEN|>) with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`deepseek_r1_distill_training.jinja`
Patched DeepSeek-R1-Distill template. Diff vs deepseek_r1_distill.jinja:
- Removes the
''split on assistant content, so the reasoning block is always rendered. The original drops everything up to and including `` β fine for inference history, but it would silently remove the reasoning from the SFT training target. - Wraps assistant message output with
{% generation %}/{% endgeneration %}markers for SFT assistant-only loss.
`deepseekv3_training.jinja`
Patched DeepSeek-V3 template. Diff vs deepseekv3.jinja:
- Uses
| tojsonontool['function']['arguments']so thatargumentscan be passed as adict(the documented format per transformers docs). The original template uses raw string concatenation, which crashes on dict inputs. - Wraps assistant message output with
{% generation %}/{% endgeneration %}markers for SFT assistant-only loss.
`diffusion_gemma_training.jinja`
Patched DiffusionGemma template. Diff vs diffusion_gemma.jinja:
Wrap the parts of a model turn that the model emits (thinking channel, tool calls, content, and the closing <turn|>) with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss. Tool responses embedded in the turn come from the environment and stay outside the generation block.
`gemma_training.jinja`
Patched Gemma template, shared by Gemma (v1) and Gemma2 (which ship identical chat templates). Diff vs gemma.jinja:
Split the unified message output line into role-specific branches, so the <start_of_turn>model\n prompt cue sits outside the generation block (it is not generated by the model), while the assistant's content and <end_of_turn>\n (which the model must learn to produce and to stop on) sit inside. Wrap the assistant content with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`gemma3_training.jinja`
Patched Gemma 3 template. Same diff as gemma_training.jinja (split the unified output line into role-specific branches so the <start_of_turn>model\n prompt cue sits outside the generation block, and wrap the assistant content with {% generation %} / {% endgeneration %}), applied to the Gemma 3 base template that supports system messages and multimodal content blocks.
`glm4moe_training.jinja`
Patched GLM-4-MoE template. Diff vs glm4moe.jinja:
Require both and to be present before parsing, to avoid incorrect splitting when the model generates only one tag:
- {%- if '</think>' in content %}
+ {%- if '<think>' in content and '</think>' in content %}Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`gptoss_training.jinja`
Patched GPT-OSS template. Diff vs gptoss.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`idefics3_training.jinja`
Patched Idefics3 template. Diff vs idefics3.jinja:
Split the assistant message into its own branch so the {% generation %} / {% endgeneration %} markers wrap the assistant content. This enables return_assistant_tokens_mask=True to produce correct masks for SFT assistant-only loss.
`lfm2_training.jinja`
Patched LFM2 template. Diff vs lfm2.jinja:
Split the unified message output line into role-specific branches, so the <|im_start|>assistant\n prompt cue sits outside the generation block (it is not generated by the model), while the assistant's content and <|im_end|>\n (which the model must learn to produce and to stop on) sit inside. Wrap the assistant content with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`lfm2_v2_training.jinja`
Patched LFM2 template, later revision. Diff vs lfm2_v2.jinja:
Wrap the assistant output with {% generation %} / {% endgeneration %}, in both the branch that renders tool_calls and the plain-content branch, so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss. The <|im_start|>assistant\n prompt cue stays outside the generation block, while the content, the tool calls and <|im_end|>\n sit inside.
`llama3_training.jinja`
Patched Llama 3 template. Diff vs llama3.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`llava_next_training.jinja`
Patched Llava-Next template. Diff vs llava_next.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`muse_glimmer_training.jinja`
Patched Muse Glimmer template. Diff vs muse_glimmer.jinja:
Wrap the whole assistant branch β every ATEM channel of the turn β with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss. Same treatment as gptoss_training.jinja, which shares the <|start|>assistant generation cue and the one-block-per-channel layout.
`nemotron_3_nano_training.jinja`
Patched Nemotron Nano template. Diff vs nemotron_3_nano.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`nemotron_3_super_training.jinja`
Patched Nemotron Super template. Diff vs nemotron_3_super.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`nemotron_3_ultra_training.jinja`
Patched Nemotron Ultra template. Diff vs nemotron_3_ultra.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`nemotron_3_5_lightning_training.jinja`
Patched Nemotron 3.5 Lightning template. Diff vs nemotron_3_5_lightning.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`phi3_training.jinja`
Patched Phi-3 template. Diff vs phi3.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`phi3_5_training.jinja`
Patched Phi-3.5 template. Diff vs phi3.5.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`qwen2_5_training.jinja`
Patched Qwen2.5 template. Diff vs qwen2_5.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`qwen2_5_vl_training.jinja`
Patched Qwen2.5-VL template (also used for Qwen2-VL, which ships a byte-identical template). Diff vs qwen2_5_vl.jinja:
Split the assistant message into its own branch so the {% generation %} / {% endgeneration %} markers wrap the assistant content. This enables return_assistant_tokens_mask=True to produce correct masks for SFT assistant-only loss.
`qwen3_training.jinja`
Patched Qwen3 template. Diff vs qwen3.jinja:
Require both and to be present before parsing, to avoid incorrect splitting when the model generates only one tag:
- {%- if '</think>' in content %}
+ {%- if '<think>' in content and '</think>' in content %}Always include the thinking block regardless of message position. The original conditionally omits it based on loop.last, which changes the assistant rendering when a tool message is appended β breaking prefix-preservation:
- {%- if loop.index0 > ns.last_query_index %}
- {%- if loop.last or (not loop.last and reasoning_content) %}
- {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content.strip('\n') + '\n</think>\n\n' + content.lstrip('\n') }}
- {%- else %}
- {{- '<|im_start|>' + message.role + '\n' + content }}
- {%- endif %}
- {%- else %}
- {{- '<|im_start|>' + message.role + '\n' + content }}
- {%- endif %}
+ {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content.strip('\n') + '\n</think>\n\n' + content.lstrip('\n') }}Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`qwen3_instruct_2507_training.jinja`
Patched Qwen3-Instruct-2507 template. Diff vs qwen3_instruct_2507.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`qwen3_vl_training.jinja`
Patched Qwen3-VL template. Diff vs qwen3_vl.jinja:
Wrap assistant message output (both content and tool_calls) with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`qwen3_5_think_training.jinja` / `qwen3_5_nothink_training.jinja`
Patched Qwen3.5 templates. Same diff as qwen3_training.jinja (require both and before parsing, drop the loop.index0 > ns.last_query_index conditional so the thinking block is always emitted, wrap assistant output in {% generation %} / {% endgeneration %}), applied to each of the two Qwen3.5 base templates. The two training variants differ only in the default value of the enable_thinking flag, inherited from their respective base templates.
`qwen3_6_training.jinja`
Patched Qwen3.6 template. Same diff as qwen3_training.jinja (require both and before parsing, drop the loop.index0 > ns.last_query_index conditional so the thinking block is always emitted, wrap assistant output in {% generation %} / {% endgeneration %}), applied to the Qwen3.6 base template.
`qwen3_8_training.jinja`
Patched Qwen3.8 template. Diff vs qwen3_8.jinja: drop the preserve_thinking / loop.index0 > ns.last_query_index conditional so the thinking block is always emitted (prefix-preservation holds even when the caller passes preserve_thinking=False), and wrap assistant output in {% generation %} / {% endgeneration %} for SFT assistant-only loss.
`smolvlm_training.jinja`
Patched SmolVLM template (also used for SmolVLM2, which ships a byte-identical template). Diff vs smolvlm.jinja:
Split the assistant message into its own branch so the {% generation %} / {% endgeneration %} markers wrap the assistant content. This enables return_assistant_tokens_mask=True to produce correct masks for SFT assistant-only loss.
Overview (`examples/grpo_harbor/harnesses/bash/README.md`)
`bash` harness
The minimal harness: a single shell tool. This is the built-in HarborBashEnv (trl.experimental.harbor.HarborBashEnv); this folder just documents it and re-exports it as BashEnv.
Tools
| Tool | Signature | What it does |
|---|---|---|
bash |
bash(command: str) -> str |
Run a shell command in the sandbox; returns combined stdout+stderr (truncated to 8k). Non-stateful between calls. |
Submission
No submit tool β write the answer to /workdir/answer.txt, e.g. echo -n "<value>" > /workdir/answer.txt. The task's verifier reads that file.
Use it
from trl.experimental.harbor import HarborSpec
spec = HarborSpec(dataset, agent="bash") # built-in nameOverview (`examples/grpo_harbor/harnesses/jupyter/README.md`)
`jupyter` harness
A stateful Python kernel harness. Variables, imports, and side-effects persist across cells, so the model builds up state like a notebook. Implemented by JupyterEnv in env.py.
How it works
On reset, the harness (_setup) uploads two helpers into the sandbox and starts a tiny kernel server:
kernel_server.pyβ a local HTTP server (127.0.0.1:8765) holding one persistent Python kernel.run_cell.pyβ sends a base64-encoded cell to the kernel server and prints its JSON result.
Each add_and_execute_code_cell call runs python3 /opt/run_cell.py against that kernel.
Tools
| Tool | Signature | What it does |
|---|---|---|
add_and_execute_code_cell |
add_and_execute_code_cell(code: str) -> str |
Execute Python in the stateful kernel; state persists across calls. Use for all computation. |
execute_shell_command |
execute_shell_command(command: str) -> str |
Run a shell command (pip install, ls, β¦). Not stateful with the Python kernel. |
Submission
Write the answer to /workdir/answer.txt β e.g. add_and_execute_code_cell(code="open('/workdir/answer.txt','w').write(str(ans))").
Use it
from trl.experimental.harbor import HarborSpec
spec = HarborSpec(dataset, agent="examples/grpo_harbor/harnesses/jupyter/env.py:JupyterEnv")Overview (`examples/grpo_harbor/harnesses/terminal_notes/README.md`)
`terminal_notes` harness
A richer shell harness: 6 shell tools (including background processes) plus a 4-tool persistent note toolkit. Implemented by TerminalNotesEnv in env.py.
Tools
Shell (backed by the sandbox)
| Tool | Signature | What it does |
|---|---|---|
shell_exec |
shell_exec(command: str, blocking: bool = True) -> str |
Run a command. Blocking β combined stdout+stderr; non-blocking β detach and return a PID. |
shell_write_content_to_file |
shell_write_content_to_file(path: str, content: str) -> str |
Write content to path (used to commit /workdir/answer.txt). |
shell_write_to_process |
shell_write_to_process(pid: str, content: str) -> str |
Write to a background process's stdin. |
shell_view |
shell_view(pid: str) -> str |
Read the captured stdout of a background process. |
shell_wait |
shell_wait(pid: str) -> str |
Wait (β€5 min) for a background process to exit, then return its output. |
shell_kill_process |
shell_kill_process(pid: str) -> str |
SIGKILL a background process. |
Notes (in-env state, persist across turns of a rollout)
| Tool | Signature | What it does |
|---|---|---|
create_note |
create_note(title: str, content: str) -> str |
Create a note. |
append_note |
append_note(title: str, content: str) -> str |
Append to an existing note. |
read_note |
read_note(title: str) -> str |
Read a note's content. |
list_note |
list_note() -> str |
List note titles + sizes. |
Unlike the original SETA agent, notes are not auto-injected into the prompt each turn (TRL owns the prompt under
environment_factory); recall them on demand withread_note/list_note.
Submission
Write the answer to /workdir/answer.txt, e.g. shell_write_content_to_file(path="/workdir/answer.txt", content="<value>").
Use it
from trl.experimental.harbor import HarborSpec
spec = HarborSpec(dataset, agent="examples/grpo_harbor/harnesses/terminal_notes/env.py:TerminalNotesEnv")Source Example Overview (`docs/source/example_overview.md`)
Examples
The examples/ directory contains a collection of self-contained examples that demonstrate how to use the TRL library for various applications. Each example lives in its own folder named after the method and the task it demonstrates (e.g. grpo_wordle, sft_gpt_oss), and holds everything the example needs: scripts, notebooks, prompts, chat templates, and evaluation code.
Basic single-trainer training scripts are not examples: they live in trl/scripts and are exposed through the command line interface (trl sft, trl dpo, trl grpo, β¦). Each trainer's documentation page also contains a complete runnable snippet.
Shared resources sit at the root of examples/:
examples/accelerate_configs: π€ Accelerate configuration files for multi-GPU, DeepSpeed ZeRO, FSDP, and context-parallel setups, used by many examples.examples/datasets: the scripts used to generate thetrl-libdatasets used across the examples.
Getting Started
Install TRL and additional dependencies as follows:
pip install --upgrade trl[quantization]Check for additional optional dependencies here. Notebook-based examples are self-contained and can run on free Colab; script-based examples run on single-GPU, multi-GPU, or DeepSpeed setups (see Distributed Training below).
Index
| Example | Description | Open in Colab |
|---|---|---|
async_distillation_math |
Async on-policy distillation on GSM8K with [experimental.async_distillation.AsyncDistillationTrainer]: the teacher is served over HTTP with vLLM, including a multi-teacher (MOPD) math + code variant. |
|
async_grpo_harbor |
AsyncGRPO training on a Harbor task suite through any installed coding agent (mini-swe-agent, opencode, claude-code, ...) served over OpenEnv: the agent owns its own loop in a sandbox and TRL trains on the token ids and logprobs recorded by the capture proxy. |
|
async_grpo_math |
Asynchronous GRPO on GSM8K with [experimental.async_grpo.AsyncGRPOTrainer], decoupling generation (vLLM server) from training, including a LoRA variant that syncs only the adapter to the vLLM server. |
|
dpo_reduce_hallucinations |
DPO fine-tuning of a Vision Language Model to reduce hallucinations using the openbmb/RLAIF-V-Dataset dataset. | |
gold_chatbot_arena |
General Online Logit Distillation (GOLD) of a Qwen2 teacher into a Llama 3.2 student (cross-tokenizer) on chatbot_arena_completions with [experimental.gold.GOLDTrainer], with full-training and LoRA variants. |
|
gold_qwen3_vl |
General Online Logit Distillation (GOLD) of Qwen3-VL-8B into smaller VLM students with [experimental.gold.GOLDTrainer], covering same-family (JSD loss) and cross-family (ULD loss) distillation. |
|
grpo_2048 |
GRPO with tool calling to teach a model to play the 2048 game. | |
grpo_browsergym |
GRPO with the BrowserGym OpenEnv environment, with LLM and VLM variants. | |
grpo_carla |
GRPO with the CARLA autonomous-driving OpenEnv environment, with LLM and VLM variants (multimodal camera-image tool responses). | |
grpo_catch |
GRPO with the Catch (OpenSpiel) OpenEnv environment. | |
grpo_continuous_batching |
GRPO with transformers' continuous batching engine for faster generation on large batches with variable completion lengths. | |
grpo_echo |
Minimal GRPO training with the Echo OpenEnv environment. | |
grpo_harbor |
GRPO training against a Harbor task suite with a pluggable base agent (bash / jupyter / terminal_notes harnesses). See the Harbor Integration guide. |
|
grpo_ministral3_vl |
GRPO Ministral 3 with QLoRA on free Colab. | |
grpo_multi_env |
Multi-environment GRPO training: Wordle + Catch OpenEnv environments in the same training run. | |
grpo_qlora |
GRPO using QLoRA on free Colab. | |
grpo_qwen3_vl |
GRPO Qwen3-VL with QLoRA on free Colab. | |
grpo_rnj_1_instruct |
GRPO on rnj-1-instruct with QLoRA on Colab to add reasoning capabilities. | |
grpo_seta |
GRPO training against the SETA ORS environment on the openreward.ai catalog. See the OpenReward Integration guide. | |
grpo_sql_agent |
GRPO to train an agent that answers questions by querying a SQL database (script and notebook; not runnable on free Colab due to OOM). | |
grpo_sudoku |
GRPO to play Sudoku on an OpenEnv environment (script and notebook). | |
grpo_visual_math |
GRPO fine-tuning of a multimodal model for reasoning using the lmms-lab/multimodal-open-r1-8k-verified dataset. | |
grpo_wordle |
GRPO to play Wordle (TextArena) on an OpenEnv environment (script and notebook). | |
gspo_math |
GSPO via the [GRPOTrainer] for math reasoning on the AI-MO/NuminaMath-TIR dataset. |
|
gspo_visual_math |
GSPO via the [GRPOTrainer] to fine-tune a multimodal model for reasoning using the lmms-lab/multimodal-open-r1-8k-verified dataset. |
|
mpo_visual_preferences |
MPO via the [DPOTrainer] to align a multimodal model based on preferences using the HuggingFaceH4/rlaif-v_formatted dataset and a set of loss weights. |
|
online_dpo_visual_math |
Online DPO fine-tuning of a Vision Language Model with [experimental.online_dpo.OnlineDPOTrainer]. |
|
rloo_math |
RLOO with the [RLOOTrainer] for math reasoning on the AI-MO/NuminaMath-TIR dataset with vLLM. |
|
rloo_visual_math |
RLOO fine-tuning of a multimodal model for reasoning using the lmms-lab/multimodal-open-r1-8k-verified dataset. | |
sdft_privileged_context |
Self-distillation fine-tuning with [experimental.sdft.SDFTTrainer], distilling privileged (teacher-only) context into the model. |
|
sdpo_math |
SDPO with [experimental.sdpo.SDPOTrainer] using verifiable math rewards and optional environment feedback on openai/gsm8k. |
|
sft_beam |
LoRA SFT of reflection/Beam-1.0-501B-A23B on one 8xB300 node with [SFTTrainer], sharded at load time by transformers' DistributedConfig (expert parallelism with token dispatch). |
|
sft_diffusion_gemma |
SFT of the DiffusionGemma block-diffusion language model on GSM8K by extending the [SFTTrainer] with a block-diffusion objective. |
|
sft_gemma3 |
SFT of Gemma 3 on the Codeforces COTS dataset. | |
sft_gemma3_vision |
SFT of Gemma 3 on vision to text tasks. | |
sft_gpt_oss |
SFT of openai/gpt-oss-20b. | |
sft_ministral3_vl |
SFT Ministral 3 with QLoRA on free Colab. | |
sft_nemotron_3 |
SFT of NVIDIA Nemotron 3 models (script and LoRA notebook). | |
sft_qlora |
SFT using QLoRA on free Colab. | |
sft_qwen3_8b_1m_context |
SFT of Qwen3-8B on 1,048,576-token sequences with context parallelism, on one 8xH100 node. | |
sft_qwen3_vl |
SFT Qwen3-VL with QLoRA on free Colab. | |
tail_sft_gsm8k |
TailSFT on GSM8K by extending [SFTTrainer] with initial-policy loss filtering. |
|
sft_muse_spark_open |
LoRA SFT of meta-models/Muse-Spark-Open-816B-A42B on 8 nodes with [SFTTrainer], sharded at load time by transformers' DistributedConfig (expert parallelism with token dispatch). |
|
sft_tool_calling |
Teaching tool calling to a model without native tool-calling support using SFT with QLoRA (script, chat template, and notebook). | |
sft_visual_chat |
SFT of a Vision Language Model in a chat setting. Only tested with LLaVA 1.5, LLaVA 1.6, and Llama-3.2-11B-Vision-Instruct; users may see unexpected behaviour in other model architectures. | |
tpo_ultrafeedback |
Triple Preference Optimization with [experimental.tpo.TPOTrainer] using the tpo-alignment/triple-preference-ultrafeedback-40K dataset. |
Distributed Training
You can run the example scripts on multiple GPUs with π€ Accelerate:
accelerate launch --config_file=examples/accelerate_configs/multi_gpu.yaml --num_processes {NUM_GPUS} path_to_script.py --all_arguments_of_the_scriptFor DeepSpeed ZeRO-{1,2,3}:
accelerate launch --config_file=examples/accelerate_configs/deepspeed_zero{1,2,3}.yaml --num_processes {NUM_GPUS} path_to_script.py --all_arguments_of_the_scriptAdjust NUM_GPUS and --all_arguments_of_the_script as needed.
Source Experimental Overview (`docs/source/experimental_overview.md`)
Experimental
This directory contains a minimal, clearly separated space for fast iteration on new ideas.
[!WARNING]
Stability contract: Anything undertrl.experimentalmay change or be removed in any release (including patch versions) without prior deprecation. Do not rely on these APIs for production workloads.
Promotion Path (Simple)
- Prototype outside the main repo: Start development in your own fork or a separate repository to iterate quickly.
- Experimental inclusion: Once itβs ready for early users, move the idea into
trl.experimental.<feature>. - Improve: Add tests, a short doc/example, and demonstrate the usage.
- Promote: Once the API proves stable and there is clear interest or adoption from the community, move it into
trl.<feature>(stable module).
Removal
Experimental is a staging area, not long-term storage: a feature is either promoted or removed. Removal needs no deprecation cycle, as stated in the stability contract above. The code remains available in the git history, and the paper index entry stays, pointing at the last release that shipped the implementation.
No threshold triggers a removal. It is a judgment call, made in the removal pull request with the numbers in front of everyone. What that judgment weighs:
- Usage. Telemetry, Hub tags, and replies on the pull request or on a discussion thread. None of them is conclusive on its own, but all of them quiet is a strong signal.
- External issues and pull requests that come from running the feature, as opposed to reports produced by scanning the code.
- Whether a stable trainer already covers it. If one does, that outweighs usage.
- Cost. CI time, maintenance, triage load.
- Downstream consumers. Libraries that import it.
- Owner. Someone who wants it promoted and is willing to do the work.
- Age. Whether it has been available long enough for the community to find it and try it.
FAQ
Why not just use branches?
Because branches are not shipped to users; experimental code inside the package lets early adopters try things and give feedback.
Can these APIs change or vanish without warning?
Yes. Anything inside trl.experimental can change or disappear in any release.
Should I use this in production?
Only if you are fine with updating your code quickly when things change.
Will maintainers promptly fix issues in trl.experimental?
Not necessarily. The experimental module is a playground for new ideas, and maintainers may not prioritize bug fixes or feature requests there. Issues may remain unresolved until (or unless) the feature graduates to the stable API.
I contributed a paper implementation. Can it be removed?
Yes. Anything under trl.experimental can be removed, paper implementations included. If that happens, the paper index entry stays and points at the last release that shipped the code.
How to silence the runtime notice?
Use: export TRL_EXPERIMENTAL_SILENCE=1.
Source Index (`docs/source/index.md`)
TRL - Transformers Reinforcement Learning
TRL is a full stack library where we provide a set of tools to train transformer language models with methods like Supervised Fine-Tuning (SFT), Group Relative Policy Optimization (GRPO), Direct Preference Optimization (DPO), Reward Modeling, and more.
The library is integrated with π€ transformers.
π What's New
π Training on AMD GPUs: TRL's trainers now run on AMD Instinct GPUs through PyTorch's ROCm build.
π₯ Beam: Fine-tune Beam with [SFTTrainer] on a single node of 8 B300s. See the LoRA example.
π₯ Muse Spark Open: Fine-tune Muse Spark Open with [SFTTrainer], sharded across your GPUs at load time. See the 8-node LoRA example.
π Training on AWS Trainium: TRL's trainers now run on AWS Trainium through TorchNeuron, PyTorch's native Neuron backend.
Taxonomy
Below is an overview of TRL trainers, organized by maturity and method type.
Online methods
Reward modeling
Offline methods
Knowledge distillation
Experimental
Online methods
Offline methods
Knowledge distillation
You can also explore TRL-related models, datasets, and demos in the TRL Hugging Face organization.
Learn
Learn post-training with TRL and other libraries in π€ smol course.
Contents
The documentation is organized into the following sections:
- Getting Started: installation and quickstart guide.
- Conceptual Guides: dataset formats, training FAQ, and understanding logs.
- How-to Guides: reducing memory usage, speeding up training, distributing training, etc.
- Integrations: DeepSpeed, PEFT, etc.
- Examples: example overview, community tutorials, etc.
- API: trainers, utils, etc.
Blog posts
<a class="!no-underline border dark:border-gray-700 p-5 rounded-lg shadow hover:shadow-lg" href="https://huggingface.co/blog/delta-weight-sync">
Published on May 27, 2026
Shipping a Trillion Parameters With a Hub Bucket: Delta Weight Sync in TRL
</a>
<a class="!no-underline border dark:border-gray-700 p-5 rounded-lg shadow hover:shadow-lg" href="https://huggingface.co/blog/trl-v1">
Published on March 31, 2026
TRL v1: Post-Training Library That Holds When the Field Invalidates Its Own Assumptions
</a>
<a class="!no-underline border dark:border-gray-700 p-5 rounded-lg shadow hover:shadow-lg" href="https://huggingface.co/blog/async-rl-training-landscape">
Published on March 10, 2026
Keep the Tokens Flowing: Lessons from 16 Open-Source RL Libraries
</a>
<a class="!no-underline border dark:border-gray-700 p-5 rounded-lg shadow hover:shadow-lg" href="https://huggingface.co/blog/ulysses-sp">
Published on March 9, 2026
Ulysses Sequence Parallelism: Training with Million-Token Contexts
</a>
<a class="!no-underline border dark:border-gray-700 p-5 rounded-lg shadow hover:shadow-lg" href="https://huggingface.co/blog/openenv">
Published October 23, 2025
Building the Open Agent Ecosystem Together: Introducing OpenEnv
</a>
<a class="!no-underline border dark:border-gray-700 p-5 rounded-lg shadow hover:shadow-lg" href="https://huggingface.co/blog/trl-vlm-alignment">
Published on August 7, 2025
Vision Language Model Alignment in TRL β‘οΈ
</a>
<a class="!no-underline border dark:border-gray-700 p-5 rounded-lg shadow hover:shadow-lg" href="https://huggingface.co/blog/vllm-colocate">
Published on June 3, 2025
NO GPU left behind: Unlocking Efficiency with Co-located vLLM in TRL
</a>
<a class="!no-underline border dark:border-gray-700 p-5 rounded-lg shadow hover:shadow-lg" href="https://huggingface.co/blog/liger-grpo">
Published on May 25, 2025
π― Liger GRPO meets TRL
</a>
<a class="!no-underline border dark:border-gray-700 p-5 rounded-lg shadow hover:shadow-lg" href="https://huggingface.co/blog/open-r1">
Published on January 28, 2025
Open-R1: a fully open reproduction of DeepSeek-R1
</a>
<a class="!no-underline border dark:border-gray-700 p-5 rounded-lg shadow hover:shadow-lg" href="https://huggingface.co/blog/dpo_vlm">
Published on July 10, 2024
Preference Optimization for Vision Language Models with TRL
</a>
<a class="!no-underline border dark:border-gray-700 p-5 rounded-lg shadow hover:shadow-lg" href="https://huggingface.co/blog/putting_rl_back_in_rlhf_with_rloo">
Published on June 12, 2024
Putting RL back in RLHF
</a>
<a class="!no-underline border dark:border-gray-700 p-5 rounded-lg shadow hover:shadow-lg" href="https://huggingface.co/blog/unsloth-trl">
Published on January 10, 2024
Make LLM Fine-tuning 2x faster with Unsloth and π€ TRL
</a>
<a class="!no-underline border dark:border-gray-700 p-5 rounded-lg shadow hover:shadow-lg" href="https://huggingface.co/blog/trl-ddpo">
Published on September 29, 2023
Finetune Stable Diffusion Models with DDPO via TRL
</a>
<a class="!no-underline border dark:border-gray-700 p-5 rounded-lg shadow hover:shadow-lg" href="https://huggingface.co/blog/dpo-trl">
Published on August 8, 2023
Fine-tune Llama 2 with DPO
</a>
<a class="!no-underline border dark:border-gray-700 p-5 rounded-lg shadow hover:shadow-lg" href="https://huggingface.co/blog/stackllama">
Published on April 5, 2023
StackLLaMA: A hands-on guide to train LLaMA with RLHFPublished on March 9, 2023
Fine-tuning 20B LLMs with RLHF on a 24GB consumer GPU
</a>
<a class="!no-underline border dark:border-gray-700 p-5 rounded-lg shadow hover:shadow-lg" href="https://huggingface.co/blog/rlhf">
Published on December 9, 2022
Illustrating Reinforcement Learning from Human Feedback
</a>Talks
<a class="!no-underline border dark:border-gray-700 p-5 rounded-lg shadow hover:shadow-lg" href="https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/Fine%20tuning%20with%20TRL%20(Oct%2025).pdf">
Talk given on October 30, 2025
Fine tuning with TRL
</a>Source Paper Index (`docs/source/paper_index.md`)
Paper Index
[!TIP]
Each paper below links to its Hugging Face paper page. You can also read any of them from the terminal with thehfCLI, e.g.hf papers read 2402.03300β particularly handy for coding agents.
Group Relative Policy Optimization
Papers relating to the [GRPOTrainer].
DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models
π Paper: https://huggingface.co/papers/2402.03300
Introduces Group Relative Policy Optimization (GRPO) and shows strong math-reasoning gains from math-centric pretraining plus group-relative PPO-style optimization. Used in TRL via [GRPOTrainer].
from trl import GRPOConfig, GRPOTrainer
# The paper doesn't specify its hyperparameters, so here we provide hyperparameters from "DeepSeek-R1 incentivizes reasoning in LLMs through reinforcement learning" instead.
training_args = GRPOConfig(
loss_type="grpo",
beta=0.001, # "the KL coefficient to 0.001"
epsilon=10.0, # "the GRPO clip ratio Ο΅ to 10"
num_generations=16, # "For each question, we sample 16 outputs..."
max_completion_length=32_768, # "...with a maximum length of 32,768"
steps_per_generation=16, # "To accelerate training, each rollout generates 8,192 outputs, which are randomly split into 16 minibatches"
# "resulting in a training batch size of 512". One way to achieve this setting with 1 device is per_device_train_batch_size=4, gradient_accumulation_steps=128
per_device_train_batch_size=4,
gradient_accumulation_steps=128,
)
trainer = GRPOTrainer(
...,
args=training_args,
)DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning
π Paper: https://huggingface.co/papers/2501.12948
DeepSeek-R1 achieves reasoning performance comparable to OpenAI-o1 through a multi-stage pipeline that transitions from pure reinforcement learning (RL) to a refined, human-aligned model. Unlike its predecessor, DeepSeek-R1-Zero, which used pure RL on a base model, R1 follows a structured four-stage evolution:
- Cold Start: The base model is fine-tuned on a small set of high-quality, long Chain-of-Thought (CoT) data to provide a stable starting point.
- Reasoning-Oriented RL: Large-scale RL is applied to enhance performance in math, coding, and logic, using rule-based rewards and a language consistency reward to reduce language mixing.
- Rejection Sampling & SFT: The RL checkpoint generates 600k reasoning samples via rejection sampling, which are combined with 200k non-reasoning (general) samples to create a new dataset for a second round of Supervised Fine-Tuning.
- RL for all Scenarios: A final RL stage aligns the model with human preferences (helpfulness and harmlessness) across all domains while maintaining reasoning strength.
Distillation: Empowering Small Models
A key contribution of the paper is demonstrating that reasoning patterns can be distilled from a large model (DeepSeek-R1) into smaller dense models (e.g., Qwen and Llama series). Distillation was found to be more effective for small models than training them with pure RL from scratch.
You can use the GRPOTrainer to replicate the reasoning-heavy stages of this pipeline.
from trl import GRPOConfig, GRPOTrainer
# Example configuration for a reasoning-oriented GRPO stage
# Based on the Open-R1 recipe for Qwen-7B
training_args = GRPOConfig(
learning_rate=4.0e-5,
max_completion_length=32768, # Support for long Chain-of-Thought
num_generations=16, # Sample 16 outputs per prompt for group relative advantage
beta=0.001, # KL coefficient
use_vllm=True, # Use vLLM backend for accelerated rollout generation
)
trainer = GRPOTrainer(
model=model,
args=training_args,
train_dataset=dataset,
reward_funcs=[accuracy_reward, format_reward], # R1-Zero used rule-based rewards
)
trainer.train()Group Sequence Policy Optimization
π Paper: https://huggingface.co/papers/2507.18071
GSPO is a GRPO variant that computes importance sampling weights at the sequence level instead of per-token. To reproduce the paper's setting, use this configuration:
from trl import GRPOConfig
training_args = GRPOConfig(
importance_sampling_level="sequence",
loss_type="grpo",
beta=0.0, # GSPO set KL regularization to zero: https://github.com/volcengine/verl/pull/2775#issuecomment-3131807306
epsilon=3e-4, # GSPO paper (v2), section 5.1
epsilon_high=4e-4, # GSPO paper (v2), section 5.1
gradient_accumulation_steps=1,
steps_per_generation=4, # partition rollout batch into 4 mini-batches. GSPO paper (v2), section 5.1. Must be 4 times gradient_accumulation_steps
)Note that this method only has an effect when training goes slightly off-policyβfor example, when steps_per_generation > gradient_accumulation_steps or num_iterations > 1. Otherwise, it is effectively equivalent to no modification.
TRL shipped an experimental GSPO-token implementation up to v1.13; it is no longer part of the library and remains available in the git history. It differed from the sequence-level objective above only when the advantage varies with \( t \), which TRL does not currently produce.
Policy ratio: GRPO vs. GSPO
In GSPO, the policy ratio is defined at the sequence-level. In other words, it is the ratio between the probability of the current policy generating a sequence over the old policy generating that same sequence.
The sequence likelihood is defined as:
$$
\pi_\theta (o_i | q) = \prod_{t=1}^{|o_i|} \pi_\theta (o_{i,t} | q, o_{i, < t} ),
$$
where \( \pi_\theta \) is the policy \( \pi \) with parameters \(\theta\), \( o_i \) is the \( i \)-th output sequence \( o \) and \(o_{i,t}\) is the \( t \)-th token in this sequence, \( q \) is the input query. The sequence likelihood ratio \( s_i (\theta) \) is defined as:
$$
s_i (\theta) = \left(\frac{\pi_\theta (o_i | q)}{\pi_{\theta_{old}} (o_i | q)} \right)^{\frac{1}{|o_i|}}
$$
The exponent \( \frac{1}{|o_i|} \) represents a sequence-length normalization, minimizing the influence of sequence length in sequence likelihood. In other terms, it computes the geometric mean of token probabilities, ensuring a fair comparison across sequences of varying lengths.
While GSPO defines the policy ratio at the sequence level, GRPO operates at the token level. Specifically, GRPO computes an importance ratio for each token in the sequence:
$$
w_{i,t}(\theta) = \frac{\pi_\theta (o_{i,t} | q, o_{i,< t})}{\pi_{\theta_{\text{old}}} (o_{i,t} | q, o_{i,< t})}
$$
This token-level ratio is then combined with a shared advantage \( \hat{A}_i \), and the GRPO objective clips and optimizes each token independently across the sequence.
Geometric-Mean Policy Optimization
π Paper: https://huggingface.co/papers/2507.20673
Geometric-Mean Policy Optimization (GMPO) is a GRPO variant that maximizes the geometric mean of the token-level importance ratios instead of the arithmetic mean. The geometric mean is far less sensitive to outlier ratios, so the policy update is more stable and tolerates a much wider clipping range. Clipping is applied per token, in log space, and is one-sided per the advantage sign (the standard PPO trust region) β crucially, before the geometric mean is taken. This is what distinguishes GMPO from GSPO (importance_sampling_level="sequence"), which clips the sequence-level ratio after averaging.
The GMPO objective replaces GRPO's per-token arithmetic mean with the geometric mean of the (clipped) token ratios:
$$
\mathcal{J}{\text{GMPO}}(\theta) = \mathbb{E}{q, {o_i}} \left[ \frac{1}{G} \sum_{i=1}^{G} \left( \prod_{t=1}^{|o_i|} \min\left[ w_{i,t}(\theta)^{\operatorname{sgn}(\hat{A}i)},\ \operatorname{clip}\left(w{i,t}(\theta)^{\operatorname{sgn}(\hat{A}_i)}, \epsilon_1, \epsilon_2\right) \right]^{\operatorname{sgn}(\hat{A}_i)} \right)^{\frac{1}{|o_i|}} \hat{A}_i \right]
$$
where \( w_{i,t}(\theta) \) is the per-token importance ratio. In practice the product and clipping are computed in log space for numerical stability, and the clip range \( (\epsilon_1, \epsilon_2) = (e^{-0.4}, e^{0.4}) \) is markedly wider than GRPO/DAPO to encourage exploration.
TRL provides an experimental implementation, see Experimental - GMPO:
from trl.experimental.gmpo import GMPOConfig, GMPOTrainer
training_args = GMPOConfig(
epsilon=0.4, # log-space clip range -> ratios clipped to (exp(-0.4), exp(0.4)); paper, Sec. 4
beta=0.0,
)DAPO: An Open-Source LLM Reinforcement Learning System at Scale
π Paper: https://huggingface.co/papers/2503.14476
The DAPO algorithm includes 5 key components:
- Overlong Filtering
- Clip-Higher
- Soft Overlong Punishment
- Token-level Loss
- Dynamic Sampling (β οΈ Not supported in TRL)
To reproduce the paper's setting, use this configuration:
from trl import GRPOConfig, GRPOTrainer
training_args = GRPOConfig(
# Overlong Filtering
mask_truncated_completions=True,
# Token-level Loss
loss_type="dapo",
# Clip-Higher
epsilon_high=0.28, # DAPO paper: section 4.1
epsilon=0.2, # DAPO paper: section 4.1
# Other parameters used
per_device_train_batch_size=512, # mini-batch size for training in the paper, DAPO paper: section 4.1
num_generations=16, # number of sample responses in the paper, DAPO paper: section 4.1
max_completion_length=20480, # maximum number of tokens for generation in the paper, DAPO paper: section 4.1
beta=0.0, # section 2.3, DAPO paper
)
# Soft Overlong Punishment
sop_reward = get_soft_overlong_punishment(max_completion_len=20480, soft_punish_cache=4096) # DAPO paper: section 4.1
trainer = GRPOTrainer(
...,
args=training_args,
reward_funcs=[..., sop_reward],
)Demystifying Long Chain-of-Thought Reasoning in LLMs
π Paper: https://huggingface.co/papers/2502.03373
This paper studies long chain-of-thought RL and introduces two complementary rule-based rewards:
- A cosine length-scaled reward ([
~rewards.get_cosine_scaled_reward], Appendix C.1) that scales the correctness reward by completion length: a correct completion is rewarded more when it is shorter, while a wrong completion is penalized less when it is longer (preserving exploration). - An n-gram repetition penalty ([
~rewards.get_repetition_penalty_reward], Appendix C.2) that discourages the degenerate, repetitive completions that emerge as a reward-hacking strategy under length shaping.
The two are designed to be used together:
from trl import GRPOTrainer
from trl.rewards import get_cosine_scaled_reward, get_repetition_penalty_reward
# max_len should match the generation budget (in tokens)
cosine_scaled_reward = get_cosine_scaled_reward(max_len=4096)
# Penalize repeated 3-grams, down to -1.0 for fully repetitive completions
repetition_penalty = get_repetition_penalty_reward(ngram_size=3, max_penalty=-1.0)
trainer = GRPOTrainer(
...,
reward_funcs=[cosine_scaled_reward, repetition_penalty],
)INTELLECT-2: A Reasoning Model Trained Through Globally Decentralized Reinforcement Learning
π Paper: https://huggingface.co/papers/2505.07291
INTELLECT-2 is the first globally distributed reinforcement learning training run of a 32 billion parameter language model using fully asynchronous RL across a dynamic, heterogeneous swarm of permissionless compute contributors. The authors propose modifications to the standard GRPO training recipe, including two-sided GRPO clipping for increased training stability. To reproduce the paper's setting, use this configuration:
from trl import GRPOConfig
training_args = GRPOConfig(
delta=4, # Ξ΄ in section 4.1 of the paper
epsilon=0.2, # Ξ΅ in section 4.1 of the paper
beta=0.001, # KL divergence coefficient in section 4.1 of the paper
num_generations=16, # responses per prompt in section 4.1 of the paper
learning_rate=3e-7, # section 4.1 of the paper
)Skywork-OR1: Open Reasoning Models
π Paper: https://huggingface.co/papers/2505.22312
Skywork-OR1 is a family of open reasoning models trained with GRPO. The paper introduces adaptive entropy control: an entropy regularization term βΞ±Β·H(Ο_ΞΈ) is added to the GRPO objective, and the coefficient Ξ± is automatically adjusted each optimizer step. When the model's mean per-token entropy falls at or below a target, Ξ± is incremented to encourage more exploration; otherwise it is decremented. The bonus is only applied while entropy is at or below the target. To replicate this adaptive entropy control, use the following configuration:
from trl import GRPOConfig, GRPOTrainer
training_args = GRPOConfig(
use_adaptive_entropy=True, # enable adaptive entropy control (Section 3.3 of the paper)
entropy_coef=0.01, # initial entropy regularization coefficient
entropy_target=5.0, # target mean per-token entropy (nats); tune for your model
entropy_coef_delta=0.005, # step size for coefficient updates per optimizer step
)
trainer = GRPOTrainer(
...,
args=training_args,
)Beyond the 80/20 Rule: High-Entropy Minority Tokens Drive Effective Reinforcement Learning for LLM Reasoning
π Paper: https://huggingface.co/papers/2506.01939
A minority of tokens with high entropy act as reasoning "forks" in the CoT path, driving exploration and performance gains for RLVR, while low-entropy majority tokens contribute little or even impede learning. RLVR mainly adjusts high-entropy tokens, largely preserving the base modelβs overall entropy patterns. Thus landing on the 80/20 rule, training on only 20% of the tokens with the highest entropy is comparable or supasses full-gradient updates for Qwen3 models.
The paper's main results use vanilla DAPO (β οΈ Dynamic Sampling is not supported in TRL). To replicate the main results, use the following configuration:
from trl import GRPOConfig, GRPOTrainer
from trl.rewards import get_soft_overlong_punishment
training_args = GRPOConfig(
# --- vanilla DAPO parameters (80/20 rule: section 5.2) --- #
# Overlong Filtering
mask_truncated_completions=True,
# Token-level Loss
loss_type="dapo",
# Clip-Higher
epsilon_high=0.28, # DAPO paper: section 4.1
epsilon=0.2, # DAPO paper: section 4.1
# Other parameters used
per_device_train_batch_size=512, # mini-batch size for training in the paper, DAPO paper: section 4.1
num_generations=16, # number of sample responses in the paper, DAPO paper: section 4.1
max_completion_length=20480, # maximum number of tokens for generation in the paper, DAPO paper: section 4.1
beta=0.0, # section 2.3, DAPO paper
# --- Gradients on the highest entropy tokens --- #
top_entropy_quantile=0.2
)
# Soft Overlong Punishment
sop_reward = get_soft_overlong_punishment(max_completion_len=20480, soft_punish_cache=4096) # DAPO paper: section 4.1
trainer = GRPOTrainer(
...,
args=training_args,
reward_funcs=[..., sop_reward],
)Dr. GRPO: Understanding R1-Zero-Like Training: A Critical Perspective
π Paper: https://huggingface.co/papers/2503.20783
A study of R1-Zero training identifies pretraining effects on RL performance and proffers Dr. GRPO to enhance token efficiency, achieving superior accuracy on AIME 2024. To reproduce the paper's setting, use this configuration:
from trl import GRPOConfig
training_args = GRPOConfig(
loss_type="dr_grpo",
per_device_train_batch_size=1, # train_batch_size_per_device in the Training section of the repository
num_generations=8, # num_samples in the Training section of the repository
max_completion_length=3000, # generate_max_length in the Training section of the repository
beta=0.0, # Ξ² in the Training section of the repository
)Part I: Tricks or Traps? A Deep Dive into RL for LLM Reasoning (Lite PPO)
π Paper: https://huggingface.co/papers/2508.08221
The authors of this paper find that the combination of:
- scaling rewards by the standard deviation computed over the entire batch and
- aggregating loss over the total number of tokens
can unlock the learning capability of critic-free policies using vanilla PPO loss. Their results demonstrate that this simple combination consistently improves performance, surpassing strategies like GRPO and DAPO.
TRL supports using these learnings to train a GRPO model by:
from trl import GRPOConfig
training_args = GRPOConfig(
...
scale_rewards="batch",
loss_type="dapo",
# Other parameters used
beta=0.0, # = init_kl_coef in the paper
top_p=0.99,
top_k=100,
temperature=0.99,
num_generations=8, # = num_return_sequences in the paper
num_iterations=1, # = ppo_epochs in the paper
per_device_train_batch_size=4,
gradient_accumulation_steps=32,
steps_per_generation=8, # (rollout_batch_size*num_return_sequences) / (per_device_train_batch_size*gradient_accumulation_steps)
)Note that when using gradient accumulation, the loss is aggregated over the total number of tokens in the batch, but not over the accumulated batch. For more details, see the GRPO Trainer - Loss types.
Truncated Importance Sampling
π° Blog: https://fengyao.notion.site/off-policy-rl
π Paper: https://huggingface.co/papers/1606.02647
Online policy learning methods commonly use an optimized inference framework for rollout generation (e.g vLLM) that is separate from the training backend. This introduces a rollout-training mismatch, exemplified in the following PPO objective:
$$
\small{
\mathbb{E}{a\sim\textcolor{red}{\pi{\text{inference}}}(\theta_{\mathrm{old}})}
\Bigl[
\min\Bigl(
\frac{\textcolor{blue}{\pi_{\text{training}}}(a, \theta)}{\textcolor{blue}{\pi_{\text{training}}}(a, \theta_{\mathrm{old}})},\hat A,
;\mathrm{clip}\bigl(\frac{\textcolor{blue}{\pi_{\text{training}}}(a, \theta)}{\textcolor{blue}{\pi_{\text{training}}}(a, \theta_{\mathrm{old}})},,1-\epsilon,,1+\epsilon\bigr),\hat A
\Bigr)
\Bigr]
}
$$
Despite \( \textcolor{red}{\pi_{\text{inference}}} \) and \( \textcolor{blue}{\pi_{\text{training}}} \) sharing the same model parameters \( \theta \), they can produce significantly different token probabilities. This unexpected behavior implicitly breaks the on-policy assumption, and silently turns training off-policy.
Truncated Importance Sampling (TIS) addresses this issue by adapting the model update via importance-sampling correction. The gradient computation of the aforementioned PPO objective becomes
$$
\small{
\mathbb{E}{a\sim\textcolor{red}{\pi{\text{inference}}}(\theta_{\mathrm{old}})}
\Bigl[
\underbrace{\text{clip}\bigl(\frac{\textcolor{blue}{\pi_{\text{training}}}(a, \theta_{\mathrm{old}})}{\textcolor{red}{\pi_{\text{inference}}}(a, \theta_{\mathrm{old}})}, C_{\min}, C_{\max}\bigr)}{\text{truncated importance ratio}} \cdot
\nabla\theta
\min\Bigl(
\frac{\textcolor{blue}{\pi_{\text{training}}}(a, \theta)}{\textcolor{blue}{\pi_{\text{training}}}(a, \theta_{\mathrm{old}})},\hat A,
;\mathrm{clip}\bigl(\frac{\textcolor{blue}{\pi_{\text{training}}}(a, \theta)}{\textcolor{blue}{\pi_{\text{training}}}(a, \theta_{\mathrm{old}})},,1-\epsilon,,1+\epsilon\bigr),\hat A
\Bigr)
\Bigr]
}
$$
where \( C_{\min} \) and \( C_{\max} \) are hyper-parameters. TIS is implemented in GRPO, and is enabled by selecting a vllm_importance_sampling_mode variant that includes the term truncate, such as "sequence_truncate" or "token_truncate".
from trl import GRPOConfig
training_args = GRPOConfig(
...
use_vllm=True,
vllm_importance_sampling_correction=True, # default True
vllm_importance_sampling_mode="sequence_truncate", # or "token_truncate"
vllm_importance_sampling_clip_max=2.0, # hyper-parameter C_max
)Masked Importance Sampling
π° Blog: https://ringtech.notion.site/icepop
π Paper: https://huggingface.co/papers/2510.18855
Masked Importance Sampling (MIS) addresses the same issue as Truncated Importance Sampling but replaces clipping with masking. MIS takes a more decisive stance by discarding updates whose discrepancy falls outside the range \( [C_{\min}, C_{\max}] \).
$$
\small{
\mathbb{E}{a\sim\textcolor{red}{\pi{\text{inference}}}(\theta_{\mathrm{old}})}
\Bigl[
\underbrace{\mathbf{1}\left[
C_{\min} \le \frac{\pi_{\text{training}}(a, \theta_{\mathrm{old}})}
{\pi_{\text{inference}}(a, \theta_{\mathrm{old}})}
\le C_{\max}
\right]
\cdot
\frac{\pi_{\text{training}}(a, \theta_{\mathrm{old}})}
{\pi_{\text{inference}}(a, \theta_{\mathrm{old}})}}{\text{masked importance ratio}} \cdot
\nabla\theta
\min\Bigl(
\frac{\textcolor{blue}{\pi_{\text{training}}}(a, \theta)}{\textcolor{blue}{\pi_{\text{training}}}(a, \theta_{\mathrm{old}})},\hat A,
;\mathrm{clip}\bigl(\frac{\textcolor{blue}{\pi_{\text{training}}}(a, \theta)}{\textcolor{blue}{\pi_{\text{training}}}(a, \theta_{\mathrm{old}})},,1-\epsilon,,1+\epsilon\bigr),\hat A
\Bigr)
\Bigr]
}
$$
MIS is implemented for GRPO, and is enabled by selecting a vllm_importance_sampling_mode variant that includes the term "mask", such as "sequence_mask" or "token_mask".
from trl import GRPOConfig
training_args = GRPOConfig(
...
use_vllm=True,
vllm_importance_sampling_correction=True, # default True
vllm_importance_sampling_mode="sequence_mask", # or "token_mask"
vllm_importance_sampling_clip_max=2.0, # hyper-parameter C_max
vllm_importance_sampling_clip_min=0.5, # hyper-parameter C_min
)Sequence-level Importance Sampling
The theoretically principled way to correct for the training-inference distribution shift is importance sampling, as introduced in the two papers above Truncated Importance Sampling and Masked Importance Sampling. However, the choice of formulation is crucial for keeping the gradient unbiased and ensuring stable training.
This work shows that sequence-level importance sampling is the sound approach for addressing the trainingβinference mismatch. Although token-level importance sampling achieves lower variance than a sequence-level ratio, it introduces bias and is therefore argued to be unsuitable for autoregressive models. The token-level gradient estimator is
$$
\mathbb{E}{x\sim\mathcal{D},, y\sim \pi^{\text{inference}}\theta(\cdot|x)}
\Bigg[
R(x,y),\cdot,
\sum_{t=0}^{|y|-1}
\frac{\pi^{\text{training}}\theta(y_t,|,x, y{<t})}
{\pi^{\text{inference}}\theta(y_t,|,x, y{<t})}
,\nabla_\theta \log \pi^{\text{training}}\theta(y_t,|,x, y{<t})
\Bigg]
$$
The correct, unbiased policy gradient estimator applies a single importance ratio over the entire generated sequence (trajectory) \( y \), The Sequence-Level IS estimator looks like:
$$
\mathbb{E}{x\sim\mathcal{D},, y\sim \pi^{\text{inference}}\theta(\cdot|x)}
\Bigg[
\frac{\pi^{\text{training}}\theta(y|x)}
{\pi^{\text{inference}}\theta(y|x)}
, R(x,y),
\nabla_\theta \log \pi^{\text{training}}_\theta(y|x)
\Bigg]
$$
TRL exposes the Importance Sampling granularity level through the vllm_importance_sampling_mode configuration parameter where "sequence_*" modes implement a sequence-level importance sampling ratio and "token_*" a per-token ratio.
The Art of Scaling Reinforcement Learning
π Paper: https://huggingface.co/papers/2510.13786
A systematic study that defines a framework for analyzing and predicting reinforcement learning scaling in large language models, identifies key design choices that affect compute efficiency and propose a best-practice recipe called ScaleRL.
You can partially reproduce the ScaleRL recipe using the [GRPOTrainer] with the following configs:
from trl import GRPOConfig
training_args = GRPOConfig(
loss_type="cispo",
epsilon_high=5.0,
num_generations=16,
scale_rewards="batch",
cast_lm_head_to_fp32=True
)It Takes Two: Your GRPO Is Secretly DPO
π Paper: https://huggingface.co/papers/2510.00977
Shows that GRPO's effectiveness stems from an implicit contrastive objective rather than accurate advantage estimation via large group sizes. This establishes a formal connection between GRPO and DPO, where group size only affects Monte Carlo estimators of the contrastive objective. The authors introduce 2-GRPO β using just two rollouts β which matches the performance of 16-GRPO at significantly lower training cost. Used in TRL via [GRPOTrainer] with num_generations=2. To reproduce the paper's setting, use this configuration:
from trl import GRPOConfig, GRPOTrainer
training_args = GRPOConfig(
num_generations=2, # minimal two-rollout case (2-GRPO) from the paper
loss_type="grpo",
)
trainer = GRPOTrainer(
...,
args=training_args,
)Soft Adaptive Policy Optimization
π Paper: https://huggingface.co/papers/2511.20347
Soft Adaptive Policy Optimization (SAPO), replaces hard clipping with a smooth, temperature-controlled gate that adaptively attenuates off-policy updates while preserving useful learning signals. Compared with GSPO and GRPO, SAPO is both sequence-coherent and token-adaptive. Like GSPO, SAPO maintains sequence-level coherence, but its soft gating forms a continuous trust region that avoids the brittle hard clipping band used in GSPO.
To reproduce the paper's setting, use this configuration:
from trl import GRPOConfig
training_args = GRPOConfig(
loss_type="sapo",
sapo_temperature_pos=1.0, # default value
sapo_temperature_neg=1.05, # default value
scale_rewards="group",
...
)DeepSeek-V3.2: Pushing the Frontier of Open Large Language Models
π Paper: https://huggingface.co/papers/2512.02556
DeepSeek-V3.2 technical report introduces several techniques to enhance the performance of GRPO. In TRL we implement:
- The Unbiased KL Estimate, which corrects the K3 estimator (as used in the original GRPO implementation) to obtain an unbiased KL estimate using the importance-sampling ratio between the current policy \( \pi_\theta \) and the behavior policy \( \pi_{\text{old}} \).
$$
\mathrm{D}_{\mathrm{KL}}\!\left(\pi_\theta(o_{i,t}) \,\|\, \pi_{\text{ref}}(o_{i,t})\right) =
\textcolor{red}{\frac{\pi_\theta(o_{i,t}\mid q, o_{i,
\log \frac{\pi_{\text{ref}}(o_{i,t}\mid q, o_{i,<t})}{\pi_\theta(o_{i,t}\mid q, o_{i,<t})}
- 1
\right).
$$
This feature is enabled by default (use_bias_correction_kl=True in the [GRPOConfig]), so you only need to set beta > 0:
from trl import GRPOConfig
training_args = GRPOConfig(
...,
beta=0.001, # the paper doesn't specify the value used, so we use the value from "DeepSeek-R1 incentivizes reasoning in LLMs through reinforcement learning"
)- The Off-Policy Masking, which stabilizes training by ignoring sequences where the policy performs poorly (negative advantage) and has drifted significantly from the old policy (high KL divergence).
The off-policy binary mask \(\textcolor{red}{M_{i,t}}\) is defined as:
$$
\textcolor{red}{M_{i,t}} = \begin{cases}
0 & \text{if } \hat{A}{i,t} < 0 \quad \text{and} \quad \frac{1}{|o_i|} \sum{t=1}^{|o_i|} \log \frac{\pi_{\theta_{\text{old}}}(o_{i,t} \mid q, o_{i,<t})}{\pi_\theta(o_{i,t} \mid q, o_{i,<t})} > \textcolor{blue}{\delta} \
1 & \text{otherwise}
\end{cases}
$$
This mask is then applied to the GRPO loss as follows:
$$
\mathcal{L}{\text{GRPO}}(\theta) = -\frac{1}{G} \sum{i=1}^G \frac{1}{|o_i|} \sum_{t=1}^{|o_i|} \left[ \min \left( \frac{\pi_\theta(o_{i,t} \mid q, o_{i,< t})}{\pi_{\theta_{\text{old}}}(o_{i,t} \mid q, o_{i,< t})} \hat{A}{i,t}, , \text{clip}\left( \frac{\pi\theta(o_{i,t} \mid q, o_{i,< t})}{\pi_{\theta_{\text{old}}}(o_{i,t} \mid q, o_{i,< t})}, 1 - \epsilon, 1 + \epsilon \right) \hat{A}{i,t} \right) \textcolor{red}{M{i,t}} - \beta \mathbb{D}{\text{KL}}\left[\pi\theta | \pi_{\text{ref}}\right] \right]
$$
To enable this feature, use the off_policy_mask_threshold (corresponding to \( \textcolor{blue}{\delta} \)) in the [GRPOConfig]:
from trl import GRPOConfig
training_args = GRPOConfig(
...,
off_policy_mask_threshold=0.5,
)While the paper doesn't specify a \( \textcolor{blue}{\delta} \) value used, a good starting point could be \( \textcolor{blue}{\delta} = 0.5 \). If training seems too conservative or too many sequences are masked, you can increase the value.
For reference, \( \textcolor{blue}{\delta} = 1.0 \) corresponds to an average log-ratio divergence of 1 nat per token, i.e. on sequences where this threshold is exceeded, the old policy was on average \( e^1 \approx 2.7 \) times more likely to generate these tokens than the current policy.
- The Keep Sampling Mask strategy, which keeps top-k/top-p sampling unbiased. The sampler draws each token from a truncated support \( \textcolor{red}{\mathcal{S}_{i,t}} \), the tokens left after truncation, so the policy log-probability is renormalized over that same support:
$$
\log \pi_\theta(o_{i,t} \mid q, o_{i,<t}) = z_{o_{i,t}} - \log \sum_{v \in \textcolor{red}{\mathcal{S}_{i,t}}} e^{z_v}.
$$
Without it the importance ratio is biased low and the gradient keeps pushing probability mass onto tokens the sampler never draws, which collapses training. This is supported in the [experimental.async_grpo.AsyncGRPOTrainer]: with top_p < 1.0 or top_k > 0, the rollout worker records each token's support from vLLM (>= 0.28, started with --return-sampling-mask --enable-scale-out) and the trainer renormalizes over it:
from trl.experimental.async_grpo import AsyncGRPOConfig
training_args = AsyncGRPOConfig(
...,
top_p=0.95,
top_k=512,
)GDPO: Group reward-Decoupled Normalization Policy Optimization for Multi-reward RL Optimization
π Paper: https://huggingface.co/papers/2601.05242
GDPO is a reinforcement learning optimization method designed for multi-reward training. While existing approaches commonly apply Group Relative Policy Optimization (GRPO) in multi-reward settings, the authors show that this leads to reward advantages collapse, reducing training signal resolution and causing unstable or failed convergence. GDPO resolves this issue by decoupling reward normalization across individual rewards, preserving their relative differences and enabling more faithful preference optimization. To enable GDPO for multi-reward RL training, simply set:
For a group of \( N \) rewards and \( G \) samples per group, GDPO normalizes each reward independently:
$$
A_n^{(i,j)} = \frac{r_n^{(i,j)} - \text{mean}{r_n^{(i,1)}, \ldots, r_n^{(i,G)}}}{\text{std}{r_n^{(i,1)}, \ldots, r_n^{(i,G)}} + \epsilon}
$$
The normalized group advantage is then aggregated across rewards:
$$
A^{(i,j)} = \sum_{n=1}^{N} w_n A_n^{(i,j)}
$$
The final per-batch normalization produces:
$$
\hat{A}^{(i,j)} = \frac{A^{(i,j)} - \text{mean}{i',j'}{A^{(i',j')}}}{\text{std}{i',j'}{A^{(i',j')}} + \epsilon}
$$
Here, \( \text{mean}{i',j'}{A^{(i',j')}} \) and \( \text{std}{i',j'}{A^{(i',j')}} \) denote statistics over all groups in the batch.
from trl import GRPOConfig
training_args = GRPOConfig(
...,
multi_objective_aggregation="normalize_then_sum",
)Note that this method only has an effect when training involve more than one reward function.
The authors provide a easy-to-use, slurm-free training example that enable the community to quickly validate GDPOβs effectiveness over GRPO, see Experiment-"Aha" moment.
Length-Unbiased Sequence Policy Optimization: Revealing and Controlling Response Length Variation in RLVR
π Paper: https://huggingface.co/papers/2602.05261
Length-Unbiased Sequence Policy Optimization (LUSPO) modifies GSPO by scaling each sequence's loss by its length. This corrects GSPO's gradient bias that penalizes longer responses. To reproduce the paper's setting, use this configuration:
from trl import GRPOConfig
training_args = GRPOConfig(
loss_type="luspo",
importance_sampling_level="sequence",
epsilon=2e-3, # section 5.1 of the paper
epsilon_high=2.5e-3, # section 5.1 of the paper
)VESPO: Variational Sequence-Level Soft Policy Optimization for Stable Off-Policy LLM Training
π Paper: https://huggingface.co/papers/2602.10693
VESPO addresses training instability in off-policy RL caused by policy staleness, asynchronous updates, and train-inference mismatches. Rather than relying on heuristic token-level clipping (GRPO) or sequence-length normalization (GSPO), VESPO derives a principled reshaping kernel from a variational framework. In practice, this yields a smooth, asymmetric Gamma weighting function that gracefully suppresses extreme sequence-level importance weights without introducing length bias.
$$
\mathcal{L}{\text{VESPO}}(\theta) = - \mathbb{E}{\tau \sim \mu} \left[ \underbrace{W(\tau)^{k} \cdot \exp\left(\lambda
(1 - W(\tau))\right)}{\phi(W) \text{ detached }} \cdot \mathcal{A}(\tau) \cdot \log \pi\theta(\tau) \right]
$$
with \( W(\tau) = \frac{\pi_\theta(\tau)}{\mu(\tau)} \) the sequence level importance ratio, and \( \phi(W) \) is detached from the computation graph to serve as a gradient scaling coefficient.
from trl import GRPOConfig
training_args = GRPOConfig(
loss_type="vespo",
use_vllm=True, # or False if not using any token-level `vllm_importance_sampling_correction` methods
vllm_importance_sampling_mode="token_truncate", # default correction mode for VESPO, `token_mask` also supported
vespo_k_pos=2.0, # power exponent (c1 in paper Section 3.4) for positive advantages
vespo_lambda_pos=3.0, # decay factor (c2 in paper Section 3.4) for positive advantages
vespo_k_neg=3.0, # power exponent (c1 in paper Section 3.4) for negative advantages
vespo_lambda_neg=2.0, # decay factor (c2 in paper Section 3.4) for negative advantages
)Optimal Advantage Regression
Papers relating to the [experimental.a2po.A2POTrainer].
Accelerating RL for LLM Reasoning with Optimal Advantage Regression
π Paper: https://huggingface.co/papers/2505.20686
A*-PO (Optimal Advantage Regression) is a two-stage RL method for LLM reasoning that avoids both an online critic and multi-sample group rollouts. Stage 1 estimates the optimal value V*(x) = Ξ²Β·log E_{yβΌΟ_ref}[exp(r(x, y)/Ξ²)] offline from reference-policy samples; Stage 2 performs on-policy updates with a single generation per prompt using a squared-error regression loss (Ξ²Β·log(Ο(y|x)/Ο_ref(y|x)) β (r(x, y) β V*(x)))Β². This yields faster training and lower peak memory than PPO/GRPO/REBEL. The method assumes a binary verifiable reward and cannot exceed the reference policy's Pass@K. See A2PO. The official code can be found in ZhaolinGao/A-PO.
from trl.experimental.a2po import A2POConfig, A2POTrainer
# Hyperparameters from the paper (Qwen2.5, GSM8K/MATH)
training_args = A2POConfig(
beta1=0.5, # KL temperature for offline V* estimation
beta2=1e-3, # KL temperature for the on-policy regression target
num_value_samples=8, # N samples per prompt from the reference policy
)
trainer = A2POTrainer(
model=...,
reward_funcs=..., # should return a binary reward in {0, 1}
args=training_args,
train_dataset=...,
)
trainer.train()Direct Policy Optimization
Papers relating to the [DPOTrainer]
Direct Preference Optimization: Your Language Model is Secretly a Reward Model
π Paper: https://huggingface.co/papers/2305.18290
Direct Preference Optimization (DPO) fine-tunes language models more efficiently and with better performance compared to reinforcement learning from human feedback (RLHF), by directly optimizing policy training based on human preferences. To reproduce the paper's setting, use this configuration:
from trl import DPOConfig
training_args = DPOConfig(
loss_type="sigmoid", # losses in Appendix B of the paper
per_device_train_batch_size=64, # batch size in Appendix B of the paper
learning_rate=1e-6, # learning rate in Appendix B of the paper
beta=0.1, # Ξ² in Appendix B of the paper
)SLiC-HF: Sequence Likelihood Calibration with Human Feedback
π Paper: https://huggingface.co/papers/2305.10425
Sequence Likelihood Calibration (SLiC) is shown to be an effective and simpler alternative to Reinforcement Learning from Human Feedback (RLHF) for learning from human preferences in language models. To reproduce the paper's setting, use this configuration:
from trl import DPOConfig
training_args = DPOConfig(
loss_type="hinge", # Section 2 of the paper
per_device_train_batch_size=512, # batch size in Section 3.2 of the paper
learning_rate=1e-4, # learning rate in Section 3.2 of the paper
)These parameters only appear in the published version
Statistical Rejection Sampling Improves Preference Optimization
π Paper: https://huggingface.co/papers/2309.06657
Proposes RSO, selecting stronger preference pairs via statistical rejection sampling to boost offline preference optimization; complements DPO/SLiC. They also introduce a new loss defined as:
$$
\mathcal{L}{\text{hinge-norm}}(\pi\theta)
= \mathbb{E}{(x, y_w, y_l) \sim \mathcal{D}}
\left[
\max\left(0,; 1 - \left[\gamma \log \frac{\pi\theta(y_w \mid x)}{\pi_\text{ref}(y_w \mid x)} - \gamma \log \frac{\pi_\theta(y_l \mid x)}{\pi_\text{ref}(y_l \mid x)}\right]\right)
\right]
$$
To train with RSO-filtered data and the hinge-norm loss, you can use the following code:
from trl import DPOConfig, DPOTrainer
dataset = ...
def rso_accept(example): # replace with your actual filter/score logic
return example["rso_keep"]
train_dataset = train_dataset.filter(rso_accept)
training_args = DPOConfig(
loss_type="hinge",
beta=0.05, # correspond to Ξ³ in the paper
)
trainer = DPOTrainer(
...,
args=training_args,
train_dataset=train_dataset,
)
trainer.train()Beyond Reverse KL: Generalizing Direct Preference Optimization with Diverse Divergence Constraints
π Paper: https://huggingface.co/papers/2309.16240
Proposes (( f \)-DPO, extending DPO by replacing the usual reverse-KL regularizer with a general (( f \)-divergence, letting you trade off mode-seeking vs mass-covering behavior (e.g. forward KL, JS, (( \alpha \)-divergences). The only change is replacing the DPO log-ratio margin with an fβ² score:
$$ \mathcal{L}_{f\text{-DPO}}(\pi_\theta) = \mathbb{E}_{(x, y_w, y_l) \sim \mathcal{D}} \left[ -\log \sigma\left( \beta \textcolor{red}{f'}\textcolor{red}{\Big(}\frac{\pi_\theta(y_w|x)}{\pi_{\text{ref}}(y_w|x)}\textcolor{red}{\Big)}
\beta \textcolor{red}{f'}\textcolor{red}{\Big(}\frac{\pi_\theta(y_l|x)}{\pi_{\text{ref}}(y_l|x)}\textcolor{red}{\Big)}
\right)
\right]
$$
Where \( f' \) is the derivative of the convex function defining the chosen (( f \)-divergence.
To reproduce:
from trl import DPOConfig
training_args = DPOConfig(
loss_type="sigmoid",
beta=0.1,
f_divergence_type="js_divergence", # or "reverse_kl" (default), "forward_kl", "js_divergence", "alpha_divergence"
f_alpha_divergence_coef=0.5, # only used if f_divergence_type="alpha_divergence"
)A General Theoretical Paradigm to Understand Learning from Human Preferences
π Paper: https://huggingface.co/papers/2310.12036
Learning from human preferences can be written as a single KL-regularized objective over pairwise preference probabilities,
$$
\max_\pi ;\mathbb{E}\big[\Psi\left(p^*(y \succ y' \mid x)\right)\big] - \tau\mathrm{KL}(\pi||\pi_{\text{ref}}),
$$
which reveals RLHF and DPO as special cases corresponding to the logit choice of \( \Psi \).
The paper shows that this logit transform amplifies near-deterministic preferences and effectively weakens KL regularization, explaining overfitting.
Using the Identity transform (IPO) avoids this pathology by optimizing preferences directly, without assuming a BradleyβTerry reward model.
To reproduce the paper's setting, use this configuration:
from trl import DPOConfig
training_args = DPOConfig(
loss_type="ipo", # Section 5.1 of the paper
per_device_train_batch_size=90, # mini-batch size in Section C.1 of the paper
learning_rate=1e-2, # learning rate in Section C.1 of the paper
)These parameters only appear in the published version
Towards Efficient and Exact Optimization of Language Model Alignment
π Paper: https://huggingface.co/papers/2402.00856
The paper shows that direct preference methods like DPO optimize the wrong KL direction, leading to blurred preference capture, and proposes EXO as an efficient way to exactly optimize the humanβpreference alignment objective by leveraging reverse KL probability matching rather than forward KL approximations. To reproduce the paper's setting, use this configuration:
from trl import DPOConfig
training_args = DPOConfig(
loss_type="exo_pair", # Section 3.2 of the paper
# From Section B of the paper
per_device_train_batch_size=64,
learning_rate=1e-6,
beta=0.1,
)Noise Contrastive Alignment of Language Models with Explicit Rewards
π Paper: https://huggingface.co/papers/2402.05369
The paper reframes language-model alignment as a noise-contrastive classification problem, proposing InfoNCA to learn a policy from explicit rewards (or preferences) by matching a reward-induced target distribution over responses, and showing DPO is a special binary case. It then introduces NCA, which adds an absolute likelihood term to prevent the likelihood collapse seen in purely relative (contrastive) objectives.
With pairwise preferences, treat the chosen/rejected \( K=2 \), define scores \( r=\beta(\log\pi_\theta-\log\pi_{\text{ref}}) \), and apply the NCA preference loss \( -\log\sigma(r_w)-\tfrac12\log\sigma(-r_w)-\tfrac12\log\sigma(-r_l) \).
To reproduce the paper's setting, use this configuration:
from trl import DPOConfig
training_args = DPOConfig(
loss_type="nca_pair",
# From Section C of the paper
per_device_train_batch_size=32,
learning_rate=5e-6,
beta=0.01,
)Provably Robust DPO: Aligning Language Models with Noisy Feedback
π Paper: https://huggingface.co/papers/2403.00409
DPO breaks under noisy human preferences because label flips bias the objective. Robust DPO fixes this by analytically debiasing the DPO loss under a simple noise model, with provable guarantees.
$$
\mathcal{L}{\text{robust}}(\pi\theta) = \frac{(1-\varepsilon)\mathcal{L}{\text{DPO}}(y_w, y_l) - \varepsilon\mathcal{L}{\text{DPO}}(y_l, y_w)}
{1-2\varepsilon}
$$
Where \( \mathcal{L}_{\text{DPO}} \) is the DPO loss defined in Direct Preference Optimization: Your Language Model is Secretly a Reward Model and \( \varepsilon \) is the probability of a label flip.
This single correction turns noisy preference data into an unbiased estimator of the clean DPO objective.
from trl import DPOConfig
training_args = DPOConfig(
loss_type="robust",
per_device_train_batch_size=16, # batch size in Section B of the paper
learning_rate=1e-3, # learning rate in Section B of the paper
beta=0.1, # Ξ² in Section B of the paper,
max_length=512, # max length in Section B of the paper
label_smoothing=0.1 # label smoothing $\varepsilon$ in Section 6 of the paper
)Binary Classifier Optimization for Large Language Model Alignment
π Paper: https://huggingface.co/papers/2404.04656
Theoretical analysis and a new algorithm, Binary Classifier Optimization, explain and enhance the alignment of large language models using binary feedback signals. To reproduce the paper's setting, use this configuration:
BCO reframes language-model alignment as behavioral cloning from an optimal reward-weighted distribution, yielding simple supervised objectives that avoid RL while remaining theoretically grounded.
It supports both unpaired reward data and pairwise preference data, with a reward-shiftβinvariant formulation that reduces to a DPO-style loss in the preference setting.
For the pairwise preference setting, the BCO loss is defined as:
$$ \mathcal{L}_{\text{bco\_pair}}(\pi_\theta) = \mathbb{E}_{(x, y_w, y_l) \sim \mathcal{D}} \left[ -\log \sigma\Big( \beta[(\log\pi_\theta-\log\pi_{\text{ref}})(y_w)
(\log\pi_\theta-\log\pi_{\text{ref}})(y_l)]
\Big)
\right]
$$
To reproduce the paper in this setting, use this configuration:
from trl import DPOConfig
training_args = DPOConfig(
loss_type="bco_pair",
# From Section C of the paper
per_device_train_batch_size=128,
learning_rate=5e-7,
beta=0.01,
)Learn Your Reference Model for Real Good Alignment
π Paper: https://huggingface.co/papers/2404.09656
Trust Region DPO (TR-DPO) updates the reference policy during training, demonstrating effectiveness against DPO on the Anthropic HH and TLDR datasets, outperforming DPO by up to 19% measured by automatic evaluation with GPT-4, improving coherence, correctness, level of detail, helpfulness, and harmlessness. To reproduce the paper's setting, use this configuration:
from trl import DPOConfig
training_args = DPOConfig(
sync_ref_model=True, # enable TR-DPO (Section 3 of the paper)
ref_model_mixup_alpha=0.6, # Ξ± soft update weight (Table 1 of the paper)
ref_model_sync_steps=512, # Ο update frequency in steps (Table 1 of the paper)
beta=0.05, # Ξ² temperature (Table 1 of the paper)
learning_rate=1e-6, # learning rate (Table 2 of the paper)
num_train_epochs=1, # Table 2 of the paper
max_length=1024, # max tokens length (Table 2 of the paper)
max_grad_norm=2, # max gradient norm (Table 2 of the paper)
warmup_steps=100, # warm-up steps (Table 2 of the paper)
)Iterative Reasoning Preference Optimization
π Paper: https://huggingface.co/papers/2404.19733
Iterative RPO improves reasoning by repeatedly generating chain-of-thought candidates, building preference pairs from correct vs. incorrect answers, and training with a DPO + NLL objective. The extra NLL term is key for learning to actually generate winning traces.
TRL can express the DPO + NLL objective by mixing "sigmoid" (DPO) with "sft" (NLL):
from trl import DPOConfig, DPOTrainer
training_args = DPOConfig(
loss_type=["sigmoid", "sft"],
loss_weights=[1.0, 1.0], # alpha in the paper, recommended value is 1.0
)
trainer = DPOTrainer(
...,
args=training_args,
)Note that the paper uses an iterative loop: each iteration regenerates CoT candidates with the current model, then retrains on fresh preference pairs. TRL does not automate that loop for you.
Self-Play Preference Optimization for Language Model Alignment
π Paper: https://huggingface.co/papers/2405.00675
A self-play method called SPPO for language model alignment achieves state-of-the-art performance by approximating Nash equilibrium policy in a constant-sum game setting, outperforming other approaches with limited data. To reproduce the paper's setting, use this configuration:
from trl import DPOConfig
training_args = DPOConfig(
loss_type="sppo_hard",
# From Section 5 of the paper
beta=0.001, # Ξ² = Ξ·^β1
per_device_train_batch_size=64,
learning_rate=5e-7,
)Provably Mitigating Overoptimization in RLHF: Your SFT Loss is Implicitly an Adversarial Regularizer
π Paper: https://huggingface.co/papers/2405.16436
Regularized Preference Optimization (RPO) mitigates overoptimization in RLHF by fusing the DPO loss with the SFT loss, provably preventing the policy from choosing actions with spurious high proxy rewards. To reproduce the paper's setting, use this configuration:
from trl import DPOConfig
training_args = DPOConfig(
loss_type=["sigmoid", "sft"], # RPO loss = DPO + SFT (Section 5 of the paper)
loss_weights=[1.0, 0.005], # Ξ·=0.005 SFT weight in Appendix E.1 of the paper
beta=0.01, # Ξ² in Appendix E.1 of the paper
learning_rate=5e-7, # learning rate in Appendix E.1 of the paper
num_train_epochs=1, # Appendix E.1 of the paper
)Distributional Preference Alignment of LLMs via Optimal Transport
π Paper: https://huggingface.co/papers/2406.05882
Alignment via Optimal Transport (AOT) aligns large language models distributionally by penalizing violations of stochastic dominance between positive and negative sample distributions, achieving state-of-the-art performance on alignment benchmarks. To reproduce the paper's setting, use this configuration:
from trl import DPOConfig
training_args = DPOConfig(
loss_type="aot",
beta=0.01, # from the caption of Figure 2
)or, for the unpaired version:
from trl import DPOConfig
training_args = DPOConfig(
loss_type="aot_unpaired",
beta=0.01, # from the caption of Figure 2
)There is no additional hyperparameter in the paper.
Discovering Preference Optimization Algorithms with and for Large Language Models
π Paper: https://huggingface.co/papers/2406.08414
An LLM-driven method automatically discovers performant preference optimization algorithms, leading to a new algorithm called DiscoPOP that blends logistic and exponential losses. To reproduce the paper's setting, use this configuration:
from trl import DPOConfig
training_args = DPOConfig(
loss_type="discopop",
per_device_train_batch_size=64, # batch size in Section B.1 of the paper
learning_rate=5e-7, # learning rate in Section B.1 of the paper
beta=0.05, # Ξ² in Section B.1 of the paper,
discopop_tau=0.05 # Ο in Section E of the paper
)WPO: Enhancing RLHF with Weighted Preference Optimization
π Paper: https://huggingface.co/papers/2406.11827
WPO reweights preference pairs by their policy probabilities to reduce the off-policy gap in DPO-style training. The loss is:
$$
\mathcal{L}{\text{WPO}} = -\mathbb{E}{(x, y_w, y_l) \sim \mathcal{D}} \left[ \textcolor{red}{w(x, y_w) w(x, y_l)} \log p(y_w \succ y_l \mid x) \right]
$$
where the weight \( w(x, y) \) is defined as:
$$
w(x, y) = \exp\left(\frac{1}{|y|}\sum_{t=1}^{|y|} \log \frac{\pi_\theta(y_t \mid x, y_{<t})}{\sum_{v \in \mathcal{V}} \pi_\theta(v \mid x, y_{<t})^2}\right)
$$
To reproduce the paper's setting, use this configuration:
from trl import DPOConfig
training_args = DPOConfig(
loss_type="sigmoid", # DPO loss used in the paper
beta=0.01, # Section 4 of the paper
use_weighting=True,
)Anchored Preference Optimization and Contrastive Revisions: Addressing Underspecification in Alignment
π Paper: https://huggingface.co/papers/2408.06266
CLAIR and APO enhance LLM alignment through more contrastive preference pairs and controlled alignment objectives, improving model performance close to GPT4-turbo. To reproduce the paper's setting, use this configuration:
from trl import DPOConfig
training_args = DPOConfig(
loss_type="apo_zero", # Section 4 of the paper
per_device_train_batch_size=64, # batch size in Section B.1 of the paper
learning_rate=2e-7, # learning rate in Section 5.2 of the paper
beta=0.1, # Ξ² in Section 5.2 of the paper,
)from trl import DPOConfig
training_args = DPOConfig(
loss_type="apo_down", # Section 4 of the paper
per_device_train_batch_size=64, # batch size in Section B.1 of the paper
learning_rate=2e-7, # learning rate in Section 5.2 of the paper
beta=0.1, # Ξ² in Section 5.2 of the paper,
)These parameters only appear in the published version
Length Desensitization in Direct Preference Optimization
π Paper: https://huggingface.co/papers/2409.06411
Shows that standard DPO is inherently length-sensitive, which often pushes models toward overly long or verbose generations. The paper proposes LD-DPO, which modifies the sequence log-prob aggregation by splitting the longer response into a shared prefix (up to the shorter response length) and an excess tail, then downweighting the tail with a factor \( \alpha \in [0,1] \):
$$
\log \pi_\theta(y_{\text{long}}|x) = \log \pi_\theta(y_{1:l_p}|x) + \alpha \cdot \log \pi_\theta(y_{l_p+1:l}|x, y_{1:l_p}),
\quad
l_p=\min(|y_w|,|y_l|).
$$
Setting \( \alpha=1 \) recovers standard DPO; smaller \( \alpha \) reduces verbosity while preserving preference quality.
The optimal \( \alpha \) depends on the model family and whether youβre training a base vs. instruct model, but the paper suggests \( \alpha=0.5 \) as a strong default starting point.
from trl import DPOConfig
training_args = DPOConfig(
ld_alpha=0.5,
)Enhancing the Reasoning Ability of Multimodal Large Language Models via Mixed Preference Optimization
π Paper: https://huggingface.co/papers/2411.10442
Introduces Mixed Preference Optimization (MPO) to improve multimodal reasoning in MLLMs, addressing distribution shift and weak Chain-of-Thought (CoT) after standard pre-training and SFT. The paper contributes (1) MMPR, an automated pipeline for high-quality multimodal preference data, and (2) MPO, a combined preference objective (pairwise + BCO-style + SFT) that boosts CoT. InternVL2-8B-MPO reaches 67.0 on MathVista (+8.7 over InternVL2-8B), comparable to the 10Γ larger InternVL2-76B. Used in TRL via [DPOConfig] with composite loss. To reproduce the paper's setting, use this configuration:
from trl import DPOConfig
training_args = DPOConfig(
loss_type=["sigmoid", "bco_pair", "sft"], # β = w_pΒ·β_p + w_qΒ·β_q + w_gΒ·β_g (Section 3.2 of the paper)
loss_weights=[0.8, 0.2, 1.0], # w_p, w_q, w_g loss weights (Section 7 of the paper)
learning_rate=5e-6, # learning rate (Section 7 of the paper)
)TΓLU 3: Pushing Frontiers in Open Language Model Post-Training
π Paper: https://huggingface.co/papers/2411.15124
The length-normalized sigmoid loss addresses length bias in DPO by dividing chosen and rejected log-ratio scores by their respective completion lengths before computing the Bradley-Terry loss. This per-token normalization was introduced in SimPO (Appendix I "DPO w/ LN" ablation) as an average log-probability reward for a reference-free setting, and was later adopted for standard reference-model-based DPO in post-training recipes such as TΓΌlu 3 (Section 4.3). The loss is:
$$
\mathcal{L}{\text{sigmoid_norm}} = -\log\sigma!\left(\beta \left({\color{red}\frac{1}{|y_w|}}\log\frac{\pi\theta(y_w|x)}{\pi_{\text{ref}}(y_w|x)} - {\color{red}\frac{1}{|y_l|}}\log\frac{\pi_\theta(y_l|x)}{\pi_{\text{ref}}(y_l|x)}\right)\right),
$$
which can be set with:
from trl import DPOConfig
training_args = DPOConfig(
loss_type="sigmoid_norm",
)KahnemanβTversky Optimization
Papers relating to the [KTOTrainer]
KTO: Model Alignment as Prospect Theoretic Optimization
π Paper: https://huggingface.co/papers/2402.01306
KTO derives an alignment objective from prospect theory and learns directly from binary human feedback (liked/disliked), matching or surpassing DPO-style methods while handling imbalanced/noisy signals well.
To reproduce the paper's setting, you can use the default configuration of [KTOTrainer]:
from trl import KTOConfig, KTOTrainer
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained(model_id)
tokenizer = AutoTokenizer.from_pretrained(model_id)
trainer = KTOTrainer(
model=model,
processing_class=tokenizer,
args=KTOConfig(),
train_dataset=...,
)
trainer.train()Supervised Fine-Tuning
Papers relating to the [SFTTrainer]
TailSFT: Filtered Fine-Tuning Improves Post-Training Performance
π Paper: https://huggingface.co/papers/2608.25756
TailSFT preserves response coverage for subsequent reinforcement learning by filtering the sequences whose length-normalized loss has improved the most relative to the initial policy. The tail_sft_gsm8k example implements the paper's distributed selection-batch filtering by subclassing [SFTTrainer]. It records each sequence's initial-policy loss before training and filters examples independently in every selection batch.
python examples/tail_sft_gsm8k/tail_sft.pyEMA Without the Lag: Bias-Corrected Iterate Averaging Schemes
π Paper: https://huggingface.co/papers/2508.00180
Bias-Corrected Exponential Moving Average (BEMA) improves the stability and efficiency of language model fine-tuning by reducing stochasticity and eliminating bias. To use BEMA with SFT as described in the paper, you can use the [BEMACallback]:
from trl import BEMACallback, SFTTrainer
trainer = SFTTrainer(
...
callbacks=[BEMACallback()],
)On the Generalization of SFT: A Reinforcement Learning Perspective with Reward Rectification
π Paper: https://huggingface.co/papers/2508.05629
Dynamic Fine-Tuning (DFT) improves the generalization of Large Language Models (LLMs) by dynamically rescaling gradients, outperforming standard Supervised Fine-Tuning (SFT) and showing competitive results in offline reinforcement learning.
$$
\mathcal{L}{\text{DFT}}(\theta) = \mathbb{E}{(x,y) \sim \mathcal{D}} \left[ - \sum_{t=1}^{|y|} \textcolor{red}{\text{sg}\big(\pi_\theta(y_t \mid y_{<t}, x)\big)} ; \log \pi_\theta(y_t \mid y_{<t}, x) \right]
$$
where \( \text{sg}(\cdot) \) is the stop-gradient operator. To use DFT with SFT as described in the paper, you can use the loss_type="dft" argument:
from trl import SFTConfig
training_args = SFTConfig(
loss_type="dft",
...
)To closely match the paperβs setup, you can use the following configuration (see Sec. 4.1). Authors also mention that the hyperparameters are not very sensitive (Sec. 4.3):
SFTConfig(
loss_type="dft",
learning_rate=5e-5,
max_length=2048,
# Target batch size 256; achieved via per-device batch 8 * grad accumulation 32
per_device_train_batch_size=8,
gradient_accumulation_steps=32,
)Fewer Truncations Improve Language Modeling
π Paper: https://huggingface.co/papers/2404.10830
The paper shows that the standard concatenate-then-split preprocessing (packing_strategy="wrapped") used for LLM training causes many documents to be arbitrarily truncated, which harms learning. It proposes packing document chunks into context windows using a Best-Fit Decreasing bin-packing algorithm, greatly reducing truncation while keeping high token utilization and improving model performance. TRL implements this as the "bfd_split" packing strategy in [SFTConfig]. For more details on packing, see the SFT documentation.
from trl import SFTConfig
training_args = SFTConfig(
packing=True,
packing_strategy="bfd_split",
max_length=4096,
)Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer
π Paper: https://huggingface.co/papers/1910.10683
The T5 paper proposes a unified text-to-text framework for transfer learning and introduces sequence packing (Section 3.5.2): grouping multiple short sequences into fixed-length blocks to reduce padding and improve training efficiency. Packing is supported in TRL via [SFTConfig] with the [SFTTrainer]. To enable packing with TRL, use this configuration:
from trl import SFTConfig
training_args = SFTConfig(
packing=True, # enable sequence packing (Section 3.5.2 of the paper)
max_length=512, # packed sequence length (Section 3.5.2 of the paper)
)Reparameterizing Uniform Diffusion Models: the Leave-One-Out Denoiser
π Paper: https://huggingface.co/papers/2605.22765
For uniform discrete diffusion models, the usual plug-in cross-entropy does not learn the plain denoiser \( \mathbb{E}[X_0 \mid X_t] \) but the leave-one-out (LOO) posterior, which predicts each token without using its own corrupted value. The paper derives exact conversions between the denoiser, the LOO posterior, and the score, so a network can be trained as either parameterization with the same cross-entropy. Given logits \( f_\theta \) whose softmax parameterizes the LOO posterior, the denoiser used in the loss is
$$
d_\theta(x_t, t)^\ell = \mathrm{softmax}!\left( f_\theta(x_t, t)^\ell + \log!\left(1 + \frac{K,\alpha_t}{1 - \alpha_t}, x_t^\ell\right) \right),
$$
i.e. an additive correction on the logit of the observed token, where \( K \) is the vocabulary size and \( \alpha_t \) the probability the uniform forward keeps the clean token. The paper reports that the LOO parameterization consistently improves generation. The block-diffusion SFT example examples/sft_diffusion_gemma/sft_diffusion_gemma.py exposes both parameterizations via --model_prediction_type (mean for the plain denoiser, matching the released checkpoint, or mean_loo for the LOO posterior):
accelerate launch --config_file examples/accelerate_configs/deepspeed_zero3.yaml \
examples/sft_diffusion_gemma/sft_diffusion_gemma.py \
--use_peft \
--gradient_checkpointing \
--model_prediction_type mean_loo \
--output_dir diffusiongemma-26B-A4B-it-gsm8k-loraReducing Activation Recomputation in Large Transformer Models
π Paper: https://huggingface.co/papers/2205.05198
Full activation checkpointing recomputes every op in a checkpointed region during backward, including attention, even though only a few ops (attention among them) account for most of the recomputation cost relative to the memory they'd cost to keep. The paper's selective activation recomputation checkpoints only those expensive ops instead of the whole region. TRL implements this as selective activation checkpointing (SAC) for [SFTTrainer], saving the attention output during the forward pass so backward does not recompute it:
from trl import SFTConfig
training_args = SFTConfig(
gradient_checkpointing=True,
gradient_checkpointing_kwargs={"selective": True},
)Parameter-Efficient Fine-Tuning (PEFT)
For general details on using PEFT with TRL, please refer to the PEFT Integration guide.
LoRA: Low-Rank Adaptation of Large Language Models
π Paper: https://huggingface.co/papers/2106.09685
Low-Rank Adaptation (LoRA) reduces the number of trainable parameters and GPU memory usage in large-scale pre-trained models while maintaining or improving performance on downstream tasks. TRL integrates LoRA via the PEFT library and can be easily enabled in any TRL trainer by passing a [~peft.LoraConfig] to the peft_config argument. Here is an example of using LoRA with the [SFTTrainer]:
from trl import SFTTrainer
from peft import LoraConfig
trainer = SFTTrainer(
...,
peft_config=LoraConfig(),
)DoRA: Weight-Decomposed Low-Rank Adaptation
π Paper: https://huggingface.co/papers/2402.09353
Weight-Decomposed Low-Rank Adaptation (DoRA) can improve the performance of LoRA, especially at low ranks. DoRA decomposes pre-trained weight into two component: magnitude and direction. Direction is handled by normal LoRA, and magnitude is learnable parameters. TRL integrate DoRA via the PEFT library and can be easily enable through setting use_dora=True to the [~peft.LoraConfig].
from peft import LoraConfig
config = LoraConfig(use_dora=True, ...)Reinforce Leave-One-Out
Papers relating to the [RLOOTrainer]
Back to Basics: Revisiting REINFORCE Style Optimization for Learning from Human Feedback in LLMs
π Paper: https://huggingface.co/papers/2402.14740
RLOO is a variant of REINFORCE that reduces variance by using leave-one-out baselines. It computes rewards by comparing each sample against the average of all other samples in the batch, providing more stable gradients than standard REINFORCE. To reproduce the paper's setting, use this configuration:
from trl import RLOOConfig
training_args = RLOOConfig(
per_device_train_batch_size=512, # section C Training Detail of the paper
steps_per_generation=2, # section C Training Detail of the paper
beta=0.03, # section C Training Detail of the paper
num_generations=2, # experiments of paper different num_generations={2,4}
learning_rate=1e-6, # section C Training Detail of the paper
)REINFORCE++: A Simple and Efficient Approach for Aligning Large Language Models
π Paper: https://huggingface.co/papers/2501.03262
REINFORCE++ is an enhanced variant of the classical REINFORCE algorithm that incorporates key optimization techniques from PPO while eliminating the need for a critic network. It achieves simplicity, enhanced training stability, and reduced computational overhead through global advantage normalization across the entire batch. To approximate the paper's setting with the [RLOOTrainer], use this configuration:
from trl import RLOOConfig
training_args = RLOOConfig(
normalize_advantages=True, # global advantage normalization, core of REINFORCE++
)Odds Ratio Preference Optimization
Papers relating to the [experimental.orpo.ORPOTrainer]
ORPO: Monolithic Preference Optimization without Reference Model
π Paper: https://huggingface.co/papers/2403.07691
The introduction of a reference model-free monolithic odds ratio preference optimization algorithm (ORPO) enhances preference alignment during supervised fine-tuning, surpassing larger models in key evaluations. To reproduce the paper's setting, use this configuration:
from trl.experimental.orpo import ORPOConfig
training_args = ORPOConfig(
beta=0.1, # Ξ» odds ratio loss weight (Table 7 of the paper, Mistral-ORPO-Ξ²)
learning_rate=5e-6, # learning rate (Appendix C of the paper)
lr_scheduler_type="inverse_sqrt", # scheduler (Appendix C of the paper)
num_train_epochs=5, # Appendix C of the paper
warmup_steps=200, # warm-up steps (Appendix C of the paper)
per_device_train_batch_size=8, # batch size (Appendix C of the paper)
)Contrastive Preference Optimization
Papers relating to the [experimental.cpo.CPOTrainer]
Contrastive Preference Optimization: Pushing the Boundaries of LLM Performance in Machine Translation
π Paper: https://huggingface.co/papers/2401.08417
Introduces Contrastive Preference Optimization (CPO), a preference-based method for machine translation that trains models to avoid adequate-but-imperfect translations instead of mimicking references as in SFT. The paper analyzes limitations of SFT on MT (including reference quality issues) and shows that applying CPO to ALMA with only 22K parallel sentences yields ALMA-R, which matches or exceeds WMT competition winners and GPT-4 on WMT'21βWMT'23. Used in TRL via [experimental.cpo.CPOTrainer]. To reproduce the paper's setting, use this configuration:
from trl.experimental.cpo import CPOConfig
training_args = CPOConfig(
loss_type="sigmoid", # preference learning loss (Section 3 of the paper)
cpo_alpha=1.0, # NLL regularizer weight (Section 3 of the paper)
beta=0.1, # Ξ² temperature (Section 4.2 of the paper)
learning_rate=1e-4, # learning rate (official code)
lr_scheduler_type="inverse_sqrt", # scheduler (official code)
num_train_epochs=1, # Section 4.2 of the paper
warmup_ratio=0.01, # warm-up ratio (Section 4.2 of the paper)
max_length=512, # max sequence length (Section 4.2 of the paper)
)SimPO: Simple Preference Optimization with a Reference-Free Reward
π Paper: https://huggingface.co/papers/2405.14734
SimPO is a simpler yet more effective preference optimization approach that uses the average log probability of a sequence as the implicit reward, eliminating the need for a reference model. It introduces a target reward margin to the Bradley-Terry objective to encourage a larger margin between winning and losing responses. To reproduce the paper's setting, use this configuration:
from trl.experimental.cpo import CPOConfig
training_args = CPOConfig(
loss_type="simpo", # SimPO loss (Section 3 of the paper)
cpo_alpha=0.0, # no BC regularizer for SimPO
beta=2.5, # Ξ² in Appendix B of the paper
simpo_gamma=1.375, # Ξ³ target reward margin, from Ξ³/Ξ²=0.55 in Appendix B of the paper
learning_rate=1e-6, # learning rate in Appendix B of the paper
num_train_epochs=1, # Appendix B of the paper
)AlphaPO -- Reward shape matters for LLM alignment
π Paper: https://huggingface.co/papers/2501.03884
AlphaPO is a new Direct Alignment Algorithms (DAAs) method that leverages an alpha-parameter to help change the shape of the reward function beyond the standard log reward. AlphaPO helps maintain fine-grained control over likelihood displacement and over-optimization. To reproduce the paper's setting, use this configuration:
from trl.experimental.cpo import CPOConfig
# Mistral-Instruct from Table 3 of the paper
training_args = CPOConfig(
loss_type="alphapo",
alpha=0.25,
beta=2.5,
simpo_gamma=0.1,
learning_rate=7e-7,
...
)Triple Preference Optimization
Papers relating to the [experimental.tpo.TPOTrainer]
Triple Preference Optimization: Achieving Better Alignment using a Single Step Optimization
π Paper: https://huggingface.co/papers/2405.16681
Introduces Triple Preference Optimization (TPO), a preference learning method that aligns an LLM with three responses per prompt β a gold (reference) completion, a preferred (chosen) completion and a dispreferred (rejected) completion β in a single optimization step. TPO combines a contrastive objective on the (chosen, rejected) pair with a supervised NLL term on the gold response, removing the need for a separate SFT stage and the reference model used in DPO. Used in TRL via [experimental.tpo.TPOTrainer]. To reproduce the paper's setting (Llama-3-Base, 5K), use this configuration:
from trl.experimental.tpo import TPOConfig
training_args = TPOConfig(
loss_type="sigmoid", # contrastive loss between chosen and rejected (Section 3 of the paper)
tpo_alpha=1.0, # weight of the NLL term on the gold response (Section 3 of the paper)
beta=0.01, # Ξ² temperature (Table 6 of the paper)
learning_rate=5e-7, # Table 6 of the paper
num_train_epochs=1,
max_length=1024,
)To use the TPO-L variant (length-normalized log-probabilities with a target reward margin Ξ³), set loss_type="tpo-l" and tpo_l_gamma:
from trl.experimental.tpo import TPOConfig
training_args = TPOConfig(
loss_type="tpo-l", # length-normalized variant (Section 3 of the paper)
tpo_alpha=1.0,
beta=0.01,
tpo_l_gamma=0.5, # Ξ³ target reward margin (Table 6 of the paper, Llama-3-Base 5K)
learning_rate=5e-7,
num_train_epochs=1,
)Nash Learning from Human Feedback
Nash Learning from Human Feedback
π Paper: https://huggingface.co/papers/2312.00886
Introduces Nash-MD, an alternative to standard RLHF that learns a preference model conditioned on two inputs and finds a policy at the Nash equilibrium. Instead of optimizing against a reward model, Nash-MD produces policies that consistently generate responses preferred over those of any competing policy. The algorithm is based on mirror descent principles. TRL shipped an implementation as NashMDTrainer up to v1.13; it is no longer part of the library and remains available in the git history.
Reward Modeling
Papers relating to the [RewardTrainer]
Helping or Herding? Reward Model Ensembles Mitigate but do not Eliminate Reward Hacking
π Paper: https://huggingface.co/papers/2312.09244
This paper proposed an auxiliary loss function designed to directly learn a centered reward model. This auxiliary loss minimizes the squared sum of the rewards, encouraging the model to naturally produce mean-zero outputs and thereby resolving the issue of underdetermination.
$$
\mathcal{L}(\theta) = - \mathbb{E}{(x,y^+,y^-) \sim \mathcal{D}} \left[ \log \sigma(r\theta(x, y^+) - r_\theta(x, y^-)) \textcolor{red}{- \eta \cdot (r_\theta(x, y^+) + r_\theta(x, y^-))^2} \right].
$$
To use this auxiliary loss with [RewardTrainer], you can use the center_rewards_coefficient argument in [RewardConfig] as follows:
from trl import RewardConfig
training_args = RewardConfig(
center_rewards_coefficient=0.01, # Ξ· in the paper
...
)Llama 2: Open Foundation and Fine-Tuned Chat Models
π Paper: https://huggingface.co/papers/2307.09288
In this paper, the authors propose to leverage their preference ratings being decomposed as a scale of four points (e.g., significantly better) to provide more informative feedback to the reward model. This is done by adding a margin to the loss function, which encourages the reward model to assign larger gaps in scores for pairs with higher preference ratings.
$$
\mathcal{L}(\theta) = - \mathbb{E}{(x,y^+,y^-,\textcolor{red}{m}) \sim \mathcal{D}} \left[ \log \sigma(r\theta(x, y^+) - r_\theta(x, y^-) \textcolor{red}{- m}) \right].
$$
You can add a margin to the loss by adding a margin column to the dataset. The following example shows how to set up a the "Margin Small" setting of the paper.
def add_margin(example):
preference_to_margin = {
"significantly better": 1.0,
"better": 2.0/3.0,
"slightly better": 1.0/3.0,
"negligibly better / unsure": 0.0,
}
return {"margin": preference_to_margin[example["preference_label"]]}
dataset = dataset.map(add_margin)Online Direct Preference Optimization
Papers relating to the [experimental.online_dpo.OnlineDPOTrainer]
Direct Language Model Alignment from Online AI Feedback
π Paper: https://huggingface.co/papers/2402.04792
Online DPO improves direct alignment from preferences methods by providing real-time feedback from a model, outperforming both DPO and PPO methods.
To use Online DPO, you can use the [experimental.online_dpo.OnlineDPOTrainer].
Exploratory Preference Optimization: Harnessing Implicit Q*-Approximation for Sample-Efficient RLHF
π Paper: https://huggingface.co/papers/2405.21046
XPO augments the DPO objective with a novel and principled exploration bonus, empowering the algorithm to explore outside the support of the initial model and human feedback data. It is a one-line change to online DPO that is provably sample-efficient and converges to a near-optimal language model policy. TRL shipped an implementation as XPOTrainer up to v1.13; it is no longer part of the library and remains available in the git history.
Distillation
Papers relating to training a student model with the help of a teacher model.
On-Policy Distillation of Language Models: Learning from Self-Generated Mistakes
π Paper: https://huggingface.co/papers/2306.13649
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). [DistillationTrainer] implements the same generalized-JSD objective for the always-on-policy case. To reproduce the paper's setting, use this configuration:
from trl.experimental.gkd import GKDConfig
# XSum summarization task (Table A.1 of the paper)
training_args = GKDConfig(
lmbda=0.5, # Ξ» student data fraction (Section 3 of the paper)
beta=0.5, # Ξ² Generalized JSD interpolation, 0=KL, 1=reverse KL (Section 3 of the paper)
temperature=1.0, # student training temperature (Appendix A of the paper)
max_steps=40000, # training steps (Table A.1 of the paper)
learning_rate=3e-4, # learning rate (Table A.1 of the paper)
per_device_train_batch_size=32, # batch size (Table A.1 of the paper)
warmup_steps=2000, # warm-up steps (Table A.1 of the paper)
max_new_tokens=64, # max output tokens (Table A.1 of the paper)
)MOPD: Multi-Teacher On-Policy Distillation for Capability Integration in LLM Post-Training
π Paper: https://huggingface.co/papers/2606.30406
Structures post-training as three stages: general SFT, independent per-domain RL training of one expert per domain (e.g. verifiable-answer RL for math, sandboxed agent RL for software engineering), and a final MOPD stage that fuses the frozen domain experts into a single unified student. In that final stage, the student generates a trajectory per prompt, each trajectory is dispatched to its corresponding domain teacher (never averaged or ensembled across teachers), and the student is updated by minimizing the per-token reverse KL against that one teacher's distribution along the trajectory. Used in TRL via [experimental.async_distillation.AsyncDistillationTrainer], which implements this third, fusion stage: passing more than one entry in teacher_server_urls enables MOPD, with each row's teacher_id column selecting which (already-trained) teacher scores it. Use beta=1.0 to match the paper's reverse-KL objective.
On the Position Bias of On-Policy Distillation
π Paper: https://huggingface.co/papers/2606.22600
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".
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.
from trl.experimental.iw_opd import IWOPDConfig
# Table 6 and Algorithm 1 of the paper, mapped to IWOPDConfig where available.
training_args = IWOPDConfig(
distillation_objective="iw_opd",
iw_opd_gamma=0.5, # Ξ³ amplification, Algorithm 1 and Appendix C.3
lmbda=1.0, # fully on-policy rollouts
learning_rate=1e-5, # Table 6
per_device_train_batch_size=1, # Table 6 uses PPO micro-batch size 1 per GPU
gradient_accumulation_steps=32, # with 32 GPUs, this gives the paper's 1024-prompt batch
num_generations=1, # Table 6 rollout samples per prompt
temperature=1.0, # Table 6 training decoding temperature
top_p=1.0, # Table 6 training decoding top-p
max_prompt_length=2048, # Table 6
max_completion_length=16384, # Table 6
warmup_ratio=0.0, # Table 6
use_vllm=True, # Table 6 uses vLLM rollouts
vllm_sync_frequency=1, # refresh rollout policy after each update
save_steps=10, # Table 6 checkpoint frequency
eval_steps=10, # Table 6 validation frequency
)On-Policy Distillation
π° Blog: https://thinkingmachines.ai/blog/on-policy-distillation/
On-Policy Distillation involves a student model generating rollouts for each batch of training data. We subsequently obtain the probability distributions for each token of the rollouts from both the student and teacher models. The student model is then optimized to minimize the negative Kullback-Leibler (KL) divergence between its own token distributions and those of the teacher model.
| Method | Sampling | Reward signal |
|---|---|---|
| Supervised finetuning | off-policy | dense |
| Reinforcement learning | on-policy | sparse |
| On-policy distillation | on-policy | dense |
On-Policy Distillation has been shown to outperform SFT, GRPO and can be used to restore generalization capabilities lost during SFT.
Additionally on-policy distillation is more compute efficient and is less prone to overfitting when trained with limited data.
To train a model with on-policy distillation using TRL, you can use the following configuration, with the [DistillationTrainer] and [DistillationConfig]:
from trl import DistillationConfig
training_args = DistillationConfig(
beta=1.0, # to ensure reverse-kl as the loss function
teacher_model_name_or_path="teacher-model", # specify the teacher model
)Alternatively, you can use the [experimental.gkd.GKDTrainer] and [experimental.gkd.GKDConfig]:
from trl.experimental.gkd import GKDConfig
training_args = GKDConfig(
lmbda=1.0, # student produces rollouts for all batches
beta=1.0, # to ensure reverse-kl as the loss function
teacher_model_name_or_path="teacher-model", # specify the teacher model
)You can also use [experimental.gold.GOLDTrainer] for on-policy distillation; see Unlocking On-Policy Distillation for Any Model Family.
Unlocking On-Policy Distillation for Any Model Family
π Project: https://huggingface.co/spaces/HuggingFaceH4/general-on-policy-logit-distillation
Introduces General Online Logit Distillation (GOLD), which extends on-policy distillation to student/teacher pairs with different tokenizers and model families. GOLD aligns textual spans across tokenizers and can apply Universal Logit Distillation (ULD) so completion tokens are not dropped. Used in TRL via [experimental.gold.GOLDTrainer].
from trl.experimental.gold import GOLDConfig
training_args = GOLDConfig(
lmbda=1.0, # student produces rollouts for all batches
beta=1.0, # to ensure reverse-kl as the loss function
use_uld_loss=True, # Universal Logit Distillation for cross-tokenizer pairs
)Knowledge Distillation of Large Language Models
π Paper: https://huggingface.co/papers/2306.08543
MiniLLM is the first on-policy knowledge distillation method, which minimizes the sequence-level reverse KLD between the teacher and the student model and is optimized by reinforcement learning.
It is a generalized version of Think Machine Lab's On-Policy Distillation, with the option to add distribution-level single-step distillation signals (like GKD when beta=1) and long-context reverse KLD signals.
TRL shipped an implementation as MiniLLMTrainer up to v1.14; it is no longer part of the library and remains available in the git history.
Reinforcement Learning via Self-Distillation
π Paper: https://huggingface.co/papers/2601.20802
Self-Distillation Policy Optimization (SDPO) enhances reinforcement learning with verifiable rewards by converting rich textual feedback (e.g., runtime errors, judge evaluations) into a dense learning signal without any external teacher or explicit reward model. SDPO treats the current model conditioned on feedback as a self-teacher and distills its feedback-informed next-token predictions back into the policy. Notably, SDPO also outperforms baselines in standard RLVR environments that only return scalar feedback by using successful rollouts as implicit feedback for failed attempts.
from trl.experimental.sdpo import SDPOConfig, SDPOTrainer
training_args = SDPOConfig(
distillation_alpha=0.5, # Jensen-Shannon divergence (recommended)
distillation_mode="topk_logits", # Explicitly select top-K logit distillation
distillation_topk=100, # Required for top-K logit distillation
distillation_is_clip=2.0, # Importance sampling clipping
distillation_weight=1.0, # Convex weight: (1-w)*policy + w*distillation; 1.0 = pure distillation
use_successful_as_teacher=True, # Use successful rollouts as teacher
teacher_model_kind="ema", # Supported: "base", "live", "ema"
teacher_update_rate=0.05, # EMA update rate
include_environment_feedback=True, # required to use the dataset's privileged_context (defaults to False)
)
trainer = SDPOTrainer(
model="Qwen/Qwen2.5-1.5B-Instruct",
reward_funcs=...,
args=training_args,
train_dataset=...,
)
trainer.train()Expected dataset columns:
promptprivileged_contextfor optional environment feedback
For more details, see the SDPO Trainer documentation.
Self-Distillation Enables Continual Learning
π Paper: https://huggingface.co/papers/2601.19897
Self-Distilled Fine-Tuning (SDFT) performs on-policy self-distillation by generating completions during training, then distilling an explicit teacher-conditioned view of those same completions back into the student. The teacher is selected by teacher_model_kind: "base" (the initial student), "live" (the current student), or "ema" (an exponentially averaged teacher). The teacher prompt is composed internally from the student prompt plus the dataset privileged_context.
from datasets import Dataset
from trl.experimental.sdft import SDFTConfig, SDFTTrainer
dataset = Dataset.from_dict(
{
"prompt": [[{"role": "user", "content": "Solve 2+2."}]],
"privileged_context": ["Example answer: 4."],
}
)
training_args = SDFTConfig(
distillation_alpha=0.5,
distillation_mode="topk_logits",
distillation_topk=5,
max_completion_length=64,
)
trainer = SDFTTrainer(
model="Qwen/Qwen2.5-1.5B-Instruct",
args=training_args,
train_dataset=dataset,
)
trainer.train()Expected dataset columns:
promptprivileged_contextcontaining only the extra teacher-only information
For more details, see the SDFT Trainer documentation.
Embarrassingly Simple Self-Distillation Improves Code Generation
π Paper: https://huggingface.co/papers/2604.01193
Simple Self-Distillation (SSD) improves code generation by sampling completions from the model at a training-time temperature and truncation configuration, then fine-tuning on those raw, unverified samples with standard cross-entropy loss. No reward model, verifier, teacher model, or reinforcement learning is needed. SSD reshapes token distributions in a context-dependent way: suppressing distractor tails at "lock" positions (where syntax leaves little ambiguity) while preserving diversity at "fork" positions (where multiple valid continuations exist).
TRL shipped an implementation as SSDTrainer up to v1.15; it is no longer part of the library and remains available in the git history.
Distributed Training
ZeRO: Memory Optimizations Toward Training Trillion Parameter Models
π Paper: https://huggingface.co/papers/1910.02054
ZeRO (Zero Redundancy Optimizer) eliminates memory redundancies in data- and model-parallel training by partitioning optimizer states, gradients, and parameters across devices while retaining low communication volume and high computational granularity. This allows for the efficient training of large models that would otherwise not fit in GPU memory.
TRL supports ZeRO via the DeepSpeed integration. To use it, provide a DeepSpeed configuration file with your desired settings,
# config.yaml
distributed_type: DEEPSPEED
num_processes: 2
deepspeed_config:
zero_stage: 3and launch the training script using accelerate launch --config_file config_file.
accelerate launch --config_file config.yaml train.pySource Quickstart (`docs/source/quickstart.md`)
Quickstart
TRL is a comprehensive library for post-training foundation models using techniques like Supervised Fine-Tuning (SFT), Group Relative Policy Optimization (GRPO), Direct Preference Optimization (DPO).
Quick Examples
Get started instantly with TRL's most popular trainers. Each example uses compact models for quick experimentation.
Supervised Fine-Tuning
from trl import SFTTrainer
from datasets import load_dataset
trainer = SFTTrainer(
model="Qwen/Qwen2.5-0.5B",
train_dataset=load_dataset("trl-lib/Capybara", split="train"),
)
trainer.train()Group Relative Policy Optimization
from trl import GRPOTrainer
from datasets import load_dataset
from trl.rewards import accuracy_reward
trainer = GRPOTrainer(
model="Qwen/Qwen2.5-0.5B-Instruct",
train_dataset=load_dataset("trl-lib/DeepMath-103K", split="train"),
reward_funcs=accuracy_reward,
)
trainer.train()Direct Preference Optimization
from trl import DPOTrainer
from datasets import load_dataset
trainer = DPOTrainer(
model="Qwen/Qwen2.5-0.5B-Instruct",
train_dataset=load_dataset("trl-lib/ultrafeedback_binarized", split="train"),
)
trainer.train()Reward Modeling
from trl import RewardTrainer
from datasets import load_dataset
dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train")
trainer = RewardTrainer(
model="Qwen/Qwen2.5-0.5B-Instruct",
train_dataset=dataset,
)
trainer.train()Command Line Interface
Skip the code entirely - train directly from your terminal:
# SFT: Fine-tune on instructions
trl sft --model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name trl-lib/Capybara
# DPO: Align with preferences
trl dpo --model_name_or_path Qwen/Qwen2.5-0.5B-Instruct \
--dataset_name trl-lib/ultrafeedback_binarized
# Reward: Train a reward model
trl reward --model_name_or_path Qwen/Qwen2.5-0.5B-Instruct \
--dataset_name trl-lib/ultrafeedback_binarizedWhat's Next?
π Learn More
- SFT Trainer - Complete SFT guide
- DPO Trainer - Preference alignment
- GRPO Trainer - Group relative policy optimization
π Scale Up
- Distributed Training - Multi-GPU setups
- Memory Optimization - Efficient training
- PEFT Integration - LoRA and QLoRA
π‘ Examples
- Example Scripts - Production-ready code
- Community Tutorials - External guides
Troubleshooting
Out of Memory?
Reduce batch size and enable optimizations:
training_args = SFTConfig(
per_device_train_batch_size=1, # Start small
gradient_accumulation_steps=8, # Maintain effective batch size
)training_args = DPOConfig(
per_device_train_batch_size=1, # Start small
gradient_accumulation_steps=8, # Maintain effective batch size
)training_args = GRPOConfig(
per_device_train_batch_size=1, # Start small
gradient_accumulation_steps=8, # Maintain effective batch size
num_generations=4, # Reduce from default 8 (GRPO generates num_generations completions per prompt)
max_completion_length=512, # Tune based on task; longer sequences cost more memory
)Loss not decreasing?
Try adjusting the learning rate:
training_args = SFTConfig(learning_rate=2e-5) # Good starting pointFor more help, open an issue on GitHub.
Source Toctree.Yml (`docs/source/_toctree.yml`)
- sections:
- local: index
title: TRL - local: installation
title: Installation - local: quickstart
title: Quickstart - local: usage_stats
title: Usage Stats Collection
title: Getting started
- local: index
- sections:
- local: chat_templates
title: Chat Templates - local: dataset_formats
title: Dataset Formats - local: paper_index
title: Paper Index
title: Conceptual Guides
- local: chat_templates
- sections: # Sorted alphabetically
- local: distillation_trainer
title: Distillation - local: dpo_trainer
title: DPO - local: grpo_trainer
title: GRPO - local: kto_trainer
title: KTO - local: reward_trainer
title: Reward - local: rloo_trainer
title: RLOO - local: sft_trainer
title: SFT
title: Trainers
- local: distillation_trainer
- sections:
- local: clis
title: Command Line Interface (CLI) - local: jobs_training
title: Training using Jobs - local: customization
title: Customizing the Training - local: reducing_memory_usage
title: Reducing Memory Usage - local: speeding_up_training
title: Speeding Up Training - local: distributing_training
title: Distributing Training - local: aws_trainium
title: Training on AWS Trainium - local: amd_gpus
title: Training on AMD GPUs - local: long_context_training
title: Training Beyond 1M Tokens - local: use_model
title: Using Trained Models
title: How-to guides
- local: clis
- sections:
- local: deepspeed_integration
title: DeepSpeed - local: harbor
title: Harbor - local: kernels_hub
title: Kernels Hub - local: openenv
title: OpenEnv - local: openreward
title: OpenReward - local: peft_integration
title: PEFT - local: rapidfire_integration
title: RapidFire AI - local: trackio_integration
title: Trackio - local: unsloth_integration
title: Unsloth - local: vllm_integration
title: vLLM
title: Integrations
- local: deepspeed_integration
- sections:
- local: example_overview
title: Example Overview - local: community_tutorials
title: Community Tutorials - local: lora_without_regret
title: LoRA Without Regret
title: Examples
- local: example_overview
- sections:
- sections:
- local: chat_template_utils
title: Chat Template Utilities - local: data_utils
title: Data Utilities - local: script_utils
title: Script Utilities
title: Utilities
- local: chat_template_utils
- local: callbacks
title: Callbacks - local: rewards
title: Reward Functions
title: API
- sections:
- sections:
- local: experimental_overview
title: Experimental Overview - local: a2po_trainer # Sorted alphabetically
title: A2PO - local: async_distillation_trainer
title: Async Distillation - local: async_grpo_trainer
title: Asynchronous GRPO - local: bema_for_reference_model
title: BEMA for Reference Model - local: cpo_trainer
title: CPO - local: gkd_trainer
title: GKD - local: gmpo
title: GMPO - local: gold_trainer
title: GOLD - local: iw_opd_trainer
title: IW-OPD - local: merge_model_callback
title: MergeModelCallback - local: online_dpo_trainer
title: Online DPO - local: orpo_trainer
title: ORPO - local: sdft_trainer
title: SDFT - local: sdpo_trainer
title: SDPO - local: tpo_trainer
title: TPO
title: Experimental
isExpanded: false
- local: experimental_overview
Source A2po Trainer (`docs/source/a2po_trainer.md`)
A2PO
TRL supports A*-PO (Optimal Advantage Regression) as described in the paper Accelerating RL for LLM Reasoning with Optimal Advantage Regression by KiantΓ© Brantley, Mingyu Chen, Zhaolin Gao, Jason D. Lee, Wen Sun, Wenhao Zhan, and Xuezhou Zhang.
The abstract from the paper is the following:
Reinforcement learning (RL) has emerged as a powerful tool for fine-tuning large language models (LLMs) to improve complex reasoning abilities. However, state-of-the-art policy optimization methods often suffer from high computational overhead and memory consumption, primarily due to the need for multiple generations per prompt and the reliance on critic networks or advantage estimates of the current policy. In this paper, we propose A*-PO, a novel two-stage policy optimization framework that directly approximates the optimal advantage function and enables efficient training of LLMs for reasoning tasks. In the first stage, we leverage offline sampling from a reference policy to estimate the optimal value function V*, eliminating the need for costly online value estimation. In the second stage, we perform on-policy updates using a simple least-squares regression loss with only a single generation per prompt. Theoretically, we establish performance guarantees and prove that the KL-regularized RL objective can be optimized without requiring complex exploration strategies. Empirically, A*-PO achieves competitive performance across a wide range of mathematical reasoning benchmarks, while reducing training time by up to 2Γ and peak memory usage by over 30% compared to PPO, GRPO, and REBEL.
Usage
A*-PO assumes a binary, verifiable reward (r β {0, 1}) and runs in two stages:
- Offline value estimation. Before training,
num_value_samplescompletions are sampled from the reference policy for every prompt and scored withreward_funcs. The optimal valueV*(x) = Ξ²βΒ·log(mean_i exp(r(x, yα΅’)/Ξ²β))is estimated and cached per prompt. - On-policy regression. During training, a single completion is generated per prompt from the current policy. The loss is the squared error between the implicit reward
Ξ²βΒ·log(Ο(y|x)/Ο_ref(y|x))and the optimal advantager(x, y) β V*(x).
from trl.experimental.a2po import A2POConfig, A2POTrainer
# A*-PO assumes a binary, verifiable reward in {0, 1}.
def reward_correct(completions, ground_truth, **kwargs):
return [float(completion.strip() == truth) for completion, truth in zip(completions, ground_truth)]
training_args = A2POConfig(
output_dir="Qwen2.5-0.5B-A2PO",
num_value_samples=8, # Stage 1: samples per prompt from the reference policy to estimate V*
beta1=0.5, # Stage 1: KL temperature for the V* estimate
beta2=1e-3, # Stage 2: KL temperature for the regression target
)
trainer = A2POTrainer(
model="Qwen/Qwen2.5-0.5B",
reward_funcs=reward_correct,
args=training_args,
train_dataset=...,
)
trainer.train()Because V* is estimated entirely from reference-policy samples, A*-PO cannot exceed the reference policy's Pass@K. The official implementation can be found at ZhaolinGao/A-PO.
A2POTrainer
[[autodoc]] experimental.a2po.A2POTrainer
- train
- save_model
- push_to_hub
A2POConfig
[[autodoc]] experimental.a2po.A2POConfig
Source Amd Gpus (`docs/source/amd_gpus.md`)
Training on AMD GPUs
TRL trainers run on AMD Instinct GPUs without code changes. PyTorch's ROCm build exposes them as cuda devices, so the commands in the rest of the documentation work as they are.
Installation
Install PyTorch's ROCm build, then TRL in the same environment:
pip install torch torchvision --index-url https://download.pytorch.org/whl/rocm7.2
pip install trlAttention kernels
The FlashAttention kernels used in the examples of this documentation (kernels-community/flash-attn2, flash-attn3 and flash-attn4) have no ROCm build. On AMD GPUs, use kernels-community/aiter-flash-attn, downloaded from the Hub by the kernels library (pip install trl[kernels]):
from trl import SFTConfig
training_args = SFTConfig(model_init_kwargs={"attn_implementation": "kernels-community/aiter-flash-attn@v2"})The kernel has no attention-logit soft-capping, and Transformers skips attn_logit_softcapping without a warning, so models that use it, such as Gemma 2, train with uncapped attention.
Generation with vLLM
GRPO and RLOO with use_vllm=True need vLLM's ROCm build, see the vLLM installation guide.
Source Async Distillation Trainer (`docs/source/async_distillation_trainer.md`)
Async Distillation
[!IMPORTANT]
This trainer requiresvllmandtransformers>=5.2.0. For distributed training, only FSDP2 is supported (DeepSpeed ZeRO is not).Currently,
vllmandtransformershave conflicting dependency constraints. To work around this, install vLLM first and then force-install transformers:bashpip install vllm pip install 'transformers>=5.2.0' --no-deps
Overview
[experimental.async_distillation.AsyncDistillationTrainer] is the async counterpart to [~trl.experimental.distillation.DistillationTrainer], architected like [experimental.async_grpo.AsyncGRPOTrainer]: a background rollout worker generates the student's own on-policy completions and scores them against a teacher, while training proceeds concurrently instead of alternating between generation and gradient updates. Unlike the synchronous trainer, the teacher is never loaded locally β only a vLLM server URL is needed, so the teacher can run on entirely separate hardware from the student and trainer, or even be a much larger model than would otherwise fit alongside the student.
compute_loss minimizes a generalized Jensen-Shannon Divergence between the student's and teacher's per-position token distributions (beta=0.0 is forward KL, beta=1.0 is reverse KL, values in between interpolate), the same objective [~trl.experimental.distillation.DistillationTrainer] and [~trl.experimental.server_distillation.ServerDistillationTrainer] use. Training is always on-policy: the student generates every completion it trains on.
Multi-teacher on-policy distillation (MOPD)
MOPD is not part of the 2306.13649 paper this trainer's core objective is based on (which describes a synchronous, single-teacher setting); it's a separate method, described in MOPD: Multi-Teacher On-Policy Distillation for Capability Integration in LLM Post-Training. There, MOPD is the third of three stages β general SFT, then independent per-domain RL training of one expert per domain, then MOPD fuses those frozen experts into a single student. AsyncDistillationTrainer implements that third, fusion stage only: the per-domain expert teachers must already exist (e.g. trained separately with [GRPOTrainer]/[RLOOTrainer]) and be served over HTTP before you point teacher_server_urls at them. The paper's own Stage 3 uses reverse KL (beta=1.0), not this trainer's default beta=0.0 (forward KL).
teacher_server_urls accepts more than one entry. With a single entry, every sample is scored by that one teacher (plain on-policy distillation). With multiple entries, each training sample's teacher_id column selects which teacher scores it β for example, routing math prompts to a math-specialist teacher and code prompts to a code-specialist teacher, each served independently. Each sample is dispatched to its one matching teacher, never averaged or ensembled across teachers. A sample with a missing or unmapped teacher_id raises rather than silently falling back to the wrong teacher. See examples/async_distillation_math/async_distillation_mopd.py for a runnable two-teacher example.
[!WARNING]
Every teacher must share the student's tokenizer. Completions travel to the teachers as raw token ids, and the candidate ids a teacher reports back index the student's own vocabulary directly incompute_loss. Teachers from the same model family as the student (as in the MOPD example, where a Qwen2.5 student is fused from Qwen2.5 and Qwen2.5-Coder experts) satisfy this; a teacher with a different vocabulary trains the student against the wrong tokens, silently unless its vocabulary is larger than the student's.
Checkpoint and resume: ignore_data_skip defaults to True; the base Trainer's skip-and-replay loop does not apply to a live rollout queue. Instead, the index of the first prompt not yet trained on is saved to rollout_state.json alongside each checkpoint and restored on resume, so the worker fast-forwards to that prompt without replaying samples. It is the trained position, not the generator's: the worker runs ahead of training by the rollout queue depth, and those buffered samples are lost when the run ends, so resuming from the generator's position would skip prompts that were generated but never trained on. Streaming datasets (IterableDataset) cannot be repositioned; their worker restarts from prompt 0 on resume.
How it differs from [`~trl.experimental.distillation.DistillationTrainer`]
In [~trl.experimental.distillation.DistillationTrainer], the teacher is a locally loaded model: generation, teacher forward pass, and the gradient update all happen sequentially in the same process. [experimental.async_distillation.AsyncDistillationTrainer] separates these concerns the same way [experimental.async_grpo.AsyncGRPOTrainer] separates GRPO's rollout from its update:
- Rollout worker (background process) β generates completions from the student's vLLM server, sends the full sequence back to the resolved teacher's
/v1/completionswithprompt_logprobsfor teacher-forced scoring (no new tokens generated by the teacher), and pushes ready-to-train samples into a queue. - Training loop (main process) β pulls samples from the queue, computes the generalized-JSD loss, and updates the student's weights.
Because the teacher is scored over HTTP rather than a local forward pass, only a sparse, top-k slice of its distribution is ever transmitted (teacher_top_k), not the full vocabulary β see [experimental.async_distillation.AsyncDistillationConfig]'s beta and teacher_top_k documentation for exactly which candidates the wire protocol guarantees a teacher logprob for at each beta regime.
After every weight_sync_steps training steps, the updated student weights are transferred to its vLLM server via NCCL. As with [experimental.async_grpo.AsyncGRPOTrainer], generation runs ahead of training, so samples may reflect a slightly stale policy; max_staleness controls how many weight updates a sample can lag behind before being discarded.
Quick start
# train_async_distillation.py
from datasets import load_dataset
from trl.experimental.async_distillation import AsyncDistillationTrainer
dataset = load_dataset("trl-lib/DeepMath-103K", split="train")
trainer = AsyncDistillationTrainer(
model="Qwen/Qwen2.5-0.5B-Instruct",
train_dataset=dataset,
)
trainer.train()The teacher server, the student's vLLM server, and the trainer must run on separate GPUs. Both are plain vllm serve instances, but they need different flags: the teacher is only ever scored (--logprobs-mode processed_logprobs so teacher_temperature reaches its returned logprobs, --max-logprobs -1 so teacher_top_k can exceed vLLM's default cap of 20), while the student is only ever generated from and needs NCCL weight transfer enabled so the trainer can push updated weights into it:
# Terminal 1: teacher server on GPU 0 (static, never updated)
CUDA_VISIBLE_DEVICES=0 vllm serve Qwen/Qwen2.5-1.5B-Instruct \
--port 8001 \
--logprobs-mode processed_logprobs \
--max-logprobs -1# Terminal 2: student's vLLM server on GPU 1 (dev mode + NCCL weight transfer are required)
CUDA_VISIBLE_DEVICES=1 VLLM_SERVER_DEV_MODE=1 vllm serve Qwen/Qwen2.5-0.5B-Instruct \
--port 8000 \
--weight-transfer-config '{"backend":"nccl"}'# Terminal 3: training on GPU 2
CUDA_VISIBLE_DEVICES=2 accelerate launch train_async_distillation.pyLoRA
Pass a peft_config to train a LoRA adapter on the student instead of the full model:
from peft import LoraConfig
trainer = AsyncDistillationTrainer(
model="Qwen/Qwen2.5-0.5B-Instruct",
train_dataset=dataset,
peft_config=LoraConfig(r=32, lora_alpha=64, target_modules="all-linear"),
)Every weight sync has to get the updated student to its server. By default the trainer merges the adapter into the base model and sends the full merged weights, which is around 15 GB for a 7B model on every sync. Start the student's vLLM server with --enable-lora and it sends only the adapter instead, about 160 MB at r=32:
CUDA_VISIBLE_DEVICES=1 VLLM_SERVER_DEV_MODE=1 VLLM_ALLOW_RUNTIME_LORA_UPDATING=1 vllm serve Qwen/Qwen2.5-0.5B-Instruct \
--port 8000 \
--weight-transfer-config '{"backend":"nccl"}' \
--enable-lora \
--max-lora-rank 32 \
--max-loras 6VLLM_ALLOW_RUNTIME_LORA_UPDATING=1 exposes the endpoint the trainer posts each new adapter to. Add VLLM_WORKER_MULTIPROC_METHOD=spawn if you serve tensor-parallel, and keep --weight-transfer-config either way: the trainer only chooses a sync mode when it starts, by which point the server is already up, and merged sync is the fallback. The teacher server is unaffected: it is never updated.
--max-lora-rank must be one of 1, 8, 16, 32, 64, 128, 256, 320, 512. It sets the highest rank the server can serve rather than the rank it will serve, so an r=4 adapter works fine under 8. --max-loras must be at least max_staleness + 2: the trainer keeps max_staleness + 1 adapter versions registered so a rollout that started under an older policy can finish under it instead of switching policies mid-generation, and each sync loads the next version before it unloads the oldest. The trainer checks both values when it starts and tells you what to restart the server with.
vLLM loads adapters from disk rather than over the network, so the trainer writes each version under <output_dir>/.vllm_lora/ and gives the server the path. If the server runs on a different machine, that directory has to be on a filesystem both of them can see.
[!WARNING]
<output_dir>/.vllm_lora/is a serving cache, not where your trained adapter is kept. It only holds the most recent versions, and each one is deleted shortly after it falls out of the staleness window. Checkpoints are what preserve the adapter you trained, so do not turn them off: withsave_strategy="no"a multi-hour run finishes and leaves nothing behind.output_diralso has to be readable by the server and has to outlive the job, so node-local scratch will not work, even though it would keep syncs off a slow shared filesystem.
[!TIP]
Adapter sync is not always faster. vLLM generates roughly 1.33β1.39x slower per token when it serves a LoRA adapter than when it serves merged weights, and generation is usually what dominates wall-clock time. What you get in return is a much shorter pause at each sync and about 1% of the sync bandwidth. If generation throughput is your bottleneck, merged sync is often still the better choice.
Logged metrics
A rollout passes through several stages before it becomes a gradient. Let's first take a look at the different stages, from a prompt to our final batch composition:
Dataset row = PROMPT (message list + optional `teacher_id`)
ββ ROLLOUT (1 prompt -> 1 student completion -> 1 teacher scoring call)
ββ generate (student's vLLM /v1/completions, sampled)
ββ score (the routed teacher's /v1/completions with `prompt_logprobs`, teacher-forced)
ββ SAMPLE (prompt + completion + the teacher's sparse per-position distribution)
ββββββββββββ Trainer process boundary: `rollout_buffer` (mp.Queue) ββββββββββββ
ββ SAMPLE (Pulled 1 at a time; dropped if staleness > `max_staleness`)
ββ ROW (Planner assigns it to one of `dp` rows, Ξ£ Lα΅’Β²-balanced)
ββ MICRO-BATCH (`dp` rows, one per rank)
ββ PACKED ROW (1 concat sequence, `position_ids` reset per sample)
ββ FORWARD (`compute_loss`, bs=1, inter-rank padding stripped)
ββ OPTIMIZER STEP (from `grad_accum` micro-batches)Reading it top to bottom:
- a rollout is one prompt generated once and scored once. There is no group: distillation has no advantage baseline to compute across generations, so a prompt is not repeated and one rollout yields exactly one training sample.
- a sample is that completion plus the teacher's top-
teacher_top_kcandidates per completion position. This is what crosses the process boundary. - past the boundary the trainer plans samples into rows (one per DP rank), packs each row into one concatenated sequence, and accumulates
grad_accummicro-batches into a single optimizer step.
So we split our metrics based on which stage (or entity) they are about:
| namespace | entity it counts |
|---|---|
rollout/ |
one generate-and-score round trip: how long it took, how far it got |
completions/ |
what the student (vLLM) generated for one prompt |
sample/ |
represents one training sample, as it arrives in the queue |
batch/ |
one micro-batch or one optimizer step (built from one or multiple samples) |
perf/ |
measured seconds and FLOPs |
Everything the loss itself measures is logged unprefixed (jsd, entropy, teacher_entropy), with a teacher_*/<teacher_id> breakdown under MOPD β see the divergence below.
Let's also define some terms to make token metrics unambiguous:
- generated tokens are what the student produced (
completions/*). - forwarded tokens are every token the forward pass processes: the prompt and the generated tokens.
- trained tokens are the subset the loss is taken over where
completion_mask == 1. Trained β generated: a completion position the teacher scored no candidate for is masked out of the divergence but still forwarded.
How the batch numbers compose
A row is what one DP rank forwards in one micro-batch: several samples concatenated into a single sequence, with position_ids restarting at each sample boundary. The planner decides which samples land in which row (balancing Ξ£ Lα΅’Β² so no rank straggles), and a data collator does the concatenating.
In all our metrics a "step" always means one FULL optimizer step, never a micro-batch. gradient_accumulation_steps micro-batches make one step, and every metric named _per_step is per optimizer step, matching global_step, logging_steps and the rest of the [Trainer] vocabulary. Where a metric is per micro-batch it says so (batch/microbatches_per_step) or it is a per-row quantity (batch/row_*, batch/samples_per_row).
One step therefore holds a fixed number of row-slots:
row-slots per step = gradient_accumulation_steps x world_sizewhich makes the batch metrics compose into a ladder you can check against each other:
sample sample/forwarded_tokens_mean
ββ packed into a ROW batch/samples_per_row, batch/row_tokens_mean
ββ one row per DP rank = one micro-batch
ββ gradient_accumulation_steps micro-batches = one STEP
batch/samples_per_step, batch/forwarded_tokens_per_stepSo batch/samples_per_step β row-slots x batch/samples_per_row, and likewise for tokens. The two sides agree only up to the variation between micro-batches inside the step β the per-step metrics are sums, the per-row ones are means β so expect a fraction of a percent, not an exact match.
batch/row_fill_frac is the one to watch when samples are long: a 10k-token sample tiles a 32k budget badly (three fit, four never do, so the packer often gets two and the row runs ~77% full), while 1k-token samples tile it almost perfectly. That is quantization, not a bug, and token_budget is the lever β bearing in mind that attention is O(LΒ²) per sequence, so a fuller row of long sequences does not cost linearly more memory.
The rollout queue
The worker pushes scored samples into a queue and the trainer pulls from it. Four metrics describe that one queue, and they answer different questions:
| metric | question |
|---|---|
sample/rollout_queue_size |
how many samples are waiting right now |
sample/time_in_queue_s |
how long a single sample sat there before being trained on β the seconds half of its off-policyness |
perf/rollout_wait_s |
how long training sat blocked because the queue was empty |
rollout/backpressure_s |
how long generation (the worker) sat blocked because the queue was full |
The last two metrics (perf/rollout_wait_s and rollout/backpressure_s) are mirror images and are never both large! Reading them together with the queue size tells you which side is the bottleneck:
- queue near empty,
perf/rollout_wait_shigh β generation-bound. The trainer is starving; look atrollout/generated_tok_s,rollout/inflightandrollout/score_s. - queue near full,
rollout/backpressure_shigh β trainer-bound. Generation is throttled and its output is aging in the queue, so watchsample/staleness_meanclimb. - both near zero β balanced !
Completions
A completion is what the student's vLLM server generated for one prompt.
| metric | meaning |
|---|---|
completions/mean_length |
generated tokens per rollout |
completions/min_length, completions/max_length |
shortest and longest completion in the window |
completions/clipped_ratio |
fraction of completions that did not end on EOS, i.e. were cut off by max_completion_length |
Rollouts
A rollout is one prompt taken all the way through: generated by the student, then teacher-forced through its teacher. Because the teacher call is on the critical path of every rollout (unlike GRPO, where scoring is a separate loop over completed groups), a slow teacher shows up directly as rollout/duration_s.
| metric | meaning |
|---|---|
rollout/duration_s |
wall time for one rollout, from dispatch to a scored sample: generation and the teacher call |
rollout/score_s |
of that, the time the teacher's /v1/completions call took |
teacher_score_s/ |
MOPD only: the same, per teacher. One slow expert throttles only the rollouts routed to it, which the blended mean hides |
rollout/generated_tok_s |
generation throughput over the last interval (windowed), so a stall shows up |
rollout/inflight |
rollouts in flight, i.e. generating or being scored |
rollout/vllm_retry_total |
retried vLLM requests, to either the student's server or a teacher's. A degraded server otherwise looks like unexplained slowness. It sits here rather than in completions/ because it counts requests to a server, not generated text |
rollout/backpressure_s |
how long generation was blocked because the rollout queue was full. See the rollout queue |
Samples arriving from the queue
Each sample is one RolloutSample: a prompt, the student's completion, and the teacher's sparse scoring of it. The worker pushes them into rollout_buffer; everything below is measured on the trainer side, as it pulls them for training.
| metric | meaning |
|---|---|
sample/forwarded_tokens_mean, sample/forwarded_tokens_max |
tokens in one sample: prompt + generated. Packing is a row-level concern, see batch/row_* |
sample/trained_tokens_mean |
of those, how many the loss is taken over |
sample/rollout_queue_size |
scored samples waiting in the queue |
sample/time_in_queue_s |
how long this sample sat in that queue before being trained on. Not the same as perf/rollout_wait_s β see the rollout queue |
sample/staleness_mean, sample/staleness_max |
how many policy versions behind the data is. jsd shows the effect of off-policyness on the loss; this shows the cause |
sample/dropped_stale_total |
samples discarded for exceeding max_staleness |
Batches
One micro-batch is world_size rows (remember packing flattens into 1 sequence), so one per DP rank, so that rank i forwards row i. gradient_accumulation_steps of those make one optimizer step. A step then covers gradient_accumulation_steps Γ world_size rows in total. Every _per_step metric below is a sum over the whole step and across every rank; the batch/row_* ones are means over the rows.
| metric | meaning |
|---|---|
batch/forwarded_tokens_per_step, batch/trained_tokens_per_step |
tokens in one optimizer step, forwarded and trained respectively |
batch/samples_per_step |
training samples per optimizer step |
batch/microbatches_per_step |
counted, not read off the config |
batch/masked_token_frac |
forwarded tokens with completion_mask == 0 β the share of the forward that earns no gradient |
batch/samples_per_row, batch/row_tokens_mean, batch/row_tokens_max |
how densely the planner packed each rank's row |
batch/row_fill_frac |
row tokens against token_budget. Low means the budget is not being used |
batch/row_imbalance |
max Ξ£ Lα΅’Β² / mean Ξ£ Lα΅’Β² across rows. Attention is O(LΒ²), so this predicts which rank stalls the gradient all-reduce. 1.0 is perfect |
batch/pad_frac |
inter-rank padding. Costs broadcast bytes only; it is stripped before the forward |
batch/dropped_oversize_total |
samples dropped for exceeding token_budget |
The divergence
What the objective itself measures, averaged over the trained tokens of the window. These are the numbers to watch for learning, as opposed to for throughput.
| metric | meaning |
|---|---|
jsd |
the generalized JSD the loss minimizes, at the configured beta. Falling means the student's distribution is converging on the teacher's |
entropy |
the student's own predictive entropy. A collapse here with a falling jsd is the student narrowing rather than learning |
teacher_entropy |
the teacher's entropy over the candidates it reported. Bounded below the true value, since only teacher_top_k candidates cross the wire |
teacher_jsd/ |
MOPD only: jsd restricted to the tokens that teacher scored. Teachers in different domains can diverge at very different rates, which the blended jsd conflates |
teacher_entropy/ |
MOPD only: the same breakdown of teacher_entropy |
teacher_token_frac/ |
MOPD only: the share of scored tokens that teacher took. Routing skew is otherwise invisible: a teacher starved of rows still reports a healthy teacher_jsd/ |
There is no per-teacher entropy: the student's entropy is a property of its own policy, not of which teacher scored the sample, so the blended metric already covers it.
Performance
Throughput measures how many tokens the trainer processes per second. Model FLOPs Utilization (MFU) estimates the percentage of the training devices' theoretical compute capacity used for model forward and backward passes. The capacity used for this estimate depends on the hardware and training precision.
Throughput and MFU are reported on two time bases:
_fwd_bwdmeasures performance during the forward and backward passes._wall_clockmeasures performance over the complete optimizer step, including optimizer updates, weight synchronization, and time spent waiting for rollouts.
The difference helps distinguish compute performance from the cost of waiting and coordination. Both MFU metrics describe the training devices only: they do not include the computation or hardware capacity of external rollout or teacher servers. Wall-clock MFU reflects the time the trainer spends waiting for those services, not the services' own utilization.
MFU is reported only when peak compute capacity is known for the local training device at the selected precision. Otherwise, MFU is omitted; training continues and throughput and timing metrics remain available.
| metric | meaning |
|---|---|
perf/step_s |
wall time between optimizer steps: compute, optimizer, weight sync and queue waits included |
perf/fwd_bwd_s |
forward + backward, summed over the step's micro-batches. This is the denominator of every _fwd_bwd metric below |
perf/fwd_s |
the forward part of it. fwd_s / fwd_bwd_s near 1/3 is the usual split; higher means the backward is cheap or recompute is being paid on the forward |
perf/optimizer_s |
optimizer.step() |
perf/rollout_wait_s |
how long the trainer sat blocked because the queue was empty. See the rollout queue |
perf/weight_sync_s |
a full sync, plus _pause_s (waiting for vLLM), _barrier_s (rank skew) and _transfer_s (the bytes) |
perf/forwarded_tok_s_fwd_bwd, perf/forwarded_tok_s_wall_clock |
forwarded tokens per second on each basis |
perf/trained_tok_s_wall_clock |
the same, counting only tokens the loss saw |
perf/mfu_fwd_bwd, perf/mfu_wall_clock |
model FLOPs utilisation on each basis |
Design philosophy
This trainer is intentionally kept minimal and is not meant to grow into a general-purpose solution. If you need a feature that is not supported, we recommend cloning the repository and adapting the trainer to your needs directly. New features will only be considered when there is significant community demand.
AsyncDistillationConfig
[[autodoc]] trl.experimental.async_distillation.AsyncDistillationConfig
AsyncDistillationTrainer
[[autodoc]] trl.experimental.async_distillation.AsyncDistillationTrainer
Source Async Grpo Trainer (`docs/source/async_grpo_trainer.md`)
Asynchronous GRPO
[!IMPORTANT]
This trainer requiresvllmandtransformers>=5.2.0. For distributed training, only FSDP2 is supported (DeepSpeed ZeRO is not).Currently,
vllmandtransformershave conflicting dependency constraints. To work around this, install vLLM first and then force-install transformers:bashpip install vllm pip install 'transformers>=5.2.0' --no-deps
Overview
[experimental.async_grpo.AsyncGRPOTrainer] implements the same GRPO algorithm but decouples rollout generation from training. A background worker continuously streams completions from a vLLM server while the training loop consumes them, so generation and gradient updates overlap instead of alternating. The API mirrors [GRPOTrainer] β for full details on the GRPO method itself (advantage computation, KL estimation, loss formulation, reward functions, etc.), see the GRPO Trainer documentation. Not all features from [GRPOTrainer] are available; refer to [experimental.async_grpo.AsyncGRPOConfig] for the supported parameters.
This trainer was contributed by Quentin GallouΓ©dec and Amine Dirhoussi.
How it differs from [`GRPOTrainer`]
In the standard [GRPOTrainer], generation and training are sequential: generate a batch, compute the loss, update weights, repeat. Even in vLLM colocate mode, where generation runs on the same GPUs, one phase must finish before the other begins.
[experimental.async_grpo.AsyncGRPOTrainer] separates these two concerns:
- Rollout worker (background process) β sends prompts to a vLLM server, scores completions with reward functions, computes advantages, and pushes ready-to-train samples into a queue.
- Training loop (main process) β pulls samples from the queue, computes the clipped surrogate loss, and updates the model weights.
The rollout worker runs in a separate process spawned from the trainer, so reward computation never contends with the training loop for the GIL. This has two consequences for what you can pass as reward_funcs, tools, and environment_factory (for the latter, see the OpenEnv guide, which covers the contract and the available integrations):
[!WARNING]
Because we run the rollout worker in a separate process, everything passed to it is pickled. Each reward function, tool, andenvironment_factory(and anything they close over) must therefore be picklable: use a module-level function,functools.partial, or a callable class instance. Tools are the exception: the model calls a tool by its name, so each tool is registered under its__name__and must be a module-level function. Lambdas and closures will raise aTypeErrorattrainer.train(). This is a difference from [GRPOTrainer], where reward functions are called in-process and closures work.The rollout process also runs with
CUDA_VISIBLE_DEVICES="", so it cannot use the GPU. A GPU-backed reward model (e.g. anAutoModelForSequenceClassificationscorer) still loads without error but silently falls back to CPU (note that in [GRPOTrainer], such a reward model shares the trainer's GPUs). Keep reward functions CPU-side and lightweight (verifiers likeaccuracy_reward, format/length checks).If you do need a GPU reward model, the recommended approach is to serve it behind its own inference engine (vLLM, TGI, β¦) on separate GPUs and have a lightweight, picklable reward function call it over HTTP. This keeps the reward model on its own device while the rollout process stays CPU-only, and it scales independently of the trainer.
Tools and environment methods can be synchronous or asynchronous. Synchronous tools run on a thread pool sized to max_inflight_tasks, so a slow tool never blocks the other in-flight vLLM requests. Within one assistant turn, tool calls run in the order the model emitted them. Up to max_inflight_tasks synchronous tools therefore run at once, so a tool that reads or writes shared state must be thread-safe.
After every weight_sync_steps training steps, the updated weights are transferred to the vLLM server via NCCL so that subsequent generations reflect the latest policy.
Because generation and training run concurrently, the training samples may have been generated by a slightly older version of the model. The max_staleness parameter controls how many weight updates a sample can lag behind before being discarded.
Concurrency
The number of concurrent requests sent to the vLLM server is capped by max_inflight_tasks. By default it is set automatically to max_staleness Γ per_device_train_batch_size Γ gradient_accumulation_steps Γ num_processes β the maximum number of samples the trainer can consume before they become stale. Generating more than this is wasteful since the excess samples will be discarded.
max_inflight_tasks is a ceiling, not the number actually in flight. The worker polls the server's /metrics endpoint every few seconds and keeps a live cap underneath it (rollout/inflight_cap): past a certain concurrency, more requests make vLLM slower, because the KV cache fills, sequences are preempted and recomputed, and cached prefixes are evicted and re-prefilled. The cap starts at KV cache tokens / max_model_len, read from the server at startup: as many requests as fit at full length, so the first step cannot thrash. While usage stays under 0.6 with nothing queued, the cap grows by 25% each time a cap's worth of rollouts has completed, up to the ceiling. When KV cache usage passes 0.8, the cap is cut by 30%; no running request is cancelled, admissions just stop until the pool has drained below the new cap, and no further cut is taken until then. If the server does not expose /metrics, the cap stays at the ceiling.
Checkpoint and resume: ignore_data_skip defaults to True; the base Trainer's skip-and-replay loop does not apply to a live rollout queue. Instead, the index of the first prompt not yet trained on is saved to rollout_state.json alongside each checkpoint and restored on resume, so the worker fast-forwards to that prompt without replaying samples. It is the trained position, not the generator's: the worker runs ahead of training by the rollout queue depth, and those buffered samples are lost when the run ends, so resuming from the generator's position would skip prompts that were generated but never trained on. Streaming datasets (IterableDataset) cannot be repositioned; their worker restarts from prompt 0 on resume.
Quick start
# train_async_grpo.py
from datasets import load_dataset
from trl.experimental.async_grpo import AsyncGRPOTrainer
from trl.rewards import accuracy_reward
dataset = load_dataset("trl-lib/DeepMath-103K", split="train")
trainer = AsyncGRPOTrainer(
model="Qwen/Qwen3-4B",
reward_funcs=accuracy_reward,
train_dataset=dataset,
)
trainer.train()The vLLM server and the trainer must run on separate GPUs. Use CUDA_VISIBLE_DEVICES to partition your GPUs. For example, with 2 GPUs, you can run the vLLM server on GPU 0 and the trainer on GPU 1 as follows:
# Terminal 1: vLLM server on GPU 0 (dev mode + NCCL weight transfer are required)
CUDA_VISIBLE_DEVICES=0 VLLM_SERVER_DEV_MODE=1 vllm serve Qwen/Qwen3-4B \
--max-model-len 4096 \
--logprobs-mode processed_logprobs \
--weight-transfer-config '{"backend":"nccl"}'[!TIP]
Set--max-model-lento the maximum total sequence length (prompt + completion) you expect. A lower value reduces GPU memory usage on the server, freeing more memory for the KV cache and increasing throughput. A good starting point is the prompt length plusmax_completion_lengthfrom your config.
# Terminal 2: training on GPU 1
CUDA_VISIBLE_DEVICES=1 accelerate launch train_async_grpo.pyAMD GPUs
The trainer runs on AMD GPUs (ROCm) with no extra setup: the commands above work as they are. Training uses the kernels-community/aiter-flash-attn attention kernel on AMD GPUs, kernels-community/flash-attn4 on NVIDIA Blackwell GPUs and kernels-community/flash-attn3 on other NVIDIA GPUs, all downloaded from the Hub by the kernels library. Models that cap their attention logits (attn_logit_softcapping), such as Gemma 2, are not supported on AMD GPUs or on NVIDIA Blackwell GPUs.
Vision-language models
Vision-language models (Qwen3.5, Qwen3.6, Qwen3-VL, β¦) can be trained on text-only datasets. Pass the model id as usual; nothing else changes:
CUDA_VISIBLE_DEVICES=0 VLLM_SERVER_DEV_MODE=1 vllm serve Qwen/Qwen3.5-2B \
--max-model-len 4096 \
--logprobs-mode processed_logprobs \
--weight-transfer-config '{"backend":"nccl"}'The vision tower is frozen. A text-only dataset never produces image tokens, so the tower is never exercised by the forward pass. Everything outside the text tower (vision tower, multimodal projector) has requires_grad=False: it gets no gradients and no optimizer state, and weight sync skips it entirely β the server keeps the values it loaded from the checkpoint. The tower is still loaded, so it costs GPU memory for its weights.
[!WARNING]
Images are not supported yet. Prompts containing images are not passed to the vLLM server or to the training forward pass. Multimodal training also needs the padding-free packing path to build 3D M-RoPE positions from the image grid, which is not implemented here.
[!WARNING]
Hybrid models (Qwen3.5, Qwen3.6) needflash-linear-attentioninstalled, or their gated-DeltaNet layers silently fall back to a pure-PyTorch scan that costs ~20x (measured on Qwen3.5-2B: 1046 vs 52 Β΅s/token). Those layers also carry recurrent state across a padding-free packed row, which the trainer does not reset at sample boundaries, so their training log-probs drift from what the server generated as more sequences are packed per row.
Top-k and top-p sampling
Top-k and top-p renormalize the sampling distribution over a restricted candidate set rather than the whole vocabulary. The server's log-probabilities reflect that (--logprobs-mode processed_logprobs), so the trainer has to normalize over the same set, otherwise every importance ratio is biased: a token's probability under the whole vocabulary is lower than under the candidate set it was actually drawn from (DeepSeek-V3.2, section 3.1). With top_p < 1 or top_k > 0, the worker asks the server for the candidate set of each sampled token and the trainer renormalizes its log-probabilities over it. This needs two more server flags, and top_k has to be set, since vLLM bounds the size of the candidate set with it:
CUDA_VISIBLE_DEVICES=0 VLLM_SERVER_DEV_MODE=1 vllm serve Qwen/Qwen3-4B \
--max-model-len 4096 \
--logprobs-mode processed_logprobs \
--weight-transfer-config '{"backend":"nccl"}' \
--return-sampling-mask \
--enable-scale-outconfig = AsyncGRPOConfig(top_p=0.95, top_k=512)--return-sampling-mask makes vLLM record the candidate set (vLLM 0.28 or later, on its V2 model runner: VLLM_USE_V2_MODEL_RUNNER=1 if the server refuses the flag), --enable-scale-out exposes the token-in-token-out endpoint that returns it. The worker checks both on the server when it starts. An older vLLM cannot return the set, so the trainer keeps normalizing over the whole vocabulary and warns about the biased ratio.
LoRA
Pass a peft_config to train a LoRA adapter instead of the full model:
from peft import LoraConfig
trainer = AsyncGRPOTrainer(
model="Qwen/Qwen3-4B",
reward_funcs=reward_len,
train_dataset=dataset,
peft_config=LoraConfig(r=32, lora_alpha=64, target_modules="all-linear"),
)Every weight sync has to get the updated policy to the server. By default the trainer merges the adapter into the base model and sends the full merged weights, which is around 15 GB for a 7B model on every sync. Start the vLLM server with --enable-lora and it sends only the adapter instead, about 160 MB at r=32:
CUDA_VISIBLE_DEVICES=0 VLLM_SERVER_DEV_MODE=1 VLLM_ALLOW_RUNTIME_LORA_UPDATING=1 vllm serve Qwen/Qwen3-4B \
--max-model-len 4096 \
--logprobs-mode processed_logprobs \
--weight-transfer-config '{"backend":"nccl"}' \
--enable-lora \
--max-lora-rank 32 \
--max-loras 6VLLM_ALLOW_RUNTIME_LORA_UPDATING=1 exposes the endpoint the trainer posts each new adapter to. Keep --weight-transfer-config even with --enable-lora: the trainer only chooses a sync mode when it starts, by which point the server is already up, and merged sync is the fallback.
--max-lora-rank must be one of 1, 8, 16, 32, 64, 128, 256, 320, 512. It sets the highest rank the server can serve rather than the rank it will serve, so an r=4 adapter works fine under 8. --max-loras must be at least max_staleness + 2: the trainer keeps max_staleness + 1 adapter versions registered so a rollout that started under an older policy can finish under it instead of switching policies mid-generation, and each sync loads the next version before it unloads the oldest. The trainer checks both values when it starts and tells you what to restart the server with.
vLLM loads adapters from disk rather than over the network, so the trainer writes each version under <output_dir>/.vllm_lora/ and gives the server the path. If the server runs on a different machine, that directory has to be on a filesystem both of them can see.
[!WARNING]
<output_dir>/.vllm_lora/is a serving cache, not where your trained adapter is kept. It only holds the most recent versions, and each one is deleted shortly after it falls out of the staleness window. Checkpoints are what preserve the adapter you trained, so do not turn them off: withsave_strategy="no"a multi-hour run finishes and leaves nothing behind.output_diralso has to be readable by the server and has to outlive the job, so node-local scratch will not work, even though it would keep syncs off a slow shared filesystem.
[!TIP]
Adapter sync is not always faster. vLLM generates roughly 1.33β1.39x slower per token when it serves a LoRA adapter than when it serves merged weights, and generation is usually what dominates wall-clock time in RL. What you get in return is a much shorter pause at each sync and about 1% of the sync bandwidth. If generation throughput is your bottleneck, merged sync is often still the better choice.
Design philosophy
This trainer is intentionally kept minimal and is not meant to grow into a general-purpose solution. If you need a feature that is not supported, we recommend cloning the repository and adapting the trainer to your needs directly. New features will only be considered when there is significant community demand.
Logged metrics
A rollout passes through several stages before it becomes a gradient. Let's first take a look at the different stages, from a prompt to our final batch composition:
Dataset row = PROMPT (message list + reward kwargs)
ββ GROUP (1 prompt, `num_generations` rollouts, 1 advantage baseline)
ββ ROLLOUT (1 conversation, keyed by `rollout_id`)
ββ TURN (1 vLLM /v1/completions call + tool messages fed back)
β ββ TurnRecord (prompt_ids, output_ids, output_log_probs)
ββ reconcile (`_chain_to_sequences` classifies drift per turn: CLEAN/REALIGN/FORK)
ββ SEQUENCE (β₯1 per rollout; new one per fork; dropped if no trained token)
ββ SAMPLE (Sequence + group advantage + reward + metrics)
ββββββββββββ Trainer process boundary: `rollout_buffer` (mp.Queue) ββββββββββββ
ββ SAMPLE (Pulled 1 at a time; dropped if staleness > `max_staleness`)
ββ ROW (Planner inserts it into one of `dp` prefix tries, attention-cost-balanced)
ββ MICRO-BATCH (`dp` rows, one per rank)
ββ PACKED ROW (Collator lays each trie out: unique tokens, depth as `position_ids`)
ββ FORWARD (`compute_loss`, bs=1, padding stripped, FA3 per trie segment)
ββ OPTIMIZER STEP (from `grad_accum` micro-batches)Reading it top to bottom:
- a group is one prompt generated
num_generationstimes. The advantage baseline is computed inside it, which is why the group is also the unit that gets scored. - a rollout is one conversation. Every turn re-tokenizes the whole message list, so drift between turns has to be reconciled at the end.
- reconciling can fork one rollout into several sequences, which is why a single conversation can produce more than one training sample.
- a sample is a sequence plus its advantage, reward and metrics. This is what crosses the process boundary.
- past the boundary the trainer plans samples into rows (one per DP rank), folds each row into a prefix trie so shared tokens are forwarded once, and accumulates
grad_accummicro-batches into a single optimizer step.
So we split our metrics based on which stage (or entity) they are about:
| namespace | entity it counts |
|---|---|
rollout/ |
represents one full conversation: its turns, its forks, how long it took to generate |
completions/ |
what the model (vLLM) generated for one prompt |
tools/ |
tool calls metrics |
sample/ |
represents one training sample, as it arrives in the queue |
batch/ |
one micro-batch or one optimizer step (built from one or multiple samples) |
perf/ |
measured seconds and FLOPs |
Let's also define some terms to make token metrics unambiguous:
- generated tokens are what the model produced (
completions/*). - forwarded tokens are every token the forward pass processes: the prompt, any tool results, and the generated tokens.
- trained tokens are the subset the loss is taken over where
completion_mask == 1. For example, a fork or a realign can demote already-generated tokens to context, so trained β generated.
How the batch numbers compose
A row is what one DP rank forwards in one micro-batch: several samples packed into a single prefix forest, so tokens they share are forwarded once β see Tree packing. The planner decides which samples land in which row (balancing attention cost so no rank straggles), and a data collator does the packing.
In all our metrics a "step" always means one FULL optimizer step, never a micro-batch. gradient_accumulation_steps micro-batches make one step, and every metric named _per_step is per optimizer step, matching global_step, logging_steps and the rest of the [Trainer] vocabulary. Where a metric is per micro-batch it says so (batch/microbatches_per_step) or it is a per-row quantity (batch/row_*, batch/samples_per_row).
One step therefore holds a fixed number of row-slots:
row-slots per step = gradient_accumulation_steps x world_sizewhich makes the batch metrics compose into a ladder you can check against each other:
sample sample/forwarded_tokens_mean
ββ packed into a ROW batch/samples_per_row, batch/row_tokens_mean
ββ one row per DP rank = one micro-batch
ββ gradient_accumulation_steps micro-batches = one STEP
batch/samples_per_step, batch/forwarded_tokens_per_stepSo batch/samples_per_step β row-slots x batch/samples_per_row, and likewise for tokens. The two sides agree only up to the variation between micro-batches inside the step β the per-step metrics are sums, the per-row ones are means β so expect a fraction of a percent, not an exact match.
batch/row_fill_frac is the one to watch when samples are long: a 10k-token sample tiles a 32k budget badly (three fit, four never do, so the packer often gets two and the row runs ~77% full), while 1k-token samples tile it almost perfectly. That is quantization, not a bug, and token_budget is the lever β bearing in mind that attention is O(LΒ²) per sequence, so a fuller row of long sequences does not cost linearly more memory.
Tree packing
A row is a prefix trie, not a concatenation: a token shared by several samples is forwarded once, and attention still shows each sample exactly its own history. Rollouts of one prompt all repeat it, and every turn of a multi-turn rollout re-sends the conversation, so the decoder's work drops by the packing ratio (raw tokens Γ· forwarded tokens). It is a layout change only: every trained token keeps its own target, advantage and old log-probability, and its log-probability matches forwarding the sample on its own.
Where it happens:
planner TreeRow.add the trie grows sample by sample; a sample is priced by its novel tokens
β (TreeRow.fits), so `token_budget` bounds unique tokens, not raw ones
collator TreeRow.pack DFS layout: unique tokens, depth as position_ids, one loss term per
β original trained token (token_index), and per segment its queries and
β the K/V indices of its root-to-segment path, all built on CPU
attention tree_attention gather K/V along the paths, one FA3 varlen call, causalHow it works, on two rollouts of one prompt:
/* Detailed source-code truncated for AI context efficiency. */A few consequences:
- Any common prefix is shared: the rollouts of a prompt, and across prompts a common system prompt or tool schema. The planner sends a sample to the row already holding its
group_id, where its prompt already is. When that row is full, the sample goes to another row if a rank is still empty, or if that does not raise the most loaded row, which sets the micro-batch's time; otherwise the micro-batch closes. - Nothing shared is plain packing: every sample is then one segment whose path is itself, which is ordinary padding-free FA3 varlen packing.
- Only K/V is duplicated: a shared prefix is gathered once per descendant segment, which is about what unpacked training would hold, and small with GQA.
What it buys: with nothing shared, it runs at about the speed of plain padding-free packing, and the speed-up grows roughly with the packing ratio (about 2.8β3x at a ratio of 3.4 on Qwen3-0.6B/1.7B, H100). The ratio is shared prefix Γ samples per row, so batch/samples_per_row is the leading indicator: near 1, nothing can be shared, and the lever is token_budget.
Judge it on throughput, batch/trained_tokens_per_step / perf/fwd_bwd_s. A packed micro-batch holds more samples, so perf/fwd_bwd_s rises and perf/mfu_*, which divides by forwarded tokens, falls β neither is a regression. More samples per row also means more rollouts drawn per step, so a generation-bound run becomes more so.
Anything that makes rows longer or fewer removes the sharing. Check fork_threshold_tokens first: a turn becomes its own row only when its re-tokenization drifts by that many tokens, and agentic drift is typically tens against a default of 1024, so nothing forks and a conversation collapses into one row per rank. rollout/fork_frac says which regime you are in.
The rollout queue
The worker pushes scored samples into a queue and the trainer pulls from it. Four metrics describe that one queue, and they answer different questions:
| metric | question |
|---|---|
sample/rollout_queue_size |
how many samples are waiting right now |
sample/time_in_queue_s |
how long a single sample sat there before being trained on β the seconds half of its off-policyness |
perf/rollout_wait_s |
how long training sat blocked because the queue was empty |
rollout/backpressure_s |
how long generation (the worker) sat blocked because the queue was full |
The last two metrics (perf/rollout_wait_s and rollout/backpressure_s) are mirror images and are never both large! Reading them together with the queue size tells you which side is the bottleneck:
- queue near empty,
perf/rollout_wait_shigh β generation-bound. The trainer is starving; look atrollout/generated_tok_s,rollout/inflightandrollout/score_queue_size. - queue near full,
rollout/backpressure_shigh β trainer-bound. Generation is throttled and its output is aging in the queue, so watchsample/staleness_meanclimb. - both near zero β balanced !
Completions
A completion is what vLLM generated for one prompt, counted across every turn of the rollout.
| metric | meaning |
|---|---|
completions/mean_length |
generated tokens per rollout, averaged over all of its turns |
completions/min_length, completions/max_length |
shortest and longest rollout in the window |
completions/clipped_ratio |
fraction of rollouts whose last turn did not end on EOS, i.e. was cut off by max_completion_length. |
Rollouts
A rollout is one full conversation: a prompt generated to completion, including every tool round-trip it took to get there. num_generations rollouts share a prompt and form a group.
| metric | meaning |
|---|---|
rollout/duration_s |
wall time for one conversation, from dispatch to its last turn. |
rollout/generated_tok_s |
generation throughput over the last interval (windowed), so a stall shows up |
rollout/inflight |
conversations in flight to vLLM. |
rollout/turns_mean, rollout/turns_max |
mean and max turns per conversation |
rollout/samples_per_rollout |
training samples produced per conversation. 1.0 means no forking happens; (<1.0 means some conversations produced no trainable sample at all) |
rollout/fork_frac, rollout/realign_frac |
how re-tokenization drift was classified at each turn boundary |
rollout/drift_tokens_mean, rollout/drift_tokens_max |
how many held tokens a turn's re-tokenization invalidated. Compare against fork_threshold_tokens |
rollout/score_queue_size |
completed groups waiting to be scored. |
rollout/score_s, rollout/score_wait_s, rollout/score_block_s |
scoring: time to score a group, group wait time to be scored, and how long generation was blocked because the scoring queue was full |
rollout/vllm_retry_total |
retried vLLM requests. A degraded server otherwise looks like unexplained slowness. It sits here rather than in completions/ because it counts requests to the server, not generated text: a retried request produced no completion at all |
rollout/backpressure_s |
how long generation was blocked because the rollout queue was full. See the rollout queue |
rollout/inflight_cap |
current cap on conversations in flight. Starts at max_inflight_tasks and moves with vLLM load. See concurrency |
rollout/kv_cache_usage |
vLLM KV cache usage (max over engines) at the last poll. The cap is cut when it passes 0.8 and grows while it stays under 0.6 |
rollout/vllm_running_requests |
requests the server's engines are decoding (vLLM's num_requests_running, summed over engines). The gap to rollout/inflight is what sits in the API server process, building responses |
rollout/vllm_queued_requests |
requests the server has accepted but not yet scheduled (vLLM's num_requests_waiting, summed over engines). The cap does not grow while this is non-zero |
rollout/vllm_preemptions_total |
requests the server preempted during the logging window (vLLM's num_preemptions_total, summed over engines). Reported only: a weight sync resets the server's caches, which preempts every running request, so it is not a pressure signal |
Tools
Logged only when the model executes tools. tools/<name>/* repeats per tool name, the same way rewards/<func> repeats per reward function, because one failing tool is invisible in an average over all of them. The built-in tool loop and the OpenEnv harness worker (HarnessRolloutWorker, see training on harnesses) log the same names, so one dashboard reads both; the path column says which produce each metric.
| metric | path | meaning |
|---|---|---|
tools/call_frequency, tools/failure_frequency |
built-in, harness | calls per rollout, and the fraction that failed. On the loop-owning harness path a failure is a tool result whose text looks like an error |
tools/latency_s, tools/<name>/latency_s |
built-in, white-box harness | time spent executing the tool. On the white-box harness path it is the wall time between one model turn's reply and the next turn's request, which includes harness overhead. The loop-owning path has no latency: OpenEnv's trace does not timestamp its entries, so there is nothing to derive it from |
tools/<name>/call_total, tools/<name>/failure_total |
built-in, harness | per-tool call and failure counts |
tools/unknown_name_total |
built-in | the model asked for a tool that does not exist: tracks a policy error, unlike a tool that ran and raised |
tools/parallel_calls_mean |
built-in | tool calls requested in a single assistant message |
tools/loop_exhausted_frac |
built-in | conversations cut off at max_tool_calling_iterations while still asking for tools |
Environments
Logged only with an environment_factory. Both calls run on the worker's tool thread pool, so a slow environment delays its own rollout but not the others in flight.
| metric | meaning |
|---|---|
env/reset_s |
time in reset() before the rollout starts |
env/reward_s |
time in get_reward() after the rollout ends |
Samples arriving from the queue
Each sample is one RolloutSample: a reconciled sequence plus the group advantage, the reward and its per-sample metrics, assembled by the worker in _score_group once every generation in a group has finished and been scored. The worker pushes them into rollout_buffer; everything below is measured on the trainer side, as it pulls them for training.
| metric | meaning |
|---|---|
sample/forwarded_tokens_mean, sample/forwarded_tokens_max |
tokens in one sample: prompt + tool context + generated. Packing is a row-level concern, see batch/row_* |
sample/trained_tokens_mean |
of those, how many the loss is taken over |
sample/rollout_queue_size |
scored samples waiting in the queue |
sample/time_in_queue_s |
how long this sample sat in that queue before being trained on. Not the same as perf/rollout_wait_s β see the rollout queue |
sample/staleness_mean, sample/staleness_max |
how many policy versions behind the data is. ratio and kl show the effect of off-policyness on the loss; this shows the cause |
sample/dropped_stale_total |
samples discarded for exceeding max_staleness |
Batches
One micro-batch is world_size rows (remember packing flattens into 1 sequence), so one per DP rank, so that rank i forwards row i. gradient_accumulation_steps of those make one optimizer step. A step then covers gradient_accumulation_steps Γ world_size rows in total. Every _per_step metric below is a sum over the whole step and across every rank; the batch/row_* ones are means over the rows.
| metric | meaning |
|---|---|
batch/forwarded_tokens_per_step, batch/trained_tokens_per_step |
tokens in one optimizer step, forwarded and trained respectively |
batch/samples_per_step, batch/groups_per_step |
training samples, and distinct prompts, per optimizer step. They differ by the fork rate |
batch/microbatches_per_step |
counted, not read off the config |
batch/masked_token_frac |
forwarded tokens with completion_mask == 0 β the share of the forward that earns no gradient |
batch/samples_per_row, batch/row_tokens_mean, batch/row_tokens_max |
how densely the planner packed each rank's row |
batch/row_fill_frac |
row tokens against token_budget. Low means the budget is not being used |
batch/row_imbalance |
max cost / mean cost across rows, where the cost is the row's attention work. This predicts which rank stalls the gradient all-reduce. 1.0 is perfect |
batch/packing_ratio |
raw tokens per forwarded token: the factor by which the decoder's work shrinks, 1.0 when nothing is shared |
batch/pad_frac |
inter-rank padding. Costs broadcast bytes only; it is stripped before the forward |
batch/dropped_oversize_total |
samples dropped for exceeding token_budget |
Performance
Throughput measures how many tokens the trainer processes per second. Model FLOPs Utilization (MFU) estimates the percentage of the training devices' theoretical compute capacity used for model forward and backward passes. The capacity used for this estimate depends on the hardware and training precision.
Throughput and MFU are reported on two time bases:
_fwd_bwdmeasures performance during the forward and backward passes._wall_clockmeasures performance over the complete optimizer step, including optimizer updates, weight synchronization, and time spent waiting for rollouts.
The difference helps distinguish compute performance from the cost of waiting and coordination. Both MFU metrics describe the training devices only: they do not include the computation or hardware capacity of external rollout or teacher servers. Wall-clock MFU reflects the time the trainer spends waiting for those services, not the services' own utilization.
MFU is reported only when peak compute capacity is known for the local training device at the selected precision. Otherwise, MFU is omitted; training continues and throughput and timing metrics remain available.
| metric | meaning |
|---|---|
perf/step_s |
wall time between optimizer steps: compute, optimizer, weight sync and queue waits included |
perf/fwd_bwd_s |
forward + backward, summed over the step's micro-batches. This is the denominator of every _fwd_bwd metric below |
perf/fwd_s |
the forward part of it. fwd_s / fwd_bwd_s near 1/3 is the usual split; higher means the backward is cheap or recompute is being paid on the forward |
perf/optimizer_s |
optimizer.step() |
perf/rollout_wait_s |
how long the trainer sat blocked because the queue was empty. See the rollout queue |
perf/weight_sync_s |
a full sync, plus _pause_s (waiting for vLLM), _barrier_s (rank skew) and _transfer_s (the bytes) |
perf/forwarded_tok_s_fwd_bwd, perf/forwarded_tok_s_wall_clock |
forwarded tokens per second on each basis |
perf/trained_tok_s_wall_clock |
the same, counting only tokens the loss saw |
perf/mfu_fwd_bwd, perf/mfu_wall_clock |
model FLOPs utilisation on each basis |
AsyncGRPOConfig
[[autodoc]] trl.experimental.async_grpo.AsyncGRPOConfig
AsyncGRPOTrainer
[[autodoc]] trl.experimental.async_grpo.AsyncGRPOTrainer
RolloutWorkerProtocol
[[autodoc]] trl.experimental.async_grpo.async_grpo_trainer.RolloutWorkerProtocol
Source Aws Trainium (`docs/source/aws_trainium.md`)
Training on AWS Trainium
AWS Trainium runs PyTorch natively through the torch-neuronx package, which registers the neuron device. TRL trainers run on it without code changes. Neuron compiles a graph for each input shape it sees, and each compilation takes about a minute, so most of this page is about keeping shapes static. For installation and the Transformers side, see the Transformers AWS Trainium guide.
Installation
Install torch-neuronx by following the AWS Neuron setup guide, then install TRL in the same environment.
pip install trlSupervised fine-tuning
The [SFTTrainer] quick start runs as is:
from datasets import load_dataset
from trl import SFTTrainer
trainer = SFTTrainer(
model="Qwen/Qwen3-0.6B",
train_dataset=load_dataset("trl-lib/Capybara", split="train"),
)
trainer.train()On Neuron, pad_to_multiple_of defaults to 128 (its "auto" value), so the sequences are padded to a multiple of 128. This bounds the number of shapes, and so of compilations, to max_length / 128. Without it, every new sequence length triggers a compilation. On one NeuronCore, with per_device_train_batch_size=4, 20 steps of the example above take 130 s with the default and 260 s with pad_to_multiple_of=None.
The same applies to every trainer that has a pad_to_multiple_of option: DPO, KTO, Reward, GRPO, RLOO and Distillation.
Compiling the model
Set torch_compile=True to compile the model with the neuron backend. Dynamo's automatic dynamic shapes produce graphs the Neuron backend can't compile yet (#7660), so turn them off:
import torch
from datasets import load_dataset
from trl import SFTConfig, SFTTrainer
torch._dynamo.config.automatic_dynamic_shapes = False
trainer = SFTTrainer(
model="Qwen/Qwen3-0.6B",
args=SFTConfig(torch_compile=True),
train_dataset=load_dataset("trl-lib/Capybara", split="train"),
)
trainer.train()The first step compiles for about 2 minutes. After that, with per_device_train_batch_size=4, Qwen3-0.6B trains at 1.15 s/step on one NeuronCore, against 1.71 s/step without compilation.
Online methods
vLLM doesn't run on Neuron, so GRPO and RLOO generate with Transformers. With the default dynamic cache, every generated token has a new shape, and a step takes several minutes. Use the static cache, which keeps the decoding shapes fixed:
from datasets import load_dataset
from trl import GRPOConfig, GRPOTrainer
# Reward completions close to 200 characters
def reward_len(completions, **kwargs):
return [-abs(200 - len(completion)) / 100 for completion in completions]
trainer = GRPOTrainer(
model="Qwen/Qwen3-0.6B",
reward_funcs=reward_len,
args=GRPOConfig(
per_device_train_batch_size=4,
num_generations=4,
max_completion_length=128,
cache_implementation="static",
model_init_kwargs={"dtype": "bfloat16"},
),
train_dataset=load_dataset("trl-lib/tldr", split="train"),
)
trainer.train()The prompts are padded to pad_to_multiple_of before generation too, so the static cache only takes a few sizes. Each new size still compiles for about 12 minutes on one NeuronCore; once a size is compiled, a step of this example takes about 25 s.
[!NOTE]
The static cache needs a bf16 model for now: with the default fp32 model, the keys and values end up in different dtypes (#7664).
Data parallelism
On a trn2.3xlarge instance, which contains one Trainium2 chip, the default Logical NeuronCore configuration (LNC=2) pairs the chip's 8 physical NeuronCores into 4 logical NeuronCores.
To train on all four, use Accelerate:
accelerate launch --num_processes 4 train.pyIn this setup, SFT runs 3.6x faster on 4 NeuronCores than on 1.
To restrict a run to a subset of NeuronCores, set NEURON_RT_VISIBLE_CORES to 0,1, 2,3, or 0-3. Other combinations such as 1,2 are not supported: the Neuron runtime fails to set up collectives across them.
Source Bema For Reference Model (`docs/source/bema_for_reference_model.md`)
BEMA for Reference Model
This feature implements the BEMA algorithm to update the reference model during DPO training.
Usage
from trl.experimental.bema_for_ref_model import BEMACallback, DPOTrainer
from datasets import load_dataset
dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train")
bema_callback = BEMACallback(update_ref_model=True)
trainer = DPOTrainer(
model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5",
train_dataset=dataset,
callbacks=[bema_callback],
)
trainer.train()DPOTrainer
[[autodoc]] experimental.bema_for_ref_model.DPOTrainer
- train
- save_model
- push_to_hub
BEMACallback
[[autodoc]] experimental.bema_for_ref_model.BEMACallback
Source Callbacks (`docs/source/callbacks.md`)
Callbacks
RichProgressCallback
[[autodoc]] RichProgressCallback
LogCompletionsCallback
[[autodoc]] LogCompletionsCallback
BEMACallback
[[autodoc]] BEMACallback
WeaveCallback
[[autodoc]] WeaveCallback
Source Chat Template Utils (`docs/source/chat_template_utils.md`)
Chat template utilities
For an overview of the chat templates bundled with TRL and the rationale behind the training patches, see Chat Templates.
clone_chat_template
[[autodoc]] clone_chat_template
is_chat_template_prefix_preserving
[[autodoc]] chat_template_utils.is_chat_template_prefix_preserving
get_training_chat_template
[[autodoc]] chat_template_utils.get_training_chat_template
Source Chat Templates (`docs/source/chat_templates.md`)
Chat Templates
A chat template is a Jinja2 snippet that formats messages into the string a model was trained on. For example:
>>> from transformers import AutoTokenizer
>>> tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2-0.5B-Instruct")
>>> tokenizer.chat_template
"{% for message in messages %}{% if loop.first and messages[0]['role'] != 'system' %}{{ '<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n' }}{% endif %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}"
>>> tokenizer.apply_chat_template([{"role": "user", "content": "Hi!"}], tokenize=False)
'<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\nHi!<|im_end|>\n'In most cases you don't need to worry about chat templates: models ship their template along with the tokenizer, and TRL applies it for you. The whole thing is transparent. But some TRL recipes rely on features that most shipped templates don't include:
- SFT with
assistant_only_loss=Trueneeds{% generation %}/{% endgeneration %}markers around assistant output, so the loss mask can target only assistant tokens. - GRPO with tool calls needs the template to be prefix-preserving: appending a tool message must not change how earlier messages are rendered.
TRL ships patched templates under trl/chat_templates/ for common families (Qwen, Llama, DeepSeek-V3, GPT-OSS, ...) and swaps them in automatically for supported models. For any other model, you'll need to patch its template yourself. The rest of this page catalogs what's bundled.
Supported model families
TRL stores reference copies of the original templates so it can identify supported models at init and swap in a training template when needed. The following families are recognized: Cohere, Cohere2, DeepSeek-V3, DeepSeek-R1-Distill, DiffusionGemma, Gemma, Gemma3, Gemma4, GLM-4-MoE, GPT-OSS, Idefics3, LFM2, LFM2.5 (including LFM2.5-2.6B and LFM2.5-24B-A2B), LFM2.5-VL, Llama 3 / 3.1 / 3.2, Llava-Next, Muse Glimmer, Nemotron 3 (Nano, Super, Ultra), Nemotron 3.5 Lightning, Phi-3, Phi-3.5, Qwen2-VL, Qwen2.5, Qwen2.5-VL, Qwen3 (including the Instruct-2507 variant), Qwen3-VL, Qwen3.5, Qwen3.6, Qwen3.8, SmolVLM (including SmolVLM2).
Training templates
Patched templates that fix training-specific issues. Swapped in at init when tools are enabled (GRPO) or when assistant_only_loss=True (SFT).
`cohere_training.jinja`
Patched Cohere template. Diff vs cohere.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`cohere2_training.jinja`
Patched Cohere2 template. Diff vs cohere2.jinja:
Move the trailing <|END_OF_TURN_TOKEN|> from after the role-dispatch {% endif %} into each role branch, and wrap the assistant branch (<|START_RESPONSE|>...<|END_RESPONSE|><|END_OF_TURN_TOKEN|>) with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`deepseek_r1_distill_training.jinja`
Patched DeepSeek-R1-Distill template. Diff vs deepseek_r1_distill.jinja:
- Removes the
''split on assistant content. The original drops everything up to and including ``, which would silently remove the reasoning from the training target. - Wraps assistant message output with
{% generation %}/{% endgeneration %}markers for SFT assistant-only loss.
`deepseekv3_training.jinja`
Patched DeepSeek-V3 template. Diff vs deepseekv3.jinja:
- Uses
| tojsonontool['function']['arguments']so thatargumentscan be passed as adict(the documented format per transformers docs). The original template uses raw string concatenation, which crashes on dict inputs. - Wraps assistant message output with
{% generation %}/{% endgeneration %}markers for SFT assistant-only loss.
`diffusion_gemma_training.jinja`
Patched DiffusionGemma template. Diff vs diffusion_gemma.jinja:
Wrap the parts of a model turn that the model emits (thinking channel, tool calls, content, and the closing <turn|>) with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss. Tool responses embedded in the turn come from the environment and stay outside the generation block.
`gemma_training.jinja`
Patched Gemma template (shared by Gemma and Gemma2, which ship identical chat templates). Diff vs gemma.jinja:
Split the unified assistant output so that the <start_of_turn>model\n header (a prompt cue, not generated by the model) sits outside the generation block, and wrap the assistant content with {% generation %} / {% endgeneration %} markers for SFT assistant-only loss.
`gemma3_training.jinja`
Patched Gemma 3 template. Same diff as gemma_training.jinja (split the unified output line into role-specific branches so the <start_of_turn>model\n prompt cue sits outside the generation block, and wrap the assistant content with {% generation %} / {% endgeneration %}), applied to the Gemma 3 base template that supports system messages and multimodal content blocks.
`glm4moe_training.jinja`
Patched GLM-4-MoE template. Diff vs glm4moe.jinja:
Require both and to be present before parsing, to avoid incorrect splitting when the model generates only one tag:
- {%- if '</think>' in content %}
+ {%- if '<think>' in content and '</think>' in content %}Wrap assistant message output (including the thinking block and tool calls) with {% generation %} / {% endgeneration %} markers for SFT assistant-only loss.
`qwen3_training.jinja`
Patched Qwen3 template. Diff vs qwen3.jinja:
Require both and to be present before parsing, to avoid incorrect splitting when the model generates only one tag:
- {%- if '</think>' in content %}
+ {%- if '<think>' in content and '</think>' in content %}Always include the thinking block regardless of message position. The original conditionally omits it based on loop.last, which changes the assistant rendering when a tool message is appended, breaking prefix-preservation:
- {%- if loop.index0 > ns.last_query_index %}
- {%- if loop.last or (not loop.last and reasoning_content) %}
- {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content.strip('\n') + '\n</think>\n\n' + content.lstrip('\n') }}
- {%- else %}
- {{- '<|im_start|>' + message.role + '\n' + content }}
- {%- endif %}
- {%- else %}
- {{- '<|im_start|>' + message.role + '\n' + content }}
- {%- endif %}
+ {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content.strip('\n') + '\n</think>\n\n' + content.lstrip('\n') }}Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`qwen3_vl_training.jinja`
Patched Qwen3-VL template. Diff vs qwen3_vl.jinja:
Wrap assistant message output (both content and tool_calls) with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`gptoss_training.jinja`
Patched GPT-OSS template. Diff vs gptoss.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`idefics3_training.jinja`
Patched Idefics3 template. Diff vs idefics3.jinja:
Split the assistant message into its own branch so the {% generation %} / {% endgeneration %} markers wrap the assistant content. This enables return_assistant_tokens_mask=True to produce correct masks for SFT assistant-only loss.
`lfm2_training.jinja`
Patched LFM2 template. Diff vs lfm2.jinja:
Split the unified message output line into role-specific branches, so the <|im_start|>assistant\n prompt cue sits outside the generation block (it is not generated by the model), while the assistant's content and <|im_end|>\n (which the model must learn to produce and to stop on) sit inside. Wrap the assistant content with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`lfm2_v2_training.jinja`
Patched LFM2 template, later revision. Diff vs lfm2_v2.jinja:
Wrap the assistant output with {% generation %} / {% endgeneration %}, in both the branch that renders tool_calls and the plain-content branch, so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss. The <|im_start|>assistant\n prompt cue stays outside the generation block, while the content, the tool calls and <|im_end|>\n sit inside.
`llama3_training.jinja`
Patched Llama 3 template. Diff vs llama3.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`llava_next_training.jinja`
Patched Llava-Next template. Diff vs llava_next.jinja:
- Wraps assistant message output with
{% generation %}/{% endgeneration %}markers for SFT assistant-only loss. - Keeps the space that follows
</s>outside the generation block. The tokenizer merges it into the next turn's first token (β[), which would otherwise be masked as assistant output and hide the</s>stop token from the end of the masked span.
`muse_glimmer_training.jinja`
Patched Muse Glimmer template. Diff vs muse_glimmer.jinja: wrap the whole assistant branch β every ATEM channel of the turn β with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`nemotron_3_nano_training.jinja`
Patched Nemotron Nano template. Diff vs nemotron_3_nano.jinja: the original is already prefix-preserving, so the only change is wrapping assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`nemotron_3_super_training.jinja`
Patched Nemotron Super template. Diff vs nemotron_3_super.jinja: same as nemotron_3_nano_training.jinja β the original is already prefix-preserving, so the only change is wrapping assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`nemotron_3_ultra_training.jinja`
Patched Nemotron Ultra template. Diff vs nemotron_3_ultra.jinja: same as nemotron_3_nano_training.jinja β the original is already prefix-preserving, so the only change is wrapping assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`nemotron_3_5_lightning_training.jinja`
Patched Nemotron 3.5 Lightning template. Diff vs nemotron_3_5_lightning.jinja: same as nemotron_3_nano_training.jinja β the original is already prefix-preserving, so the only change is wrapping assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`phi3_training.jinja`
Patched Phi-3 template. Diff vs phi3.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`phi3_5_training.jinja`
Patched Phi-3.5 template. Diff vs phi3_5.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`qwen2_5_training.jinja`
Patched Qwen2.5 template. Diff vs qwen2_5.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`qwen2_5_vl_training.jinja`
Patched Qwen2.5-VL template (also used for Qwen2-VL, which ships a byte-identical template). Diff vs qwen2_5_vl.jinja:
Split the assistant message into its own branch so the {% generation %} / {% endgeneration %} markers wrap the assistant content. This enables return_assistant_tokens_mask=True to produce correct masks for SFT assistant-only loss.
`qwen3_instruct_2507_training.jinja`
Patched Qwen3-Instruct-2507 template (used by models like Qwen3-4B-Instruct-2507, which ship a simpler Qwen3 variant without reasoning_content / `` parsing, multi_step_tool tracking, or the enable_thinking flag). Diff vs qwen3_instruct_2507.jinja:
Wrap assistant message output with {% generation %} / {% endgeneration %} so that return_assistant_tokens_mask=True produces correct masks for SFT assistant-only loss.
`qwen3_5_think_training.jinja` / `qwen3_5_nothink_training.jinja`
Patched Qwen3.5 templates, shared logic across both flavors (they differ only in the default value of the enable_thinking flag β qwen3_5_think_training.jinja defaults to thinking enabled, used by Qwen3.5-4B and larger; qwen3_5_nothink_training.jinja defaults to thinking disabled, used by Qwen3.5-2B and smaller). Diff vs qwen3_5_think.jinja / qwen3_5_nothink.jinja: same set of changes as qwen3_training.jinja β require both and to be present before parsing, drop the loop.index0 > ns.last_query_index conditional so the thinking block is always emitted (prefix-preservation), and wrap assistant output with {% generation %} / {% endgeneration %} markers for SFT assistant-only loss.
`qwen3_6_training.jinja`
Patched Qwen3.6 template. Diff vs qwen3_6.jinja: same set of changes as qwen3_training.jinja β require both and to be present before parsing, drop the loop.index0 > ns.last_query_index conditional so the thinking block is always emitted (prefix-preservation), and wrap assistant output with {% generation %} / {% endgeneration %} markers for SFT assistant-only loss.
`qwen3_8_training.jinja`
Patched Qwen3.8 template. Diff vs qwen3_8.jinja: drop the preserve_thinking / loop.index0 > ns.last_query_index conditional so the thinking block is always emitted β prefix-preservation then holds even when the caller passes preserve_thinking=False β and wrap assistant output with {% generation %} / {% endgeneration %} markers for SFT assistant-only loss.
`smolvlm_training.jinja`
Patched SmolVLM template (also used for SmolVLM2, which ships a byte-identical template). Diff vs smolvlm.jinja:
Split the assistant message into its own branch so the {% generation %} / {% endgeneration %} markers wrap the assistant content. This enables return_assistant_tokens_mask=True to produce correct masks for SFT assistant-only loss.
Related utilities
See Chat Template Utilities for the helper functions ([clone_chat_template], [is_chat_template_prefix_preserving], [get_training_chat_template]) that operate on these templates.
Source Clis (`docs/source/clis.md`)
Command Line Interfaces (CLIs)
TRL provides a powerful command-line interface (CLI) to fine-tune large language models (LLMs) using methods like Supervised Fine-Tuning (SFT), Direct Preference Optimization (DPO), and more. The CLI abstracts away much of the boilerplate, letting you launch training jobs quickly and reproducibly.
Commands
Currently supported commands are:
Training Commands
trl distillation: distill a teacher model into a student with on-policy knowledge distillationtrl dpo: fine-tune a LLM with DPOtrl grpo: fine-tune a LLM with GRPOtrl kto: fine-tune a LLM with KTOtrl reward: train a Reward Modeltrl rloo: fine-tune a LLM with RLOOtrl sft: fine-tune a LLM with SFT
Other Commands
trl env: get the system informationtrl vllm-serve: serve a model with vLLM (deprecated, usevllm servedirectly, see vLLM integration)
Fine-Tuning with the TRL CLI
Basic Usage
You can launch training directly from the CLI by specifying required arguments like the model and dataset:
trl sft \
--model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name stanfordnlp/imdbtrl dpo \
--model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name anthropic/hh-rlhftrl reward \
--model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name trl-lib/ultrafeedback_binarizedtrl grpo \
--model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name HuggingFaceH4/Polaris-Dataset-53K \
--reward_funcs accuracy_rewardtrl rloo \
--model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name HuggingFaceH4/Polaris-Dataset-53K \
--reward_funcs accuracy_rewardtrl kto \
--model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name trl-lib/kto-mix-14kUsing Configuration Files
To keep your CLI commands clean and reproducible, you can define all training arguments in a YAML configuration file:
# sft_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: stanfordnlp/imdbLaunch with:
trl sft --config sft_config.yaml# dpo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: anthropic/hh-rlhfLaunch with:
trl dpo --config dpo_config.yaml# reward_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: trl-lib/ultrafeedback_binarizedLaunch with:
trl reward --config reward_config.yaml# grpo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: HuggingFaceH4/Polaris-Dataset-53K
reward_funcs:
- accuracy_rewardLaunch with:
trl grpo --config grpo_config.yaml# rloo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: HuggingFaceH4/Polaris-Dataset-53K
reward_funcs:
- accuracy_rewardLaunch with:
trl rloo --config rloo_config.yaml# kto_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: trl-lib/kto-mix-14kLaunch with:
trl kto --config kto_config.yamlScaling Up with Accelerate
TRL CLI natively supports π€ Accelerate, making it easy to scale training across multiple GPUs, machines, or use advanced setups like DeepSpeed β all from the same CLI.
You can pass any accelerate launch arguments directly to trl, such as --num_processes. For more information see Using accelerate launch.
trl sft \
--model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name stanfordnlp/imdb \
--num_processes 4or, with a config file:
# sft_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: stanfordnlp/imdb
num_processes: 4Launch with:
trl sft --config sft_config.yamltrl dpo \
--model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name anthropic/hh-rlhf \
--num_processes 4or, with a config file:
# dpo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: anthropic/hh-rlhf
num_processes: 4Launch with:
trl dpo --config dpo_config.yamltrl reward \
--model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name trl-lib/ultrafeedback_binarized \
--num_processes 4or, with a config file:
# reward_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: trl-lib/ultrafeedback_binarized
num_processes: 4Launch with:
trl reward --config reward_config.yamltrl grpo \
--model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name HuggingFaceH4/Polaris-Dataset-53K \
--reward_funcs accuracy_reward \
--num_processes 4or, with a config file:
# grpo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: HuggingFaceH4/Polaris-Dataset-53K
reward_funcs:
- accuracy_reward
num_processes: 4Launch with:
trl grpo --config grpo_config.yamltrl rloo \
--model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name HuggingFaceH4/Polaris-Dataset-53K \
--reward_funcs accuracy_reward \
--num_processes 4or, with a config file:
# rloo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: HuggingFaceH4/Polaris-Dataset-53K
reward_funcs:
- accuracy_reward
num_processes: 4Launch with:
trl rloo --config rloo_config.yamltrl kto \
--model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name trl-lib/kto-mix-14k \
--num_processes 4or, with a config file:
# kto_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: trl-lib/kto-mix-14k
num_processes: 4Launch with:
trl kto --config kto_config.yamlUsing `--accelerate_config` for Accelerate Configuration
The --accelerate_config flag lets you easily configure distributed training with π€ Accelerate. This flag accepts either:
- the name of a predefined config profile (built into TRL), or
- a path to a custom Accelerate YAML config file.
Predefined Config Profiles
TRL provides several ready-to-use Accelerate configs to simplify common training setups:
| Name | Description |
|---|---|
fsdp1 |
Fully Sharded Data Parallel Stage 1 |
fsdp2 |
Fully Sharded Data Parallel Stage 2 |
zero1 |
DeepSpeed ZeRO Stage 1 |
zero2 |
DeepSpeed ZeRO Stage 2 |
zero3 |
DeepSpeed ZeRO Stage 3 |
multi_gpu |
Multi-GPU training |
single_gpu |
Single-GPU training |
To use one of these, just pass the name to --accelerate_config. TRL will automatically load the corresponding config file from trl/accelerate_config/.
Example Usage
trl sft \
--model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name stanfordnlp/imdb \
--accelerate_config zero2 # or path/to/my/accelerate/config.yamlor, with a config file:
# sft_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: stanfordnlp/imdb
accelerate_config: zero2 # or path/to/my/accelerate/config.yamlLaunch with:
trl sft --config sft_config.yamltrl dpo \
--model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name anthropic/hh-rlhf \
--accelerate_config zero2 # or path/to/my/accelerate/config.yamlor, with a config file:
# dpo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: anthropic/hh-rlhf
accelerate_config: zero2 # or path/to/my/accelerate/config.yamlLaunch with:
trl dpo --config dpo_config.yamltrl reward \
--model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name trl-lib/ultrafeedback_binarized \
--accelerate_config zero2 # or path/to/my/accelerate/config.yamlor, with a config file:
# reward_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: trl-lib/ultrafeedback_binarized
accelerate_config: zero2 # or path/to/my/accelerate/config.yamlLaunch with:
trl reward --config reward_config.yamltrl grpo \
--model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name HuggingFaceH4/Polaris-Dataset-53K \
--reward_funcs accuracy_reward \
--accelerate_config zero2 # or path/to/my/accelerate/config.yamlor, with a config file:
# grpo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: HuggingFaceH4/Polaris-Dataset-53K
reward_funcs:
- accuracy_reward
accelerate_config: zero2 # or path/to/my/accelerate/config.yamlLaunch with:
trl grpo --config grpo_config.yamltrl rloo \
--model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name HuggingFaceH4/Polaris-Dataset-53K \
--reward_funcs accuracy_reward \
--accelerate_config zero2 # or path/to/my/accelerate/config.yamlor, with a config file:
# rloo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: HuggingFaceH4/Polaris-Dataset-53K
reward_funcs:
- accuracy_reward
accelerate_config: zero2 # or path/to/my/accelerate/config.yamlLaunch with:
trl rloo --config rloo_config.yamltrl kto \
--model_name_or_path Qwen/Qwen2.5-0.5B \
--dataset_name trl-lib/kto-mix-14k \
--accelerate_config zero2 # or path/to/my/accelerate/config.yamlor, with a config file:
# kto_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: trl-lib/kto-mix-14k
accelerate_config: zero2 # or path/to/my/accelerate/config.yamlLaunch with:
trl kto --config kto_config.yamlUsing dataset mixtures
You can use dataset mixtures to combine multiple datasets into a single training dataset. This is useful for training on diverse data sources or when you want to mix different types of data.
# sft_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
datasets:
- path: stanfordnlp/imdb
- path: roneneldan/TinyStoriesLaunch with:
trl sft --config sft_config.yaml# dpo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
datasets:
- path: BAAI/Infinity-Preference
- path: argilla/Capybara-PreferencesLaunch with:
trl dpo --config dpo_config.yaml# reward_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
datasets:
- path: trl-lib/tldr-preference
- path: trl-lib/lm-human-preferences-sentimentLaunch with:
trl reward --config reward_config.yaml# grpo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
datasets:
- path: HuggingFaceH4/Polaris-Dataset-53K
- path: trl-lib/DeepMath-103K
reward_funcs:
- accuracy_rewardLaunch with:
trl grpo --config grpo_config.yaml# rloo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
datasets:
- path: HuggingFaceH4/Polaris-Dataset-53K
- path: trl-lib/DeepMath-103K
reward_funcs:
- accuracy_rewardLaunch with:
trl rloo --config rloo_config.yaml# kto_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
datasets:
- path: trl-lib/kto-mix-14k
- path: argilla/ultrafeedback-binarized-preferences-cleanedLaunch with:
trl kto --config kto_config.yamlTo see all the available keywords for defining dataset mixtures, refer to the [scripts.utils.DatasetConfig] and [DatasetMixtureConfig] classes.
Getting the System Information
You can get the system information by running the following command:
trl envThis will print out the system information, including the GPU information, the CUDA version, the PyTorch version, the transformers version, the TRL version, and any optional dependencies that are installed.
Copy-paste the following information when reporting an issue:
- Platform: Linux-5.15.0-1048-aws-x86_64-with-glibc2.31
- Python version: 3.11.9
- PyTorch version: 2.4.1
- accelerator(s): NVIDIA H100 80GB HBM3
- Transformers version: 4.45.0.dev0
- Accelerate version: 0.34.2
- Accelerate config:
- compute_environment: LOCAL_MACHINE
- distributed_type: DEEPSPEED
- mixed_precision: no
- use_cpu: False
- debug: False
- num_processes: 4
- machine_rank: 0
- num_machines: 1
- rdzv_backend: static
- same_network: True
- main_training_function: main
- enable_cpu_affinity: False
- deepspeed_config: {'gradient_accumulation_steps': 4, 'offload_optimizer_device': 'none', 'offload_param_device': 'none', 'zero3_init_flag': False, 'zero_stage': 2}
- downcast_bf16: no
- tpu_use_cluster: False
- tpu_use_sudo: False
- tpu_env: []
- Datasets version: 3.0.0
- HF Hub version: 0.24.7
- TRL version: 0.12.0.dev0+acb4d70
- bitsandbytes version: 0.41.1
- DeepSpeed version: 0.15.1
- Diffusers version: 0.30.3
- Liger-Kernel version: 0.3.0
- LLM-Blender version: 0.0.2
- OpenAI version: 1.46.0
- PEFT version: 0.12.0
- vLLM version: not installedThis information is required when reporting an issue.
Source Community Tutorials (`docs/source/community_tutorials.md`)
Community Tutorials
Community tutorials are made by active members of the Hugging Face community who want to share their knowledge and expertise with others. They are a great way to learn about the library and its features, and to get started with core classes and modalities.
Language Models
Tutorials
| Task | Class | Description | Author | Tutorial | Colab |
|---|---|---|---|---|---|
| Reinforcement Learning | [GRPOTrainer] |
Efficient Online Training with GRPO and vLLM in TRL | Sergio Paniego | Link | |
| Reinforcement Learning | [GRPOTrainer] |
Post training an LLM for reasoning with GRPO in TRL | Sergio Paniego | Link | |
| Reinforcement Learning | [GRPOTrainer] |
Mini-R1: Reproduce Deepseek R1 βaha momentβ a RL tutorial | Philipp Schmid | Link | |
| Reinforcement Learning | [GRPOTrainer] |
RL on LLaMA 3.1-8B with GRPO and Unsloth optimizations | Andrea Manzoni | Link | |
| Instruction tuning | [SFTTrainer] |
Fine-tuning Google Gemma LLMs using ChatML format with QLoRA | Philipp Schmid | Link | |
| Structured Generation | [SFTTrainer] |
Fine-tuning Llama-2-7B to generate Persian product catalogs in JSON using QLoRA and PEFT | Mohammadreza Esmaeilian | Link | |
| Preference Optimization | [DPOTrainer] |
Align Mistral-7b using Direct Preference Optimization for human preference alignment | Maxime Labonne | Link | |
| Preference Optimization | [experimental.orpo.ORPOTrainer] |
Fine-tuning Llama 3 with ORPO combining instruction tuning and preference alignment | Maxime Labonne | Link | |
| Instruction tuning | [SFTTrainer] |
How to fine-tune open LLMs in 2025 with Hugging Face | Philipp Schmid | Link | |
| Step-Level Reasoning | [GRPOTrainer] |
Supervised Reinforcement Learning (SRL) for step-by-step reasoning with vLLM | Deepak Swaminathan | Link |
Videos
| Task | Title | Author | Video |
|---|---|---|---|
| Instruction tuning | Fine-tuning open AI models using Hugging Face TRL | Wietse Venema | |
| Instruction tuning | How to fine-tune a smol-LM with Hugging Face, TRL, and the smoltalk Dataset | Mayurji |
β οΈ Deprecated features notice for "How to fine-tune a smol-LM with Hugging Face, TRL, and the smoltalk Dataset" (click to expand)
[!WARNING]
The tutorial uses two deprecated features:
SFTTrainer(..., tokenizer=tokenizer): UseSFTTrainer(..., processing_class=tokenizer)instead, or simply omit it (it will be inferred from the model).setup_chat_format(model, tokenizer): UseSFTConfig(..., chat_template_path="Qwen/Qwen3-0.6B"), wherechat_template_pathspecifies the model whose chat template you want to copy.
Vision Language Models
Tutorials
| Task | Class | Description | Author | Tutorial | Colab |
|---|---|---|---|---|---|
| Visual QA | [SFTTrainer] |
Fine-tuning Qwen2-VL-7B for visual question answering on ChartQA dataset | Sergio Paniego | Link | |
| Visual QA | [SFTTrainer] |
Fine-tuning SmolVLM with TRL on a consumer GPU | Sergio Paniego | Link | |
| SEO Description | [SFTTrainer] |
Fine-tuning Qwen2-VL-7B for generating SEO-friendly descriptions from images | Philipp Schmid | Link | |
| Visual QA | [DPOTrainer] |
PaliGemma π€ Direct Preference Optimization | Merve Noyan | Link | |
| Visual QA | [DPOTrainer] |
Fine-tuning SmolVLM using direct preference optimization (DPO) with TRL on a consumer GPU | Sergio Paniego | Link | |
| Object Detection Grounding | [SFTTrainer] |
Fine tuning a VLM for Object Detection Grounding using TRL | Sergio Paniego | Link | |
| Visual QA | [DPOTrainer] |
Fine-Tuning a Vision Language Model with TRL using MPO | Sergio Paniego | Link | |
| Reinforcement Learning | [GRPOTrainer] |
Post training a VLM for reasoning with GRPO using TRL | Sergio Paniego | Link |
Speech Language Models
Tutorials
| Task | Class | Description | Author | Tutorial |
|---|---|---|---|---|
| Text-to-Speech | [GRPOTrainer] |
Post training a Speech Language Model with GRPO using TRL | Steven Zheng | Link |
Contributing
If you have a tutorial that you would like to add to this list, please open a PR to add it. We will review it and merge it if it is relevant to the community.
Source Cpo Trainer (`docs/source/cpo_trainer.md`)
CPO Trainer
Overview
Contrastive Preference Optimization (CPO) as introduced in the paper Contrastive Preference Optimization: Pushing the Boundaries of LLM Performance in Machine Translation by Haoran Xu, Amr Sharaf, Yunmo Chen, Weiting Tan, Lingfeng Shen, Benjamin Van Durme, Kenton Murray, and Young Jin Kim. At a high level, CPO trains models to avoid generating adequate, but not perfect, translations in Machine Translation (MT) tasks. However, CPO is a general approximation of the DPO loss and can be applied to other domains, such as chat.
CPO aims to mitigate two fundamental shortcomings of SFT. First, SFTβs methodology of minimizing the discrepancy between predicted outputs and gold-standard references inherently caps model performance at the quality level of the training data. Secondly, SFT lacks a mechanism to prevent the model from rejecting mistakes in translations. The CPO objective is derived from the DPO objective.
Quick start
This example demonstrates how to train a model using the CPO method. We use the Qwen 0.5B model as the base model. We use the preference data from the UltraFeedback dataset. You can view the data in the dataset here:
Below is the script to train the model:
# train_cpo.py
from datasets import load_dataset
from trl.experimental.cpo import CPOConfig, CPOTrainer
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2-0.5B-Instruct")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2-0.5B-Instruct")
train_dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train")
training_args = CPOConfig(output_dir="Qwen2-0.5B-CPO")
trainer = CPOTrainer(model=model, args=training_args, processing_class=tokenizer, train_dataset=train_dataset)
trainer.train()Execute the script using the following command:
accelerate launch train_cpo.pyExpected dataset type
CPO requires a preference dataset. The [experimental.cpo.CPOTrainer] supports both conversational and standard dataset formats. When provided with a conversational dataset, the trainer will automatically apply the chat template to the dataset.
Logged metrics
While training and evaluating, we record the following metrics:
rewards/chosen: the mean log probabilities of the policy model for the chosen responses scaled by betarewards/rejected: the mean log probabilities of the policy model for the rejected responses scaled by betarewards/accuracies: mean of how often the chosen rewards are > than the corresponding rejected rewardsrewards/margins: the mean difference between the chosen and corresponding rejected rewardsnll_loss: the mean negative log likelihood loss of the policy model for the chosen responses
CPO variants
Simple Preference Optimization (SimPO)
Simple Preference Optimization (SimPO) by Yu Meng, Mengzhou Xia, and Danqi Chen proposes a simpler and more effective preference optimization algorithm than DPO without using a reference model. The key designs in SimPO are (1) using length-normalized log likelihood as the implicit reward, and (2) incorporating a target reward margin in the Bradley-Terry ranking objective. The official code can be found at princeton-nlp/SimPO.
The abstract from the paper is the following:
Direct Preference Optimization (DPO) is a widely used offline preference optimization algorithm that reparameterizes reward functions in reinforcement learning from human feedback (RLHF) to enhance simplicity and training stability. In this work, we propose SimPO, a simpler yet more effective approach. The effectiveness of SimPO is attributed to a key design: using the average log probability of a sequence as the implicit reward. This reward formulation better aligns with model generation and eliminates the need for a reference model, making it more compute and memory efficient. Additionally, we introduce a target reward margin to the Bradley-Terry objective to encourage a larger margin between the winning and losing responses, further enhancing the algorithm's performance. We compare SimPO to DPO and its latest variants across various state-of-the-art training setups, including both base and instruction-tuned models like Mistral and Llama3. We evaluated on extensive instruction-following benchmarks, including AlpacaEval 2, MT-Bench, and the recent challenging Arena-Hard benchmark. Our results demonstrate that SimPO consistently and significantly outperforms existing approaches without substantially increasing response length. Specifically, SimPO outperforms DPO by up to 6.4 points on AlpacaEval 2 and by up to 7.5 points on Arena-Hard. Our top-performing model, built on Llama3-8B-Instruct, achieves a remarkable 44.7 length-controlled win rate on AlpacaEval 2 -- surpassing Claude 3 Opus on the leaderboard, and a 33.8 win rate on Arena-Hard -- making it the strongest 8B open-source model.
The SimPO loss is integrated in the [experimental.cpo.CPOTrainer], as it's an alternative loss that adds a reward margin, allows for length normalization, and does not use BC regularization. To use this loss, just turn on loss_type="simpo" and cpo_alpha=0.0 in the [experimental.cpo.CPOConfig] and set the simpo_gamma to a recommended value.
CPO-SimPO
We also offer the combined use of CPO and SimPO, which enables more stable training and improved performance. Learn more details at CPO-SimPO GitHub. To use this method, simply enable SimPO by setting loss_type="simpo" and a non-zero cpo_alpha in the [experimental.cpo.CPOConfig].
AlphaPO
The AlphaPO -- Reward shape matters for LLM alignment (AlphaPO) method by Aman Gupta, Shao Tang, Qingquan Song, Sirou Zhu, Jiwoo Hong, Ankan Saha, Viral Gupta, Noah Lee, Eunki Kim, Jason Zhu, Natesh Pillai, and S. Sathiya Keerthi is also implemented in the [experimental.cpo.CPOTrainer]. AlphaPO is an alternative method that applies a transformation to the reward function shape in the context of SimPO loss. The abstract from the paper is the following:
Reinforcement Learning with Human Feedback (RLHF) and its variants have made huge strides toward the effective alignment of large language models (LLMs) to follow instructions and reflect human values. More recently, Direct Alignment Algorithms (DAAs) have emerged in which the reward modeling stage of RLHF is skipped by characterizing the reward directly as a function of the policy being learned. Some popular examples of DAAs include Direct Preference Optimization (DPO) and Simple Preference Optimization (SimPO). These methods often suffer from likelihood displacement, a phenomenon by which the probabilities of preferred responses are often reduced undesirably. In this paper, we argue that, for DAAs the reward (function) shape matters. We introduce AlphaPO, a new DAA method that leverages an Ξ±-parameter to help change the shape of the reward function beyond the standard log reward. AlphaPO helps maintain fine-grained control over likelihood displacement and overoptimization. Compared to SimPO, one of the best performing DAAs, AlphaPO leads to about 7% to 10% relative improvement in alignment performance for the instruct versions of Mistral-7B and Llama3-8B while achieving 15% to 50% relative improvement over DPO on the same models. The analysis and results presented highlight the importance of the reward shape and how one can systematically change it to affect training dynamics, as well as improve alignment performance.
To use this loss as described in the paper, we can set the loss_type="alphapo" which automatically sets loss_type="simpo" and cpo_alpha=0.0, together with alpha and simpo_gamma to recommended values in the [experimental.cpo.CPOConfig]. Alternatively, you can manually set loss_type="simpo", cpo_alpha=0.0, together with alpha and simpo_gamma to recommended values. Other variants of this method are also possible, such as setting loss_type="ipo" and alpha to any non-zero value.
Loss functions
The CPO algorithm supports several loss functions. The loss function can be set using the loss_type parameter in the [experimental.cpo.CPOConfig]. The following loss functions are supported:
loss_type= |
Description |
|---|---|
"sigmoid" (default) |
Given the preference data, we can fit a binary classifier according to the Bradley-Terry model, and in fact, the DPO authors propose the sigmoid loss on the normalized likelihood via the logsigmoid to fit a logistic regression. |
"hinge" |
The RSO authors propose to use a hinge loss on the normalized likelihood from the SLiC paper. In this case, the beta is the reciprocal of the margin. |
"ipo" |
The IPO authors provide a deeper theoretical understanding of the DPO algorithms and identify an issue with overfitting and propose an alternative loss. In this case, the beta is the reciprocal of the gap between the log-likelihood ratios of the chosen vs the rejected completion pair, and thus the smaller the beta, the larger this gap is. As per the paper, the loss is averaged over log-likelihoods of the completion (unlike DPO, which is summed only). |
"simpo" |
The SimPO method is also implemented in the [experimental.cpo.CPOTrainer]. SimPO is an alternative loss that adds a reward margin, allows for length normalization, and does not use BC regularization. To use this loss, simply set loss_type="simpo" and cpo_alpha=0.0 in the [experimental.cpo.CPOConfig] and simpo_gamma to a recommended value. |
"alphapo" |
The AlphaPO method is also implemented in the [experimental.cpo.CPOTrainer]. This is syntactic sugar that automatically sets loss_type="simpo" and cpo_alpha=0.0. AlphaPO applies a transformation to the reward function shape in the context of SimPO loss when the alpha parameter is non-zero. |
For Mixture of Experts Models: Enabling the auxiliary loss
MOEs are the most efficient if the load is about equally distributed between experts.
To ensure that we train MOEs similarly during preference-tuning, it is beneficial to add the auxiliary loss from the load balancer to the final loss.
This option is enabled by setting output_router_logits=True in the model config (e.g., [~transformers.MixtralConfig]).
To scale how much the auxiliary loss contributes to the total loss, use the hyperparameter router_aux_loss_coef=... (default: 0.001) in the model config.
CPOTrainer
[[autodoc]] experimental.cpo.CPOTrainer
- train
- save_model
- push_to_hub
CPOConfig
[[autodoc]] experimental.cpo.CPOConfig
Source Customization (`docs/source/customization.md`)
Training customization
TRL is designed with modularity in mind so that users are able to efficiently customize the training loop for their needs. Below are examples on how you can apply and test different techniques.
[!NOTE]
Although these examples use the [DPOTrainer], these customization methods apply to most (if not all) trainers in TRL.
Use different optimizers and schedulers
By default, the [DPOTrainer] creates a torch.optim.AdamW optimizer. You can create and define a different optimizer and pass it to [DPOTrainer] as follows:
from datasets import load_dataset
from torch import optim
from transformers import AutoModelForCausalLM
from trl import DPOTrainer
dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train")
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
optimizer = optim.SGD(model.parameters(), lr=1e-6)
trainer = DPOTrainer(
model=model,
train_dataset=dataset,
optimizers=(optimizer, None),
)
trainer.train()Add a learning rate scheduler
You can also add learning rate schedulers by passing both optimizer and scheduler:
from torch import optim
optimizer = optim.AdamW(model.parameters(), lr=1e-6)
lr_scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
trainer = DPOTrainer(..., optimizers=(optimizer, lr_scheduler))Pass 8-bit reference models
Since trl supports all keyword arguments when loading a model from transformers using from_pretrained, you can also leverage load_in_8bit from transformers for more memory efficient fine-tuning.
Read more about 8-bit model loading in transformers Load in 8bit or 4bit.
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
quantization_config = BitsAndBytesConfig(load_in_8bit=True)
ref_model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct", quantization_config=quantization_config)
trainer = DPOTrainer(..., ref_model=ref_model)Add custom callbacks
You can customize the training loop by adding callbacks for logging, monitoring, or early stopping. Callbacks allow you to execute custom code at specific points during training.
from transformers import TrainerCallback
class CustomLoggingCallback(TrainerCallback):
def on_log(self, args, state, control, logs=None, **kwargs):
if logs is not None:
print(f"Step {state.global_step}: {logs}")
trainer = DPOTrainer(..., callbacks=[CustomLoggingCallback()])Add custom evaluation metrics
You can define custom evaluation metrics to track during training. This is useful for monitoring model performance on specific tasks.
def compute_metrics(eval_preds):
logits, labels = eval_preds
# Add your metric computation here
return {"custom_metric": 0.0}
training_args = DPOConfig(..., eval_strategy="steps", eval_steps=100)
trainer = DPOTrainer(..., eval_dataset=eval_dataset, compute_metrics=compute_metrics)Use mixed precision training
Mixed precision training can significantly speed up training and reduce memory usage. You can enable it by setting bf16=True or fp16=True in the training config.
# Use bfloat16 precision (recommended for modern GPUs)
training_args = DPOConfig(..., bf16=True)Note: Use bf16=True for Ampere GPUs (A100, RTX 30xx) or newer, and fp16=True for older GPUs.
Use gradient accumulation
When training with limited GPU memory, gradient accumulation allows you to simulate larger batch sizes by accumulating gradients over multiple steps before updating weights.
# Simulate a batch size of 32 with per_device_train_batch_size=4 and gradient_accumulation_steps=8
training_args = DPOConfig(
...,
per_device_train_batch_size=4,
gradient_accumulation_steps=8,
)Change the training objective
Subclassing a trainer is the simplest way to customize training. The loss is defined in the compute_loss method, so to train with a different objective, subclass the trainer and override it. Data preparation, generation, logging, and checkpointing are inherited, so the subclass holds only what actually changes.
import torch
from trl import DPOTrainer
from trl.trainer.utils import selective_log_softmax
class MyDPOTrainer(DPOTrainer):
def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None):
# In this example: the hinge loss from https://huggingface.co/papers/2309.06657
input_ids, attention_mask = inputs["input_ids"], inputs["attention_mask"]
shift_labels, shift_completion_mask = input_ids[:, 1:], inputs["completion_mask"][:, 1:]
logits = model(input_ids=input_ids, attention_mask=attention_mask).logits
with torch.no_grad():
ref_logits = self.ref_model(input_ids=input_ids, attention_mask=attention_mask).logits
# Sum the log-probs over the completion tokens, for the policy and for the reference model
logps = (selective_log_softmax(logits[:, :-1], shift_labels) * shift_completion_mask).sum(dim=1)
ref_logps = (selective_log_softmax(ref_logits[:, :-1], shift_labels) * shift_completion_mask).sum(dim=1)
chosen_logps, rejected_logps = logps.chunk(2, dim=0) # batch is [chosen, rejected]
ref_chosen_logps, ref_rejected_logps = ref_logps.chunk(2, dim=0)
delta = (chosen_logps - ref_chosen_logps) - (rejected_logps - ref_rejected_logps)
return torch.relu(1 - self.beta * delta).mean()Add parameters to the config
Subclass the config to declare the parameters your loss needs:
from dataclasses import dataclass, field
from trl import DPOConfig
@dataclass
class MyDPOConfig(DPOConfig):
my_coef: float = field(default=0.1, metadata={"help": "Coefficient of the custom term."})Change the batch format
When the loss needs inputs that the default collator doesn't produce, pass your own collator, and override _prepare_dataset to leave the dataset untouched when it is already in the expected format:
class MyDPOTrainer(DPOTrainer):
def _prepare_dataset(self, dataset, *args, **kwargs):
return dataset
trainer = MyDPOTrainer(..., data_collator=my_collator)Complete example
The block-diffusion SFT example examples/sft_diffusion_gemma/sft_diffusion_gemma.py combines the three: DiffusionGemmaSFTConfig extends [SFTConfig] with the canvas and corruption parameters, and DiffusionGemmaSFTTrainer extends [SFTTrainer] and replaces the autoregressive cross-entropy with a block-diffusion denoising objective. Neither requires a change to the library.
Antidoom, an open-source tool from Liquid AI, extends [DPOTrainer] with Final Token Preference Optimization (Antislop), a preference loss over a single token position that reduces repetition loops in reasoning models.
Source Data Utils (`docs/source/data_utils.md`)
Data Utilities
is_conversational
[[autodoc]] is_conversational
maybe_convert_to_chatml
[[autodoc]] maybe_convert_to_chatml
extract_prompt
[[autodoc]] extract_prompt
unpair_preference_dataset
[[autodoc]] unpair_preference_dataset
Source Dataset Formats (`docs/source/dataset_formats.md`)
Dataset formats and types
This guide provides an overview of the dataset formats and types supported by each trainer in TRL.
Overview of the dataset formats and types
- The format of a dataset refers to how the data is structured, typically categorized as either standard or conversational.
- The type is associated with the specific task the dataset is designed for, such as prompt-only or preference. Each type is characterized by its columns, which vary according to the task, as shown in the table.
| Type \ Format | Standard | Conversational |
|---|---|---|
| Language modeling | {"text": "The sky is blue."} |
{"messages": [{"role": "user", "content": "What color is the sky?"}, |
{"role": "assistant", "content": "It is blue."}]} || Prompt-only | {"prompt": "The sky is"} | {"prompt": [{"role": "user", "content": "What color is the sky?"}]} |
| Prompt-completion | {"prompt": "The sky is",
"completion": " blue."} | {"prompt": [{"role": "user", "content": "What color is the sky?"}],
"completion": [{"role": "assistant", "content": "It is blue."}]} |
| Preference | {"prompt": "The sky is",
"chosen": " blue.",
"rejected": " green."}
or, with implicit prompt:
{"chosen": "The sky is blue.",
"rejected": "The sky is green."} | {"prompt": [{"role": "user", "content": "What color is the sky?"}],
"chosen": [{"role": "assistant", "content": "It is blue."}],
"rejected": [{"role": "assistant", "content": "It is green."}]}
or, with implicit prompt:
{"chosen": [{"role": "user", "content": "What color is the sky?"},
{"role": "assistant", "content": "It is blue."}],
"rejected": [{"role": "user", "content": "What color is the sky?"},
{"role": "assistant", "content": "It is green."}]} |
Formats
Standard
The standard dataset format typically consists of plain text strings. The columns in the dataset vary depending on the task. This is the format expected by TRL trainers. Below are examples of standard dataset formats for different tasks:
# Language modeling
language_modeling_example = {"text": "The sky is blue."}
# Preference
preference_example = {"prompt": "The sky is", "chosen": " blue.", "rejected": " green."}
# Unpaired preference
unpaired_preference_example = {"prompt": "The sky is", "completion": " blue.", "label": True}Conversational
Conversational datasets are used for tasks involving dialogues or chat interactions between users and assistants. Unlike standard dataset formats, these contain sequences of messages where each message has a role (e.g., "user" or "assistant") and content (the message text).
messages = [
{"role": "user", "content": "Hello, how are you?"},
{"role": "assistant", "content": "I'm doing great. How can I help you today?"},
{"role": "user", "content": "I'd like to show off how chat templating works!"},
]Just like standard datasets, the columns in conversational datasets vary depending on the task. Below are examples of conversational dataset formats for different tasks:
# Prompt-completion
prompt_completion_example = {"prompt": [{"role": "user", "content": "What color is the sky?"}],
"completion": [{"role": "assistant", "content": "It is blue."}]}
# Preference
preference_example = {
"prompt": [{"role": "user", "content": "What color is the sky?"}],
"chosen": [{"role": "assistant", "content": "It is blue."}],
"rejected": [{"role": "assistant", "content": "It is green."}],
}Tool Calling
Some chat templates support tool calling, which allows the model to interact with external functionsβreferred to as toolsβduring generation. This extends the conversational capabilities of the model by enabling it to output a "tool_calls" field instead of a standard "content" message whenever it decides to invoke a tool.
After the assistant initiates a tool call, the tool executes and returns its output. The assistant can then process this output and continue the conversation accordingly.
Hereβs a simple example of a tool-calling interaction:
messages = [
{"role": "user", "content": "Turn on the living room lights."},
{"role": "assistant", "tool_calls": [
{"type": "function", "function": {
"name": "control_light",
"arguments": {"room": "living room", "state": "on"}
}}]
},
{"role": "tool", "name": "control_light", "content": "The lights in the living room are now on."},
{"role": "assistant", "content": "Done!"}
]When preparing datasets for Supervised Fine-Tuning (SFT) with tool calling, it is important that your dataset includes an additional column named tools. This column contains the list of available tools for the model, which is usually used by the chat template to construct the system prompt.
The tools must be specified in a codified JSON schema format. You can automatically generate this schema from Python function signatures using the [~transformers.utils.get_json_schema] utility:
import json
from transformers.utils import get_json_schema
def control_light(room: str, state: str) -> str:
"""
Controls the lights in a room.
Args:
room: The name of the room.
state: The desired state of the light ("on" or "off").
Returns:
str: A message indicating the new state of the lights.
"""
return f"The lights in {room} are now {state}."
# Generate JSON schema
json_schema = get_json_schema(control_light)The generated schema would look like:
{"type": "function", "function": {"name": "control_light", "description": "Controls the lights in a room.", "parameters": {"type": "object", "properties": {"room": {"type": "string", "description": "The name of the room."}, "state": {"type": "string", "description": "The desired state of the light (\"on\" or \"off\")."}}, "required": ["room", "state"]}, "return": {"type": "string", "description": "str: A message indicating the new state of the lights."}}}A complete dataset entry for SFT might look like:
{"messages": messages, "tools": [json_schema]}To get a Dataset you need to use the Json() type for tool arguments since they are arbitrary JSON objects, and not dictionaries with fixed fields and types:
from datasets import Dataset
data = [
{"messages": messages1, "tools": [json_schema1]},
{"messages": messages2, "tools": [json_schema2]},
]
# auto-apply the Json() type
dataset = Dataset.from_list(data, on_mixed_types="use_json")
# or specify the features manually
from datasets import Features, Json, List, Value
features = Features(
{
"messages": List({"role": Value("string"), "content": Value("string"), "tool_calls": List(Json())}),
"tools": List(Json()),
}
)
dataset = Dataset.from_list(data, features=features)On older versions of datasets (<4.7.0) that don't have the Json() type, you should store tools as a JSON str (with json.dumps([...])):
dataset = Dataset.from_list(
[{"messages": messages1, "tools": json.dumps([json_schema1])},
{"messages": messages2, "tools": json.dumps([json_schema2])}]
)For more detailed information on tool calling, refer to the Tool Calling section in the transformers documentation and the blog post Tool Use, Unified.
Harmony
The Harmony response format was introduced with the OpenAI GPT OSS models. It extends the conversational format by adding richer structure for reasoning, function calls, and metadata about the modelβs behavior. Key features include:
Developer role β Provides high level instructions (similar to a system prompt) and lists available tools.
Channels β Separate types of assistant output into distinct streams:
analysisβ for internal reasoning, from the key"thinking"finalβ for the user-facing answer, from the key"content"commentaryβ for tool calls or meta notes
Reasoning effort β Signals how much thinking the model should show (e.g.,
"low","medium","high").Model identity β Explicitly defines the assistantβs persona.
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("openai/gpt-oss-20b")
messages = [
{"role": "developer", "content": "Use a friendly tone."},
{"role": "user", "content": "What is the meaning of life?"},
{"role": "assistant", "thinking": "Deep reflection...", "content": "The final answer is..."},
]
print(
tokenizer.apply_chat_template(
messages,
tokenize=False,
reasoning_effort="low",
model_identity="You are HuggingGPT, a large language model trained by Hugging Face.",
)
)This produces:
<|start|>system<|message|>You are HuggingGPT, a large language model trained by Hugging Face.
Knowledge cutoff: 2024-06
Current date: 2025-08-03
Reasoning: low
# Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>developer<|message|># Instructions
Use a friendly tone.<|end|><|start|>user<|message|>What is the meaning of life?<|end|><|start|>assistant<|channel|>analysis<|message|>Deep reflection...<|end|><|start|>assistant<|channel|>final<|message|>The final answer is...<|return|>For full details on message structure, supported fields, and advanced usage, see the Harmony documentation.
Types
Language modeling
A language modeling dataset consists of a column "text" (or "messages" for conversational datasets) containing a full sequence of text.
# Standard format
language_modeling_example = {"text": "The sky is blue."}
# Conversational format
language_modeling_example = {"messages": [
{"role": "user", "content": "What color is the sky?"},
{"role": "assistant", "content": "It is blue."}
]}Prompt-only
In a prompt-only dataset, only the initial prompt (the question or partial sentence) is provided under the key "prompt". The training typically involves generating completion based on this prompt, where the model learns to continue or complete the given input.
# Standard format
prompt_only_example = {"prompt": "The sky is"}
# Conversational format
prompt_only_example = {"prompt": [{"role": "user", "content": "What color is the sky?"}]}For examples of prompt-only datasets, refer to the Prompt-only datasets collection.
[!TIP]
While both the prompt-only and language modeling types are similar, they differ in how the input is handled. In the prompt-only type, the prompt represents a partial input that expects the model to complete or continue, while in the language modeling type, the input is treated as a complete sentence or sequence. These two types are processed differently by TRL. Below is an example showing the difference in the output of theapply_chat_templatefunction for each type:pythonfrom transformers import AutoTokenizer from trl import apply_chat_template tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-mini-128k-instruct") # Example for prompt-only type prompt_only_example = {"prompt": [{"role": "user", "content": "What color is the sky?"}]} apply_chat_template(prompt_only_example, tokenizer) # Output: {'prompt': '<|user|>\nWhat color is the sky?<|end|>\n<|assistant|>\n'} # Example for language modeling type lm_example = {"messages": [{"role": "user", "content": "What color is the sky?"}]} apply_chat_template(lm_example, tokenizer) # Output: {'text': '<|user|>\nWhat color is the sky?<|end|>\n<|endoftext|>'}
- The prompt-only output includes a
'<|assistant|>\n', indicating the beginning of the assistantβs turn and expecting the model to generate a completion.- In contrast, the language modeling output treats the input as a complete sequence and terminates it with
'<|endoftext|>', signaling the end of the text and not expecting any additional content.
Prompt-completion
A prompt-completion dataset includes a "prompt" and a "completion".
# Standard format
prompt_completion_example = {"prompt": "The sky is", "completion": " blue."}
# Conversational format
prompt_completion_example = {"prompt": [{"role": "user", "content": "What color is the sky?"}],
"completion": [{"role": "assistant", "content": "It is blue."}]}For examples of prompt-completion datasets, refer to the Prompt-completion datasets collection.
Preference
A preference dataset is used for tasks where the model is trained to choose between two or more possible completions to the same prompt. This dataset includes a "prompt", a "chosen" completion, and a "rejected" completion. The model is trained to select the "chosen" response over the "rejected" response.
Some datasets may not include the "prompt" column, in which case the prompt is implicit and directly included in the "chosen" and "rejected" completions. We recommend using explicit prompts whenever possible.
# Standard format
## Explicit prompt (recommended)
preference_example = {"prompt": "The sky is", "chosen": " blue.", "rejected": " green."}
# Implicit prompt
preference_example = {"chosen": "The sky is blue.", "rejected": "The sky is green."}
# Conversational format
## Explicit prompt (recommended)
preference_example = {"prompt": [{"role": "user", "content": "What color is the sky?"}],
"chosen": [{"role": "assistant", "content": "It is blue."}],
"rejected": [{"role": "assistant", "content": "It is green."}]}
## Implicit prompt
preference_example = {"chosen": [{"role": "user", "content": "What color is the sky?"},
{"role": "assistant", "content": "It is blue."}],
"rejected": [{"role": "user", "content": "What color is the sky?"},
{"role": "assistant", "content": "It is green."}]}For examples of preference datasets, refer to the Preference datasets collection.
Some preference datasets can be found with the tag dpo on Hugging Face Hub. You can also explore the librarian-bots' DPO Collections to identify preference datasets.
Unpaired preference
An unpaired preference dataset is similar to a preference dataset but instead of having "chosen" and "rejected" completions for the same prompt, it includes a single "completion" and a "label" indicating whether the completion is preferred or not.
# Standard format
unpaired_preference_example = {"prompt": "The sky is", "completion": " blue.", "label": True}
# Conversational format
unpaired_preference_example = {"prompt": [{"role": "user", "content": "What color is the sky?"}],
"completion": [{"role": "assistant", "content": "It is blue."}],
"label": True}For examples of unpaired preference datasets, refer to the Unpaired preference datasets collection.
Which dataset type to use?
Choosing the right dataset type depends on the task you are working on and the specific requirements of the TRL trainer you are using. Below is a brief overview of the dataset types supported by each TRL trainer.
| Trainer | Expected dataset type |
|---|---|
[DPOTrainer] |
Preference (explicit prompt recommended) |
[GRPOTrainer] |
Prompt-only |
[KTOTrainer] |
Unpaired preference or Preference (explicit prompt recommended) |
[RewardTrainer] |
Preference (implicit prompt recommended) |
[RLOOTrainer] |
Prompt-only |
[SFTTrainer] |
Language modeling or Prompt-completion |
[experimental.cpo.CPOTrainer] |
Preference (explicit prompt recommended) |
[experimental.gkd.GKDTrainer] |
Prompt-completion |
[experimental.online_dpo.OnlineDPOTrainer] |
Prompt-only |
[experimental.orpo.ORPOTrainer] |
Preference (explicit prompt recommended) |
Using any dataset with TRL: preprocessing and conversion
Many datasets come in formats tailored to specific tasks, which might not be directly compatible with TRL. To use such datasets with TRL, you may need to preprocess and convert them into the required format.
To make this easier, we provide a set of example scripts that cover common dataset conversions.
Example: UltraFeedback dataset
Letβs take the UltraFeedback dataset as an example. Here's a preview of the dataset:
As shown above, the dataset format does not match the expected structure. Itβs not in a conversational format, the column names differ, and the results pertain to different models (e.g., Bard, GPT-4) and aspects (e.g., "helpfulness", "honesty").
By using the provided conversion script examples/datasets/ultrafeedback.py, you can transform this dataset into an unpaired preference type, and push it to the Hub:
python examples/datasets/ultrafeedback.py --push_to_hub --repo_id trl-lib/ultrafeedback-gpt-3.5-turbo-helpfulnessOnce converted, the dataset will look like this:
Now, you can use this dataset with TRL!
By adapting the provided scripts or creating your own, you can convert any dataset into a format compatible with TRL.
Utilities for converting dataset types
This section provides example code to help you convert between different dataset types. While some conversions can be performed after applying the chat template (i.e., in the standard format), we recommend performing the conversion before applying the chat template to ensure it works consistently.
For simplicity, some of the examples below do not follow this recommendation and use the standard format. However, the conversions can be applied directly to the conversational format without modification.
| From \ To | Language modeling | Prompt-completion | Prompt-only | Preference with implicit prompt | Preference | Unpaired preference |
|---|---|---|---|---|---|---|
| Language modeling | N/A | N/A | N/A | N/A | N/A | N/A |
| Prompt-completion | π | N/A | π | N/A | N/A | N/A |
| Prompt-only | N/A | N/A | N/A | N/A | N/A | N/A |
| Preference with implicit prompt | π | π | π | N/A | π | π |
| Preference | π | π | π | π | N/A | π |
| Unpaired preference | π | π | π | N/A | N/A | N/A |
From prompt-completion to language modeling dataset
To convert a prompt-completion dataset into a language modeling dataset, concatenate the prompt and the completion.
from datasets import Dataset
dataset = Dataset.from_dict({
"prompt": ["The sky is", "The sun is"],
"completion": [" blue.", " in the sky."],
})
def concat_prompt_completion(example):
return {"text": example["prompt"] + example["completion"]}
dataset = dataset.map(concat_prompt_completion, remove_columns=["prompt", "completion"])>>> dataset[0]
{'text': 'The sky is blue.'}From prompt-completion to prompt-only dataset
To convert a prompt-completion dataset into a prompt-only dataset, remove the completion.
from datasets import Dataset
dataset = Dataset.from_dict({
"prompt": ["The sky is", "The sun is"],
"completion": [" blue.", " in the sky."],
})
dataset = dataset.remove_columns("completion")>>> dataset[0]
{'prompt': 'The sky is'}From preference with implicit prompt to language modeling dataset
To convert a preference with implicit prompt dataset into a language modeling dataset, remove the rejected, and rename the column "chosen" to "text".
from datasets import Dataset
dataset = Dataset.from_dict({
"chosen": ["The sky is blue.", "The sun is in the sky."],
"rejected": ["The sky is green.", "The sun is in the sea."],
})
dataset = dataset.rename_column("chosen", "text").remove_columns("rejected")>>> dataset[0]
{'text': 'The sky is blue.'}From preference with implicit prompt to prompt-completion dataset
To convert a preference dataset with implicit prompt into a prompt-completion dataset, extract the prompt with [extract_prompt], remove the rejected, and rename the column "chosen" to "completion".
from datasets import Dataset
from trl import extract_prompt
dataset = Dataset.from_dict({
"chosen": [
[{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}],
[{"role": "user", "content": "Where is the sun?"}, {"role": "assistant", "content": "In the sky."}],
],
"rejected": [
[{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is green."}],
[{"role": "user", "content": "Where is the sun?"}, {"role": "assistant", "content": "In the sea."}],
],
})
dataset = dataset.map(extract_prompt).remove_columns("rejected").rename_column("chosen", "completion")>>> dataset[0]
{'prompt': [{'role': 'user', 'content': 'What color is the sky?'}], 'completion': [{'role': 'assistant', 'content': 'It is blue.'}]}From preference with implicit prompt to prompt-only dataset
To convert a preference dataset with implicit prompt into a prompt-only dataset, extract the prompt with [extract_prompt], and remove the rejected and the chosen.
from datasets import Dataset
from trl import extract_prompt
dataset = Dataset.from_dict({
"chosen": [
[{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}],
[{"role": "user", "content": "Where is the sun?"}, {"role": "assistant", "content": "In the sky."}],
],
"rejected": [
[{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is green."}],
[{"role": "user", "content": "Where is the sun?"}, {"role": "assistant", "content": "In the sea."}],
],
})
dataset = dataset.map(extract_prompt).remove_columns(["chosen", "rejected"])>>> dataset[0]
{'prompt': [{'role': 'user', 'content': 'What color is the sky?'}]}From implicit to explicit prompt preference dataset
To convert a preference dataset with implicit prompt into a preference dataset with explicit prompt, extract the prompt with [extract_prompt].
from datasets import Dataset
from trl import extract_prompt
dataset = Dataset.from_dict({
"chosen": [
[{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}],
[{"role": "user", "content": "Where is the sun?"}, {"role": "assistant", "content": "In the sky."}],
],
"rejected": [
[{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is green."}],
[{"role": "user", "content": "Where is the sun?"}, {"role": "assistant", "content": "In the sea."}],
],
})
dataset = dataset.map(extract_prompt)>>> dataset[0]
{'prompt': [{'role': 'user', 'content': 'What color is the sky?'}],
'chosen': [{'role': 'assistant', 'content': 'It is blue.'}],
'rejected': [{'role': 'assistant', 'content': 'It is green.'}]}From preference with implicit prompt to unpaired preference dataset
To convert a preference dataset with implicit prompt into an unpaired preference dataset, extract the prompt with [extract_prompt], and unpair the dataset with [unpair_preference_dataset].
from datasets import Dataset
from trl import extract_prompt, unpair_preference_dataset
dataset = Dataset.from_dict({
"chosen": [
[{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}],
[{"role": "user", "content": "Where is the sun?"}, {"role": "assistant", "content": "In the sky."}],
],
"rejected": [
[{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is green."}],
[{"role": "user", "content": "Where is the sun?"}, {"role": "assistant", "content": "In the sea."}],
],
})
dataset = dataset.map(extract_prompt)
dataset = unpair_preference_dataset(dataset)>>> dataset[0]
{'prompt': [{'role': 'user', 'content': 'What color is the sky?'}],
'completion': [{'role': 'assistant', 'content': 'It is blue.'}],
'label': True}[!WARNING]
Keep in mind that the"chosen"and"rejected"completions in a preference dataset can be both good or bad.
Before applying [unpair_preference_dataset], please ensure that all"chosen"completions can be labeled as good and all"rejected"completions as bad.
This can be ensured by checking absolute rating of each completion, e.g. from a reward model.
From preference to language modeling dataset
To convert a preference dataset into a language modeling dataset, remove the rejected, concatenate the prompt and the chosen into the "text" column.
from datasets import Dataset
dataset = Dataset.from_dict({
"prompt": ["The sky is", "The sun is"],
"chosen": [" blue.", " in the sky."],
"rejected": [" green.", " in the sea."],
})
def concat_prompt_chosen(example):
return {"text": example["prompt"] + example["chosen"]}
dataset = dataset.map(concat_prompt_chosen, remove_columns=["prompt", "chosen", "rejected"])>>> dataset[0]
{'text': 'The sky is blue.'}From preference to prompt-completion dataset
To convert a preference dataset into a prompt-completion dataset, remove the rejected, and rename the column "chosen" to "completion".
from datasets import Dataset
dataset = Dataset.from_dict({
"prompt": ["The sky is", "The sun is"],
"chosen": [" blue.", " in the sky."],
"rejected": [" green.", " in the sea."],
})
dataset = dataset.remove_columns("rejected").rename_column("chosen", "completion")>>> dataset[0]
{'prompt': 'The sky is', 'completion': ' blue.'}From preference to prompt-only dataset
To convert a preference dataset into a prompt-only dataset, remove the rejected and the chosen.
from datasets import Dataset
dataset = Dataset.from_dict({
"prompt": ["The sky is", "The sun is"],
"chosen": [" blue.", " in the sky."],
"rejected": [" green.", " in the sea."],
})
dataset = dataset.remove_columns(["chosen", "rejected"])>>> dataset[0]
{'prompt': 'The sky is'}From explicit to implicit prompt preference dataset
To convert a preference dataset with explicit prompt into a preference dataset with implicit prompt, concatenate the prompt to both chosen and rejected, and remove the prompt.
from datasets import Dataset
dataset = Dataset.from_dict({
"prompt": [
[{"role": "user", "content": "What color is the sky?"}],
[{"role": "user", "content": "Where is the sun?"}],
],
"chosen": [
[{"role": "assistant", "content": "It is blue."}],
[{"role": "assistant", "content": "In the sky."}],
],
"rejected": [
[{"role": "assistant", "content": "It is green."}],
[{"role": "assistant", "content": "In the sea."}],
],
})
def concat_prompt_to_completions(example):
return {"chosen": example["prompt"] + example["chosen"], "rejected": example["prompt"] + example["rejected"]}
dataset = dataset.map(concat_prompt_to_completions, remove_columns="prompt")>>> dataset[0]
{'chosen': [{'role': 'user', 'content': 'What color is the sky?'}, {'role': 'assistant', 'content': 'It is blue.'}],
'rejected': [{'role': 'user', 'content': 'What color is the sky?'}, {'role': 'assistant', 'content': 'It is green.'}]}From preference to unpaired preference dataset
To convert dataset into an unpaired preference dataset, unpair the dataset with [unpair_preference_dataset].
from datasets import Dataset
from trl import unpair_preference_dataset
dataset = Dataset.from_dict({
"prompt": [
[{"role": "user", "content": "What color is the sky?"}],
[{"role": "user", "content": "Where is the sun?"}],
],
"chosen": [
[{"role": "assistant", "content": "It is blue."}],
[{"role": "assistant", "content": "In the sky."}],
],
"rejected": [
[{"role": "assistant", "content": "It is green."}],
[{"role": "assistant", "content": "In the sea."}],
],
})
dataset = unpair_preference_dataset(dataset)>>> dataset[0]
{'prompt': [{'role': 'user', 'content': 'What color is the sky?'}],
'completion': [{'role': 'assistant', 'content': 'It is blue.'}],
'label': True}[!WARNING]
Keep in mind that the"chosen"and"rejected"completions in a preference dataset can be both good or bad.
Before applying [unpair_preference_dataset], please ensure that all"chosen"completions can be labeled as good and all"rejected"completions as bad.
This can be ensured by checking absolute rating of each completion, e.g. from a reward model.
From unpaired preference to language modeling dataset
To convert an unpaired preference dataset into a language modeling dataset, concatenate prompts with good completions into the "text" column, and remove the prompt, completion and label columns.
from datasets import Dataset
dataset = Dataset.from_dict({
"prompt": ["The sky is", "The sun is", "The sky is", "The sun is"],
"completion": [" blue.", " in the sky.", " green.", " in the sea."],
"label": [True, True, False, False],
})
def concatenate_prompt_completion(example):
return {"text": example["prompt"] + example["completion"]}
dataset = dataset.filter(lambda x: x["label"]).map(concatenate_prompt_completion).remove_columns(["prompt", "completion", "label"])>>> dataset[0]
{'text': 'The sky is blue.'}From unpaired preference to prompt-completion dataset
To convert an unpaired preference dataset into a prompt-completion dataset, filter for good labels, then remove the label columns.
from datasets import Dataset
dataset = Dataset.from_dict({
"prompt": ["The sky is", "The sun is", "The sky is", "The sun is"],
"completion": [" blue.", " in the sky.", " green.", " in the sea."],
"label": [True, True, False, False],
})
dataset = dataset.filter(lambda x: x["label"]).remove_columns(["label"])>>> dataset[0]
{'prompt': 'The sky is', 'completion': ' blue.'}From unpaired preference to prompt-only dataset
To convert an unpaired preference dataset into a prompt-only dataset, remove the completion and the label columns.
from datasets import Dataset
dataset = Dataset.from_dict({
"prompt": ["The sky is", "The sun is", "The sky is", "The sun is"],
"completion": [" blue.", " in the sky.", " green.", " in the sea."],
"label": [True, True, False, False],
})
dataset = dataset.remove_columns(["completion", "label"])>>> dataset[0]
{'prompt': 'The sky is'}Vision datasets
Some trainers also support fine-tuning vision-language models (VLMs) using image-text pairs. In this scenario, it's recommended to use a conversational format, as each model handles image placeholders in text differently.
A conversational vision dataset differs from a standard conversational dataset in two key ways:
- The dataset must contain the key
imageswith the image data (as lists of PIL images) orimagewith a single PIL image. - The
"content"field in messages must be a list of dictionaries, where each dictionary specifies the type of data:"image"or"text".
Example:
# Textual dataset:
"content": "What color is the sky?"
# Vision dataset:
"content": [
{"type": "image"},
{"type": "text", "text": "What color is the sky in the image?"}
]An example of a conversational vision dataset is the openbmb/RLAIF-V-Dataset. Below is an embedded view of the dataset's training data, allowing you to explore it directly:
[!NOTE]
Mixing text-only and vision-language data in the dataset is possible, but it requirestransformersversion 4.57.0 or later. Example:pythondataset = Dataset.from_dict({ "prompt": [ [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What color is the sky in the image?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What is the capital of France?"}]}], ], "completion": [ [{"role": "assistant", "content": [{"type": "text", "text": "It is blue."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Paris."}]}], ], "images": [ [PIL.Image.open("path/to/sky_image1.png")], [], ], })
Source Deepspeed Integration (`docs/source/deepspeed_integration.md`)
DeepSpeed Integration
TRL supports training with DeepSpeed, a library that implements advanced training optimization techniques. These include optimizer state partitioning, offloading, gradient partitioning, and more.
DeepSpeed integrates the Zero Redundancy Optimizer (ZeRO), which allows you to scale the model size proportional to the number of devices with sustained high efficiency.
Installation
To use DeepSpeed with TRL, install it using the following command:
pip install deepspeedRunning Training Scripts with DeepSpeed
No modifications to your training script are required. Simply run it with the DeepSpeed configuration file:
accelerate launch --config_file <ACCELERATE_WITH_DEEPSPEED_CONFIG_FILE.yaml> train.pyWe provide ready-to-use DeepSpeed configuration files in the examples/accelerate_configs directory. For example, to run training with ZeRO Stage 2, use the following command:
accelerate launch --config_file examples/accelerate_configs/deepspeed_zero2.yaml train.pyAdditional Resources
Consult the π€ Accelerate documentation for more information about the DeepSpeed plugin.
Source Distillation Trainer (`docs/source/distillation_trainer.md`)
Distillation Trainer
Overview
The Distillation Trainer implements on-policy knowledge distillation as described in On-Policy Distillation of Language Models: Learning from Self-Generated Mistakes by Rishabh Agarwal, Nino Vieillard, Yongchao Zhou, Piotr Stanczyk, Sabela Ramos, Matthieu Geist, and Olivier Bachem.
The abstract from the paper is the following:
Knowledge distillation (KD) is widely used for compressing a teacher model to reduce its inference cost and memory footprint, by training a smaller student model. However, current KD methods for auto-regressive sequence models suffer from distribution mismatch between output sequences seen during training and those generated by the student during inference. To address this issue, we introduce Generalized Knowledge Distillation (GKD). Instead of solely relying on a fixed set of output sequences, GKD trains the student on its self-generated output sequences by leveraging feedback from the teacher on such sequences. Unlike supervised KD approaches, GKD also offers the flexibility to employ alternative loss functions between the student and teacher, which can be useful when the student lacks the expressivity to mimic the teacher's distribution.
The DistillationTrainer trains a smaller student model to match a teacher's next-token distribution on the student's own on-policy generations. It generates the student's completions on-policy (optionally vLLM-powered) and matches the teacher over its full next-token distribution with a memory-efficient chunked Jensen-Shannon divergence loss, so the teacher's dense distribution is never materialized in full.
This trainer was contributed by Carlos Miguel PatiΓ±o.
For an async counterpart that decouples generation from training and scores against a teacher served over HTTP instead of a local forward pass, see AsyncDistillationTrainer.
Quick start
This example demonstrates how to train a model using the distillation method. We distill a Qwen 2.5 0.5B Instruct model from a Qwen 2.5 1.5B Instruct teacher on the prompts from the UltraFeedback prompt dataset. You can view the data in the dataset here:
Below is the script to train the model.
# train_distillation.py
from datasets import load_dataset
from trl import DistillationTrainer
dataset = load_dataset("trl-lib/ultrafeedback-prompt", split="train")
trainer = DistillationTrainer(
model="Qwen/Qwen2.5-0.5B-Instruct",
teacher_model="Qwen/Qwen2.5-1.5B-Instruct",
train_dataset=dataset,
)
trainer.train()Execute the script using the following command:
accelerate launch train_distillation.pyLooking deeper into the distillation method
On-policy knowledge distillation trains a student to reproduce a teacher's next-token distribution on completions the student generates itself, rather than on a fixed set of teacher outputs. Learning from its own generations lets the student correct its own mistakes, which generally outperforms off-policy distillation. This section breaks down how it works in practice, covering the two key steps: generating completions and computing the loss.
Generating completions
At each training step, the student generates a batch of completions for the sampled prompts.
Computing the loss
The loss is the generalized Jensen-Shannon divergence (JSD) between the student distribution \( p_S \) and the teacher distribution \( p_T \) over the generated completion tokens, interpolated by beta and defined as:
$$
\mathcal{L}\beta = \beta , \mathbb{D}{\mathrm{KL}}!\left[ p_T | p_M \right] + (1 - \beta) , \mathbb{D}_{\mathrm{KL}}!\left[ p_S | p_M \right], \qquad p_M = (1 - \beta) , p_S + \beta , p_T,
$$
where \( p_M \) is the \( \beta \) -mixture of the two distributions. The endpoints reduce to the pure divergences: beta=0.0 gives the forward KL \( \mathbb{D}{\mathrm{KL}}!\left[ p_T | p_S \right] \) and beta=1.0 the reverse KL \( \mathbb{D}{\mathrm{KL}}!\left[ p_S | p_T \right] \).
In practice, the projection to vocabulary logits and the divergence are computed in chunks, so peak activation memory does not scale with the full vocabulary Γ sequence-length logits tensor. See Reducing Memory Usage.
Expected dataset type
The dataset should be formatted as a conversational prompt-only dataset. The student generates its own completions on-policy, so only the prompt is needed:
{"prompt": [{"role": "user", "content": "What color is the sky?"}]}Logged metrics
While training and evaluating, we record the following metrics:
num_tokens: The total number of tokens processed so far, including both prompts and completions. When using tools, only non-tool tokens are counted.step_time: The average time (in seconds) taken per training step (including generation).completions/mean_length: The average length of generated completions. When using tools, only non-tool tokens are counted.completions/min_length: The minimum length of generated completions. When using tools, only non-tool tokens are counted.completions/max_length: The maximum length of generated completions. When using tools, only non-tool tokens are counted.completions/mean_terminated_length: The average length of generated completions that terminate with EOS. When using tools, only non-tool tokens are counted.completions/min_terminated_length: The minimum length of generated completions that terminate with EOS. When using tools, only non-tool tokens are counted.completions/max_terminated_length: The maximum length of generated completions that terminate with EOS. When using tools, only non-tool tokens are counted.completions/clipped_ratio: The ratio of truncated (clipped) completions.tools/call_frequency: The average number of tool calls per completion in the generation batch. Logged only whentoolsare provided.tools/failure_frequency: The fraction of tool calls that failed (the tool was not found, raised an exception, or the call type is unsupported). It is0.0when no tool was called. Logged only whentoolsare provided.entropy: Average entropy of token predictions across generated completions (in nats).
Customization
Speed up training with vLLM-powered generation
Generation is often the main bottleneck when training with on-policy methods. To accelerate generation, you can use vLLM, a high-throughput, low-latency inference engine for LLMs. To enable it, first install the package with
pip install trl[vllm]We support two ways of using vLLM during training: colocate mode and server mode.
Option 1: Colocate mode
In this mode, vLLM runs inside the trainer process and shares GPU memory with the training model. This avoids launching a separate server and can improve GPU utilization, but may lead to memory contention on the training GPUs. This is the default mode.
from trl import DistillationConfig
training_args = DistillationConfig(
...,
use_vllm=True, # vllm_mode="colocate" by default
)Option 2: Server mode
In this mode, vLLM runs in a separate process (and using separate GPUs) and communicates with the trainer via HTTP. This is ideal if you have dedicated GPUs for inference.
Start the vLLM server:
bashVLLM_SERVER_DEV_MODE=1 vllm serve <model_name> \ --weight-transfer-config '{"backend": "nccl"}' \ --logprobs-mode processed_logprobs \ --max-logprobs -1Enable server mode in your training script:
pythonfrom trl import DistillationConfig training_args = DistillationConfig( ..., use_vllm=True, vllm_mode="server", )
[!WARNING]
Make sure that the server is using different GPUs than the trainer, otherwise you may run into NCCL errors. You can specify the GPUs to use with theCUDA_VISIBLE_DEVICESenvironment variable.
[!TIP]
Depending on the model size and the overall GPU memory requirements for training, you may need to adjust thevllm_gpu_memory_utilizationparameter in [DistillationConfig] to avoid underutilization or out-of-memory errors.
For more information, see Speeding up training with vLLM.
Train adapters with PEFT
We support tight integration with the π€ PEFT library, letting you train adapters and share them on the Hub rather than training the whole student.
from datasets import load_dataset
from trl import DistillationTrainer
from peft import LoraConfig
dataset = load_dataset("trl-lib/ultrafeedback-prompt", split="train")
trainer = DistillationTrainer(
model="Qwen/Qwen2.5-0.5B-Instruct",
teacher_model="Qwen/Qwen2.5-1.5B-Instruct",
train_dataset=dataset,
peft_config=LoraConfig(),
)
trainer.train()[!WARNING]
The distillation loss readslm_head.weightdirectly and runs the student backbone without going throughPeftModel.forward(). Adapters onlm_head(viatarget_modules) and prompt-learning methods (PromptTuning, PrefixTuning, P-Tuning) are therefore rejected, since they would be silently ignored. To train the head, usemodules_to_save=["lm_head"]instead.
Agent Training
[DistillationTrainer] supports agent training: the student calls tools during generation and is distilled on the resulting trajectory. Tool-result tokens are masked out of the loss, so the student is only trained on the tokens it generated itself.
Tools
The tools argument expects a list of Python functions that define the tools available to the agent:
from trl import DistillationTrainer
trainer = DistillationTrainer(
tools=[tool1, tool2],
...,
)Each tool must be a standard Python function with type-hinted arguments and return types, along with a Google-style docstring describing its purpose, arguments, and return value.
For more details, see the Passing tools guide.
[!TIP]
The tool call loop requires the chat template to be prefix-preserving (appending a tool message must not change how earlier messages are rendered). For known model families (e.g. Qwen3, DeepSeek-V3), TRL automatically swaps in a patched training template when tools are enabled. See Chat Templates for the full list.
Use max_tool_calling_iterations in the [DistillationConfig] to cap the number of tool-calling turns. By default there is no limit, and generation stops when the student produces a response turn with no tool calls.
Example:
from trl import DistillationTrainer
def multiply(a: int, b: int) -> int:
"""
Multiplies two integers.
Args:
a: The first integer.
b: The second integer.
Returns:
The product of the two integers.
"""
return a * b
trainer = DistillationTrainer(
tools=[multiply],
...,
)[!WARNING]
Async tools are not supported yet β pass synchronous functions.
Multimodal Tool Responses
Tools can return images alongside text by returning a list of content blocks. This is useful for VLM agent training where the tool provides visual feedback (e.g., screenshots, plots, camera captures).
from PIL import Image
def take_screenshot() -> list:
"""
Takes a screenshot of the current screen.
Returns:
The screenshot image with a description.
"""
img = Image.open("screenshot.png")
return [{"type": "image", "image": img}, {"type": "text", "text": "Here is the screenshot."}]The returned images are automatically injected into the conversation and passed to the VLM for subsequent generation turns.
Training Vision Language Models
[DistillationTrainer] supports distilling Vision-Language Models (VLMs) on multimodal datasets containing both text and images. Pass a VLM as both the student and the teacher, and provide a prompt-only dataset with either an image column (single image per sample) or an images column (list of images per sample). For more information on the expected dataset structure, see the Dataset Format β Vision datasets section.
Tested with:
- Gemma 3 β e.g.,
google/gemma-3-4b-it - LLaVA-NeXT β e.g.,
llava-hf/llava-v1.6-mistral-7b-hf - Qwen2-VL β e.g.,
Qwen/Qwen2-VL-2B-Instruct - Qwen2.5-VL β e.g.,
Qwen/Qwen2.5-VL-3B-Instruct
[!TIP]
Compatibility with all VLMs is not guaranteed. If you believe a model should be supported, feel free to open an issue on GitHub β or better yet, submit a pull request with the required changes.
Command line interface
Use the trl distillation CLI to launch distillation training from the command line. It supports full training and LoRA via the standard ModelConfig flags.
# Full training:
trl distillation \
--model_name_or_path Qwen/Qwen2.5-0.5B-Instruct \
--teacher_model_name_or_path Qwen/Qwen2.5-1.5B-Instruct \
--dataset_name trl-lib/ultrafeedback-prompt \
--learning_rate 2e-5 \
--per_device_train_batch_size 4 \
--gradient_accumulation_steps 8 \
--output_dir distilled-model \
--num_train_epochs 1# LoRA:
trl distillation \
--model_name_or_path Qwen/Qwen2.5-0.5B-Instruct \
--teacher_model_name_or_path Qwen/Qwen2.5-1.5B-Instruct \
--dataset_name trl-lib/ultrafeedback-prompt \
--learning_rate 2e-4 \
--per_device_train_batch_size 4 \
--gradient_accumulation_steps 8 \
--output_dir distilled-model \
--num_train_epochs 1 \
--use_peft \
--lora_r 64 \
--lora_alpha 16DistillationTrainer
[[autodoc]] DistillationTrainer
- train
- save_model
- push_to_hub
DistillationConfig
[[autodoc]] DistillationConfig
Source Distributing Training (`docs/source/distributing_training.md`)
Distributing Training
Multi-GPU Training with TRL
The trainers in TRL use π€ Accelerate to enable distributed training across multiple GPUs or nodes. To do so, first create an π€ Accelerate config file by running
accelerate configand answering the questions according to your multi-GPU / multi-node setup. You can then launch distributed training by running:
accelerate launch train.pyWe also provide config files in the examples folder that can be used as templates. To use these templates, simply pass the path to the config file when launching a job, e.g.:
accelerate launch --config_file examples/accelerate_configs/multi_gpu.yaml train.py <SCRIPT_ARGS>This automatically distributes the workload across all available GPUs.
Under the hood, π€ Accelerate creates one model per GPU. Each process:
- Processes its own batch of data
- Computes the loss and gradients for that batch
- Shares gradient updates across all GPUs
The effective batch size is calculated as:
$$
\text{Batch Size} = \text{per_device_train_batch_size} \times \text{num_devices} \times \text{gradient_accumulation_steps}
$$
To maintain a consistent batch size when scaling to multiple GPUs, make sure to update per_device_train_batch_size and gradient_accumulation_steps accordingly.
Example, these configurations are equivalent, and should yield the same results:
| Number of GPUs | Per device batch size | Gradient accumulation steps | Comments |
|---|---|---|---|
| 1 | 32 | 1 | Possibly high memory usage, but faster training |
| 1 | 4 | 8 | Lower memory usage, slower training |
| 8 | 4 | 1 | Multi-GPU to get the best of both worlds |
[!TIP]
Having one model per GPU can lead to high memory usage, which may not be feasible for large models or low-memory GPUs. In such cases, you can leverage DeepSpeed, which provides optimizations like model sharding, Zero Redundancy Optimizer, mixed precision training, and offloading to CPU or NVMe. Check out our DeepSpeed Integration guide for more details.
[!TIP]
Training on very long sequences has its own guide: Training Beyond 1M Tokens.
Multi-Node Training
When a single machine doesn't have enough GPUs, TRL can scale training across multiple machines (nodes) using π€ Accelerate.
Accelerate Configuration
Create an accelerate config file (e.g., multi_node.yaml) for multi-node training. Key fields:
compute_environment: LOCAL_MACHINE
distributed_type: MULTI_GPU
num_machines: 2
machine_rank: 0 # 0 for main node, 1 for second node
main_process_ip: 10.0.0.1 # IP of rank 0 node
main_process_port: 29500
num_processes: 16 # total processes across nodes
mixed_precision: bf16
use_cpu: false
same_network: trueAdjust num_processes to match the total number of GPUs across all nodes.
[!NOTE]
Replace10.0.0.1with the actual IP address of the rank 0 (main) node.
Launching
Option 1: Manual Launch (Non-HPC)
Run the following on each node manually:
# Node 0 (main node)
accelerate launch --config_file multi_node.yaml --machine_rank 0 train.py
# Node 1
accelerate launch --config_file multi_node.yaml --machine_rank 1 train.pyOption 2: SLURM Launch (HPC Clusters)
For clusters using SLURM job scheduler, create a job script (e.g., slurm_job.sh):
#!/bin/bash
#SBATCH --nodes=2
#SBATCH --gpus-per-node=8
#SBATCH --job-name=trl_multi
srun accelerate launch --config_file multi_node.yaml train.pyThen submit the job:
sbatch slurm_job.shSLURM automatically distributes the training across all requested nodes and GPUs, and srun configures the necessary environment variables for multi-node communication.
Key SLURM directives:
--nodes=2: Request 2 compute nodes--gpus-per-node=8: Allocate 8 GPUs per node (16 total)--job-name: Label for tracking in the job queue
You can combine multi-node with DeepSpeed by setting distributed_type: DEEPSPEED and adding a deepspeed_config block. See the DeepSpeed integration guide.
Further Reading
- Accelerate: Launching Scripts
- Accelerate: Example Zoo
- SLURM Workload Manager Documentation - For cluster job scheduling
Source Dpo Trainer (`docs/source/dpo_trainer.md`)
DPO Trainer
Overview
TRL supports the Direct Preference Optimization (DPO) Trainer for training language models, as described in the paper Direct Preference Optimization: Your Language Model is Secretly a Reward Model by Rafael Rafailov, Archit Sharma, Eric Mitchell, Stefano Ermon, Christopher D. Manning, Chelsea Finn.
The abstract from the paper is the following:
While large-scale unsupervised language models (LMs) learn broad world knowledge and some reasoning skills, achieving precise control of their behavior is difficult due to the completely unsupervised nature of their training. Existing methods for gaining such steerability collect human labels of the relative quality of model generations and fine-tune the unsupervised LM to align with these preferences, often with reinforcement learning from human feedback (RLHF). However, RLHF is a complex and often unstable procedure, first fitting a reward model that reflects the human preferences, and then fine-tuning the large unsupervised LM using reinforcement learning to maximize this estimated reward without drifting too far from the original model. In this paper we introduce a new parameterization of the reward model in RLHF that enables extraction of the corresponding optimal policy in closed form, allowing us to solve the standard RLHF problem with only a simple classification loss. The resulting algorithm, which we call Direct Preference Optimization (DPO), is stable, performant, and computationally lightweight, eliminating the need for sampling from the LM during fine-tuning or performing significant hyperparameter tuning. Our experiments show that DPO can fine-tune LMs to align with human preferences as well as or better than existing methods. Notably, fine-tuning with DPO exceeds PPO-based RLHF in ability to control sentiment of generations, and matches or improves response quality in summarization and single-turn dialogue while being substantially simpler to implement and train.
This post-training method was contributed by Kashif Rasul and later refactored by Quentin GallouΓ©dec.
Quick start
This example demonstrates how to train a language model using the [DPOTrainer] from TRL. We train a Qwen 3 0.6B model on the UltraFeedback dataset.
from trl import DPOTrainer
from datasets import load_dataset
trainer = DPOTrainer(
model="Qwen/Qwen3-0.6B",
train_dataset=load_dataset("trl-lib/ultrafeedback_binarized", split="train"),
)
trainer.train()Expected dataset type and format
DPO requires a preference dataset. The [DPOTrainer] is compatible with both standard and conversational dataset formats. When provided with a conversational dataset, the trainer will automatically apply the chat template to the dataset.
# Standard format
## Explicit prompt (recommended)
preference_example = {"prompt": "The sky is", "chosen": " blue.", "rejected": " green."}
# Implicit prompt
preference_example = {"chosen": "The sky is blue.", "rejected": "The sky is green."}
# Conversational format
## Explicit prompt (recommended)
preference_example = {"prompt": [{"role": "user", "content": "What color is the sky?"}],
"chosen": [{"role": "assistant", "content": "It is blue."}],
"rejected": [{"role": "assistant", "content": "It is green."}]}
## Implicit prompt
preference_example = {"chosen": [{"role": "user", "content": "What color is the sky?"},
{"role": "assistant", "content": "It is blue."}],
"rejected": [{"role": "user", "content": "What color is the sky?"},
{"role": "assistant", "content": "It is green."}]}If your dataset is not in one of these formats, you can preprocess it to convert it into the expected format. Here is an example with the Vezora/Code-Preference-Pairs dataset:
from datasets import load_dataset
dataset = load_dataset("Vezora/Code-Preference-Pairs")
def preprocess_function(example):
return {
"prompt": [{"role": "user", "content": example["input"]}],
"chosen": [{"role": "assistant", "content": example["accepted"]}],
"rejected": [{"role": "assistant", "content": example["rejected"]}],
}
dataset = dataset.map(preprocess_function, remove_columns=["instruction", "input", "accepted", "ID"])
print(next(iter(dataset["train"]))){
"prompt": [{"role": "user", "content": "Create a nested loop to print every combination of numbers [...]"}],
"chosen": [{"role": "assistant", "content": "Here is an example of a nested loop in Python [...]"}],
"rejected": [{"role": "assistant", "content": "Here is an example of a nested loop in Python [...]"}],
}Looking deeper into the DPO method
Direct Preference Optimization (DPO) is a training method designed to align a language model with preference data. Instead of supervised inputβoutput pairs, the model is trained on pairs of completions to the same prompt, where one completion is preferred over the other. The objective directly optimizes the model to widen the margin between the log-likelihoods of preferred and dispreferred completions, relative to a reference model, without requiring an explicit reward model. In practice, this is typically achieved by suppressing the likelihood of dispreferred completions rather than by increasing the likelihood of preferred ones.
This section breaks down how DPO works in practice, covering the key steps: preprocessing and loss computation.
Preprocessing and tokenization
During training, each example is expected to contain a prompt along with a preferred (chosen) and a dispreferred (rejected) completion. For more details on the expected formats, see Dataset formats.
The [DPOTrainer] tokenizes each input using the model's tokenizer.
Computing the loss
The loss used in DPO is defined as follows:
$$
\mathcal{L}{\mathrm{DPO}}(\theta) = -\mathbb{E}{(x,y^{+},y^{-})}!\left[\log \sigma!\left(\beta\Big(\log\frac{\pi_{\theta}(y^{+}!\mid x)}{\pi_{\mathrm{ref}}(y^{+}!\mid x)}-\log \frac{\pi_{\theta}(y^{-}!\mid x)}{\pi_{\mathrm{ref}}(y^{-}!\mid x)}\Big)\right)\right]
$$
where \( x \) is the prompt, \( y^+ \) is the preferred completion and \( y^- \) is the dispreferred completion. \( \pi_{\theta} \) is the policy model being trained, \( \pi_{\mathrm{ref}} \) is the reference model, \( \sigma \) is the sigmoid function, and \( \beta > 0 \) is a hyperparameter that controls the strength of the preference signal.
Loss Types
Several formulations of the objective have been proposed in the literature. Initially, the objective of DPO was defined as presented above.
loss_type= |
Description |
|---|---|
"sigmoid" (default) |
Given the preference data, we can fit a binary classifier according to the Bradley-Terry model and in fact the DPO authors propose the sigmoid loss on the normalized likelihood via the logsigmoid to fit a logistic regression. |
"hinge" |
The RSO authors propose to use a hinge loss on the normalized likelihood from the SLiC paper. In this case, the beta is the reciprocal of the margin. |
"ipo" |
The IPO authors argue the logit transform can overfit and propose the identity transform to optimize preferences directly; TRL exposes this as loss_type="ipo". |
"exo_pair" |
The EXO authors propose reverse-KL preference optimization. label_smoothing must be strictly greater than 0.0; a recommended value is 1e-3 (see Eq. 16 for the simplified pairwise variant). The full method uses K>2 SFT completions and approaches PPO as K grows. |
"nca_pair" |
The NCA authors shows that NCA optimizes the absolute likelihood for each response rather than the relative likelihood. |
"robust" |
The Robust DPO authors propose an unbiased DPO loss under noisy preferences. Use label_smoothing in [DPOConfig] to model label-flip probability; valid values are in the range [0.0, 0.5). |
"bco_pair" |
The BCO authors train a binary classifier whose logit serves as a reward so that the classifier maps {prompt, chosen completion} pairs to 1 and {prompt, rejected completion} pairs to 0. |
"sppo_hard" |
The SPPO authors claim that SPPO is capable of solving the Nash equilibrium iteratively by pushing the chosen rewards to be as large as 1/2 and the rejected rewards to be as small as -1/2 and can alleviate data sparsity issues. The implementation approximates this algorithm by employing hard label probabilities, assigning 1 to the winner and 0 to the loser. |
"aot" or loss_type="aot_unpaired" |
The AOT authors propose Distributional Preference Alignment via Optimal Transport. loss_type="aot" is for paired data; loss_type="aot_unpaired" is for unpaired data. Both enforce stochastic dominance via sorted quantiles; larger per-GPU batch sizes help. |
"apo_zero" or loss_type="apo_down" |
The APO method introduces an anchored objective. apo_zero boosts winners and downweights losers (useful when the model underperforms the winners). apo_down downweights both, with stronger pressure on losers (useful when the model already outperforms winners). |
"discopop" |
The DiscoPOP paper uses LLMs to discover more efficient offline preference optimization losses. In the paper the proposed DiscoPOP loss (which is a log-ratio modulated loss) outperformed other optimization losses on different tasks (IMDb positive text generation, Reddit TLDR summarization, and Alpaca Eval 2.0). |
"sft" |
SFT (Supervised Fine-Tuning) loss is the negative log likelihood loss, used to train the model to generate preferred responses. |
"sigmoid_norm" |
The SimPO authors address the length-bias in the original sigmoid loss by normalizing by the number of non-mask tokens; TRL exposes this as loss_type="sigmoid_norm". |
Logged metrics
While training and evaluating, we record the following metrics:
global_step: The total number of optimizer steps taken so far.epoch: The current epoch number, based on dataset iteration.num_tokens: The total number of tokens processed so far.loss: The average DPO loss over the current logging interval.entropy: The average entropy of the model's predicted token distribution over non-masked tokens.aux_loss: The load-balancing auxiliary loss of a Mixture-of-Experts model, before it is scaled byrouter_aux_loss_coefand added to the loss. Logged only when the model is a MoE model androuter_aux_loss_coefis nonzero.mean_token_accuracy: The proportion of non-masked tokens for which the modelβs top-1 prediction matches the token from the chosen completion.learning_rate: The current learning rate, which may change dynamically if a scheduler is used.grad_norm: The L2 norm of the gradients, computed before gradient clipping.logits/chosen: The average logit values assigned by the model to the tokens in the chosen completion.logits/rejected: The average logit values assigned by the model to the tokens in the rejected completion.logps/chosen: The average log-probability assigned by the model to the tokens in the chosen completion.logps/rejected: The average log-probability assigned by the model to the tokens in the rejected completion.rewards/chosen: The average implicit reward computed for the chosen completion, computed as \( \beta \log \frac{\pi_{\theta}(y^{+}!\mid x)}{\pi_{\mathrm{ref}}(y^{+}!\mid x)} \).rewards/rejected: The average implicit reward computed for the rejected completion, computed as \( \beta \log \frac{\pi_{\theta}(y^{-}!\mid x)}{\pi_{\mathrm{ref}}(y^{-}!\mid x)} \).rewards/margins: The average implicit reward margin between the chosen and rejected completions.rewards/accuracies: The proportion of examples where the implicit reward for the chosen completion is higher than that for the rejected completion.
Customization
Compatibility and constraints
Some argument combinations are intentionally restricted in the current [DPOTrainer] implementation:
use_weighting=Trueis not supported withloss_type="aot"orloss_type="aot_unpaired".- PEFT adapters targeting
lm_headare not supported (usemodules_to_save=["lm_head"]instead). sync_ref_model=Trueis not supported when training with PEFT models that do not keep a standaloneref_model.sync_ref_model=Truecannot be combined withprecompute_ref_log_probs=True.precompute_ref_log_probs=Trueis not supported withIterableDataset(train or eval).
Multi-loss combinations
The DPO trainer supports combining multiple loss functions with different weights, enabling more sophisticated optimization strategies. This is particularly useful for implementing algorithms like MPO (Mixed Preference Optimization). MPO is a training approach that combines multiple optimization objectives, as described in the paper Enhancing the Reasoning Ability of Multimodal Large Language Models via Mixed Preference Optimization.
To combine multiple losses, specify the loss types and corresponding weights as lists:
# MPO: Combines DPO (sigmoid) for preference and BCO (bco_pair) for quality
training_args = DPOConfig(
loss_type=["sigmoid", "bco_pair", "sft"], # loss types to combine
loss_weights=[0.8, 0.2, 1.0] # corresponding weights, as used in the MPO paper
)Model initialization
You can directly pass the kwargs of the [~transformers.AutoModelForCausalLM.from_pretrained()] method to the [DPOConfig]. For example, if you want to load a model in a different precision, analogous to
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B", dtype=torch.bfloat16)you can do so by passing the model_init_kwargs={"dtype": torch.bfloat16} argument to the [DPOConfig].
from trl import DPOConfig
training_args = DPOConfig(
model_init_kwargs={"dtype": torch.bfloat16},
)Note that all keyword arguments of [~transformers.AutoModelForCausalLM.from_pretrained()] are supported.
Train adapters with PEFT
We support tight integration with π€ PEFT library, allowing any user to conveniently train adapters and share them on the Hub, rather than training the entire model.
from datasets import load_dataset
from trl import DPOTrainer
from peft import LoraConfig
dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train")
trainer = DPOTrainer(
"Qwen/Qwen3-0.6B",
train_dataset=dataset,
peft_config=LoraConfig(),
)
trainer.train()You can also continue training your [~peft.PeftModel]. For that, first load a PeftModel outside [DPOTrainer] and pass it directly to the trainer without the peft_config argument being passed.
from datasets import load_dataset
from trl import DPOTrainer
from peft import AutoPeftModelForCausalLM
model = AutoPeftModelForCausalLM.from_pretrained("trl-lib/Qwen3-4B-LoRA", is_trainable=True)
dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train")
trainer = DPOTrainer(
model=model,
train_dataset=dataset,
)
trainer.train()[!TIP]
When training adapters, you typically use a higher learning rate (β1eβ5) than full fine-tuning since only new parameters are being learned.pythonDPOConfig(learning_rate=1e-5, ...)
Rapid Experimentation for DPO
RapidFire AI is an open-source experimentation engine that sits on top of TRL and lets you launch multiple DPO configurations at once, even on a single GPU. Instead of trying configurations sequentially, RapidFire lets you see all their learning curves earlier, stop underperforming runs, and clone promising ones with new settings in flight without restarting. For more information, see RapidFire AI Integration.
Train with Unsloth
Unsloth is an openβsource framework for fineβtuning and reinforcement learning that trains LLMs (like Llama, Mistral, Gemma, DeepSeek, and more) up to 2Γ faster with up to 70% less VRAM, while providing a streamlined, Hugging Faceβcompatible workflow for training, evaluation, and deployment. For more information, see Unsloth Integration.
Tool Calling with DPO
The [DPOTrainer] fully supports fine-tuning models with tool calling capabilities. In this case, each dataset example should include:
- The conversation messages (prompt, chosen and rejected), including any tool calls (
tool_calls) and tool responses (toolrole messages) - The list of available tools in the
toolscolumn, typically provided as JSON schemas
For details on the expected dataset structure, see the Dataset Format β Tool Calling section.
Training Vision Language Models
[DPOTrainer] fully supports training Vision-Language Models (VLMs). To train a VLM, provide a dataset with either an image column (single image per sample) or an images column (list of images per sample). For more information on the expected dataset structure, see the Dataset Format β Vision Dataset section.
An example of such a dataset is the RLAIF-V Dataset dataset.
from trl import DPOConfig, DPOTrainer
from datasets import load_dataset
trainer = DPOTrainer(
model="Qwen/Qwen2.5-VL-3B-Instruct",
args=DPOConfig(max_length=None),
train_dataset=load_dataset("HuggingFaceH4/rlaif-v_formatted", split="train"),
)
trainer.train()[!TIP]
For VLMs, truncating may remove image tokens, leading to errors during training. To avoid this, setmax_length=Nonein the [DPOConfig]. This allows the model to process the full sequence length without truncating image tokens.pythonDPOConfig(max_length=None, ...)Only use
max_lengthwhen you've verified that truncation won't remove image tokens for the entire dataset.
DPOTrainer
[[autodoc]] DPOTrainer
- train
- save_model
- push_to_hub
DPOConfig
[[autodoc]] DPOConfig
Source Gkd Trainer (`docs/source/gkd_trainer.md`)
Generalized Knowledge Distillation Trainer
Overview
Generalized Knowledge Distillation (GKD) was proposed in On-Policy Distillation of Language Models: Learning from Self-Generated Mistakes by Rishabh Agarwal, Nino Vieillard, Yongchao Zhou, Piotr Stanczyk, Sabela Ramos, Matthieu Geist, and Olivier Bachem.
The abstract from the paper is the following:
Knowledge distillation (KD) is widely used for compressing a teacher model to reduce its inference cost and memory footprint, by training a smaller student model. However, current KD methods for auto-regressive sequence models suffer from distribution mismatch between output sequences seen during training and those generated by the student during inference. To address this issue, we introduce Generalized Knowledge Distillation (GKD). Instead of solely relying on a fixed set of output sequences, GKD trains the student on its self-generated output sequences by leveraging feedback from the teacher on such sequences. Unlike supervised KD approaches, GKD also offers the flexibility to employ alternative loss functions between the student and teacher, which can be useful when the student lacks the expressivity to mimic the teacher's distribution. Furthermore, GKD facilitates the seamless integration of distillation with RL fine-tuning (RLHF). We demonstrate the efficacy of GKD for distilling auto-regressive language models on summarization, translation, and arithmetic reasoning tasks, and task-agnostic distillation for instruction-tuning.
The key aspects of GKD are:
- It addresses the train-inference distribution mismatch in auto-regressive sequence models by training the student model on its self-generated output sequences.
- GKD allows flexibility in choosing different divergence measures between student and teacher models via the generalized Jensen-Shannon Divergence (JSD), which can be useful when the student lacks the capacity to fully mimic the teacher.
This post-training method was contributed by Kashif Rasul and Lewis Tunstall.
Usage tips
The [experimental.gkd.GKDTrainer] is a wrapper around the [SFTTrainer] class that takes in a teacher model argument. It needs three parameters to be set via the [experimental.gkd.GKDConfig] namely:
lmbda: controls the student data fraction, i.e., the proportion of on-policy student-generated outputs. Whenlmbda=0.0, the loss reduces to supervised JSD where the student is trained with the token-level probabilities of the teacher. Whenlmbda=1.0, the loss reduces to on-policy JSD, where the student generates output sequences and token-specific feedback on these sequences from the teacher. For values in between [0, 1] it is random between the two based on thelmbdavalue for each batch.seq_kd: controls whether to perform Sequence-Level KD (can be viewed as supervised FT on teacher-generated out). Whenseq_kd=Trueandlmbda=0.0, the loss reduces to supervised JSD, where the teacher generates output sequences and the student receives token-specific feedback on these sequences from the teacher.beta: controls the interpolation in the generalized Jensen-Shannon Divergence. Whenbeta=0.0the loss approximates forward KL divergence, while forbeta=1.0the loss approximates reverse KL divergence. For values in between [0, 1] it interpolates between the two.
The authors find that on-policy data (high lmbda) performs better and the optimal beta varied depending on the task and evaluation method.
[!WARNING]
Make sure thatattn_implementation="kernels-community/flash-attn2"when training Gemma models. Otherwise you will encounter NaNs in the logits due to the soft capping technique adopted by this architecture.
The basic API is as follows:
from datasets import Dataset
from transformers import AutoModelForCausalLM, AutoTokenizer
from trl.experimental.gkd import GKDConfig, GKDTrainer
NUM_DUMMY_SAMPLES = 100
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2-0.5B-Instruct")
# The model to optimise
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2-0.5B-Instruct")
# The teacher model to calculate the KL divergence against
teacher_model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2-1.5B-Instruct")
train_dataset = Dataset.from_dict(
{
"messages": [
[
{"role": "user", "content": "Hi, how are you?"},
{"role": "assistant", "content": "I'm great thanks"},
]
]
* NUM_DUMMY_SAMPLES
}
)
eval_dataset = Dataset.from_dict(
{
"messages": [
[
{"role": "user", "content": "What colour is the sky?"},
{"role": "assistant", "content": "The sky is blue"},
]
]
* NUM_DUMMY_SAMPLES
}
)
training_args = GKDConfig(output_dir="gkd-model", per_device_train_batch_size=1)
trainer = GKDTrainer(
model=model,
teacher_model=teacher_model,
args=training_args,
processing_class=tokenizer,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
)
trainer.train()Expected dataset type
The dataset should be formatted as a list of "messages" where each message is a list of dictionaries with the following keys:
role: eithersystem,assistantorusercontent: the message content
GKDTrainer
[[autodoc]] experimental.gkd.GKDTrainer
- train
- save_model
- push_to_hub
GKDConfig
[[autodoc]] experimental.gkd.GKDConfig
Source Gmpo (`docs/source/gmpo.md`)
GMPO
In the paper Geometric-Mean Policy Optimization, the authors propose a GRPO variant that maximizes the geometric mean of the token-level importance ratios instead of the arithmetic mean. Because the geometric mean is far less sensitive to outlier ratios, the policy update is more stable and tolerates a much wider clipping range. Clipping is applied per token, in log space, and one-sided per the advantage sign (the standard PPO trust region) β crucially, before the geometric mean is taken.
To use GMPO, you can use the [experimental.gmpo.GMPOTrainer] class in trl.experimental.gmpo.
Usage
from trl.experimental.gmpo import GMPOConfig, GMPOTrainer
training_args = GMPOConfig(
epsilon=0.4, # log-space clip range -> ratios clipped to (exp(-0.4), exp(0.4)); paper, Sec. 4
beta=0.0,
)
trainer = GMPOTrainer(
model="Qwen/Qwen3-0.6B",
reward_funcs=...,
train_dataset=...,
args=training_args,
)
trainer.train()In GMPO, clipping is applied to the per-token log-importance ratios (i.e. in log space) before the geometric mean is taken, so epsilon and epsilon_high are expressed in log space: the effective ratio clipping range is (exp(-epsilon), exp(epsilon_high)). The paper recommends a markedly wider range than GRPO/DAPO, (exp(-0.4), exp(0.4)), to encourage exploration.
GMPOTrainer
[[autodoc]] experimental.gmpo.GMPOTrainer
- train
- save_model
- push_to_hub
GMPOConfig
[[autodoc]] experimental.gmpo.GMPOConfig
Source Gold Trainer (`docs/source/gold_trainer.md`)
General Online Logit Distillation (GOLD) Trainer
Overview
General Online Logit Distillation (GOLD) is an extension of Universal Logit Distillation (ULD) that supports student/teacher pairs with different tokenizers. It aligns the textual spans produced by both tokenizers and merges the associated logits so no completion tokens are dropped. This enables cross-tokenizer knowledge distillation, including mixed model families (for example, LLaMA students with Qwen teachers).
Key capabilities:
- Cross-tokenizer alignment β GOLD incrementally decodes the student and teacher tokens, groups passages with the same visible text, and merges probabilities inside each group. This guarantees loss terms are computed over the full completion even when token boundaries differ.
- Hybrid ULD loss β when
uld_use_hybrid_lossis enabled, GOLD compares exact vocabulary matches directly and falls back to the original sorted-probability ULD loss for unmatched tokens. This improves stability for students whose vocabularies only partially overlap with the teacher. - Seamless integration with GKD β GOLD inherits the on-policy vs. off-policy scheduling from the [
experimental.gkd.GKDTrainer], so you can combine sequence-level KD, generalized JSD, and cross-tokenizer distillation in a single training run.
[!NOTE]
GOLD is currently part of thetrl.experimentalnamespace. APIs may change without notice while the feature is iterated on.
Usage tips
The [experimental.gold.GOLDTrainer] subclasses [SFTTrainer] and accepts the same datasets as other TRL trainers (lists of ChatML style messages). Important configuration flags on [experimental.gold.GOLDConfig] include:
use_uld_lossβ toggles Universal Logit Distillation. Set this toTruefor cross-tokenizer setups.teacher_tokenizer_name_or_pathβ required whenuse_uld_loss=True; GOLD uses the teacher tokenizer to align tokens.uld_use_hybrid_loss,uld_hybrid_matched_weight,uld_hybrid_unmatched_weightβ enables and weights the hybrid matched/unmatched loss.beta,lmbda,seq_kdβ inherited from [experimental.gkd.GKDConfig], controlling the generalized JSD interpolation and on-policy sampling ratio.num_generations,generation_batch_sizeβ control buffered rollout generation across gradient accumulation windows.generation_batch_sizeis the number of unique prompts per worker per optimizer step.
A minimal end-to-end example:
from datasets import load_dataset
from trl.experimental.gold import GOLDConfig, GOLDTrainer
train_dataset = load_dataset(
"HuggingFaceTB/OpenR1-Math-220k-default-verified",
"all",
split="train[:1024]",
)
trainer = GOLDTrainer(
model="meta-llama/Llama-3.2-1B-Instruct",
teacher_model="Qwen/Qwen2.5-0.5B-Instruct",
args=GOLDConfig(output_dir="gold-model", use_uld_loss=True, teacher_tokenizer_name_or_path="Qwen/Qwen2.5-0.5B-Instruct"),
train_dataset=train_dataset,
)
trainer.train()For quick-start workflows you can rely on string identifiers as shown aboveβthe trainer will load the model and tokenizer for you. Explicitly instantiating AutoModelForCausalLM, AutoTokenizer, or populating GOLDConfig is recommended only for advanced use cases where you need fine-grained control over initialization.
A more explicit setup might look like this when you need to customise model loading, tokenizer settings, or training arguments:
from datasets import load_dataset
from trl.experimental.gold import GOLDConfig, GOLDTrainer
from transformers import AutoModelForCausalLM, AutoTokenizer
student_name = "meta-llama/Llama-3.2-1B-Instruct"
teacher_name = "Qwen/Qwen2.5-0.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(student_name)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForCausalLM.from_pretrained(student_name)
teacher_model = AutoModelForCausalLM.from_pretrained(teacher_name)
train_dataset = load_dataset(
"HuggingFaceTB/Countdown-Task-GOLD",
"verified_Qwen2.5-0.5B-Instruct",
split="train",
)
training_args = GOLDConfig(
output_dir="gold-model",
per_device_train_batch_size=1,
teacher_model_name_or_path=teacher_name,
teacher_tokenizer_name_or_path=teacher_name,
use_uld_loss=True,
uld_use_hybrid_loss=True,
)
trainer = GOLDTrainer(
model=model,
teacher_model=teacher_model,
args=training_args,
processing_class=tokenizer,
train_dataset=train_dataset,
)
trainer.train()[!NOTE]
GOLD buffers one full optimizer-window generation batch (per_device_train_batch_size * gradient_accumulation_steps) and reuses it across accumulation steps. If the final batch is undersized, GOLD warns and drops that last batch (Dropping last batch due to unexpected batch size). Setdataloader_drop_last=Trueto avoid this warning.
Expected dataset type
GOLD requires a conversational language modeling dataset, e.g.:
{"messages": [{"role": "user", "content": "What color is the sky?"},
{"role": "assistant", "content": "It is blue."}]}GOLDTrainer keeps the raw messages so the ChatML collator can construct prompts and completions with the correct boundaries.
How Token Merging Works
When student and teacher use different tokenizers, the same text may be split differently:
- Student:
"Hugging Face"β 1 token - Teacher:
"Hugging"," Face"β 2 tokens
GOLD aligns these sequences and merges the teacher's multi-token probabilities into a single distribution that can be compared with the student's single-token distribution.
Probability Merging
For a teacher sequence of tokens [tokenβ, tokenβ, ..., tokenβ] that maps to a single student token, GOLD computes:
P_merged(y) = P(y | context) Γ P(tokenβ | tokenβ, context) Γ ... Γ P(tokenβ | ..., context)where:
P(y | context)is the marginal probability distribution over all vocabulary tokens at the first positionP(tokenα΅’ | ..., context)are scalar conditional probabilities of the actual tokens that were generated
Key insight: Only the conditional probabilities of the actual continuation tokens are extracted as scalars. The full marginal distribution at the first position is then scaled by multiplying these scalar probabilities.
This ensures:
- Correct joint probability for the actual generated sequence (by the chain rule)
- Reasonable approximation for counterfactual tokens (scaled by the same continuation likelihood)
- Unnormalized distributions that preserve the correct relative probabilities for ULD loss computation
Example
Given:
P(xβ): ["HF": 0.6, "is": 0.3, "cool": 0.1]
P(xβ | "HF"): ["HF": 0.05, "is": 0.9, "cool": 0.05]If tokens 0 and 1 are merged, and the actual sequence was ["HF", "is"]:
P_merged("HF") = 0.6 Γ 0.9 = 0.54 β (correct joint probability)
P_merged("is") = 0.3 Γ 0.9 = 0.27
P_merged("cool") = 0.1 Γ 0.9 = 0.09The merged distribution is unnormalized (sums to 0.81), but this is intentional and correct for ULD loss computation, which uses sorting and L1 distance.
Example script
Use examples/gold_chatbot_arena/gold_chatbot_arena.py to launch GOLD training from the command line. The script supports full training and LoRA via the standard ModelConfig flags.
python examples/gold_chatbot_arena/gold_chatbot_arena.py \
--model_name_or_path meta-llama/Llama-3.2-1B-Instruct \
--teacher_model_name_or_path Qwen/Qwen2-1.5B-Instruct \
--dataset_name trl-lib/chatbot_arena_completions \
--learning_rate 2e-5 \
--per_device_train_batch_size 4 \
--gradient_accumulation_steps 8 \
--output_dir gold-model \
--num_train_epochs 1 \
--push_to_hubTraining Vision Language Models
[experimental.gold.GOLDTrainer] supports VLM-to-VLM distillation. Both student and teacher must be vision-language models. To train a VLM, provide a dataset with either an image column (single image per sample) or an images column (list of images per sample). For more information on the expected dataset structure, see the Dataset Format β Vision datasets section.
When the student and teacher share the same architecture and tokenizer (e.g. Qwen3-VL-8B to Qwen3-VL-2B), the standard generalized JSD loss applies directly. When they have different model_type (e.g. Qwen3-VL to LFM2.5-VL), set use_uld_loss=True to enable cross-tokenizer alignment via Universal Logit Distillation. Images are processed separately through each model's processor.
from datasets import load_dataset
import torch
from transformers import AutoModelForImageTextToText, AutoProcessor
from trl.experimental.gold import GOLDConfig, GOLDTrainer
student_name = "Qwen/Qwen3-VL-2B-Instruct"
teacher_name = "Qwen/Qwen3-VL-8B-Instruct"
processor = AutoProcessor.from_pretrained(student_name, padding_side="left")
student_model = AutoModelForImageTextToText.from_pretrained(student_name, dtype=torch.bfloat16)
teacher_model = AutoModelForImageTextToText.from_pretrained(teacher_name, dtype=torch.bfloat16)
train_dataset = load_dataset("trl-lib/llava-instruct-mix", split="train")
trainer = GOLDTrainer(
model=student_model,
teacher_model=teacher_model,
args=GOLDConfig(
output_dir="gold-vlm-model",
max_length=None,
teacher_model_name_or_path=teacher_name,
use_uld_loss=False,
),
train_dataset=train_dataset,
processing_class=processor,
)
trainer.train()For cross-family distillation, set use_uld_loss=True and teacher_tokenizer_name_or_path to the teacher model name.
Use examples/gold_qwen3_vl/gold_qwen3_vl.py to launch GOLD VLM training from the command line:
# Same-family distillation (JSD loss, vLLM enabled)
accelerate launch examples/gold_qwen3_vl/gold_qwen3_vl.py \
--student_model_name Qwen/Qwen3-VL-2B-Instruct \
--teacher_model_name Qwen/Qwen3-VL-8B-Instruct
# Cross-family distillation (ULD loss, local generation)
accelerate launch examples/gold_qwen3_vl/gold_qwen3_vl.py \
--student_model_name LiquidAI/LFM2.5-VL-1.6B \
--teacher_model_name Qwen/Qwen3-VL-8B-Instruct \
--use_uld_loss \
--no-use_vllm[!TIP]
For VLMs,truncation_mode='keep_end'is not supported because image tokens reside in the prompt portion of the sequence and may be silently dropped. Usetruncation_mode='keep_start'(the default) or setmax_length=Nonein the [experimental.gold.GOLDConfig]. This allows the model to process the full sequence length without truncating image tokens.pythonGOLDConfig(max_length=None, ...)Only use
max_lengthwhen you've verified that truncation won't remove image tokens for the entire dataset.
[!NOTE]
Cross-architecture VLM distillation requiresuse_uld_loss=True. The trainer will raise an error if you attempt cross-architecture distillation without ULD loss.
GOLDTrainer
[[autodoc]] experimental.gold.GOLDTrainer
- train
- generate_on_policy_outputs
- save_model
- push_to_hub
GOLDConfig
[[autodoc]] experimental.gold.GOLDConfig
Source Grpo Trainer (`docs/source/grpo_trainer.md`)
GRPO Trainer
Overview
TRL supports the GRPO Trainer for training language models, as described in the paper DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models by Zhihong Shao, Peiyi Wang, Qihao Zhu, Runxin Xu, Junxiao Song, Mingchuan Zhang, Y. K. Li, Y. Wu, Daya Guo.
The abstract from the paper is the following:
Mathematical reasoning poses a significant challenge for language models due to its complex and structured nature. In this paper, we introduce DeepSeekMath 7B, which continues pre-training DeepSeek-Coder-Base-v1.5 7B with 120B math-related tokens sourced from Common Crawl, together with natural language and code data. DeepSeekMath 7B has achieved an impressive score of 51.7% on the competition-level MATH benchmark without relying on external toolkits and voting techniques, approaching the performance level of Gemini-Ultra and GPT-4. Self-consistency over 64 samples from DeepSeekMath 7B achieves 60.9% on MATH. The mathematical reasoning capability of DeepSeekMath is attributed to two key factors: First, we harness the significant potential of publicly available web data through a meticulously engineered data selection pipeline. Second, we introduce Group Relative Policy Optimization (GRPO), a variant of Proximal Policy Optimization (PPO), that enhances mathematical reasoning abilities while concurrently optimizing the memory usage of PPO.
This post-training method was contributed by Quentin GallouΓ©dec.
Quick start
This example demonstrates how to train a model using the GRPO method. We train a Qwen2.5 0.5B Instruct model with the prompts from the DeepMath-103K dataset. You can view the data in the dataset here:
Below is the script to train the model.
# train_grpo.py
from datasets import load_dataset
from trl import GRPOTrainer
from trl.rewards import accuracy_reward
dataset = load_dataset("trl-lib/DeepMath-103K", split="train")
trainer = GRPOTrainer(
model="Qwen/Qwen2.5-0.5B-Instruct",
reward_funcs=accuracy_reward,
train_dataset=dataset,
)
trainer.train()Execute the script using the following command:
accelerate launch train_grpo.pyDistributed across 8 GPUs, the training takes approximately 1 day.
Note: The reward curves above were generated with
Qwen/Qwen2-0.5B-Instruct. Results withQwen/Qwen2.5-0.5B-Instructwill be qualitatively similar.
Looking deeper into the GRPO method
GRPO is an online learning algorithm, meaning it improves iteratively by using the data generated by the trained model itself during training. The intuition behind GRPO objective is to maximize the advantage of the generated completions, while ensuring that the model remains close to the reference policy. To understand how GRPO works, it can be broken down into four main steps: Generating completions, computing the advantage, estimating the KL divergence, and computing the loss.
Generating completions
At each training step, we sample a batch of prompts and generate a set of \( G \) completions for each prompt (denoted as \( o_i \)).
Computing the advantage
For each of the \( G \) sequences, we compute the reward using a reward model or reward function. To align with the comparative nature of reward modelsβtypically trained on datasets of comparisons between outputs for the same questionβthe advantage is calculated to reflect these relative comparisons. It is normalized as follows:
$$\hat{A}_{i,t} = \frac{r_i - \text{mean}(\mathbf{r})}{\text{std}(\mathbf{r})}$$
This approach gives the method its name: Group Relative Policy Optimization (GRPO).
[!TIP]
It was shown in the paper Understanding R1-Zero-Like Training: A Critical Perspective that scaling by \( \text{std}(\mathbf{r}) \) may cause a question-level difficulty bias. You can disable this scaling by settingscale_rewards=Falsein [GRPOConfig].
Note that turning off std-based scaling also removes variance normalization, so update magnitudes depend directly on the raw reward scale and batch composition.
[!TIP]
As shown in Part I: Tricks or Traps? A Deep Dive into RL for LLM Reasoning (Lite PPO), calculating the mean at the local (group) level and the standard deviation at the global (batch) level enables more robust reward shaping. You can use this scaling strategy by settingscale_rewards="batch"in [GRPOConfig].
Estimating the KL divergence
KL divergence is estimated using the approximator introduced by Schulman et al. (2020). The approximator is defined as follows:
$$\mathbb{D}{\text{KL}}\left[\pi\theta |\pi_{\text{ref}}\right] = \frac{\pi_{\text{ref}}(o_{i,t} \mid q, o_{i,<t})}{\pi_\theta(o_{i,t} \mid q, o_{i,<t})} - \log \frac{\pi_{\text{ref}}(o_{i,t} \mid q, o_{i,<t})}{\pi_\theta(o_{i,t} \mid q, o_{i,<t})} - 1,
$$
Computing the loss
The objective is to maximize the advantage while ensuring that the model remains close to the reference policy. Consequently, the loss is defined as follows:
$$
\mathcal{L}{\text{GRPO}}(\theta) = -\frac{1}{\sum{i=1}^G |o_i|} \sum_{i=1}^G \sum_{t=1}^{|o_i|} \left[ \frac{\pi_\theta(o_{i,t} \mid q, o_{i,< t})}{\left[\pi_\theta(o_{i,t} \mid q, o_{i,< t})\right]{\text{no grad}}} \hat{A}{i,t} - \beta \mathbb{D}{\text{KL}}\left[\pi\theta | \pi_{\text{ref}}\right] \right],
$$
where the first term represents the scaled advantage and the second term penalizes deviations from the reference policy through KL divergence.
[!TIP]
Note that compared to the original formulation in DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models, we don't scale by \( \frac{1}{|o_i|} \) because it was shown in the paper Understanding R1-Zero-Like Training: A Critical Perspective that this introduces a response-level length bias. More details in loss types.
[!TIP]
Note that compared to the original formulation in DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models, we use \( \beta = 0.0 \) by default, meaning that the KL divergence term is not used. This choice is motivated by several recent studies (e.g., Open-Reasoner-Zero: An Open Source Approach to Scaling Up Reinforcement Learning on the Base Model) which have shown that the KL divergence term is not essential for training with GRPO. As a result, it has become common practice to exclude it (e.g. Understanding R1-Zero-Like Training: A Critical Perspective, DAPO: An Open-Source LLM Reinforcement Learning System at Scale). If you wish to include the KL divergence term, you can setbetain [GRPOConfig] to a non-zero value.
In the original paper, this formulation is generalized to account for multiple updates after each generation (denoted \( \mu \), can be set with num_iterations in [GRPOConfig]) by leveraging the clipped surrogate objective:
$$
\mathcal{L}{\text{GRPO}}(\theta) = - \frac{1}{\sum{i=1}^G |o_i|} \sum_{i=1}^G \sum_{t=1}^{|o_i|} \left[ \min \left( \frac{\pi_\theta(o_{i,t} \mid q, o_{i,< t})}{\pi_{\theta_{\text{old}}}(o_{i,t} \mid q, o_{i,< t})} \hat{A}{i,t}, , \text{clip}\left( \frac{\pi\theta(o_{i,t} \mid q, o_{i,< t})}{\pi_{\theta_{\text{old}}}(o_{i,t} \mid q, o_{i,< t})}, 1 - \epsilon, 1 + \epsilon \right) \hat{A}{i,t} \right) - \beta \mathbb{D}{\text{KL}}\left[\pi_\theta | \pi_{\text{ref}}\right] \right],
$$
where \(\text{clip}(\cdot, 1 - \epsilon, 1 + \epsilon) \) ensures that updates do not deviate excessively from the reference policy by bounding the policy ratio between \( 1 - \epsilon \) and \( 1 + \epsilon \).
When \( \mu = 1 \) (default in TRL), the clipped surrogate objective simplifies to the original objective.
Loss Types
Several formulations of the objective have been proposed in the literature. Initially, the objective of GRPO was defined as follows:
$$
\mathcal{L}{\text{GRPO}}(\theta) = - \frac{1}{G} \sum{i=1}^G \frac{1}{|o_i|} \sum_{t=1}^{|o_i|} l_{i,t},
$$
where
$$
l_{i,t} = \frac{\pi_\theta(o_{i,t} \mid q, o_{i,< t})}{\left[\pi_\theta(o_{i,t} \mid q, o_{i,< t})\right]{\text{no grad}}} \hat{A}{i,t} - \beta \mathbb{D}{\text{KL}}\left[\pi\theta | \pi_{\text{ref}}\right].
$$
The DAPO paper highlights the limitations of the GRPO algorithmβs sample-level loss in long-CoT scenarios, where longer responses are under-penalized, leading to poorer quality outputs. The proposed solution is a token-level normalization, which better handles longer sequences by assigning more balanced rewards to individual tokens, regardless of response length:
$$
\mathcal{L}{\text{DAPO}}(\theta) = - \frac{1}{\sum{i=1}^G |o_i|} \sum_{i=1}^G \sum_{t=1}^{|o_i|} l_{i,t},
$$
To use this formulation, set loss_type="dapo" in [GRPOConfig].
Furthermore, it was demonstrated in the paper Understanding R1-Zero-Like Training: A Critical Perspective that the initial GRPO formulation introduces a response length bias. They show that while the DAPO formulation reduces this bias, it does not eliminate it completely. To fully remove this bias, they propose dividing by a constant instead of the sequence length, resulting in the following formulation:
$$
\mathcal{L}{\text{Dr. GRPO}}(\theta) = - \frac{1}{LG} \sum{i=1}^G \sum_{t=1}^{|o_i|} l_{i,t},
$$
This constant is recommended to be the maximum completion length. To use this formulation, set loss_type="dr_grpo" in the [GRPOConfig].
Alternatively, in the SAPO paper, the Qwen team proposes replacing the "hard" clipping mechanism of GRPO with a smooth, temperature-controlled soft gating mechanism. While GRPO zeroes out gradients when the policy deviates too far from the reference, SAPO uses a soft trust region that smoothly decays the gradient weight. This allows the model to retain useful learning signals from "near-on-policy" tokens while suppressing noise from extreme deviations.
The loss function is defined as:
$$
\mathcal{L}{\text{SAPO}}(\theta) = - \frac{1}{G} \sum{i=1}^G \frac{1}{|o_i|} \sum_{t=1}^{|o_i|} f_{i,t} \left( \frac{\pi_\theta(o_{i,t} | q, o_{i,<t})}{\pi_{\theta_{old}}(o_{i,t} | q, o_{i,<t})} \right) \hat{A}_{i,t}
$$
The soft-gating function \( f_{i,t} \) is defined using the sigmoid function \( \sigma \) as:
$$
f_{i,t}(x) = \sigma \left( \tau_{i,t} (x - 1) \right) \cdot \frac{4}{\tau_{i,t}}
$$
The temperature \( \tau_{i,t} \) is chosen based on the sign of the advantage \( \hat{A}_{i,t} \):
$$
\tau_{i,t} = \begin{cases}
\tau_{\text{pos}}, & \text{if } \hat{A}{i,t} > 0 \
\tau{\text{neg}}, & \text{otherwise}
\end{cases}
$$
They recommend using asymmetric temperatures, \( \tau_{\text{neg}} > \tau_{\text{pos}} \) (defaults are \( \tau_{\text{pos}}=1.0, \tau_{\text{neg}}=1.05 \) ). This ensures that the model is penalized more strictly for "bad" actions to prevent instability, while being more permissive with "good" actions.
To use this formulation, set loss_type="sapo" in the [GRPOConfig].
Logged metrics
While training and evaluating, we record the following metrics:
num_tokens: The total number of tokens processed so far, including both prompts and completions. When using tools, only non-tool tokens are counted.step_time: The average time (in seconds) taken per training step (including generation).completions/mean_length: The average length of generated completions. When using tools, only non-tool tokens are counted.completions/min_length: The minimum length of generated completions. When using tools, only non-tool tokens are counted.completions/max_length: The maximum length of generated completions. When using tools, only non-tool tokens are counted.completions/mean_terminated_length: The average length of generated completions that terminate with EOS. When using tools, only non-tool tokens are counted.completions/min_terminated_length: The minimum length of generated completions that terminate with EOS. When using tools, only non-tool tokens are counted.completions/max_terminated_length: The maximum length of generated completions that terminate with EOS. When using tools, only non-tool tokens are counted.completions/clipped_ratio: The ratio of truncated (clipped) completions.tools/call_frequency: The average number of tool calls per completion in the generation batch. Logged only whentoolsare provided.tools/failure_frequency: The fraction of tool calls that failed (the tool was not found, raised an exception, or the call type is unsupported). It is0.0when no tool was called. Logged only whentoolsare provided.rewards/{reward_func_name}/mean: The average reward from a specific reward function. When an environment owns the reward viaget_reward,{reward_func_name}is the environment's class name.rewards/{reward_func_name}/std: The standard deviation of the reward from a specific reward function.reward: The overall average reward after summing rewards across functions (weighted byreward_weights).reward_std: The standard deviation of summed rewards across functions (weighted byreward_weights), computed over the full batch.frac_reward_zero_std: The fraction of samples in the generation batch with a reward std of zero, implying there is little diversity for that prompt (all answers are correct or incorrect).sampling/sampling_logp_difference/mean: The average absolute difference between the log probabilities returned by the sampler (vLLM) and the ones recomputed by the training model, over completion tokens. A growing value indicates a widening train-inference mismatch. Logged only whenuse_vllm=Trueandvllm_importance_sampling_correction=True.sampling/sampling_logp_difference/max: The largest such absolute difference. Logged only whenuse_vllm=Trueandvllm_importance_sampling_correction=True.sampling/importance_sampling_ratio/min: The smallest importance sampling ratio used to correct the train-inference mismatch, after the constraint selected byvllm_importance_sampling_modehas been applied (clipped to[C_min, C_max]for*_truncatemodes, set to zero for*_maskmodes). Computed over completion tokens, or over sequences for thesequence_*modes. Logged only whenuse_vllm=Trueandvllm_importance_sampling_correction=True.sampling/importance_sampling_ratio/mean: The average constrained importance sampling ratio. Logged only whenuse_vllm=Trueandvllm_importance_sampling_correction=True.sampling/importance_sampling_ratio/max: The largest constrained importance sampling ratio. Logged only whenuse_vllm=Trueandvllm_importance_sampling_correction=True.policy_loss: The policy gradient loss value (before any entropy bonus). Logged whenentropy_coefis nonzero oruse_adaptive_entropy=True.entropy: Average entropy of token predictions across generated completions. (Ifmask_truncated_completions=True, masked sequences tokens are excluded.)entropy_coef: The current entropy regularization coefficient. Logged whenentropy_coefis nonzero oruse_adaptive_entropy=True. Updated once per optimizer step whenuse_adaptive_entropy=True.aux_loss: The load-balancing auxiliary loss of a Mixture-of-Experts model, before it is scaled byrouter_aux_loss_coefand added to the loss. Logged only when the model is a MoE model androuter_aux_loss_coefis nonzero.kl: The average KL divergence between the model and the reference model, calculated over generated completions. Logged only ifbetais nonzero.clip_ratio/region_mean: The ratio of token (or sequence, ifimportance_sampling_level="sequence") probabilities where the GRPO objective is clipped to stay within the trust region: \( \text{clip}\left( r_{i,t}(\theta), 1 - \epsilon_\mathrm{low}, 1 + \epsilon_\mathrm{high} \right),, \quad r_{i,t}(\theta) = \frac{\pi_\theta(o_{i,t} \mid q, o_{i,< t})}{\pi_{\theta_{\text{old}}}(o_{i,t} \mid q, o_{i,< t})} \). A higher value means more tokens are clipped, which constrains how much the policy $\pi_\theta$ can change.clip_ratio/low_mean: The average ratio of token (or sequence, ifimportance_sampling_level="sequence") probabilities that were clipped on the lower bound of the trust region: \(r_{i,t}(\theta) < 1 - \epsilon_\mathrm{low}\).clip_ratio/low_min: The smallest per-completion fraction of tokens (or the sequence itself, ifimportance_sampling_level="sequence") clipped on the lower bound of the trust region: \(r_{i,t}(\theta) < 1 - \epsilon_\mathrm{low}\).clip_ratio/high_mean: The average ratio of token (or sequence, ifimportance_sampling_level="sequence") probabilities that were clipped on the upper bound of the trust region: \(r_{i,t}(\theta) > 1 + \epsilon_\mathrm{high}\).clip_ratio/high_max: The largest per-completion fraction of tokens (or the sequence itself, ifimportance_sampling_level="sequence") clipped on the upper bound of the trust region: \(r_{i,t}(\theta) > 1 + \epsilon_\mathrm{high}\).cispo_clip_ratio: The ratio of token (or sequence, ifimportance_sampling_level="sequence") importance sampling weights that were clipped atepsilon_highwhile having a positive advantage: \(r_{i,t}(\theta) > \epsilon_\mathrm{high}\). Logged only whenloss_type="cispo".vespo/phi_seq_mean: The average value of the VESPO Gamma weighting \(\varphi(w)\) applied to the sequence-level importance weights. Values below1.0mean sequences are being down-weighted. Logged only whenloss_type="vespo".
Customization
Speed up training with vLLM-powered generation
Generation is often the main bottleneck when training with online methods. To accelerate generation, you can use vLLM, a high-throughput, low-latency inference engine for LLMs. To enable it, first install the package with
pip install trl[vllm]We support two ways of using vLLM during training: server mode and colocate mode.
[!TIP]
By default, sequence-level Masked Importance Sampling (vllm_importance_sampling_mode="sequence_mask") is activated for vLLM generation to address the generation-training mismatch that occurs when using different frameworks. This can be turned off by settingvllm_importance_sampling_correction=False. For more information, see Masked Importance Sampling and Sequence-level Importance Sampling
Option 1: Colocate mode
In this mode, vLLM runs inside the trainer process and shares GPU memory with the training model. This avoids launching a separate server and can improve GPU utilization, but may lead to memory contention on the training GPUs. This is the default mode.
from trl import GRPOConfig
training_args = GRPOConfig(
...,
use_vllm=True, # vllm_mode="colocate" by default
)Option 2: Server mode
In this mode, vLLM runs in a separate process (and using separate GPUs) and communicates with the trainer via HTTP. This is ideal if you have dedicated GPUs for inference.
Start the vLLM server:
bashVLLM_SERVER_DEV_MODE=1 vllm serve <model_name> \ --weight-transfer-config '{"backend": "nccl"}' \ --logprobs-mode processed_logprobs \ --max-logprobs -1Enable server mode in your training script:
pythonfrom trl import GRPOConfig training_args = GRPOConfig( ..., use_vllm=True, vllm_mode="server", )
[!WARNING]
Make sure that the server is using different GPUs than the trainer, otherwise you may run into NCCL errors. You can specify the GPUs to use with theCUDA_VISIBLE_DEVICESenvironment variable.
[!TIP]
Depending on the model size and the overall GPU memory requirements for training, you may need to adjust thevllm_gpu_memory_utilizationparameter in [GRPOConfig] to avoid underutilization or out-of-memory errors.We provide a HF Space to help estimate the recommended GPU memory utilization based on your model configuration and experiment settings. Simply use it as follows to get
vllm_gpu_memory_utilizationrecommendation:If the recommended value does not work in your environment, we suggest adding a small buffer (e.g., +0.05 or +0.1) to the recommended value to ensure stability.
If you still find you are getting out-of-memory errors set
vllm_enable_sleep_modeto True and the vllm parameters and cache will be offloaded during the optimization step. For more information, see Reducing Memory Usage with vLLM Sleep Mode.
[!TIP]
By default, GRPO usesMASTER_ADDR=localhostandMASTER_PORT=12345for vLLM, but you can override these values by setting the environment variables accordingly.
For more information, see Speeding up training with vLLM.
Dealing with the Training-Inference Mismatch
While vLLM greatly accelerates inference, it also decouples the inference engine from the training engine. In theory these engines are mathematically identical, in practice however they can produce different outputs due to precision effects and hardware specific optimizations. This divergence reflects the different optimization objectives of the two systems. Inference engines aim to maximize sampling throughput, typically measured in tokens per second, while maintaining acceptable sampling fidelity. Training frameworks instead focus on numerical stability and precision for gradient computation, often using higher precision formats like FP32 for master weights and optimizer states. These differing priorities and constraints introduce an inevitable, albeit subtle, mismatch between training and inference.
This mismatch leads to a biased gradient update which has been observed to destabilize training ([1][2][3][4][5]). For simplicity, consider the REINFORCE policy gradient:
$$
\nabla_\theta \mathcal{J}(x,\theta)
= \mathbb{E}{y \sim \pi^\text{train}(\cdot \mid x,\theta)}
\left[ \nabla\theta \log \pi^\text{train}(y \mid x,\theta) \cdot R(x,y) \right]
$$
Here \( x \) denotes prompts sampled from some data distribution, and \( \pi^\text{train} \) is the policy implemented by the training engine. With vLLM in the loop we obtain a separate inference policy \( \pi^\text{inference} \), so the effective policy gradient becomes
$$
\nabla_\theta \mathcal{J}{\text{biased}}(x,\theta)
= \mathbb{E}{y \sim \pi^\text{inference}(\cdot \mid x,\theta)}
\left[ \nabla_\theta \log \pi^\text{train}(y \mid x,\theta) \cdot R(x,y) \right].
$$
This turns an otherwise on policy RL problem into an off policy one.
The standard way to correct for this distribution shift is importance sampling (IS). We provide two IS variants: Truncated Importance Sampling (TIS) and Masked Importance Sampling (MIS). Both variants can be applied either at the token level or at the sequence level. Let \( \rho \) denote the importance weight, for example \( \rho_t \) per token or \( \rho_{\text{seq}} \) per sequence. Under TIS, ratios outside of the range [vllm_importance_sampling_clip_min, vllm_importance_sampling_clip_max] are clipped,
$$
\rho \leftarrow \text{clip}(\rho, C_{\min}, C_{\max}).
$$
The original TIS paper proposed a single upper-bound clipping mechanism, i.e., \( \min(\rho, C_{\max}) \). The implementation in TRL generalized this by also introducing a lower bound, yielding the two-sided formulation shown above, inspired by the IcePop method.
Note that in IcePop, the bounds are labelled as \( \alpha \) and \( \beta \) while in TRL we use \( C_{\min} \) and \( C_{\max} \).
Under MIS, ratios outside of this range are set to zero, so those samples do not contribute to the gradient. In other words, outlier samples are downweighted under TIS and discarded under MIS. The configuration flag vllm_importance_sampling_mode chooses both the IS variant (masking or truncation) and the granularity (token level or sequence level).
Importance sampling is the principled algorithmic response to the trainingβinference mismatch. However, there are also more direct approaches that attempt to reduce the mismatch between the two engines themselves. Most of these are engineering solutions. For example, MiniMax M1 uses an FP32 language model head in the inference engine. Thinking Machines has explored deterministic inference kernels, although this comes with a significant efficiency cost. vLLM has shown bitwise consistent policies by building on the batch invariant deterministic kernels from Thinking Machines, but as of November 2025 there remains a substantial throughput penalty relative to standard vLLM inference.
Speed up training with transformers continuous batching
As an alternative to vLLM, you can use transformers' built-in continuous batching engine for faster generation. Continuous batching removes finished sequences from the batch immediately rather than waiting for the slowest one to finish. For tasks with variable completion lengths (e.g., math reasoning), this yields faster generation and lower VRAM usage than the default generate() at large batch sizes (Nβ₯32).
[!TIP]
Continuous batching is a drop-in upgrade with no server setup or weight synchronization. It runs in-process and is well-suited for single-GPU training or memory-constrained environments. For maximum generation throughput at scale, use vLLM instead.
from trl import GRPOConfig
training_args = GRPOConfig(
...,
use_transformers_continuous_batching=True,
transformers_continuous_batching_config={
"use_cuda_graph": False,
"max_memory_percent": 0.4, # lower values leave more VRAM for the training backward pass
},
)[!TIP]
TRL defaultsmax_memory_percentto0.5(instead of transformers'0.9) to leave enough VRAM for the training backward pass. Tune it down to0.3β0.4for large generation batches (Nβ₯32) or if you see out-of-memory errors.
For a full training example, see examples/grpo_continuous_batching/grpo_continuous_batching.py.
GRPO at scale: train a 70B+ Model on multiple nodes
When training large models like Qwen2.5-72B, you need several key optimizations to make the training efficient and scalable across multiple GPUs and nodes. These include:
- DeepSpeed ZeRO Stage 3: ZeRO leverages data parallelism to distribute model states (weights, gradients, optimizer states) across multiple GPUs and CPUs, reducing memory and compute requirements on each device. Since large models cannot fit on a single GPU, using ZeRO Stage 3 is required for training such models. For more details, see DeepSpeed Integration.
- Accelerate: Accelerate is a library that simplifies distributed training across multiple GPUs and nodes. It provides a simple API to launch distributed training and handles the complexities of distributed training, such as data parallelism, gradient accumulation, and distributed data loading. For more details, see Distributing Training.
- vLLM: See the previous section on how to use vLLM to speed up generation.
Below is an example SLURM script to train a 70B model with GRPO on multiple nodes. This script trains a model on 4 nodes and uses the 5th node for vLLM-powered generation.
#!/bin/bash
#SBATCH --nodes=5
#SBATCH --gres=gpu:8
# Get the list of allocated nodes
NODELIST=($(scontrol show hostnames $SLURM_JOB_NODELIST))
# Assign the first 4 nodes for training and the 5th node for vLLM
TRAIN_NODES="${NODELIST[@]:0:4}" # Nodes 0, 1, 2, 3 for training
VLLM_NODE="${NODELIST[4]}" # Node 4 for vLLM
# Run training on the first 4 nodes (Group 1)
srun --nodes=4 --ntasks=4 --nodelist="${NODELIST[@]:0:4}" accelerate launch \
--config_file examples/accelerate_configs/deepspeed_zero3.yaml \
--num_processes 32 \
--num_machines 4 \
--main_process_ip ${NODELIST[0]} \
--machine_rank $SLURM_PROCID \
--rdzv_backend c10d \
train_grpo.py \
--server_ip $VLLM_NODE &
# Run vLLM server on the 5th node (Group 2)
srun --nodes=1 --ntasks=1 --nodelist="${NODELIST[4]}" env VLLM_SERVER_DEV_MODE=1 vllm serve Qwen/Qwen2.5-72B --tensor-parallel-size 8 \
--weight-transfer-config '{"backend": "nccl"}' \
--logprobs-mode processed_logprobs \
--max-logprobs -1 &
waitimport argparse
from datasets import load_dataset
from trl import GRPOTrainer, GRPOConfig
from trl.rewards import accuracy_reward
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--vllm_server_host", type=str, default="", help="The server IP")
args = parser.parse_args()
dataset = load_dataset("trl-lib/DeepMath-103K", split="train")
training_args = GRPOConfig(
per_device_train_batch_size=4,
use_vllm=True,
vllm_mode="server",
vllm_server_host=args.vllm_server_host.replace("ip-", "").replace("-", "."), # from ip-X-X-X-X to X.X.X.X
)
trainer = GRPOTrainer(
model="Qwen/Qwen2.5-72B",
args=training_args,
reward_funcs=accuracy_reward,
train_dataset=dataset
)
trainer.train()
if __name__=="__main__":
main()Using a custom reward function
The [GRPOTrainer] supports using custom reward functions instead of dense reward models. To ensure compatibility, your reward function must satisfy the following requirements:
Reward functions can be either synchronous Python callables or asynchronous async def coroutines. When you provide multiple asynchronous reward functions, they are awaited concurrently (run in parallel via asyncio.gather) so their latency overlaps.
Input arguments:
The function must accept the following as keyword arguments:
prompts(contains the prompts),completions(contains the generated completions),completion_ids(contains the tokenized completions),trainer_state([~transformers.TrainerState]): The current state of the trainer. This can be used to implement dynamic reward functions, such as curriculum learning, where the reward is adjusted based on the training progress.log_extra: a callablelog_extra(column: str, values: list)to add extra columns to the completions table. See Example 6.log_metric: a callablelog_metric(name: str, value: float)to log scalar metrics as plots alongsidekl,entropy, etc. See Example 6.environments: a list of environment instances, one per completion. Only present whenenvironment_factoryis provided. Use this to read state accumulated during the episode (e.g.,env.counter). For stateful environments, prefer letting the environment own the reward via aget_rewardmethod instead β see Rewards.- All column names (but
prompt) that the dataset may have. For example, if the dataset contains a column namedground_truth, the function will be called withground_truthas a keyword argument.
The easiest way to comply with this requirement is to use
**kwargsin the function signature.Depending on the dataset format, the input will vary:
- For standard format,
promptsandcompletionswill be lists of strings. - For conversational format,
promptsandcompletionswill be lists of message dictionaries.
- For standard format,
Return value: The function must return a list of floats. Each float represents the reward corresponding to a single completion.
Example 1: Reward longer completions
Below is an example of a reward function for a standard format that rewards longer completions:
def reward_func(completion_ids, **kwargs):
"""Reward function that assigns higher scores to longer completions (in terms of token count)."""
return [float(len(ids)) for ids in completion_ids]You can test it as follows:
>>> prompts = ["The sky is", "The sun is"] # not used in the reward function, but the trainer will pass it
>>> completions = [" blue.", " in the sky."] # not used in the reward function, but the trainer will pass it
>>> completion_ids = [[6303, 13], [304, 279, 12884, 13]]
>>> reward_func(prompts=prompts, completions=completions, completion_ids=completion_ids)
[2.0, 4.0]Example 1.1: Reward longer completions (based on the number of characters)
Same as the previous example, but this time the reward function is based on the number of characters instead of tokens.
def reward_func(completions, **kwargs):
"""Reward function that assigns higher scores to longer completions (in terms of character count)."""
return [float(len(completion)) for completion in completions]You can test it as follows:
>>> prompts = ["The sky is", "The sun is"]
>>> completions = [" blue.", " in the sky."]
>>> completion_ids = [[6303, 13], [304, 279, 12884, 13]] # not used in the reward function, but the trainer will pass it
>>> reward_func(prompts=prompts, completions=completions, completion_ids=completion_ids)
[6.0, 12.0]Example 2: Reward completions with a specific format
Below is an example of a reward function that checks if the completion has a specific format. This example is inspired by the format reward function used in the paper DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning.
It is designed for a conversational format, where prompts and completions consist of structured messages.
import re
def format_reward_func(completions, **kwargs):
"""Reward function that checks if the completion has a specific format."""
pattern = r"^<think>.*?</think><answer>.*?</answer>$"
completion_contents = [completion[0]["content"] for completion in completions]
matches = [re.match(pattern, content) for content in completion_contents]
return [1.0 if match else 0.0 for match in matches]You can test this function as follows:
>>> prompts = [
... [{"role": "assistant", "content": "What is the result of (1 + 2) * 4?"}],
... [{"role": "assistant", "content": "What is the result of (3 + 1) * 2?"}],
... ]
>>> completions = [
... [{"role": "assistant", "content": "<think>The sum of 1 and 2 is 3, which we multiply by 4 to get 12.</think><answer>(1 + 2) * 4 = 12</answer>"}],
... [{"role": "assistant", "content": "The sum of 3 and 1 is 4, which we multiply by 2 to get 8. So (3 + 1) * 2 = 8."}],
... ]
>>> format_reward_func(prompts=prompts, completions=completions)
[1.0, 0.0]Example 3: Reward completions based on a reference
Below is an example of a reward function that checks if the completion is correct. This example is inspired by the accuracy reward function used in the paper DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning.
This example is designed for standard format, where the dataset contains a column named ground_truth.
import re
def reward_func(completions, ground_truth, **kwargs):
# Regular expression to capture content inside \boxed{}
matches = [re.search(r"\\boxed\{(.*?)\}", completion) for completion in completions]
contents = [match.group(1) if match else "" for match in matches]
# Reward 1 if the content is the same as the ground truth, 0 otherwise
return [1.0 if c == gt else 0.0 for c, gt in zip(contents, ground_truth)]You can test this function as follows:
>>> prompts = ["Problem: Solve the equation $2x + 3 = 7$. Solution:", "Problem: Solve the equation $3x - 5 = 10$."]
>>> completions = [r" The solution is \boxed{2}.", r" The solution is \boxed{6}."]
>>> ground_truth = ["2", "5"]
>>> reward_func(prompts=prompts, completions=completions, ground_truth=ground_truth)
[1.0, 0.0]Example 4: Multi-task reward functions
Below is an example of using multiple reward functions in the [GRPOTrainer]. In this example, we define two task-specific reward functions: math_reward_func and coding_reward_func. The math_reward_func rewards math problems based on their correctness, while the coding_reward_func rewards coding problems based on whether the solution works.
/* Detailed source-code truncated for AI context efficiency. */In this example, the math_reward_func and coding_reward_func are designed to work with a mixed dataset that contains both math and coding problems. The task column in the dataset is used to determine which reward function to apply to each problem. If there is no relevant reward function for a sample in the dataset, the reward function will return None, and the [GRPOTrainer] will continue with the valid functions and tasks. This allows the [GRPOTrainer] to handle multiple reward functions with different applicability.
Note that the [GRPOTrainer] will ignore the None rewards returned by the reward functions and only consider the rewards returned by the relevant functions. This ensures that the model is trained on the relevant tasks and ignores the tasks for which there is no relevant reward function.
Example 5: Asynchronous reward functions
Custom reward functions can also be defined as async def coroutines. This is useful if your reward depends on slow I/O (for example, calling a remote service). When you pass multiple async reward functions, [GRPOTrainer] executes them concurrently so their latency overlaps.
Below is a minimal example of an async reward function that simulates an I/O-bound operation:
import asyncio
async def async_reward_func(prompts, completions, **kwargs):
# Simulate an I/O-bound call (e.g., HTTP request, database lookup)
await asyncio.sleep(0.01)
# Simple toy reward: 1.0 if the completion is non-empty, else 0.0
return [1.0 if completion else 0.0 for completion in completions]Example 6: Logging extra columns and metrics
Below is an example of a reward function that logs extra columns to the completions table and scalar metrics as plots.
import re
def reward_func(completions, ground_truth, log_extra=None, log_metric=None, **kwargs):
extracted = [re.search(r"\\boxed\{(.*?)\}", c) for c in completions]
extracted = [m.group(1) if m else None for m in extracted]
rewards = [1.0 if e == gt else 0.0 for e, gt in zip(extracted, ground_truth)]
if log_extra:
log_extra("golden_answer", list(ground_truth))
log_extra("extracted_answer", [e or "[none]" for e in extracted])
if log_metric:
log_metric("accuracy", sum(rewards) / len(rewards))
return rewardsPassing the reward function to the trainer
To use your custom reward function, pass it to the [GRPOTrainer] as follows:
from trl import GRPOTrainer
trainer = GRPOTrainer(
reward_funcs=reward_func,
...,
)You can pass several reward functions as a list; this list may include both synchronous and asynchronous functions:
from trl import GRPOTrainer
trainer = GRPOTrainer(
reward_funcs=[reward_func, async_reward_func1, async_reward_func2],
...,
)and the reward will be computed as the sum of the rewards from each function, or the weighted sum if reward_weights is provided in the config.
Note that [GRPOTrainer] supports multiple reward functions of different types. See the parameters documentation for more details.
Entropy regularization
To encourage exploration and prevent the policy from collapsing to near-deterministic outputs, you can add an entropy bonus to the training objective. The entropy regularization augments the GRPO loss as follows:
$$
\mathcal{L}(\theta) = \mathcal{L}{\text{GRPO}}(\theta) - \alpha \cdot \mathcal{H}(\pi\theta),
$$
where \(\mathcal{H}(\pi_\theta)\) is the mean per-token entropy of the policy and \(\alpha\) is the entropy coefficient. The bonus is always the mean per-token entropy regardless of loss_type; it is not rescaled to match a loss type's policy normalization (e.g. Dr. GRPO's batch_size * max_completion_length denominator), so entropy_coef has the same meaning for every loss type.
Static entropy β a fixed coefficient throughout training:
from trl import GRPOConfig, GRPOTrainer
training_args = GRPOConfig(entropy_coef=0.05, ...)Adaptive entropy β the coefficient is updated each optimizer step based on a target entropy, as introduced in Skywork-OR1. When the current entropy falls at or below entropy_target, the coefficient is incremented by entropy_coef_delta; otherwise it is decremented. The coefficient is only applied (i.e. non-zero) while entropy is at or below the target:
training_args = GRPOConfig(
entropy_coef=0.01, # initial coefficient
use_adaptive_entropy=True,
entropy_target=5.0, # target mean per-token entropy (nats); tune for your model
entropy_coef_delta=0.005, # step size per optimizer step
entropy_coef_min=0.0,
entropy_coef_max=1.0,
...
)Typical language models have per-token entropies of 2β10 nats, so the default entropy_target=0.2 almost never triggers regularization β the bonus only engages once entropy is at or below the target, i.e. near-complete collapse. Set it to a value meaningful for your model, e.g. close to the entropy you observe early in training (logged as the entropy metric). When using top_entropy_quantile < 1.0, entropy_target applies to the high-entropy token subset β that subset's entropy will be higher than the logged full-token entropy, so calibrate accordingly.
When use_adaptive_entropy=True, the current entropy coefficient entropy_coef is saved alongside each checkpoint and restored on resume, so training is fully resumable.
Rapid Experimentation for GRPO
RapidFire AI is an open-source experimentation engine that sits on top of TRL and lets you launch multiple GRPO configurations at once, even on a single GPU. Instead of trying configurations sequentially, RapidFire lets you see all their learning curves earlier, stop underperforming runs, and clone promising ones with new settings in flight without restarting. For more information, see RapidFire AI Integration.
Agent Training
GRPO supports agent training: the model calls tools during generation and learns from the outcome.
- A tool is a plain Python function (sync or async) exposed to the model. Use
toolsfor stateless calls (a calculator, a web search). - An environment is the more general form: a stateful object built fresh per rollout, whose public methods are exposed as tools, plus a
resetlifecycle hook and an optionalget_rewardthat lets it own the reward. Useenvironment_factorywhen you need per-rollout state, a reset hook, or environment-owned reward.
They compose β you can pass standalone tools alongside an environment_factory.
Tools
The tools argument expects a list of Python functions (sync or async) that define the tools available to the agent:
from trl import GRPOTrainer
trainer = GRPOTrainer(
tools=[tool1, tool2],
...,
)Each tool must be a standard Python function with type-hinted arguments and return types, along with a Google-style docstring describing its purpose, arguments, and return value.
For more details, see the Passing tools guide.
[!TIP]
The GRPO tool call loop requires the chat template to be prefix-preserving (appending a tool message must not change how earlier messages are rendered). For known model families (e.g. Qwen3, DeepSeek-V3), TRL automatically swaps in a patched training template when tools are enabled. See Chat Templates for the full list.
Use max_tool_calling_iterations in the [GRPOConfig] to cap the number of tool-calling turns. By default there is no limit, and generation stops when the model produces a response turn with no tool calls.
Example:
from trl import GRPOTrainer
def multiply(a: int, b: int) -> int:
"""
Multiplies two integers.
Args:
a: The first integer.
b: The second integer.
Returns:
The product of the two integers.
"""
return a * b
async def async_add(a: int, b: int) -> int:
"""
Asynchronously adds two integers.
Args:
a: The first integer.
b: The second integer.
Returns:
The sum of the two integers.
"""
return a + b
trainer = GRPOTrainer(
tools=[multiply, async_add],
...,
)Environments
You can also provide tools through environment_factory. In this mode, [GRPOTrainer] creates one environment instance per rollout and exposes the environment's public methods as tools.
[!IMPORTANT]
environment_factoryrequirestransformers>=5.2.0.
When you use an environment_factory, the environment owns the data: there is no external train_dataset. On each rollout, reset() produces the task β self-sampling from a corpus the environment holds, or generating a state procedurally β and returns the prompt. max_steps sets the training length. The following is a minimal example of an environment that self-samples a target and exposes an increment method (exposed as a tool).
import random
from trl import GRPOConfig, GRPOTrainer
class IncrementEnv:
# Reserved methods
def reset(self, **kwargs) -> str | None: # required; called at the start of each rollout
self.counter = 0
self.target = random.randint(1, 6) # self-sample the task
return f"Increment the counter by {self.target}." # returned string becomes the prompt
def get_reward(self) -> float: # optional: the environment scores itself from its own state
return float(self.counter == self.target)
# Public methods (exposed as tools)
def increment(self, step: int) -> int:
"""
Increment the internal counter.
Args:
step: Value to add to the counter.
Returns:
The updated counter value.
"""
self.counter += step
return self.counter
trainer = GRPOTrainer(
model="Qwen/Qwen3-0.6B",
args=GRPOConfig(max_steps=1000, chat_template_kwargs={"enable_thinking": False}),
environment_factory=IncrementEnv,
)
trainer.train()An environment class has two reserved methods: reset and get_reward. Unlike any other public methods, these are not exposed to the model as tools.
reset(required) is called when the rollout starts. It can return eitherNoneor a string. In GRPO, when it returns a string, that string is the user prompt.get_reward(optional, sync or async) takes no argument and returns afloat: the environment scores the episode it just ran from its own internal state (did the game end in a win? was the word guessed?). It is called once per completed rollout and acts as a reward source.
[!NOTE]
Because the environment self-samples on eachreset(), theGmembers of a GRPO group may not share the same initial state, making the group baseline slightly noisier. For identical states within a group, havereset()derive the state deterministically from a shared key.
Providing an external dataset (optional)
Instead of letting the environment self-sample, you can provide an external train_dataset. For example when your tasks already live in a dataset, or to route between multiple environments. Per rollout, the trainer samples one row, uses its "prompt" column as the prompt, and passes the row's other columns to reset() as keyword arguments (so the environment reads the task from the dataset).
from datasets import Dataset
from trl import GRPOTrainer
# Each row carries the prompt and the task data (here, the counter target to reach).
dataset = Dataset.from_dict(
{
"prompt": [[{"role": "user", "content": f"Increment the counter to {i}."}] for i in range(1, 7)],
"target": list(range(1, 7)),
}
)
class IncrementEnv:
def reset(self, target, **kwargs) -> None: # the row's `target` column arrives as a keyword argument
self.counter = 0
self.target = target
def get_reward(self) -> float:
return float(self.counter == self.target)
def increment(self, step: int) -> int: # exposed as a tool (see the full docstring above)
self.counter += step
return self.counter
trainer = GRPOTrainer(
model="Qwen/Qwen3-0.6B",
train_dataset=dataset, # the dataset owns the task; the "target" column reaches reset(target=...)
environment_factory=IncrementEnv,
)
trainer.train()Rewards
Environment-owned and trainer-owned rewards are not exclusive. Pass reward_funcs for rewards that are natural to compute from the completion (e.g. a format check on the text) and let the environment return its state-dependent reward through get_reward. All sources are summed; reward_weights applies to the reward_funcs only (the environment owns its own scale).
def format_reward(completions, **kwargs): # trainer-owned: scores the completion text
...
trainer = GRPOTrainer(
model="Qwen/Qwen3-0.6B",
args=GRPOConfig(max_steps=1000),
reward_funcs=format_reward, # trainer-owned: scores the completion
environment_factory=IncrementEnv, # env-owned: scores its internal state via `get_reward`
)A reward_func can also read environment state directly: when environment_factory is set, the trainer passes an environments kwarg (one instance per completion). This is occasionally useful, but for state-dependent rewards prefer get_reward.
This works identically for AsyncGRPOTrainer.
Multiple environments
To train on a mix of tasks in a single run (e.g. a coding task and a game), pass a dictionary mapping environment names to factories instead of a single callable. Each rollout selects its environment through an environment field in the dataset, and only that environment's tools are exposed. This avoids leaking irrelevant tools (the game's move tool to a coding example, and vice versa). Each environment still owns its data and returns the prompt from reset(), so the dataset only needs the environment column to route between them.
import random
from datasets import Dataset
from trl import GRPOConfig, GRPOTrainer
class CodingEnv:
def reset(self, **kwargs) -> str:
n = random.randint(1, 20)
return f"Compute the {n}th Fibonacci number."
def run_code(self, code: str) -> str: # exposed as a tool only for "coding" examples
"""Run the given Python code and return its stdout.
Args:
code: The Python source to execute.
Returns:
The captured standard output.
"""
...
class GameEnv:
def reset(self, **kwargs) -> str:
return "Reach the goal tile."
def move(self, action: str) -> str: # exposed as a tool only for "game" examples
"""Apply an action to the game and return the new observation.
Args:
action: One of "up", "down", "left", "right".
Returns:
The observation after the move.
"""
...
# The dataset only routes: each row picks the environment for that rollout
dataset = Dataset.from_dict({"environment": ["coding", "game"] * 500})
trainer = GRPOTrainer(
model="Qwen/Qwen3-0.6B",
args=GRPOConfig(chat_template_kwargs={"enable_thinking": False}),
train_dataset=dataset,
reward_funcs=reward_func,
environment_factory={"coding": CodingEnv, "game": GameEnv},
)
trainer.train()The reward function still receives every example's environment through reward_kwargs["environments"], so it can branch on the environment type (e.g. isinstance(env, CodingEnv)). Environment instances are reused across steps (reset between episodes), so an expensive __init__ is paid once rather than every step. The environment field is consumed by the trainer and is not forwarded to reset.
Multimodal Tool Responses
Tools can return images alongside text by returning a list of content blocks. This is useful for VLM agent training where the tool provides visual feedback (e.g., screenshots, plots, camera captures).
from PIL import Image
def take_screenshot() -> list:
"""
Takes a screenshot of the current screen.
Returns:
The screenshot image with a description.
"""
img = Image.open("screenshot.png")
return [{"type": "image", "image": img}, {"type": "text", "text": "Here is the screenshot."}]The returned images are automatically injected into the conversation and passed to the VLM for subsequent generation turns.
Supported Models
Tested with:
- Beam β e.g.,
reflection/Beam-1.0-501B-A23B - Gemma4 β e.g.,
google/gemma-4-E2B-it - GLM-4-MoE (4.5, 4.6 or 4.7) β e.g.,
zai-org/GLM-4.7 - GPT-OSS β e.g.,
openai/gpt-oss-20b - Llama 3.1 β e.g.,
meta-llama/Llama-3.1-8B-Instruct - Llama 3.2 β e.g.,
meta-llama/Llama-3.2-3B-Instruct - Qwen2.5 β e.g.,
Qwen/Qwen2.5-0.5B-Instruct - Qwen3 β e.g.,
Qwen/Qwen3-0.6B - Qwen3-VL β e.g.,
Qwen/Qwen3-VL-2B-Instruct - Qwen3.5 β e.g.,
Qwen/Qwen3.5-2B - Qwen3.6 β e.g.,
Qwen/Qwen3.6-35B-A3B
[!TIP]
Compatibility with all LLMs is not guaranteed. If you believe a model should be supported, feel free to open an issue on GitHub β or better yet, submit a pull request with the required changes.
Environment Integrations
All environments plug into the same environment_factory slot, so they are interchangeable at the TRL level β pick the one whose ecosystem fits your task:
| Integration | What it is | Use it when |
|---|---|---|
| OpenEnv | The open environment standard (Gymnasium-style API, served over WebSocket or containerised execution), backed by Hugging Face and the community. | You're using a ready-made OpenEnv environment from the Hub, or defining your own against the open standard (e.g. Wordle, Sudoku, Catch). |
| OpenReward | An integration with ORS-speaking environments (the openreward.ai catalog or your own ORS server); tasks and rewards are served over HTTP. | You want to train against an ORS environment: the catalog (e.g. Eigent/SETA), one you self-host on your own infra, or a local server you're developing. |
| Harbor | An integration with Harbor task suites: each task is an instruction, a real sandbox image (docker, e2b, ...), and an in-sandbox verifier. |
You want to train against a Harbor task suite: a tree of tasks, each a self-contained sandbox plus verifier (e.g. a data-analysis agent that explores files in a sandbox and writes an answer a grader checks). |
Training Vision Language Models
GRPO supports training Vision-Language Models (VLMs) on multimodal datasets containing both text and images.
Supported Models
Tested with:
- Gemma3 β e.g.,
google/gemma-3-4b-it - LLaVA-NeXT β e.g.,
llava-hf/llava-v1.6-mistral-7b-hf - Qwen2-VL β e.g.,
Qwen/Qwen2-VL-2B-Instruct - Qwen2.5-VL β e.g.,
Qwen/Qwen2.5-VL-3B-Instruct - SmolVLM2 β e.g.,
HuggingFaceTB/SmolVLM2-2.2B-Instruct
[!TIP]
Compatibility with all VLMs is not guaranteed. If you believe a model should be supported, feel free to open an issue on GitHub β or better yet, submit a pull request with the required changes.
Quick Start
Use grpo_vlm.py to fine-tune a VLM. Example command for training on lmms-lab/multimodal-open-r1-8k-verified:
accelerate launch \
--config_file=examples/accelerate_configs/deepspeed_zero3.yaml \
examples/grpo_visual_math/grpo_visual_math.py \
--model_name_or_path Qwen/Qwen2.5-VL-3B-Instruct \
--output_dir grpo-Qwen2.5-VL-3B-Instruct \
--learning_rate 1e-5 \
--dtype bfloat16 \
--max_completion_length 1024 \
--use_vllm \
--vllm_mode colocate \
--use_peft \
--lora_target_modules "q_proj", "v_proj" \
--log_completionsConfiguration Tips
- Use LoRA on vision-language projection layers
- Enable 4-bit quantization to reduce memory usage
- VLMs are memory-intensive β start with smaller batch sizes
- Most models are compatible with vLLM (
serverandcolocatemodes)
Dataset Format
Each training sample should include:
prompt: Text formatted via the processor's chat templateimage/images: PIL Image or list of PIL Images
The trainer automatically handles image-to-tensor conversion via the modelβs image processor.
GRPOTrainer
[[autodoc]] GRPOTrainer
- train
- save_model
- push_to_hub
GRPOConfig
[[autodoc]] GRPOConfig
Source Harbor (`docs/source/harbor.md`)
Harbor Integration for Training LLMs with Environments
Harbor is a framework for running agentic tasks in sandboxes. It decouples a task (instruction + sandbox image + verifier), a harness/agent (the tool surface + loop), and a sandbox (docker, e2b, daytona, gke, β¦) so they can be mixed freely. This makes it a natural fit for RL: the same task suite can be trained with different tool surfaces, on whichever sandbox backend you prefer.
This guide covers how to integrate Harbor with TRL. For Harbor itself, see the Harbor docs.
[!NOTE]
The integration lives attrl.experimental.harborand is gated behind thetrl[harbor]extra (lazy-imported β non-users pay nothing).
When to use Harbor environments
[GRPOTrainer] supports environment-based training via the environment_factory slot β see OpenEnv for the general contract. Use Harbor when you want to train against a Harbor task suite: a directory tree of tasks, each a self-contained sandbox + verifier (for example, a data-analysis agent suite where the model explores files in a sandbox and writes an answer that a grader checks).
Installation
pip install trl[harbor][!IMPORTANT]
Harbor drives generation through vLLM and usesenvironment_factory, which requiresvllmandtransformers>=5.2.0.bashpip install vllm
This installs the harbor framework (Python >= 3.12). The integration imports harbor lazily and runs it in-process, so users who don't touch trl.experimental.harbor aren't affected.
A sandbox backend must also be installed and reachable at train time. Harbor keeps cloud backends behind its own extras, so install the one you intend to use and provide its credentials:
pip install "harbor[e2b]" # E2B cloud sandbox -> environment_type="e2b", needs E2B_API_KEY
# docker backend (environment_type="docker", Harbor's default) just needs a reachable Docker daemonQuick start
HarborSpec wires a single Harbor task suite into the three TRL trainer slots β train_dataset, environment_factory, reward_funcs β by exposing properties that map 1:1 to those kwarg names:
from trl import GRPOConfig, GRPOTrainer
from trl.experimental.harbor import HarborSpec
spec = HarborSpec("AdithyaSK/data_agent_rl_environment_train", agent="bash", num_tasks=64)
trainer = GRPOTrainer(
model="Qwen/Qwen3-4B",
args=GRPOConfig(
num_generations=8,
max_steps=50,
max_tool_calling_iterations=25,
log_completions=True,
),
train_dataset=spec.train_dataset,
environment_factory=spec.environment_factory,
reward_funcs=spec.reward_funcs,
)
trainer.train()Under the hood HarborSpec does three things, lazily on first access:
spec.train_dataset: resolves the task suite to local task directories (downloading the HF dataset if needed) and builds adatasets.Datasetwithprompt(empty β the env's instruction is appended atreset),task_dir,task_index, plus per-tasktask.tomlmetadata columns.spec.environment_factory: returns a zero-arg callable producing a fresh per-rollout [~trl.experimental.harbor.HarborEnv]. Onreset(task_dir)it starts the task's Harbor sandbox and returns its instruction; tool methods exec into the sandbox;env.rewardruns the verifier once after the rollout.spec.reward_funcs: an outcome reward that reads the Harbor verifier's scalar per rollout.
The dataset
dataset is either a Hugging Face dataset repo id holding a Harbor task tree, or a local path containing a tasks/ subtree. Each task is a directory:
tasks/<task_id>/
βββ instruction.md # the task prompt (returned by reset)
βββ task.toml # config + metadata (gold answer, difficulty, ...)
βββ environment/ # Dockerfile (+ any pre-agent data hooks)
βββ tests/ # test.sh / grader β writes the rewardSelect a subset with num_tasks or indices (mutually exclusive):
spec = HarborSpec("AdithyaSK/data_agent_rl_environment_train", num_tasks=10) # first 10
spec = HarborSpec("AdithyaSK/data_agent_rl_environment_train", indices=[0, 5, 13]) # specificAgents: external vs installed
Harbor supports two ways an agent drives a task, and the distinction determines what can be trained with RL:
- External agents run outside the sandbox and drive the loop themselves, issuing commands into the container through Harbor's environment interface ("typically by executing bash commands via the
execmethod"). The agent decides each action and interprets each result; the sandbox only executes. - Installed agents are installed into the container image and run there as a headless subprocess (extending
BaseInstalledAgent). Harbor launches the agent inside the sandbox and parses its trajectory file afterward (populate_context_post_run); the agent runs autonomously with its own inference.
TRL's integration is the external-agent pattern, and only that pattern is supported for now. RL training requires the trainer to drive the rollout turn by turn: the policy model being trained generates each turn, and TRL captures its tokens and log-probs and applies the environment mask β exactly what environment_factory provides over a black-box rollout_func. An installed agent is opaque to this: it runs inside the container with its own model and only emits a trajectory after the fact, so there are no policy tokens or log-probs for the trainer to optimize, and the model under training is never invoked. A [~trl.experimental.harbor.HarborEnv] is therefore an external agent β its tool methods exec into the sandbox, but the loop, and the model under training, stay in TRL.
Selecting the base agent (harness)
The base agent is the harness β which tool methods the env exposes and how it submits. Select it with agent=:
HarborSpec(dataset, agent="bash") # built-in single-bash-tool harness
HarborSpec(dataset, agent="my_pkg.harnesses:JupyterEnv") # import path to your HarborEnv subclass
HarborSpec(dataset, agent="path/to/harness.py:JupyterEnv") # file path to your HarborEnv subclass
HarborSpec(dataset, agent=MyHarborEnv) # a HarborEnv subclass directlyThe built-in "bash" harness ([~trl.experimental.harbor.HarborBashEnv]) exposes one bash tool and submits by writing /workdir/answer.txt. Two richer harnesses ship as examples β each in its own folder with a README listing its tools β under examples/grpo_harbor/harnesses/:
jupyter/(JupyterEnv) β a stateful Python kernel (variables persist across cells) + a shell tool.terminal_notes/(TerminalNotesEnv) β 6 shell tools (incl. background processes) + a 4-tool persistent note toolkit.
HarborSpec(dataset, agent="examples/grpo_harbor/harnesses/jupyter/env.py:JupyterEnv")
HarborSpec(dataset, agent="examples/grpo_harbor/harnesses/terminal_notes/env.py:TerminalNotesEnv")To write your own harness, subclass [~trl.experimental.harbor.HarborEnv] and add tool methods β every public method becomes a tool (TRL discovers them with inspect.getmembers), so give each a typed signature and a docstring (used to build the tool schema). Keep helpers underscore-prefixed. Use self._exec(cmd) to run shell commands in the sandbox, and set PROMPT_SUFFIX to append harness guidance to the task instruction:
from trl.experimental.harbor import HarborEnv
class GrepEnv(HarborEnv):
PROMPT_SUFFIX = "\n\nUse `grep` and `read_file`. Submit by writing /workdir/answer.txt."
def grep(self, pattern: str, path: str) -> str:
"""Search for `pattern` under `path`.
Args:
pattern: The regex to search for.
path: The file or directory to search.
"""
return self._exec(f"grep -rn {pattern!r} {path!r}")The sandbox backend
environment_type is passed straight through to Harbor (not validated by TRL):
HarborSpec(dataset, environment_type="e2b") # cloud sandbox (offloads provisioning), needs E2B_API_KEY
HarborSpec(dataset, environment_type="docker") # default; needs a local Docker daemone2b is recommended for cluster training: only environment.exec crosses into the cloud sandbox, so the GPUs stay dedicated to the policy and you can run many rollouts concurrently.
Reward functions
spec.reward_funcs defaults to an outcome reward β per rollout it reads the Harbor verifier's scalar (env.reward), computed once after the rollout by running the task's tests/ verifier in the sandbox. For a custom reward, write a regular TRL reward function:
def my_reward(environments, **kwargs) -> list[float]:
return [env.reward for env in environments]API
[[autodoc]] trl.experimental.harbor.HarborSpec
[[autodoc]] trl.experimental.harbor.HarborEnv
[[autodoc]] trl.experimental.harbor.HarborBashEnv
Limitations
- The integration is in
trl.experimentalβ APIs may change. SetTRL_EXPERIMENTAL_SILENCE=1to silence the warning in CI logs. - Harbor's async sandbox client is bound to one event loop, so each env drives start/exec/verify synchronously on its own loop; sandbox provisioning is therefore sequential across the generation batch (cloud backends like
e2bmitigate the per-sandbox cost). - A single
HarborSpeccovers one task suite + one harness; multi-suite training is not supported yet.
Reference
Source Installation (`docs/source/installation.md`)
Installation
You can install TRL either from PyPI or from source:
PyPI
Install the library with pip or uv:
uv is a fast Rust-based Python package and project manager. Refer to Installation for installation instructions.
uv pip install trlpip install trlSource
You can also install the latest version from source. First clone the repo and then run the installation with pip:
git clone https://github.com/huggingface/trl.git
cd trl/
pip install -e .If you want the development install you can replace the pip install with the following:
pip install -e ".[dev]"Source Iw Opd Trainer (`docs/source/iw_opd_trainer.md`)
IW-OPD
In the paper On the Position Bias of On-Policy Distillation, the authors introduce Importance-Weighted On-Policy Distillation (IW-OPD). IW-OPD addresses position bias in on-policy distillation by reweighting sampled-token updates according to accumulated teacher-student prefix discrepancy. Early tokens keep larger weights, while later tokens after high drift are downweighted.
To use IW-OPD, you can use the [experimental.iw_opd.IWOPDTrainer] class in trl.experimental.iw_opd.
[!NOTE]
IW-OPD is currently part of thetrl.experimentalnamespace. APIs may change without notice while the feature is iterated on.
Usage
from trl.experimental.iw_opd import IWOPDConfig, IWOPDTrainer
training_args = IWOPDConfig(
distillation_objective="iw_opd",
iw_opd_gamma=0.5,
)
trainer = IWOPDTrainer(
model="Qwen/Qwen3-0.6B",
teacher_model="...",
train_dataset=...,
args=training_args,
)
trainer.train()IW-OPD is an on-policy objective: distillation_objective="iw_opd" requires lmbda=1.0 (the default). iw_opd_gamma is the importance-weight amplification from Algorithm 1 of the paper.
IWOPDTrainer
[[autodoc]] experimental.iw_opd.IWOPDTrainer
- train
- save_model
- push_to_hub
IWOPDConfig
[[autodoc]] experimental.iw_opd.IWOPDConfig
Source Jobs Training (`docs/source/jobs_training.md`)
Training with Jobs
Hugging Face Jobs runs your training on Hugging Face GPUs. You pick the hardware for each run and pay only for the seconds it runs. With --push_to_hub, the trained model is uploaded to the Hub at the end.
In this guide, you'll learn how to:
- Launch a TRL training script with one command
- Follow a run and see its loss curves
- Run your own script, with its hardware settings in the file
- Keep checkpoints and resume an interrupted run
- Train on several GPUs
For how Jobs works in general, including hardware and pricing, see the Jobs documentation.
Requirements
- A Hugging Face account with a positive credit balance. Jobs is pay-as-you-go: you only pay for the seconds you use.
- Logged in to the Hugging Face Hub (
hf auth login)
A first run
This command fine-tunes Qwen2-0.5B-Instruct on the Capybara chat dataset with the TRL SFT script, then pushes the model to your account:
hf jobs uv run \
--flavor a10g-small \
--timeout 30m \
--secrets HF_TOKEN \
-- \
https://raw.githubusercontent.com/huggingface/trl/refs/heads/main/trl/scripts/sft.py \
--model_name_or_path Qwen/Qwen2-0.5B-Instruct \
--dataset_name trl-lib/Capybara \
--max_steps 100 \
--output_dir Qwen2-0.5B-SFT \
--push_to_hubfrom huggingface_hub import run_uv_job
run_uv_job(
"https://raw.githubusercontent.com/huggingface/trl/refs/heads/main/trl/scripts/sft.py",
flavor="a10g-small",
timeout="30m",
secrets={"HF_TOKEN": "hf_..."},
script_args=[
"--model_name_or_path", "Qwen/Qwen2-0.5B-Instruct",
"--dataset_name", "trl-lib/Capybara",
"--max_steps", "100",
"--output_dir", "Qwen2-0.5B-SFT",
"--push_to_hub",
],
)It takes a few minutes and costs well under $1 on a10g-small. The options before -- are for Jobs: the hardware, a time limit and the token used to push the model. --secrets HF_TOKEN sends the token you logged in with. In Python, pass the token itself. The arguments after the script URL go to the script. hf jobs uv run runs a Python script with uv. sft.py lists its dependencies in a header at the top of the file, so Jobs installs TRL before the script starts. The Job stops when the script exits, and billing stops with it.
--max_steps 100 keeps this first run short. Remove it for the full run: three epochs, the script's default, take a few hours on a10g-small, so raise --timeout to match, for example --timeout 3h. Jobs stops a run when it reaches its timeout, which is 30 minutes by default. A larger GPU such as a100-large finishes sooner. See Hardware for the flavors and their prices.
Follow the run
hf jobs uv run streams the logs and holds your terminal until the run ends. Ctrl+C stops only the log stream: the Job keeps running. Add --detach to get the Job ID back straight away, then:
hf jobs logs -f <job_id> # stream the logs
hf jobs ps # list your running Jobs
hf jobs inspect <job_id> # status, and the error message if it failed
hf jobs cancel <job_id> # stop itSee Manage Jobs for more.
For loss curves, TRL logs to Trackio. Add --env TRACKIO_SPACE_ID=<your-username>/trackio before -- to name a Space for the dashboard, and --report_to trackio after the script URL. Trackio creates the Space, and a bucket for the metrics, if they do not exist. Both are public by default. Other trackers that Transformers supports work too. For Weights & Biases, add --with wandb --secrets WANDB_API_KEY before -- and --report_to wandb after the script URL. --secrets WANDB_API_KEY reads the value from WANDB_API_KEY in your local environment. See environment variables and secrets for other ways to pass them.
Run your own script
Write your training code in a Python file (for example, train.py). List its dependencies at the top of the file, in a script header, the same way the TRL scripts do. Jobs installs them before the script starts:
# /// script
# dependencies = [
# "trl",
# "peft",
# ]
# ///
from datasets import load_dataset
from peft import LoraConfig
from trl import SFTConfig, SFTTrainer
dataset = load_dataset("trl-lib/Capybara", split="train")
trainer = SFTTrainer(
model="Qwen/Qwen2.5-0.5B",
train_dataset=dataset,
peft_config=LoraConfig(),
args=SFTConfig(output_dir="Qwen2.5-0.5B-SFT", max_steps=100),
)
trainer.train()
trainer.push_to_hub()Then launch it with the hf jobs CLI or the Python API:
hf jobs uv run \
--flavor a10g-small \
--timeout 30m \
--secrets HF_TOKEN \
train.pyfrom huggingface_hub import run_uv_job
run_uv_job(
"train.py",
flavor="a10g-small",
timeout="30m",
secrets={"HF_TOKEN": "hf_..."},
)For a script without a header, list the dependencies at launch instead with --with, once per package: --with trl --with peft (dependencies=["trl", "peft"] in Python). The script can also be a URL, such as a GitHub raw link, a Gist or a file in a public Hub repo.
The launch settings can also live in the script. A [tool.hf-jobs] table in the header sets the hardware, timeout and secrets:
#