Search

Try a topic

Writing

Explainer Policy Optimization for LLMs: From Fundamentals to Production · Part 3 of 5

GRPO: Eliminating the Value Network

Group Relative Policy Optimization replaces PPO's learned value function with a simple insight: sample multiple outputs and use their relative rewards as advantages. Roughly 44% less model memory than PPO, a simpler implementation, and the algorithm behind DeepSeek-R1.

Read first: Reinforcement Learning Foundations for LLM Alignment , PPO for Language Models: The RLHF Workhorse

Part 3 of 5: The Memory-Efficient Alternative

Updated 31 Oct 2026: Fixed the prerequisite link, which pointed off-site. The std-normalization paragraph had the effect backwards: dividing by the group std up-weights prompts that are nearly always or nearly never solved. The training code never exercised the clip, because the ratio was always 1; it now has a μ\mu loop that reuses each batch. Corrected the memory numbers: about 44% less model state than PPO, not 33%. Also: the std convention, the RLOO view of the baseline, the zero-variance-group limitation, a hedge on the KL estimator, the DPO coefficient, group-size advice, the R1 “aha moment” excerpt and R1’s stage-3 data, and several formulas that rendered wrong.

TL;DR: GRPO (Group Relative Policy Optimization) replaces PPO’s learned value function with a simple insight: sample GG outputs per prompt and use their normalized relative rewards as advantages. Dropping the critic removes a whole trainable model — about 44% of PPO’s model state for a 7B model — while staying competitive with PPO, especially with verifiable rewards. The algorithm powers DeepSeekMath’s 51.7% MATH benchmark score and later became central to DeepSeek-R1’s remarkable reasoning capabilities.

Reading time: ~32 minutes

Prerequisites: Part 2: PPO for Language Models covers the clipped objective and GAE that GRPO builds upon. Symbols follow the series notation.


The Memory Problem Revisited

In Part 2, we saw PPO’s four-model architecture. With mixed-precision Adam, a trainable model costs about 16 bytes per parameter (weights, gradients, fp32 master weights, and Adam’s two moments) and a frozen one about 2:

ModelPurposeMemory (7B)
Policy πθ\pi_\thetaGenerate responses~112 GB
Reference πref\pi_{\text{ref}}KL anchor~14 GB
Value VψV_\psiAdvantage estimation~112 GB
Reward rϕr_\phiScore responses~14 GB
Total~252 GB

The value network is the most expensive kind of model in the setup — a trainable one — and it’s there for a single purpose: estimating advantages via GAE.

But what if we didn’t need to learn V(s)V(s)? What if we could estimate advantages directly from samples?

This is GRPO’s insight: instead of learning “how good is this state?”, just sample multiple completions for the same prompt and compare them. The mean reward becomes your baseline. No neural network required.


Table of Contents

  1. The Core Insight: Group-Relative Advantages
  2. Mathematical Formulation
  3. Why This Works
  4. Outcome vs. Process Supervision
  5. Decoupled KL Regularization
  6. Iterative GRPO
  7. PyTorch Implementation
  8. DeepSeek-R1: GRPO at Scale
  9. A Unified View: GRPO, PPO, DPO, and RFT
  10. Limitations and When to Use Alternatives
  11. Key Takeaways

The Core Insight: Group-Relative Advantages

The Algorithm

For each prompt qq, GRPO:

  1. Sample a group of GG outputs {o1,o2,…,oG}\{o_1, o_2, \ldots, o_G\} from the current policy
  2. Score each output with the reward model or verifier: {r1,r2,…,rG}\{r_1, r_2, \ldots, r_G\}
  3. Normalize rewards using group statistics
  4. Use normalized rewards as advantages for all tokens in each output

The advantage for output oio_i is:

A^i=ri−mean(r)std(r)\hat{A}_i = \frac{r_i - \text{mean}(\mathbf{r})}{\text{std}(\mathbf{r})}

where r={r1,…,rG}\mathbf{r} = \{r_1, \ldots, r_G\} are rewards for all outputs in the group.

That’s it. No learned value function. No GAE computation. Just sample, score, normalize.

Visual Comparison

graph TB subgraph PPO["<b>PPO: Learned Value Function</b>"] direction TB P_prompt["Prompt q"] --> P_policy["Policy π_θ"] P_policy --> P_output["Output o"] P_output --> P_reward["Reward r"] P_output --> P_value["<b>Value Network V_ψ</b><br/>━━━━━━━━━━<br/>Same size as policy<br/>Learns V(s) for all states<br/><i>~112 GB with Adam states</i>"] P_value --> P_gae["GAE Computation<br/>━━━━━━━━━━<br/>δ_t = r + γV(s') − V(s)<br/>Â = Σ (γλ)^k δ_{t+k}"] P_reward --> P_gae P_gae --> P_adv["Advantage Â_t<br/><i>per token</i>"] end subgraph GRPO["<b>GRPO: Group Statistics</b>"] direction TB G_prompt["Prompt q"] --> G_policy["Policy π_θ"] G_policy --> G_o1["o₁ → r₁"] G_policy --> G_o2["o₂ → r₂"] G_policy --> G_dots["⋮"] G_policy --> G_oG["o_G → r_G"] G_o1 --> G_norm["<b>Normalize</b><br/>━━━━━━━━━━<br/>Â_i = (r_i − μ) / σ<br/>No neural network<br/><i>0 GB memory</i>"] G_o2 --> G_norm G_dots --> G_norm G_oG --> G_norm G_norm --> G_adv["Advantage Â_i<br/><i>per output</i>"] end style P_value stroke:#b0606a,stroke-width:2.5px style P_gae stroke:#a07a3c,stroke-width:2px style G_norm stroke:#b0606a,stroke-width:2.5px style PPO stroke:#b0606a style GRPO stroke:#b0606a

Memory Savings

SetupPolicyCriticReferenceRewardTotal
PPO, learned reward model112 GB112 GB14 GB14 GB252 GB
GRPO, learned reward model112 GB—14 GB14 GB140 GB (−44%)
GRPO, verifier reward (RLVR)112 GB—14 GB0 GB126 GB

About 44% less model state for a 7B model. That still doesn’t fit on one 80 GB GPU, so the saving shows up as fewer GPUs, or room for bigger batches and longer generations.

These totals exclude activations, the KV cache and the generation engine. ZeRO/FSDP sharding and offloading change the per-GPU numbers, and a PPO critic can be smaller than the policy, which shrinks the gap.

Key Insight: GRPO trades compute (sampling GG outputs per prompt) for memory (no value network). For LLMs where memory is the bottleneck, this is an excellent trade.


Mathematical Formulation

Let’s formalize GRPO precisely.

The GRPO Objective

GRPO maximizes:

JGRPO(θ)=Eq∼P(Q), {oi}i=1G∼πθold[1G∑i=1G1∣oi∣∑t=1∣oi∣Li,t−β DKL[πθ ∥ πref]]\mathcal{J}_{\text{GRPO}}(\theta) = \mathbb{E}_{q \sim P(Q),\, \{o_i\}_{i=1}^G \sim \pi_{\theta_{\text{old}}}}\left[\frac{1}{G}\sum_{i=1}^{G} \frac{1}{|o_i|}\sum_{t=1}^{|o_i|} \mathcal{L}_{i,t} - \beta\, \mathbb{D}_{\text{KL}}[\pi_\theta \,\|\, \pi_{\text{ref}}]\right]

The Per-Token Loss

Like PPO, GRPO uses the clipped surrogate objective:

Li,t=min⁡[ρi,t A^i,t,  clip(ρi,t,1−ε,1+ε)⋅A^i,t]\mathcal{L}_{i,t} = \min\left[\rho_{i,t}\, \hat{A}_{i,t},\; \text{clip}(\rho_{i,t}, 1-\varepsilon, 1+\varepsilon) \cdot \hat{A}_{i,t}\right]

where the importance ratio is:

ρi,t=πθ(oi,t ∣ q,oi,<t)πθold(oi,t ∣ q,oi,<t)\rho_{i,t} = \frac{\pi_\theta(o_{i,t} \,\mid\, q, o_{i,<t})}{\pi_{\theta_{\text{old}}}(o_{i,t} \,\mid\, q, o_{i,<t})}

The clipping mechanism is identical to PPO—what changes is how we compute A^i,t\hat{A}_{i,t}.

Group-Relative Advantage Estimation

For outcome supervision (the standard case), all tokens in output oio_i share the same advantage:

A^i,t=r~i=ri−mean(r)std(r)\hat{A}_{i,t} = \tilde{r}_i = \frac{r_i - \text{mean}(\mathbf{r})}{\text{std}(\mathbf{r})}

where:

  • ri=rϕ(q,oi)r_i = r_\phi(q, o_i) is the reward for output oio_i
  • r={r1,r2,…,rG}\mathbf{r} = \{r_1, r_2, \ldots, r_G\} are rewards for all GG outputs
  • mean(r)=1G∑i=1Gri\text{mean}(\mathbf{r}) = \frac{1}{G}\sum_{i=1}^G r_i
  • std(r)=1G∑i=1G(ri−mean(r))2\text{std}(\mathbf{r}) = \sqrt{\frac{1}{G}\sum_{i=1}^G (r_i - \text{mean}(\mathbf{r}))^2} — the population std. PyTorch’s rewards.std() defaults to the sample std (divide by G−1G-1); rewards.std(unbiased=False) matches this formula, and the code below uses it.

Properties of Group-Relative Advantages

1. Zero-mean: Within each group, advantages sum to zero — exactly, up to the small ϵstd\epsilon_{\text{std}} added to the denominator: ∑i=1Gr~i=0\sum_{i=1}^G \tilde{r}_i = 0

This provides balanced training signal: reinforcing good outputs requires suppressing worse ones.

2. Unit variance: By dividing by std, advantages are normalized: Var(r~)=1\text{Var}(\tilde{r}) = 1 This holds exactly only with the population std, and only when the group’s rewards aren’t all equal (then every advantage is 0).

This makes gradients comparable across prompts with different reward scales.

3. Relative, not absolute: The advantage measures performance relative to the group, not absolute quality. An output with reward 0.5 could have positive advantage (if others scored 0.3) or negative (if others scored 0.7).


Why This Works

The group-relative approach succeeds for several reasons:

1. Natural Baseline

The mean reward mean(r)\text{mean}(\mathbf{r}) estimates the expected reward under the current policy for this prompt:

Eo∼πθ(O∣q)[r(q,o)]≈1G∑i=1Gri\mathbb{E}_{o \sim \pi_\theta(O|q)}[r(q, o)] \approx \frac{1}{G}\sum_{i=1}^G r_i

With G=64G=64 samples (as in DeepSeekMath), this estimate is reasonably accurate. This is exactly what we want from a baseline.

One subtlety: the mean includes rir_i itself, and a baseline that depends on the sample it’s applied to can in principle bias the gradient. Here it doesn’t change the gradient’s direction. The group-mean advantage is the leave-one-out (RLOO) advantage, scaled by a constant:

ri−1G∑j=1Grj  =  G−1G(ri−1G−1∑j≠irj)r_i - \frac{1}{G}\sum_{j=1}^{G} r_j \;=\; \frac{G-1}{G}\Big(r_i - \frac{1}{G-1}\sum_{j \ne i} r_j\Big)

The leave-one-out baseline uses only the other samples, so it is a valid, unbiased baseline, and the factor (G−1)/G(G-1)/G only rescales the step. The real biases in GRPO come from elsewhere: dividing by the std and by the response length ∣oi∣|o_i| (Part 5 covers both).

2. Matched to Reward Model Training

Reward models are typically trained on comparisons between outputs for the same prompt, with a Bradley–Terry model: P(oA≻oB∣q)=σ(r(q,oA)−r(q,oB))P(o_A \succ o_B \mid q) = \sigma\big(r(q, o_A) - r(q, o_B)\big). Only differences within a prompt enter that loss, so adding any constant c(q)c(q) to every reward for prompt qq leaves the model unchanged. A Bradley–Terry reward is defined only up to a per-prompt constant.

Subtracting the group mean removes exactly that constant. GRPO’s advantage uses only the part of the reward the reward model was actually trained to get right.

3. Std Normalization Is a Weighting

Dividing by the group std is not a neutral rescale; it changes how much each prompt counts. Take binary rewards and G=16G = 16. A prompt solved half the time has std 0.5, so its advantages are ±1\pm 1. A prompt solved once in 16 tries has std about 0.24, and its one success gets an advantage of about +3.9+3.9.

So dividing by the group std up-weights prompts whose rewards barely vary — those nearly always or nearly never solved. Sometimes that’s what you want (a rare success on a hard prompt gets a loud signal); often it’s a bias toward prompts that carry little information. Dr. GRPO (Part 5) removes it for this reason.

4. No Bootstrapping Bias

GAE uses V(st+1)V(s_{t+1}) estimates, introducing bias if the value function is inaccurate. GRPO uses only observed rewards—no bootstrapping, no bias from learned estimates.

5. Simplicity

The entire advantage computation is:

advantages = (rewards - rewards.mean()) / (rewards.std(unbiased=False) + eps)

No recursive GAE computation, no value function forward passes, no TD errors.

Key Insight: The group mean is a Monte Carlo estimate of V(s0)V(s_0) for this prompt. By sampling enough outputs, we get a good baseline without learning anything.


Outcome vs. Process Supervision

GRPO supports two supervision modes.

Outcome Supervision (Default)

A single reward at sequence end:

A^i,t=r~i∀ t∈{1,…,∣oi∣}\hat{A}_{i,t} = \tilde{r}_i \quad \forall\, t \in \{1, \ldots, |o_i|\}

All tokens receive the same advantage—the normalized final reward.

Pros:

  • Simple: Works with any output-level reward model
  • Stable: Single reward signal, no step-level noise

Cons:

  • Coarse credit assignment: Every token gets equal “blame” or “credit”

Process Supervision

Rewards at each reasoning step (e.g., separated by newlines):

Let idx(j)\text{idx}(j) be the token index at the end of step jj, and KiK_i the total steps in output oio_i.

Step 1: Normalize per-step rewards

r~iidx(j)=riidx(j)−mean(R)std(R)\tilde{r}_i^{\text{idx}(j)} = \frac{r_i^{\text{idx}(j)} - \text{mean}(\mathbf{R})}{\text{std}(\mathbf{R})}

where R\mathbf{R} contains all step rewards across all outputs in the group.

Step 2: Compute return-to-go

A^i,t=∑idx(j)≥tr~iidx(j)\hat{A}_{i,t} = \sum_{\text{idx}(j) \geq t} \tilde{r}_i^{\text{idx}(j)}

The advantage at token tt is the sum of normalized rewards from all subsequent steps.

graph LR subgraph Process["<b>Process Supervision</b>"] direction LR S1["Step 1<br/>r̃₁ = 0.5<br/>━━━━━<br/>Â = 1.4"] --> S2["Step 2<br/>r̃₂ = −0.2<br/>━━━━━<br/>Â = 0.9"] S2 --> S3["Step 3<br/>r̃₃ = 0.8<br/>━━━━━<br/>Â = 1.1"] S3 --> S4["Step 4<br/>r̃₄ = 0.3<br/>━━━━━<br/>Â = 0.3"] end subgraph Outcome["<b>Outcome Supervision</b>"] direction LR T1["Step 1<br/>━━━━━<br/>Â = 0.7"] --> T2["Step 2<br/>━━━━━<br/>Â = 0.7"] T2 --> T3["Step 3<br/>━━━━━<br/>Â = 0.7"] T3 --> T4["Step 4<br/>r = 0.7<br/>━━━━━<br/>Â = 0.7"] end style S1 stroke:#b0606a style S2 stroke:#b0606a style S3 stroke:#b0606a style S4 stroke:#b0606a style T1 stroke:#5f7f8e style T2 stroke:#5f7f8e style T3 stroke:#5f7f8e style T4 stroke:#5f7f8e style Process stroke:#b0606a style Outcome stroke:#5f7f8e

Which to Choose?

SituationRecommendation
Standard RLHFOutcome supervision
Multi-step reasoning (math, code)Process supervision if PRM available
No process reward modelOutcome supervision
Training stability concernsOutcome supervision

DeepSeekMath experiments show process supervision slightly outperforms outcome supervision for math, but the difference is modest. Unless you have a high-quality Process Reward Model (PRM), outcome supervision is the simpler default.


Decoupled KL Regularization

GRPO handles KL penalty differently from standard PPO.

PPO: KL in the Reward

Standard PPO-RLHF adds KL to the per-token reward:

rttotal=rt−βlog⁡πθ(ot∣q,o<t)πref(ot∣q,o<t)r_t^{\text{total}} = r_t - \beta \log\frac{\pi_\theta(o_t|q, o_{<t})}{\pi_{\text{ref}}(o_t|q, o_{<t})}

This KL penalty flows into GAE computation, coupling reward signal with regularization.

GRPO: KL in the Loss

GRPO adds KL directly to the loss, decoupled from advantages:

JGRPO=LCLIP−β DKL[πθ ∥ πref]\mathcal{J}_{\text{GRPO}} = \mathcal{L}^{\text{CLIP}} - \beta\, \mathbb{D}_{\text{KL}}[\pi_\theta \,\|\, \pi_{\text{ref}}]

The Schulman KL Estimator

GRPO uses the “k3” KL estimator from Schulman’s note on approximating KL:

DKL[πθ ∥ πref]=πref(ot∣q,o<t)πθ(ot∣q,o<t)−log⁡πref(ot∣q,o<t)πθ(ot∣q,o<t)−1\mathbb{D}_{\text{KL}}[\pi_\theta \,\|\, \pi_{\text{ref}}] = \frac{\pi_{\text{ref}}(o_t|q, o_{<t})}{\pi_\theta(o_t|q, o_{<t})} - \log\frac{\pi_{\text{ref}}(o_t|q, o_{<t})}{\pi_\theta(o_t|q, o_{<t})} - 1

Properties:

  • Non-negative: Always ≥0\geq 0
  • Zero at equality: Exactly 0 when πθ=πref\pi_\theta = \pi_{\text{ref}}
  • Unbiased, with caveats: For tokens sampled from πθ\pi_\theta, its expected value equals DKL[πθ ∥ πref]\mathbb{D}_{\text{KL}}[\pi_\theta \,\|\, \pi_{\text{ref}}]. In GRPO the tokens come from πθold\pi_{\theta_{\text{old}}}, not πθ\pi_\theta, and the gradient of this estimator is not the gradient of the KL. Treat it as a well-behaved penalty that tracks the KL, not as an exact KL gradient.

Why Decouple?

1. Cleaner advantage signal: Advantages reflect only task performance, not regularization.

2. Independent tuning: KL coefficient β\beta doesn’t interact with advantage normalization.

3. Simpler implementation: No need to track per-token KL during advantage computation.

Key Insight: Decoupling KL from rewards simplifies the algorithm. The policy gradient focuses on task performance; the KL term separately ensures we don’t drift too far from the reference.


Iterative GRPO

As training progresses, the reward model may become a poor discriminator for the improved policy’s outputs. Iterative GRPO addresses this.

The Staleness Problem

  1. Initially: Policy outputs vary in quality; reward model discriminates well
  2. After training: Policy consistently produces good outputs; reward model’s training distribution no longer matches
  3. Result: Degraded training signal

The Solution: Co-evolve Policy and Reward Model

Algorithm: Iterative GRPO

For iteration = 1, ..., I:
    
    ┌─────────────────────────────────────────────────────┐
    │ Phase 1: Reset Reference                            │
    └─────────────────────────────────────────────────────┘
    π_ref ← π_θ   (current policy becomes new reference)
    
    ┌─────────────────────────────────────────────────────┐
    │ Phase 2: Policy Training                            │
    └─────────────────────────────────────────────────────┘
    For step = 1, ..., M:
        Sample batch of prompts
        Sample G outputs per prompt from π_θ
        Compute rewards using r_φ
        Compute group-relative advantages
        Update π_θ using GRPO objective
    
    ┌─────────────────────────────────────────────────────┐
    │ Phase 3: Reward Model Update                        │
    └─────────────────────────────────────────────────────┘
    Generate comparison data from improved π_θ
    Continually train r_φ with:
        - 90% new data (from current policy)
        - 10% historical data (replay buffer)

Key Design Choices

Reference reset: Each iteration starts fresh, allowing larger cumulative policy drift while constraining per-iteration change.

Replay buffer: 10% historical data prevents the reward model from forgetting how to score earlier, easier outputs.

Single GRPO update: DeepSeekMath uses one gradient step per sample batch (μ=1\mu = 1), prioritizing fresh samples over reuse.


PyTorch Implementation

Here’s a complete, educational implementation of GRPO:

"""
Group Relative Policy Optimization (GRPO)
=========================================

Memory-efficient alternative to PPO that eliminates the value network
by computing advantages from groups of sampled outputs.

Reference: DeepSeekMath (Shao et al., 2024)
"""

import torch
import torch.nn.functional as F
from torch import Tensor
from dataclasses import dataclass
from typing import Tuple, List


@dataclass
class GRPOConfig:
    """GRPO hyperparameters."""
    
    # Core settings
    group_size: int = 16           # G: outputs per prompt (DeepSeekMath used 64)
    clip_epsilon: float = 0.2      # ε: PPO clipping
    kl_coef: float = 0.04          # β: KL penalty coefficient
    mu: int = 1                    # μ: gradient steps per sampled batch. DeepSeekMath
                                   # used 1; then the ratio is exactly 1 and the clip
                                   # never fires. Set μ > 1 to reuse each batch.
    
    # Generation
    max_length: int = 1024
    temperature: float = 1.0
    
    # Training
    learning_rate: float = 1e-6
    max_grad_norm: float = 1.0
    
    # Numerical stability
    eps: float = 1e-8


def compute_group_advantages(rewards: Tensor, eps: float = 1e-8) -> Tensor:
    """
    Compute group-relative advantages.
    
    This is GRPO's key innovation: use group statistics
    instead of a learned value function.
    
    Args:
        rewards: [G] rewards for each output in the group
        eps: Small constant for numerical stability
        
    Returns:
        advantages: [G] normalized advantages
    """
    mean_r = rewards.mean()
    std_r = rewards.std(unbiased=False) + eps   # population std, as in the formula
    advantages = (rewards - mean_r) / std_r
    return advantages


def compute_schulman_kl(
    policy_log_probs: Tensor,
    reference_log_probs: Tensor,
) -> Tensor:
    """
    Compute Schulman's k3 KL estimator.
    
    D_KL = π_ref/π_θ - log(π_ref/π_θ) - 1
    
    This estimator is:
    - Non-negative (always >= 0)
    - Unbiased in value for tokens sampled from π_θ (here they come
      from π_old, and its gradient is not the KL's gradient)
    - Zero when π_θ = π_ref
    
    Args:
        policy_log_probs: [batch, seq_len] from π_θ
        reference_log_probs: [batch, seq_len] from π_ref
        
    Returns:
        kl: [batch, seq_len] per-token KL
    """
    log_ratio = reference_log_probs - policy_log_probs  # log(π_ref/π_θ)
    kl = torch.exp(log_ratio) - log_ratio - 1
    return kl


def compute_grpo_loss(
    policy_log_probs: Tensor,
    old_log_probs: Tensor,
    reference_log_probs: Tensor,
    advantages: Tensor,
    response_mask: Tensor,
    clip_epsilon: float = 0.2,
    kl_coef: float = 0.04,
) -> Tuple[Tensor, dict]:
    """
    Compute GRPO loss with clipping and KL penalty.
    
    Args:
        policy_log_probs: [G, seq_len] current policy
        old_log_probs: [G, seq_len] policy at sampling time
        reference_log_probs: [G, seq_len] reference policy
        advantages: [G] per-output advantages (will broadcast)
        response_mask: [G, seq_len] valid token mask
        clip_epsilon: PPO clipping parameter
        kl_coef: KL penalty coefficient
        
    Returns:
        loss: Scalar loss to minimize
        metrics: Dictionary of training metrics
    """
    G, seq_len = policy_log_probs.shape
    
    # =========================================
    # 1. Importance ratio: ρ = π_θ / π_old
    # =========================================
    log_ratio = policy_log_probs - old_log_probs
    ratio = torch.exp(log_ratio)  # [G, seq_len]
    
    # =========================================
    # 2. Expand advantages to token level
    # =========================================
    # [G] -> [G, seq_len]
    token_advantages = advantages.unsqueeze(1).expand(-1, seq_len)
    
    # =========================================
    # 3. PPO clipped objective
    # =========================================
    clipped_ratio = torch.clamp(ratio, 1 - clip_epsilon, 1 + clip_epsilon)
    
    policy_loss_1 = ratio * token_advantages
    policy_loss_2 = clipped_ratio * token_advantages
    policy_loss = torch.min(policy_loss_1, policy_loss_2)
    
    # =========================================
    # 4. KL penalty (decoupled from reward)
    # =========================================
    kl_penalty = compute_schulman_kl(policy_log_probs, reference_log_probs)
    
    # =========================================
    # 5. Combine losses
    # =========================================
    # Maximize policy objective, minimize KL
    token_loss = -policy_loss + kl_coef * kl_penalty
    
    # Mask and average
    masked_loss = token_loss * response_mask
    per_output_loss = masked_loss.sum(dim=1) / response_mask.sum(dim=1).clamp(min=1)
    loss = per_output_loss.mean()
    
    # =========================================
    # 6. Metrics
    # =========================================
    with torch.no_grad():
        clip_fraction = ((ratio - 1).abs() > clip_epsilon).float()
        clip_fraction = (clip_fraction * response_mask).sum() / response_mask.sum()
        
        mean_kl = (kl_penalty * response_mask).sum() / response_mask.sum()
    
    metrics = {
        "loss": loss.item(),
        "clip_fraction": clip_fraction.item(),
        "mean_kl": mean_kl.item(),
        "mean_advantage": advantages.mean().item(),
        "advantage_std": advantages.std().item(),
    }
    
    return loss, metrics


class GRPOTrainer:
    """
    GRPO trainer for LLM fine-tuning.
    
    Unlike PPO, requires only THREE models (no value network):
    - policy_model: The LLM being trained
    - reference_model: Frozen copy for KL penalty
    - reward_model: Scores complete responses
    
    Memory: ~44% less model state than PPO with a learned reward model
    """
    
    def __init__(
        self,
        policy_model: torch.nn.Module,
        reference_model: torch.nn.Module,
        reward_model,  # Can be a function or model
        tokenizer,
        config: GRPOConfig,
    ):
        self.policy = policy_model
        self.reference = reference_model
        self.reward_fn = reward_model
        self.tokenizer = tokenizer
        self.config = config
        
        # Freeze reference model
        for param in self.reference.parameters():
            param.requires_grad = False
        
        # Single optimizer (no value network!)
        self.optimizer = torch.optim.AdamW(
            self.policy.parameters(),
            lr=config.learning_rate,
            betas=(0.9, 0.95),
            weight_decay=0.1
        )
    
    @torch.no_grad()
    def sample_group(
        self,
        prompt_ids: Tensor,
        attention_mask: Tensor,
    ) -> Tuple[Tensor, Tensor]:
        """
        Sample G outputs for a single prompt.
        
        Args:
            prompt_ids: [1, prompt_len] token ids
            attention_mask: [1, prompt_len]
            
        Returns:
            output_ids: [G, total_len] generated sequences
            output_masks: [G, total_len] attention masks
        """
        G = self.config.group_size
        device = prompt_ids.device
        
        # Expand prompt for batch generation
        prompt_ids = prompt_ids.expand(G, -1)
        attention_mask = attention_mask.expand(G, -1)
        
        # Sample from policy
        self.policy.eval()
        outputs = self.policy.generate(
            input_ids=prompt_ids,
            attention_mask=attention_mask,
            max_new_tokens=self.config.max_length,
            do_sample=True,
            temperature=self.config.temperature,
            pad_token_id=self.tokenizer.pad_token_id,
            return_dict_in_generate=True,
        )
        
        output_ids = outputs.sequences
        output_masks = (output_ids != self.tokenizer.pad_token_id).long()
        
        return output_ids, output_masks
    
    @torch.no_grad()
    def compute_rewards(
        self,
        prompt_text: str,
        output_ids: Tensor,
        prompt_len: int,
    ) -> Tensor:
        """
        Compute rewards for each output in the group.
        
        Args:
            prompt_text: The original prompt string
            output_ids: [G, total_len] generated sequences
            prompt_len: Length of prompt in tokens
            
        Returns:
            rewards: [G] scalar reward per output
        """
        G = output_ids.shape[0]
        rewards = []
        
        for i in range(G):
            # Extract response tokens
            response_ids = output_ids[i, prompt_len:]
            response_text = self.tokenizer.decode(
                response_ids, skip_special_tokens=True
            )
            
            # Score with reward model
            reward = self.reward_fn(prompt_text, response_text)
            rewards.append(reward)
        
        return torch.tensor(rewards, device=output_ids.device)
    
    def compute_log_probs(
        self,
        model: torch.nn.Module,
        input_ids: Tensor,
        attention_mask: Tensor,
        response_start_idx: int,
    ) -> Tensor:
        """
        Compute per-token log probabilities for response tokens.
        
        Args:
            model: Language model
            input_ids: [batch, seq_len] full sequences
            attention_mask: [batch, seq_len]
            response_start_idx: Where response tokens begin
            
        Returns:
            log_probs: [batch, response_len] per-token log probs
        """
        outputs = model(
            input_ids=input_ids,
            attention_mask=attention_mask,
            return_dict=True,
        )
        logits = outputs.logits  # [batch, seq_len, vocab]
        
        # Shift for next-token prediction
        shift_logits = logits[:, :-1, :]
        shift_labels = input_ids[:, 1:]
        
        # Log probabilities
        log_probs = F.log_softmax(shift_logits, dim=-1)
        
        # Gather log probs for actual tokens
        token_log_probs = log_probs.gather(
            dim=-1,
            index=shift_labels.unsqueeze(-1)
        ).squeeze(-1)
        
        # Extract response tokens only
        response_log_probs = token_log_probs[:, response_start_idx - 1:]
        
        return response_log_probs
    
    def train_step(
        self,
        prompt_text: str,
        prompt_ids: Tensor,
        attention_mask: Tensor,
    ) -> dict:
        """
        Single GRPO training step.
        
        Args:
            prompt_text: Original prompt string
            prompt_ids: [1, prompt_len] tokenized prompt
            attention_mask: [1, prompt_len]
            
        Returns:
            metrics: Training metrics dictionary
        """
        prompt_len = prompt_ids.shape[1]
        device = prompt_ids.device
        
        # =============================================
        # Step 1: Sample group of outputs
        # =============================================
        output_ids, output_masks = self.sample_group(prompt_ids, attention_mask)
        G = output_ids.shape[0]
        
        # =============================================
        # Step 2: Compute rewards
        # =============================================
        rewards = self.compute_rewards(prompt_text, output_ids, prompt_len)
        
        # =============================================
        # Step 3: Compute group-relative advantages
        # =============================================
        advantages = compute_group_advantages(rewards, self.config.eps)
        
        # =============================================
        # Step 4: Freeze π_old and π_ref log probs for this batch
        # =============================================
        with torch.no_grad():
            old_log_probs = self.compute_log_probs(
                self.policy, output_ids, output_masks, prompt_len
            )
            reference_log_probs = self.compute_log_probs(
                self.reference, output_ids, output_masks, prompt_len
            )
        
        # Response mask
        response_mask = output_masks[:, prompt_len:]
        
        # =============================================
        # Step 5: μ gradient steps on the same batch
        # =============================================
        # Advantages and old_log_probs stay fixed. From the second step on,
        # π_θ has moved, so the ratio leaves 1 and the clip starts to matter.
        self.policy.train()
        for _ in range(self.config.mu):
            policy_log_probs = self.compute_log_probs(
                self.policy, output_ids, output_masks, prompt_len
            )
            loss, metrics = compute_grpo_loss(
                policy_log_probs,
                old_log_probs,
                reference_log_probs,
                advantages,
                response_mask,
                self.config.clip_epsilon,
                self.config.kl_coef,
            )
            self.optimizer.zero_grad()
            loss.backward()
            torch.nn.utils.clip_grad_norm_(
                self.policy.parameters(), 
                self.config.max_grad_norm
            )
            self.optimizer.step()
        
        # Add reward metrics
        metrics["mean_reward"] = rewards.mean().item()
        metrics["reward_std"] = rewards.std().item()
        
        return metrics

Implementation Notes

Group size selection: DeepSeekMath used G=64G=64; G=8G = 8–1616 is common. Larger groups give a better baseline per prompt, but small groups work when each batch has enough prompts.

Reusing a batch: With μ=1\mu = 1 (DeepSeekMath’s setting) the policy hasn’t moved when the loss is computed, so the ratio is exactly 1 and the clip never fires; GRPO is then a plain policy gradient with a KL penalty. With μ>1\mu > 1, advantages stay fixed for the batch; the importance ratio and clip correct for the policy drifting between steps — that is what they are for.

Dropout: Compute old_log_probs and the training-step log probs with the same dropout setting (most RLHF setups disable dropout), or the ratio won’t be 1 even on the first step.

KL coefficient: Start with β=0.04\beta = 0.04. Lower values allow more divergence; higher values constrain updates.


DeepSeek-R1: GRPO at Scale

In January 2025, DeepSeek released DeepSeek-R1, a reasoning model matching OpenAI’s o1 on many benchmarks. GRPO is central to its training.

R1’s Training Pipeline

graph TB subgraph R1Pipeline["<b>DeepSeek-R1 Training Pipeline</b>"] direction TB Stage1["<b>Stage 1: Cold Start</b><br/>━━━━━━━━━━<br/>Small amount of long CoT data<br/>Teaches extended reasoning format"] Stage2["<b>Stage 2: Reasoning RL</b><br/>━━━━━━━━━━<br/>GRPO with rule-based rewards<br/>Math correctness, code execution<br/>Model develops 'thinking' traces"] Stage3["<b>Stage 3: Rejection Sampling + SFT</b><br/>━━━━━━━━━━<br/>Sample from RL checkpoint<br/>Filter by rules + DeepSeek-V3 judge<br/>~600k reasoning + ~200k other samples"] Stage4["<b>Stage 4: Final RL</b><br/>━━━━━━━━━━<br/>Broader GRPO training<br/>Helpfulness + harmlessness<br/>Multi-objective alignment"] Stage1 --> Stage2 Stage2 --> Stage3 Stage3 --> Stage4 end style Stage1 stroke:#85807a,stroke-width:2px style Stage2 stroke:#b0606a,stroke-width:2.5px style Stage3 stroke:#8a6a94,stroke-width:2px style Stage4 stroke:#b0606a,stroke-width:2.5px style R1Pipeline stroke:#85807a,stroke-width:1px

Why GRPO for R1?

1. Long reasoning traces: R1 produces outputs with thousands of tokens. A value network would need to predict final reward from any point in this long sequence—extremely difficult.

2. Verifiable rewards: For math and code, rewards are rule-based (correct/incorrect). No reward model uncertainty.

3. Memory efficiency: At R1’s scale, GRPO’s memory savings are crucial.

4. Training stability: Simpler architecture means fewer things can go wrong.

The “Aha Moment” Phenomenon

The R1 paper reports an “aha moment” in an intermediate checkpoint of R1-Zero (the RL-only model, below): mid-solution, the model stops, flags its own approach, and re-evaluates. The paper’s example, abridged (the ellipses are the paper’s):

Question: If a > 1, then the sum of the real solutions of
√(a − √(a + x)) = x is equal to

Response: <think>
To solve the equation √(a − √(a + x)) = x, let's start by squaring both ···
(√(a − √(a + x)))² = x² ⟹ a − √(a + x) = x².
Rearrange to isolate the inner square root term:
(a − x²)² = a + x ⟹ a² − 2ax² + (x²)² = a + x ⟹ x⁴ − 2ax² − x + (a² − a) = 0
···
Wait, wait. Wait. That's an aha moment I can flag here.
Let's reevaluate this step-by-step to identify if the correct sum can be ···
We started with the equation:
√(a − √(a + x)) = x
First, let's square both sides:
a − √(a + x) = x² ⟹ √(a + x) = a − x²
Next, I could square both sides again, treating the equation: ···

No one supervised this behaviour directly; only the correctness reward did. How much of it RL creates is contested: the Dr. GRPO paper reports that self-reflection of this kind already appears in base models before RL, so RL may amplify it more than invent it.

R1-Zero: Pure RL Without SFT

DeepSeek also released R1-Zero, trained with GRPO directly from the base model without any SFT stage. Remarkably, it still develops coherent reasoning, though with some readability issues that the full R1 pipeline addresses.

This demonstrates GRPO’s power: given only a correctness signal, the algorithm can induce sophisticated reasoning behavior.

Key Insight: GRPO’s simplicity makes it stable enough for extreme-scale training. DeepSeek-R1 shows that you can train world-class reasoning models without the complexity of PPO’s value network.


A Unified View: GRPO, PPO, DPO, and RFT

The DeepSeekMath paper provides a unified framework for understanding different alignment methods (its Section 5.2).

The General Policy Gradient

All methods can be viewed through:

∇θJ=E(q,o)∼D[∑t∇θlog⁡πθ(ot∣q,o<t)⋅w(q,o)]\nabla_\theta J = \mathbb{E}_{(q, o) \sim \mathcal{D}}\left[\sum_t \nabla_\theta \log \pi_\theta(o_t \mid q, o_{<t}) \cdot w(q, o)\right]

Methods differ in:

  1. Data source D\mathcal{D}: Where do samples come from?
  2. Gradient coefficient w(q,o)w(q, o): How are outputs weighted?

Method Comparison

MethodData SourceCoefficient wwOnline?
SFTHuman demosw=1w = 1 (uniform)❌
RFTFiltered policy samplesw=1w = 1 (filtered)❌
DPOPreference pairso+o^+: σ(βℓ−−βℓ+)\sigma(\beta\ell^- - \beta\ell^+); o−o^-: its negative❌
Online RFTCurrent policyw=1w = 1 (filtered)✅
PPOCurrent policyw=Atw = A_t (GAE)✅
GRPOCurrent policyw=r~w = \tilde{r} (group-normalized)✅

For DPO, ℓ±=log⁡πθ(o±∣q)/πref(o±∣q)\ell^\pm = \log \pi_\theta(o^\pm \mid q) / \pi_{\text{ref}}(o^\pm \mid q) is the log-ratio of the chosen (o+o^+) or rejected (o−o^-) response. The chosen response is pushed up, and the rejected one down, by the same weight: large when the model still prefers the rejected one, near zero once it clearly prefers the chosen one.

Key Insights

Online vs. Offline: GRPO, PPO, and Online RFT sample from the current policy and can adapt. DPO and RFT use fixed datasets.

Weighted vs. Filtered: RFT keeps only correct outputs (binary filter). GRPO keeps all outputs but weights by normalized reward (continuous weighting).

Advantage estimation: PPO learns V(s)V(s); GRPO uses group statistics; DPO implicitly computes advantages through preferences.

graph LR subgraph Methods["<b>Alignment Methods: A Unified View</b>"] direction LR SFT["<b>SFT</b><br/>━━━━━<br/>Offline<br/>w = 1"] --> RFT["<b>RFT</b><br/>━━━━━<br/>Offline<br/>w = 1 (filtered)"] RFT --> DPO["<b>DPO</b><br/>━━━━━<br/>Offline<br/>w = ±σ(βℓ⁻ − βℓ⁺)"] DPO --> PPO["<b>PPO</b><br/>━━━━━<br/>Online<br/>w = GAE"] PPO --> GRPO["<b>GRPO</b><br/>━━━━━<br/>Online<br/>w = (r−μ)/σ"] end style SFT stroke:#85807a,stroke-width:2px style RFT stroke:#85807a,stroke-width:2px style DPO stroke:#8a6a94,stroke-width:2px style PPO stroke:#a07a3c,stroke-width:2px style GRPO stroke:#b0606a,stroke-width:2.5px style Methods stroke:#85807a,stroke-width:1px

Limitations and When to Use Alternatives

GRPO Limitations

1. Sampling cost: GG outputs per prompt (commonly 8–16; DeepSeekMath used 64). More generation per prompt than PPO’s single sample, though no critic forward and backward passes.

2. Advantage variance: With small groups, the mean is a noisy baseline. In practice G=8G = 8–1616 is common, and small groups work with enough prompts per batch: the GDPO paper trained with 4 rollouts (tool calling) and 16 (maths), and 2-GRPO, with just two completions per prompt, was reported to match 16-GRPO at about 1/8 the cost (overview).

3. Zero-variance groups: If every completion in a group is right, or every one is wrong, every advantage is zero and the group contributes no gradient. With binary rewards this is the biggest practical issue: as the model improves, more prompts become all-correct and the useful batch shrinks. DAPO’s dynamic sampling (Part 5) filters these groups out and samples more.

4. Uniform token credit: All tokens get the same advantage. No fine-grained credit assignment without process supervision.

5. Reward model dependency: Like all RL methods, susceptible to reward hacking if the reward model has exploits.

6. No value function benefits: Can’t use value-guided search or intrinsic motivation bonuses.

When to Use Each Method

SituationRecommendation
Memory-constrained✅ GRPO
Outcome rewards only✅ GRPO
Dense per-step rewardsConsider PPO (GAE)
Offline preferences onlyUse DPO
No reward modelUse DPO or RFT
Value-guided search neededUse PPO
Multi-reward trainingUse GDPO (Part 4)

Key Takeaways

The Core Innovation

A^i=ri−mean(r)std(r)\hat{A}_i = \frac{r_i - \text{mean}(\mathbf{r})}{\text{std}(\mathbf{r})}

Sample multiple outputs, normalize rewards, use as advantages. No value network.

Memory Savings

  • Eliminates a trainable value network: ~112 GB of model state for a 7B model with Adam
  • About 44% less model state than PPO (252 GB → 140 GB with a learned reward model; 126 GB with a verifier)
  • Excludes activations, KV cache and the generation engine; sharding changes per-GPU numbers

Design Choices

  • Group size: G=8G = 8–1616 is common (DeepSeekMath used 64)
  • Std: population std in the formula; std(unbiased=False) in code
  • Clip epsilon: ε=0.2\varepsilon = 0.2 (standard PPO)
  • KL coefficient: β=0.04\beta = 0.04
  • Decoupled KL: In loss, not reward

When GRPO Excels

  • Memory-constrained training
  • Outcome-based rewards
  • Long generations (where value prediction is hard)
  • Verifiable rewards (math, code)

DeepSeek-R1 Connection

GRPO powers R1’s remarkable reasoning through:

  • Rule-based correctness rewards
  • Self-reflection that RL amplified (“aha moments”), which the Dr. GRPO paper finds already present in base models
  • Stable training at scale

What’s Next

In Part 4: GDPO, we’ll see what happens when GRPO meets multi-reward settings:

  • Why combined reward normalization causes “advantage collapse”
  • GDPO’s solution: decouple normalization per reward
  • Production patterns for tool calling, math, and coding

Further Reading

Original Papers:

Related Work:

Implementations:

Series

Policy Optimization for LLMs: From Fundamentals to Production

Part 3 of 5

  1. 1 Reinforcement Learning Foundations for LLM Alignment
  2. 2 PPO for Language Models: The RLHF Workhorse
  3. 3 GRPO: Eliminating the Value Network (this article)
  4. 4 GDPO: Multi-Reward RL Done Right
  5. 5 After GDPO: A Map of the GRPO Family

Cite this article

Sousa, V. (2026). GRPO: Eliminating the Value Network. vitorsousa.com. https://www.vitorsousa.com/blog/grpo/

@misc{sousa2026grpo,
  title={{GRPO: Eliminating the Value Network}},
  author={Sousa, Vitor},
  year={2026},
  howpublished={\url{https://www.vitorsousa.com/blog/grpo/}},
  url={https://www.vitorsousa.com/blog/grpo/}
}