Skip to content

Commit 6b56292

Browse files
author
Kevin-Li-2025
committed
Merge T4 GRPO experiment into L20-CodeForge
1 parent d2cb22f commit 6b56292

16 files changed

Lines changed: 1513 additions & 0 deletions

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ The repository contains:
8585
- Candidate health audits for syntax, entrypoint, and execution failure modes.
8686
- A trajectory schema for repo-repair agents and model training data.
8787
- SFT, DPO, reward-function, and GRPO/RLVR scaffolding.
88+
- A migrated T4 GRPO reasoning experiment under
89+
[`experiments/grpo_t4/`](experiments/grpo_t4/), retained as an experiment
90+
artifact rather than a separate public repo.
8891
- Single-L20 setup scripts and GPU sanity checks.
8992

9093
## Quickstart

experiments/grpo_t4/.gitignore

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.ipynb_checkpoints
6+
.venv/
7+
venv/
8+
ENV/
9+
env/
10+
11+
# OS-specific
12+
.DS_Store
13+
.DS_Store?
14+
._*
15+
.Spotlight-V100
16+
.Trashes
17+
ehthumbs.db
18+
Thumbs.db
19+
20+
# Kaggle metadata and generated outputs
21+
kernel-metadata.json
22+
grpo_notebook.ipynb
23+
inference_notebook.ipynb
24+
grpo_runs/
25+
grpo_r1_adapter/
26+
my_model/
27+
*.log

experiments/grpo_t4/README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# DeepSeek-R1-Mini-T4: GRPO Reinforcement Learning Alignment Pipeline
2+
3+
> Migrated from
4+
> [`Kevin-Li-2025/grpo-deepseek-r1-t4`](https://github.com/Kevin-Li-2025/grpo-deepseek-r1-t4)
5+
> into L20-CodeForge as `experiments/grpo_t4/`. Generated Unsloth compiled-cache
6+
> files were intentionally not migrated; this directory keeps only the scripts,
7+
> metrics, plots, and written results needed to preserve the experiment.
8+
9+
This repository contains a complete pipeline to train and evaluate a reasoning LLM using **GRPO (Group Relative Policy Optimization)** on resource-constrained hardware (e.g., Kaggle's free Tesla T4 GPUs).
10+
11+
It implements a DeepSeek-R1 style reinforcement learning loop that teaches a 1.5B parameters model to reason step-by-step using `<reasoning>` and `<answer>` tags, optimized for training and inference stability.
12+
13+
---
14+
15+
## 🚀 Key Features
16+
17+
* **GRPO Training Implementation**: Direct policy reinforcement optimization using custom rule-based rewards without requiring a Critic model (saving up to 50% VRAM).
18+
* **Multi-Aspect Reward Design**:
19+
1. `format_reward`: Enforces thinking layout containing strict `<reasoning>` and `<answer>` blocks.
20+
2. `correctness_reward`: Parses mathematical responses and validates calculations against ground truth (GSM8K).
21+
3. `length_penalty`: Mitigates "thought-length hacking" (penalizes redundant loops).
22+
* **VRAM-Optimized Environment Setup**: Configured to run on standard T4 GPUs by disabling memory-heavy vLLM dependencies, enabling gradient checkpointing, and using `Unsloth` 4-bit quantization.
23+
* **Automated Cloud Execution**: Includes wrapper scripts to convert Python code into Jupyter Notebooks and programmatically deploy them to Kaggle Kernels.
24+
25+
---
26+
27+
## 📐 GRPO Architecture
28+
29+
```mermaid
30+
graph TD
31+
Prompt[Input Math Problem] --> Model[Qwen2.5-Math-1.5B Policy]
32+
Model -->|Generate G=4 completions| Outputs[Completions 1, 2, 3, 4]
33+
Outputs --> R1[Format Reward: <reasoning> and <answer> validation]
34+
Outputs --> R2[Correctness Reward: Match ground truth output]
35+
Outputs --> R3[Length Penalty: Exclude redundant outputs]
36+
R1 & R2 & R3 --> GRPO[Compute Relative Advantages & Update Weights]
37+
GRPO --> Model
38+
```
39+
40+
---
41+
42+
## 📁 Repository Structure
43+
44+
```
45+
├── grpo_trainer.py # Main GRPO training script using TRL and Unsloth
46+
├── push_kernel.py # Converts training script to Notebook and pushes to Kaggle
47+
├── inference_test.py # Inference evaluation script loading the merged model
48+
├── push_inference.py # Deploys inference test to Kaggle referencing the training outputs
49+
└── .gitignore # Git exclusion rules
50+
```
51+
52+
---
53+
54+
## 🛠️ Usage Instructions
55+
56+
### 1. Prerequisite: Kaggle CLI
57+
Install the Kaggle CLI and set up your authentication key:
58+
```bash
59+
pip install kaggle
60+
mkdir -p ~/.kaggle
61+
echo '{"username":"YOUR_USERNAME","key":"YOUR_KEY"}' > ~/.kaggle/kaggle.json
62+
chmod 600 ~/.kaggle/kaggle.json
63+
```
64+
65+
### 2. Triggering the Training Job
66+
Run the push script to convert `grpo_trainer.py` to a Kaggle Notebook format and deploy it to a remote GPU container:
67+
```bash
68+
python3 push_kernel.py
69+
```
70+
Monitor the run using:
71+
```bash
72+
kaggle kernels status YOUR_USERNAME/grpo-deepseek-r1-t4
73+
```
74+
75+
### 3. Running Inference Verification
76+
Once the training kernel completes and saves the merged weights, deploy the inference evaluation:
77+
```bash
78+
python3 push_inference.py
79+
```
80+
81+
---
82+
83+
## 🎯 Verification Results
84+
85+
During testing, the aligned `Qwen2.5-Math-1.5B` model generated perfect reasoning trajectories. Here is a sample evaluation output:
86+
87+
### Problem: Algebra Equation
88+
* **Question**: `Solve for x: 3x + 7 = 22. Provide the numeric value.`
89+
* **Generated Response**:
90+
```html
91+
To solve the equation 3x + 7 = 22 for x, we will follow these steps:
92+
93+
1. Isolate the term with the variable x by subtracting 7 from both sides:
94+
3x + 7 - 7 = 22 - 7
95+
3x = 15
96+
97+
2. Solve for x by dividing both sides of the equation by 3:
98+
3x/3 = 15/3
99+
x = 5
100+
101+
So, the solution to the equation is \boxed{5}.
102+
```

experiments/grpo_t4/benchmark.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import os
2+
import re
3+
import argparse
4+
import torch
5+
from unsloth import FastLanguageModel
6+
from datasets import load_dataset
7+
from tqdm import tqdm
8+
9+
def extract_gt(text):
10+
if "####" in text:
11+
return text.split("####")[-1].strip()
12+
return None
13+
14+
def extract_answer(content):
15+
# Extract prediction inside <answer>...</answer>
16+
pred_match = re.search(r"<answer>(.*?)</answer>", content, re.DOTALL)
17+
if pred_match:
18+
return pred_match.group(1).strip()
19+
return None
20+
21+
def verify_format(content):
22+
# Verify presence of <reasoning> and <answer> blocks
23+
pattern = r"<reasoning>.*?</reasoning>\s*<answer>.*?</answer>"
24+
return 1.0 if re.search(pattern, content, re.DOTALL) else 0.0
25+
26+
def run_benchmark(model_name, num_samples=30):
27+
print(f"Loading model: {model_name}...")
28+
max_seq_length = 1024
29+
30+
model, tokenizer = FastLanguageModel.from_pretrained(
31+
model_name = model_name,
32+
max_seq_length = max_seq_length,
33+
load_in_4bit = True,
34+
fast_inference = False
35+
)
36+
model.eval()
37+
38+
print("Loading GSM8K test dataset...")
39+
dataset = load_dataset("openai/gsm8k", "main", split=f"test[:{num_samples}]")
40+
41+
SYSTEM_PROMPT = """
42+
Respond in the following format:
43+
<reasoning>
44+
...
45+
</reasoning>
46+
<answer>
47+
...
48+
</answer>
49+
"""
50+
51+
correct = 0
52+
format_compliant = 0
53+
total_thought_length = 0
54+
55+
print(f"\nEvaluating on {num_samples} samples...")
56+
for item in tqdm(dataset):
57+
question = item["question"]
58+
raw_ans = item["answer"]
59+
gt = extract_gt(raw_ans)
60+
61+
messages = [
62+
{"role": "system", "content": SYSTEM_PROMPT},
63+
{"role": "user", "content": question}
64+
]
65+
66+
inputs = tokenizer.apply_chat_template(
67+
messages,
68+
tokenize=True,
69+
add_generation_prompt=True,
70+
return_tensors="pt"
71+
).to("cuda")
72+
73+
with torch.no_grad():
74+
outputs = model.generate(
75+
input_ids=inputs,
76+
max_new_tokens=512,
77+
temperature=0.6,
78+
top_p=0.95,
79+
use_cache=True
80+
)
81+
82+
generated_tokens = outputs[0][len(inputs[0]):]
83+
response = tokenizer.decode(generated_tokens, skip_special_tokens=True)
84+
85+
# Evaluate metrics
86+
pred = extract_answer(response)
87+
is_compliant = verify_format(response)
88+
89+
# Calculate thought length (characters inside <reasoning> tags)
90+
reasoning_match = re.search(r"<reasoning>(.*?)</reasoning>", response, re.DOTALL)
91+
thought_len = len(reasoning_match.group(1)) if reasoning_match else 0
92+
total_thought_length += thought_len
93+
94+
if is_compliant:
95+
format_compliant += 1
96+
if gt and pred and gt == pred:
97+
correct += 1
98+
99+
accuracy = (correct / num_samples) * 100.0
100+
compliance = (format_compliant / num_samples) * 100.0
101+
avg_thought_len = total_thought_length / num_samples
102+
103+
print("\n--- Benchmark Results ---")
104+
print(f"Model: {model_name}")
105+
print(f"Accuracy: {accuracy:.2f}% ({correct}/{num_samples})")
106+
print(f"Format Compliance: {compliance:.2f}% ({format_compliant}/{num_samples})")
107+
print(f"Average Thought Length: {avg_thought_len:.2f} chars")
108+
109+
# Return metrics for report generation
110+
return {
111+
"accuracy": accuracy,
112+
"compliance": compliance,
113+
"thought_len": avg_thought_len
114+
}
115+
116+
if __name__ == "__main__":
117+
parser = argparse.ArgumentParser()
118+
parser.add_argument("--model", type=str, required=True, help="Path or repo name of the model to evaluate")
119+
parser.add_argument("--samples", type=int, default=30, help="Number of test samples")
120+
args = parser.parse_args()
121+
122+
run_benchmark(args.model, args.samples)

0 commit comments

Comments
 (0)