Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Expert guidance for GRPO/RL fine-tuning with TRL for reasoning and task-specific model training
.claude/skills/openlair-grpo-rl-training/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-18 | ✗→✓ | ▲ Improved | 188% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 220% | 0% |
| case-11 | ✓→✓ | = Same ✓ | 206% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 167% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 162% | 0% |
Expert-level guidance for implementing Group Relative Policy Optimization (GRPO) using the Transformer Reinforcement Learning (TRL) library. This skill provides battle-tested patterns, critical insights, and production-ready workflows for fine-tuning language models with custom reward functions.
Use GRPO training when you need to:
Do NOT use GRPO for:
Key Mechanism:
Critical Difference from PPO:
Mathematical Intuition:
For each prompt p:
1. Generate N completions: {c₁, c₂, ..., cₙ}
2. Compute rewards: {r₁, r₂, ..., rₙ}
3. Learn to increase probability of high-reward completions
relative to low-reward ones in the same groupGolden Rules:
Reward Function Types:
| Type | Use Case | Example Weight | |------|----------|----------------| | Correctness | Verifiable tasks (math, code) | 2.0 (highest) | | Format | Strict structure enforcement | 0.5-1.0 | | Length | Encourage verbosity/conciseness | 0.1-0.5 | | Style | Penalize unwanted patterns | -0.5 to 0.5 |
Critical Requirements:
Example Structure:
pythonfrom datasets import load_dataset, Dataset SYSTEM_PROMPT = """ Respond in the following format: <reasoning> [Your step-by-step thinking] </reasoning> <answer> [Final answer] </answer> """ def prepare_dataset(raw_data): """ Transform raw data into GRPO-compatible format. Returns: Dataset with columns: - 'prompt': List[Dict] with role/content (system + user messages) - 'answer': str (ground truth, optional but recommended) """ return raw_data.map(lambda x: { 'prompt': [ {'role': 'system', 'content': SYSTEM_PROMPT}, {'role': 'user', 'content': x['question']} ], 'answer': extract_answer(x['raw_answer']) })
Pro Tips:
Template Structure:
pythondef reward_function_name( prompts, # List[List[Dict]]: Original prompts completions, # List[List[Dict]]: Model generations answer=None, # Optional: Ground truth from dataset **kwargs # Additional dataset columns ) -> list[float]: """ Evaluate completions and return rewards. Returns: List of floats (one per completion) """ # Extract completion text responses = [comp[0]['content'] for comp in completions] # Compute rewards rewards = [] for response in responses: score = compute_score(response) rewards.append(score) return rewards
Example 1: Correctness Reward (Math/Coding)
pythondef correctness_reward(prompts, completions, answer, **kwargs): """Reward correct answers with high score.""" responses = [comp[0]['content'] for comp in completions] extracted = [extract_final_answer(r) for r in responses] return [2.0 if ans == gt else 0.0 for ans, gt in zip(extracted, answer)]
Example 2: Format Reward (Structured Output)
pythonimport re def format_reward(completions, **kwargs): """Reward XML-like structured format.""" pattern = r'<reasoning>.*?</reasoning>\s*<answer>.*?</answer>' responses = [comp[0]['content'] for comp in completions] return [1.0 if re.search(pattern, r, re.DOTALL) else 0.0 for r in responses]
Example 3: Incremental Format Reward (Partial Credit)
pythondef incremental_format_reward(completions, **kwargs): """Award partial credit for format compliance.""" responses = [comp[0]['content'] for comp in completions] rewards = [] for r in responses: score = 0.0 if '<reasoning>' in r: score += 0.25 if '</reasoning>' in r: score += 0.25 if '<answer>' in r: score += 0.25 if '</answer>' in r: score += 0.25 # Penalize extra text after closing tag if r.count('</answer>') == 1: extra_text = r.split('</answer>')[-1].strip() score -= len(extra_text) * 0.001 rewards.append(score) return rewards
Critical Insight: Combine 3-5 reward functions for robust training. Order matters less than diversity of signals.
Memory-Optimized Config (Small GPU)
pythonfrom trl import GRPOConfig training_args = GRPOConfig( output_dir="outputs/grpo-model", # Learning rate learning_rate=5e-6, # Lower = more stable adam_beta1=0.9, adam_beta2=0.99, weight_decay=0.1, warmup_ratio=0.1, lr_scheduler_type='cosine', # Batch settings per_device_train_batch_size=1, gradient_accumulation_steps=4, # Effective batch = 4 # GRPO-specific num_generations=8, # Group size: 8-16 recommended max_prompt_length=256, max_completion_length=512, # Training duration num_train_epochs=1, max_steps=None, # Or set fixed steps (e.g., 500) # Optimization bf16=True, # Faster on A100/H100 optim="adamw_8bit", # Memory-efficient optimizer max_grad_norm=0.1, # Logging logging_steps=1, save_steps=100, report_to="wandb", # Or "none" for no logging )
High-Performance Config (Large GPU)
pythontraining_args = GRPOConfig( output_dir="outputs/grpo-model", learning_rate=1e-5, per_device_train_batch_size=4, gradient_accumulation_steps=2, num_generations=16, # Larger groups = better signal max_prompt_length=512, max_completion_length=1024, num_train_epochs=1, bf16=True, use_vllm=True, # Fast generation with vLLM logging_steps=10, )
Critical Hyperparameters:
| Parameter | Impact | Tuning Advice | |-----------|--------|---------------| | num_generations | Group size for comparison | Start with 8, increase to 16 if GPU allows | | learning_rate | Convergence speed/stability | 5e-6 (safe), 1e-5 (faster, riskier) | | max_completion_length | Output verbosity | Match your task (512 for reasoning, 256 for short answers) | | gradient_accumulation_steps | Effective batch size | Increase if GPU memory limited |
Standard Setup (Transformers)
pythonimport torch from transformers import AutoModelForCausalLM, AutoTokenizer from peft import LoraConfig from trl import GRPOTrainer # Load model model_name = "Qwen/Qwen2.5-1.5B-Instruct" model = AutoModelForCausalLM.from_pretrained( model_name, torch_dtype=torch.bfloat16, attn_implementation="flash_attention_2", # 2-3x faster device_map="auto" ) tokenizer = AutoTokenizer.from_pretrained(model_name) tokenizer.pad_token = tokenizer.eos_token # Optional: LoRA for parameter-efficient training peft_config = LoraConfig( r=16, # Rank (higher = more capacity) lora_alpha=32, # Scaling factor (typically 2*r) target_modules=[ "q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj" ], task_type="CAUSAL_LM", lora_dropout=0.05, ) # Initialize trainer trainer = GRPOTrainer( model=model, processing_class=tokenizer, reward_funcs=[ incremental_format_reward, format_reward, correctness_reward, ], args=training_args, train_dataset=dataset, peft_config=peft_config, # Remove for full fine-tuning ) # Train trainer.train() # Save trainer.save_model("final_model")
Unsloth Setup (2-3x Faster)
pythonfrom unsloth import FastLanguageModel model, tokenizer = FastLanguageModel.from_pretrained( model_name="google/gemma-3-1b-it", max_seq_length=1024, load_in_4bit=True, fast_inference=True, max_lora_rank=32, ) model = FastLanguageModel.get_peft_model( model, r=32, target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], lora_alpha=32, use_gradient_checkpointing="unsloth", ) # Rest is identical to standard setup trainer = GRPOTrainer(model=model, ...) trainer.train()
Key metrics to watch:
reward: Average across all completionsreward_std: Diversity within groups (should remain > 0)kl: KL divergence from reference (should grow moderately)Healthy Training Pattern:
Step Reward Reward_Std KL
100 0.5 0.3 0.02
200 0.8 0.25 0.05
300 1.2 0.2 0.08 ← Good progression
400 1.5 0.15 0.12Warning Signs:
| Problem | Symptom | Solution | |---------|---------|----------| | Mode collapse | All completions identical | Increase num_generations, add diversity penalty | | No learning | Flat rewards | Check reward function logic, increase LR | | OOM errors | GPU memory exceeded | Reduce num_generations, enable gradient checkpointing | | Slow training | < 1 it/s | Enable use_vllm=True, use Unsloth, reduce seq length | | Format ignored | Model doesn't follow structure | Increase format reward weight, add incremental rewards |
For complex tasks, train in stages:
python# Stage 1: Format compliance (epochs=1) trainer_stage1 = GRPOTrainer( model=model, reward_funcs=[incremental_format_reward, format_reward], ... ) trainer_stage1.train() # Stage 2: Correctness (epochs=1) trainer_stage2 = GRPOTrainer( model=model, reward_funcs=[format_reward, correctness_reward], ... ) trainer_stage2.train()
pythonclass AdaptiveReward: def __init__(self, base_reward_func, initial_weight=1.0): self.func = base_reward_func self.weight = initial_weight def __call__(self, *args, **kwargs): rewards = self.func(*args, **kwargs) return [r * self.weight for r in rewards] def adjust_weight(self, success_rate): """Increase weight if model struggling, decrease if succeeding.""" if success_rate < 0.3: self.weight *= 1.2 elif success_rate > 0.8: self.weight *= 0.9
pythondef load_custom_knowledge_base(csv_path): """Example: School communication platform docs.""" import pandas as pd df = pd.read_csv(csv_path) dataset = Dataset.from_pandas(df).map(lambda x: { 'prompt': [ {'role': 'system', 'content': CUSTOM_SYSTEM_PROMPT}, {'role': 'user', 'content': x['question']} ], 'answer': x['expert_answer'] }) return dataset
python# Merge LoRA adapters into base model if hasattr(trainer.model, 'merge_and_unload'): merged_model = trainer.model.merge_and_unload() merged_model.save_pretrained("production_model") tokenizer.save_pretrained("production_model")
pythonfrom transformers import pipeline generator = pipeline( "text-generation", model="production_model", tokenizer=tokenizer ) result = generator( [ {'role': 'system', 'content': SYSTEM_PROMPT}, {'role': 'user', 'content': "What is 15 + 27?"} ], max_new_tokens=256, do_sample=True, temperature=0.7, top_p=0.9 ) print(result[0]['generated_text'])
Before Training:
During Training:
After Training:
python# Debug reward function def debug_reward(completions, **kwargs): responses = [comp[0]['content'] for comp in completions] for i, r in enumerate(responses[:2]): # Print first 2 print(f"Response {i}: {r[:200]}...") return [1.0] * len(responses) # Dummy rewards # Test without training trainer = GRPOTrainer(..., reward_funcs=[debug_reward]) trainer.generate_completions(dataset[:1]) # Generate without updating
Official Documentation:
Example Repositories:
Recommended Reading:
When this skill is loaded:
templates/ directory as starting pointsexamples/ for task-specific implementationsCritical Reminders:
This skill is designed for expert-level implementation. Beginners should start with supervised fine-tuning before attempting GRPO.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-04 | pass→pass | 12,422 | 15,028 | +21% | 1 | 1 | 0% | 2,327 | 7,458 | +220% | 0 | 0 | — |
case-11 | pass→pass | 10,666 | 8,801 | -17% | 1 | 1 | 0% | 2,084 | 6,378 | +206% | 0 | 0 | — |
case-03 | pass→pass | 14,222 | 9,757 | -31% | 1 | 1 | 0% | 2,380 | 6,349 | +167% | 0 | 0 | — |
case-01 | pass→pass | 15,361 | 11,260 | -27% | 1 | 1 | 0% | 2,636 | 6,914 | +162% | 0 | 0 | — |
case-02 | pass→pass | 11,184 | 10,460 | -6% | 1 | 1 | 0% | 2,036 | 6,604 | +224% | 0 | 0 | — |
case-05 | pass→pass | 15,050 | 10,357 | -31% | 1 | 1 | 0% | 2,549 | 6,564 | +158% | 0 | 0 | — |
case-06 | pass→pass | 3,659 | 2,494 | -32% | 1 | 1 | 0% | 657 | 5,143 | +683% | 0 | 0 | — |
case-07 | pass→pass | 10,096 | 5,535 | -45% | 1 | 1 | 0% | 1,808 | 5,726 | +217% | 0 | 0 | — |
case-08 | pass→pass | 11,790 | 9,085 | -23% | 1 | 1 | 0% | 2,187 | 6,422 | +194% | 0 | 0 | — |
case-09 | pass→pass | 17,089 | 6,810 | -60% | 1 | 1 | 0% | 3,039 | 5,955 | +96% | 0 | 0 | — |
case-10 | pass→pass | 3,165 | 2,271 | -28% | 1 | 1 | 0% | 588 | 5,099 | +767% | 0 | 0 | — |
case-12 | pass→pass | 3,342 | 2,950 | -12% | 1 | 1 | 0% | 568 | 5,294 | +832% | 0 | 0 | — |
case-13 | pass→pass | 14,900 | 12,723 | -15% | 1 | 1 | 0% | 2,874 | 7,462 | +160% | 0 | 0 | — |
case-14 | pass→pass | 14,526 | 15,787 | +9% | 1 | 1 | 0% | 2,570 | 7,743 | +201% | 0 | 0 | — |
case-15 | pass→pass | 6,453 | 4,375 | -32% | 1 | 1 | 0% | 1,103 | 5,491 | +398% | 0 | 0 | — |
case-16 | pass→pass | 5,220 | 3,088 | -41% | 1 | 1 | 0% | 1,006 | 5,295 | +426% | 0 | 0 | — |
case-17 | pass→pass | 13,417 | 5,305 | -60% | 1 | 1 | 0% | 2,356 | 5,660 | +140% | 0 | 0 | — |
case-18 | fail→pass | 10,952 | 5,993 | -45% | 1 | 1 | 0% | 2,085 | 6,013 | +188% | 0 | 0 | — |
case-19 | pass→pass | 5,984 | 2,806 | -53% | 1 | 1 | 0% | 1,232 | 5,309 | +331% | 0 | 0 | — |
case-20 | pass→pass | 13,286 | 8,779 | -34% | 1 | 1 | 0% | 2,332 | 6,262 | +169% | 0 | 0 | — |
case-21 | pass→pass | 9,715 | 11,695 | +20% | 1 | 1 | 0% | 1,759 | 6,959 | +296% | 0 | 0 | — |
case-22 | pass→pass | 10,986 | 8,330 | -24% | 1 | 1 | 0% | 1,948 | 6,205 | +219% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.