This document contains questions from the assignment PDF, implemented solutions, and experimental results.
In this assignment, we gain hands-on experience training a language model to reason and solve downstream tasks using reinforcement learning (RL).
(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.
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()
| 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
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?
I worked out the baseline calculations on paper:
✓ Completed variance calculations for policy gradient estimator
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
Run GRPO training with the specified hyperparameters and multiple random seeds.
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"
)
Ran GRPO experiments with 4 random seeds (42, 43, 44, 45):
Key findings:
✓ Implemented complete GRPO training pipeline
✓ Ran experiments with multiple seeds
✓ Achieved significant performance improvement
Perform a sweep over learning rates and report final validation rewards.
Tested 5 learning rates: 1e-6, 3e-6, 1e-5, 3e-5, 1e-4
Key findings:
✓ Completed learning rate sweep
✓ Identified optimal learning rate
Run GRPO with question_only and r1_zero_three_shot prompts.
Tested with question_only prompt:
Results:
✓ Completed prompt ablation study
Compare the theoretical and practical differences between sequence length normalization and constant normalization.
Length Normalization:
Constant Normalization:
Trade-offs:
✓ Analyzed theoretical differences between normalization strategies
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
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
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