← All notes

Reinforcement Learning for Large Language Models

Contents

Yann LeCun has described intelligence with a cake analogy: “If intelligence is a cake, the bulk of the cake is unsupervised learning, the icing on the cake is supervised learning, and the cherry on the cake is reinforcement learning (RL).”

Following this, LLMs typically go through a three-step training pipeline:

  1. Pre-training. Self-supervised learning on vast swaths of internet data makes up the majority of the cake, especially when viewed in compute spent (FLOPs).
  2. Supervised fine-tuning (SFT). The beginning of post-training.
  3. Reinforcement learning. Used for alignment, via RLHF (Reinforcement Learning from Human Feedback), and for creating “reasoning” or “thinking” models, via RLVR (Reinforcement Learning with Verifiable Rewards).

Reinforcement Learning from Human Feedback (RLHF)

RLHF is the original alignment method. Its goal is to align LLMs with human preferences.

Line drawing of a huge many-eyed tentacled Shoggoth, labelled Unsupervised Learning. A small pink human-like face on one tentacle is labelled Supervised Fine-tuning, and the yellow smiley face it holds up in front of itself is labelled RLHF (cherry on top).
The Shoggoth meme drawn as the training pipeline: pretraining grows the creature, supervised fine-tuning gives it a human face, and RLHF is the smiley mask held up in front. Source: Dan Smith, "The Meaning of Shoggoth AI Memes", LessWrong.

There is no verifiable answer to check against for most of what we ask a model to do, so RLHF gets its signal from people instead. Thousands of human judgements are collected, each one a pairwise preference between two candidate answers to the same prompt: A over B, D over C, and so on. A neural network, the reward model, is then trained to imitate those preferences, so it can score responses the way the annotators would have.

Diagram: three boxes of pairwise human preferences, answer A over answer B, answer D over answer C, answer E over answer F, plus a note that there are thousands more, all feeding by arrows into a reward model described as a whole separate network
Thousands of pairwise human judgements are distilled into a reward model, a whole separate network. Source: GRPO explained.

That reward model is what the policy is then optimized against, which is the proxy the RLVR section below contrasts with.

It is worth being precise about what this buys. RLHF makes a model safer with respect to the behaviors and situations that its training and evaluation data represent. It does not guarantee safety under distribution shift. A prompt unlike anything the annotators considered can still pull out the behavior the training was meant to suppress, which is the point the painted-on face above is making.

TODO: will update this section later.

RL Formulation for LLMs

Applying any of this to an LLM starts with fixing what each piece of the RL vocabulary refers to once the environment is a model emitting tokens.

Two columns mapping general RL terms onto their LLM counterparts: policy maps to LLM, initial state to prompt, action to output token, state to prompt and output, and reward to a verifier or reward model.
The RL vocabulary, mapped onto an LLM. Source: Cameron R. Wolfe, "PPO for LLMs: A Guide for Normal People".

There is no transition function to speak of in this setup, because the transition is completely deterministic. Starting from a prompt xx, our initial state is s0={x}s_0 = \{x\}. Once the LLM predicts a token t1t_1 we are in s1={x,t1}s_1 = \{x, t_1\}, and once it predicts t2t_2 we are in s2={x,t1,t2}s_2 = \{x, t_1, t_2\}: the subscript is the timestep, counting how many tokens have been generated so far.1 In other words, our state is just the running completion being generated by the LLM for a given prompt xx.

For LLMs, there are two key ways in which RL can be formulated, differing in how they model actions:

In the context of LLMs, we already know how to compute the probability of both individual tokens and the full completion for a prompt. Therefore, we have the ability to model RL using either an MDP or a bandit formulation.2 Given that LLMs usually only receive outcome rewards, however, the bandit formulation, despite being very simple, is quite fitting for LLMs. REINFORCE and RLOO adopt the bandit formulation, while algorithms like PPO use a per-token MDP formulation.3

Policy Gradients for LLMs

Before PPO, the plainest thing that can be done with this setup is the policy gradient itself. Its estimator, derived here, is

∇θJ(θ)=Eτ∼pθ(τ)[(∑t=1T∇θlog⁡πθ(at∣st))r(τ)]≈1N∑i=1N(∑t=1T∇θlog⁡πθ(ai,t∣si,t))r(τi)\nabla_\theta J(\theta) = \mathbb{E}_{\tau \sim p_\theta(\tau)} \left[ \left( \sum_{t=1}^{T} \nabla_\theta \log \pi_\theta(a_t \mid s_t) \right) r(\tau) \right] \approx \frac{1}{N} \sum_{i=1}^{N} \left( \sum_{t=1}^{T} \nabla_\theta \log \pi_\theta(a_{i,t} \mid s_{i,t}) \right) r(\tau_i)

We are increasing the probability of actions taken on trajectories with high rewards: taking a step with this gradient pushes up the log-probability of each action in proportion to r(τ)r(\tau), the sum of all the rewards obtained along that trajectory.

Reading it back through the mapping above: a trajectory τi\tau_i is one completion, the inner sum runs over that completion’s tokens, and r(τi)r(\tau_i) is the single outcome reward a verifier or reward model returns once the completion is finished. Under the bandit formulation the whole inner sum collapses into the log-probability of the completion as a single action; under the per-token MDP formulation it stays a sum over tokens.

In code, one training step of the basic policy gradient is4:

One training step of the basic policy gradient, with each stage highlighted as it is described. Source: Cameron R. Wolfe, "REINFORCE: Easy Online RL for LLMs".

REINFORCE & RLOO for LLMs

Adapted from Cameron R. Wolfe, “REINFORCE: Easy Online RL for LLMs”.

REINFORCE

The structure of the policy gradient used by REINFORCE is similar to the baselined policy gradient estimate covered earlier. However, REINFORCE specifically uses the average of rewards observed during RL training as a baseline. This average can be computed in a few different ways; e.g., a moving average of rewards throughout training or an average of rewards present in the current batch.

The REINFORCE policy gradient, with each part labelled: the gradient of J with respect to theta is an expectation over prompts x drawn from the dataset and completions y sampled from the policy, of the gradient of log pi_theta(y given x) multiplied by the sample reward R(x, y) minus the average reward R-bar. Labels mark x as the prompt, y as the sample or completion, R(x, y) as the sample reward, and R-bar as the average reward serving as the baseline.
The REINFORCE policy gradient, with the prompt, completion, sample reward and baseline marked. Source: Cameron R. Wolfe, "REINFORCE: Easy Online RL for LLMs".

The figure follows the source’s notation: the completion is yy and its reward R(x,y)R(x, y), written as the action aa and reward rr elsewhere in this note.

The expression for the policy gradient in REINFORCE is shown above. To compute a gradient update over a batch, we perform the following steps:

  1. Generate a completion for each prompt using the current policy.
  2. Store the log probabilities for the tokens in each completion.
  3. Assign a reward to each completion (usually with a reward model).
  4. Obtain a baseline by taking an average of rewards.
  5. Compute the advantage by subtracting the baseline from the reward.
  6. Compute the sum of log probabilities multiplied by the advantage for each completion, then average over the batch to form a Monte Carlo estimate.

Note that this is a bandit formulation: the advantage here is defined at the completion level, rather than separately for every token as in the per-token MDP view.5

We also incorporate the Kullback-Leibler (KL) divergence with respect to a reference policy, usually a prior SFT-trained checkpoint of our model, into REINFORCE. Adding the KL divergence into RL regularizes the training process and allows us to ensure that our policy does not deviate significantly from the reference policy. There are several different approaches for approximating KL divergence. A common approach is to approximate it as the difference in log probabilities between the policy and the reference policy.6 Once we have made this approximation, the KL divergence is directly incorporated into the reward:

The KL-penalized reward, with each part labelled: R-hat of x and y equals R of x and y minus beta times the quantity log pi_theta of y given x minus log pi_ref of y given x. Labels mark R(x, y) as the reward, beta as the KL penalty coefficient, log pi_theta as the policy log-probabilities, log pi_ref as the reference log-probabilities, and the bracketed difference of the two as the KL divergence approximation.
The KL penalty folded into the reward itself, with the penalty coefficient, the policy and reference log-probabilities, and the difference approximating the KL divergence marked. Source: Cameron R. Wolfe, "REINFORCE: Easy Online RL for LLMs".

Note that unlike PPO and GRPO, the KL divergence here is not incorporated into the loss function, but directly into the reward.

Most LLMs are trained using an outcome reward setting, meaning that only the final <eos> token generated by the LLM is assigned a reward. The KL divergence, however, is computed on a per-token basis, and is subtracted from the reward as above. This is where the choice of formulation starts to matter: REINFORCE treats the entire completion as a single action, whereas regular PPO treats each completion token as an individual action. Since typically only the <eos> token gets a true reward, which is very sparse, PPO (which is formulated as an MDP) would attribute that reward to the <eos> token alone, whereas REINFORCE attributes it to the whole completion.

We create a completion-level (bandit formulation) reward by summing the per-token KL divergences and rewards over the sequence. Similarly, we can sum the token-level log probabilities to get the log probability of the completion (or trajectory). We can then use these completion-level components to compute the policy gradient much as before:

  1. Subtract the baseline (average reward) from the completion-level reward.
  2. Multiply this difference by the completion log probability.
  3. Run a backward pass to compute the final policy gradient.

The figure below runs the whole process on an example completion, with numbers at every step.

Diagram of the bandit formulation in REINFORCE. A row of completion tokens (Token 1, Token 2, Token 3, ellipsis, eos) each carry a policy log-probability (-2.1, -0.5, -3.6, -1.2) and a reference log-probability (-2.0, -0.3, -2.8, -1.4); subtracting them gives a per-token KL divergence (-0.1, -0.2, -0.8, 0.2). A reward model assigns 2.0 to the eos token only. To the right of a dashed line marked 'sum over sequence', the log-probabilities sum to -7.4 and the KL divergences sum to -0.9, which added to the 2.0 reward gives a full completion reward of 1.1. Those two numbers feed the policy gradient expression log pi_theta(y given x) times the quantity R(x, y) minus b, which is used to take a gradient and update the model.
The bandit formulation in REINFORCE: per-token log-probabilities, KL divergences and the single outcome reward are summed over the sequence into one completion log-probability and one completion-level reward, which are what the policy gradient expression consumes. Source: Cameron R. Wolfe, "REINFORCE: Easy Online RL for LLMs".

This computes the policy gradient for a single prompt and completion pair, and we generally average that gradient over a batch of completions.

REINFORCE Leave One Out (RLOO)

In REINFORCE, we generate a single on-policy completion per prompt during training and use the rewards from these completions to form our baseline via a moving average or an average of rewards in the batch.7

The problem with this is that an average reward taken across completely different prompts isn’t necessarily very meaningful. Different prompts can have very different reward distributions, and ideally we’d like to ask whether a given completion is good compared with other completions for the same prompt. This is what REINFORCE leave-one-out (RLOO) does. Specifically, it:

  1. Samples multiple (KK) completions per prompt.
  2. Uses those completions to compute the reward average separately for each individual prompt.

Given KK completions {y1,y2,…,yK}\{y_1, y_2, \ldots, y_K\} for the same prompt xx, RLOO defines the baseline for completion yiy_i as shown below, which is simply an average over all rewards for completions to prompt xx excluding the completion itself, yiy_i. We “leave out” the reward of the completion for which the policy gradient is being computed and average over the rewards of the other completions to the same prompt. The reason is that when evaluating yiy_i, we want the baseline to be constructed from independent alternative samples, rather than allowing yiy_i‘s own reward to influence the baseline it is measured against. Hence: leave one out.

The RLOO baseline: one over K minus 1, times the sum over j from 1 to K with j not equal to i, of R(x, y_j). Labels mark K as the number of samples per prompt, the j not equal to i condition as leaving out the reward from the i-th sample, and R(x, y_j) as the reward for the j-th sample.
The RLOO baseline for completion i: the mean reward of the other completions to the same prompt. Source: Cameron R. Wolfe, "REINFORCE: Easy Online RL for LLMs".

From here, we can compute the advantage estimate in RLOO by (i) computing this baseline for every completion for the prompt and (ii) subtracting the baseline from the reward received by that completion, as in the first equation below.8

The RLOO advantage: A(x, y_i) equals R(x, y_i) minus the quantity one over K minus 1 times the sum over j from 1 to K with j not equal to i of R(x, y_j). Labels mark A(x, y_i) as the advantage for the i-th sample, R(x, y_i) as the reward for the i-th sample, and the boxed subtracted term as the leave-one-out average reward.
The advantage for completion i: its own reward minus the leave-one-out average. Source: Cameron R. Wolfe, "REINFORCE: Easy Online RL for LLMs".

To compute the baseline efficiently, we can first compute a fixed average reward over the KK completions and reformulate the advantage as in the second equation below. This lets us compute the average reward once and avoid re-computing the leave-one-out average for all KK completions to the prompt xx.

The same advantage rewritten as K over K minus 1, times the quantity R(x, y_i) minus one over K times the sum over j from 1 to K of R(x, y_j). The subtracted term is boxed and labelled the fixed average reward, now taken over all K completions including the i-th.
The same quantity rewritten around a single fixed average over all K completions. Source: Cameron R. Wolfe, "REINFORCE: Easy Online RL for LLMs".

This modified advantage estimate can be plugged into the same policy gradient expression used by REINFORCE. As in REINFORCE, RLOO uses a per-completion (as opposed to per-token) loss, and there is no learned value model.

For each completion we have its advantage AiA_i and its log probability log⁡πθ(yi∣xi)\log \pi_\theta(y_i \mid x_i). Writing BB for the number of prompts in the batch and KK for the number of completions sampled per prompt, the loss is

L=−1BK∑i=1B∑k=1KAi,klog⁡πθ(yi,k∣xi)L = -\frac{1}{BK} \sum_{i=1}^{B} \sum_{k=1}^{K} A_{i,k} \log \pi_\theta(y_{i,k} \mid x_i)

from which we take ∇θL\nabla_\theta L and a single optimizer step:

θ←θ−η∇θL\theta \leftarrow \theta - \eta \nabla_\theta L

So a batch of 32 prompts with K=8K = 8 completions each puts 32×8=25632 \times 8 = 256 completion samples behind one gradient step. The leave-one-out baseline lowers variance relative to standard REINFORCE by using multiple samples per prompt to derive the policy gradient estimate. Compared to a single-sample approach, this benefits training stability, speed, and performance.

KL Divergence for LLMs

The definition, its discrete and continuous forms, and why it is not symmetric are in the standalone KL divergence note. This section is about what it is doing once the two distributions are language models.

Throughout LLM post-training, there are many cases where we optimize our model subject to a KL divergence constraint. The canonical optimization objective used within RLHF has the form shown below.

The standard RLHF objective with a KL constraint: maximize over the policy pi the expectation, over prompts x drawn from the dataset and completions y drawn from pi given x, of r(x, y) minus beta times the KL divergence between pi(y given x) and pi_ref(y given x). Labels mark pi as the LLM or policy, r(x, y) as the reward, and the subtracted beta-weighted term as the penalty term, itself the KL divergence between the current policy and the reference policy.
The standard RLHF objective with a KL constraint: reward on the left, the beta-weighted divergence from the reference policy subtracted on the right. Source: Cameron R. Wolfe, "Direct Preference Optimization (DPO)".
The adjusted reward r-prime of x and y equals the original reward r of x and y minus beta times the KL divergence between the current policy pi of y given x and the reference policy pi-ref of y given x. The original reward term is labeled red and the KL divergence term is labeled blue.
The KL penalty folded into an adjusted reward: start with the original reward, then subtract the beta-weighted divergence from the reference policy. Source: Cameron R. Wolfe, "Direct Preference Optimization (DPO)".

As we can see, we want to maximize rewards while minimizing a penalty term, the KL divergence weighted by β\beta, that is subtracted from these rewards. A larger β\beta puts more weight on staying close to the reference policy. A smaller β\beta allows more change in pursuit of reward. We usually estimate the KL divergence between the completion distributions predicted by our current policy and a fixed reference policy, often the original SFT model. Intuitively, adding this constraint to the reward used during RL training, as shown above, discourages the policy being trained from drifting too far from the reference policy. For a prompt xx, this KL divergence has the following form:

KL divergence between two LLMs: D_KL of pi_theta given x against pi_SFT given x equals the expectation, over completions y drawn from pi_theta given x, of the log of pi_theta(y given x) over pi_SFT(y given x). Labels mark pi_theta as the current policy, the LLM being trained, and pi_SFT as the reference policy.
The same divergence with both arguments named: the policy being trained against the SFT checkpoint it started from. Source: Cameron R. Wolfe, "Direct Preference Optimization (DPO)".

This form of the KL divergence looks at the ratio of probabilities predicted by both the current and reference model for a completion yy given a prompt xx as input. The probability of a completion yy is simply the product of next token probabilities predicted by the LLM for each token within a completion. By computing the KL divergence over these completion probabilities, we capture the similarity between the token distributions predicted by the two models.9

The log ratio for a sampled completion is exact for that particular completion. KL, however, compares two entire completion distributions, so its exact definition includes every possible completion:

DKL ⁣(πθ(⋅∣x) ∥ πref(⋅∣x))=∑yπθ(y∣x)log⁡πθ(y∣x)πref(y∣x).D_{\mathrm{KL}}\!\left(\pi_\theta(\cdot \mid x) \,\|\, \pi_{\mathrm{ref}}(\cdot \mid x)\right) = \sum_y \pi_\theta(y \mid x) \log \frac{\pi_\theta(y \mid x)}{\pi_{\mathrm{ref}}(y \mid x)}.

This is the conditional KL for one prompt. During training, we typically average this quantity over prompts drawn from a prompt distribution D\mathcal{D}.

We don’t compute KL exactly and instead estimate it because of the large number of possible completions. Even with a vocabulary of only 100 tokens and completions that are exactly 10 tokens long, there are

10010=1020100^{10} = 10^{20}

possible completions. Computing and adding one term for each is impractical. Another practical reason is that during training we generally do not retain the full distribution (all token probabilities) at every position. To save GPU memory and I/O, we keep only the log-probabilities of the tokens actually generated along each trajectory.

From the exact KL to a sampled token. At each generated prefix, let

ht=(x,y<t)h_t = (x, y_{<t})

denote the prompt together with the tokens generated before position tt. The exact reverse KL between the current and reference next-token distributions is

DKL(t)=DKL ⁣(πθ(⋅∣ht) ∥ πref(⋅∣ht))=∑v∈Vπθ(v∣ht)[log⁡πθ(v∣ht)−log⁡πref(v∣ht)]=Ev∼πθ(⋅∣ht)[log⁡πθ(v∣ht)−log⁡πref(v∣ht)].\begin{aligned} D_{\mathrm{KL}}^{(t)} &= D_{\mathrm{KL}}\!\left( \pi_\theta(\cdot \mid h_t) \,\|\, \pi_{\mathrm{ref}}(\cdot \mid h_t) \right) \\ &= \sum_{v \in \mathcal{V}} \pi_\theta(v \mid h_t) \left[ \log \pi_\theta(v \mid h_t) - \log \pi_{\mathrm{ref}}(v \mid h_t) \right] \\ &= \mathbb{E}_{v \sim \pi_\theta(\cdot \mid h_t)} \left[ \log \pi_\theta(v \mid h_t) - \log \pi_{\mathrm{ref}}(v \mid h_t) \right]. \end{aligned}

Computing this expression directly requires summing over the entire vocabulary at every generated position. A rollout instead supplies one token sampled from the current policy,

yt∼πθ(⋅∣ht).y_t \sim \pi_\theta(\cdot \mid h_t).

Evaluating that token under both policies gives the one-sample estimator

D^KL(t)=log⁡πθ(yt∣ht)−log⁡πref(yt∣ht).\widehat{D}_{\mathrm{KL}}^{(t)} = \log \pi_\theta(y_t \mid h_t) - \log \pi_{\mathrm{ref}}(y_t \mid h_t).

Conditioned on the prefix, this estimator is unbiased:

Eyt∼πθ(⋅∣ht)[D^KL(t)]=DKL(t).\mathbb{E}_{y_t \sim \pi_\theta(\cdot \mid h_t)} \left[ \widehat{D}_{\mathrm{KL}}^{(t)} \right] = D_{\mathrm{KL}}^{(t)}.

This is ordinary Monte Carlo estimation with one sample, providing a KL signal at every generated token. An individual sampled value can be negative even though the exact KL is nonnegative.

Summing these sampled token-level log ratios gives the log ratio for the complete sampled completion:10

∑t=1TD^KL(t)=log⁡πθ(y∣x)−log⁡πref(y∣x).\sum_{t=1}^{T} \widehat{D}_{\mathrm{KL}}^{(t)} = \log \pi_\theta(y \mid x) - \log \pi_{\mathrm{ref}}(y \mid x).

A good estimator should ideally be unbiased, so it has the right mean, and low-variance, so a finite batch gives a stable answer. For each sampled prompt-completion pair, define the inverse likelihood ratio

wi=πref(y(i)∣x(i))πθ(y(i)∣x(i)).w_i = \frac{\pi_{\mathrm{ref}}(y^{(i)} \mid x^{(i)})} {\pi_\theta(y^{(i)} \mid x^{(i)})}.

Three common choices are

k1(wi)=−log⁡wi,k2(wi)=12(log⁡wi)2,k3(wi)=(wi−1)−log⁡wi.k_1(w_i) = -\log w_i, \qquad k_2(w_i) = \frac{1}{2}(\log w_i)^2, \qquad k_3(w_i) = (w_i - 1) - \log w_i.

For samples from the current policy, k1k_1 is unbiased but can have high variance. k2k_2 can reduce variance when the two policies are close, but it is biased. k3k_3 is unbiased, nonnegative, and typically lower-variance. The standard PPO-style log-ratio penalty described above uses k1k_1, while the original DeepSeekMath formulation of GRPO uses k3k_3 at each token. Implementations can vary; see John Schulman’s note on KL approximation for the derivations.

Whichever estimator we choose, the computation follows the same pattern:

  1. Sample. Draw NN prompt-completion pairs, with x(i)∼Dx^{(i)} \sim \mathcal{D} and y(i)∼πθ(⋅∣x(i))y^{(i)} \sim \pi_\theta(\cdot \mid x^{(i)}).
  2. Compute log-probabilities. Evaluate each completion under both models to obtain log⁡πθ(y(i)∣x(i))\log \pi_\theta(y^{(i)} \mid x^{(i)}) and log⁡πref(y(i)∣x(i))\log \pi_{\mathrm{ref}}(y^{(i)} \mid x^{(i)}), then form log⁡wi=log⁡πref−log⁡πθ\log w_i = \log \pi_{\mathrm{ref}} - \log \pi_\theta.
  3. Apply the estimator. Compute k1(wi)k_1(w_i), k2(wi)k_2(w_i), or k3(wi)k_3(w_i) for each pair.
  4. Average. Average those values over the batch.

Using k1k_1 gives the estimator already developed above:

D^KL=1N∑i=1N[log⁡πθ(y(i)∣x(i))−log⁡πref(y(i)∣x(i))],x(i)∼D,y(i)∼πθ(⋅∣x(i)).\widehat{D}_{\mathrm{KL}} = \frac{1}{N} \sum_{i=1}^{N} \left[ \log \pi_\theta(y^{(i)} \mid x^{(i)}) - \log \pi_{\mathrm{ref}}(y^{(i)} \mid x^{(i)}) \right], \qquad x^{(i)} \sim \mathcal{D}, \quad y^{(i)} \sim \pi_\theta(\cdot \mid x^{(i)}).

Each bracketed log ratio is exact for its sampled prompt-completion pair, but their finite average is a Monte Carlo estimate of the KL averaged over the prompt distribution. As NN grows, this empirical average approaches the true prompt-averaged KL divergence.11

In PPO, the token-level penalty and the batch-level Monte Carlo estimate use the same samples. With k1k_1, each completion’s log ratio is the sum of its sampled token log ratios. PPO can apply those sampled per-token log ratios directly to the corresponding token rewards, while summing them within each completion and averaging across the batch gives the scalar KL estimate used for monitoring or adapting β\beta.

Proximal Policy Optimization (PPO) for LLMs

PPO is what the RLHF pipeline above optimizes with, and relies upon the MDP formulation. The derivation, from the importance ratio through clipping to the surrogate objective, is in the actor-critic note; this section is about what the algorithm looks like in the setup above.

In the context of LLMs, during generation actions are taken by selecting a token from the model’s next-token distribution: each token is its own action. After a token is predicted, it is added to the current state and used by the LLM to predict the next token. Eventually, the LLM predicts a stop token (e.g. <|end_of_text|> or <eos>) to complete the generation process, thus yielding a complete trajectory.

As we saw in the REINFORCE section, we first sample a batch of prompts and generate completions to these prompts with our LLM or policy. Then, we compute the rewards for these completions and use these rewards to derive a policy update.

Each training iteration of PPO performs the following sequence of steps:

  1. Sample a diverse batch of prompts.
  2. Generate a completion from the policy for each prompt.
  3. Compute advantage estimates for each completion.
  4. Perform several policy updates over this sampled data.

To see the whole thing in one place, here is a single token’s PPO update worked end to end, for an LLM answering “What’s 2 + 2?”:

A five-step PPO walkthrough for one token: compute the old and new policy's probabilities for the token '4' (0.10 and 0.15) and their ratio 1.5; compute the raw score from the verifier reward and the value head's expected reward, giving advantage +0.6 and raw_score = 1.5 x 0.6 = 0.90; clip the ratio to clamp(1.5, 0.8, 1.2) = 1.2, giving clipped_score = 0.72; take final_score = min(0.90, 0.72) = 0.72; and compute loss = -final_score + beta * KL(new_policy || reference_model).
ratio         = new_policy_prob / old_policy_prob
raw_score     = ratio * advantage
advantage     = actual_reward - expected_reward
clipped_ratio = clamp(ratio, 0.8, 1.2)
clipped_score = clipped_ratio * advantage
final_score   = min(raw_score, clipped_score)
loss          = -final_score
              + β * KL(new_policy || reference_policy)

The value head reads the same text the policy did, What's 2 + 2? The answer is, and predicts the reward the verifier will end up handing back. Subtracting that prediction from the reward actually received is what turns a raw reward into the advantage in step 2.

One token's PPO update, end to end. Diagram source: Sebastian Raschka.

The last line adds one piece the derivation elsewhere doesn’t cover. LLM setups usually attach a KL penalty to the clipped objective, weighted by β\beta, and it’s measured against a reference model: the model as it was before RL started, frozen for the entire run. That’s a different thing from the snapshot in the ratio’s denominator, which is refreshed every batch. The ratio keeps a single update from moving too far; the KL term keeps the whole run from drifting far from where it began.

PPO’s inner loop takes several gradient steps on one collected batch, and what sits in the ratio’s denominator through all of them is worth being precise about. πθold\pi_{\theta_{\text{old}}} is not the model from the previous gradient step; it’s a frozen snapshot taken before the batch was collected. Nothing trains that copy, and it does two jobs at once: it generates the rollouts (prompt 1 → response A, prompt 2 → response B, and so on), and it supplies πθold(a∣s)\pi_{\theta_{\text{old}}}(a \mid s) for every ratio computed off them. So as the inner loop runs, the numerator πθ\pi_\theta moves every step, after one update, then after two, then after three, while the denominator stays πθold\pi_{\theta_{\text{old}}} the whole way through. The advantage is frozen alongside it: A^\hat{A} is computed once from this batch and reused unchanged by every step, recomputed once per rollout rather than once per optimizer step.

For LLM RL, “several steps” usually means 2 to 10 epochs over the same batch. Then the data is discarded, θold←θ\theta_{\text{old}} \leftarrow \theta makes the latest model the new frozen snapshot (playing both roles again), and the loop restarts from a completely fresh batch. Resetting that often is what bounds the staleness that several gradient steps on one batch introduce: every action and reward in the batch came from πθold\pi_{\theta_{\text{old}}}, so still training on them twenty updates later means optimizing against behavior the model no longer exhibits.

The reason to squeeze several steps out of one batch at all is cost. Generating rollouts dominates: a single batch might mean generating 16,000 completions, scoring each one with a reward model or verifier, and computing advantages, which can take minutes. A gradient step on data already sitting in GPU memory is cheap by comparison, so taking exactly one update per batch would mean paying the expensive part over and over for one cheap step each time. Despite the importance ratio, PPO is still usually called an on-policy algorithm, because the data it trains on always comes from the current policy or one that’s only a few optimization steps old.

KL Divergence for PPO: There are two common ways to incorporate KL divergence into PPO training. The first is to subtract it from the reward, as in the KL-adjusted reward shown earlier. The second is to add it directly as a penalty term in the training objective, as in the RLHF objective with a KL constraint. In both cases, the aim is to maximize reward without allowing the current policy to drift too far from the reference policy.

Both approaches have been used successfully and represent two views of essentially the same regularized objective. Papers commonly write KL divergence as a penalty in the objective, while PPO implementations often incorporate its sampled token-level contribution into the reward because this integrates naturally with return and advantage computation.

For the reward-penalty implementation, a typical PPO rollout proceeds as follows:

  1. Sample a batch of prompts.
  2. Freeze a snapshot of the current policy as πθold\pi_{\theta_{\text{old}}} and use it to generate completions for those prompts.
  3. For every generated token, record the log-probability assigned to that token by both πθold\pi_{\theta_{\text{old}}} and the frozen reference policy πref\pi_{\mathrm{ref}}, usually the original SFT model πSFT\pi_{\mathrm{SFT}}. Computing the exact categorical KL at each position would instead require both models’ full probability distributions over the vocabulary.
  4. Compute a sampled per-token KL term from the log ratio log⁡πθold(at∣st)−log⁡πref(at∣st)\log \pi_{\theta_{\text{old}}}(a_t \mid s_t) - \log \pi_{\mathrm{ref}}(a_t \mid s_t), or use another KL estimator.
  5. Multiply each token’s KL term by β\beta and subtract it from that token’s reward. In the alternative formulation, add the corresponding penalty directly to the training loss.
  6. Aggregate the token-level terms for monitoring, commonly by summing over each completion and averaging across the batch.12

The reference policy remains fixed throughout PPO training, so it serves as a stable anchor for the behavior learned during SFT. A low β\beta creates a weak penalty, giving the current policy more freedom to pursue reward but increasing the risk of excessive drift. A high β\beta creates a strong penalty, preserving behavior closer to the reference policy but potentially limiting reward improvement.

Choosing β\beta often requires experimentation. An adaptive KL controller can adjust it using the KL observed in each batch: when KL exceeds a target, the controller increases β\beta to strengthen the constraint; when KL falls below the target, it decreases β\beta to permit more optimization. See ApX’s “The Role of the KL Divergence Penalty” for a broader overview.

Advantage in PPO: The advantage function is defined as the difference between the state-action value function and the value function, A(st,at)=Q(st,at)−V(st)A(s_t, a_t) = Q(s_t, a_t) - V(s_t). In PPO, we estimate the state-action value function using the actual reward observed for a trajectory. The value function, in contrast, is estimated using a learned model, the critic. For an LLM completion with TT generated tokens, PPO evaluates this quantity at every token-level state-action pair, producing [A^1,A^2,…,A^T][\hat{A}_1, \hat{A}_2, \ldots, \hat{A}_T]: each token position receives its own advantage value.13

The critic that supplies V(st)V(s_t) is usually built by copying the policy and swapping its language modeling head, which outputs one logit per vocabulary token, for a value head: a small linear layer that maps each position’s hidden state to a single scalar. That copy is initialized from the policy’s weights, but it trains on its own objective from then on, so the two networks drift apart as PPO proceeds.

Taking a partial response as input, the critic predicts the expected final reward for every token position in the sequence. Most LLMs are trained with outcome supervision, meaning a reward is only assigned once the model has generated a complete response, after the <eos> token has been emitted. The critic and the reward model are similar in that both are learned models that predict rewards. The difference is what each one is given: the critic predicts an expected reward from a partial completion, while the reward model scores an entire finished response.

Value model versus reward model. A prompt feeds a row of generated tokens, Token 1, Token 2, through Token t, Token T-1, and Token T, the end-of-sequence token. A single wide box labelled Value Model or Critic spans every token and emits one value estimate per position, V of s_1, V of s_2, V of s_t, V of s_T-1 and V of s_T. A separate small box labelled RM sits over the final token only and emits a single reward r_T, braced and labelled the outcome reward setting.
The critic scores every token position, while the reward model fires once, on the finished response. Under outcome supervision the only real reward arrives at the final token.

The value function is on-policy. Unlike the reward model, which is fixed at the start of RL training, the critic is trained alongside the LLM at every policy update so its predictions stay on-policy. This is done by adding an extra mean-squared error (MSE) loss, as in training the value network, to the surrogate loss.

The update in code

Here is the whole thing as code, in two files. ppo.py holds the algorithm: it caches the rollout targets once, then takes several gradient steps against that frozen snapshot. ppo_llm_example.py is the harness around it, wiring up a small pretrained model, a frozen reference copy, and a critic with a value head, then running one update on a batch of three prompts. That batch is small enough to follow by hand, so it is worth walking the tensors through ppo_update once: every shape, every mask, and the numbers that come out at each step are traced in a footnote.14

ppo.py

import torch
import torch.nn.functional as F


def token_log_probs(model, input_ids, attention_mask):
    # Position t predicts token t + 1.
    logits = model(
        input_ids=input_ids,
        attention_mask=attention_mask,
    ).logits[:, :-1]

    logps = F.log_softmax(logits.float(), dim=-1)

    return logps.gather(
        dim=-1,
        index=input_ids[:, 1:].unsqueeze(-1),
    ).squeeze(-1)


def critic_values(critic, input_ids, attention_mask):
    # Assumes CRITIC returns [B, L] or [B, L, 1].
    values = critic(
        input_ids=input_ids,
        attention_mask=attention_mask,
    )

    if values.ndim == 3:
        values = values.squeeze(-1)

    # Value before each target token is generated.
    return values[:, :-1].float()


def response_mean(x, mask):
    # Equal weight per response.
    x = x.masked_fill(~mask, 0.0)
    return (
        x.sum(dim=-1)
        / mask.sum(dim=-1).clamp_min(1)
    ).mean()


def ppo_update(
    LLM,
    REF,
    CRITIC,
    optimizer,
    completions,       # [B, L]: prompt + response + padding
    attention_mask,    # [B, L]: valid prompt and response positions
    completion_mask,   # [B, L - 1]: response target positions only
    outcome_rewards,   # [B]: frozen reward-model scores
    num_ppo_steps=4,
    kl_beta=0.1,
    critic_weight=0.5,
    ppo_eps=0.2,
    gamma=1.0,
    max_grad_norm=1.0,
):
    mask = completion_mask.bool()

    if mask.shape != completions[:, 1:].shape:
        raise ValueError("completion_mask must have shape [B, L - 1].")

    if not mask.any(dim=-1).all().item():
        raise ValueError("Every example must contain a response token.")

    # Disable dropout. eval() still allows gradients.
    LLM.eval()
    REF.eval()
    CRITIC.eval()

    # LLM must still be the policy that generated this rollout.
    # Cache all rollout targets BEFORE any optimizer updates.
    with torch.no_grad():
        old_token_logps = token_log_probs(
            LLM, completions, attention_mask
        )
        ref_token_logps = token_log_probs(
            REF, completions, attention_mask
        )
        old_values = critic_values(
            CRITIC, completions, attention_mask
        )

        # Fixed KL-shaped rollout rewards.
        sampled_kl = old_token_logps - ref_token_logps
        token_rewards = (-kl_beta * sampled_kl).masked_fill(
            ~mask, 0.0
        )

        # Actual final response position, including the prompt offset.
        positions = torch.arange(
            mask.size(1), device=mask.device
        ).unsqueeze(0).expand_as(mask)

        last_indices = positions.masked_fill(
            ~mask, -1
        ).amax(dim=-1)

        rows = torch.arange(mask.size(0), device=mask.device)

        token_rewards[rows, last_indices] += outcome_rewards.to(
            device=token_rewards.device,
            dtype=token_rewards.dtype,
        )

        # Monte Carlo return-to-go.
        # Assumes contiguous responses and terminal episode endings.
        returns = torch.zeros_like(token_rewards)
        running_return = torch.zeros_like(token_rewards[:, 0])

        for t in reversed(range(token_rewards.size(1))):
            running_return = (
                token_rewards[:, t] + gamma * running_return
            )
            running_return = running_return.masked_fill(
                ~mask[:, t], 0.0
            )
            returns[:, t] = running_return

        # Fixed advantages: do NOT recompute after critic updates.
        advantages = (returns - old_values).masked_fill(
            ~mask, 0.0
        )

    trainable_params = [
        parameter
        for group in optimizer.param_groups
        for parameter in group["params"]
        if parameter.requires_grad
    ]

    metrics = []

    # Reuse this rollout for multiple full-batch gradient steps.
    for step in range(num_ppo_steps):
        token_logps = token_log_probs(
            LLM, completions, attention_mask
        )
        values = critic_values(
            CRITIC, completions, attention_mask
        )

        log_ratio = (token_logps - old_token_logps).masked_fill(
            ~mask, 0.0
        )
        ratio = log_ratio.exp()

        clipped_ratio = ratio.clamp(
            1.0 - ppo_eps,
            1.0 + ppo_eps,
        )

        actor_loss = -response_mean(
            torch.minimum(
                ratio * advantages,
                clipped_ratio * advantages,
            ),
            mask,
        )

        critic_loss = response_mean(
            (values - returns).square(),
            mask,
        )

        loss = actor_loss + critic_weight * critic_loss

        optimizer.zero_grad(set_to_none=True)
        loss.backward()

        torch.nn.utils.clip_grad_norm_(
            trainable_params, max_grad_norm
        )

        optimizer.step()

        metrics.append({
            "step": step + 1,
            "loss": loss.detach().item(),
            "actor_loss": actor_loss.detach().item(),
            "critic_loss": critic_loss.detach().item(),
        })

    return metrics

ppo_llm_example.py

"""Run the PPO update on a batch using a pretrained causal language model."""

from copy import deepcopy

import torch
import torch.nn as nn
from transformers import AutoModel, AutoModelForCausalLM, AutoTokenizer

from ppo import ppo_update


MODEL_NAME = "sshleifer/tiny-gpt2"


class TransformerCritic(nn.Module):
    """A pretrained transformer backbone with a token-level value head."""

    def __init__(self, model_name):
        super().__init__()
        self.backbone = AutoModel.from_pretrained(
            model_name,
            local_files_only=True,
        )
        self.value_head = nn.Linear(
            self.backbone.config.hidden_size,
            1,
        )

    def forward(self, input_ids, attention_mask):
        hidden_states = self.backbone(
            input_ids=input_ids,
            attention_mask=attention_mask,
        ).last_hidden_state
        return self.value_head(hidden_states).squeeze(-1)


def make_completion_batch(tokenizer, prompts, responses):
    """Tokenize prompt/response pairs and mark response target positions."""
    sequences = []
    prompt_lengths = []

    for prompt, response in zip(prompts, responses, strict=True):
        prompt_ids = tokenizer.encode(prompt, add_special_tokens=False)
        response_ids = tokenizer.encode(response, add_special_tokens=False)
        sequences.append(
            prompt_ids + response_ids + [tokenizer.eos_token_id]
        )
        prompt_lengths.append(len(prompt_ids))

    max_length = max(map(len, sequences))
    batch_size = len(sequences)
    completions = torch.full(
        (batch_size, max_length),
        tokenizer.pad_token_id,
        dtype=torch.long,
    )
    attention_mask = torch.zeros(
        (batch_size, max_length),
        dtype=torch.long,
    )
    completion_mask = torch.zeros(
        (batch_size, max_length - 1),
        dtype=torch.bool,
    )

    for row, (sequence, prompt_length) in enumerate(
        zip(sequences, prompt_lengths, strict=True)
    ):
        sequence_length = len(sequence)
        completions[row, :sequence_length] = torch.tensor(sequence)
        attention_mask[row, :sequence_length] = 1

        # Shifted index prompt_length - 1 predicts the first response token.
        completion_mask[
            row,
            prompt_length - 1 : sequence_length - 1,
        ] = True

    return completions, attention_mask, completion_mask


def main():
    torch.manual_seed(7)

    print(f"Loading cached model: {MODEL_NAME}", flush=True)
    tokenizer = AutoTokenizer.from_pretrained(
        MODEL_NAME,
        local_files_only=True,
    )
    tokenizer.pad_token = tokenizer.eos_token

    policy = AutoModelForCausalLM.from_pretrained(
        MODEL_NAME,
        local_files_only=True,
    )
    policy.config.pad_token_id = tokenizer.pad_token_id

    reference = deepcopy(policy)
    reference.requires_grad_(False)
    critic = TransformerCritic(MODEL_NAME)

    prompts = [
        "The capital of France is",
        "Two plus two equals",
        "Water freezes at",
    ]
    responses = [
        " Paris.",
        " four.",
        " zero degrees Celsius.",
    ]
    completions, attention_mask, completion_mask = (
        make_completion_batch(tokenizer, prompts, responses)
    )

    # In a real run, these fixed scores come from a reward model or evaluator.
    outcome_rewards = torch.tensor([1.0, 0.7, 0.9])

    optimizer = torch.optim.AdamW(
        [*policy.parameters(), *critic.parameters()],
        lr=5e-5,
    )

    print("Running PPO update...", flush=True)
    metrics = ppo_update(
        LLM=policy,
        REF=reference,
        CRITIC=critic,
        optimizer=optimizer,
        completions=completions,
        attention_mask=attention_mask,
        completion_mask=completion_mask,
        outcome_rewards=outcome_rewards,
        num_ppo_steps=4,
    )

    print(f"model={MODEL_NAME} batch_size={len(prompts)}")
    for prompt, response, reward in zip(
        prompts,
        responses,
        outcome_rewards.tolist(),
        strict=True,
    ):
        print(f"reward={reward:.1f} | {prompt}{response}")

    for metric in metrics:
        print(
            "step={step} loss={loss:.6f} actor_loss={actor_loss:.6f} "
            "critic_loss={critic_loss:.6f}".format(**metric)
        )


if __name__ == "__main__":
    main()

The code above takes the simplest route to an advantage: a Monte Carlo return-to-go, then one subtraction, A^t=Gt−Vold(st)\hat{A}_t = G_t - V_{\text{old}}(s_t). That is unbiased but noisy, since GtG_t is a single sampled trajectory. GAE interpolates toward the low-variance end instead, by discounting a backward sum of one-step TD residuals δt=rt+γV(st+1)−V(st)\delta_t = r_t + \gamma V(s_{t+1}) - V(s_t) with a factor γλ\gamma\lambda, so λ=1\lambda = 1 recovers the Monte Carlo estimate and λ=0\lambda = 0 leaves the single residual. Dropped into ppo_update in place of the return-to-go loop, it looks like this:

gae_lambda = 0.95

advantages = torch.zeros_like(token_rewards)
running_advantage = torch.zeros_like(token_rewards[:, 0])

for t in reversed(range(token_rewards.size(1))):
    if t + 1 < token_rewards.size(1):
        # Bootstrap only if the next position is in the response.
        next_valid = mask[:, t + 1]
        next_value = old_values[:, t + 1].masked_fill(
            ~next_valid, 0.0
        )
    else:
        next_valid = torch.zeros_like(mask[:, t])
        next_value = torch.zeros_like(running_advantage)

    delta = (
        token_rewards[:, t]
        + gamma * next_value
        - old_values[:, t]
    )

    running_advantage = (
        delta
        + gamma * gae_lambda
        * running_advantage.masked_fill(~next_valid, 0.0)
    )

    running_advantage = running_advantage.masked_fill(
        ~mask[:, t], 0.0
    )

    advantages[:, t] = running_advantage

# Fixed critic targets derived from GAE.
returns = (advantages + old_values).masked_fill(~mask, 0.0)

Group Relative Policy Optimization (GRPO)

PPO was the original algorithm used in RLHF, and from a technical standpoint it works perfectly fine to use it in the RL pipeline used to develop reasoning models. However, DeepSeek-R1’s RL pipeline instead used an algorithm called Group Relative Policy Optimization (GRPO), introduced as a variant of Proximal Policy Optimization (PPO) that improves mathematical reasoning ability while reducing PPO’s memory footprint.

GRPO shares a very similar surrogate loss to PPO, but avoids learning a value function, which would otherwise require keeping another copy of the policy language model in memory just to estimate value. That is GRPO’s whole idea: drop the critic, sample a group of responses to the same prompt, and use the spread of their scores as the training signal instead.

What GRPO drops is the critic, not the reward model. Where the group’s scores come from is a separate question, and GRPO is perfectly happy to take them from a learned reward model, which is what the figure below shows. Replacing that reward model with a rule-based check is a different idea, RLVR, covered further down; DeepSeek-R1-Zero is what you get when the two are combined.

Side-by-side comparison of PPO and GRPO. PPO sends a prompt q through the policy model to one output o, which goes to a reference model, a reward model and a trained value model (the critic), combining through GAE into a single advantage A. GRPO samples a group of outputs o1 to oG from the policy model, scores them with a reward model into rewards r1 to rG, and derives advantages A1 to AG by group computation, with the critic's slot left as an empty dashed box. Annotations read: sample multiple answers, GRPO forgoes the critic (value model), and compute average reward of multiple sampled outputs
Annotated figure from DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models, illustrating the differences between PPO and GRPO.

GRPO does this by simplifying value estimation: instead of a learned value function, it assigns the same baseline to every token in an episode, estimated via a Monte Carlo estimate over multiple completions (ai,ja_{i,j}) and their rewards (ri,jr_{i,j}), sampled from the same initial prompt/state (sis_i).

A training step works on a batch of NN prompts s1,…,sNs_1, \ldots, s_N. Note that the subscript has changed meaning: from here on it indexes a prompt in the batch, not a timestep, so sis_i is the ii-th prompt (an initial state) rather than the state after ii tokens. For each prompt sis_i, GRPO samples a group of GG completions ai,1,…,ai,Ga_{i,1}, \ldots, a_{i,G} from the old policy πθold\pi_{\theta_{\text{old}}}. GG is the group size, and three indices matter below: i=1,…,Ni = 1, \ldots, N runs over prompts in the batch, j=1,…,Gj = 1, \ldots, G runs over completions in that prompt’s group, and t=1,…,Ti,jt = 1, \ldots, T_{i,j} runs over the tokens inside completion jj.

The split between the last two is the thing to hold onto, because the two halves of the objective live at different levels. The importance ratio is per token: writing ai,j,ta_{i,j,t} for the tt-th token of completion jj and ai,j,<ta_{i,j,<t} for the tokens before it,

ρi,j,t(θ)=πθ(ai,j,t∣si, ai,j,<t)πθold(ai,j,t∣si, ai,j,<t),\rho_{i,j,t}(\theta) = \frac{\pi_\theta(a_{i,j,t} \mid s_i,\, a_{i,j,<t})}{\pi_{\theta_{\text{old}}}(a_{i,j,t} \mid s_i,\, a_{i,j,<t})},

so a completion of Ti,jT_{i,j} tokens contributes Ti,jT_{i,j} separate ratios.15 The reward and the advantage are per sequence: the reward is scored on the finished completion, so there is a single Ai,jA_{i,j}16 for completion jj and every token in it is pushed by that same number.

Putting the levels together, and estimating the expectations by the batch itself, the objective (or loss) is:

J(θ)≈1N∑i=1N1G∑j=1G1Ti,j∑t=1Ti,jmin⁡(ρi,j,tAi,j, clip(ρi,j,t,1−ε,1+ε)Ai,j)−β DKL(πθ∥πref)J(\theta) \approx \frac{1}{N}\sum_{i=1}^{N} \frac{1}{G}\sum_{j=1}^{G} \frac{1}{T_{i,j}} \sum_{t=1}^{T_{i,j}} \min\left(\rho_{i,j,t} A_{i,j},\ \text{clip}\left(\rho_{i,j,t}, 1-\varepsilon, 1+\varepsilon\right) A_{i,j}\right) - \beta\, \mathcal{D}_{\text{KL}}(\pi_\theta \Vert \pi_{\text{ref}})

Both averages are Monte Carlo estimates, which is why this is an ≈\approx and not an ==. The objective is an expectation over two draws: which prompt comes out of the training set, and which group of GG completions the old policy happens to produce for it. The batch estimates the first with 1N∑i\frac{1}{N}\sum_i and the second with 1G∑j\frac{1}{G}\sum_j. Everything from here to the end of the section happens inside one prompt’s group, with ii along only to say which prompt’s.

Concretely, if completion jj of prompt sis_i is “The answer is 12”, tokenized as ai,j,1=Thea_{i,j,1} = \text{The}, ai,j,2= answera_{i,j,2} = \text{ answer}, ai,j,3= isa_{i,j,3} = \text{ is}, ai,j,4= 12a_{i,j,4} = \text{ 12}, then Ti,j=4T_{i,j} = 4 and that completion contributes four ratios ρi,j,1,…,ρi,j,4\rho_{i,j,1}, \ldots, \rho_{i,j,4}, each multiplied by the same sequence-level advantage Ai,jA_{i,j}.

To make an update meaningful, the advantage Ai,jA_{i,j} is computed by subtracting this baseline from the reward. A positive advantage means the response was better than expected, so its probability should increase; a negative advantage means it was worse than expected, so its probability should decrease.

In PPO, the baseline comes from a learned value function. GRPO instead gets it from the group itself, using the rewards ri,1,…,ri,Gr_{i,1}, \ldots, r_{i,G} of the completions sampled for prompt sis_i:

baselinei=mean(ri,1,…,ri,G),Ai,j=ri,j−baselineistd(ri,1,…,ri,G).\text{baseline}_i = \text{mean}(r_{i,1}, \ldots, r_{i,G}), \qquad A_{i,j} = \frac{r_{i,j} - \text{baseline}_i}{\text{std}(r_{i,1}, \ldots, r_{i,G})}.

Completions that score above the group average get a positive advantage and become more likely; completions that score below average get a negative advantage and become less likely. The model is always learning relative to itself, not some externally defined standard.

Diagram titled 'Let the group be its own baseline': a prompt q and a verifier feed K sampled responses, drawn as bars against a dashed horizontal line marking the group mean; bars above the mean carry upward arrows and bars below carry downward arrows, with a note reading 'no value network, no extra model'
The group mean is the baseline: bars above it are reinforced, bars below are suppressed, and no value network is needed to draw the line. Source: GRPO explained.

One thing the notation hides: πθold\pi_{\theta_{\text{old}}} is not the model as of the previous gradient step. It is frozen when the group is sampled and left alone while several gradient steps run against that group, so on the first of those steps ρi,j,t=1\rho_{i,j,t} = 1 exactly, and it drifts away from 1 only as θ\theta moves off the snapshot that produced the completions. Ai,jA_{i,j} is frozen the same way: the mean and standard deviation that define it were computed from the rewards of πθold\pi_{\theta_{\text{old}}}‘s own samples, so every step taken on this group reuses the same Ai,jA_{i,j}. Advantages are recomputed once per rollout group, not once per optimizer step. PPO’s inner loop works the same way, spelled out above.

Reinforcement Learning with Verifiable Rewards (RLVR)

The training method for these models, Reinforcement Learning with Verifiable Rewards (RLVR)17, proceeds very similarly to RLHF, but it makes the reward model optional in lieu of a scoring function that returns a positive reward when the answer is correct and 0 otherwise.

RLHF collects human preference data, trains a reward model on it, then optimizes the policy against that proxy. RLVR skips all of that when you have a ground truth to check against: the reward signal is the ground truth itself, not a learned approximation of it.

This can be seen as an RL feedback loop where, instead of a reward model, a verification function scores the agent’s completions.

RLVR feedback loop diagram: training data provides prompts to an agent (policy pi-theta), which produces completions; a Verifiable Reward block scores the completion (reward = gamma if correct, else 0) and feeds the scalar reward back to the agent for a policy update
RLVR as an RL feedback loop: a verification function stands in for the reward model. Source: RLHF Book.

See below to see how different it is to score responses for RLHF versus RLVR. In RLHF, a reward model must evaluate subjective qualities. In contrast, RLVR uses verification functions that return definitive scores.

RLHF example: a prompt asking to explain opportunity cost in economics, with an open-ended prose response and no verification step, since a reward model must judge subjective quality
RLHF: scoring is a subjective judgment call. Source: RLHF Book.
RLVR example: a prompt asking for the sum of primes less than 20, with a response boxing the final answer 77, verified by the check extracted_answer == 77 giving Reward = 1
RLVR: scoring is a definitive check against the extracted answer. Source: RLHF Book.
RLVR example for code generation: a prompt asking for a Python Fibonacci function, verified by unit tests (fib(0) == 0, fib(1) == 1, fib(10) == 55), all passing for Reward = 1
RLVR for code generation: verification via unit tests. Source: RLHF Book.

One motivation here is to avoid noisy or expensive human or learned rewards by using automatic correctness checks as the supervision signal during RL. The other is cost: by using “cheap” tools like calculators, we replace both the expensive reward model training and the reward model itself. Since the reward model is usually a whole pre-trained model with a regression head on top, dropping it makes RLVR much more efficient.

DeepSeek-R1 used RLVR with GRPO, which eliminates two expensive models from the training procedure, the reward model and the value model (critic), as illustrated in the figure below.

Three panels comparing how many models each setup keeps. RLHF with PPO holds four: original policy, new policy, critic, and reward model. RLHF with GRPO crosses out the critic, leaving three. RLVR with GRPO crosses out both the critic and the reward model, leaving only the original and new policy
What each setup has to keep in memory: PPO needs a critic and a reward model, GRPO drops the critic, and RLVR drops the reward model too. Source: Sebastian Raschka, “The State of LLM Reasoning Model Training”.

Unlike RLHF, there is no reward model to train, no reward model to overfit, and no proxy misalignment to worry about. But this only works when correctness is cleanly verifiable: for tasks where quality is subjective or multi-dimensional, you are back to needing a learned reward.

prompt

“What is 17 × 24?”
sample 4 answers

① score each answer

o₁
408 ✓
score
1.0
o₂
391 ✗
score
0.0
o₃
408 ✓
score
1.0
o₄
~400 ≈
score
0.5
average the scores

② group average

(1.0 + 0.0 + 1.0 + 0.5) ÷ 4 = 0.625
advantage = score − average

③ who was above or below average?

1.0 − 0.625
+0.38
reinforce ↑
0.0 − 0.625
−0.63
suppress ↓
1.0 − 0.625
+0.38
reinforce ↑
0.5 − 0.625
−0.13
suppress ↓
update weights: more of o₁, o₃ · less of o₂, o₄

Adapted from Castform's GRPO introduction.

Large reasoning models

RLVR and GRPO can be used to improve the reasoning capabilities of LLMs on verifiable tasks, and research on this topic has led to the creation of large reasoning models (LRMs). The key distinction between an LRM and a standard LLM is the ability to dynamically “think” about a prompt prior to providing a final output. By increasing the length of the thinking process, these LRMs can use inference-time scaling, simply spending more compute on generating a completion, to improve their performance.

Architecturally, an LRM and a standard LLM are identical; the whole difference is in how they generate. Special tokens separate the thinking from the answer: the reasoning trajectory is generated first, wrapped between <think> tokens, and the model ends its reasoning process with a </think> token before proceeding to generate its final response.

LRMs also differ from standard LLMs in their training methodology. Both undergo similar pretraining and alignment phases, consisting of supervised finetuning (SFT) and RLHF. LRMs extend that standard process by performing large-scale RLVR on verifiable domains like math and code. Because verifiable reward signals are less prone to reward hacking, this RL training can be run at larger scale, meaning simply running it longer, with less risk of training collapse.

LRMs are not always the best tool to use, though. Because the training process for an LRM is focused on verifiable domains like math and code, its performance may be biased towards those domains, and away from non-verifiable ones like creative writing.

How DeepSeek-R1 Models Were Trained Using RL

The DeepSeek-R1 release was three kinds of model, differing in how much RL and SFT each one got:

  1. DeepSeek-R1-Zero, trained with pure RL.
  2. DeepSeek-R1, trained with instruction fine-tuning (SFT) and RL.
  3. DeepSeek-R1-Distill variants, created via instruction fine-tuning (SFT) without RL.
Training pipeline for the DeepSeek-R1 family: DeepSeek-V3 (671B) is trained with RL using accuracy and format rewards (RLVR) into DeepSeek-R1-Zero, which generates SFT cold-start data; that data plus RL with accuracy, format and consistency rewards produces SFT CoT data, which with rule-based verification and human preference (RLVR plus RLHF) yields DeepSeek-R1; the SFT CoT and knowledge data are then used to fine-tune Llama 3 and Qwen 2.5 into the DeepSeek-R1-Distill-Qwen and DeepSeek-R1-Distill-Llama variants
Training pipeline for the DeepSeek-R1 family. Source: “The State of LLM Reasoning Model Training”, Sebastian Raschka.

DeepSeek-R1-Zero was trained using verifiable rewards (RLVR) with GRPO, starting directly from the DeepSeek-V3 base model: no SFT stage and no human-preference reward model were used for it. That turned out to be sufficient for the model to exhibit reasoning abilities via intermediate-step generation, showing that it is possible to skip the SFT stage altogether. The model improves its reasoning abilities through exploration instead of learning from examples.

Diagram: a base model box, then an SFT warm-up box fed by human-written examples but crossed out with a large X, then an RLVR box, with an arrow running from the base model straight through to RLVR
R1-Zero drops the SFT warm-up on human-written examples and goes from the base model straight into RLVR.

What it runs instead is a loop: sample completions from the current policy, check them with the verifier, update the policy on the result, and repeat.

rollout verify update repeat
The RLVR loop R1-Zero trains in: roll out completions, verify them, update the policy, repeat.

Because there is no SFT stage, and training starts from the base pretrained model, the model has no idea what a good reasoning trace even looks like. Nobody ever showed it one. All it gets is the verifier’s verdict on the final answer.

Line chart of mean response length against RL steps: the curve rises steeply at first, keeps climbing through the middle of training, then flattens out near the end
Mean response length over RL steps. Source: GRPO explained.

Nothing in the reward gives credit for length, yet the mean response length climbs on its own: longer reasoning chains help the model land on verifiable answers more often, and the verifier keeps rewarding that.

So R1-Zero proved reasoning could emerge from pure RL. The catch was that the output was hard to read. The model would slip between languages mid-thought, or produce formatting no human reader could follow. Nothing in the reward signal cared about readability, only correctness.

An R1-Zero output solving 2x + 3 = 17: step 1 is in English, step 2 reads 'donc x = 7, entonces la', mixing French and Spanish mid-thought, and step 3 reads '##ANSWER**<x=7>|| ::end'. Callouts label these 'switches language mid-thought' and 'formatting no reader can follow'
Correct answers, unreadable traces: the reward never asked for readability. Source: GRPO explained.

Emergent abilities from RL

One of the most remarkable discoveries in R1-Zero’s training was the emergence of a phenomenon known as the “Aha Moment”, somewhat similar to how humans experience sudden realizations while problem-solving. It works like this:

  1. The model makes an initial attempt at solving a problem.
  2. It recognizes potential errors or inconsistencies.
  3. It adjusts its approach based on that recognition.
  4. It can explain why the new approach is better.

This ability emerged naturally from RL training, without being explicitly programmed, demonstrating learning rather than mere memorization of a process from the training data.

An R1-Zero transcript solving an equation with nested square roots. Partway through the working the model writes, in red, 'Wait, wait. Wait. That's an aha moment I can flag here.', then restarts the derivation step by step from the original equation
An interesting aha moment from DeepSeek-R1-Zero.

More detail on all of this is in the Hugging Face LLM course’s chapter on the breakthrough aha moment.

DeepSeek-R1 is the flagship model, the one with the best performance. The difference compared to DeepSeek-R1-Zero is that they alternated instruction fine-tuning, RLVR, and RLHF. The paper demonstrated clearly that supervised fine-tuning followed by reinforcement learning outperforms RL alone.

The fundamental claim behind DeepSeek-R1 and R1-Zero is that RLVR explicitly induces reasoning capabilities. However, recent findings suggest that reasoning behaviors, including the “Aha moment” described above, might already be present in base models because of pre-training on extensive chain-of-thought data. One critical study finds that DeepSeek-V3-Base already exhibits an “Aha moment” before any RL, and attributes the reasoning of some base models to pretraining biases.[1] A separate line of work plants deliberate errors in reasoning chains and measures whether the model catches them, finding that self-reflection and self-correction emerge progressively throughout pre-training, across various domains and model sizes.[2] Both further complicate attributing reasoning capabilities solely to RL.

A broader evaluation of RLVR reaches the same conclusion by looking beyond pass@1, comparing base and RLVR-trained models across model families, RL algorithms, and math, coding, and visual-reasoning benchmarks.[3] RLVR models usually do better on pass@1, meaning they are more likely to produce a correct solution on the first attempt. But pass@k counts a problem as solved if any of kk sampled answers is correct; at large values of kk, commonly 128–1024, base models often solve more distinct problems than their RLVR counterparts. The authors argue that current RLVR therefore improves sampling efficiency by shifting probability toward rewarded reasoning paths that the base model could already produce, rather than teaching fundamentally new reasoning patterns. Off-policy distillation is different: supervised fine-tuning on trajectories from a stronger teacher can transfer patterns outside the student’s original reasoning distribution and expand what it can solve.

Zhou et al. (2023) call this broader view the Superficial Alignment Hypothesis in LIMA: Less Is More for Alignment.[4] The hypothesis says that knowledge and capabilities are learned primarily during pre-training, while alignment mostly teaches the model which behaviors and response formats to produce. LIMA fine-tunes a 65B LLaMA model on only 1,000 carefully curated examples, without reinforcement learning or human preference modeling, and finds that this small dataset is sufficient to teach strong instruction following and response formatting. From this perspective, RLVR’s pass@1 gain is an alignment effect: it makes an existing correct reasoning path more likely to appear.

Taken together, the evidence suggests a distinction between exposing capacity and expanding it. Current RLVR reliably turns latent reasoning into reliable behavior, but does not necessarily expand the model’s underlying reasoning boundary. In short, RL only shapes behavior; it does not teach new knowledge well. Distillation can expand that boundary by directly supplying new chain-of-thought patterns, and pre-training on data that contains such patterns may do the same before post-training begins.

Issues That Come Up for RL for LLMs

1. Policy entropy collapse

Reinforcement learning improves a policy by exploring alternatives, but every reward also concentrates probability on what already worked. If that concentration goes too far, exploration dies. That failure is policy entropy collapse.

Early in RL training the policy is still exploring: for a given prompt it can generate many different reasoning paths, spreading probability across a wide range of next tokens. As RL runs, that exploration can quietly die. Instead of a healthy spread of options, probability collapses onto a single repeated token.

Bar chart of next-token probabilities early in training: eight bars of roughly similar height, labelled a healthy spread of options
Early on: probability is spread across many candidate tokens. Source: GRPO explained.
Bar chart of next-token probabilities after collapse: one tall bar with all the others flattened to near zero, labelled probability collapses onto one token, above a row of identical boxes each reading then
After collapse: one token takes everything, and the output repeats. Source: GRPO explained.

The policy has stopped exploring. It just repeats whatever already scores well, and once exploration is gone there is nothing left to discover.

Line chart of reward against RL steps: the curve rises steeply then flattens, with the flat portion drawn in red
Reward rises for a while, then flattens. Source: GRPO explained.

The reward curve rises for a while and then goes flat, not because the model solved the task but because it stopped trying anything new.

2. Objective bias from the normalization terms

The bias here is not in the data, it is sitting inside the objective itself. The clean objective built above is not quite neutral. Look again at its two normalization terms, the 1Ti,j\frac{1}{T_{i,j}} that divides by response length and the std(ri,1,…,ri,G)\text{std}(r_{i,1}, \ldots, r_{i,G}) that divides the advantage by the group’s spread. Each one quietly bends the gradient in a direction that has nothing to do with whether the answer was right.

Length. Take two answers to the same problem, both correct, both earning a reward of 1, but one short and one long. Because the advantage is divided by the length of the response, the long answer’s signal gets diluted across more tokens. The optimizer reads that as “longer was better” and starts rewarding length for its own sake. That is not a signal about reasoning, it is an artifact of the math.

Diagram of the length divisor 1 over the size of completion i: one problem with two sampled answers, both correct. The short answer's four tokens each carry advantage over 4, while the long answer's twelve tokens each carry advantage over 12, captioned same reward, thinner per-token signal, the long answer gets diluted
Same reward, thinner per-token signal: dividing by length dilutes the longer answer. Source: GRPO explained.

Spread. The standard deviation term does something similar across prompts. It overweights problems that are trivially easy or impossibly hard, where every sample in the group earns nearly the same reward, so std(r)\text{std}(r) sits near zero and dividing by it inflates whatever tiny differences are left. The prompts that actually discriminate, where the group genuinely disagrees, get weighted down relative to those.

Diagram of the standard-deviation divisor across three prompts: a trivially easy problem where all four samples are correct and std of r is about 0, a real problem where two of four are correct and std of r is 0.5, and an impossibly hard problem where no samples are correct and std of r is about 0. Bars below show the easy and hard prompts weighted large and the real problem weighted small
Near-zero spread on the easy and impossible prompts inflates their weight; the discriminating problem in the middle is weighted down. Source: GRPO explained.

The fix from the Dr. GRPO[1] work is to recognize these as objective biases, properties of the loss itself rather than bugs in the data, and simply drop both normalization terms.

TODO: more issues to come.

GRPO vs GSPO

TODO: write this section, from “From GRPO to DAPO and GSPO: What, Why, and How”.

TODO

  1. Refer to the Castform GRPO website.
  2. RLHF Book: Reasoning
  3. “The State of LLM Reasoning Model Training”, Sebastian Raschka
  4. “Group Relative Policy Optimization (GRPO)”, Cameron R. Wolfe
  5. “Direct Preference Optimization (DPO)”, Cameron R. Wolfe

Footnotes

  1. Indexing from s0s_0 here, rather than from s1s_1 as in the trajectory definition in the policy gradients note, so that the subscript on sts_t reads directly as a token count. Under that note’s 1-indexed convention the prompt alone is s1s_1 and this same state would be s3s_3. Nothing downstream depends on which one you pick; it just has to be fixed before counting. ↩

  2. Worked through on one example. Take the prompt xx = “What is 2 + 2?”, and suppose the model produces “The answer is 4”, tokenized as t1t_1 = “The”, t2t_2 = ” answer”, t3t_3 = ” is”, t4t_4 = ” 4”. A verifier checks the final answer and returns r=+1r = +1. Write sk−1={x,t1,…,tk−1}s_{k-1} = \{x, t_1, \ldots, t_{k-1}\} for the state the kk-th token was sampled from, so s0={x}s_0 = \{x\} is the prompt alone.

    Under the bandit formulation the whole completion is a single action, and its probability is what an autoregressive model already computes by chaining per-token probabilities:

    πθ(t1,t2,t3,t4∣x)=πθ(t1∣x) πθ(t2∣x,t1) πθ(t3∣x,t1,t2) πθ(t4∣x,t1,t2,t3)\pi_\theta(t_1, t_2, t_3, t_4 \mid x) = \pi_\theta(t_1 \mid x) \, \pi_\theta(t_2 \mid x, t_1) \, \pi_\theta(t_3 \mid x, t_1, t_2) \, \pi_\theta(t_4 \mid x, t_1, t_2, t_3)

    Taking logs turns that product into a sum, so the update for this one action is

    ∇θlog⁡πθ(completion∣x) r=[∑k=14∇θlog⁡πθ(tk∣sk−1)]r\nabla_\theta \log \pi_\theta(\text{completion} \mid x) \, r = \left[ \sum_{k=1}^{4} \nabla_\theta \log \pi_\theta(t_k \mid s_{k-1}) \right] r

    Under the per-token MDP formulation there are instead four actions, a1a_1 = “The” through a4a_4 = ” 4”, and the sum is there from the start rather than arriving by expansion:

    ∑k=14∇θlog⁡πθ(ak∣sk−1) r\sum_{k=1}^{4} \nabla_\theta \log \pi_\theta(a_k \mid s_{k-1}) \, r

    These are algebraically the same if every token gets the same final reward. The bandit view has one scalar for the whole response, so all it can say is “this completion scored +1+1, make it more likely.” The MDP view has a per-action slot, which can hold a different number for every token:

    ∑k∇θlog⁡πθ(ak∣sk−1) A^k\sum_{k} \nabla_\theta \log \pi_\theta(a_k \mid s_{k-1}) \, \hat{A}_k

    If the advantages came out as +0.1+0.1, +0.2+0.2, +0.1+0.1, +0.8+0.8 across the four tokens here, the update would push hardest on ” 4”, the token that actually carried the answer, rather than reinforcing “The answer is” just as strongly for having been in the same lucky sentence. Filling that slot is exactly what PPO’s critic is for, and what GRPO gives up when it drops the critic and falls back to one number per completion. ↩

  3. This framing follows Cameron R. Wolfe, “REINFORCE: Easy Online RL for LLMs”. ↩

  4. The loss line in the walkthrough is a surrogate: not a meaningful number in itself, but a scalar whose gradient is the estimator above. Ignoring the mask and the averaging for the moment, it is

    L=−∑tlog⁡πθ(at∣st) r(τ)L = -\sum_t \log \pi_\theta(a_t \mid s_t) \, r(\tau)

    and since r(τ)r(\tau) comes from the environment rather than the network, autograd treats it as a constant and carries it straight through:

    ∇θL=−∑t∇θlog⁡πθ(at∣st) r(τ)\nabla_\theta L = -\sum_t \nabla_\theta \log \pi_\theta(a_t \mid s_t) \, r(\tau)

    which is the policy gradient, negated. The minus sign is there because torch descends, and this objective is one we want to climb. ↩

  5. The bandit formulation makes a simplifying choice: the state ss is the prompt xx, the action aa is the entire completion, and the reward r(x,a)r(x, a) arrives only once the whole completion has been seen. The advantage is then whatever multiplies the log-probability in the gradient above, which is one number for the completion as a whole:

    A(x,a)=r(x,a)−rˉA(x, a) = r(x, a) - \bar{r}

    Suppose the prompt “What is 2 + 2?” is sampled four times, and the baseline rˉ=0.5\bar{r} = 0.5 is the average of the four rewards that come back:

    CompletionRewardAdvantage =r−rˉ= r - \bar{r}
    “4”1.01.0−0.5=+0.51.0 - 0.5 = +0.5
    ”The answer is 4.”1.01.0−0.5=+0.51.0 - 0.5 = +0.5
    ”5”0.00.0−0.5=−0.50.0 - 0.5 = -0.5
    ”It is 3.”0.00.0−0.5=−0.50.0 - 0.5 = -0.5

    The update increases the probability of the completions with positive advantage and decreases the probability of those with negative advantage. Since the action is the whole sequence y=(t1,t2,t3,t4)y = (t_1, t_2, t_3, t_4), the estimate is

    ∇θJ≈1N∑i[∑k∇θlog⁡πθ(ti,k∣si,k−1)]Ai\nabla_\theta J \approx \frac{1}{N} \sum_i \left[ \sum_k \nabla_\theta \log \pi_\theta(t_{i,k} \mid s_{i,k-1}) \right] A_i

    and the same AiA_i multiplies every token in completion ii. “The answer is 4.” and “4” are both reinforced by +0.5+0.5 across every one of their tokens, with nothing distinguishing the token that carried the answer from the ones that set it up. ↩

  6. To put numbers on it, take the same prompt “What is 2 + 2?” with the policy generating “The answer is 4”, tokenized as t1t_1 = “The”, t2t_2 = ” answer”, t3t_3 = ” is”, t4t_4 = ” 4”, t5t_5 = <eos>. The answer is correct, so the verifier returns r=+1r = +1, and in the outcome-reward setting that single number lands on <eos>.

    The KL term is computed differently: token by token, against two models, the policy πθ\pi_\theta we are currently training and the reference πref\pi_{\text{ref}}, usually the original SFT checkpoint. For each generated token we compare the probability the two assign to the token that was actually produced. The per-token quantity is the log of their ratio, which is the same thing as the difference of the logs:

    log⁡πθ(t∣s)πref(t∣s)=log⁡πθ(t∣s)−log⁡πref(t∣s)\log \frac{\pi_\theta(t \mid s)}{\pi_{\text{ref}}(t \mid s)} = \log \pi_\theta(t \mid s) - \log \pi_{\text{ref}}(t \mid s)
    Tokenπθ\pi_\thetaπref\pi_{\text{ref}}log⁡πθ−log⁡πref\log \pi_\theta - \log \pi_{\text{ref}}
    ”The”0.400.45−0.118-0.118
    ” answer”0.700.65+0.074+0.074
    ” is”0.800.82−0.025-0.025
    ” 4”0.900.85+0.057+0.057

    Summing that last column over the sequence gives the completion-level KL approximation, −0.012-0.012, and with a penalty coefficient β=0.1\beta = 0.1 the adjusted reward is

    r^=r−β∑k(log⁡πθ(tk∣sk−1)−log⁡πref(tk∣sk−1))=1−0.1×(−0.012)=1.0012\hat{r} = r - \beta \sum_k \left( \log \pi_\theta(t_k \mid s_{k-1}) - \log \pi_{\text{ref}}(t_k \mid s_{k-1}) \right) = 1 - 0.1 \times (-0.012) = 1.0012

    The policy has barely moved from the reference on this completion, so the penalty is almost nothing. It only bites once the two distributions separate, which is the whole point of the term: it is a leash on how far training can drag the model, not a second reward signal. ↩

  7. For example, take three prompts:

    x1="What is 2 + 2?",x2="What is 3 + 3?",x3="What is 5 + 5?"x_1 = \text{"What is 2 + 2?"}, \quad x_2 = \text{"What is 3 + 3?"}, \quad x_3 = \text{"What is 5 + 5?"}

    Standard REINFORCE might sample one answer per prompt:

    PromptCompletionReward
    x1x_1”4”1.0
    x2x_2”6”1.0
    x3x_3”11”0.0

    A simple approach is to take the batch average as the baseline:

    b=1+1+03=0.667b = \frac{1 + 1 + 0}{3} = 0.667

    which gives:

    PromptRewardAdvantage =r−b= r - b
    x1x_11.01.0−0.667=+0.3331.0 - 0.667 = +0.333
    x2x_21.01.0−0.667=+0.3331.0 - 0.667 = +0.333
    x3x_30.00.0−0.667=−0.6670.0 - 0.667 = -0.667
    ↩
  8. Take a single prompt, xx = “What is 2 + 2?”, and instead of generating one answer generate K=4K = 4:

    CompletionReward
    y1y_1: “4”1.0
    y2y_2: “The answer is 4”0.9
    y3y_3: “5”0.0
    y4y_4: “Four”1.0

    RLOO now evaluates each completion against the other completions for the same prompt. For y1y_1 the baseline leaves y1y_1 out and averages the remaining three, and the advantage is that completion’s own reward minus it:

    CompletionRewardLeave-one-out baselineAdvantage
    ”4”1.00.9+0.0+1.03=0.633\frac{0.9 + 0.0 + 1.0}{3} = 0.6331.0−0.633=+0.3671.0 - 0.633 = +0.367
    ”The answer is 4”0.91.0+0.0+1.03=0.667\frac{1.0 + 0.0 + 1.0}{3} = 0.6670.9−0.667=+0.2330.9 - 0.667 = +0.233
    ”5”0.01.0+0.9+1.03=0.967\frac{1.0 + 0.9 + 1.0}{3} = 0.9670.0−0.967=−0.9670.0 - 0.967 = -0.967
    ”Four”1.01.0+0.9+0.03=0.633\frac{1.0 + 0.9 + 0.0}{3} = 0.6331.0−0.633=+0.3671.0 - 0.633 = +0.367

    Every completion gets a different baseline, because each one is an average over a different three. ↩

  9. Suppose the prompt is

    x="The capital of France is"x = \text{"The capital of France is"}

    and the sampled completion is

    y=(Paris,<eos>).y = (\texttt{Paris}, \texttt{<eos>}).

    The two models assign these next-token probabilities:

    TokenCurrent policyReference policy
    Paris0.700.700.500.50
    <eos> after Paris0.800.800.900.90

    The probability of the entire completion is the product of its token probabilities:

    πθ(y∣x)=0.70×0.80=0.56,\pi_\theta(y \mid x) = 0.70 \times 0.80 = 0.56, πref(y∣x)=0.50×0.90=0.45.\pi_{\mathrm{ref}}(y \mid x) = 0.50 \times 0.90 = 0.45.

    The completion-level probability ratio is therefore

    πθ(y∣x)πref(y∣x)=0.560.45≈1.244.\frac{\pi_\theta(y \mid x)}{\pi_{\mathrm{ref}}(y \mid x)} = \frac{0.56}{0.45} \approx 1.244.

    Its log ratio is

    log⁡0.560.45≈0.219.\log \frac{0.56}{0.45} \approx 0.219.

    This shows that the current model considers the entire completion about 1.2441.244 times as likely as the reference model does. ↩

  10. Continuing the earlier completion-probability example, use the simplest log-ratio estimator. Suppose

    x="The capital of France is"x = \text{"The capital of France is"}

    and the current policy samples

    y=(Paris,<eos>).y = (\texttt{Paris}, \texttt{<eos>}).

    An LLM generates tokens one at a time:

    p(y∣x)=∏t=1Tp(yt∣x,y<t).p(y \mid x) = \prod_{t=1}^{T} p(y_t \mid x, y_{<t}).

    Each token probability is conditional on the prompt and all preceding tokens. Taking logs turns that product into a sum:

    log⁡p(y∣x)=∑t=1Tlog⁡p(yt∣x,y<t).\log p(y \mid x) = \sum_{t=1}^{T} \log p(y_t \mid x, y_{<t}).

    So the sequence log ratio becomes a sum of token-level log ratios:

    log⁡πθ(y∣x)πref(y∣x)=log⁡πθ(y∣x)−log⁡πref(y∣x)=∑t=1T[log⁡πθ(yt∣x,y<t)−log⁡πref(yt∣x,y<t)].\begin{aligned} \log \frac{\pi_\theta(y \mid x)}{\pi_{\mathrm{ref}}(y \mid x)} &= \log \pi_\theta(y \mid x) - \log \pi_{\mathrm{ref}}(y \mid x) \\ &= \sum_{t=1}^{T} \left[ \log \pi_\theta(y_t \mid x, y_{<t}) - \log \pi_{\mathrm{ref}}(y_t \mid x, y_{<t}) \right]. \end{aligned}

    I. Generate from the current policy. The completion is sampled from πθ\pi_\theta, not from πref\pi_{\mathrm{ref}}:

    y∼πθ(⋅∣x).y \sim \pi_\theta(\cdot \mid x).

    II. Obtain each token’s log-probability.

    TokenCurrent policyCurrent log-probabilityReference policyReference log-probability
    Paris0.700.70log⁡0.70=−0.357\log 0.70 = -0.3570.500.50log⁡0.50=−0.693\log 0.50 = -0.693
    <eos>0.800.80log⁡0.80=−0.223\log 0.80 = -0.2230.900.90log⁡0.90=−0.105\log 0.90 = -0.105

    Both models evaluate the same sampled tokens.

    III. Sum the token log-probabilities. For the current policy,

    log⁡πθ(y∣x)=−0.357−0.223=−0.580.\log \pi_\theta(y \mid x) = -0.357 - 0.223 = -0.580.

    For the reference policy,

    log⁡πref(y∣x)=−0.693−0.105=−0.798.\log \pi_{\mathrm{ref}}(y \mid x) = -0.693 - 0.105 = -0.798.

    IV. Subtract the sequence log-probabilities.

    log⁡πθ(y∣x)−log⁡πref(y∣x)=−0.580−(−0.798)=0.218≈0.219.\log \pi_\theta(y \mid x) - \log \pi_{\mathrm{ref}}(y \mid x) = -0.580 - (-0.798) = 0.218 \approx 0.219.

    This is equivalent to the completion-level calculation:

    log⁡πθ(y∣x)πref(y∣x)=log⁡0.560.45≈0.219.\log \frac{\pi_\theta(y \mid x)}{\pi_{\mathrm{ref}}(y \mid x)} = \log \frac{0.56}{0.45} \approx 0.219. ↩
  11. Using k1k_1, the prompts in a Monte Carlo batch do not have to match. Suppose we sample four prompts from D\mathcal{D} and one completion from the current policy for each:

    iiPrompt x(i)x^{(i)}Sampled completion y(i)y^{(i)}Current πθ(y(i)∣x(i))\pi_\theta(y^{(i)} \mid x^{(i)})Reference πref(y(i)∣x(i))\pi_{\mathrm{ref}}(y^{(i)} \mid x^{(i)})Log ratio
    1”The capital of France is”Paris <eos>0.560.560.450.45log⁡(0.56/0.45)=0.219\log(0.56 / 0.45) = 0.219
    2”What is 2 + 2?”4 <eos>0.720.720.600.60log⁡(0.72/0.60)=0.182\log(0.72 / 0.60) = 0.182
    3”The daytime sky is”blue <eos>0.400.400.500.50log⁡(0.40/0.50)=−0.223\log(0.40 / 0.50) = -0.223
    4”The opposite of hot is”cold <eos>0.300.300.250.25log⁡(0.30/0.25)=0.182\log(0.30 / 0.25) = 0.182

    The Monte Carlo estimate is the average across the four prompt-completion pairs:

    D^KL=0.219+0.182−0.223+0.1824=0.090.\widehat{D}_{\mathrm{KL}} = \frac{0.219 + 0.182 - 0.223 + 0.182}{4} = 0.090.

    Each row’s log ratio is exact for that sampled completion conditioned on its own prompt. What is approximated is the expectation over prompts and completions. With more independently sampled pairs, the empirical average approaches the true prompt-averaged KL divergence.

    Individual log ratios can be negative. Their exact expectation over the prompt and current-policy completion distributions is guaranteed to be nonnegative by Gibbs’ inequality. A finite-sample Monte Carlo estimate can still be negative because of sampling noise. ↩

  12. Suppose a PPO batch contains two prompts, each with a two-token completion, and let β=0.1\beta = 0.1. The reward model assigns reward only to <eos>. For each sampled token, apply k1k_1: subtract the reference log-probability from the rollout policy’s log-probability, then subtract the scaled result from the token reward:

    PromptTokenOriginal reward rtr_tlog⁡πθold\log \pi_{\theta_{\text{old}}}log⁡πref\log \pi_{\mathrm{ref}}Sampled KL termAdjusted reward rt−βktr_t - \beta k_t
    “The capital of France is”Paris00−0.357-0.357−0.693-0.693−0.357−(−0.693)=+0.336-0.357 - (-0.693) = +0.3360−0.1(0.336)=−0.03360 - 0.1(0.336) = -0.0336
    ”The capital of France is”<eos>1.01.0−0.223-0.223−0.105-0.105−0.223−(−0.105)=−0.118-0.223 - (-0.105) = -0.1181.0−0.1(−0.118)=1.01181.0 - 0.1(-0.118) = 1.0118
    ”What is 2 + 2?”400−0.223-0.223−0.511-0.511−0.223−(−0.511)=+0.288-0.223 - (-0.511) = +0.2880−0.1(0.288)=−0.02880 - 0.1(0.288) = -0.0288
    ”What is 2 + 2?”<eos>0.80.8−0.105-0.105−0.223-0.223−0.105−(−0.223)=+0.118-0.105 - (-0.223) = +0.1180.8−0.1(0.118)=0.78820.8 - 0.1(0.118) = 0.7882

    The sampled sequence-level KL terms are

    k(1)=0.336−0.118=0.218,k(2)=0.288+0.118=0.406.k^{(1)} = 0.336 - 0.118 = 0.218, \qquad k^{(2)} = 0.288 + 0.118 = 0.406.

    Their batch average, which can be logged or supplied to an adaptive KL controller, is

    k‾=0.218+0.4062=0.312.\overline{k} = \frac{0.218 + 0.406}{2} = 0.312.

    Meanwhile, the KL-adjusted total rewards used to compute returns and advantages are 0.97820.9782 for the first completion and 0.75940.7594 for the second:

    1.0−0.1(0.218)=0.9782,0.8−0.1(0.406)=0.7594.1.0 - 0.1(0.218) = 0.9782, \qquad 0.8 - 0.1(0.406) = 0.7594.

    In the alternative formulation, nothing touches the reward. The token rewards keep their original values, so returns and advantages are computed from completion totals of 1.01.0 and 0.80.8, and the same k‾\overline{k} enters as an extra term in the loss. Reusing the naming from the walkthrough above:

    loss=−final score+βk‾=−final score+0.1(0.312)=−final score+0.0312.\text{loss} = -\text{final score} + \beta \overline{k} = -\text{final score} + 0.1(0.312) = -\text{final score} + 0.0312.

    Implementations that average the KL per token rather than per completion use (0.336−0.118+0.288+0.118)/4=0.156(0.336 - 0.118 + 0.288 + 0.118)/4 = 0.156 and add 0.01560.0156 instead.

    “Final score” is that walkthrough’s clipped surrogate, min⁡(ρtA^t,clip⁡(ρt)A^t)\min(\rho_t \hat{A}_t, \operatorname{clip}(\rho_t) \hat{A}_t), and it is the one quantity here that stays symbolic. Numbering it would need two things this example does not carry: the current policy’s log-probabilities, since the table holds only the frozen πθold\pi_{\theta_{\text{old}}} and πref\pi_{\mathrm{ref}}, and a value estimate to turn rewards into advantages. Its formula is the same on both routes, but the term itself is not: the reward route feeds it advantages built from the adjusted rewards 0.97820.9782 and 0.75940.7594, while the loss route feeds it advantages built from 1.01.0 and 0.80.8.

    So the same 0.3120.312 shows up on both routes, but it enters at a different place. On the reward route it is folded into the advantages before any gradient is taken, and since the log ratio is measured against the frozen πθold\pi_{\theta_{\text{old}}} and πref\pi_{\mathrm{ref}}, it is a constant that only shifts those advantage numbers. On the loss route the penalty is recomputed from the current πθ\pi_\theta at every inner step, so it carries a gradient of its own that pulls πθ\pi_\theta back toward πref\pi_{\mathrm{ref}} directly. ↩

  13. Work through one rollout. Suppose the prompt is “What is 12 × 7?” and the model generates 12 × 7 = 84. Treat 84 as the terminal action for this simplified example; an implementation that includes <eos> explicitly would place the outcome reward there instead. With sts_t defined as the prompt plus everything generated before token tt, the rollout is

    s1→a1s2→a2s3→a3s4→a4s5→a5s6,(a1,a2,a3,a4,a5)=(12,×,7,=,84).s_1 \xrightarrow{a_1} s_2 \xrightarrow{a_2} s_3 \xrightarrow{a_3} s_4 \xrightarrow{a_4} s_5 \xrightarrow{a_5} s_6, \qquad (a_1, a_2, a_3, a_4, a_5) = (\texttt{12}, \times, \texttt{7}, \texttt{=}, \texttt{84}).

    PPO does not directly observe which token caused the final success. It constructs a learning signal for each token from the rewards and a learned value function.

    1. Record the rewards and critic values. Suppose the finished response earns Rfinal=1R_{\mathrm{final}} = 1, with no environment reward before termination. Imagine the critic assigns these values to the five states from which the tokens were sampled:

    ttToken ata_tState sts_tImmediate reward rtr_tCritic value V(st)V(s_t)
    112prompt only000.300.30
    2×prompt + 12000.400.40
    37prompt + 12 ×000.450.45
    4=prompt + 12 × 7000.650.65
    584prompt + 12 × 7 =110.750.75

    The state after 84 is terminal, so PPO masks its bootstrap value and uses V(s6)=0V(s_6)=0. The immediate rewards alone, [0,0,0,0,1][0, 0, 0, 0, 1], do not assign credit to the earlier tokens.

    2. Compute one-step TD residuals. For each token, the residual

    δt=rt+γV(st+1)−V(st)\delta_t = r_t + \gamma V(s_{t+1}) - V(s_t)

    asks whether the next state turned out better or worse than the critic expected. With γ=1\gamma=1:

    TokenTD residual
    12δ1=0+0.40−0.30=+0.10\delta_1 = 0 + 0.40 - 0.30 = +0.10
    ×δ2=0+0.45−0.40=+0.05\delta_2 = 0 + 0.45 - 0.40 = +0.05
    7δ3=0+0.65−0.45=+0.20\delta_3 = 0 + 0.65 - 0.45 = +0.20
    =δ4=0+0.75−0.65=+0.10\delta_4 = 0 + 0.75 - 0.65 = +0.10
    84δ5=1+0−0.75=+0.25\delta_5 = 1 + 0 - 0.75 = +0.25

    3. Propagate future credit backward with GAE. Using γ=1\gamma=1 and λ=0.9\lambda=0.9, compute the advantages backward with

    A^t=δt+γλA^t+1,A^5=δ5.\hat{A}_t = \delta_t + \gamma\lambda\hat{A}_{t+1}, \qquad \hat{A}_5 = \delta_5.
    TokenAdvantage calculation
    84A^5=0.25\hat{A}_5 = 0.25
    =A^4=0.10+0.9(0.25)=0.325\hat{A}_4 = 0.10 + 0.9(0.25) = 0.325
    7A^3=0.20+0.9(0.325)=0.4925\hat{A}_3 = 0.20 + 0.9(0.325) = 0.4925
    ×A^2=0.05+0.9(0.4925)=0.49325\hat{A}_2 = 0.05 + 0.9(0.4925) = 0.49325
    12A^1=0.10+0.9(0.49325)=0.543925\hat{A}_1 = 0.10 + 0.9(0.49325) = 0.543925

    Rounding to three decimals gives one advantage per token:

    [A^1,A^2,A^3,A^4,A^5]=[0.544,0.493,0.493,0.325,0.250].[\hat{A}_1, \hat{A}_2, \hat{A}_3, \hat{A}_4, \hat{A}_5] = [0.544, 0.493, 0.493, 0.325, 0.250].

    This is the backward propagation of the final outcome through the rollout:

    final reward⟶TD residuals⟶GAE⟶(A^1,A^2,…,A^T).\boxed{\text{final reward} \longrightarrow \text{TD residuals} \longrightarrow \text{GAE} \longrightarrow (\hat{A}_1, \hat{A}_2, \ldots, \hat{A}_T)}.

    4. Apply PPO at every token. To avoid confusing the importance ratio with the reward rtr_t, call the ratio ρt\rho_t:

    ρt(θ)=πθ(at∣st)πθold(at∣st).\rho_t(\theta) = \frac{\pi_\theta(a_t \mid s_t)}{\pi_{\theta_{\mathrm{old}}}(a_t \mid s_t)}.

    Each token contributes its own clipped surrogate term,

    LtCLIP(θ)=min⁡ ⁣(ρt(θ)A^t,clip⁡(ρt(θ),1−ϵ,1+ϵ)A^t).L_t^{\mathrm{CLIP}}(\theta) = \min\!\left( \rho_t(\theta)\hat{A}_t, \operatorname{clip}(\rho_t(\theta), 1-\epsilon, 1+\epsilon)\hat{A}_t \right).

    Every advantage in this example is positive, so each term encourages a higher probability for its sampled token, subject to clipping:

    TokenAdvantageToken-level effect
    12+0.544+0.544Reinforce πθ(12∣prompt)\pi_\theta(\texttt{12} \mid \text{prompt})
    ×+0.493+0.493Reinforce πθ(×∣prompt,12)\pi_\theta(\times \mid \text{prompt}, \texttt{12})
    7+0.493+0.493Reinforce πθ(7∣prompt,12 ×)\pi_\theta(\texttt{7} \mid \text{prompt}, \texttt{12}\,\times)
    =+0.325+0.325Reinforce πθ(=∣…)\pi_\theta(\texttt{=} \mid \ldots)
    84+0.250+0.250Reinforce πθ(84∣…)\pi_\theta(\texttt{84} \mid \ldots)

    The resulting numbers are critic-dependent credit signals, not proof that PPO has identified which token was causally responsible for the correct answer. ↩

  14. The batch in ppo_llm_example.py is three prompts, so ppo_update can be followed all the way through by hand. Here it is, tensor by tensor.

    The batch is B=3B = 3 samples padded out to L=8L = 8 token positions, and ppo_update takes four tensors built from it:

    completions      [3, 8]   prompt + response + padding
    attention_mask   [3, 8]   which positions hold a real token
    completion_mask  [3, 7]   which next-token predictions are the response
    outcome_rewards  [3]      one score per sample

    The padding is only there to make the batch rectangular, which is what GPUs want. The three examples have different lengths on their own:

    completions[0]:  The capital of France is Paris . <eos>
    completions[1]:  Two plus two equals four . <eos> PAD
    completions[2]:  Water freezes at zero degrees Celsius . <eos>

    attention_mask marks the real tokens, so the models ignore the pad:

    sample 1:  1 1 1 1 1 1 1 1
    sample 2:  1 1 1 1 1 1 1 0
    sample 3:  1 1 1 1 1 1 1 1

    completion_mask is one position shorter, because it lines up with next-token predictions rather than with tokens, and it marks only the predictions that belong to the response:

    sample 1:  0 0 0 0 1 1 1    prompt is 5 tokens, so the response starts at prediction 4
    sample 2:  0 0 0 1 1 1 0    prompt is 4 tokens; the last slot predicts padding
    sample 3:  0 0 1 1 1 1 1    prompt is 3 tokens

    And outcome_rewards is [1.0, 0.7, 0.9], one frozen reward-model score per completion.

    The rollout targets. Everything up to the inner loop happens inside torch.no_grad() and is computed exactly once. First the log-probabilities under the policy that generated this batch:

    old_token_logps = token_log_probs(LLM, completions, attention_mask)

    token_log_probs runs the whole batch through the model and drops the final position, since nothing follows it. Before that slice the logits are [3, 8, vocabulary_size]. The seven prediction tasks left in the first row are:

    The      → capital
    capital  → of
    of       → France
    France   → is
    is       → Paris
    Paris    → .
    .        → <eos>

    log_softmax turns each position’s vocabulary scores into log-probabilities, one for every token that could have come next. PPO only needs the one that actually did, which is what gather pulls out, indexing with input_ids[:, 1:]: capital, of, France, is, Paris, ., <eos>. The result is [3, 7], and the first row holds

    [ log P(capital | The),
      log P(of      | The capital),
      log P(France  | The capital of),
      log P(is      | The capital of France),
      log P(Paris   | The capital of France is),
      log P(.       | ... Paris),
      log P(<eos>   | ... Paris.) ]

    The same call against the frozen REF gives ref_token_logps, also [3, 7]. Every observed token now carries two log-probabilities: one under the policy being trained, one under the reference.

    Then the critic, which emits one scalar per position rather than a distribution. For the first sequence those values read as the reward it expects to end up with, given what it has seen so far:

    after "The"    → expected future reward before choosing "capital"
    ...
    after "is"     → expected future reward before choosing "Paris"
    after "Paris"  → expected future reward before choosing "."
    after "."      → expected future reward before choosing <eos>

    That leaves three aligned [3, 7] tensors, old_token_logps, ref_token_logps and old_values, plus the mask. At every next-token decision PPO now knows how likely the policy considered the chosen token, how likely the reference considered it, and how much future reward the critic expected.

    KL-shaped token rewards. The KL term is the gap between those first two:

    sampled_kl = old_token_logps - ref_token_logps
    token_rewards = (-kl_beta * sampled_kl).masked_fill(~mask, 0.0)

    Say the policy gives “Paris” probability 0.200.20 where the reference gives it 0.100.10. The sampled KL is log⁡0.20−log⁡0.10=log⁡2≈0.693\log 0.20 - \log 0.10 = \log 2 \approx 0.693, and with β=0.1\beta = 0.1 that token’s reward becomes −0.1×0.693=−0.0693-0.1 \times 0.693 = -0.0693. The mask then zeroes the prompt positions, so only divergence during the response is charged for:

    prediction   mask   KL reward
    capital       0     zeroed
    of            0     zeroed
    France        0     zeroed
    is            0     zeroed
    Paris         1     kept
    .             1     kept
    <eos>         1     kept

    The outcome reward. Only one number per completion arrives from the reward model, and it belongs on the last response token. Since the responses end at different places, the code finds that position instead of assuming it:

    last_indices = positions.masked_fill(~mask, -1).amax(dim=-1)
    token_rewards[rows, last_indices] += outcome_rewards

    Masking to −1-1 and taking the maximum picks the last position the mask kept:

    mask:        0   0   0   0   1   1   1
    positions:  -1  -1  -1  -1   4   5   6    → amax = 6

    Across the batch that is index 6 for “Paris.”, 5 for “four.” (whose last slot is padding), and 6 for “zero degrees Celsius.”. Suppose the KL penalties on the first response came out as −0.05-0.05, −0.02-0.02 and −0.01-0.01. Adding its outcome reward of 1.01.0 at index 6 gives

    Paris  → -0.05
    .      → -0.02
    <eos>  →  0.99      (-0.01 + 1.0)

    Returns. With gamma = 1.0 the backward loop is a running sum:

    <eos> return  =  0.99
    "." return    = -0.02 + 0.99 = 0.97
    Paris return  = -0.05 + 0.97 = 0.92

    The masked_fill inside that loop is what stops a return leaking backwards into prompt or padding positions.

    Advantages. Subtract what the critic had predicted, At=Gt−Vold(st)A_t = G_t - V_{\text{old}}(s_t), where GtG_t is the KL-adjusted return that actually resulted. If the critic predicted 0.400.40, 0.600.60 and 0.800.80 at those three positions:

    Paris advantage = 0.92 - 0.40 = +0.52
    "." advantage   = 0.97 - 0.60 = +0.37
    <eos> advantage = 0.99 - 0.80 = +0.19

    advantages and returns are now frozen alongside old_token_logps, and none of the three is recomputed again during this update, even as the critic changes underneath them.

    The inner loop. Each of the num_ppo_steps steps re-runs both models, this time tracking gradients, so token_logps and values move after every optimizer step while the cached quantities stay put. The ratio is measured against that fixed snapshot:

    log_ratio = (token_logps - old_token_logps).masked_fill(~mask, 0.0)
    ratio = log_ratio.exp()

    which is ρt=πcurrent(at∣st)/πold(at∣st)\rho_t = \pi_{\text{current}}(a_t \mid s_t) / \pi_{\text{old}}(a_t \mid s_t) at every response token. Clipping and the elementwise minimum then give the surrogate, negated because optimizers minimize while PPO wants to maximize. The critic loss is the squared error against the fixed return: predict 0.40.4 where the return was 0.90.9 and that position contributes (0.4−0.9)2=0.25(0.4 - 0.9)^2 = 0.25.

    Why response_mean and not a plain mean. Counting <eos>, the three responses here are 3, 3 and 5 tokens long. response_mean averages within each response before averaging across them:

    response 1 mean = sum of its 3 token losses / 3
    response 2 mean = sum of its 3 token losses / 3
    response 3 mean = sum of its 5 token losses / 5
    batch loss      = (response 1 + response 2 + response 3) / 3

    so all three count equally. A single mean over every unmasked token would give the longer third response more influence purely for being longer. Note that this is the same per-response length normalization that turns out to have a bias of its own: dividing by length is fairer across responses, and it is also what teaches the model that longer was better. ↩

  15. PPO write-ups usually call this ratio rtr_t, but ri,jr_{i,j} is already the reward of completion jj here, so the ratio gets ρ\rho instead. ↩

  16. This is the key difference in granularity. PPO generally has a separate advantage for every token-level state-action pair:

    [A^i,j,1, A^i,j,2, …, A^i,j,Ti,j].[\hat{A}_{i,j,1},\, \hat{A}_{i,j,2},\, \ldots,\, \hat{A}_{i,j,T_{i,j}}].

    These values can differ across positions because the critic estimates the value of each token-level state, and GAE uses those estimates to construct a separate advantage at each timestep. Standard outcome-reward GRPO instead computes its group-relative advantage once per completed response, then uses that scalar for every token in the response:

    A^i,j,1GRPO=A^i,j,2GRPO=⋯=A^i,j,Ti,jGRPO=Ai,j.\hat{A}^{\text{GRPO}}_{i,j,1} = \hat{A}^{\text{GRPO}}_{i,j,2} = \cdots = \hat{A}^{\text{GRPO}}_{i,j,T_{i,j}} = A_{i,j}.

    For example, the token-level multipliers for a four-token response might look like this:

    MethodToken 1Token 2Token 3Token 4
    PPO+0.3+0.3+0.7+0.7−0.1-0.1+1.1+1.1
    GRPO, if Ai,j=+1.2A_{i,j}=+1.2+1.2+1.2+1.2+1.2+1.2+1.2+1.2+1.2

    In short, PPO usually uses token-specific advantages A^i,j,t\hat{A}_{i,j,t}, while standard outcome-reward GRPO uses one response-level advantage Ai,jA_{i,j} shared by all its tokens. Dropping the critic causes this collapse: without a value function, GRPO cannot separately evaluate the state at each token position, so its signal comes from comparing completed responses. ↩

  17. See the RLHF Book’s discussion of RLVR. ↩

Bibliography

  1. Zichen Liu et al., “Understanding R1-Zero-Like Training: A Critical Perspective,” arXiv preprint arXiv:2503.20783, 2025.
  2. Darsh J. Shah et al. (Essential AI), “Rethinking Reflection in Pre-Training,” arXiv preprint arXiv:2504.04022, 2025.
  3. Yang Yue et al., “Does Reinforcement Learning Really Incentivize Reasoning Capacity in LLMs Beyond the Base Model?” in Advances in Neural Information Processing Systems (NeurIPS), 2025.
  4. Chunting Zhou et al., “LIMA: Less Is More for Alignment,” in Advances in Neural Information Processing Systems (NeurIPS), 2023.