junaid0600 commited on
Commit
79b5f71
Β·
verified Β·
1 Parent(s): 1bee2ce

Update training/train_agent.py

Browse files
Files changed (1) hide show
  1. training/train_agent.py +211 -282
training/train_agent.py CHANGED
@@ -1,372 +1,301 @@
1
  """
2
  training/train_agent.py β€” SQL Database Engineer Agent
3
- FIXED: Uses local DatabaseSimulator for rewards (no HF Space calls)
4
- - No shared singleton state
5
- - Real delta rewards (0.0 for wrong actions, 40-75pts for correct)
6
- - Clear reward difference teaches model to prefer create_index over inspect_query
7
  """
8
 
9
- import os, json, sys, time
10
- from pathlib import Path
 
 
 
11
 
12
- # ── GPU check ─────────────────────────────────────────────────
13
- UNSLOTH_AVAILABLE = False
14
  try:
15
- import torch
16
- if not torch.cuda.is_available():
17
- print("❌ No GPU. Unsloth requires CUDA GPU.")
18
- sys.exit(1)
19
  from unsloth import FastLanguageModel
20
  from trl import GRPOTrainer, GRPOConfig
21
- from datasets import Dataset
22
  UNSLOTH_AVAILABLE = True
23
- print(f"βœ… GPU: {torch.cuda.get_device_name(0)}")
24
- print(f"βœ… VRAM: {torch.cuda.get_device_properties(0).total_memory/1e9:.1f}GB")
25
- except ImportError as e:
26
- print(f"❌ {e}\nRun: pip install unsloth trl transformers datasets accelerate")
27
- sys.exit(1)
28
 
29
- # Add project root so we can import DatabaseSimulator
30
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
31
- from env.db_simulator import DatabaseSimulator
32
 
33
- # ── Config ────────────────────────────────────────────────────
34
  ENV_URL = os.getenv("ENV_URL", "https://junaid0600-sql-db-engineer-agent.hf.space")
35
  HF_TOKEN = os.getenv("HF_TOKEN", "")
36
- MODEL_NAME = os.getenv("MODEL_NAME", "unsloth/Qwen2.5-1.5B-Instruct")
37
  OUTPUT_DIR = os.getenv("OUTPUT_DIR", "./sdea-trained")
38
- MAX_STEPS = int(os.getenv("MAX_STEPS", "100"))
39
 
40
- print(f"\n[CONFIG] Model: {MODEL_NAME}")
41
- print(f"[CONFIG] Max steps: {MAX_STEPS}")
42
- print(f"[CONFIG] Output: {OUTPUT_DIR}\n")
 
 
 
 
 
 
43
 
44
- # ── System prompt ─────────────────────────────────────────────
45
- SYSTEM_PROMPT = """You are a senior database engineer fixing slow database queries.
46
- You will see slow queries and table structures. Choose the BEST action.
47
 
48
- Key insight: create_index on the RIGHT columns fixes slow queries.
49
- Wrong columns = no improvement. Right columns = massive improvement.
50
 
51
- Respond with ONLY valid JSON:
52
- {"action_type": "create_index", "payload": {"table": "TABLE_NAME", "columns": ["COL1", "COL2"]}}
 
53
 
54
- Available actions: inspect_query, analyze_indexes, create_index, rewrite_query, analyze_statistics, submit_report"""
 
55
 
56
- # ── Load all 15 scenarios ─────────────────────────────────────
57
- def load_all_scenarios() -> list:
58
- scenarios = []
59
- base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
60
- for fname in ["easy_scenarios.json", "medium_scenarios.json", "hard_scenarios.json"]:
61
- path = os.path.join(base, "dataset", fname)
62
- try:
63
- with open(path) as f:
64
- data = json.load(f)
65
- scenarios.extend(data)
66
- print(f" βœ… Loaded {len(data)} from {fname}")
67
- except FileNotFoundError:
68
- print(f" ⚠️ {fname} not found")
69
- print(f" Total: {len(scenarios)} scenarios\n")
70
- return scenarios
71
 
72
- ALL_SCENARIOS = load_all_scenarios()
 
 
 
 
 
 
73
 
74
- # ── Parse LLM output ─────────────────────────────────────────
75
- def parse_action(text: str) -> dict:
76
- """Parse LLM output into action dict."""
77
  try:
78
- text = text.strip()
79
- for marker in ["```json", "```"]:
80
- if marker in text:
81
- parts = text.split(marker)
82
- text = parts[1] if len(parts) > 1 else parts[0]
83
- text = text.strip()
84
- data = json.loads(text)
85
- if "action_type" in data and "payload" in data:
86
- return data
87
- except Exception:
88
  pass
89
- return None # None = invalid JSON = penalized
90
 
91
-
92
- # ── LOCAL reward function using DatabaseSimulator ─────────────
93
- def compute_local_reward(action: dict, scenario: dict) -> tuple:
94
- """
95
- Compute reward LOCALLY using DatabaseSimulator.
96
- No HF Space calls. No shared state. Clean every time.
97
-
98
- Returns (reward_score, db_delta, milestone_bonus)
99
- """
100
- sim = DatabaseSimulator(scenario)
101
- baseline = sim.get_performance_score()
102
- hints = scenario.get("missing_index_hints", [])
103
-
104
- action_type = action.get("action_type", "")
105
- payload = action.get("payload", {})
106
-
107
- # Apply action to simulator
108
- if action_type == "create_index":
109
- result = sim.apply_action("create_index", payload)
110
- delta = result.get("delta", 0.0)
111
-
112
- elif action_type == "inspect_query":
113
- # Investigation β€” small reward, no DB change
114
- delta = 0.0
115
-
116
- elif action_type == "analyze_indexes":
117
- delta = 0.0
118
-
119
- elif action_type == "rewrite_query":
120
- result = sim.apply_action("rewrite_query", payload)
121
- delta = result.get("delta", 0.0)
122
-
123
- elif action_type == "analyze_statistics":
124
- result = sim.apply_action("analyze_statistics", payload)
125
- delta = result.get("delta", 0.0)
126
-
127
- elif action_type == "partition_table":
128
- result = sim.apply_action("partition_table", payload)
129
- delta = result.get("delta", 0.0)
130
-
131
- elif action_type == "submit_report":
132
- # Terminal: score based on how much DB improved so far
133
- final = sim.get_performance_score()
134
- improvement = max(0, final - baseline)
135
- delta = improvement
136
-
137
- else:
138
- delta = -5.0 # Unknown action = penalty
139
-
140
- final_score = sim.get_performance_score()
141
- improvement = max(0.0, final_score - baseline)
142
- max_possible = max(1.0, 100.0 - baseline)
143
-
144
- # ── Reward components ─────────────────────────────────────
145
- # 1. Step reward β€” different per action type
146
- step_rewards = {
147
- "inspect_query": 0.10,
148
- "analyze_indexes": 0.10,
149
- "create_index": 0.15,
150
- "rewrite_query": 0.20,
151
- "analyze_statistics":0.08,
152
- "partition_table": 0.15,
153
- "submit_report": 0.05,
154
  }
155
- step_r = step_rewards.get(action_type, 0.001)
156
 
157
- # 2. Delta reward β€” proportional to actual improvement
158
- delta_r = min(0.70, (improvement / max_possible) * 0.70)
159
 
160
- # 3. Milestone bonus β€” one-time for big improvements
161
- milestone_r = 0.0
162
- if improvement / max_possible >= 0.75:
163
- milestone_r = 0.40
164
- elif improvement / max_possible >= 0.50:
165
- milestone_r = 0.25
166
- elif improvement / max_possible >= 0.25:
167
- milestone_r = 0.15
168
 
169
- # 4. Penalty for wrong index (delta=0 on create_index)
170
- wrong_index_pen = 0.0
171
- if action_type == "create_index" and delta <= 0.0:
172
- wrong_index_pen = -0.15 # created useless index
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
- total = step_r + delta_r + milestone_r + wrong_index_pen
175
- total = max(0.001, min(0.999, total))
176
 
177
- return total, improvement, milestone_r
178
 
179
 
180
- # ── GRPO reward function ──────────────────────────────────────
181
  def reward_fn(prompts, completions, **kwargs):
182
  """
183
- LOCAL reward β€” no HTTP calls, no shared state.
184
- Each completion gets its own fresh DatabaseSimulator.
185
-
186
- Reward differences:
187
- inspect_query (always): 0.10 + 0.0 = 0.10
188
- create_index (wrong col): 0.15 - 0.15 = 0.001
189
- create_index (right col): 0.15 + 0.60 = 0.75+
190
-
191
- GRPO will learn: right create_index >> inspect_query >> wrong create_index
192
  """
193
  rewards = []
 
194
 
195
  for i, (prompt, completion) in enumerate(zip(prompts, completions)):
196
  try:
197
- # Get text
198
- if isinstance(completion, list):
199
- text = completion[0].get("content", "") if completion else ""
200
- else:
201
- text = str(completion)
202
-
203
- # Pick scenario (rotate through all)
204
- scenario = ALL_SCENARIOS[i % len(ALL_SCENARIOS)]
205
-
206
- # Parse action
207
  action = parse_action(text)
208
 
209
- if action is None:
210
- # Invalid JSON output β€” penalize
211
- rewards.append(0.001)
212
- print(f" [REWARD] scenario={scenario['id']} | "
213
- f"INVALID JSON | score=0.001")
214
- continue
215
-
216
- # Compute reward locally
217
- score, improvement, milestone = compute_local_reward(action, scenario)
 
 
 
 
 
 
 
 
 
 
218
  rewards.append(score)
219
 
220
- print(f" [REWARD] scenario={scenario['id']} | "
221
- f"action={action.get('action_type')} | "
222
- f"improvement=+{improvement:.1f}pts | "
223
- f"milestone=+{milestone:.2f} | "
224
- f"score={score:.3f}")
225
-
226
  except Exception as e:
227
- print(f" [REWARD] Error: {e}")
228
- rewards.append(0.001)
229
 
230
  return rewards
231
 
232
 
233
- # ── Build dataset ─────────────────────────────────────────────
234
- def build_dataset() -> Dataset:
235
- examples = []
236
- for i, s in enumerate(ALL_SCENARIOS):
237
- tables_str = json.dumps(s.get("tables", []))
238
- queries_str = json.dumps(s.get("slow_queries", []))
239
- hints_str = json.dumps(s.get("missing_index_hints", []))
240
-
241
- prompt = (
242
- f"{SYSTEM_PROMPT}\n\n"
243
- f"=== DATABASE STATE ===\n"
244
- f"Scenario: {s['id']}\n"
245
- f"Description: {s.get('description','')}\n"
246
- f"Tables: {tables_str}\n"
247
- f"Slow Queries: {queries_str}\n"
248
- f"Missing Index Hints: {hints_str}\n"
249
- f"Performance: {s.get('performance_score_baseline',0)}/100 "
250
- f"β†’ Target: {s.get('target_score',85)}/100\n\n"
251
- f"What action should you take? Output JSON only:"
252
- )
253
- examples.append({
254
- "prompt": prompt,
255
- "scenario_id": s["id"],
256
- })
257
 
258
- print(f" βœ… Dataset: {len(examples)} examples")
259
- return Dataset.from_list(examples)
 
260
 
 
 
 
 
 
 
 
 
 
261
 
262
- # ── Generate plots ────────────────────────────────────────────
263
- def generate_plots(trainer):
264
- import matplotlib
265
- matplotlib.use("Agg")
266
- import matplotlib.pyplot as plt
267
 
268
- logs = [l for l in trainer.state.log_history if "loss" in l]
269
- if not logs:
270
- print("⚠️ No logs for plotting")
271
- return
272
 
273
- steps = [l.get("step", i) for i,l in enumerate(logs)]
274
- losses = [l.get("loss", 0) for l in logs]
 
 
 
 
 
275
 
276
- fig, ax = plt.subplots(1, 1, figsize=(8, 4))
277
- fig.suptitle("GRPO Training β€” SQL Database Engineer Agent",
278
- fontsize=13, fontweight="bold")
279
 
280
- ax.plot(steps, losses, "b-o", lw=2, ms=4)
281
- ax.set_xlabel("Training Step")
282
- ax.set_ylabel("Loss")
283
- ax.set_title("Training Loss (↓ = model learning DBA pattern)")
284
- ax.grid(True, alpha=0.3)
285
 
286
- if losses:
287
- ax.annotate(f"Start: {losses[0]:.4f}",
288
- xy=(steps[0], losses[0]),
289
- xytext=(steps[0]+1, losses[0]*1.1),
290
- fontsize=9, color="red")
291
- ax.annotate(f"End: {losses[-1]:.4f}",
292
- xy=(steps[-1], losses[-1]),
293
- xytext=(steps[-1]-5, losses[-1]*1.1),
294
- fontsize=9, color="green")
295
 
296
- plt.tight_layout()
297
- plt.savefig("loss_curve.png", dpi=150, bbox_inches="tight")
298
- print("βœ… loss_curve.png saved")
299
- print(f" Loss: {losses[0]:.4f} β†’ {losses[-1]:.4f}")
300
 
 
 
 
301
 
302
- # ── Main ──────────────────────────────────────────────────────
303
  def train():
304
- if not ALL_SCENARIOS:
305
- print("❌ No scenarios found. Check dataset/ folder.")
306
- sys.exit(1)
 
 
 
 
307
 
308
- print(f"⏳ Loading {MODEL_NAME}...")
309
  model, tokenizer = FastLanguageModel.from_pretrained(
310
  model_name = MODEL_NAME,
311
- max_seq_length = 2048,
312
  load_in_4bit = True,
313
  token = HF_TOKEN or None,
314
  )
 
 
315
  model = FastLanguageModel.get_peft_model(
316
  model,
317
- r=16, lora_alpha=16,
318
- target_modules=["q_proj","k_proj","v_proj","o_proj",
319
- "gate_proj","up_proj","down_proj"],
320
- lora_dropout=0, bias="none",
321
- use_gradient_checkpointing="unsloth",
322
- random_state=42,
 
323
  )
324
- print("βœ… Model ready\n")
325
 
 
326
  dataset = build_dataset()
327
 
 
328
  config = GRPOConfig(
329
  output_dir = OUTPUT_DIR,
330
- max_steps = MAX_STEPS,
331
- per_device_train_batch_size = 1,
332
- gradient_accumulation_steps = 4,
333
- learning_rate = 5e-6,
334
- max_completion_length = 150,
335
- num_generations = 4, # compare 4 actions per step
336
- temperature = 1.0,
337
- logging_steps = 1,
338
- save_steps = 25,
339
- save_total_limit = 3,
340
  warmup_ratio = 0.1,
341
  report_to = "none",
342
- remove_unused_columns = False,
343
  )
344
 
 
 
 
 
 
 
345
  trainer = GRPOTrainer(
346
- model = model,
347
- tokenizer = tokenizer,
348
- reward_funcs = reward_fn,
349
- args = config,
350
  train_dataset = dataset,
351
  )
352
 
353
- print(f"πŸ‹οΈ GRPO training β€” {MAX_STEPS} steps")
354
- print("Watch for: improvement > 0 and score > 0.5 on create_index\n")
355
  trainer.train()
356
- print("\nβœ… Training complete!")
357
 
358
- Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
359
  model.save_pretrained(f"{OUTPUT_DIR}/final")
360
  tokenizer.save_pretrained(f"{OUTPUT_DIR}/final")
361
- print(f"βœ… Saved to {OUTPUT_DIR}/final")
362
-
363
- generate_plots(trainer)
364
-
365
- print("\n" + "="*50)
366
- print("NEXT: python training/evaluate_agent.py")
367
- print("THEN: git add loss_curve.png reward_curve.png")
368
- print("="*50)
369
 
370
 
371
  if __name__ == "__main__":
372
- train()
 
1
  """
2
  training/train_agent.py β€” SQL Database Engineer Agent
3
+ Unsloth + GRPO training script.
4
+ Run on venue GPU (April 25-26) with compute credits.
5
+ ENV_URL points to live HF Space environment.
 
6
  """
7
 
8
+ import os
9
+ import re
10
+ import json
11
+ import requests
12
+ from datasets import Dataset
13
 
14
+ # ── Try importing Unsloth (GPU only) ─────────────────────────
 
15
  try:
 
 
 
 
16
  from unsloth import FastLanguageModel
17
  from trl import GRPOTrainer, GRPOConfig
 
18
  UNSLOTH_AVAILABLE = True
19
+ except ImportError:
20
+ UNSLOTH_AVAILABLE = False
21
+ print("⚠️ Unsloth not available. Run: pip install unsloth trl")
 
 
22
 
23
+ # ─────────────────────────────────────────────
24
+ # CONFIG
25
+ # ─────────────────────────────────────────────
26
 
 
27
  ENV_URL = os.getenv("ENV_URL", "https://junaid0600-sql-db-engineer-agent.hf.space")
28
  HF_TOKEN = os.getenv("HF_TOKEN", "")
29
+ MODEL_NAME = os.getenv("MODEL_NAME", "unsloth/Qwen2.5-7B-Instruct")
30
  OUTPUT_DIR = os.getenv("OUTPUT_DIR", "./sdea-trained")
 
31
 
32
+ SYSTEM_PROMPT = """You are a senior database engineer.
33
+ Given the current database state with slow queries, choose the BEST action to improve performance.
34
+ Think step by step:
35
+ 1. If you haven't inspected queries yet β†’ use inspect_query
36
+ 2. If you haven't analyzed indexes β†’ use analyze_indexes
37
+ 3. If you know which index is missing β†’ use create_index
38
+ 4. If query can be rewritten better β†’ use rewrite_query
39
+ 5. If table is huge (1M+ rows) β†’ use partition_table
40
+ 6. When performance target is reached β†’ use submit_report
41
 
42
+ Respond with JSON only β€” no explanation, no markdown:
43
+ {"action_type": "...", "payload": {...}}"""
 
44
 
 
 
45
 
46
+ # ─────────────────────────────────────────────
47
+ # REWARD FUNCTION (calls live HF Space)
48
+ # ─────────────────────────────────────────────
49
 
50
+ def parse_action(text: str) -> dict:
51
+ """Safely parse model output into an action dict.
52
 
53
+ Supports markdown ```json blocks and returns a fallback action on failure.
54
+ """
55
+ if not isinstance(text, str):
56
+ text = str(text)
 
 
 
 
 
 
 
 
 
 
 
57
 
58
+ candidate = text.strip()
59
+
60
+ fenced = re.search(r"```(?:json)?\s*(\{.*\})\s*```", candidate, re.S | re.I)
61
+ if fenced:
62
+ candidate = fenced.group(1).strip()
63
+ elif candidate.startswith("```") and candidate.endswith("```"):
64
+ candidate = candidate[3:-3].strip()
65
 
 
 
 
66
  try:
67
+ action = json.loads(candidate)
68
+ if isinstance(action, dict):
69
+ action["_parse_fallback"] = False
70
+ return action
71
+ except json.JSONDecodeError:
 
 
 
 
 
72
  pass
 
73
 
74
+ start = candidate.find("{")
75
+ end = candidate.rfind("}")
76
+ if start != -1 and end != -1 and end > start:
77
+ try:
78
+ action = json.loads(candidate[start:end + 1])
79
+ if isinstance(action, dict):
80
+ action["_parse_fallback"] = False
81
+ return action
82
+ except json.JSONDecodeError:
83
+ pass
84
+
85
+ return {
86
+ "action_type": "inspect_query",
87
+ "payload": {"query_id": "q1"},
88
+ "_parse_fallback": True,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  }
 
90
 
 
 
91
 
92
+ def compute_reward(action_dict: dict, obs: dict, env_response: dict) -> float:
93
+ """Apply shaping rules on top of environment reward.
 
 
 
 
 
 
94
 
95
+ This strengthens the signal, penalizes repeated inspection, empty payloads,
96
+ and bounds final reward between 0.0 and 1.0.
97
+ """
98
+ base_score = env_response.get("score", 0.0)
99
+ try:
100
+ base_score = float(base_score)
101
+ except (TypeError, ValueError):
102
+ base_score = 0.0
103
+
104
+ if action_dict.get("_parse_fallback", False):
105
+ return 0.0
106
+
107
+ action_type = action_dict.get("action_type", "")
108
+ payload = action_dict.get("payload") or {}
109
+
110
+ if action_type == "inspect_query":
111
+ query_id = payload.get("query_id")
112
+ inspected_queries = []
113
+ if isinstance(obs, dict):
114
+ inspected_queries = obs.get("inspected_queries") or obs.get("inspected_query_ids") or []
115
+
116
+ if isinstance(inspected_queries, list) and query_id in inspected_queries:
117
+ return max(0.0, base_score - 0.2)
118
+
119
+ if not payload or (isinstance(payload, dict) and len(payload) == 0):
120
+ return max(0.0, base_score - 0.3)
121
+
122
+ valid_actions = {
123
+ "inspect_query",
124
+ "analyze_indexes",
125
+ "create_index",
126
+ "fix_query",
127
+ "optimize_query",
128
+ }
129
+ if action_type not in valid_actions:
130
+ return 0.0
131
 
132
+ if base_score > 0.9:
133
+ return min(1.0, base_score + 0.1)
134
 
135
+ return base_score
136
 
137
 
 
138
  def reward_fn(prompts, completions, **kwargs):
139
  """
140
+ GRPO reward function β€” calls /step on live environment.
141
+ Returns list of float rewards, one per completion.
 
 
 
 
 
 
 
142
  """
143
  rewards = []
144
+ task_ids = kwargs.get("task_ids", ["easy_s001"] * len(prompts))
145
 
146
  for i, (prompt, completion) in enumerate(zip(prompts, completions)):
147
  try:
148
+ text = completion[0]["content"] if isinstance(completion, list) else str(completion)
 
 
 
 
 
 
 
 
 
149
  action = parse_action(text)
150
 
151
+ task_id = task_ids[i] if i < len(task_ids) else "easy_s001"
152
+ requests.post(
153
+ f"{ENV_URL}/reset",
154
+ json={"task_id": task_id},
155
+ timeout=15,
156
+ )
157
+
158
+ resp = requests.post(
159
+ f"{ENV_URL}/step",
160
+ json=action,
161
+ timeout=15,
162
+ )
163
+ env_response = resp.json()
164
+ score = compute_reward(
165
+ action_dict=action,
166
+ obs=env_response.get("observation", {}),
167
+ env_response=env_response,
168
+ )
169
+ score = max(0.0, min(1.0, float(score)))
170
  rewards.append(score)
171
 
 
 
 
 
 
 
172
  except Exception as e:
173
+ print(f"Reward fn error: {e}")
174
+ rewards.append(0.0)
175
 
176
  return rewards
177
 
178
 
179
+ # ─────────────────────────────────────────────
180
+ # BUILD TRAINING DATASET
181
+ # ─────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
+ def build_dataset():
184
+ """Build training examples from all 15 Round 2 scenarios."""
185
+ scenarios = []
186
 
187
+ # Load all scenario files
188
+ for fname in ["dataset/easy_scenarios.json",
189
+ "dataset/medium_scenarios.json",
190
+ "dataset/hard_scenarios.json"]:
191
+ try:
192
+ with open(fname) as f:
193
+ scenarios.extend(json.load(f))
194
+ except FileNotFoundError:
195
+ print(f"{fname} not found, skipping")
196
 
197
+ if not scenarios:
198
+ # Fallback: fetch from live environment
199
+ resp = requests.get(f"{ENV_URL}/tasks", timeout=15)
200
+ tasks = resp.json().get("tasks", [])
201
+ scenarios = [{"id": t["id"], "description": t["description"]} for t in tasks]
202
 
203
+ examples = []
204
+ for s in scenarios:
205
+ prompt = f"""{SYSTEM_PROMPT}
 
206
 
207
+ Current Database State:
208
+ - Scenario: {s.get('id', 'unknown')}
209
+ - Description: {s.get('description', '')}
210
+ - Tables: {json.dumps(s.get('tables', []))}
211
+ - Slow Queries: {json.dumps(s.get('slow_queries', []))}
212
+ - Performance Score: {s.get('performance_score_baseline', 0)} / 100
213
+ - Target Score: {s.get('target_score', 85)}
214
 
215
+ What is your next action?"""
 
 
216
 
217
+ examples.append({
218
+ "prompt": prompt,
219
+ "task_id": s.get("id", "easy_s001"),
220
+ })
 
221
 
222
+ print(f"Built {len(examples)} training examples")
223
+ return Dataset.from_list(examples)
 
 
 
 
 
 
 
224
 
 
 
 
 
225
 
226
+ # ─────────────────────────────────────────────
227
+ # MAIN TRAINING
228
+ # ─────────────────────────────────────────────
229
 
 
230
  def train():
231
+ if not UNSLOTH_AVAILABLE:
232
+ print("Cannot train β€” Unsloth not installed")
233
+ print("Run: pip install unsloth trl transformers datasets accelerate")
234
+ return
235
+
236
+ print(f"πŸš€ Loading model: {MODEL_NAME}")
237
+ print(f"🌐 Environment: {ENV_URL}")
238
 
239
+ # Load model with Unsloth 4-bit quantization
240
  model, tokenizer = FastLanguageModel.from_pretrained(
241
  model_name = MODEL_NAME,
242
+ max_seq_length = 4096,
243
  load_in_4bit = True,
244
  token = HF_TOKEN or None,
245
  )
246
+
247
+ # Add LoRA adapters
248
  model = FastLanguageModel.get_peft_model(
249
  model,
250
+ r = 16,
251
+ lora_alpha = 16,
252
+ target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
253
+ "gate_proj", "up_proj", "down_proj"],
254
+ lora_dropout = 0,
255
+ bias = "none",
256
+ use_gradient_checkpointing = "unsloth",
257
  )
 
258
 
259
+ # Build dataset
260
  dataset = build_dataset()
261
 
262
+ # GRPO config
263
  config = GRPOConfig(
264
  output_dir = OUTPUT_DIR,
265
+ num_train_epochs = 3,
266
+ per_device_train_batch_size = 2,
267
+ gradient_accumulation_steps = 8,
268
+ learning_rate = 5e-5,
269
+ max_completion_length = 256,
270
+ num_generations = 4,
271
+ logging_steps = 10,
272
+ save_steps = 50,
 
 
273
  warmup_ratio = 0.1,
274
  report_to = "none",
 
275
  )
276
 
277
+ # Reward function wrapper
278
+ def reward_wrapper(prompts, completions, **kwargs):
279
+ task_ids = [ex.get("task_id", "easy_s001") for ex in kwargs.get("batch", [])]
280
+ return reward_fn(prompts, completions, task_ids=task_ids)
281
+
282
+ # Train
283
  trainer = GRPOTrainer(
284
+ model = model,
285
+ tokenizer = tokenizer,
286
+ reward_funcs = reward_wrapper,
287
+ args = config,
288
  train_dataset = dataset,
289
  )
290
 
291
+ print("πŸ‹οΈ Starting GRPO training...")
 
292
  trainer.train()
 
293
 
294
+ # Save
295
  model.save_pretrained(f"{OUTPUT_DIR}/final")
296
  tokenizer.save_pretrained(f"{OUTPUT_DIR}/final")
297
+ print(f"Training complete. Model saved to {OUTPUT_DIR}/final")
 
 
 
 
 
 
 
298
 
299
 
300
  if __name__ == "__main__":
301
+ train()