PPO for Language Models › Implementation: PPO Training Step

Michael BrenndoerferDecember 28, 20254 min read
Language AI HandbookAll chapters
Part 37 of 67Alignment and RLHF
Chapter 279PPO for Language Models › Implementation: PPO Training Step
Reading section 9 of 15 · Implementation: PPO Training Step · 4 minSection 9 of 15 · 4 min

Book contents

Section 9 of 15 · 4 min read

PPO for Language Models › Implementation: PPO Training StepOne section · Sections in this chapter9 / 15 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let us now assemble a more complete implementation showing how these pieces fit together in a training step. This simplified version captures the needed structure while omitting some production details like distributed training, gradient accumulation, and advanced memory management. This example shows the flow of data and computation.

The overall structure follows a pattern that will look familiar from supervised learning, but with key differences. We generate responses before computing loss, which makes the training loop longer. We maintain two versions of log probabilities, old and new, to compute the probability ratio. We track the KL divergence as a health metric. These differences reflect the basic distinction between RL and supervised learning: in RL, the data distribution depends on the current policy.

In[21]:
Code
from dataclasses import dataclass
from typing import Tuple

import torch
import torch.nn.functional as F


@dataclass
class PPOConfig:
    """Configuration for PPO training."""

    clip_epsilon: float = 0.2
    kl_coef: float = 0.1
    value_coef: float = 0.5
    gamma: float = 1.0
    lam: float = 0.95


class LanguageModelPPO:
    """
    PPO trainer for language models.

    This simplified implementation demonstrates the key components
    without the full complexity of production systems.
    """

    def __init__(self, policy_model, ref_model, value_model, config: PPOConfig):
        """
        Args:
            policy_model: The LLM being optimized
            ref_model: Frozen reference model for KL computation
            value_model: Value function (often shares backbone with policy)
            config: PPO hyperparameters
        """
        self.policy = policy_model
        self.ref = ref_model
        self.value = value_model
        self.config = config

        # Freeze reference model
        for param in self.ref.parameters():
            param.requires_grad = False

    def compute_log_probs(self, model, input_ids, response_ids):
        """
        Compute log probabilities for response tokens.

        Args:
            model: Language model
            input_ids: Full sequence (prompt + response)
            response_ids: Just the response tokens

        Returns:
            Log probabilities for each response token
        """
        with torch.no_grad() if model == self.ref else torch.enable_grad():
            outputs = model(input_ids)
            logits = outputs.logits if hasattr(outputs, "logits") else outputs

            # Get logits for positions that predict response tokens
            # (shifted by 1 for autoregressive prediction)
            response_start = input_ids.shape[1] - response_ids.shape[1]
            response_logits = logits[:, response_start - 1 : -1, :]

            # Compute log probabilities
            log_probs = F.log_softmax(response_logits, dim=-1)

            # Gather log probs for actual tokens
            token_log_probs = torch.gather(
                log_probs, dim=-1, index=response_ids.unsqueeze(-1)
            ).squeeze(-1)

        return token_log_probs

    def compute_rewards_and_advantages(
        self,
        reward_scores: torch.Tensor,
        log_probs_policy: torch.Tensor,
        log_probs_ref: torch.Tensor,
        values: torch.Tensor,
    ) -> Tuple[torch.Tensor, torch.Tensor]:
        """
        Compute KL-shaped rewards and GAE advantages.
        """
        batch_size, seq_len = log_probs_policy.shape

        # KL penalty per token
        kl = log_probs_policy - log_probs_ref

        # Initialize rewards (sparse: only at final token)
        rewards = torch.zeros_like(log_probs_policy)
        rewards[:, -1] = reward_scores

        # Apply KL shaping
        shaped_rewards = rewards - self.config.kl_coef * kl

        # Compute GAE
        advantages = torch.zeros_like(shaped_rewards)
        last_gae = torch.zeros(batch_size, device=shaped_rewards.device)

        for t in reversed(range(seq_len)):
            next_value = (
                values[:, t + 1]
                if t < seq_len - 1
                else torch.zeros_like(last_gae)
            )
            delta = (
                shaped_rewards[:, t]
                + self.config.gamma * next_value
                - values[:, t]
            )
            advantages[:, t] = last_gae = (
                delta + self.config.gamma * self.config.lam * last_gae
            )

        returns = advantages + values

        return advantages, returns, kl

    def ppo_loss(
        self,
        log_probs_new: torch.Tensor,
        log_probs_old: torch.Tensor,
        advantages: torch.Tensor,
    ) -> torch.Tensor:
        """
        Compute clipped PPO policy loss.
        """
        # Normalize advantages
        advantages = (advantages - advantages.mean()) / (
            advantages.std() + 1e-8
        )

        # Probability ratio
        ratio = torch.exp(log_probs_new - log_probs_old)

        # Clipped surrogate
        clipped_ratio = torch.clamp(
            ratio, 1 - self.config.clip_epsilon, 1 + self.config.clip_epsilon
        )

        # Loss (negative because we minimize)
        policy_loss = -torch.min(
            ratio * advantages, clipped_ratio * advantages
        ).mean()

        return policy_loss

    def value_loss(
        self, values: torch.Tensor, returns: torch.Tensor
    ) -> torch.Tensor:
        """
        Compute value function loss.
        """
        return F.mse_loss(values, returns)
In[22]:
Code
# Initialize configuration
config = PPOConfig()
Out[23]:
Console
LanguageModelPPO trainer initialized with components:
  - Policy model (being optimized)
  - Reference model (frozen, for KL computation)
  - Value model (critic)

Key hyperparameters:
  - clip_epsilon: 0.2
  - kl_coef (beta): 0.1
  - value_coef: 0.5
  - GAE lambda: 0.95

These hyperparameters define the constraints for the optimization. The clipping epsilon and KL coefficient are particularly necessary for preventing the model from collapsing or drifting too far from its original capabilities. The value coefficient balances the policy and value function losses, and the GAE lambda controls the bias-variance trade-off in advantage estimation.