trl Documentation

Train transformer language models with reinforcement learning.

19,571 stars Python Python 42 files Β· ~128,137 tokens
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy trl on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name trl --restart always huggingface/trl:latest

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, KTOTrainer and 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:

bash
pip install trl

From source

If you want to use the latest features before an official release, you can install TRL from source:

bash
pip install git+https://github.com/huggingface/trl.git

Repository

If you want to use the examples you can clone the repository with the following command:

bash
git clone https://github.com/huggingface/trl.git

Quick 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:

python
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.

python
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 the reasoning_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:

python
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:

python
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:

python
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:

bash
trl sft --model_name_or_path Qwen/Qwen2.5-0.5B \
    --dataset_name trl-lib/Capybara \
    --output_dir Qwen2.5-0.5B-SFT

DPO:

bash
trl dpo --model_name_or_path Qwen/Qwen2.5-0.5B-Instruct \
    --dataset_name argilla/Capybara-Preferences \
    --output_dir Qwen2.5-0.5B-DPO

KTO:

bash
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-KTO

Read 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:

bash
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:

python
from trl.experimental.new_trainer import NewTrainer

Read more in the Experimental docs.

Citation

bibtex
@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 # /// script header 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:

sh
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 package
harbor_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 TrainingTrace with required masks. Prompt and completion IDs come
    from the inference engine; TRL does not re-tokenize captured prompts or select agent turns.
  • loss_mask covers prompt plus completion. Prompt positions are zero; completion positions may mix
    zeros and ones. TRL maps the completion span to its generic TurnRecord.output_mask.
  • For example, prompt [10, 11], completion [12, 13, 14], and mask [0, 0, 1, 0, 1] train on tokens
    12 and 14, while retaining 13 as 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:

sh
python -m pytest tests/experimental/test_openenv_tito.py \
    tests/experimental/test_async_grpo_trainer.py::TestReconciler -q

Overview (`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

text
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:

bash
python -m scripts.generate_tiny_models.for_causal_lm.qwen3_for_causal_lm

Each script:

  1. Checks that the installed transformers version matches the one pinned in the script (fails otherwise).
  2. Calls set_seed to seed the RNGs that model construction draws from.
  3. Builds the tiny model with random weights.
  4. Runs smoke_test β€” a minimal forward pass to catch config misspecification and NaNs.
  5. 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).
  6. Runs print_config_diff β€” prints every flat-key difference between the reference Hub config and the tiny model's config (for debugging scale-downs).
  7. 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:

bash
python -m scripts.generate_tiny_models.for_causal_lm.qwen3_for_causal_lm --create-pr

Direct 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:

text
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

  1. Pick the right subfolder based on the model class suffix (ForCausalLM, ForSequenceClassification, ForConditionalGeneration).
  2. Copy an existing script with the closest shape and adapt it β€” reference model id, config class, model class, special kwargs.
  3. 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.
  4. Keep the set_seed() call that the copied script already has, directly after the version check.
  5. 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):

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

bash
# test
pytest tests/invariant/ -m invariant

# record references
python -m tests.invariant.test_invariant      # all classes
python -m tests.invariant.test_invariant sft  # one class

Snapshot updates must be justified in the PR description.


Overview (`trl/chat_templates/README.md`)

Chat Templates

Jinja2 chat templates stored here serve two purposes:

  1. Identity comparison: detecting which model is being used (by comparing tokenizer.chat_template against known templates) to add the appropriate response schema (add_response_schema) or swap in a training template (get_training_chat_template).
  2. 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 | tojson on tool['function']['arguments'] so that arguments can be passed as a dict (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:

diff
- {%- 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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:

diff
- {%- 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:

diff
- {%- 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

python
from trl.experimental.harbor import HarborSpec
spec = HarborSpec(dataset, agent="bash")   # built-in name

Overview (`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

python
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 with read_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

python
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 the trl-lib datasets used across the examples.

Getting Started

Install TRL and additional dependencies as follows:

bash
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:

shell
accelerate launch --config_file=examples/accelerate_configs/multi_gpu.yaml --num_processes {NUM_GPUS} path_to_script.py --all_arguments_of_the_script

For DeepSpeed ZeRO-{1,2,3}:

shell
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_script

Adjust 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 under trl.experimental may 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)

  1. Prototype outside the main repo: Start development in your own fork or a separate repository to iterate quickly.
  2. Experimental inclusion: Once it’s ready for early users, move the idea into trl.experimental.<feature>.
  3. Improve: Add tests, a short doc/example, and demonstrate the usage.
  4. 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

text
<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 RLHF
text
Published 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

text
<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 the hf CLI, 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].

python
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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.

python
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:

python
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:

python
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:

python
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:

python
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:

python
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:

python
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:

python
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:

python
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:

  1. scaling rewards by the standard deviation computed over the entire batch and
  2. 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:

python
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".

python
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

πŸ“° Blog: https://yingru.notion.site/When-Speed-Kills-Stability-Demystifying-RL-Collapse-from-the-Training-Inference-Mismatch-271211a558b7808d8b12d403fd15edda

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".

python
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

πŸ“° Blog: https://yingru.notion.site/When-Speed-Kills-Stability-Demystifying-RL-Collapse-from-the-Training-Inference-Mismatch-271211a558b7808d8b12d403fd15edda

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:

python
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:

python
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:

python
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:

python
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]:

python
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:

python
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.

python
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:

python
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.

python
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.

python
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:

python
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:

python
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:

python
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:

python
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:

python
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:

python
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:

python
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.

python
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:

python
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:

python
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):

python
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:

python
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:

python
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:

python
from trl import DPOConfig

training_args = DPOConfig(
    loss_type="aot",
    beta=0.01,  # from the caption of Figure 2
)

or, for the unpaired version:

python
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:

python
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:

python
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:

python
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,
)
python
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.

python
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:

python
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:

python
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]:

python
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.

sh
python examples/tail_sft_gsm8k/tail_sft.py

EMA 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]:

python
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:

python
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):

python
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.

python
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:

python
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):

bash
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-lora

Reducing 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:

python
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]:

python
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].

python
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:

python
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:

python
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:

python
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:

python
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:

python
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:

python
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:

python
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:

python
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:

python
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.

python
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:

python
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.

python
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]:

python
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]:

python
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].

python
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.

python
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:

  • prompt
  • privileged_context for 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.

python
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:

  • prompt
  • privileged_context containing 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,

yaml
# config.yaml
distributed_type: DEEPSPEED
num_processes: 2
deepspeed_config:
  zero_stage: 3

and launch the training script using accelerate launch --config_file config_file.

sh
accelerate launch --config_file config.yaml train.py

Source 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

python
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

python
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

python
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

python
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:

bash
# 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_binarized

What's Next?

πŸ“š Learn More

πŸš€ Scale Up

πŸ’‘ Examples

Troubleshooting

Out of Memory?

Reduce batch size and enable optimizations:

python
training_args = SFTConfig(
    per_device_train_batch_size=1,  # Start small
    gradient_accumulation_steps=8,  # Maintain effective batch size
)
python
training_args = DPOConfig(
    per_device_train_batch_size=1,  # Start small
    gradient_accumulation_steps=8,  # Maintain effective batch size
)
python
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:

python
training_args = SFTConfig(learning_rate=2e-5)  # Good starting point

For 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
  • sections:
    • local: chat_templates
      title: Chat Templates
    • local: dataset_formats
      title: Dataset Formats
    • local: paper_index
      title: Paper Index
      title: Conceptual Guides
  • 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
  • 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
  • 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
  • sections:
    • local: example_overview
      title: Example Overview
    • local: community_tutorials
      title: Community Tutorials
    • local: lora_without_regret
      title: LoRA Without Regret
      title: Examples
  • 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: callbacks
      title: Callbacks
    • local: rewards
      title: Reward Functions
      title: API
  • 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

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:

  1. Offline value estimation. Before training, num_value_samples completions are sampled from the reference policy for every prompt and scored with reward_funcs. The optimal value V*(x) = β₁·log(mean_i exp(r(x, yα΅’)/β₁)) is estimated and cached per prompt.
  2. 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 advantage r(x, y) βˆ’ V*(x).
python
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:

bash
pip install torch torchvision --index-url https://download.pytorch.org/whl/rocm7.2
pip install trl

Attention 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]):

python
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 requires vllm and transformers>=5.2.0. For distributed training, only FSDP2 is supported (DeepSpeed ZeRO is not).

Currently, vllm and transformers have conflicting dependency constraints. To work around this, install vLLM first and then force-install transformers:

bash
pip 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 in compute_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/completions with prompt_logprobs for 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

python
# 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:

bash
# 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
bash
# 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"}'
bash
# Terminal 3: training on GPU 2
CUDA_VISIBLE_DEVICES=2 accelerate launch train_async_distillation.py

LoRA

Pass a peft_config to train a LoRA adapter on the student instead of the full model:

python
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:

bash
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 6

VLLM_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: with save_strategy="no" a multi-hour run finishes and leaves nothing behind. output_dir also 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:

text
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_k candidates 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_accum micro-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:

text
row-slots per step = gradient_accumulation_steps x world_size

which makes the batch metrics compose into a ladder you can check against each other:

text
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_step

So 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_s high β†’ generation-bound. The trainer is starving; look at rollout/generated_tok_s, rollout/inflight and rollout/score_s.
  • queue near full, rollout/backpressure_s high β†’ trainer-bound. Generation is throttled and its output is aging in the queue, so watch sample/staleness_mean climb.
  • 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_bwd measures performance during the forward and backward passes.
  • _wall_clock measures 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 requires vllm and transformers>=5.2.0. For distributed training, only FSDP2 is supported (DeepSpeed ZeRO is not).

Currently, vllm and transformers have conflicting dependency constraints. To work around this, install vLLM first and then force-install transformers:

bash
pip 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, and environment_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 a TypeError at trainer.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. an AutoModelForSequenceClassification scorer) 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 like accuracy_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

python
# 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:

bash
# 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-len to 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 plus max_completion_length from your config.

bash
# Terminal 2: training on GPU 1
CUDA_VISIBLE_DEVICES=1 accelerate launch train_async_grpo.py

AMD 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:

bash
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) need flash-linear-attention installed, 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:

bash
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-out
python
config = 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:

python
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:

bash
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 6

VLLM_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: with save_strategy="no" a multi-hour run finishes and leaves nothing behind. output_dir also 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:

text
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_generations times. 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_accum micro-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:

text
row-slots per step = gradient_accumulation_steps x world_size

which makes the batch metrics compose into a ladder you can check against each other:

text
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_step

So 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:

text
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, causal

How it works, on two rollouts of one prompt:

text
/* 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_s high β†’ generation-bound. The trainer is starving; look at rollout/generated_tok_s, rollout/inflight and rollout/score_queue_size.
  • queue near full, rollout/backpressure_s high β†’ trainer-bound. Generation is throttled and its output is aging in the queue, so watch sample/staleness_mean climb.
  • 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_bwd measures performance during the forward and backward passes.
  • _wall_clock measures 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.

bash
pip install trl

Supervised fine-tuning

The [SFTTrainer] quick start runs as is:

python
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:

python
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:

python
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:

bash
accelerate launch --num_processes 4 train.py

In 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

python
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:

python
>>> 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=True needs &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% endif %&#125; into each role branch, and wrap the assistant branch (<|START_RESPONSE|>...<|END_RESPONSE|><|END_OF_TURN_TOKEN|>) with &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; markers for SFT assistant-only loss.

`deepseekv3_training.jinja`

Patched DeepSeek-V3 template. Diff vs deepseekv3.jinja:

  • Uses | tojson on tool['function']['arguments'] so that arguments can be passed as a dict (the documented format per transformers docs). The original template uses raw string concatenation, which crashes on dict inputs.
  • Wraps assistant message output with &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125;), 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:

diff
- {%- if '</think>' in content %}
+ {%- if '<think>' in content and '</think>' in content %}

Wrap assistant message output (including the thinking block and tool calls) with &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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:

diff
- {%- 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:

diff
- {%- 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125;, 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; 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 &#123;% generation %&#125; / &#123;% endgeneration %&#125; markers wrap the assistant content. This enables return_assistant_tokens_mask=True to produce correct masks for SFT assistant-only loss.

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 distillation
  • trl dpo: fine-tune a LLM with DPO
  • trl grpo: fine-tune a LLM with GRPO
  • trl kto: fine-tune a LLM with KTO
  • trl reward: train a Reward Model
  • trl rloo: fine-tune a LLM with RLOO
  • trl sft: fine-tune a LLM with SFT

Other Commands

  • trl env: get the system information
  • trl vllm-serve: serve a model with vLLM (deprecated, use vllm serve directly, 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:

bash
trl sft \
  --model_name_or_path Qwen/Qwen2.5-0.5B \
  --dataset_name stanfordnlp/imdb
bash
trl dpo \
  --model_name_or_path Qwen/Qwen2.5-0.5B \
  --dataset_name anthropic/hh-rlhf
bash
trl reward \
  --model_name_or_path Qwen/Qwen2.5-0.5B \
  --dataset_name trl-lib/ultrafeedback_binarized
bash
trl grpo \
  --model_name_or_path Qwen/Qwen2.5-0.5B \
  --dataset_name HuggingFaceH4/Polaris-Dataset-53K \
  --reward_funcs accuracy_reward
bash
trl rloo \
  --model_name_or_path Qwen/Qwen2.5-0.5B \
  --dataset_name HuggingFaceH4/Polaris-Dataset-53K \
  --reward_funcs accuracy_reward
bash
trl kto \
  --model_name_or_path Qwen/Qwen2.5-0.5B \
  --dataset_name trl-lib/kto-mix-14k

Using Configuration Files

To keep your CLI commands clean and reproducible, you can define all training arguments in a YAML configuration file:

yaml
# sft_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: stanfordnlp/imdb

Launch with:

bash
trl sft --config sft_config.yaml
yaml
# dpo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: anthropic/hh-rlhf

Launch with:

bash
trl dpo --config dpo_config.yaml
yaml
# reward_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: trl-lib/ultrafeedback_binarized

Launch with:

bash
trl reward --config reward_config.yaml
yaml
# grpo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: HuggingFaceH4/Polaris-Dataset-53K
reward_funcs:
  - accuracy_reward

Launch with:

bash
trl grpo --config grpo_config.yaml
yaml
# rloo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: HuggingFaceH4/Polaris-Dataset-53K
reward_funcs:
  - accuracy_reward

Launch with:

bash
trl rloo --config rloo_config.yaml
yaml
# kto_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: trl-lib/kto-mix-14k

Launch with:

bash
trl kto --config kto_config.yaml

Scaling 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.

bash
trl sft \
  --model_name_or_path Qwen/Qwen2.5-0.5B \
  --dataset_name stanfordnlp/imdb \
  --num_processes 4

or, with a config file:

yaml
# sft_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: stanfordnlp/imdb
num_processes: 4

Launch with:

bash
trl sft --config sft_config.yaml
bash
trl dpo \
  --model_name_or_path Qwen/Qwen2.5-0.5B \
  --dataset_name anthropic/hh-rlhf \
  --num_processes 4

or, with a config file:

yaml
# dpo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: anthropic/hh-rlhf
num_processes: 4

Launch with:

bash
trl dpo --config dpo_config.yaml
bash
trl reward \
  --model_name_or_path Qwen/Qwen2.5-0.5B \
  --dataset_name trl-lib/ultrafeedback_binarized \
  --num_processes 4

or, with a config file:

yaml
# reward_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: trl-lib/ultrafeedback_binarized
num_processes: 4

Launch with:

bash
trl reward --config reward_config.yaml
bash
trl grpo \
  --model_name_or_path Qwen/Qwen2.5-0.5B \
  --dataset_name HuggingFaceH4/Polaris-Dataset-53K \
  --reward_funcs accuracy_reward \
  --num_processes 4

or, with a config file:

yaml
# grpo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: HuggingFaceH4/Polaris-Dataset-53K
reward_funcs:
  - accuracy_reward
num_processes: 4

Launch with:

bash
trl grpo --config grpo_config.yaml
bash
trl rloo \
  --model_name_or_path Qwen/Qwen2.5-0.5B \
  --dataset_name HuggingFaceH4/Polaris-Dataset-53K \
  --reward_funcs accuracy_reward \
  --num_processes 4

or, with a config file:

yaml
# rloo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: HuggingFaceH4/Polaris-Dataset-53K
reward_funcs:
  - accuracy_reward
num_processes: 4

Launch with:

bash
trl rloo --config rloo_config.yaml
bash
trl kto \
  --model_name_or_path Qwen/Qwen2.5-0.5B \
  --dataset_name trl-lib/kto-mix-14k \
  --num_processes 4

or, with a config file:

yaml
# kto_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
dataset_name: trl-lib/kto-mix-14k
num_processes: 4

Launch with:

bash
trl kto --config kto_config.yaml

Using `--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

bash
trl sft \
  --model_name_or_path Qwen/Qwen2.5-0.5B \
  --dataset_name stanfordnlp/imdb \
  --accelerate_config zero2  # or path/to/my/accelerate/config.yaml

or, with a config file:

yaml
# 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.yaml

Launch with:

bash
trl sft --config sft_config.yaml
bash
trl dpo \
  --model_name_or_path Qwen/Qwen2.5-0.5B \
  --dataset_name anthropic/hh-rlhf \
  --accelerate_config zero2  # or path/to/my/accelerate/config.yaml

or, with a config file:

yaml
# 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.yaml

Launch with:

bash
trl dpo --config dpo_config.yaml
bash
trl 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.yaml

or, with a config file:

yaml
# 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.yaml

Launch with:

bash
trl reward --config reward_config.yaml
bash
trl 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.yaml

or, with a config file:

yaml
# 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.yaml

Launch with:

bash
trl grpo --config grpo_config.yaml
bash
trl 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.yaml

or, with a config file:

yaml
# 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.yaml

Launch with:

bash
trl rloo --config rloo_config.yaml
bash
trl 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.yaml

or, with a config file:

yaml
# 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.yaml

Launch with:

bash
trl kto --config kto_config.yaml

Using 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.

yaml
# sft_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
datasets:
  - path: stanfordnlp/imdb
  - path: roneneldan/TinyStories

Launch with:

bash
trl sft --config sft_config.yaml
yaml
# dpo_config.yaml
model_name_or_path: Qwen/Qwen2.5-0.5B
datasets:
  - path: BAAI/Infinity-Preference
  - path: argilla/Capybara-Preferences

Launch with:

bash
trl dpo --config dpo_config.yaml
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-sentiment

Launch with:

bash
trl reward --config reward_config.yaml
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_reward

Launch with:

bash
trl grpo --config grpo_config.yaml
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_reward

Launch with:

bash
trl rloo --config rloo_config.yaml
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-cleaned

Launch with:

bash
trl kto --config kto_config.yaml

To 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:

bash
trl env

This 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.

txt
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 installed

This 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): Use SFTTrainer(..., processing_class=tokenizer) instead, or simply omit it (it will be inferred from the model).
  • setup_chat_format(model, tokenizer): Use SFTConfig(..., chat_template_path="Qwen/Qwen3-0.6B"), where chat_template_path specifies 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:

python
# 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:

bash
accelerate launch train_cpo.py

Expected 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 beta
  • rewards/rejected: the mean log probabilities of the policy model for the rejected responses scaled by beta
  • rewards/accuracies: mean of how often the chosen rewards are > than the corresponding rejected rewards
  • rewards/margins: the mean difference between the chosen and corresponding rejected rewards
  • nll_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:

python
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:

python
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.

python
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.

python
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.

python
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.

python
# 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.

python
# 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.

python
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:

python
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:

python
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?"},
text
{"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:

python
# 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).

python
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:

python
# 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:

python
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:

python
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:

python
{"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:

python
{"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:

python
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([...])):

python
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.

python
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:

txt
<|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.

python
# 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.

python
# 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 the apply_chat_template function for each type:

python
from 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".

python
# 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.

python
# 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.

python
# 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:

sh
python examples/datasets/ultrafeedback.py --push_to_hub --repo_id trl-lib/ultrafeedback-gpt-3.5-turbo-helpfulness

Once 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.

python
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"])
python
>>> 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.

python
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")
python
>>> 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".

python
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")
python
>>> 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".

python
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")
python
>>> 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.

python
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"])
python
>>> 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].

python
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)
python
>>> 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].

python
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)
python
>>> 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.

python
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"])
python
>>> 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".

python
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")
python
>>> 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.

python
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"])
python
>>> 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.

python
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")
python
>>> 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].

python
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)
python
>>> 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.

python
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"])
python
>>> 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.

python
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"])
python
>>> 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.

python
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"])
python
>>> 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:

  1. The dataset must contain the key images with the image data (as lists of PIL images) or image with a single PIL image.
  2. The "content" field in messages must be a list of dictionaries, where each dictionary specifies the type of data: "image" or "text".

Example:

python
# 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 requires transformers version 4.57.0 or later. Example:

python
dataset = 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:

bash
pip install deepspeed

Running Training Scripts with DeepSpeed

No modifications to your training script are required. Simply run it with the DeepSpeed configuration file:

bash
accelerate launch --config_file <ACCELERATE_WITH_DEEPSPEED_CONFIG_FILE.yaml> train.py

We 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:

bash
accelerate launch --config_file examples/accelerate_configs/deepspeed_zero2.yaml train.py

Additional 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.

python
# 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:

bash
accelerate launch train_distillation.py

Looking 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:

python
{"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 when tools are 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 is 0.0 when no tool was called. Logged only when tools are 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

shell
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.

python
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.

  1. Start the vLLM server:

    bash
    VLLM_SERVER_DEV_MODE=1 vllm serve <model_name> \
        --weight-transfer-config '{"backend": "nccl"}' \
        --logprobs-mode processed_logprobs \
        --max-logprobs -1
  2. Enable server mode in your training script:

    python
    from 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 the CUDA_VISIBLE_DEVICES environment variable.

[!TIP]
Depending on the model size and the overall GPU memory requirements for training, you may need to adjust the vllm_gpu_memory_utilization parameter 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.

python
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 reads lm_head.weight directly and runs the student backbone without going through PeftModel.forward(). Adapters on lm_head (via target_modules) and prompt-learning methods (PromptTuning, PrefixTuning, P-Tuning) are therefore rejected, since they would be silently ignored. To train the head, use modules_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:

python
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:

python
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).

python
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.

bash
# 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
bash
# 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 16

DistillationTrainer

[[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

bash
accelerate config

and answering the questions according to your multi-GPU / multi-node setup. You can then launch distributed training by running:

bash
accelerate launch train.py

We 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.:

shell
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:

yaml
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: true

Adjust num_processes to match the total number of GPUs across all nodes.

[!NOTE]
Replace 10.0.0.1 with the actual IP address of the rank 0 (main) node.

Launching

Option 1: Manual Launch (Non-HPC)

Run the following on each node manually:

bash
# 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.py

Option 2: SLURM Launch (HPC Clusters)

For clusters using SLURM job scheduler, create a job script (e.g., slurm_job.sh):

bash
#!/bin/bash
#SBATCH --nodes=2
#SBATCH --gpus-per-node=8
#SBATCH --job-name=trl_multi

srun accelerate launch --config_file multi_node.yaml train.py

Then submit the job:

bash
sbatch slurm_job.sh

SLURM 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


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.

python
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.

python
# 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:

python
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"])))
json
{
    "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 by router_aux_loss_coef and added to the loss. Logged only when the model is a MoE model and router_aux_loss_coef is 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=True is not supported with loss_type="aot" or loss_type="aot_unpaired".
  • PEFT adapters targeting lm_head are not supported (use modules_to_save=["lm_head"] instead).
  • sync_ref_model=True is not supported when training with PEFT models that do not keep a standalone ref_model.
  • sync_ref_model=True cannot be combined with precompute_ref_log_probs=True.
  • precompute_ref_log_probs=True is not supported with IterableDataset (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:

python
# 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

python
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].

python
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.

python
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.

python
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.

python
DPOConfig(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 (tool role messages)
  • The list of available tools in the tools column, 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.

python
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, set max_length=None in the [DPOConfig]. This allows the model to process the full sequence length without truncating image tokens.

python
DPOConfig(max_length=None, ...)

Only use max_length when 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:

  1. It addresses the train-inference distribution mismatch in auto-regressive sequence models by training the student model on its self-generated output sequences.
  2. 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. When lmbda=0.0, the loss reduces to supervised JSD where the student is trained with the token-level probabilities of the teacher. When lmbda=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 the lmbda value for each batch.
  • seq_kd: controls whether to perform Sequence-Level KD (can be viewed as supervised FT on teacher-generated out). When seq_kd=True and lmbda=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. When beta=0.0 the loss approximates forward KL divergence, while for beta=1.0 the 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 that attn_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:

python
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: either system, assistant or user
  • content: 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

python
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:

  1. 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.
  2. Hybrid ULD loss – when uld_use_hybrid_loss is 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.
  3. 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 the trl.experimental namespace. 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 to True for cross-tokenizer setups.
  • teacher_tokenizer_name_or_path – required when use_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_size is the number of unique prompts per worker per optimizer step.

A minimal end-to-end example:

python
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:

python
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). Set dataloader_drop_last=True to avoid this warning.

Expected dataset type

GOLD requires a conversational language modeling dataset, e.g.:

python
{"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:

text
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 position
  • P(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:

  1. Correct joint probability for the actual generated sequence (by the chain rule)
  2. Reasonable approximation for counterfactual tokens (scaled by the same continuation likelihood)
  3. Unnormalized distributions that preserve the correct relative probabilities for ULD loss computation

Example

Given:

text
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"]:

text
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.09

The 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.

bash
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_hub

Training 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.

python
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:

bash
# 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. Use truncation_mode='keep_start' (the default) or set max_length=None in the [experimental.gold.GOLDConfig]. This allows the model to process the full sequence length without truncating image tokens.

python
GOLDConfig(max_length=None, ...)

Only use max_length when you've verified that truncation won't remove image tokens for the entire dataset.

[!NOTE]
Cross-architecture VLM distillation requires use_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.

python
# 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:

bash
accelerate launch train_grpo.py

Distributed across 8 GPUs, the training takes approximately 1 day.

Note: The reward curves above were generated with Qwen/Qwen2-0.5B-Instruct. Results with Qwen/Qwen2.5-0.5B-Instruct will 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 setting scale_rewards=False in [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 setting scale_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 set beta in [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 when tools are 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 is 0.0 when no tool was called. Logged only when tools are provided.
  • rewards/{reward_func_name}/mean: The average reward from a specific reward function. When an environment owns the reward via get_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 by reward_weights).
  • reward_std: The standard deviation of summed rewards across functions (weighted by reward_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 when use_vllm=True and vllm_importance_sampling_correction=True.
  • sampling/sampling_logp_difference/max: The largest such absolute difference. Logged only when use_vllm=True and vllm_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 by vllm_importance_sampling_mode has been applied (clipped to [C_min, C_max] for *_truncate modes, set to zero for *_mask modes). Computed over completion tokens, or over sequences for the sequence_* modes. Logged only when use_vllm=True and vllm_importance_sampling_correction=True.
  • sampling/importance_sampling_ratio/mean: The average constrained importance sampling ratio. Logged only when use_vllm=True and vllm_importance_sampling_correction=True.
  • sampling/importance_sampling_ratio/max: The largest constrained importance sampling ratio. Logged only when use_vllm=True and vllm_importance_sampling_correction=True.
  • policy_loss: The policy gradient loss value (before any entropy bonus). Logged when entropy_coef is nonzero or use_adaptive_entropy=True.
  • entropy: Average entropy of token predictions across generated completions. (If mask_truncated_completions=True, masked sequences tokens are excluded.)
  • entropy_coef: The current entropy regularization coefficient. Logged when entropy_coef is nonzero or use_adaptive_entropy=True. Updated once per optimizer step when use_adaptive_entropy=True.
  • aux_loss: The load-balancing auxiliary loss of a Mixture-of-Experts model, before it is scaled by router_aux_loss_coef and added to the loss. Logged only when the model is a MoE model and router_aux_loss_coef is nonzero.
  • kl: The average KL divergence between the model and the reference model, calculated over generated completions. Logged only if beta is nonzero.
  • clip_ratio/region_mean: The ratio of token (or sequence, if importance_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, if importance_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, if importance_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, if importance_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, if importance_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, if importance_sampling_level="sequence") importance sampling weights that were clipped at epsilon_high while having a positive advantage: \(r_{i,t}(\theta) > \epsilon_\mathrm{high}\). Logged only when loss_type="cispo".
  • vespo/phi_seq_mean: The average value of the VESPO Gamma weighting \(\varphi(w)\) applied to the sequence-level importance weights. Values below 1.0 mean sequences are being down-weighted. Logged only when loss_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

shell
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 setting vllm_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.

python
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.

  1. Start the vLLM server:

    bash
    VLLM_SERVER_DEV_MODE=1 vllm serve <model_name> \
        --weight-transfer-config '{"backend": "nccl"}' \
        --logprobs-mode processed_logprobs \
        --max-logprobs -1
  2. Enable server mode in your training script:

    python
    from 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 the CUDA_VISIBLE_DEVICES environment variable.

[!TIP]
Depending on the model size and the overall GPU memory requirements for training, you may need to adjust the vllm_gpu_memory_utilization parameter 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_utilization recommendation:

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_mode to 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 uses MASTER_ADDR=localhost and MASTER_PORT=12345 for 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.

python
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 defaults max_memory_percent to 0.5 (instead of transformers' 0.9) to leave enough VRAM for the training backward pass. Tune it down to 0.3–0.4 for 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.

sh
#!/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 &

wait
python
import 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.

  1. 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 callable log_extra(column: str, values: list) to add extra columns to the completions table. See Example 6.
      • log_metric: a callable log_metric(name: str, value: float) to log scalar metrics as plots alongside kl, entropy, etc. See Example 6.
      • environments: a list of environment instances, one per completion. Only present when environment_factory is 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 a get_reward method instead β€” see Rewards.
      • All column names (but prompt) that the dataset may have. For example, if the dataset contains a column named ground_truth, the function will be called with ground_truth as a keyword argument.

      The easiest way to comply with this requirement is to use **kwargs in the function signature.

    • Depending on the dataset format, the input will vary:

  2. 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:

python
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:

python
>>> 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.

python
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:

python
>>> 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.

python
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:

python
>>> 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.

python
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:

python
>>> 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.

text
/* 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:

python
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.

python
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 rewards

Passing the reward function to the trainer

To use your custom reward function, pass it to the [GRPOTrainer] as follows:

python
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:

python
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:

python
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:

python
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 tools for 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 reset lifecycle hook and an optional get_reward that lets it own the reward. Use environment_factory when 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:

python
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:

python
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_factory requires transformers>=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).

python
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 either None or 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 a float: 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 each reset(), the G members of a GRPO group may not share the same initial state, making the group baseline slightly noisier. For identical states within a group, have reset() 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).

python
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).

python
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.

python
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).

python
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:

bash
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_completions

Configuration 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 (server and colocate modes)

Dataset Format

Each training sample should include:

  • prompt: Text formatted via the processor's chat template
  • image/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 at trl.experimental.harbor and is gated behind the trl[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

bash
pip install trl[harbor]

[!IMPORTANT]
Harbor drives generation through vLLM and uses environment_factory, which requires vllm and transformers>=5.2.0.

bash
pip 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:

bash
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 daemon

Quick 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:

python
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:

  1. spec.train_dataset: resolves the task suite to local task directories (downloading the HF dataset if needed) and builds a datasets.Dataset with prompt (empty β€” the env's instruction is appended at reset), task_dir, task_index, plus per-task task.toml metadata columns.
  2. spec.environment_factory: returns a zero-arg callable producing a fresh per-rollout [~trl.experimental.harbor.HarborEnv]. On reset(task_dir) it starts the task's Harbor sandbox and returns its instruction; tool methods exec into the sandbox; env.reward runs the verifier once after the rollout.
  3. 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:

text
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 reward

Select a subset with num_tasks or indices (mutually exclusive):

python
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])  # specific

Agents: 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 exec method"). 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=:

python
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 directly

The 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.
python
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:

python
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):

python
HarborSpec(dataset, environment_type="e2b")   # cloud sandbox (offloads provisioning), needs E2B_API_KEY
HarborSpec(dataset, environment_type="docker")  # default; needs a local Docker daemon

e2b 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:

python
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. Set TRL_EXPERIMENTAL_SILENCE=1 to 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 e2b mitigate the per-sandbox cost).
  • A single HarborSpec covers 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.

bash
uv pip install trl
bash
pip install trl

Source

You can also install the latest version from source. First clone the repo and then run the installation with pip:

bash
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:

bash
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 the trl.experimental namespace. APIs may change without notice while the feature is iterated on.

Usage

python
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:

bash
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_hub
python
from 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:

bash
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 it

See 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:

python
# /// 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:

bash
hf jobs uv run \
    --flavor a10g-small \
    --timeout 30m \
    --secrets HF_TOKEN \
    train.py
python
from 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:

python
#