Stanford CS336 Assignment 5: Alignment - Reasoning RL

Github repo.

This document contains questions from the assignment PDF, implemented solutions, and experimental results.

Assignment Overview

In this assignment, we gain hands-on experience training a language model to reason and solve downstream tasks using reinforcement learning (RL).

What We Implement

What We Run

Part 1: Prompting

Problem (prompting_baselines): Run OLMo-2-0425-1B on GSM8K (5 points)

(a) Write a script to evaluate OLMo-2-0425-1B performance on GSM8K with zero-shot question_only, zero-shot r1_zero, and few-shot r1_zero_three_shot prompts.

(b) Observing the model outputs, characterize the model's behavior with each prompt.

Solution: Prompting Baselines Implementation

I implemented a basic prompting baseline script:

from vllm_utils import VLLMServer

def prompting_baselines():
    vllm_server = VLLMServer("allenai/OLMo-2-0425-1B")
    vllm_server.start()

if __name__ == "__main__":
    prompting_baselines()

Experimental Results

Prompt Style Total Questions Answers with Right Format Right Answers
Question only 1319 412 6
Zero shot prompting 1319 516 0
Three shot prompting 1319 1219 233

✓ Implemented prompting baselines with vLLM server

✓ Evaluated on GSM8K dataset with multiple prompt strategies

Part 2: Group Relative Policy Optimization (GRPO)

Problem (baseline_calcs): Compute the variance of the policy gradient estimator (5 points)

Let π_θ define a policy over the binary action space A = {0, 1} with π_θ(A=1) = p = σ(θ). Our binary reward function will assign reward 1 to action A = 1 and reward 0 otherwise.

(a) What is the variance of the policy gradient estimator?

(b) What is the variance of the baseline-adjusted policy gradient estimator?

(c) What is the resulting variance if we substitute the "population mean" baseline b = p?

Solution: Baseline Calculations

I worked out the baseline calculations on paper:

Baseline calculation page 1 Baseline calculation page 2 Baseline calculation page 3 Baseline calculation page 4 Baseline calculation page 5

✓ Completed variance calculations for policy gradient estimator

Part 3: GRPO Implementation

Core GRPO Components

Solution: GRPO Implementation

Key components from grpo.py:

def tokenize_prompt_and_output(
    prompt_strs: list[str],
    output_strs: list[str],
    tokenizer: PreTrainedTokenizer,
) -> dict[str, torch.Tensor]:
    prompt_ids = tokenizer(prompt_strs, add_special_tokens=False)["input_ids"]
    output_ids = tokenizer(output_strs, add_special_tokens=False)["input_ids"]

    seqs = [p + o for p, o in zip(prompt_ids, output_ids)]
    max_len = max(len(s) for s in seqs)

    pad_id = tokenizer.pad_token_id
    batch_size = len(seqs)
    padded = torch.full((batch_size, max_len), pad_id, dtype=torch.long)
    is_response = torch.zeros((batch_size, max_len), dtype=torch.bool)

    for i, (p, o) in enumerate(zip(prompt_ids, output_ids)):
        seq = p + o
        padded[i, :len(seq)] = torch.tensor(seq, dtype=torch.long)
        is_response[i, len(p): len(seq)] = True

    return {
        "input_ids": padded[:, :-1],
        "labels": padded[:, 1:],
        "response_mask": is_response[:, 1:]
    }

def grpo_train_step(
    model: PreTrainedModel,
    tokenizer: PreTrainedTokenizer,
    optimizer: torch.optim.Optimizer,
    gradient_accumulation_steps: int,
    max_grad_norm: float | None,
    reward_fn: Callable[[str, str], dict[str, float]],
    repeated_prompts: list[str],
    rollout_responses: list[str],
    repeated_ground_truths: list[str],
    group_size: int,
    baseline: Literal["mean", "none"] = "mean",
    advantage_eps: float = 1e-6,
    advantage_normalizer: Literal["std", "none", "mean"] = "std",
    importance_reweighting_method: Literal["none", "noclip", "grpo", "gspo"] = "none",
    old_log_probs: torch.Tensor | None = None,
    cliprange: float | None = None,
    loss_normalization: Literal["sequence", "constant"] = "sequence",
    normalization_constant: int | None = None,
) -> tuple[torch.Tensor, dict[str, torch.Tensor | float]]:
    # Implementation details...
    # Tokenize prompts and outputs
    tokenized = tokenize_prompt_and_output(repeated_prompts, rollout_responses, tokenizer)

    # Compute rewards
    raw_rewards, reward_metadata = compute_rollout_rewards(
        reward_fn, rollout_responses, repeated_ground_truths
    )

    # Normalize rewards to get advantages
    advantages, _ = compute_group_normalized_rewards(
        raw_rewards, group_size, baseline, advantage_eps, advantage_normalizer
    )

    # Training with gradient accumulation...
    return torch.tensor(total_loss), metadata

✓ Implemented all core GRPO components

✓ Integrated components into complete training step

Part 4: GRPO Training and Experiments

Problem (grpo_experiments_standard_on_policy): Use GRPO to improve OLMo-2-0425-1B performance on GSM8K (10 points)

Run GRPO training with the specified hyperparameters and multiple random seeds.

Solution: GRPO Training Script

Complete training script from train_grpo.py:

# Hardcoded configuration
RANDOM_SEED = 42
MODEL_NAME = "allenai/OLMo-2-0425-1B"
DATASET_PATH = "../data/gsm8k"
PROMPT_PATH = "../prompts/r1_zero.prompt"

# Training hyperparameters
N_TRAIN_EXAMPLES = 6400
N_VAL_EXAMPLES = 1024
NUM_ROLLOUT_STEPS = 200
LEARNING_RATE = 1e-5
ROLLOUT_BATCH_SIZE = 256
TRAIN_BATCH_SIZE = 256
GROUP_SIZE = 8
GRADIENT_ACCUMULATION_STEPS = 32
SAMPLING_TEMPERATURE = 1.0
SAMPLING_MAX_TOKENS = 512
MAX_GRAD_NORM = 1.0

def main():
    # Initialize model and datasets
    model, tokenizer = get_model_and_tokenizer(MODEL_NAME)
    train_data = load_dataset("train")[:N_TRAIN_EXAMPLES]
    val_data = load_dataset("test")[:N_VAL_EXAMPLES]

    # Initialize vLLM for inference
    vllm_engine = LLM(model=MODEL_NAME, tensor_parallel_size=1)

    # Training loop
    for step in tqdm(range(NUM_ROLLOUT_STEPS)):
        # Sync weights to vLLM
        sync_weights_to_vllm(model, vllm_engine, temp_dir)

        # Generate rollouts
        repeated_prompts, rollout_responses = generate_rollouts(
            vllm_engine, batch_prompts, SAMPLING_TEMPERATURE,
            SAMPLING_MAX_TOKENS, GROUP_SIZE
        )

        # GRPO training step
        loss, metadata = grpo_train_step(
            model, tokenizer, optimizer,
            gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS,
            max_grad_norm=MAX_GRAD_NORM,
            reward_fn=r1_zero_reward_fn,
            repeated_prompts=repeated_prompts,
            rollout_responses=rollout_responses,
            repeated_ground_truths=repeated_ground_truths,
            group_size=GROUP_SIZE,
            baseline="mean",
            advantage_normalizer="std",
            loss_normalization="sequence"
        )

Experimental Results

Ran GRPO experiments with 4 random seeds (42, 43, 44, 45):

GRPO results graph 1 GRPO results graph 2 GRPO results graph 3

Key findings:

✓ Implemented complete GRPO training pipeline

✓ Ran experiments with multiple seeds

✓ Achieved significant performance improvement

Part 5: Learning Rate Sweep

Problem (grpo_learning_rate): Tune the learning rate (3 points)

Perform a sweep over learning rates and report final validation rewards.

Solution: Learning Rate Sweep

Tested 5 learning rates: 1e-6, 3e-6, 1e-5, 3e-5, 1e-4

Learning rate sweep results 1 Learning rate sweep results 2 Learning rate sweep results 3

Key findings:

✓ Completed learning rate sweep

✓ Identified optimal learning rate

Part 6: Prompt Ablation

Problem (grpo_prompt_ablation): Prompt ablation (3 points)

Run GRPO with question_only and r1_zero_three_shot prompts.

Solution: Prompt Ablation Results

Tested with question_only prompt:

Question-only prompt results

Results:

✓ Completed prompt ablation study

Part 7: Length Normalization Analysis

Problem: Length normalization vs constant normalization

Compare the theoretical and practical differences between sequence length normalization and constant normalization.

Solution: Normalization Analysis

Length Normalization:

Constant Normalization:

Trade-offs:

✓ Analyzed theoretical differences between normalization strategies

Part 8: RL Algorithm Variants

RL Variants to Implement

Solution: RL Variants Implementation

Extended grpo.py to support variants:

def compute_group_normalized_rewards(
    raw_rewards: torch.Tensor,
    group_size: int,
    baseline: Literal["mean", "none"] = "mean",
    advantage_eps: float = 1e-6,
    advantage_normalizer: Literal["std", "none", "mean"] = "std",
):
    group_wise_rewards = raw_rewards.reshape(len(raw_rewards) // group_size, group_size)
    mean_rewards = group_wise_rewards.mean(dim=-1, keepdim=True)
    std_rewards = group_wise_rewards.std(dim=-1, keepdim=True)

    if baseline == "mean":
        group_wise_rewards -= mean_rewards

    if advantage_normalizer == "std":
        advantages = group_wise_rewards / (std_rewards + torch.tensor(advantage_eps))
    elif advantage_normalizer == "mean":
        advantages = group_wise_rewards / mean_rewards
    else:
        advantages = group_wise_rewards

    return advantages.flatten(), {}

⚠ Partial Implementation: Need to run full experiments comparing Dr. GRPO, RFT, and MaxRL variants

Part 9: Off-Policy RL

Off-Policy Components

Solution: Off-Policy Implementation

Extended policy gradient loss for off-policy training:

def compute_policy_gradient_loss(
    raw_rewards_or_advantages: torch.Tensor,
    policy_log_probs: torch.Tensor,
    importance_reweighting_method: Literal["none", "noclip", "grpo", "gspo"] = "none",
    old_log_probs: torch.Tensor | None = None,
    cliprange: float | None = None,
    response_mask: torch.Tensor | None = None,
) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:
    if importance_reweighting_method != "none":
        raise NotImplementedError  # Placeholder for off-policy implementation

    # Standard on-policy loss computation
    if raw_rewards_or_advantages.dim() == 1:
        raw_rewards_or_advantages = raw_rewards_or_advantages.unsqueeze(-1)

    per_token_loss = -raw_rewards_or_advantages * policy_log_probs
    return per_token_loss, {}

⚠ Yet to be done: Complete off-policy experiments with importance reweighting and clipping

Summary and Conclusions

Key Achievements

Areas for Future Work

Code Repository Structure

stanford-cs336-assignment-5/
├── cs336_alignment/
│   ├── grpo.py                 # Core GRPO implementation
│   ├── train_grpo.py           # Training script
│   ├── prompting_baselines.py  # Prompting experiments
│   ├── checkpoint.py           # Model loading utilities
│   ├── vllm_utils.py          # vLLM server utilities
│   └── drgrpo_grader.py      # Reward function
├── prompts/
│   ├── r1_zero.prompt
│   ├── r1_zero_three_shot_gsm8k.prompt
│   └── question_only.prompt
└── data/
    └── gsm8k/
        ├── train.jsonl
        └── test.jsonl