Magistral: Scaling Reasoning with Reinforcement Learning
Core Concept
Magistral demonstrates that pure reinforcement learning (without cold-start distilled reasoning traces) achieves strong reasoning through carefully designed rewards and scalable infrastructure. The system improves AIME-24 pass@1 accuracy by nearly 50% using a modified GRPO algorithm, asynchronous weight updates, and multi-stage curriculum learning. Novel insights: RL improves smaller models beyond distillation baselines and unexpectedly enhances multimodal capabilities despite text-only training.
Architecture Overview
- Modified GRPO Algorithm: Five adaptations including eliminated KL divergence penalty, loss normalization across generation groups, advantage normalization, relaxed clipping (Clip-Higher), and zero-variance filtering
- Four-Component Reward Architecture: Formatting (think tags), correctness (verified answers/test passing), length penalty (soft constraints), language consistency (multilingual response matching)
- Asynchronous Infrastructure: Continuous weight updates without interrupting generators; NCCL broadcast under 5 seconds; GPU-to-GPU communication minimized
- Multi-Stage Curriculum: Difficulty progression across stages; batch size reduction (8k→4k→2k); completion length increases (16k→24k→32k tokens)
- Data Curation Pipeline: Mathematical problems filtered from 699k to 38k; code problems validated at 35k with comprehensive test suites
Implementation
Step 1: Modified GRPO Optimizer Setup
import torch
import torch.nn as nn
from torch.optim import Optimizer
class ModifiedGRPO(Optimizer):
"""
Group Relative Policy Optimization with Magistral adaptations:
- Eliminated KL divergence penalty
- Loss normalization across generation groups
- Advantage normalization at minibatch level
- Relaxed upper clipping (Clip-Higher strategy)
- Zero-variance group filtering
"""
def __init__(self, params, lr=1e-5, epsilon=1e-6,
clip_ratio=2.0, clip_lower=0.5):
defaults = dict(lr=lr, epsilon=epsilon,
clip_ratio=clip_ratio, clip_lower=clip_lower)
super().__init__(params, defaults)
def step(self, closure=None):
loss = None
if closure is not None:
loss = closure()
for group in self.param_groups:
for p in group['params']:
if p.grad is None:
continue
grad = p.grad.data
# Clip-Higher strategy: relaxed upper bound
# clip_ratio acts as soft upper bound, not hard limit
if group['clip_ratio'] is not None:
# Allow exploration beyond standard PPO bounds
grad_magnitude = torch.norm(grad)
if grad_magnitude > group['clip_ratio']:
grad = grad / (grad_magnitude / group['clip_ratio'])
p.data.add_(grad, alpha=-group['lr'])
return loss
Step 2: Reward Shaping Architecture
class RewardShaper:
"""
Four-component reward function for reasoning tasks.
Combines formatting, correctness, length penalty, and language consistency.
"""
def __init__(self, language_code='en', length_target=2000):
self.language_code = language_code
self.length_target = length_target
self.code_executor = CodeExecutor()
self.answer_verifier = AnswerVerifier()
def compute_reward(self, generated_text, reference_answer=None,
user_language='en', is_code=False):
"""
Compute total reward from four components.
Returns scalar reward in [0, 1] range.
"""
rewards = {}
# Component 1: Formatting reward
has_think_tags = '<think>' in generated_text and '</think>' in generated_text
rewards['formatting'] = 0.25 if has_think_tags else 0.0
# Component 2: Correctness reward
if is_code:
passed_tests = self.code_executor.run_tests(generated_text)
rewards['correctness'] = min(passed_tests / 10, 1.0) * 0.50 # Up to 50%
else:
is_correct = self.answer_verifier.verify(generated_text, reference_answer)
rewards['correctness'] = 0.50 if is_correct else 0.0
# Component 3: Length penalty (soft constraint)
response_length = len(generated_text.split())
length_penalty = 1.0 - abs(response_length - self.length_target) / (2 * self.length_target)
length_penalty = max(0, length_penalty)
rewards['length'] = length_penalty * 0.15 # Up to 15%
# Component 4: Language consistency
response_language = self._detect_language(generated_text)
language_bonus = 0.10 if response_language == user_language else 0.0
rewards['language'] = language_bonus
total_reward = sum(rewards.values())
return {
'total': min(total_reward, 1.0),
'breakdown': rewards
}
def _detect_language(self, text):
"""Detect primary language of generated text."""
# Simplified: check for non-ASCII patterns
if any(ord(c) > 127 for c in text):
return 'non-en'
return 'en'
Step 3: Asynchronous Training Infrastructure
class AsynchronousTrainer:
"""
Manages continuous weight updates without interrupting generators.
Key innovation: GPU-to-GPU NCCL broadcast completes in <5 seconds.
"""
def __init__(self, model, reward_shaper, num_generators=8):
self.model = model
self.reward_shaper = reward_shaper
self.num_generators = num_generators
self.model_queue = []
self.latest_weights = None
def async_generation_loop(self, generator_id, input_batch):
"""
Generator continuously produces samples without blocking on updates.
Receives periodic weight synchronization via broadcast.
"""
local_model = copy.deepcopy(self.model)
while True:
# Generate samples with current local weights
with torch.no_grad():
outputs = local_model.generate(
input_batch,
max_length=2048,
temperature=1.0,
do_sample=True,
num_return_sequences=4
)
# Non-blocking weight sync: receive latest model via NCCL
if self.latest_weights is not None:
# GPU-to-GPU broadcast: <5 seconds
local_model.load_state_dict(self.latest_weights, non_blocking=True)
yield outputs
def update_weights(self, loss_tensor):
"""
Optimizer step updates weights and broadcasts to all generators.
Does not block generation pipeline.
"""
self.optimizer.zero_grad()
loss_tensor.backward()
# Apply gradient clipping
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=1.0)
self.optimizer.step()
# Broadcast updated weights to all generators asynchronously
self.latest_weights = dict(self.model.named_parameters())
Step 4: Multi-Stage Curriculum Learning
class CurriculumScheduler:
"""
Manages difficulty progression and training configuration across stages.
Increases challenge: batch size reduction, completion length increase.
"""
def __init__(self, num_stages=3):
self.num_stages = num_stages
self.current_stage = 0
self.stage_config = [
{'batch_size': 8192, 'max_length': 16000, 'difficulty': 'easy'},
{'batch_size': 4096, 'max_length': 24000, 'difficulty': 'medium'},
{'batch_size': 2048, 'max_length': 32000, 'difficulty': 'hard'}
]
def get_current_config(self):
return self.stage_config[min(self.current_stage, len(self.stage_config) - 1)]
def progress_stage(self, val_accuracy):
"""Advance to next stage based on validation performance."""
if val_accuracy > 0.7 and self.current_stage < self.num_stages - 1:
self.current_stage += 1
print(f"Curriculum progression: stage {self.current_stage}")
Practical Guidance
Data Preparation:
- Mathematical problems: Collect diverse sources (competition, textbooks); validate with multiple verifiers (SymPy, WolframAlpha)
- Code problems: Use comprehensive test suites (50+ test cases minimum); separate train/test data carefully
- Filtering strategy: Remove ambiguous or duplicate problems; keep diverse difficulty spectrum
Reward Design Principles:
- Formatting reward (25%): Enforce think tags for chain-of-thought; strong signal during early training
- Correctness reward (50%): Most important component; use automated verification (answer matching or test execution)
- Length penalty (15%): Soft constraint; encourage natural length without hard cutoffs
- Language consistency (10%): Multilingual support; bonus when response matches user's language
Scaling Considerations:
- Batch size correlation: Larger batches enable stable advantage normalization; 8k-16k optimal
- Asynchronous updates: Maximum 10-second intervals between weight syncs; generators may use slightly stale weights
- GPU utilization: 8-16 generator GPUs + 2-4 optimizer GPUs; communication bandwidth saturates around 16 generators
When to Use Magistral Approach:
- Building reasoning from scratch without distillation data
- Multilingual reasoning models (language consistency reward handles this)
- Long-context reasoning (curriculum handles up to 32k tokens)
- Smaller models (<25B) where RL outperforms distillation
Reference
- Group Relative Policy Optimization (GRPO): advantage normalization improves stability in RL for generation
- NCCL all-reduce: collective communication primitive; broadcast is unidirectional variant
- Curriculum learning: gradual difficulty progression prevents early convergence to local optima