Skip to content
Back to skills

Truncated Ppo

ASecurity

T-PPO improves training efficiency via truncated rollouts and extended GAE, enabling batch continuity without waiting for full sequence completion.

  • 6 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 9, 2026
toolspythongit

Works with

  • terminal
  • cli

Security analysis

A100/100

Scanned September 9, 2026

npx -y skills add ADu2021/skillXiv --skill truncated-ppo --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Truncated Ppo?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Truncated Ppo
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/adu2021-truncated-ppo/badge)](https://www.skillsdirectory.com/skills/adu2021-truncated-ppo)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: truncated-ppo
title: "Truncated Proximal Policy Optimization"
version: 0.0.2
engine: skillxiv-v0.0.2-claude-opus-4.6
license: MIT
url: "https://arxiv.org/abs/2506.15050"
keywords: [policy-optimization, PPO, training-efficiency, reasoning, truncated-rollouts]
description: "T-PPO improves training efficiency via truncated rollouts and extended GAE, enabling batch continuity without waiting for full sequence completion."
---

# Truncated Proximal Policy Optimization

## Core Concept

Truncated PPO (T-PPO) enhances training efficiency for reasoning LLMs through truncated rollouts and extended generalized advantage estimation (EGAE). Rather than waiting for complete generation sequences before updating, T-PPO divides long sequences into fixed-window chunks and applies EGAE to compute advantages from incomplete trajectories. This approach maintains constant batch size through replacement of finished sequences while achieving 60% wall-clock time reduction and 2.5x training efficiency improvement without sacrificing convergence on mathematical reasoning tasks.

## Architecture Overview

- **Truncated Rollout Strategy**: Divide response generation into fixed-length windows rather than waiting for completion
- **Extended GAE (EGAE)**: Generalize advantage estimation to incomplete trajectories by assuming terminal value equals penultimate state value
- **Batch Continuity**: Replace finished sequences with new samples, maintaining constant batch size and GPU utilization
- **Token Filtering**: Selectively filter training tokens to enable independent policy/value optimization
- **Successive Batching**: Continuously feed new prompts while earlier ones still generate

## Implementation

### Step 1: Implement Truncated Rollout Strategy

Divide long sequences into manageable chunks:

```python
import torch
import torch.nn as nn
from typing import List, Dict

class TruncatedRolloutBuffer:
    """
    Manages truncated rollouts: divides generation into windows.
    """
    def __init__(self, window_size=128, max_total_length=512):
        self.window_size = window_size
        self.max_total_length = max_total_length

    def create_windows(self, prompt_ids, max_new_tokens):
        """
        Divide expected generation into fixed-length windows.

        Args:
            prompt_ids: [batch, prompt_len] token IDs
            max_new_tokens: maximum tokens to generate per prompt

        Returns:
            window_config: dict with window boundaries and settings
        """
        batch_size = prompt_ids.shape[0]
        num_windows = (max_new_tokens + self.window_size - 1) // self.window_size

        window_config = {
            'window_size': self.window_size,
            'num_windows': num_windows,
            'batch_size': batch_size,
            'max_new_tokens': max_new_tokens
        }

        return window_config

    def process_truncated_generation(self, prompt_ids, generation_outputs,
                                    window_idx):
        """
        Extract tokens for a specific generation window.

        Args:
            prompt_ids: [batch, prompt_len]
            generation_outputs: full generated sequences (may not be complete)
            window_idx: which window to extract

        Returns:
            window_tokens: [batch, window_size] tokens in this window
            is_complete: [batch] boolean indicating if sequence completed
        """
        start_idx = window_idx * self.window_size
        end_idx = (window_idx + 1) * self.window_size

        # Extract window
        window_tokens = generation_outputs[:, start_idx:end_idx]

        # Pad if incomplete
        if window_tokens.shape[1] < self.window_size:
            padding = torch.full(
                (window_tokens.shape[0], self.window_size - window_tokens.shape[1]),
                fill_value=0  # or pad token ID
            )
            window_tokens = torch.cat([window_tokens, padding], dim=1)

        # Check completion
        is_complete = (window_tokens != 0).sum(dim=1) < self.window_size

        return window_tokens, is_complete
```

### Step 2: Implement Extended GAE

Generalize GAE to incomplete trajectories:

```python
class ExtendedGeneralizedAdvantageEstimation:
    """
    Extended GAE: handles incomplete (truncated) trajectories.
    Key assumption: V(s_l) = V(s_{l-1}) for ungenerated states.
    """
    def __init__(self, gamma=0.99, gae_lambda=0.95):
        self.gamma = gamma
        self.gae_lambda = gae_lambda

    def compute_advantages_truncated(self, rewards, values, dones,
                                    trajectory_length):
        """
        Compute advantages assuming incomplete trajectories.

        Args:
            rewards: [batch, seq_len] rewards at each position
            values: [batch, seq_len] state value estimates
            dones: [batch, seq_len] episode termination flags
            trajectory_length: list of actual lengths per batch

        Returns:
            advantages: [batch, seq_len] advantage estimates
            returns: [batch, seq_len] return estimates
        """
        batch_size, seq_len = rewards.shape
        advantages = torch.zeros_like(rewards)
        gae = 0

        # Iterate backwards through time
        for t in reversed(range(seq_len)):
            if t == seq_len - 1:
                # Last position: assume next value equals current for ungenerated
                next_value = values[:, t].clone()
                # Check if trajectory continues beyond window
                next_non_terminal = 1 - dones[:, t]
            else:
                next_value = values[:, t + 1]
                next_non_terminal = 1 - dones[:, t]

            delta = (
                rewards[:, t] +
                self.gamma * next_value * next_non_terminal -
                values[:, t]
            )

            # GAE computation with temporal decay
            gae = (
                delta +
                self.gamma * self.gae_lambda * next_non_terminal * gae
            )

            advantages[:, t] = gae

        # Compute returns
        returns = advantages + values

        return advantages, returns

    def compute_advantages_complete(self, rewards, values, dones):
        """
        Standard GAE for completed trajectories (reference).
        """
        batch_size, seq_len = rewards.shape
        advantages = torch.zeros_like(rewards)
        gae = 0

        # Bootstrap from final value
        next_value = values[:, -1].clone()

        for t in reversed(range(seq_len)):
            if t == seq_len - 1:
                next_non_terminal = 1 - dones[:, t]
                next_value = values[:, t]
            else:
                next_non_terminal = 1 - dones[:, t]
                next_value = values[:, t + 1]

            delta = (
                rewards[:, t] +
                self.gamma * next_value * next_non_terminal -
                values[:, t]
            )

            gae = (
                delta +
                self.gamma * self.gae_lambda * next_non_terminal * gae
            )

            advantages[:, t] = gae

        returns = advantages + values

        return advantages, returns
```

### Step 3: Implement Token Filtering

Selectively filter tokens for optimization:

```python
class TokenFilter:
    """
    Filters training tokens to improve efficiency.
    """
    @staticmethod
    def filter_high_advantage(tokens, advantages, log_probs,
                             percentile_threshold=50):
        """
        Focus on tokens with high absolute advantage.

        Args:
            tokens: [batch, seq_len] token IDs
            advantages: [batch, seq_len] advantage estimates
            log_probs: [batch, seq_len] log probabilities
            percentile_threshold: keep tokens above this percentile

        Returns:
            filtered_mask: [batch, seq_len] boolean mask
        """
        # Compute percentile threshold
        advantage_threshold = torch.kthvalue(
            advantages.reshape(-1),
            int(advantages.numel() * (100 - percentile_threshold) / 100)
        ).values

        # Create mask
        filtered_mask = torch.abs(advantages) > advantage_threshold

        return filtered_mask

    @staticmethod
    def filter_redundant_tokens(log_probs, entropy_threshold=0.1):
        """
        Filter low-entropy (confident) tokens to reduce redundancy.

        Args:
            log_probs: [batch, seq_len, vocab_size]
            entropy_threshold: keep if entropy > threshold

        Returns:
            filtered_mask: [batch, seq_len] boolean mask
        """
        # Compute entropy
        probs = torch.softmax(log_probs, dim=-1)
        entropy = -(probs * torch.log(probs + 1e-8)).sum(dim=-1)

        # Filter
        filtered_mask = entropy > entropy_threshold

        return filtered_mask

    @staticmethod
    def filter_composite(advantages, log_probs, weight_advantage=0.7):
        """
        Combine multiple filtering criteria.
        """
        mask_advantage = TokenFilter.filter_high_advantage(
            None, advantages, None, percentile_threshold=40
        )

        mask_entropy = TokenFilter.filter_redundant_tokens(
            log_probs, entropy_threshold=0.15
        )

        # Weighted combination
        combined_mask = (
            weight_advantage * mask_advantage.float() +
            (1 - weight_advantage) * mask_entropy.float()
        ) > 0.5

        return combined_mask
```

### Step 4: Implement Successive Batching

Maintain constant batch size with continuous sampling:

```python
class SuccessiveBatchSampler:
    """
    Samples batches continuously, replacing finished sequences.
    Maintains constant batch size for GPU efficiency.
    """
    def __init__(self, prompt_dataset, batch_size=32, window_size=128):
        self.prompt_dataset = prompt_dataset
        self.batch_size = batch_size
        self.window_size = window_size
        self.current_idx = 0
        self.active_sequences = []

    def get_next_batch(self):
        """
        Return batch with new samples replacing finished ones.

        Returns:
            batch: dict with prompts, generation_states
        """
        batch = {
            'new_prompts': [],
            'in_progress': self.active_sequences.copy()
        }

        # Add new sequences to replace finished ones
        num_new_needed = self.batch_size - len(self.active_sequences)

        for _ in range(num_new_needed):
            if self.current_idx >= len(self.prompt_dataset):
                self.current_idx = 0  # Cycle through dataset

            prompt = self.prompt_dataset[self.current_idx]
            batch['new_prompts'].append(prompt)
            self.active_sequences.append({
                'prompt': prompt,
                'generated_tokens': [],
                'generation_step': 0
            })
            self.current_idx += 1

        return batch

    def update_generation_state(self, window_outputs, finished_mask):
        """
        Update tracking of in-progress generations.

        Args:
            window_outputs: [batch, window_size] generated tokens
            finished_mask: [batch] boolean indicating finished sequences
        """
        # Remove finished sequences
        self.active_sequences = [
            seq for i, seq in enumerate(self.active_sequences)
            if not finished_mask[i]
        ]
```

### Step 5: Implement T-PPO Training Loop

Complete training with truncation and successive batching:

```python
class TruncatedPPOTrainer:
    """
    Full T-PPO trainer with truncated rollouts and batch continuity.
    """
    def __init__(self, model, value_model, window_size=128, batch_size=32):
        self.model = model  # policy model
        self.value_model = value_model
        self.window_size = window_size
        self.batch_size = batch_size
        self.egae = ExtendedGeneralizedAdvantageEstimation()
        self.rollout_buffer = TruncatedRolloutBuffer(window_size)
        self.batch_sampler = SuccessiveBatchSampler(
            [], batch_size=batch_size, window_size=window_size
        )

    def training_step(self, batch_prompts, batch_responses, window_idx,
                     verification_fn):
        """
        Single training step on a generation window.

        Args:
            batch_prompts: [batch, prompt_len]
            batch_responses: [batch, total_len] responses generated so far
            window_idx: which window to train on
            verification_fn: function to verify answer correctness
        """
        # Extract window
        window_tokens, is_complete = self.rollout_buffer.process_truncated_generation(
            batch_prompts, batch_responses, window_idx
        )

        # Compute rewards based on verification
        rewards = torch.zeros(self.batch_size, self.window_size)

        for i in range(self.batch_size):
            response_text = self.model.tokenizer.decode(batch_responses[i])
            is_correct = verification_fn(response_text)

            # Sparse reward: only at end of window or completion
            if is_complete[i] or (window_idx == self.rollout_buffer.window_size - 1):
                rewards[i, -1] = 1.0 if is_correct else 0.0

        # Compute values using value model
        with torch.no_grad():
            value_outputs = self.value_model(window_tokens)
            values = value_outputs.squeeze(-1)  # [batch, window_size]

        # Compute advantages
        advantages, returns = self.egae.compute_advantages_truncated(
            rewards,
            values,
            ~is_complete.unsqueeze(1),  # dones flags
            self.window_size
        )

        # Get policy log probs
        policy_outputs = self.model(window_tokens)
        log_probs = policy_outputs.log_probs  # [batch, window_size]

        # Filter tokens if desired
        token_filter = TokenFilter()
        filter_mask = token_filter.filter_composite(
            advantages, policy_outputs.logits
        )

        # PPO loss with clipping
        ratio = torch.exp(log_probs - policy_outputs.old_log_probs)
        clipped_ratio = torch.clamp(ratio, 1 - 0.2, 1 + 0.2)

        loss_actor = -torch.min(
            ratio * advantages,
            clipped_ratio * advantages
        )[filter_mask].mean()

        # Value loss
        loss_value = torch.nn.functional.mse_loss(values, returns)

        # Total loss
        loss = loss_actor + 0.5 * loss_value

        return loss, {
            'actor_loss': loss_actor.item(),
            'value_loss': loss_value.item(),
            'filtered_fraction': filter_mask.float().mean().item()
        }

    def train_epoch(self, dataloader, num_epochs=1):
        """
        Train for one epoch with successive batching.
        """
        optimizer = torch.optim.Adam(
            list(self.model.parameters()) +
            list(self.value_model.parameters()),
            lr=1e-4
        )

        for epoch in range(num_epochs):
            epoch_loss = 0
            epoch_metrics = {}

            for batch_idx, (prompts, responses) in enumerate(dataloader):
                # Determine number of windows
                max_len = responses.shape[1]
                num_windows = (max_len + self.window_size - 1) // self.window_size

                # Train on each window
                for window_idx in range(num_windows):
                    loss, metrics = self.training_step(
                        prompts, responses, window_idx,
                        verification_fn=self._verify_correctness
                    )

                    optimizer.zero_grad()
                    loss.backward()
                    torch.nn.utils.clip_grad_norm_(
                        list(self.model.parameters()) +
                        list(self.value_model.parameters()),
                        1.0
                    )
                    optimizer.step()

                    epoch_loss += loss.item()

                    for k, v in metrics.items():
                        if k not in epoch_metrics:
                            epoch_metrics[k] = []
                        epoch_metrics[k].append(v)

            # Log results
            avg_loss = epoch_loss / (len(dataloader) * num_windows)
            print(f"Epoch {epoch + 1}, Loss: {avg_loss:.4f}")

            for k, v in epoch_metrics.items():
                print(f"  {k}: {sum(v) / len(v):.4f}")

    def _verify_correctness(self, response_text):
        """Verify if response is correct (domain-specific)"""
        return True  # Placeholder
```

## Practical Guidance

- **Window Size**: Larger windows (256) reduce overhead but increase memory; start with 128
- **EGAE Assumption**: V(s_l) = V(s_{l-1}) works well empirically; validate on your tasks
- **Token Filtering**: Apply selectively; too aggressive filtering can bias updates
- **Batch Continuity**: Requires prompt dataset large enough to avoid repetition
- **Speedup Target**: Expect 2-3x speedup; measure actual wall-clock time, not just theoretical
- **Hyperparameters**: Use standard PPO settings (clip=0.2, GAE lambda=0.95); adjust window size first
- **Evaluation**: Benchmark on long-sequence reasoning tasks (AIME, MATH500, code generation)
- **Comparison**: Always compare to standard PPO on same tasks to validate efficiency gains

## Reference

Paper: arXiv:2506.15050
Key metrics: 60% wall-clock reduction, 2.5x training efficiency on math reasoning
EGAE improvement: Handles incomplete trajectories enabling continuous batching
Related work: PPO, GAE, policy optimization, training efficiency

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.