🖊️ ing down my thoughts here

GRPO explained in depth.

diving into why group relative policy optimization (grpo) is needed

hi there! let’s dive deep into group relative policy optimization (grpo), a reinforcement learning (rl) method that’s been a game-changer, especially for fine-tuning large language models (llms) on tasks like reasoning and math problems. if you’re new to rl or need a refresher, i’d suggest checking out this post: Proximal Policy Optimisation. it’ll get you up to speed so we can focus entirely on grpo here.

this is going to be a long, detailed journey, and i’ll walk you through it like we’re chatting over coffee. we’ll explore why grpo is necessary, building up the concepts brick by brick, deriving every equation step by step, and weaving in the reasoning behind each piece without explicitly labeling it as intuition. my goal is to make the math feel natural and motivated, as if we’re discovering it together. let’s get started!

the challenge of optimizing policies in rl

in rl, we’re training an agent—like an llm generating text—to make decisions that maximize a total reward over time. the agent follows a policy, a strategy that picks an action (like the next token to generate) based on the current state (like the prompt and text so far). this policy is a neural network with parameters θ, written as πθ(a|s), which gives the probability of choosing action a in state s.

picture a robot navigating a maze. at each time step t, it’s in state st, picks an action at using its policy, receives a reward rt, and moves to state st+1. the robot’s goal is to rack up as much reward as possible over time. our job is to tweak the policy’s parameters θ to maximize the expected total reward, which we’ll call the objective J(θ). in policy gradient methods, we compute the gradient of J(θ) with respect to θ and use gradient ascent to adjust θ in the direction that increases the reward. but this process is tricky, especially for something as complex as an llm. let’s break down the challenges by starting with the basics.

vanilla policy gradients: the starting point

let’s begin with the simplest policy gradient method to understand where things go wrong. our objective J(θ) is the expected total reward starting from an initial state, following the policy πθ. imagine the robot starting at the maze entrance—we want to know the average total reward it can expect by following πθ. we can write this as:

J(θ)=𝔼π[G0]

here, G0 is the total discounted reward starting from the initial state s0. the robot collects rewards r0,r1,r2, at each step, but future rewards are worth less than immediate ones—think of getting $1 today versus $1 next year. to account for this, we use a discount factor γ (between 0 and 1), so:

G0=r0+γr1+γ2r2+=t=0γtrt

the expectation 𝔼π is taken over all possible trajectories (sequences of states, actions, and rewards) the robot might experience under πθ. a trajectory τ is a sequence (s0,a0,r0,s1,a1,r1,), and its probability depends on the policy πθ and the environment’s dynamics. the probability of a trajectory is:

p(τθ)=p(s0)t=0πθ(atst)p(st+1st,at)

where p(s0) is the initial state distribution, πθ(atst) is the policy’s probability of choosing at in st, and p(st+1st,at) is the environment’s transition probability. so we can rewrite J(θ) as:

J(θ)=p(τθ)G(τ)dτ

where G(τ) is the total discounted reward for trajectory τ. to improve the policy, we need the gradient θJ(θ). since p(τθ) depends on θ through πθ, we need to differentiate this integral. let’s compute the gradient:

θJ(θ)=θp(τθ)G(τ)dτ=θp(τθ)G(τ)dτ

since G(τ) doesn’t depend on θ, we focus on θp(τθ). there’s a handy trick here: notice that θp(τθ)=p(τθ)θlogp(τθ), because:

θlogp(τθ)=θp(τθ)p(τθ)

substituting this in, we get:

θJ(θ)=p(τθ)(θlogp(τθ))G(τ)dτ=𝔼π[θlogp(τθ)G(τ)]

now, let’s compute θlogp(τθ). first, take the log of the trajectory probability:

logp(τθ)=logp(s0)+t=0(logπθ(atst)+logp(st+1st,at))

differentiate with respect to θ:

θlogp(τθ)=θ(logp(s0)+t=0logπθ(atst)+t=0logp(st+1st,at))

the terms logp(s0) and logp(st+1st,at) don’t depend on θ, so their gradients are zero. the only terms that depend on θ are the policy terms:

θlogp(τθ)=t=0θlogπθ(atst)

plug this back into the gradient:

θJ(θ)=𝔼π[(t=0θlogπθ(atst))G(τ)]

in practice, we approximate this by using Gt, the total discounted reward from time t onward:

Gt=rt+γrt+1+γ2rt+2+=k=0γkrt+k

this leads to the policy gradient form:

θJ(θ)=𝔼π[t=0θlogπθ(atst)Gt]

to use this, we sample trajectories, compute Gt for each step, and update θ to make high-reward actions more likely. but there’s a problem: Gt can vary a lot between trajectories. one path might get a huge reward by chance, while another gets almost nothing, even if the actions were similar. this high variance makes the gradient noisy, causing the robot to overcorrect—like always turning left because one left turn got lucky. also, a single high-reward trajectory can cause a massive update to θ, drastically changing the policy and potentially ruining performance. we need to reduce this variance and control how much the policy changes.

reinforce with a baseline: tackling variance

let’s improve this by adding a baseline to reduce variance, leading to the reinforce algorithm with a baseline. instead of using Gt directly, we subtract a baseline to compute the advantage. the idea is to measure how much better an action is compared to the average, rather than looking at the raw reward. we define the advantage as:

At=Gtb(st)

here, b(st) is a baseline, often chosen as the expected reward in state st, which we approximate with a value function V(st) that estimates the average future reward starting from st under πθ. the policy gradient becomes:

θJ(θ)=𝔼π[t=0θlogπθ(atst)At]

let’s see why this reduces variance. without the baseline, our update term is θlogπθ(atst)Gt. the variance of this term depends on Gt, which can swing widely. for example, if Gt ranges from -100 to 100 across trajectories, the updates will be all over the place. now, with the baseline, we use At=Gtb(st). if we choose b(st) to be close to the expected value of Gt in state st, i.e., b(st)𝔼π[Gtst], then:

𝔼π[Atst]=𝔼π[Gtst]b(st)0

this centers At around 0, reducing its fluctuations. to quantify this, let’s compare the variances. the variance of Gt is:

Var(Gtst)=𝔼π[Gt2st](𝔼π[Gtst])2

now, the variance of At:

Var(Atst)=𝔼π[(Gtb(st))2st](𝔼π[Gtb(st)]st)2

since 𝔼π[Gtb(st)]st0, the second term is near 0, and the first term, 𝔼π[(Gtb(st))2st], is the variance of Gt around b(st), which is smaller than the variance of Gt around its mean. this makes the updates more stable, as At doesn’t swing as much as Gt.

we estimate V(st) using a separate neural network, called the critic, which we train to predict the expected reward. but this comes with challenges. the critic is another network, often as big as the policy network, doubling our memory and compute needs. for an llm with billions of parameters, this is a major issue—imagine needing twice the gpu memory! also, if the critic’s estimate V(st) is off, At becomes misleading, leading to poor updates. training the critic is tough, especially in llm tasks where rewards are sparse, like in math problems where only the final answer gets a reward, and earlier steps get nothing. we need a way to compute advantages without a critic, as it’s too resource-heavy and error-prone.

proximal policy optimization (ppo): controlling policy changes

let’s look at ppo, a method that addresses the issue of large policy updates. ppo uses a trust region to ensure we don’t change the policy too much at once. the idea is to keep the new policy close to the old one, so we don’t risk a performance drop—like the robot suddenly changing its entire maze strategy and getting lost.

ppo compares the new policy πθ to the old policy πθold (before the update) using a probability ratio:

rt(θ)=πθ(atst)πθold(atst)

if rt(θ)>1, the new policy makes at more likely; if rt(θ)<1, it makes at less likely. a natural objective would be to maximize:

L(θ)=𝔼t[rt(θ)At]

this approximates J(θ) locally, because for small changes in θ, rt(θ)1, and the gradient of this objective matches the policy gradient. but if rt(θ) gets too large or too small, the update can be too aggressive. let’s derive this issue. if At>0, we want to increase πθ(atst), so rt(θ) grows. without a limit, rt(θ) could become very large, causing a huge update that overshoots and harms performance.

ppo introduces clipping to prevent this:

Lclip(θ)=𝔼t[min(rt(θ)At,clip(rt(θ),1ϵ,1+ϵ)At)]

where ϵ is a clipping parameter (like 0.2). the function clip(rt(θ),1ϵ,1+ϵ) ensures rt(θ) stays between 1ϵ and 1+ϵ (e.g., 0.8 to 1.2). if At>0, we increase rt(θ), but cap it at 1+ϵ; if At<0, we decrease rt(θ), but cap it at 1ϵ. the min ensures we take the smaller of the two terms, limiting the update size.

to see why this works, consider the unclipped term rt(θ)At. if At>0 and rt(θ) grows to, say, 10, the update would heavily favor at, potentially ignoring other good actions. with clipping, if rt(θ)>1+ϵ, we use (1+ϵ)At instead, keeping the update bounded. this trust region approach makes ppo more stable than vanilla policy gradients.

but ppo still relies on a critic to compute At, bringing back the same memory and accuracy issues. plus, for llms, generating samples and optimizing is already costly, and ppo’s critic and multiple epochs add to this burden. we need a method that avoids the critic, cuts compute costs, and maintains stability.

why grpo is the answer

let’s summarize the gaps in these methods and see how grpo fills them:

imagine training an llm to solve math problems. instead of relying on a critic, you let the model generate several answers to the same problem, score them (like checking if the answer is correct), and use those scores to adjust the policy. grpo makes this process efficient and stable—let’s build it up.

building grpo: group sampling and advantages

for each prompt q (the state s, like a math question), grpo generates G outputs {a1,a2,,aG} using the current policy πθ. each output ai is a sequence of tokens—think of ai as one complete answer to the math problem, made up of tokens ai,1,ai,2,,ai,N, where N is the sequence length.

scoring the outputs

we need to evaluate each output to guide the policy. since ai is a sequence, we score it token by token, considering the partial sequence up to each position. for the t-th token in sequence ai, we compute a reward that combines a raw score from a reward model with a penalty to prevent the policy from straying too far.

let’s define the raw reward first. we use a reward model rϕ(q,ai,t), which scores the partial sequence ai,t=(ai,1,ai,2,,ai,t) given the prompt q. this could be, for example, a measure of how correct the partial answer is at step t. in a math problem, if the partial sequence is leading toward the correct solution, rϕ might give a positive score; if it’s going off track, the score might be negative.

now, we don’t want the policy πθ to deviate too much from a reference policy πref (often the initial supervised fine-tuned model), because large deviations can lead to reward hacking—where the model learns to exploit the reward model in unintended ways, like generating nonsense that somehow scores high. to control this, we add a penalty based on the kl divergence between πθ and πref.

the kl divergence measures how much one probability distribution differs from another. for two policies in a given state, the kl divergence from πref to πθ is:

KL(πrefπθ)=𝔼πref[logπref(as)πθ(as)]

in our case, the state at step t is the prompt q plus the tokens generated so far, ai,<t=(ai,1,,ai,t1). we’re interested in the action ai,t, the t-th token. the kl divergence for this step would be:

KL(πrefπθ)=aπref(aq,ai,<t)logπref(aq,ai,<t)πθ(aq,ai,<t)

we don’t compute the full kl divergence in practice, because summing over all possible actions a (e.g., the entire vocabulary) is expensive. instead, we approximate its effect by penalizing the log ratio for the action ai,t that was actually taken:

logπθ(ai,tq,ai,<t)πref(ai,tq,ai,<t)

this is the log of the ratio of probabilities under the two policies. if πθ(ai,tq,ai,<t)>πref(ai,tq,ai,<t), the ratio is greater than 1, and the log is positive; if πθ assigns a lower probability, the log is negative. we subtract this term, scaled by a coefficient β (like 0.1), to penalize deviations from πref. so the per-token reward is:

ri,t=rϕ(q,ai,t)βlogπθ(ai,tq,ai,<t)πref(ai,tq,ai,<t)

if πθ makes ai,t much more likely than πref, the log term is positive, and the penalty reduces the reward, discouraging large deviations. if πθ makes ai,t less likely, the log term is negative, increasing the reward, but β ensures this effect is controlled.

to get the total reward for the sequence ai, we sum over all tokens:

ri=t=1Nri,t

this gives us a reward ri for each output ai, balancing the quality of the sequence (via rϕ) with a constraint on policy deviation (via the kl penalty).

computing advantages

now we have rewards {r1,r2,,rG} for the G outputs. we need to compute advantages to guide the policy update, but without a critic. grpo’s insight is to use the group of outputs to create a relative measure of performance.

imagine the G outputs as different attempts at solving the same math problem. some answers might be correct (high reward), while others are wrong (low reward). we want to encourage the policy to favor the better attempts, but we need a baseline to compare against. instead of using a critic, grpo uses the group itself as the baseline by normalizing the rewards:

A^i,t=rimean(r)std(r)

where mean(r) is the average reward across the group:

mean(r)=1Gi=1Gri

and std(r) is the standard deviation:

std(r)=1Gi=1G(rimean(r))2

let’s see why this normalization helps. without normalization, we’d use ri directly, but the raw rewards can vary widely depending on the task. for example, if rewards range from -10 to 10, the updates would be noisy, similar to the variance issue in vanilla policy gradients. by subtracting mean(r), we center the rewards around 0:

𝔼[A^i,t]=𝔼[rimean(r)std(r)]=𝔼[ri]mean(r)std(r)=0

dividing by std(r) scales the values so the variance becomes 1:

Var(A^i,t)=Var(rimean(r)std(r))=Var(ri)(std(r))2=1

this gives us a standardized measure: A^i,t>0 means ai performed better than the group average, and A^i,t<0 means it performed worse. for simplicity, we use the same advantage for all tokens in sequence ai, though some implementations compute per-token advantages. this group-relative approach reduces variance without needing a critic, as the group provides the context for comparison.

the grpo objective

now we use these advantages to update the policy, ensuring the updates are safe. grpo adopts a clipped objective similar to ppo:

LGRPO(θ)=𝔼t[min(rt(θ)A^t,clip(rt(θ),1ϵ,1+ϵ)A^t)]

where:

rt(θ)=πθ(atst)πθold(atst)

here, st is the state at step t, which in our case is (q,ai,<t), and at is the token ai,t. the expectation is over all tokens in all sequences. the clipping with ϵ (like 0.2) ensures we don’t change the policy too much, just like in ppo, while A^t guides the update based on the group-relative performance.

the grpo algorithm

let’s put it all together:

  1. generate G outputs {a1,a2,,aG} for each prompt q using πθ.
  2. compute rewards ri for each output by summing per-token rewards with the kl penalty.
  3. calculate advantages A^i,t by normalizing the rewards.
  4. optimize LGRPO(θ) using gradient ascent over a few epochs.
  5. set πθoldπθ, and repeat.

wrapping up

grpo is a clever solution for llm training, removing the need for a critic, reducing variance through group sampling, and ensuring stability with clipping and kl penalties. it’s especially well-suited for reasoning tasks, making rl training more efficient and practical. if you have questions or want to dig deeper, let me know!