GRPO Training
Overview
Group Relative Policy Optimization (GRPO) is a reinforcement learning method that improves language models by generating multiple completions per prompt, scoring them with reward functions, and using the relative ranking within each group to compute advantage estimates. Unlike DPO, which requires pre-collected preference pairs, GRPO generates its own training data online and can work with any programmatic reward signal (math correctness, format compliance, code execution results, etc.).
Use GRPO when you have a task with a verifiable reward signal and want the model to discover solution strategies on its own. Use DPO when you already have human preference data. Use SFT when you have gold-standard completions to imitate directly.
Axolotl’s GRPO implementation builds on TRL. Synchronous GRPO runs on TRL’s GRPOTrainer; async GRPO (async_prefetch: true) runs on TRL’s AsyncGRPOTrainer, extended with zero-signal filtering, parallel reward workers, reward weighting, and a cap on the policy/sampler ratio. Both scale across GPUs with FSDP2; synchronous GRPO also supports DeepSpeed.
Architecture
GRPO training uses a two-process architecture: a vLLM server for fast generation and a trainer process for scoring and gradient updates.
Terminal 1 (GPU 0) Terminal 2 (GPU 1)
┌──────────────────────┐ ┌──────────────────────────────────┐
│ vLLM Server │ │ Trainer │
│ │ HTTP │ │
│ Serves base model │◄────────────►│ Send prompts to vLLM │
│ + LoRA adapter │ /v1/* │ Score completions (rewards) │
│ │ LoRA load │ Calculate advantages │
│ Punica kernels for │ │ Compute policy log-probs │
│ LoRA inference │ │ PPO-clip gradient update │
│ │ │ Sync weights to vLLM │
└──────────────────────┘ └──────────────────────────────────┘
Data flow for each synchronous training step:
- The trainer sends prompts to vLLM, which generates
num_generationscompletions per prompt. - Completions are scored with your reward functions.
- Advantages are computed within each prompt group (group-relative normalization).
- Policy log-probabilities are computed by running a forward pass on the training model.
- The PPO-clip loss is computed and gradients are applied.
- Periodically, weights are synced back to vLLM so future generations reflect the updated policy.
Async GRPO moves steps 1-3 into a separate process that runs continuously, so the trainer never waits for a full batch to finish generating.
Quick Start
A GRPO training run requires three components: a YAML config, a reward module (Python file), and a running vLLM server.
1. Write a reward module
Create a file called rewards.py in your working directory:
# rewards.py
import re
def accuracy_reward(completions, answer, **kwargs) -> list[float]:
"""Check if the completion contains the correct numerical answer."""
rewards = []
for completion, correct in zip(completions, answer):
text = completion[0]["content"]
# Extract the last number from the completion
numbers = re.findall(r"-?\d+(?:\.\d+)?", text)
predicted = numbers[-1] if numbers else ""
rewards.append(1.0 if predicted == str(correct) else 0.0)
return rewards
def format_reward(completions, **kwargs) -> list[float]:
"""Reward completions that use a structured thinking format."""
rewards = []
for completion in completions:
text = completion[0]["content"]
has_think = "<think>" in text and "</think>" in text
has_answer = "<answer>" in text and "</answer>" in text
rewards.append(1.0 if has_think and has_answer else 0.0)
return rewards
def prompt_transform(cfg, *args, **kwargs):
"""Convert GSM8K dataset rows into chat prompts."""
def transform_fn(example, tokenizer=None):
label = example["answer"].split("####")[-1].strip().replace(",", "")
return {
"prompt": [
{"role": "system", "content": "Solve the math problem. Show your reasoning in <think> tags and your final numerical answer in <answer> tags."},
{"role": "user", "content": example["question"]},
],
"answer": label,
}
return transform_fn, {"remove_columns": ["question"]}2. Write the config
Create config.yaml:
base_model: Qwen/Qwen2.5-1.5B-Instruct
rl: grpo
chat_template: tokenizer_default
vllm:
host: 0.0.0.0
port: 8000
gpu_memory_utilization: 0.85
dtype: auto
max_model_len: 2048
adapter: lora
lora_r: 32
lora_alpha: 64
lora_target_linear: true
trl:
use_vllm: true
async_prefetch: true
loss_type: cispo # see Loss below; measured against dapo on LoRA runs
vllm_server_host: 0.0.0.0
vllm_server_port: 8000
vllm_server_timeout: 300
vllm_lora_sync: true
num_generations: 8
max_completion_length: 512
temperature: 0.7
reward_funcs:
- rewards.accuracy_reward
- rewards.format_reward
reward_weights:
- 1.0
- 0.5
datasets:
- path: openai/gsm8k
name: main
type: rewards.prompt_transform
split: train
skip_prepare_dataset: true
val_set_size: 0.0
sequence_len: 512
micro_batch_size: 2
gradient_accumulation_steps: 4
max_steps: 200
learning_rate: 5.0e-6
optimizer: adamw_torch_fused
lr_scheduler: cosine
warmup_steps: 10
bf16: true
attn_implementation: flash_attention_2
activation_checkpointing: true
special_tokens:
pad_token: "<|endoftext|>"
output_dir: ./grpo-output
logging_steps: 13. Start vLLM and train
# Terminal 1: Start vLLM server on GPU 0
CUDA_VISIBLE_DEVICES=0 axolotl vllm-serve config.yaml
# Wait 30-90 seconds for model loading and CUDA graph capture
# Terminal 2: Train on GPU 1
CUDA_VISIBLE_DEVICES=1 axolotl train config.yamlUse tmux or separate terminal sessions to manage the two processes. The vLLM server must remain running for the entire training duration.
Custom Reward Functions
Function signature
TRL calls reward functions with this signature:
def my_reward(completions, **kwargs) -> list[float]:completionsis a list of single-element lists, where each element is a dict{"role": "assistant", "content": "..."}. Socompletions[i][0]["content"]gives you the text of the i-th completion.**kwargscontains all dataset columns that were not removed by the dataset transform. This is how you pass ground truth answers, metadata, or any other information to your reward function.- Return a
list[float]with the same length ascompletions. You may returnNonefor individual elements to exclude them from aggregation.
Example: accuracy reward with answer extraction
def accuracy_reward(completions, answer, **kwargs) -> list[float]:
rewards = []
for completion, correct_answer in zip(completions, answer):
text = completion[0]["content"]
# Extract answer from <answer>...</answer> tags
match = re.search(r"<answer>(.*?)</answer>", text, re.DOTALL)
predicted = match.group(1).strip() if match else ""
rewards.append(1.0 if predicted == str(correct_answer) else 0.0)
return rewardsExample: length penalty
def length_penalty(completions, **kwargs) -> list[float]:
"""Penalize very short or very long completions."""
rewards = []
for completion in completions:
length = len(completion[0]["content"])
if length < 50:
rewards.append(-0.5)
elif length > 2000:
rewards.append(-0.2)
else:
rewards.append(0.0)
return rewardsMultiple rewards and weighting
You can combine multiple reward functions with different weights:
trl:
reward_funcs:
- rewards.accuracy_reward
- rewards.format_reward
- rewards.length_penalty
reward_weights:
- 1.0 # accuracy is most important
- 0.5 # format compliance
- 0.1 # mild length preferenceRewards are combined by the multi_objective_aggregation strategy:
sum_then_normalize(default): weights and sums all rewards first, then normalizes across the group.normalize_then_sum(GDPO): normalizes each reward independently, then sums. This prevents one reward from dominating and is recommended when using multiple reward functions with different scales.
trl:
multi_objective_aggregation: normalize_then_sumDataset transforms
The dataset transform converts raw HuggingFace dataset rows into chat-format prompts:
def prompt_transform(cfg, *args, **kwargs):
def map_fn(example, tokenizer=None):
return {
"prompt": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": example["question"]},
],
# Keep 'answer' column for the reward function
"answer": example["answer"],
}
# Remove columns consumed by the transform; keep columns needed by rewards
return map_fn, {"remove_columns": ["question"]}The transform returns a tuple of (map_function, kwargs_dict). The remove_columns in the kwargs dict removes columns that are no longer needed. Columns that your reward functions reference via **kwargs (like answer) must not be removed.
The reward module must be importable from the directory where you run axolotl train. If your reward file is rewards.py, the import path is rewards.accuracy_reward. If it is inside a package my_rewards/scoring.py, use my_rewards.scoring.accuracy_reward.
Reward models (neural network rewards)
Instead of a Python function, you can pass a HuggingFace model path as a reward function. TRL will load it as a reward model and use its scalar output as the reward (synchronous GRPO only; async GRPO scores in a process without a GPU):
trl:
reward_funcs:
- OpenAssistant/reward-model-deberta-v3-large-v2
- rewards.format_reward
reward_weights:
- 1.0
- 0.3Using math_verify
The math_verify library provides robust mathematical answer verification but uses signal.alarm() internally, which only works in the main thread. If you use math_verify in a reward function, set reward_num_workers to use subprocess workers:
trl:
reward_num_workers: 4Each worker runs in its own subprocess with its own main thread, so signal.alarm() works correctly.
vLLM Setup
GRPO generates with vLLM, either as a separate server (below) or, on one GPU, in colocate mode. For a complete guide on server modes, LoRA sync, weight synchronization, and restart procedures, see vLLM Serving.
The minimal setup:
vllm:
host: 0.0.0.0
port: 8000
gpu_memory_utilization: 0.85
trl:
use_vllm: true
vllm_lora_sync: true # Recommended with LoRA — faster sync, no NCCL contention
weight_sync_steps: 5 # Sync weights every 5 stepsCUDA_VISIBLE_DEVICES=0 axolotl vllm-serve config.yaml # GPU 0: vLLM
CUDA_VISIBLE_DEVICES=1 axolotl train config.yaml # GPU 1: trainingvLLM must be restarted between experiments — stale weight syncs corrupt server state. See Restart Requirements.
LoRA weight sync precision
Without vllm_lora_sync, the adapter reaches vLLM merged into BF16 weights. At GRPO learning rates the BF16 cast drops most of the LoRA update: vLLM generates from a policy that kept about 24% of the update at learning_rate: 1e-5 and about 2.5% at 1e-6 (Qwen3-1.7B-Base, rank-16 LoRA on all linear layers, step 100). The trainer logs lora_merge/retained at the first sync and every trl.vllm_merge_check_interval steps, and warns when less than half the update survives. Keep the update with one of these options:
trl.vllm_lora_sync: truewith async GRPO (async_prefetch), which runs in server mode. vLLM then applies the adapter unmerged.lora_merge_aware: bf16when weights must be merged: colocate mode, the standard trainer, or async GRPO withoutvllm_lora_sync. The trainer then optimizes the merged BF16 weights directly.
See Merged Sync Rounding for details and restrictions.
Async Training Features
Async GRPO decouples generation from training. It runs on TRL’s experimental AsyncGRPOTrainer:
- A CUDA-free rollout process streams requests to the vLLM server (
max_inflight_tasksat a time), scores each prompt group as soon as itsnum_generationscompletions finish, and computes the group’s advantages. - Scored samples wait in a queue tagged with the policy version that generated them. The trainer drops any sample more than
max_stalenessweight syncs old. - The trainer packs samples into micro-batches of at most
token_budgettokens, forwarding the prompt shared by a group once (tree attention), with no padding. - The policy ratio is taken against vLLM’s own sampling log-probabilities, so there is no extra scoring forward pass on the trainer.
- Weights are synced to vLLM every
weight_sync_stepsoptimizer steps: only the adapter when the server has LoRA enabled, merged weights over NCCL otherwise.
Enabling async GRPO
trl:
use_vllm: true
async_prefetch: true
vllm_lora_sync: true # `axolotl vllm-serve` starts vLLM with LoRA enabled
weight_sync_steps: 1 # weight sync every optimizer step
max_staleness: 4 # drop rollouts more than 4 syncs old
reward_num_workers: 4 # score rewards in 4 processesStart the server with axolotl vllm-serve config.yaml as usual; with async_prefetch it reserves max_staleness + 2 LoRA slots, one per policy version a queued rollout may still name.
Requirements:
vllm_mode: server. Colocate mode is not supported.- FSDP2 or DDP. DeepSpeed and
context_parallel_size > 1are not supported. - Conversational prompts (a list of messages in the
promptcolumn). Text-only: multimodal datasets are not supported. - Reward functions get each completion as an assistant message. TRL splits out reasoning and tool calls only for the chat templates it ships a response parser for (Qwen, Llama 3.1/3.2, Gemma 4 and a few others); with any other template, including axolotl’s
chatmlandllama3, the message content is the decoded completion. - No reference model:
betamust be 0. The loss isloss_type: score_centering(default, as in TRL),dapoorcispo; see Loss below. - Reward functions run in a spawned process with no GPU, so they must be picklable (module-level functions, not lambdas). Reward models are not supported.
- A quantized base model (QLoRA, FP8) needs
vllm_lora_sync: true, since merged weights cannot be folded into quantized ones. - The trainer packs with TRL’s tree attention, which uses the
kernels-community/flash-attn3hub kernel, orkernels-community/flash-attn4on Blackwell. Where TRL’s kernel cannot load (FA3 has builds for SM80 and SM90 only, and FA4 fails to import undernvidia-cutlass-dslreleases it was not built against), axolotl falls back tokernels-community/flash-attn2. - Models with recurrent or linear-attention layers share prompt prefixes too. Gated DeltaNet layers (Qwen3.5, Qwen3-Next, OLMo-Hybrid) follow the trie through TRL’s own Triton kernels (
patch_tree_gdn); thetree-recurrencepackage makes the other mixers follow it: Mamba2 (Mamba2, Nemotron-H, Bamba, Granite-MoE-Hybrid) and LFM2’s short convs. Axolotl’s Kimi-Linear and Bailing models follow it through their own forwards. Mamba, Falcon-Mamba, Falcon-H1 and Qwen4-exp pack each rollout as its own sequence, seeing its boundaries through the same patches assample_packing. Other recurrent models are rejected when the config or the model loads, as is a mixer whose forwarduse_kernelsreplaced.
Without max_steps, training stops after num_epochs passes over the prompts and max_steps becomes a loose upper bound, so use lr_scheduler: constant or set max_steps for a decaying schedule.
What axolotl adds to TRL’s trainer
Zero-signal filtering (skip_zero_advantage_batches, default true). A group whose completions all get the same reward has zero advantage everywhere: its tokens add nothing to the policy gradient but cost a full forward and backward. Async GRPO drops such samples before packing, which cut forwarded tokens per trained signal-bearing token by 20% with a quarter of the groups zero-signal, 42% with half, and 71% with three quarters (measured through TRL’s batcher and collator). As in TRL’s own loop, a group without signal is dropped in the rollout process and reaches the trainer as its id only; the trainer drops the remaining zero-advantage samples, such as completions no reward function scored. As in DAPO’s dynamic sampling, the dropped tokens also leave the loss’s token count. Dropped groups still count as consumed, so epochs and checkpoint resume stay keyed on the prompts. Watch frac_reward_zero_std (the share of groups without signal), sample/dropped_zero_adv_groups_total and sample/dropped_zero_adv_total. As in TRL, reward averages every scored completion and reward_kept those sent to the trainer.
Dropped rollouts and the end of a run. A rollout can be dropped before training for having zero advantage, being truncated under mask_truncated_completions, being more than max_staleness syncs old, or not fitting token_budget. Each drop counts its prompt group as consumed (sample/dropped_<reason>_total). An epoch-bounded run stops once num_train_epochs passes over the prompts are consumed, even when the last groups were all dropped; an optimizer step that stop interrupts is not applied. A run that trains no group during a full pass over the prompts fails with the number of groups dropped for each reason and what to change, whether num_train_epochs or max_steps bounds it.
Steps that train nothing. When rewards are scored on the trainer (EBFT), zero-advantage and truncated samples are taken out of the loss rather than dropped, so a whole optimizer step can hold no trained token. Such a step counts: global_step and the learning-rate schedule advance. It applies no update, though: Adam’s momentum and weight decay would move the weights even without a gradient, so the optimizer skips every parameter and keeps its state as it was. The weights being unchanged, the following weight sync is skipped and the policy version stays, so in-flight rollouts do not age. A group whose every sample is excluded counts toward the no-progress check like a dropped one, so a full pass over the prompts that trains nothing fails the run here too. Watch batch/empty_steps_total and perf/weight_sync_skipped_total.
Parallel reward workers (reward_num_workers). TRL scores one group at a time and runs synchronous reward functions on threads. With reward_num_workers > 1, rewards run in that many processes and as many groups score at once. On a reward costing ~10 ms of CPU per completion, 4 workers scored groups 4.3x faster and 8 workers 6.2x; on math_verify checks of short answers (~1 ms each), 4 workers gave 1.6x and more workers nothing further. Each worker runs rewards on its main thread, where signal.alarm-based timeouts (math_verify) work; on threads they must be switched off.
Reward options. reward_weights, multi_objective_aggregation (including normalize_then_sum for GDPO), scale_rewards (group or none) and mask_truncated_completions keep their GRPO meaning. Advantages are normalized per group as each group finishes, so scale_rewards: batch is not available.
Loss (loss_type). Three objectives, which differ in what happens to a token whose probability has moved since vLLM sampled it. score_centering is the default, as in TRL; EBFT and NeMo Gym multi-turn, which cannot score-center, default to dapo.
dapois TRL’s clipped objective: the PPO surrogate with the ratio taken against the vLLM sampler’s log-probabilities, clipped at[1 - epsilon, 1 + epsilon_high]. It logs TRL’sclip_ratio/*metrics. Settingvllm_importance_sampling_correction,_modeor_capwith it is an error.cispo(opt-in) keeps every token’s gradient. Each rollout is trained on once, so, as in synchronous GRPO, the PPO ratio is taken against the policy itself and never clips, and the policy/sampler ratio becomes a detached per-token weight:vllm_importance_sampling_mode: token_truncate(default) caps it atvllm_importance_sampling_cap(default 3.0),token_maskdrops tokens beyond the cap. It logssampling/capped_token_frac. Settingepsilonorepsilon_highwith it is an error: unlike synchronous GRPO’scispo, which caps atepsilon_high, asynccispocaps atvllm_importance_sampling_cap.score_centering(default) is TRL’s score centering with masked importance sampling, which TRL introduced for samplers running in lower precision than the trainer. Each token’s gradient is weighted by its policy/sampler ratio, and tokens whose ratio falls outside [vllm_importance_sampling_clip_min,vllm_importance_sampling_clip_max] (default 0.5 and 5.0) are dropped. From that score, its expectation under the sampler is subtracted, so that a constant advantage does not pull the policy toward the sampler. The expectation is taken over the sampler’snum_sampler_logprobsmost likely tokens (default 8), which the rollout worker requests from vLLM for every generated token. The vLLM server’s--max-logprobsmust allow that many. TRL reports that serializing them costs 15-20% of generation throughput, and recommends--api-server-count 8on vLLM 0.32+. It logssampling/masked_mean(the share of dropped tokens) andsampling/sampler_mass(how much of the sampler’s probability the top tokens cover). Settingepsilon,epsilon_highorcispo’s importance-sampling options with it is an error. It needs TRL’s own rollout loop, so EBFT and NeMo Gym multi-turn cannot use it.dapostays the objective above: TRL’s clipped objective also drops tokens outside the importance-sampling bounds, but axolotl’sdapodoes not.
top_p < 1 and min_p > 0 need top_k > 0 (e.g. top_k: 512), and with top_p < 1 or top_k > 0, a vLLM 0.28+ server must be started with --return-sampling-mask --enable-scale-out, which axolotl vllm-serve passes for such a config. The trainer then normalizes each token’s log-probability over the candidate set it was sampled from, as vLLM did, under every loss. An older vLLM cannot return those sets, so the log-probabilities stay normalized over the whole vocabulary, with a warning; so do EBFT and NeMo Gym rollouts, which call vLLM themselves. Synchronous GRPO with vLLM importance sampling (vllm_importance_sampling_correction, on by default) also needs top_k with top_p < 1 or min_p > 0.
The policy/sampler ratio grows with staleness and trainer/inference mismatch. Under dapo, a token whose probability moved past epsilon since sampling stops getting gradient, and those are the tokens being learned. That matters when the policy moves far per step, as with LoRA or higher learning rates. On Qwen2.5-1.5B LoRA (r 16, lr 2e-5, max_staleness: 4) with a GSM8K accuracy + answer-format reward, cispo reached a 0.9 format rate in 17-20 steps, like axolotl’s removed trainer (15-18), and stayed stable over 200 steps. dapo took 38-49 steps, and from about step 80 its completions ran to max_completion_length in both of two seeds: the model stopped ending them. On 128 GSM8K test prompts, the final dapo models truncated 75-78% of completions and scored 0.46-0.52 accuracy; cispo truncated 4% and scored 0.64, the removed trainer 0.63. With dapo, watch completions/clipped_ratio. Validation warns when a LoRA or QLoRA run keeps dapo with rollouts allowed to be more than 2 optimizer steps old (max_staleness × weight_sync_steps; TRL’s max_staleness defaults to 4).
Every loss logs ratio and kl (policy/sampler divergence) and sampling/ratio_min, the step’s smallest policy/sampler ratio.
Out-of-place merged sync. When the server has no LoRA support, TRL merges the adapter into the base weights for the send and unmerges it afterwards. In bf16 that round trip does not restore the weights: one sync changed 6% of a layer’s frozen weights, and 1000 syncs with a moving adapter left 58% changed and a 1.3% relative drift. Async GRPO instead streams W + ΔW computed in fp32 per parameter, so the trained model is never written to. DoRA is supported this way on a single GPU or DDP, but not under FSDP.
Adapter checkpoints in fp32. Under FSDP with mixed precision, TRL writes checkpoints in the bf16/fp16 compute dtype, and FSDP2 trains LoRA factors in that dtype unless lora_fp32_gradients: true. Async GRPO writes a LoRA adapter’s tensors in fp32 either way; a fully fine-tuned model is still written in the compute dtype, as TRL does.
Migrating from the data producer trainer
Axolotl’s own GRPO trainer, which ran behind use_data_producer and, before TRL shipped AsyncGRPOTrainer, behind async_prefetch, has been removed. trl.use_data_producer: true remains as a deprecated alias for async_prefetch: true with max_staleness: 0: rollouts of an older policy are dropped, so training stays close to on-policy (the dataloader fetches one micro-batch ahead, so a step’s first micro-batch can still be one weight sync old).
The alias is slower than the trainer it replaces: every weight sync discards the generation requests still in flight. Training Qwen2.5-1.5B with LoRA on GSM8K, it dropped about 85 samples per step and took 22.5 s per step, against 16.7 s on the old synchronous path. Unless every rollout must come from the current policy, set async_prefetch: true and keep the default max_staleness (4): the same run was 12% faster per step than the old trainer’s async mode.
Config validation rejects the trainer’s other options and names what replaces each:
| Option | Replacement |
|---|---|
use_data_producer |
async_prefetch: true with the default max_staleness; max_staleness: 0 keeps rollouts near-synchronous at the cost above |
prefetch_depth |
max_staleness: how many weight syncs a rollout may lag |
streaming_partial_batch, streaming_min_groups |
None needed: groups are scored as they finish |
replay_buffer_size, replay_recompute_logps |
None: each rollout is trained on once, and skip_zero_advantage_batches drops samples without signal |
reroll_start_fraction, reroll_max_groups |
None |
batch_flattening (with rl: grpo) |
token_budget: rollouts are packed padding-free up to a token budget |
off_policy_mask_threshold, use_bias_correction_kl |
Synchronous GRPO only |
vllm_importance_sampling_mode: sequence_* |
Synchronous GRPO only; async GRPO uses the token modes |
beta, sync_ref_model, loss_type other than dapo, cispo or score_centering, num_iterations > 1 |
Synchronous GRPO only |
skip_zero_advantage_batches and reward_num_workers apply to async GRPO. Plugins’ on_rollouts_scored hook is called once per optimizer step on rank 0, with the step’s prompts, completions, per-function rewards and advantages.
Importance Sampling and Off-Policy Correction
Synchronous GRPO recomputes log-probabilities on the trainer and weights each token by the trainer/sampler ratio, with sequence-level weights and OPSM as options. Async GRPO, by default (loss_type: score_centering), weights by that ratio and drops tokens outside vllm_importance_sampling_clip_min / _max, taking it from the training forward; it clips the ratio with loss_type: dapo and weights by it, capped, with loss_type: cispo (see Loss). Synchronous GRPO:
trl:
vllm_importance_sampling_correction: true
importance_sampling_level: token # 'token' recommended (especially with Liger kernel)
off_policy_mask_threshold: 0.5 # KL threshold — masks sequences that are too off-policyUse token level IS. Sequence-level has numerical issues with Liger’s chunked computation. The off_policy_mask_threshold (OPSM, synchronous GRPO only) is a safety net that drops sequences where KL divergence exceeds the threshold — 0.5 is a reasonable starting point.
For detailed coverage of IS modes (token_mask, token_truncate, etc.), capping, and bias-corrected KL, see vLLM Serving — IS Correction.
Scaling
Fused LM head
TRL scores GRPO and GDPO tokens, and structured EBFT’s, with a fused LM head: per-token log-probabilities are computed in chunks from the hidden states, so the [batch, seq, vocab] logits are never built. The head bypasses the model’s own forward, so cut_cross_entropy and liger_fused_linear_cross_entropy are not applied to these trainers (a warning says so), and trl.use_liger_loss only applies Liger’s layer kernels. A LoRA adapter on lm_head is rejected; train the head with lora_modules_to_save: [lm_head].
Activation checkpointing and offloading
Both GRPO trainers, synchronous and async, take activation_checkpointing, including selective and offload.all / offload.hidden_states, as described in Activation Checkpointing & Offloading. TRL’s GRPO trainers have no activation offloading of their own, so axolotl runs their training step inside its offload context. Selective checkpointing keeps saving attention after the synchronous trainer turns checkpointing off for its scoring pass and back on. All three are off unless the config sets them; TRL’s AsyncGRPOConfig alone would turn checkpointing on.
FP8 training
FP8 quantization halves model VRAM usage with minimal impact on training quality. It does not significantly speed up computation for small models but allows larger models to fit in memory.
fp8: true
torch_compile: trueFP8 requires patching for zero-padding edge cases. The act_quant_kernel can produce NaN when input is all zeros (padding positions). If you see NaN in grad norms, check whether your padding token embedding is non-zero.
FSDP (Fully Sharded Data Parallel)
FSDP distributes model parameters across multiple GPUs for training while vLLM runs on a separate GPU:
fsdp_version: 2
fsdp_config:
auto_wrap_policy: TRANSFORMER_BASED_WRAP
transformer_layer_cls_to_wrap: Qwen2DecoderLayer
reshard_after_forward: true
activation_checkpointing:
reentrant: falseLaunch with:
# GPU 0: vLLM
CUDA_VISIBLE_DEVICES=0 axolotl vllm-serve config.yaml
# GPUs 0,1: Training (FSDP will use both visible GPUs)
CUDA_VISIBLE_DEVICES=0,1 axolotl train config.yamlAsync GRPO runs no collectives off the main thread: rank 0 pulls samples from the rollout queue and the dataloader dispatches one packed row to each rank.
DeepSpeed ZeRO-3
deepspeed: deepspeed_configs/zero3_bf16.json
activation_checkpointing:
reentrant: true # Required -- non-reentrant causes CheckpointError with ZeRO-3DeepSpeed ZeRO-3 requires activation_checkpointing.reentrant: true. This is the opposite of the FSDP recommendation. Non-reentrant checkpointing causes tensor metadata mismatches during recomputation with ZeRO-3’s parameter partitioning.
Multi-GPU considerations
| Concern | Recommendation |
|---|---|
| vLLM GPU allocation | Dedicate one or more GPUs to vLLM; do not share with trainer GPUs |
| Weight sync contention | Use vllm_lora_sync: true to avoid NCCL contention between training and vLLM |
| Generation bottleneck | Add vLLM nodes; see Multi-Node Serving |
| Async GRPO | FSDP2 or DDP; DeepSpeed is not supported |
| DeepSpeed + gradient checkpoint | Must use use_reentrant: true |
| OOM during scoring | Reduce micro_batch_size or num_generations. The logits tensor scales with batch_size * vocab_size |
Monitoring and Debugging
For detailed metric ranges, failure diagnosis, and OOM debugging, see Training Stability & Debugging.
Quick health checks during GRPO training:
rewards/*/meanshould be > 0.15 within 20 steps — if it stays at 0, test your reward function standalonereward_stdshould be > 0 on most steps — all-zero means no learning signalentropyin 0.05-0.5 — below 0.01 suggests mode collapsegrad_normin 0.001-1.0 — > 10 is unstable, 0.0 is expected when zero-advantage skip fires- Async GRPO:
perf/rollout_wait_snear 0 means training is the bottleneck, a large value means generation is;sample/staleness_meanshows how far behind the policy rollouts are;sample/dropped_fracis the share of samples filtered out
Pipe training output to a log file: axolotl train config.yaml 2>&1 | tee /tmp/training.log
Configuration Reference
All GRPO-specific options live under the trl: key in your config. Standard training options (learning_rate, micro_batch_size, etc.) are set at the top level as usual.
Core GRPO
| Option | Type | Default | Description |
|---|---|---|---|
use_vllm |
bool | false |
Enable vLLM for generation |
vllm_mode |
"server" or "colocate" |
null |
vLLM deployment mode. colocate hosts the engine on the training GPU (single-GPU); engine options come from the vllm: block, see Colocate Mode |
vllm_server_host |
str | "0.0.0.0" |
vLLM server hostname |
vllm_server_port |
int | 8000 |
vLLM server port |
vllm_server_timeout |
int | null |
Timeout (seconds) for vLLM responses |
num_generations |
int | null |
Completions generated per prompt |
generation_batch_size |
int | null |
Number of unique prompts per generation step |
max_completion_length |
int | null |
Maximum tokens per completion |
beta |
float | null |
KL penalty coefficient |
num_iterations |
int | null |
Iterations per batch (mu in the GRPO paper) |
epsilon |
float | null |
PPO clipping lower bound (async GRPO: loss_type: dapo only) |
epsilon_high |
float | null |
PPO clipping upper bound (async GRPO: loss_type: dapo only) |
loss_type |
str | null |
Loss formulation: grpo, bnpo, dr_grpo and TRL’s others; async GRPO: score_centering (default), dapo or cispo |
scale_rewards |
bool or str | true |
Reward std scaling: group, batch, or none (bool accepted: true->group, false->none) |
mask_truncated_completions |
bool | false |
Exclude truncated completions from loss |
log_multimodal |
bool | null |
Log multimodal content (images/videos) alongside completions (TRL >= 1.9) |
Entropy regularization
Available in TRL >= 1.8. Adds an entropy bonus to encourage exploration.
| Option | Type | Default | Description |
|---|---|---|---|
entropy_coef |
float | null |
Static entropy regularization coefficient |
use_adaptive_entropy |
bool | null |
Adaptively tune entropy_coef toward entropy_target (Skywork-OR1 style) |
entropy_target |
float | null |
Target entropy when use_adaptive_entropy is enabled |
entropy_coef_delta |
float | null |
Step size for adjusting entropy_coef |
entropy_coef_min |
float | null |
Lower bound for adaptive entropy_coef |
entropy_coef_max |
float | null |
Upper bound for adaptive entropy_coef |
Reward functions
| Option | Type | Default | Description |
|---|---|---|---|
reward_funcs |
list[str] | null |
Import paths to reward functions or HF model IDs |
reward_weights |
list[float] | null |
Relative weights for each reward function |
multi_objective_aggregation |
str | null |
"sum_then_normalize" (GRPO) or "normalize_then_sum" (GDPO) |
rollout_func |
str | null |
Import path to custom rollout function for OpenEnv-style tasks |
Generation parameters
| Option | Type | Default | Description |
|---|---|---|---|
temperature |
float | null |
Sampling temperature |
top_p |
float | null |
Nucleus sampling probability |
top_k |
int | null |
Top-k sampling |
min_p |
float | null |
Minimum probability threshold |
repetition_penalty |
float | null |
Penalty for repeated tokens |
generation_kwargs |
dict | null |
Additional vLLM SamplingParams (e.g., stop_token_ids); not with async_prefetch |
chat_template_kwargs |
dict | null |
Chat template kwargs (e.g., {enable_thinking: false}) |
vllm_guided_decoding_regex |
str | null |
Regex constraint for structured/guided decoding (mapped to TRL’s vllm_structured_outputs_regex on TRL >= 1.7); not with async_prefetch |
Async pipeline
| Option | Type | Default | Description |
|---|---|---|---|
async_prefetch |
bool | false |
Async GRPO on TRL’s AsyncGRPOTrainer (also structured EBFT) |
weight_sync_steps |
int | null (1) |
Sync weights to the rollout servers every N optimizer steps (vllm_sync_interval is a deprecated alias) |
vllm_lora_sync |
bool | false |
Sync only the LoRA adapter; makes axolotl vllm-serve enable LoRA |
vllm_merge_check_interval |
int | 50 |
Log how much of the LoRA update survives merged weight sync every N steps; 0 disables |
max_staleness |
int | null (4) |
Async GRPO: drop rollouts more than N weight syncs old |
max_inflight_tasks |
int | null |
Async GRPO: concurrent generation requests (default max_staleness x samples per step) |
token_budget |
int | null |
Async GRPO: max tokens per rank’s micro-batch (default: vLLM max_model_len; <= 0 packs micro_batch_size samples) |
scoring_token_budget |
int | null |
Async GRPO with rewards scored on the trainer (EBFT): max tokens per scoring forward; a rank’s scored sequences are split into as many forwards as needed (default: token_budget; <= 0 one forward per micro-batch) |
skip_zero_advantage_batches |
bool | true |
Async GRPO: drop zero-advantage samples |
reward_num_workers |
int | 1 |
Processes for synchronous reward functions |
queue_maxsize |
int | null (1024) |
Async GRPO: rollout samples buffered for the trainer |
request_timeout |
int | null (600) |
Async GRPO: seconds per generation request |
weight_sync_timeout |
int | null (1800) |
Async GRPO: seconds per vLLM weight transfer before it raises |
heartbeat_stale_after_s |
float | null (300) |
Async GRPO: abort when the rollout worker has not reported for this long |
fork_threshold_tokens |
int | null (1024) |
Async GRPO multi-turn: re-tokenization drift realigned as context instead of forking a new row |
max_tool_calling_iterations |
int | null |
Async GRPO: tool-calling turns per rollout (unlimited) |
router_aux_loss_coef |
float | null |
Async GRPO with MoE: router load-balancing loss weight (default: the model config’s) |
use_data_producer |
bool | false |
Deprecated: async_prefetch: true with max_staleness: 0, slower than the default staleness (migration) |
vllm_enable_sleep_mode |
bool | null |
Offload vLLM weights when idle (colocate mode) |
Importance sampling
| Option | Type | Default | Description |
|---|---|---|---|
vllm_importance_sampling_correction |
bool | null |
Enable IS correction for async distribution shift (async GRPO: implied by loss_type: cispo) |
importance_sampling_level |
"token" or "sequence" |
null |
Granularity of IS ratios. Use token with Liger |
vllm_importance_sampling_mode |
str | null |
token_mask, token_truncate, sequence_mask, or sequence_truncate (async GRPO: token modes, loss_type: cispo) |
vllm_importance_sampling_cap |
float | null |
Cap C for IS ratio clipping/masking (async GRPO: loss_type: cispo, default 3.0) |
vllm_importance_sampling_clip_min |
float | null |
Lower clip bound for the IS ratio (TRL >= 1.6; async GRPO: loss_type: score_centering, default 0.5) |
vllm_importance_sampling_clip_max |
float | null |
Upper clip bound for the IS ratio (TRL >= 1.6; async GRPO: loss_type: score_centering, default 5.0) |
num_sampler_logprobs |
int | null |
Async GRPO, loss_type: score_centering: top log-probs requested from vLLM per generated token (default 8) |
off_policy_mask_threshold |
float | null |
KL threshold for off-policy sequence masking (OPSM), synchronous GRPO |
use_bias_correction_kl |
bool | null |
Apply IS correction to KL divergence term, synchronous GRPO |
Reference model
| Option | Type | Default | Description |
|---|---|---|---|
sync_ref_model |
bool | false |
Periodically sync reference model with training model |
ref_model_mixup_alpha |
float | 0.9 |
EMA coefficient for reference model sync |
ref_model_sync_steps |
int | 64 |
Sync reference model every N steps |
Logging
| Option | Type | Default | Description |
|---|---|---|---|
log_completions |
bool | false |
Log sample completions to W&B |
num_completions_to_print |
int | null |
Number of completions to print per step |
use_liger_loss |
bool | null |
Deprecated by TRL: GRPO scores tokens with TRL’s fused LM head, so this only applies Liger’s layer kernels |