TRL documentation
Asynchronous GRPO Trainer
Asynchronous GRPO Trainer
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.
This trainer was contributed by Amine Dirhoussi and Quentin Gallouédec.
Quick start
This example demonstrates how to train a model using the Asynchronous GRPO method. We train a Qwen3.5 2B 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. The trainer needs vLLM and kernels, and accuracy_reward needs math-verify, which you can install with pip install trl[vllm,kernels,math_verify].
# train_async_grpo.py
from datasets import load_dataset
from trl.experimental.async_grpo import AsyncGRPOTrainer
from trl.rewards import accuracy_reward
if __name__ == "__main__": # the rollout worker is a spawned process that re-imports this script
dataset = load_dataset("trl-lib/DeepMath-103K", split="train")
trainer = AsyncGRPOTrainer(
model="Qwen/Qwen3.5-2B",
reward_funcs=accuracy_reward,
train_dataset=dataset,
)
trainer.train()Generation runs on a vLLM server, on different GPUs than the trainer. Start it on GPU 0:
CUDA_VISIBLE_DEVICES=0 VLLM_SERVER_DEV_MODE=1 vllm serve Qwen/Qwen3.5-2B \
--weight-transfer-config '{"backend": "nccl"}' \
--logprobs-mode processed_logprobs \
--api-server-count 8 \
--max-model-len 4096Then execute the script on GPU 1 using the following command:
CUDA_VISIBLE_DEVICES=1 python train_async_grpo.py
Compatibility and constraints
Some argument combinations are intentionally restricted in the current experimental.async_grpo.AsyncGRPOTrainer implementation:
- Generation always runs on a separate vLLM server, started with
vllm serve. There is no colocate mode. - Prompts from the dataset must be in conversational format. With an
environment_factory, the dataset may have nopromptcolumn, andreset()provides it. - Reward functions must be Python callables. Reward models given as a model id or a PreTrainedModel are not supported, see Using a custom reward function.
- The trainer needs
transformers>=5.2.0. - Sharded training uses FSDP2. DeepSpeed is not supported, and neither are context and sequence parallelism (
cp_size > 1orsp_size > 1inparallelism_config). use_liger_kernel=Trueis not supported.- PEFT adapters targeting
lm_headare not supported. - Models with attention sinks are not supported. The only non-attention layers supported are the gated DeltaNet layers of hybrid models (Qwen3.5, Qwen3.6, Qwen3-Next, …), which need
transformers>=5.15.0. - Images are not supported, see Training Vision Language Models.
Training loads its attention kernel from the Hub through the kernels library: kernels-community/aiter-flash-attn on AMD GPUs (ROCm), kernels-community/flash-attn4 on NVIDIA Blackwell GPUs and kernels-community/flash-attn3 on other NVIDIA GPUs. Models that cap their attention logits (attn_logit_softcapping), such as Gemma 2, are not supported on AMD GPUs or on NVIDIA Blackwell GPUs.
Looking deeper into the Asynchronous GRPO method
In GRPOTrainer, generation and training are sequential: generate a batch, compute the loss, update the weights, repeat. Even in colocate mode, one phase must finish before the other begins. In experimental.async_grpo.AsyncGRPOTrainer, a rollout worker generates and scores completions in a background process while the training loop computes the loss and updates the model. To understand how it works, it can be broken down into three main steps: Generating completions, computing the advantage, and computing the loss. There is no reference model, so there is no KL divergence term (GRPO’s default, beta=0.0).
Generating completions
The rollout worker keeps up to max_concurrent_rollouts requests in flight to the vLLM server. For each prompt, it generates a group of completions (num_generations) and, once the whole group is done, scores it and computes its advantages. The resulting training samples go into a queue, which the training loop pulls from. Generation waits only when it is max_staleness steps ahead of training (see Concurrency), and a training step only waits when the queue is empty.
Every weight_sync_steps training steps, the trainer sends its updated weights to the vLLM server over NCCL, so the next completions come from the latest policy. A completion can therefore come from a policy a few steps older than the one being trained: a sample more than max_staleness weight updates behind when the trainer pulls it is dropped.
The metrics and the rest of this page use the following terms for the stages a completion goes through:
- a group is one prompt and its rollouts. The advantage is computed within it.
- a rollout is one full conversation generated for a prompt, including every tool call. It is made of one or more turns, one vLLM request each.
- a sample is what the loss is computed on: the tokens of a rollout, with its advantage. A multi-turn rollout can produce more than one sample (see Multi-turn rollouts).
- a row is what one process forwards in one micro-batch: several samples packed together (see Tree packing). A micro-batch holds one row per process, and
gradient_accumulation_stepsmicro-batches make one step, the optimizer step.
Token counts come in three kinds: generated tokens are what the model produced, forwarded tokens are every token of a sample (prompt and tool results included), and trained tokens are the ones the loss is taken over.
Computing the advantage
For each of the completions of a group, the reward is the sum of the rewards from each reward function. The advantage is normalized within the group, as in GRPO:
There is no scale_rewards option: the advantage is always scaled by the group’s standard deviation. A completion for which every reward function returned None is left out of the group’s mean and standard deviation and gets an advantage of zero.
A group whose rewards are all equal is dropped: with the default advantage it is zero everywhere, so training on it would cost a forward and backward pass and move nothing. The fraction of such groups is logged as frac_reward_zero_std. The step is filled from the groups that are kept, so many dropped groups means more generation per step. A dropped group still counts toward the epoch, which stays one pass over the prompts.
Computing the loss
The vLLM server’s log-probabilities never match the trainer’s exactly: different kernels, a different precision, and a policy a few steps behind (see Dealing with the Training-Inference Mismatch). The trainer uses the sampler’s log-probabilities as the old policy, so a single importance ratio covers both the mismatch and the staleness:
Tokens whose ratio falls outside [vllm_importance_sampling_clip_min, vllm_importance_sampling_clip_max] are dropped from the loss. For more information, see Masked Importance Sampling.
By default (score_centering=True), the trainer uses Score Centering. Importance weights alone do not remove the mismatch: the score of a sampled token does not average to zero under the sampler , so every update also pulls the policy toward the sampler. Score centering subtracts that expectation at every position:
where inside and outside. The inner expectation is taken over the sampler’s num_sampler_logprobs most likely tokens at each position, which the rollout worker requests from the server.
With score_centering=False, the trainer uses GRPO’s clipped surrogate objective instead, with the same masked ratio:
where is epsilon and is epsilon_high.
In both cases, the loss is normalized by the number of trained tokens in the micro-batch, counted across all processes, and averaged over the gradient_accumulation_steps micro-batches of a step.
Logged metrics
While training, we record the following metrics:
num_tokens: The total number of tokens forwarded so far, prompts and completions. With tree packing, a prefix shared by several samples is counted once.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, whose last turn does not end with an EOS token.completions/duplicate_frac: The fraction of rollouts that repeat an earlier rollout of their group token for token. See Saturation and duplicates.tools/call_frequency: The average number of tool calls per completion, over every rollout, including the groups that are then dropped. Logged only when the trainer has tools.tools/failure_frequency: The fraction of tool calls that failed (the tool was not found or raised an exception), over every tool call, including the groups that are then dropped. It is NaN in a logging window with no tool call, where GRPO logs0.0. Logged only when the trainer has tools.rewards/{reward_func_name}/mean: The average reward from a specific reward function, over every completion it scored, including the groups that are then dropped. When an environment owns the reward viaget_reward,{reward_func_name}is the environment’s class name.rewards/{reward_func_name}/group_std: The standard deviation of the reward from a specific reward function, computed within each group and averaged over the trained samples.reward: The overall average reward after summing rewards across functions, over every scored completion, including the groups that are then dropped. It is the policy’s reward, so it is the one to track:reward_keptleaves out the groups whose rewards are all equal.reward_group_std: The standard deviation of summed rewards across functions, computed within each group and averaged over the trained samples. It is what the advantage is divided by, unlike GRPO’sreward_std, which is taken over the whole batch.reward_kept: The average reward of the trained samples, which excludes the dropped groups.reward_dropped: The average reward of the groups that were dropped, the reward they are stuck at. See Saturation and duplicates to read it withfrac_reward_zero_stdandreward.frac_reward_zero_std: The fraction of groups with a reward std of zero (all answers are correct or incorrect), or with fewer than two scorable completions. These groups are dropped.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 trained tokens.sampling/sampling_logp_difference/max: The largest such absolute difference.sampling/importance_sampling_ratio/min: The smallest importance sampling ratio between the model and the sampler, over trained tokens, before the tokens outside[vllm_importance_sampling_clip_min, vllm_importance_sampling_clip_max]are dropped.sampling/importance_sampling_ratio/mean: The average such importance sampling ratio.sampling/importance_sampling_ratio/max: The largest such importance sampling ratio.sampling/masked_mean: The fraction of trained tokens dropped from the loss because their importance sampling ratio falls outside[vllm_importance_sampling_clip_min, vllm_importance_sampling_clip_max].sampling/sampler_mass: The average probability mass the sampler’snum_sampler_logprobsmost likely tokens cover. Logged only whenscore_centering=True.entropy: Average entropy of token predictions across trained tokens.aux_loss: The load-balancing auxiliary loss of a Mixture-of-Experts model, before it is scaled byrouter_aux_loss_coefand added to the loss. Logged only when the model is a MoE model androuter_aux_loss_coefis nonzero.clip_ratio/region_mean: The ratio of token probabilities where the GRPO objective is clipped to stay within the trust region: . Withscore_centering=Truethe loss does not clip, so theclip_ratio/*metrics count the tokens outside the trust region.clip_ratio/low_mean: The average ratio of token probabilities that were clipped on the lower bound of the trust region: .clip_ratio/low_min: The smallest per-completion fraction of tokens clipped on the lower bound of the trust region: .clip_ratio/high_mean: The average ratio of token probabilities that were clipped on the upper bound of the trust region: .clip_ratio/high_max: The largest per-completion fraction of tokens clipped on the upper bound of the trust region: .
The other metrics describe the asynchronous pipeline.
Metrics are logged once per logging window: everything recorded since the previous log, which covers the last logging_steps optimizer steps. Over the window:
- A mean measured per rollout, sample or call, like
reward,rollout/turns_meanandtools/latency_s, is averaged over all the rollouts, samples or calls in the window. A rate likerollout/generated_tokens_per_sis the total tokens divided by the total seconds. - Otherwise, a name whose last part contains the word
countis summed:sample/dropped_stale_countis the number of samples dropped since the last log, not since the start of training. - A name whose last part contains the word
maxormin, likesample/staleness_maxorcompletions/max_length, keeps the largest or smallest value. - Any other metric is the mean of the values recorded in the window.
Units:
_s: seconds.tokens_per_s: tokens per second._frac: fraction between 0 and 1._per_step: amount in one optimizer step, averaged over the steps in the window.
Rollout metrics
rollout/duration_s: The wall time of one rollout, from dispatch until the rollout loop collects it.rollout/generated_tokens_per_s: The generation throughput, in generated tokens per second, since the previous report.rollout/concurrency: The number of rollouts in flight to the vLLM server.rollout/concurrency_cap: The current cap on rollouts in flight, which follows the server’s load. See Concurrency.rollout/turns_mean,rollout/turns_max: The average and maximum number of turns per rollout.rollout/samples_per_rollout: The number of training samples per rollout. Above1.0, some rollouts were forked; below1.0, some produced no trained token. See Multi-turn rollouts.rollout/fork_frac: The fraction of turn transitions whose prompt does not start with the held tokens, so the turn started a new sample.rollout/drift_tokens_mean,rollout/drift_tokens_max: The number of held tokens a fork re-rendered, averaged over turn transitions (one that does not fork counts as0). The new sample shares the tokens before the divergence, so this is what each fork adds to the forward.rollout/score_queue_size: The number of completed groups waiting to be scored.rollout/queue_size: The number of scored samples waiting in the rollout queue, measured where the trainer pulls them. See The rollout queue.rollout/score_s: The time to score a group.rollout/score_wait_s: The time a completed group waited before being scored.rollout/score_block_s: The time generation was blocked because the scoring queue was full.rollout/backpressure_s: The time generation waited to start a rollout because it wasmax_stalenesssteps ahead of training, averaged over the waits in the logging window. See Concurrency.rollout/vllm_retry_count: The number of retried vLLM requests. A degraded server otherwise looks like unexplained slowness.rollout/vllm_kv_cache_usage_frac: The vLLM KV cache usage (maximum over engines), averaged over the polls in the logging window.rollout/vllm_running_requests: The number of requests the server’s engines are decoding (vLLM’snum_requests_running, summed over engines). The gap torollout/concurrencyis what sits in the API server process, building responses.rollout/vllm_queued_requests: The number of requests the server has accepted but not yet scheduled (vLLM’snum_requests_waiting, summed over engines).rollout/vllm_preemption_count: The number of requests the server preempted (vLLM’snum_preemptions_total, summed over engines). A weight sync resets the server’s caches, which preempts every running request, so this is not a pressure signal.
Tool metrics
tools/latency_s,tools/{tool_name}/latency_s: The time spent executing a tool call, over all tools and per tool.tools/{tool_name}/call_count,tools/{tool_name}/failure_count: The number of calls and failed calls per tool.tools/unknown_name_count: The number of calls to a tool that does not exist. Unlike a tool that ran and raised, this is a policy error.tools/parallel_calls_mean: The average number of tool calls in an assistant message that calls tools.tools/loop_exhausted_frac: The fraction of rollouts cut off atmax_tool_calling_iterationswhile still calling tools, over every rollout, including rollouts with no call and the groups that are then dropped. Logged only when the trainer has tools.
HarnessAsyncGRPOTrainer logs most of these under the same names. See Training on harnesses for the differences.
Environment metrics
env/reset_s: The time spent in the environment’sreset()before a rollout starts. Logged only whenenvironment_factoryis provided.env/reward_s: The time spent in the environment’sget_reward()after a rollout ends. Logged only when the environment definesget_reward.
Sample metrics
sample/forwarded_tokens_mean,sample/forwarded_tokens_max: The average and maximum number of forwarded tokens per sample.sample/trained_tokens_mean: The average number of trained tokens per sample.sample/time_in_queue_s: The time a sample waited in the rollout queue before being trained on.sample/staleness_mean,sample/staleness_max: The number of weight updates between the policy that generated a sample and the one training on it. Thesampling/importance_sampling_ratio/*andsampling/sampling_logp_difference/*metrics combine this staleness with the training-inference mismatch: a ratio away from1.0or a log-probability difference away from0.0at zero staleness points to the mismatch, for example a dtype difference with the server.sample/dropped_stale_count: The number of samples dropped for exceedingmax_staleness.
Batch metrics
batch/forwarded_tokens_per_step,batch/trained_tokens_per_step: The number of forwarded and trained tokens per step, over all processes.batch/samples_per_step,batch/groups_per_step: The number of samples and of distinct groups per step. They differ by the number of samples per group.batch/microbatches_per_step: The number of micro-batches per step.batch/masked_token_frac: The fraction of forwarded tokens that are not trained.batch/samples_per_row,batch/row_tokens_mean,batch/row_tokens_max: The number of samples per row, and the average and maximum number of forwarded tokens per row.batch/row_fill_frac: The fraction oftoken_budgetthe rows use.batch/row_imbalance: The ratio of the most expensive row’s attention cost to the average one. The process with the most expensive row stalls the others at the gradient all-reduce;1.0is perfect balance.batch/packing_ratio: The number of tokens before packing per forwarded token: the factor by which packing shrinks the forward,1.0when nothing is shared.batch/pad_frac: The fraction of padding added to make the rows the same length. It only costs transfer, it is removed before the forward.batch/dropped_oversize_count: The number of samples dropped for exceedingtoken_budget.
Performance metrics
perf/step_s: The wall time of a step, including the optimizer, the weight sync and the time waiting for samples.perf/fwd_bwd_s,perf/fwd_s: The time spent in the forward and backward passes of a step, and in its forward passes alone.perf/optimizer_s: The time spent in the optimizer step.perf/rollout_wait_s: The time training waited because the rollout queue was empty. See The rollout queue.perf/weight_sync_s: The time of a weight sync, split intoperf/weight_sync_pause_s(waiting for vLLM to pause),perf/weight_sync_barrier_s(waiting for the other processes) andperf/weight_sync_transfer_s(sending the weights).perf/fwd_bwd_forwarded_tokens_per_s,perf/wall_clock_forwarded_tokens_per_s: The number of forwarded tokens per second, over the forward and backward passes or over the whole step.perf/wall_clock_trained_tokens_per_s: The number of trained tokens per second, over the whole step.perf/fwd_bwd_mfu,perf/wall_clock_mfu: The model FLOPs utilization of the training devices, over the forward and backward passes or over the whole step. They exclude the vLLM server. Logged only when the peak compute of the training device at the training precision is known, and when the FLOPs estimate supports the model’s architecture (such as Llama, Qwen2, Qwen3, Qwen3-MoE or Qwen3-VL).
Customization
Configuring the vLLM server
The trainer generates with a vLLM server, which it reaches at vllm_server_base_url (http://localhost:8000 by default):
VLLM_SERVER_DEV_MODE=1 vllm serve <model_name> \
--weight-transfer-config '{"backend": "nccl"}' \
--logprobs-mode processed_logprobs \
--api-server-count 8 \
--max-model-len 4096VLLM_SERVER_DEV_MODE=1and--weight-transfer-configexpose the endpoints the trainer sends its weights through.--logprobs-mode processed_logprobsmakes the server return the log-probabilities of the distribution it sampled from, after temperature and top-k/top-p, which is what the importance ratio compares against.--api-server-count 8keeps the top log-probabilities that score centering requests cheap to serve (see the warning below).--max-model-lenis the maximum total sequence length (prompt and completion) you expect. A lower value reduces GPU memory usage on the server, freeing more memory for the KV cache and increasing throughput. A good starting point is the prompt length plusmax_completion_lengthfrom your config. With tools, every turn adds up tomax_completion_lengthtokens plus the tool results, so set it to the longest conversation you expect. If a turn’s prompt plusmax_completion_lengthis longer than--max-model-len, the server rejects the request and the run stops with the server’s error. Withtop_kortop_p, the server instead stops that turn when it reaches--max-model-len, and only rejects a prompt that is already that long. It is also the defaulttoken_budget.
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_DEVICESenvironment variable.
Serializing the top log-probabilities is CPU work in vLLM’s API server, and with its default single API server
num_sampler_logprobs=8halves generation throughput. Start the server with--api-server-count 8on vLLM 0.32 or later: measured on one H100 with 1.5B to 7B models, the top-8 log-probabilities then cost under 3% of generation throughput. On vLLM 0.31 the same flag still leaves a 20 to 40% loss, fixed upstream by vllm-project/vllm#58181.
Concurrency
Three limits decide how many rollouts run at once. The first two adapt on their own; the third is yours to set.
Staleness. max_staleness sets how many training steps generation may run ahead of the trainer. The worker tracks this in tokens, because the number of samples in a step changes as completions get longer or shorter. After each step, the trainer counts the tokens it used, and lets the worker keep up to max(max_staleness, 1) times that many tokens queued or in progress. Before the first step, it assumes every row is full. The worker starts a new group of rollouts only while it is under that limit. At the very start it doesn’t know yet how many tokens a rollout produces, so it starts one group at a time until the first group finishes. A sample that still ends up more than max_staleness steps old is dropped.
vLLM load. Sending vLLM too many requests at once makes it slower: the KV cache fills up, and vLLM has to pause and redo some requests. So the worker also keeps a live limit on how many rollouts run at once, logged as rollout/concurrency_cap, and checks the server’s /metrics every 5 seconds to adjust it. The limit starts at the number of requests that fit in the KV cache at full length (KV cache tokens / max_model_len), so the server isn’t overloaded at the start. While KV-cache usage stays below 60% and no requests are waiting, the limit goes up by 25%, at most once per cap’s worth of finished rollouts. When usage goes above 80%, it goes down by 30%: running requests keep going, and new ones wait. If the server has no /metrics endpoint, the limit is max_concurrent_rollouts.
Resources. max_concurrent_rollouts (512 by default) is the upper limit on rollouts running at once. It’s the one limit you set yourself, because the trainer can’t see what your environments and tools can handle. Each running rollout uses an environment, can run synchronous tool calls on a thread pool of this size, and keeps a connection open to the vLLM server. Set it to what those can handle, for example the number of sessions your environment server accepts:
from trl.experimental.async_grpo import AsyncGRPOConfig
training_args = AsyncGRPOConfig(
...,
max_concurrent_rollouts=64,
)If it’s too low, generation slows down. A rollout keeps its slot while it runs tools or waits between turns, not only while vLLM generates, so rollouts that call many tools need more slots to keep vLLM busy. If it’s much higher than the default, the vLLM server can start dropping connections.
The logged metrics show which limit is holding generation back:
rollout/backpressure_sabove zero: the staleness limit. Generation is ahead of training, which is expected when inference is faster than training.rollout/concurrencyatrollout/concurrency_cap, belowmax_concurrent_rollouts: the vLLM server’s load.rollout/concurrencyatmax_concurrent_rollouts, with lowrollout/vllm_kv_cache_usage_fracand highperf/rollout_wait_s: the ceiling. Raisemax_concurrent_rolloutsif your environments and tools allow it.
The rollout queue
The worker pushes scored samples into a queue and the trainer pulls from it. Four metrics describe that queue:
rollout/queue_size: how many samples are waiting.sample/time_in_queue_s: how long a single sample waited before being trained on, the time part of its staleness.perf/rollout_wait_s: how long training waited because the queue was empty.rollout/backpressure_s: how long generation waited because it wasmax_stalenesssteps ahead of training.
The last two are mirror images. Read together with the queue size, they tell you which side is the bottleneck:
- queue near empty,
perf/rollout_wait_shigh: generation-bound. The trainer is starving; look atrollout/generated_tokens_per_s,rollout/concurrencyandrollout/score_queue_size. - queue holding about
max_stalenesssteps of tokens,rollout/backpressure_shigh: trainer-bound. Generation is held back, andsample/staleness_meansits near its bound. - both high: the
max_stalenesslimit itself is too tight. The rollouts in flight fill it before their samples reach the queue, so the trainer waits while generation is held back. Raisemax_staleness. - both near zero: balanced.
Saturation and duplicates
The policy learns from the differences between the rollouts of a group: each advantage is taken against the group’s mean reward. When every rollout of a group gets the same reward, they all get a zero advantage, so the group is dropped (see Logged metrics) and teaches the policy nothing. As a run saturates, more and more groups end up like this, either because every rollout solves the prompt or because none does. Four metrics tell these cases apart, and each one answers its own question, so you can read them straight off their plots:
| metric | question |
|---|---|
frac_reward_zero_std | what share of groups carry no signal |
reward_dropped | where those groups are stuck: their mean reward. With 0/1 rewards it is the share of them in which every rollout passed, so 0.9 means that almost all of them are solved, and 0.1 that almost all of them fail every rollout |
reward | how well the policy does over every scored group, dropped ones included |
completions/duplicate_frac | whether sampling still gives different rollouts for the same prompt |
Read the first three together, against the range of your reward:
frac_reward_zero_std | reward_dropped | reward | diagnosis | what to do |
|---|---|---|---|---|
| low | any (noisy over few groups) | any | healthy: most groups produce a gradient | nothing |
| climbing | near the top of the range | climbing | saturating at the top: the prompts have become too easy for the policy, which is how a successful run usually ends | stop the run, or move to harder prompts |
| high | near the bottom of the range | low and flat | stuck at the bottom: the prompts are too hard, or the reward function never gives credit | check completions/clipped_ratio first, since a completion cut off before its answer scores 0, then read a few completions to check that the reward function parses them |
| climbing | falling | falling | regression: groups that used to pass now fail together | check completions/duplicate_frac and entropy for a collapse |
| high | in the middle | any | a mix: with 0/1 rewards, some prompts are too easy and others too hard. With partial-credit rewards, groups can also be stuck on the same partial score | read the rollouts |
completions/duplicate_frac is the share of rollouts that repeat an earlier rollout of their group token for token, across all of their turns. Identical rollouts get identical rewards, so duplicates also push frac_reward_zero_std up.
completions/duplicate_frac | diagnosis |
|---|---|
| near 0, with a small baseline on tasks whose completions are short enough to repeat by chance | healthy: the rollouts of a group are sampled independently |
jumps towards (num_generations - 1) / num_generations | sampling is no longer random: every group comes back as copies of one rollout. For example, a temperature of 0, or data-parallel vLLM engines decoding the same text because they share a sampling seed (the worker sends a unique seed with every request to prevent this) |
climbs slowly, with entropy falling | collapse: the policy is converging onto one answer per prompt |
These metrics are recorded when the worker scores a group, not when its samples are trained on, so they run slightly ahead of training and include groups whose samples are later dropped for exceeding max_staleness. A group in which no rollout could be scored, because every reward function returned None, is dropped and counted in frac_reward_zero_std, but it has no reward, so reward_dropped leaves it out.
Tree packing
The samples of a micro-batch are packed into one row per process, without padding. 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. The rollouts of a group all repeat its prompt, and every turn of a multi-turn rollout repeats the conversation so far, so the forward shrinks by the packing ratio (batch/packing_ratio). 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.
On two rollouts of one prompt, where p is the prompt and a, b the two completions:
1. Rollouts r0: p1 p2 p3 a1 a2 10 tokens
r1: p1 p2 p3 b1 b2
2. Trie S0 [p1 p2] ─┬─ S1 [p3 a1 a2] p3 is not shared: a position predicts
└─ S2 [p3 b1 b2] one token, and p3 predicts a1 or b1
3. Packed row index 0 1 | 2 3 4 | 5 6 7
(depth-first) input_ids p1 p2 | p3 a1 a2 | p3 b1 b2 8 tokens: everything but attention
position_ids 0 1 | 2 3 4 | 2 3 4 runs on 8 instead of 10
segment S0 | S1 | S2
4. Attention segment queries keys and values: the segment's path in the trie
(one call) S0 p1 p2 p1 p2 index 0 1
S1 p3 a1 a2 p1 p2 p3 a1 a2 index 0 1 2 3 4
S2 p3 b1 b2 p1 p2 p3 b1 b2 index 0 1 5 6 7
5. Loss r0: a1 a2 predicted at index 2 3 each trained token is predicted at the index
r1: b1 b2 predicted at index 5 6 of the token before it in its rolloutA few consequences:
- Any common prefix is shared: the rollouts of a prompt, and across prompts a common system prompt or tool schema. A sample goes to the row that already holds its group, where its prompt already is. When that row is full, the sample goes to another row if a process still has an empty one, 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 its own segment, which is ordinary padding-free 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 grouped-query attention.
- Gated DeltaNet hybrids are supported (Qwen3.5, Qwen3.6, Qwen3-Next, …): their convolution reads each token’s own predecessors, and their chunked delta rule starts each segment from its parent’s final state.
token_budget bounds the tokens of a row, counting a shared token once. By default, it is the server’s max_model_len, so no sample can exceed it. With nothing shared, tree packing runs at about the speed of plain padding-free packing, and the speed-up grows roughly with the packing ratio (about 2.8 to 3x at a ratio of 3.4 on Qwen3-0.6B and 1.7B, on H100). The ratio is shared prefix times samples per row, so batch/samples_per_row is the leading indicator: near 1, nothing can be shared, and the lever is token_budget. batch/row_fill_frac matters when samples are long: a 10k-token sample tiles a 32k budget badly (three fit, four never do, so the planner often gets two and the row runs about 77% full), while 1k-token samples tile it almost perfectly.
Judge tree packing 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/fwd_bwd_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.
The batch metrics compose: one step holds gradient_accumulation_steps × num_processes rows, so batch/samples_per_step is about that many times batch/samples_per_row, and likewise for tokens. The two sides agree only up to the variation between the micro-batches of a step, since the per-step metrics are sums and the per-row ones are means.
Anything that makes rows longer or fewer removes the sharing. A multi-turn conversation that never forks is one long sample, and
rollout/samples_per_rolloutsays how many samples a conversation gives.
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 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:
VLLM_SERVER_DEV_MODE=1 vllm serve <model_name> \
--weight-transfer-config '{"backend": "nccl"}' \
--logprobs-mode processed_logprobs \
--api-server-count 8 \
--max-model-len 4096 \
--return-sampling-mask \
--enable-scale-outfrom trl.experimental.async_grpo import AsyncGRPOConfig
training_args = 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), and --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.
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 model.
from datasets import load_dataset
from peft import LoraConfig
from trl.experimental.async_grpo import AsyncGRPOTrainer
from trl.rewards import accuracy_reward
if __name__ == "__main__":
dataset = load_dataset("trl-lib/DeepMath-103K", split="train")
trainer = AsyncGRPOTrainer(
model="Qwen/Qwen3.5-2B",
reward_funcs=accuracy_reward,
train_dataset=dataset,
peft_config=LoraConfig(r=32, lora_alpha=64, target_modules="all-linear"),
)
trainer.train()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:
VLLM_SERVER_DEV_MODE=1 VLLM_ALLOW_RUNTIME_LORA_UPDATING=1 vllm serve <model_name> \
--weight-transfer-config '{"backend": "nccl"}' \
--logprobs-mode processed_logprobs \
--api-server-count 8 \
--max-model-len 4096 \
--enable-lora \
--max-lora-rank 32 \
--max-loras 6VLLM_ALLOW_RUNTIME_LORA_UPDATING=1 exposes the endpoint the trainer posts each new adapter to. Keep --weight-transfer-config even with --enable-lora: the trainer only chooses a sync mode when it starts, by which point the server is already up, and merged sync is the fallback.
--max-lora-rank must be one of 1, 8, 16, 32, 64, 128, 256, 320, 512. It sets the highest rank the server can serve rather than the rank it will serve, so an r=4 adapter works fine under 8. --max-loras must be at least max_staleness + 2: the trainer keeps max_staleness + 1 adapter versions registered so a rollout that started under an older policy can finish under it instead of switching policies mid-generation, and each sync loads the next version before it unloads the oldest. The trainer checks both values when it starts and tells you what to restart the server with.
vLLM loads adapters from disk rather than over the network, so the trainer writes each version under <output_dir>/.vllm_lora/ and gives the server the path. If the server runs on a different machine, that directory has to be on a filesystem both of them can see.
<output_dir>/.vllm_lora/is a serving cache, not where your trained adapter is kept. It only holds the most recent versions, and each one is deleted shortly after it falls out of the staleness window. Checkpoints are what preserve the adapter you trained, so do not turn them off: withsave_strategy="no"a multi-hour run finishes and leaves nothing behind.output_diralso has to be readable by the server and has to outlive the job, so node-local scratch will not work, even though it would keep syncs off a slow shared filesystem.
Adapter sync is not always faster. vLLM generates roughly 1.33 to 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.
Using a custom reward function
The experimental.async_grpo.AsyncGRPOTrainer supports using custom reward functions. 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 reward functions, they run concurrently: asynchronous ones are awaited together and synchronous ones run in threads, so their latency overlaps.
Input arguments:
The function must accept the following as keyword arguments:
prompts(contains the prompts),completions(contains the generated completions),completion_ids(contains the tokenized completions),- All column names (but
prompt) that the dataset may have. For example, if the dataset contains a column namedground_truth, the function will be called withground_truthas a keyword argument.
The easiest way to comply with this requirement is to use
**kwargsin the function signature.The dataset is in conversational format, so
promptsandcompletionsare lists of message dictionaries.
Return value: The function must return a list of floats. Each float represents the reward corresponding to a single completion.
Reward functions run in the rollout worker, a separate process, so everything passed to it is pickled. Each reward function, tool, and
environment_factory(and anything they close over) must be picklable: use a module-level function,functools.partial, or a callable class instance. Lambdas and closures raise aTypeErrorattrainer.train(). This is a difference from GRPOTrainer, where reward functions are called in-process and closures work.The rollout process also runs with
CUDA_VISIBLE_DEVICES="", so it cannot use the GPU. A GPU-backed reward model (e.g. anAutoModelForSequenceClassificationscorer) loads without error but silently falls back to the CPU. Keep reward functions lightweight (verifiers likeaccuracy_reward, format or length checks). If you need a GPU reward model, serve it behind its own inference engine (vLLM, TGI, …) on separate GPUs and have a picklable reward function call it over HTTP.
Example 1: Reward longer completions
Below is an example of a reward function that rewards longer completions:
def reward_func(completion_ids, **kwargs):
"""Reward function that assigns higher scores to longer completions (in terms of token count)."""
return [float(len(ids)) for ids in completion_ids]You can test it as follows:
>>> prompts = [[{"role": "user", "content": "The sky is"}], [{"role": "user", "content": "The sun is"}]] # not used in the reward function, but the trainer will pass it
>>> completions = [[{"role": "assistant", "content": " blue."}], [{"role": "assistant", "content": " 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 2: Reward completions with a specific format
Below is an example of a reward function that checks if the completion has a specific format. This example is inspired by the format reward function used in the paper DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning. It is designed for a conversational format, where prompts and completions consist of structured messages.
import re
def format_reward_func(completions, **kwargs):
"""Reward function that checks if the completion has a specific format."""
pattern = r"^<think>.*?</think><answer>.*?</answer>$"
completion_contents = [completion[0]["content"] for completion in completions]
matches = [re.match(pattern, content) for content in completion_contents]
return [1.0 if match else 0.0 for match in matches]You can test this function as follows:
>>> prompts = [
... [{"role": "user", "content": "What is the result of (1 + 2) * 4?"}],
... [{"role": "user", "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. The dataset contains a column named ground_truth.
import re
def reward_func(completions, ground_truth, **kwargs):
# Regular expression to capture content inside \boxed{}
matches = [re.search(r"\\boxed\{(.*?)\}", completion[0]["content"]) for completion in completions]
contents = [match.group(1) if match else "" for match in matches]
# Reward 1 if the content is the same as the ground truth, 0 otherwise
return [1.0 if c == gt else 0.0 for c, gt in zip(contents, ground_truth)]You can test this function as follows:
>>> prompts = [
... [{"role": "user", "content": "Solve the equation $2x + 3 = 7$."}],
... [{"role": "user", "content": "Solve the equation $3x - 5 = 10$."}],
... ]
>>> completions = [
... [{"role": "assistant", "content": r"The solution is \boxed{2}."}],
... [{"role": "assistant", "content": 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 experimental.async_grpo.AsyncGRPOTrainer. 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.
from datasets import Dataset
from trl.experimental.async_grpo import AsyncGRPOTrainer
# Define a dataset that contains both math and coding problems
dataset = Dataset.from_list(
[
{"prompt": [{"role": "user", "content": "What is 2+2?"}], "task": "math"},
{"prompt": [{"role": "user", "content": "Write a function that returns the sum of two numbers."}], "task": "code"},
{"prompt": [{"role": "user", "content": "What is 3*4?"}], "task": "math"},
{"prompt": [{"role": "user", "content": "Write a function that returns the product of two numbers."}], "task": "code"},
]
)
# Math-specific reward function
def math_reward_func(prompts, completions, task, **kwargs):
rewards = []
for prompt, completion, t in zip(prompts, completions, task):
if t == "math":
# Calculate math-specific reward
correct = check_math_solution(prompt, completion)
reward = 1.0 if correct else -1.0
rewards.append(reward)
else:
# Return None for non-math tasks
rewards.append(None)
return rewards
# Coding-specific reward function
def coding_reward_func(prompts, completions, task, **kwargs):
rewards = []
for prompt, completion, t in zip(prompts, completions, task):
if t == "code":
# Calculate coding-specific reward
works = test_code_solution(prompt, completion)
reward = 1.0 if works else -1.0
rewards.append(reward)
else:
# Return None for non-coding tasks
rewards.append(None)
return rewards
# Use both task-specific reward functions
if __name__ == "__main__":
trainer = AsyncGRPOTrainer(
model="Qwen/Qwen2.5-0.5B-Instruct",
reward_funcs=[math_reward_func, coding_reward_func],
train_dataset=dataset,
)
trainer.train()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 experimental.async_grpo.AsyncGRPOTrainer will continue with the valid functions and tasks. This allows the experimental.async_grpo.AsyncGRPOTrainer to handle multiple reward functions with different applicability.
Note that the experimental.async_grpo.AsyncGRPOTrainer 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, experimental.async_grpo.AsyncGRPOTrainer executes them concurrently so their latency overlaps.
Below is a minimal example of an async reward function that simulates an I/O-bound operation:
import asyncio
async def async_reward_func(prompts, completions, **kwargs):
# Simulate an I/O-bound call (e.g., HTTP request, database lookup)
await asyncio.sleep(0.01)
# Simple toy reward: 1.0 if the completion is non-empty, else 0.0
return [1.0 if completion else 0.0 for completion in completions]Passing the reward function to the trainer
To use your custom reward function, pass it to the experimental.async_grpo.AsyncGRPOTrainer as follows:
from trl.experimental.async_grpo import AsyncGRPOTrainer
trainer = AsyncGRPOTrainer(
reward_funcs=reward_func,
...,
)You can pass several reward functions as a list; this list may include both synchronous and asynchronous functions:
from trl.experimental.async_grpo import AsyncGRPOTrainer
trainer = AsyncGRPOTrainer(
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.
Customizing the rollout loop
Generation, rewards, advantages and filtering run in the rollout process, in experimental.async_grpo.AsyncRolloutLoop. To change one of them, subclass the loop, override the method for that step, and set the subclass as loop_cls on a trainer subclass. The trainer’s constructor doesn’t change.
| Method | Called | Default |
|---|---|---|
filter_prompt(row_index, row, epoch) | once per prompt, before generation | admits every prompt |
generate_one(prompt, tools, group_id) | once per rollout | the multi-turn tool loop |
generate_one_turn(prompt_ids) | once per sampling request | one vLLM request with the config’s sampling settings |
compute_advantages(group) | once per group, after the rewards are summed into group.rewards | reward standardized within the group |
setup(), close() | when the loops start and stop | nothing |
For example, a curriculum that stops sampling prompts the policy already solves, with its threshold in a config subclass:
from dataclasses import dataclass
import numpy as np
from trl.experimental.async_grpo import AsyncGRPOConfig, AsyncGRPOTrainer, AsyncRolloutLoop
@dataclass
class CurriculumConfig(AsyncGRPOConfig):
pass_rate_cap: float = 0.9
class CurriculumRolloutLoop(AsyncRolloutLoop):
def filter_prompt(self, row_index, row, epoch):
return self.pass_rates.get(row_index, 0.0) < self.config.pass_rate_cap
async def setup(self):
self.pass_rates = {}
def compute_advantages(self, group):
scored = group.rewards[~np.isnan(group.rewards)]
if len(scored):
self.pass_rates[group.row_index] = float(np.mean(scored > 0))
return super().compute_advantages(group)
class CurriculumTrainer(AsyncGRPOTrainer):
loop_cls = CurriculumRolloutLoopRewards are summed over reward_funcs, environments that define get_reward, and the reward a custom generate_one sets on its experimental.async_grpo.Rollout: to score with a judge, pass an async reward function, or score inside generate_one. compute_advantages runs for every group, including one that is then dropped because all its scored rewards are equal; the drop looks at the rewards, not at the advantages. The loop is built in a spawned process, so define the subclass at module level and keep the training code under if __name__ == "__main__":. Create clients in setup, which runs on the loop’s event loop, rather than overriding __init__. A prompt that filter_prompt rejects is skipped for that epoch and counts toward it, like a dropped group. If it rejects every prompt of an epoch, the loop raises.
Resuming from a checkpoint
The base Trainer resumes by skipping and replaying the batches it already trained on, which does not apply to a live rollout queue, so ignore_data_skip is always True. 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, so their worker restarts from the first prompt on resume.
Agent Training
Asynchronous GRPO supports agent training: the model calls tools during generation and learns from the outcome.
- A tool is a plain Python function (sync or async) exposed to the model. Use
toolsfor stateless calls (a calculator, a web search). - An environment is the more general form: a stateful object built fresh per rollout, whose public methods are exposed as tools, plus a
resetlifecycle hook and an optionalget_rewardthat lets it own the reward. Useenvironment_factorywhen you need per-rollout state, a reset hook, or environment-owned reward.
They compose — you can pass standalone tools alongside an environment_factory.
Tools
The tools argument expects a list of Python functions (sync or async) that define the tools available to the agent:
from trl.experimental.async_grpo import AsyncGRPOTrainer
trainer = AsyncGRPOTrainer(
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.
The model calls a tool by its name, so each tool is registered under its __name__ and must be a module-level function, which also keeps it picklable for the rollout worker. Synchronous tools run on a thread pool sized to max_concurrent_rollouts, so a slow tool never blocks the other rollouts in flight. Within one assistant turn, tool calls run in the order the model emitted them. Up to max_concurrent_rollouts synchronous tools can therefore run at once, so a tool that reads or writes shared state must be thread-safe.
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 experimental.async_grpo.AsyncGRPOConfig to cap the number of tool-calling turns. By default there is no limit, and generation stops when the model produces a response turn with no tool calls.
Example:
from trl.experimental.async_grpo import AsyncGRPOTrainer
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 = AsyncGRPOTrainer(
tools=[multiply, async_add],
...,
)Environments
You can also provide tools through environment_factory. In this mode, experimental.async_grpo.AsyncGRPOTrainer creates one environment instance per rollout and exposes the environment’s public methods as tools.
When you use an environment_factory, the environment owns the data: there is no external train_dataset. On each rollout, reset() produces the task — self-sampling from a corpus the environment holds, or generating a state procedurally — and returns the prompt. max_steps sets the training length. The following is a minimal example of an environment that self-samples a target and exposes an increment method (exposed as a tool).
import random
from trl.experimental.async_grpo import AsyncGRPOConfig, AsyncGRPOTrainer
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
if __name__ == "__main__":
trainer = AsyncGRPOTrainer(
model="Qwen/Qwen3-0.6B",
args=AsyncGRPOConfig(max_steps=1000, chat_template_kwargs={"enable_thinking": False}),
environment_factory=IncrementEnv,
)
trainer.train()An environment class has two reserved methods: reset and get_reward. Unlike any other public methods, these are not exposed to the model as tools.
reset(required) is called when the rollout starts. It can return eitherNoneor a string. When it returns a string, that string is the user prompt.get_reward(optional, sync or async) takes no argument and returns afloat: the environment scores the episode it just ran from its own internal state (did the game end in a win? was the word guessed?). It is called once per completed rollout and acts as a reward source.
Both run on the worker’s tool thread pool, so a slow environment delays its own rollout but not the others in flight. Their durations are logged as env/reset_s and env/reward_s.
Because the environment self-samples on each
reset(), theGmembers of a GRPO group may not share the same initial state, making the group baseline slightly noisier. For identical states within a group, havereset()derive the state deterministically from a shared key.
Providing an external dataset (optional)
Instead of letting the environment self-sample, you can provide an external train_dataset. For example when your tasks already live in a dataset, or to route between multiple environments. Per rollout, the trainer samples one row, uses its "prompt" column as the prompt, and passes the row’s columns to reset() as keyword arguments (so the environment reads the task from the dataset).
from datasets import Dataset
from trl.experimental.async_grpo import AsyncGRPOTrainer
# 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
if __name__ == "__main__":
trainer = AsyncGRPOTrainer(
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 with weight 1.
def format_reward(completions, **kwargs): # trainer-owned: scores the completion text
...
trainer = AsyncGRPOTrainer(
model="Qwen/Qwen3-0.6B",
args=AsyncGRPOConfig(max_steps=1000),
reward_funcs=format_reward, # trainer-owned: scores the completion
environment_factory=IncrementEnv, # env-owned: scores its internal state via `get_reward`
)Multiple environments
To train on a mix of tasks in a single run (e.g. a coding task and a game), pass a dictionary mapping environment names to factories instead of a single callable. Each rollout selects its environment through an environment field in the dataset, and only that environment’s tools are exposed. This avoids leaking irrelevant tools (the game’s move tool to a coding example, and vice versa). Each environment still owns its data and returns the prompt from reset(), so the dataset only needs the environment column to route between them.
import random
from datasets import Dataset
from trl.experimental.async_grpo import AsyncGRPOConfig, AsyncGRPOTrainer
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
if __name__ == "__main__":
dataset = Dataset.from_dict({"environment": ["coding", "game"] * 500})
trainer = AsyncGRPOTrainer(
model="Qwen/Qwen3-0.6B",
args=AsyncGRPOConfig(chat_template_kwargs={"enable_thinking": False}),
train_dataset=dataset,
reward_funcs=reward_func,
environment_factory={"coding": CodingEnv, "game": GameEnv},
)
trainer.train()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.
Multi-turn rollouts
Every turn renders the whole conversation through the chat template and generates from it. A chat template does not always render a growing conversation the same way (some drop the reasoning of earlier turns, for example), so a turn’s prompt tokens can differ from the tokens held so far. At the end of the rollout, the worker reconciles the turns into training samples: a turn whose prompt starts with the held tokens is appended to the same sample, and any other turn starts a new one. Every generated token is trained in exactly one sample, so a fork costs forward tokens but no training signal.
Every sample of a rollout gets the rollout’s advantage. rollout/fork_frac and rollout/drift_tokens_* show how the turns were reconciled.
Environment Integrations
To train agents that run inside their own harness, such as coding agents in Harbor sandboxes, see Training on harnesses in the OpenEnv guide.
Training 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.
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 model (vision tower, multimodal projector) has requires_grad=False: it gets no gradients and no optimizer state, and weight sync skips it, so the server keeps the values it loaded from the checkpoint. The tower is still loaded, so it costs GPU memory for its weights.
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.
AsyncGRPOTrainer
class trl.experimental.async_grpo.AsyncGRPOTrainer
< source >( model: str | PreTrainedModelreward_funcs: collections.abc.Callable[..., list[float]] | list[collections.abc.Callable[..., list[float]]] | None = Noneargs: trl.experimental.async_grpo.async_grpo_config.AsyncGRPOConfig | None = Nonetrain_dataset: datasets.arrow_dataset.Dataset | datasets.iterable_dataset.IterableDataset | None = Noneprocessing_class: transformers.tokenization_utils_base.PreTrainedTokenizerBase | None = Nonecallbacks: list[transformers.trainer_callback.TrainerCallback] | None = Noneoptimizers: tuple[typing.Optional[torch.optim.Optimizer], typing.Optional[torch.optim.lr_scheduler.LambdaLR]] = (None, None)peft_config: PeftConfig | None = Nonetools: list[collections.abc.Callable] | None = Noneenvironment_factory: collections.abc.Callable[[], trl.experimental.async_grpo.async_grpo_trainer._SupportsReset] | dict[str, collections.abc.Callable[[], trl.experimental.async_grpo.async_grpo_trainer._SupportsReset]] | None = None )
Parameters
- model (
stror PreTrainedModel) — Model to be trained. Can be either:- A string, being the model id of a pretrained model hosted inside a model repo on huggingface.co, or a
path to a directory containing model weights saved using
save_pretrained, e.g.,
'./my_model_directory/'. The model is loaded with the architecture declared in its config: AutoModelForImageTextToText for a vision-language checkpoint (whose vision tower is loaded but frozen, see Training Vision Language Models), AutoModelForCausalLM otherwise. - A PreTrainedModel object. Its attention implementation is switched to the trainer’s tree attention backend for packed training.
The model name from the config is used to identify the model on the vLLM server used for generation.
- A string, being the model id of a pretrained model hosted inside a model repo on huggingface.co, or a
path to a directory containing model weights saved using
save_pretrained, e.g.,
- reward_funcs (
RewardFunc | list[RewardFunc], optional) — Reward functions to be used for computing the rewards. To compute the rewards, we call all the reward functions with the prompts and completions and sum the rewards. May be omitted when the reward is supplied by the environment throughenvironment_factory(see below). Can be either:- A single reward function: The function is provided with the prompts and the generated completions, plus
any additional columns in the dataset. It should return a list of rewards. Reward functions can be either
synchronous or asynchronous and can also return
Nonewhen the reward is not applicable to those samples. This is useful for multi-task training where different reward functions apply to different types of samples. When a reward function returnsNonefor a sample, that reward function is excluded from the reward calculation for that sample. For more details, see Using a custom reward function. - A list of reward functions, where each item is a reward function as described above. Rewards from all functions are summed.
Unlike GRPOTrainer, rewards are computed in a spawned child process, so each reward function (along with
toolsandenvironment_factory) must be picklable: use a module-level function,functools.partial, or a callable class instance — lambdas and closures will fail at startup. Tools are registered under their__name__, so they must be module-level functions. The child process also runs withCUDA_VISIBLE_DEVICES="", so a GPU-backed reward model runs on CPU (slow), not the trainer’s GPU. - A single reward function: The function is provided with the prompts and the generated completions, plus
any additional columns in the dataset. It should return a list of rewards. Reward functions can be either
synchronous or asynchronous and can also return
- args (experimental.async_grpo.AsyncGRPOConfig, optional) —
Configuration for this trainer. If
None, a default configuration is used. - train_dataset (Dataset or IterableDataset, optional) —
Dataset to use for training. It must include a column
"prompt". Any additional columns in the dataset are ignored. The samples must be in conversational format: each sample contains structured messages (e.g., role and content).May be omitted only when an
environment_factoryis provided and the environment owns (or procedurally generates) the data, returning the prompt from itsreset()method. In that case,max_stepsmust be set to define the training length. - processing_class (PreTrainedTokenizerBase, optional) —
Processing class used to process the data. The padding side must be set to “left”. If
None, the processing class is loaded from the model’s name with from_pretrained. A padding token,tokenizer.pad_token, must be set. If the processing class has not set a padding token,tokenizer.eos_tokenwill be used as the default. - callbacks (list of TrainerCallback, optional) —
List of callbacks to customize the training loop. Will add those to the list of default callbacks detailed
in here.
If you want to remove one of the default callbacks used, use the remove_callback method.
- optimizers (
tuple[torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None], optional, defaults to(None, None)) — A tuple containing the optimizer and the scheduler to use. Will default to an instance ofAdamWon your model and a scheduler given by get_linear_schedule_with_warmup controlled byargs. - peft_config (PeftConfig, optional) —
PEFT configuration used to wrap the model. If
None, the model is not wrapped. When the vLLM server was started with--enable-lora, a LoRA adapter is synced to it as an adapter rather than as merged weights; see the LoRA section of the AsyncGRPO documentation. - tools (list of
Callable, optional) — A list of callable tool functions (sync or async) that the model can invoke during generation. Each tool should be a standard Python function with properly type-hinted arguments and return values, and a Google-style docstring describing its purpose, arguments, and return value. For more details, see: https://huggingface.co/docs/transformers/en/chat_extras#passing-tools. The model uses the function’s name, type hints, and docstring to determine how to call it. Ensure that the model’s chat template supports tool use and that it has been fine-tuned for tool calling. - environment_factory (
EnvironmentFactoryordict[str, EnvironmentFactory], optional) — A callable that creates and returns an environment instance, or a dictionary mapping environment names to such callables. The environment class should define methods that can be invoked as tools during generation. Each method should comply with the same requirements as thetoolsdescribed above. The environment must also implement a callableresetmethod that can be used to reset state between generations. Theresetmethod should return eitherNoneor a string: when it returns a string, that string is appended to the last user message before generation. The environment may also define aget_rewardmethod taking no argument and returning afloat: when present, the environment owns the reward, andget_rewardis called once per completed rollout to score it from the environment’s internal state. It acts as an additional reward source (with weight 1, logged under the environment’s class name) alongsidereward_funcs, which then becomes optional.With a single callable, every example uses the same environment, with one instance per rollout so their interactions stay isolated. With a dictionary, each example must carry an
environmentfield selecting its environment by name, and only that environment’s tools are exposed in its prompt — letting a single run mix tasks (e.g. a coding environment and a game). This feature is experimental and may change or be removed at any time without prior notice.
Trainer for the Group Relative Policy Optimization (GRPO) method. This algorithm was initially proposed in the paper DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models. This trainer is the asynchronous version of GRPO, where generation is offloaded to an external vLLM server that runs asynchronously alongside training, decoupling rollout from the gradient update loop.
Example:
>>> from trl.experimental.async_grpo import AsyncGRPOTrainer
>>> from trl.rewards import accuracy_reward
>>> from datasets import load_dataset
>>> dataset = load_dataset("trl-lib/DeepMath-103K", split="train")
>>> trainer = AsyncGRPOTrainer(
... model="Qwen/Qwen2.5-0.5B-Instruct",
... reward_funcs=accuracy_reward,
... train_dataset=dataset,
... )
>>> trainer.train()To customize which prompts are sampled, how a rollout is generated or how advantages are computed, subclass experimental.async_grpo.AsyncRolloutLoop and set it as the trainer’s loop_cls. TRL builds the loop in the
rollout process:
>>> class CurriculumTrainer(AsyncGRPOTrainer):
... loop_cls = CurriculumRolloutLooptrain
< source >( resume_from_checkpoint: str | bool | None = Nonetrial: optuna.Trial | dict[str, Any] | None = Noneignore_keys_for_eval: list[str] | None = None ) → ~trainer_utils.TrainOutput
Parameters
- resume_from_checkpoint (
strorbool, optional) — If astr, local path to a saved checkpoint as saved by a previous instance ofTrainer. If abooland equalsTrue, load the last checkpoint in args.output_dir as saved by a previous instance ofTrainer. If present, training will resume from the model/optimizer/scheduler states loaded here. - trial (
optuna.Trialordict[str, Any], optional) — The trial run or the hyperparameter dictionary for hyperparameter search. - ignore_keys_for_eval (
list[str], optional) — A list of keys in the output of your model (if it is a dictionary) that should be ignored when gathering predictions for evaluation during the training.
Returns
~trainer_utils.TrainOutput
Object containing the global step count, training loss, and metrics.
Main training entry point.
Will save the model, so you can reload it using from_pretrained().
Will only save from the main process.
push_to_hub
< source >( commit_message: str | None = 'End of training'blocking: bool = Truetoken: str | None = Nonerevision: str | None = None**kwargs )
Parameters
- commit_message (
str, optional, defaults to"End of training") — Message to commit while pushing. - blocking (
bool, optional, defaults toTrue) — Whether the function should return only when thegit pushhas finished. - token (
str, optional, defaults toNone) — Token with write permission to overwrite Trainer’s original args. - revision (
str, optional) — The git revision to commit from. Defaults to the head of the “main” branch. - kwargs (
dict[str, Any], optional) — Additional keyword arguments passed along to~Trainer.create_model_card.
Upload self.model and self.processing_class to the 🤗 model hub on the repo self.args.hub_model_id.
AsyncGRPOConfig
class trl.experimental.async_grpo.AsyncGRPOConfig
< source >( output_dir: str | None = Noneper_device_train_batch_size: int = 8num_train_epochs: float = 3.0max_steps: int = -1learning_rate: float = 1e-06lr_scheduler_type: str = 'constant'lr_scheduler_kwargs: dict | str | None = Nonewarmup_steps: float = 0optim: transformers.training_args.OptimizerNames | str = 'adamw_torch_fused'optim_args: str | None = Noneweight_decay: float = 0.0adam_beta1: float = 0.9adam_beta2: float = 0.999adam_epsilon: float = 1e-08optim_target_modules: None | str | list[str] = Nonegradient_accumulation_steps: int = 1average_tokens_across_devices: bool = Truemax_grad_norm: float = 1.0label_smoothing_factor: float = 0.0bf16: bool | None = Nonefp16: bool = Falsebf16_full_eval: bool = Falsefp16_full_eval: bool = Falsetf32: bool | None = Nonegradient_checkpointing: bool = Truegradient_checkpointing_kwargs: dict[str, typing.Any] | str | None = Nonetorch_compile: bool = Falsetorch_compile_backend: str | None = Nonetorch_compile_mode: str | None = Noneuse_liger_kernel: bool = Falseliger_kernel_config: dict[str, bool] | None = Noneuse_cache: bool = Falseneftune_noise_alpha: float | None = Nonetorch_empty_cache_steps: int | None = Noneauto_find_batch_size: bool = Falselogging_strategy: transformers.trainer_utils.IntervalStrategy | str = 'steps'logging_steps: float = 1logging_first_step: bool = Falselog_on_each_node: bool = Truelogging_nan_inf_filter: bool = Trueinclude_num_input_tokens_seen: str | bool = 'no'log_level: str = 'passive'log_level_replica: str = 'warning'disable_tqdm: bool | None = Nonereport_to: None | str | list[str] = 'none'run_name: str | None = Noneproject: str = 'huggingface'trackio_space_id: str | None = Nonetrackio_bucket_id: str | None = Nonetrackio_static_space_id: typing.Union[str, NoneType, typing.Literal[False]] = Noneeval_strategy: transformers.trainer_utils.IntervalStrategy | str = 'no'eval_steps: float | None = Noneeval_delay: float = 0per_device_eval_batch_size: int = 8prediction_loss_only: bool = Falseeval_on_start: bool = Falseeval_do_concat_batches: bool = Trueeval_use_gather_object: bool = Falseeval_accumulation_steps: int | None = Noneinclude_for_metrics: list[str] = <factory>batch_eval_metrics: bool = Falsesave_only_model: bool = Falsesave_strategy: transformers.trainer_utils.SaveStrategy | str = 'steps'save_steps: float = 500save_on_each_node: bool = Falsesave_total_limit: int | None = Noneenable_jit_checkpoint: bool = Falsepush_to_hub: bool = Falsehub_token: str | None = Nonehub_private_repo: bool | None = Nonehub_model_id: str | None = Nonehub_strategy: transformers.trainer_utils.HubStrategy | str = 'every_save'hub_always_push: bool = Falsehub_revision: str | None = Noneload_best_model_at_end: bool = Falsemetric_for_best_model: str | None = Nonegreater_is_better: bool | None = Noneignore_data_skip: bool = Truerestore_callback_states_from_checkpoint: bool = Falsefull_determinism: bool = Falseseed: int = 42data_seed: int | None = Noneuse_cpu: bool = Falseaccelerator_config: dict | str | None = Noneparallelism_config: accelerate.parallelism_config.ParallelismConfig | None = Nonedataloader_drop_last: bool = Falsedataloader_num_workers: int = 0dataloader_pin_memory: bool = Truedataloader_persistent_workers: bool = Falsedataloader_prefetch_factor: int | None = Nonedataloader_multiprocessing_context: str | None = Nonedataloader_in_order: bool = Trueremove_unused_columns: bool = Falselabel_names: list[str] | None = Nonetrain_sampling_strategy: str = 'random'length_column_name: str = 'length'ddp_find_unused_parameters: bool | None = Noneddp_bucket_cap_mb: int | None = Noneddp_broadcast_buffers: bool | None = Noneddp_static_graph: bool | None = Noneddp_backend: str | None = Noneddp_timeout: int = 1800fsdp: str | None = Nonefsdp_config: dict[str, typing.Any] | str | None = Nonedeepspeed: dict | str | None = Nonedebug: str | list[transformers.debug_utils.DebugOption] = ''skip_memory_metrics: bool = Truedo_train: bool = Falsedo_eval: bool = Falsedo_predict: bool = Falseresume_from_checkpoint: str | None = Nonelocal_rank: int = -1model_init_kwargs: dict[str, typing.Any] | str | None = Nonedtype: str = 'float32'trust_remote_code: bool = Falserouter_aux_loss_coef: float | None = Nonenum_generations: int = 8max_completion_length: int = 2048temperature: float = 1.0top_p: float = 1.0top_k: int = 0min_p: float | None = Nonechat_template_kwargs: dict | str | None = Nonemax_tool_calling_iterations: int | None = Nonevllm_server_base_url: str = 'http://localhost:8000'vllm_server_timeout: float = 1800.0epsilon: float = 0.2epsilon_high: float | None = Nonescore_centering: bool = Truevllm_importance_sampling_clip_min: float = 0.5vllm_importance_sampling_clip_max: float = 5.0num_sampler_logprobs: int = 8token_budget: int | None = Nonemax_concurrent_rollouts: int = 512max_staleness: int = 4weight_sync_steps: int = 1rollout_worker_timeout: float = 300.0log_completions: bool = Falsenum_completions_to_print: int | None = None )
Parameters that control the model
- model_init_kwargs (
dict[str, Any]orstr, optional) — Keyword arguments for from_pretrained, used when instantiating the model from a path. Therevisionvalue is also used when loading the processing class. - dtype (
str, optional, defaults to"float32") — Data type to load the model under, one of"auto","bfloat16","float16"or"float32". It defaults to"float32"because the training-inference mismatch this trainer is measured against (Defeating the Training-Inference Mismatch via FP16) is sensitive to the trainer’s own precision. Closing that gap end to end also requires serving the vLLM server in the same dtype (vllm serve --dtype); a mismatch is logged as a warning at train start. Adtypeinmodel_init_kwargstakes precedence.This only sets the dtype the weights are loaded in. Under FSDP with mixed precision (
bf16=Trueis the default), accelerate upcasts every trainable parameter to fp32 before sharding, so the master weights and the optimizer state are fp32 whateverdtypesays, while the forward and backward run in bf16 or fp16. Checkpoints are then written in that mixed-precision dtype, not in fp32. Without FSDP nothing upcasts:dtype="bfloat16"withbf16=Truekeeps bf16 weights and optimizer states, which is pure bf16 training. - trust_remote_code (
bool, optional, defaults toFalse) — Whether to allow loading models and tokenizers that ship custom Python code from the Hub. Forwarded to from_pretrained and from_pretrained. - router_aux_loss_coef (
float, optional) — Coefficient of the load-balancing auxiliary loss for Mixture-of-Experts (MoE) models, added to the training loss with this weight. When not set, the value declared by the model config is used. Set to0.0to disable it. Fails when used with a non-MoE model.
Parameters that control generation
- num_generations (
int, optional, defaults to8) — Number of generations per prompt to sample. - max_completion_length (
int, optional, defaults to2048) — Maximum number of tokens generated per vLLM request, so per turn in a multi-turn rollout. - temperature (
float, optional, defaults to1.0) — Temperature for sampling. The higher the temperature, the more random the completions. - top_p (
float, optional, defaults to1.0) — Float that controls the cumulative probability of the top tokens to consider. Must be in (0, 1]. Set to 1.0 to consider all tokens. Below1.0,top_kmust be set too, and the vLLM server (>= 0.28) started with--return-sampling-mask --enable-scale-out: the trainer renormalizes its log-probabilities over the candidate set each token was sampled from, as the server does. An older vLLM keeps the whole vocabulary. - top_k (
int, optional, defaults to0) — Number of highest probability vocabulary tokens to keep for top-k-filtering. If0, top-k-filtering is disabled and all tokens are considered. Any other value needs the server flags described undertop_p. - min_p (
float, optional) — Minimum token probability, which will be scaled by the probability of the most likely token. It must be a value between 0.0 and 1.0. Typical values are in the 0.01-0.2 range. Above0.0,top_kmust be set too, with the server flags described undertop_p. - chat_template_kwargs (
dict[str, Any], optional) — Additional keyword arguments to pass to theapply_chat_templatefunction when generating completions. - max_tool_calling_iterations (
int, optional) — Maximum number of tool-calling turns when training an agent. IfNone, there is no limit and generation stops when the model generates a response turn with no tool calls.
Parameters that control the vLLM server
- vllm_server_base_url (
str, optional, defaults to"http --//localhost:8000"): Base URL of the vLLM server used for generation (e.g.,"http://localhost:8000"). - vllm_server_timeout (
float, optional, defaults to1800.0) — Timeout in seconds for calls to the vLLM server: waiting for it to be ready at startup, each generation request, and each weight-sync call. Waiting for the server and weight-sync calls raise when it runs out; a generation request that times out is retried.
Parameters that control the training
- epsilon (
float, optional, defaults to0.2) — Epsilon value for clipping. - epsilon_high (
float, optional) — Upper-bound epsilon value for clipping. If not specified, it defaults to the same value as the lower-bound specified in argumentepsilon. Paper DAPO recommends0.28. - score_centering (
bool, optional, defaults toTrue) — Whether to train with score centering (paper) instead of the clipped objective: the importance-weighted score of each sampled token minus its expectation under the vLLM sampler, so that a constant advantage does not pull the policy toward the sampler. The expectation is taken over thenum_sampler_logprobsmost likely tokens at each position, whose log-probabilities the sampler reports. - vllm_importance_sampling_clip_min (
float, optional, defaults to0.5) — Lower bound C_min of the importance ratio between the policy and the vLLM sampler. Tokens whose ratio falls outside [vllm_importance_sampling_clip_min,vllm_importance_sampling_clip_max] are dropped from the loss (masked importance sampling). - vllm_importance_sampling_clip_max (
float, optional, defaults to5.0) — Upper bound C_max of the importance ratio. - num_sampler_logprobs (
int, optional, defaults to8) — Number of top log-probabilities requested from the vLLM sampler for every generated token, used by score centering. More cover more of the sampler’s probability mass but cost more to serialize; the server’s--max-logprobsbounds it. - token_budget (
int, optional) — Maximum number of tokens forwarded in a single row (one DP rank’s forward). ATokenBudgetBatcherforms attention-cost-balanced micro-batches whose rows each stay within this budget, bounding peak memory independently of the sample count (the number of samples per row becomes dynamic). The budget bounds unique tokens rather than raw ones, so tokens shared by several samples count once, and it bounds the row’s loss terms too. IfNone(default), it is set to the vLLM server’smax_model_len(queried at train start) — the cap on prompt + completion length — so no rollout sample can ever exceed the budget. A sample longer thantoken_budgetfits in no row and is dropped with a warning.
Parameters that control the async rollout pipeline
- max_concurrent_rollouts (
int, optional, defaults to512) — Maximum number of rollouts running at once. Each one holds an environment, may run synchronous tool calls, and keeps a connection to the vLLM server, so set it to what those resources can take. Below it, the number of rollouts in flight follows the vLLM server’s KV-cache usage andmax_staleness. - max_staleness (
int, optional, defaults to4) — Maximum number of weight update steps a rollout sample can lag behind the current model version before being dropped. Generation is held to it: a new group of rollouts starts only while the tokens queued and in flight fit in what the trainer consumes inmax(max_staleness, 1)steps. - weight_sync_steps (
int, optional, defaults to1) — Number of training steps between weight synchronizations to the vLLM server. - rollout_worker_timeout (
float, optional, defaults to300.0) — Seconds since the rollout worker’s last heartbeat after which the trainer treats it as hung and aborts.
Parameters that control the logging
Configuration class for the experimental.async_grpo.AsyncGRPOTrainer.
This class includes only the parameters that are specific to asynchronous GRPO training. For a full list of training arguments, please refer to the TrainingArguments documentation. Note that default values in this class may differ from those in TrainingArguments.
These parameters have default values different from TrainingArguments:
logging_steps: Defaults to1instead of500.gradient_checkpointing: Defaults toTrueinstead ofFalse.bf16: Defaults toTrueiffp16is not set, instead ofFalse.learning_rate: Defaults to1e-6instead of5e-5.lr_scheduler_type: Defaults toconstantinstead oflinear(see below).ignore_data_skip: Defaults toTrueinstead ofFalse; the base Trainer’s skip-and-replay loop does not apply to the async rollout queue.remove_unused_columns: Defaults toFalseinstead ofTrue, and must stayFalse: every dataset column is passed to the reward functions.
Training duration and learning rate under message-mode reconciliation: A multi-turn conversation can fork into a variable number of training rows (a rewrite of the conversation starts a new row), so the number of samples, and therefore the number of optimizer steps, per epoch is not known up front. As a consequence:
num_train_epochsbounds training by full passes over the prompt dataset, counted as the number of distinct prompts actually trained on. This is independent of how many rows the forks produce, so requesting N epochs always trains on N passes over the data. Whenmax_stepsis left unset, this is the stop condition andmax_stepsis only a safety ceiling.max_steps, if set explicitly (> 0), takes over as the stop condition (bounding by optimizer steps rather than by epochs) and disables the epoch-based stop.lr_scheduler_typedefaults toconstantbecause a decay horizon is measured in optimizer steps, which cannot be known up front when the step count depends on the fork rate. For a decaying learning rate, set a decaying schedule together with an explicitmax_steps.
AsyncRolloutLoop
class trl.experimental.async_grpo.AsyncRolloutLoop
< source >( config: AsyncGRPOConfigmodel_name: strdataset: Datasetreward_funcs: list[collections.abc.Callable[..., list[float]]]processing_class: PreTrainedTokenizerBaseeos_token_ids: list[int]rollout_buffer: Queuemodel_version_value: Synchronizedtoken_allowance: Synchronizedheartbeat_value: Synchronizedfailed_event: Eventexception_info_queue: Queuemetrics_queue: Queuetools: list[collections.abc.Callable] | None = Noneenvironment_factory: collections.abc.Callable[[], object] | dict[str, collections.abc.Callable[[], object]] | None = Nonescore_queue_maxsize: int = 16lora_name: str | None = Nonedataset_start_index: int = 0 )
Generates and scores rollouts for experimental.async_grpo.AsyncGRPOTrainer, in a spawned rollout process.
The default implements GRPO. To customize a run, subclass it, override only the methods that change, and set the
subclass as the trainer’s loop_cls. Each prompt goes through, in order:
- filter_prompt(): whether to generate for the prompt at all.
- generate_one(): one full rollout, which calls generate_one_turn() for each sampling request.
- The group of
num_generationsrollouts is scored: its rewards are the sum of the trainer’sreward_funcs, of environments that defineget_reward, and of Rollout.reward. Then compute_advantages() runs once per group.
setup() and close() create
and release resources on the loop’s event loop, like an HTTP client. A hook can read self.config (a subclass of experimental.async_grpo.AsyncGRPOConfig can carry its own fields) and self.tokenizer. Since the loop is built
in a spawned process, the subclass must be importable (defined at module level).
filter_prompt
< source >( row_index: introw: dict[str, typing.Any]epoch: int ) → bool
Whether to generate for a prompt. A rejected prompt is skipped for this epoch and counts as consumed, like a dropped group. Admits every prompt by default. Rejecting every prompt of a full epoch raises, since no batch can reach the trainer after that.
generate_one
< source >( prompt: list[dict[str, str]]tools: list[collections.abc.Callable]group_id: int = 0 ) → Rollout
Parameters
- prompt (
list[dict[str, Any]]) — Prompt messages. - tools (
list[Callable]) — Tools available to this rollout: the trainer’stools, plus the public methods of the environment when usingenvironment_factory. - group_id (
int, optional, defaults to0) — Identifier of the group the rollout belongs to.
Returns
The finished rollout.
Generate one rollout for a prompt.
By default, a multi-turn tool loop: each turn renders the whole conversation with the chat template, samples a
reply with generate_one_turn() and runs the tool calls it contains,
until the model stops calling tools or max_tool_calling_iterations is reached. Override it to generate
differently, for example with an agent framework. Return every sampling request in Rollout.turns, since the
training sequences are built from them, and set Rollout.reward to score the rollout here.
generate_one_turn
< source >( prompt_ids: list[int] ) → TurnRecord
Sample one reply from the vLLM server, with the config’s sampling settings (temperature, top_p, top_k, min_p, repetition_penalty and max_completion_length).
compute_advantages
< source >( group: RolloutGroup ) → np.ndarray
Parameters
- group (RolloutGroup) —
The
num_generationsrollouts of one prompt, with their total rewards ingroup.rewards.
Returns
np.ndarray
One advantage per rollout, in the order of group.rewards.
Compute one advantage per rollout of a group. By default, each reward standardized within the group; a rollout
with a NaN reward gets advantage 0 and is left out of the mean and std.
It runs for every group. A group whose scored rewards are all equal is then dropped, whatever advantages this returns.
Create the resources the other hooks need, such as an HTTP client. Runs on the loop’s event loop before the first prompt. Does nothing by default.
Release what setup() created. Runs when the loops stop, including on failure. Does nothing by default.
Rollout
class trl.experimental.async_grpo.Rollout
< source >( completion: list[dict[str, str]]completion_ids: list[int]turns: list[trl.experimental.async_grpo.async_grpo_trainer.TurnRecord]tool_call_count: int = 0tool_failure_count: int = 0reward: float | None = Nonerollout_id: str = <factory> )
Parameters
- completion (
list[dict[str, Any]]) — Messages generated after the prompt, in conversational format. Passed to the reward functions ascompletions. - completion_ids (
list[int]) — Token ids of the generated completion. Passed to the reward functions ascompletion_ids. - turns (
list[TurnRecord]) — The sampling requests that produced the rollout, in order. The training sequences are built from them: the sampled tokens are trained on, the rest of each prompt is context. - tool_call_count (
int, optional, defaults to0) — Number of tool calls made during the rollout. - tool_failure_count (
int, optional, defaults to0) — Number of those tool calls that failed. - reward (
float, optional) — Reward computed while generating, for example by an external grader. It is added to the rewards of the reward functions and environments. IfNone, this rollout gets no reward from this source. - rollout_id (
str, optional) — Identifies the rollout in its training rows, for debugging. Defaults to a new random hex id.
One finished rollout, as returned by generate_one().
TurnRecord
class trl.experimental.async_grpo.async_grpo_trainer.TurnRecord
< source >( prompt_ids: list[int]output_ids: list[int]output_log_probs: list[float] = <factory>output_mask: list[int] | None = Noneoutput_sampling_mask: list[list[int]] | None = Noneoutput_sampler_logprobs: list[dict[int, float]] | None = None )
Parameters
- prompt_ids (
list[int]) — Token ids of the full prompt sent for this turn, including every earlier turn of the conversation. - output_ids (
list[int]) — Token ids the sampler generated. - output_log_probs (
list[float], optional, defaults to[]) — Sampler log-probability of each token inoutput_ids. Required when any of them is trained on. - output_mask (
list[int], optional) — One value per token inoutput_ids:1to train on the token,0to keep it as context only. IfNone, every output token is trained on. - output_sampling_mask (
list[list[int]], optional) — One list per token inoutput_ids: the token ids the sampler could draw from undertop_k/top_ptruncation. IfNone, the full vocabulary. - output_sampler_logprobs (
list[dict[int, float]], optional) — One dict per token inoutput_ids: the sampler’s log-probabilities of its most likely tokens, keyed by token id. Used by score centering (num_sampler_logprobs).
One sampling request: the prompt sent to the sampler and the tokens it generated.
RolloutGroup
class trl.experimental.async_grpo.async_grpo_trainer.RolloutGroup
< source >( prompts: list[list[dict[str, str]]]reward_kwargs: dict[str, list[typing.Any]]completions: list[list[dict[str, str]]]completions_ids: list[list[int]]completions_sequences: list[list[trl.experimental.async_grpo.async_grpo_trainer.TrainingSequence]]tool_call_counts: list[int]tool_failure_counts: list[int]model_version: intgroup_id: introw_index: intenv_rewards: list[tuple[type, float] | None]rollout_rewards: list[float | None]queued_at: float = 0.0rewards: numpy.ndarray | None = None )
Parameters
- prompts (
list[list[dict[str, Any]]]) — Prompt of each rollout. - reward_kwargs (
dict[str, list[Any]]) — The other columns of the prompt’s dataset row, passed to the reward functions as keyword arguments. - completions (
list[list[dict[str, Any]]]) — Completion messages of each rollout. - completions_ids (
list[list[int]]) — Completion token ids of each rollout. - completions_sequences (
list[list[TrainingSequence]]) — Training sequences built from each rollout. A rollout whose re-tokenized history changed between turns gives more than one. - tool_call_counts (
list[int]) — Number of tool calls in each rollout. - tool_failure_counts (
list[int]) — Number of failed tool calls in each rollout. - model_version (
int) — Policy version when the group’s first rollout started. - group_id (
int) — Identifier of the group, counted from0in the rollout process. - row_index (
int) — Index of the prompt’s row in the training dataset. - env_rewards (
list[tuple[type, float] or None]) — For a rollout whose environment definesget_reward, the environment class and its reward; elseNone. - rollout_rewards (
list[float or None]) — Rollout.rewardof each rollout. - queued_at (
float, optional, defaults to0.0) —time.monotonic()when the group was queued for scoring. - rewards (
np.ndarray, optional) — Total reward of each rollout, summed over every reward source, NaN when no source scored it. Set before compute_advantages() is called.
The num_generations rollouts of one prompt, scored together. Each list has one entry per rollout.