chaddy81 commited on
Commit
09aa08f
·
verified ·
1 Parent(s): 26cdc56

Upload train_grpo_code.py with huggingface_hub

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