File size: 9,563 Bytes
09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f 0da65ef 09aa08f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 | #!/usr/bin/env python3
# /// script
# dependencies = [
# "trl>=0.12.0",
# "peft>=0.7.0",
# "transformers>=4.36.0",
# "accelerate>=0.24.0",
# "datasets",
# "trackio",
# "torch",
# ]
# ///
"""
GRPO Training for Code Generation with Execution-Based Rewards
Continues training from SFT model using GRPO with verifiable code rewards.
The reward function executes generated Python code against test cases.
Model: chaddy81/qwen3-0.6b-multicode-sft (LoRA on Qwen3-0.6B)
Dataset: open-r1/codeforces (verifiable-prompts subset)
Reward: Code execution correctness (0.0 = fail, 1.0 = pass)
"""
import os
import re
import subprocess
import tempfile
from typing import Any
import torch
import trackio
from datasets import load_dataset
from peft import LoraConfig, PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
from trl import GRPOTrainer, GRPOConfig
print("=" * 60)
print("๐ GRPO Code Training - Execution-Based Rewards")
print("=" * 60)
# Configuration
BASE_MODEL = "Qwen/Qwen3-0.6B"
SFT_ADAPTER = "chaddy81/qwen3-0.6b-multicode-sft"
OUTPUT_REPO = "chaddy81/qwen3-0.6b-multicode-grpo"
MAX_EXAMPLES = 1000 # Reduced for faster training
print(f"\n๐ฆ Configuration:")
print(f" Base model: {BASE_MODEL}")
print(f" SFT adapter: {SFT_ADAPTER}")
print(f" Output: {OUTPUT_REPO}")
print(f" Max examples: {MAX_EXAMPLES}")
# ============================================================================
# Code Execution Reward Function
# ============================================================================
def extract_python_code(text: str) -> str:
"""Extract Python code from model output (handles markdown blocks)."""
# Try to find code in markdown blocks first
patterns = [
r"```python\n(.*?)```",
r"```py\n(.*?)```",
r"```\n(.*?)```",
]
for pattern in patterns:
matches = re.findall(pattern, text, re.DOTALL)
if matches:
return matches[-1].strip()
# If no markdown blocks, try to find code after common markers
markers = ["Solution:", "Answer:", "Code:"]
for marker in markers:
if marker in text:
code_part = text.split(marker)[-1].strip()
if code_part:
return code_part
# Fallback: return text as-is (might be raw code)
return text.strip()
def run_python_code(code: str, stdin_input: str, timeout: float = 3.0) -> tuple[bool, str]:
"""Execute Python code with given input and return (success, output)."""
try:
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(code)
temp_file = f.name
try:
result = subprocess.run(
['python3', temp_file],
input=stdin_input,
capture_output=True,
text=True,
timeout=timeout,
)
output = result.stdout.strip()
return True, output
except subprocess.TimeoutExpired:
return False, "TIMEOUT"
except Exception as e:
return False, f"RUNTIME_ERROR: {str(e)}"
finally:
os.unlink(temp_file)
except Exception as e:
return False, f"SETUP_ERROR: {str(e)}"
def normalize_output(output: str) -> str:
"""Normalize output for comparison."""
return '\n'.join(line.strip() for line in output.strip().split('\n'))
def code_execution_reward(
completions: list[str],
official_tests: list[list[dict]],
examples: list[list[dict]],
**kwargs
) -> list[float]:
"""
Reward function that executes generated code against test cases.
Returns:
- 1.0 if code passes all tests
- Partial credit for some tests
- 0.0 if fails all tests
"""
rewards = []
for completion, tests, exs in zip(completions, official_tests, examples):
code = extract_python_code(completion)
if not code or len(code) < 10:
rewards.append(0.0)
continue
# Combine tests (limit to avoid long execution)
all_tests = []
if tests:
all_tests.extend(tests[:2])
if exs:
all_tests.extend(exs[:2])
if not all_tests:
rewards.append(0.0)
continue
# Run tests
passed = 0
total = len(all_tests)
for test in all_tests:
test_input = test.get('input', '')
expected_output = test.get('output', '')
success, actual_output = run_python_code(code, test_input, timeout=2.0)
if success:
if normalize_output(actual_output) == normalize_output(expected_output):
passed += 1
# Calculate reward
if passed == total:
reward = 1.0
elif passed > 0:
reward = 0.5 * (passed / total)
else:
reward = 0.0
rewards.append(reward)
return rewards
# ============================================================================
# Dataset Preparation
# ============================================================================
print("\n๐ฅ Loading dataset...")
dataset = load_dataset(
"open-r1/codeforces",
name="verifiable-prompts",
split="train"
)
print(f" Total examples: {len(dataset)}")
# Filter for Python problems with tests
print(" Filtering for Python problems with tests...")
dataset = dataset.filter(
lambda x: x.get('language') == 'python' and
((x.get('official_tests') and len(x['official_tests']) > 0) or
(x.get('examples') and len(x['examples']) > 0))
)
print(f" Filtered: {len(dataset)}")
# Limit dataset size
if len(dataset) > MAX_EXAMPLES:
dataset = dataset.shuffle(seed=42).select(range(MAX_EXAMPLES))
print(f" Limited to: {MAX_EXAMPLES}")
print(f"\nโ
Final dataset: {len(dataset)} examples")
# ============================================================================
# Model Loading - Merge SFT then save for GRPO
# ============================================================================
print("\n๐ง Loading and preparing model...")
# Step 1: Load base model and SFT adapter
print(" Loading base model...")
base_model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL,
torch_dtype=torch.bfloat16,
device_map="cpu",
trust_remote_code=True,
)
print(" Loading SFT adapter...")
model = PeftModel.from_pretrained(base_model, SFT_ADAPTER)
print(" Merging SFT adapter into base model...")
model = model.merge_and_unload()
# Step 2: Save merged model temporarily
merged_path = "/tmp/merged_sft_model"
print(f" Saving merged model to {merged_path}...")
model.save_pretrained(merged_path, safe_serialization=True)
# Load tokenizer
print(" Loading tokenizer...")
tokenizer = AutoTokenizer.from_pretrained(SFT_ADAPTER, trust_remote_code=True)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "left"
tokenizer.save_pretrained(merged_path)
# Free memory
del model
del base_model
torch.cuda.empty_cache()
print(" โ
Merged model saved")
# ============================================================================
# GRPO Training with fresh LoRA
# ============================================================================
print("\nโ๏ธ Configuring GRPO trainer...")
# LoRA config for GRPO (smaller rank for efficiency)
peft_config = LoraConfig(
r=8,
lora_alpha=16,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
bias="none",
task_type="CAUSAL_LM",
)
config = GRPOConfig(
# Output & Hub
output_dir="qwen3-grpo-code",
push_to_hub=True,
hub_model_id=OUTPUT_REPO,
hub_strategy="every_save",
hub_private_repo=False,
# GRPO parameters
num_generations=4,
max_completion_length=256, # Shorter for faster training
# Training parameters
num_train_epochs=1,
per_device_train_batch_size=1, # Small batch for memory
gradient_accumulation_steps=8,
learning_rate=5e-7,
# Optimization
warmup_ratio=0.1,
lr_scheduler_type="cosine",
bf16=True,
gradient_checkpointing=True,
# Logging & checkpoints
logging_steps=10,
save_strategy="steps",
save_steps=50,
save_total_limit=2,
# Monitoring
report_to="trackio",
project="qwen3-grpo-code",
run_name="grpo-codeforces-v2",
)
print(" Initializing trainer with merged SFT model + new LoRA...")
trainer = GRPOTrainer(
model=merged_path, # Pass path - trainer loads with proper gradients
processing_class=tokenizer,
reward_funcs=code_execution_reward,
train_dataset=dataset,
args=config,
peft_config=peft_config, # New LoRA for GRPO
)
print("\n๐ Starting GRPO training...")
print(" Training will generate code, execute it, and learn from results.")
print("=" * 60)
trainer.train()
print("\n๐พ Pushing to Hub...")
trainer.push_to_hub()
# Finish tracking
trackio.finish()
print("\n" + "=" * 60)
print("โ
GRPO Training Complete!")
print(f"๐ฆ Model: https://huggingface.co/{OUTPUT_REPO}")
print(f"๐ Metrics: https://huggingface.co/spaces/chaddy81/trackio")
print("=" * 60)
|