vanishingradient commited on
Commit
a54bfe0
·
verified ·
1 Parent(s): 8b492f7

Add kaggle/train_grpo.py

Browse files
Files changed (1) hide show
  1. kaggle/train_grpo.py +603 -0
kaggle/train_grpo.py ADDED
@@ -0,0 +1,603 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Kaggle GRPO trainer for deepagents-rl coding.
3
+
4
+ Hard requirements:
5
+ - W&B metrics every step
6
+ - BEFORE every checkpoint: held-out eval -> Langfuse scores/trace -> THEN save/push HF
7
+ - HF adapter revisions tagged grpo-step-{n}
8
+
9
+ Secrets (Kaggle User Secrets OR env):
10
+ HF_TOKEN, WANDB_API_KEY, LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY
11
+ optional LANGFUSE_HOST / LANGFUSE_BASE_URL
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import os
17
+ import re
18
+ import subprocess
19
+ import sys
20
+ import traceback
21
+ from dataclasses import dataclass
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Inline reward helpers (also shipped in package; duplicated for notebook isolation)
27
+ # ---------------------------------------------------------------------------
28
+
29
+
30
+ def extract_python_code(completion: str) -> str:
31
+ if "```python" in completion:
32
+ start = completion.find("```python") + len("```python")
33
+ end = completion.find("```", start)
34
+ return completion[start:end if end != -1 else None].strip()
35
+ if "```" in completion:
36
+ start = completion.find("```") + 3
37
+ end = completion.find("```", start)
38
+ if end != -1:
39
+ return completion[start:end].strip()
40
+ return completion.strip()
41
+
42
+
43
+ def run_hidden_tests(solution_code: str, hidden_tests: list[str], timeout_s: float = 5.0) -> dict[str, Any]:
44
+ per_test = []
45
+ passed = 0
46
+ for i, assertion in enumerate(hidden_tests):
47
+ script = f"{solution_code}\n{assertion}\n"
48
+ try:
49
+ proc = subprocess.run(
50
+ [sys.executable, "-c", script],
51
+ capture_output=True,
52
+ text=True,
53
+ timeout=timeout_s,
54
+ )
55
+ ok = proc.returncode == 0
56
+ err = (proc.stderr or "")[:300]
57
+ except Exception as e: # noqa: BLE001
58
+ ok = False
59
+ err = f"{type(e).__name__}: {e}"
60
+ per_test.append({"i": i, "ok": ok, "stderr": err})
61
+ if ok:
62
+ passed += 1
63
+ total = len(hidden_tests)
64
+ return {
65
+ "pass_frac": (passed / total) if total else 0.0,
66
+ "passed": passed,
67
+ "total": total,
68
+ "per_test": per_test,
69
+ }
70
+
71
+
72
+ def grade_completion(completion: str, hidden_tests: list[str]) -> dict[str, Any]:
73
+ code = extract_python_code(completion)
74
+ g = run_hidden_tests(code, hidden_tests)
75
+ g["extracted_code"] = code
76
+ return g
77
+
78
+
79
+ def coding_reward(completions: list[str], hidden_tests: list[Any], **kwargs: Any) -> list[float]:
80
+ """TRL GRPO reward: fraction of hidden unit tests passed."""
81
+ rewards = []
82
+ for completion, tests in zip(completions, hidden_tests):
83
+ if isinstance(tests, str):
84
+ tests = json.loads(tests)
85
+ g = grade_completion(completion, list(tests))
86
+ rewards.append(float(g["pass_frac"]))
87
+ return rewards
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Secrets
92
+ # ---------------------------------------------------------------------------
93
+
94
+
95
+ def load_secrets() -> None:
96
+ try:
97
+ from kaggle_secrets import UserSecretsClient
98
+
99
+ us = UserSecretsClient()
100
+ for key in (
101
+ "HF_TOKEN",
102
+ "WANDB_API_KEY",
103
+ "LANGFUSE_PUBLIC_KEY",
104
+ "LANGFUSE_SECRET_KEY",
105
+ "LANGFUSE_HOST",
106
+ "LANGFUSE_BASE_URL",
107
+ ):
108
+ try:
109
+ val = us.get_secret(key)
110
+ if val:
111
+ os.environ[key] = val
112
+ except Exception:
113
+ pass
114
+ except Exception:
115
+ pass
116
+
117
+ missing = [
118
+ k
119
+ for k in (
120
+ "HF_TOKEN",
121
+ "WANDB_API_KEY",
122
+ "LANGFUSE_PUBLIC_KEY",
123
+ "LANGFUSE_SECRET_KEY",
124
+ )
125
+ if not os.getenv(k)
126
+ ]
127
+ if missing:
128
+ raise RuntimeError(
129
+ f"Missing required secrets/env: {missing}. "
130
+ "Add them in Kaggle Notebook Secrets (Add-ons -> Secrets)."
131
+ )
132
+ os.environ.setdefault("LANGFUSE_HOST", "https://cloud.langfuse.com")
133
+ os.environ.setdefault("LANGFUSE_BASE_URL", os.environ["LANGFUSE_HOST"])
134
+ os.environ.setdefault("WANDB_PROJECT", "deepagents-rl-coding")
135
+ os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "0")
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # Model resolve
140
+ # ---------------------------------------------------------------------------
141
+
142
+ CANDIDATES = [
143
+ "Qwen/Qwen2.5-Coder-1.5B-Instruct",
144
+ "Qwen/Qwen2.5-Coder-0.5B-Instruct",
145
+ "Qwen/Qwen2.5-1.5B-Instruct",
146
+ "HuggingFaceTB/SmolLM2-1.7B-Instruct",
147
+ "google/gemma-2-2b-it",
148
+ ]
149
+
150
+
151
+ def resolve_model() -> str:
152
+ from huggingface_hub import HfApi
153
+
154
+ override = os.getenv("TRAIN_MODEL_ID")
155
+ api = HfApi(token=os.environ["HF_TOKEN"])
156
+ tried = []
157
+ for repo in ([override] if override else []) + CANDIDATES:
158
+ if not repo or repo in tried:
159
+ continue
160
+ tried.append(repo)
161
+ try:
162
+ api.model_info(repo)
163
+ print(f"[resolve] using {repo}")
164
+ Path("/kaggle/working/resolved_model.json").write_text(
165
+ json.dumps({"train_model_id": repo, "tried": tried}, indent=2),
166
+ encoding="utf-8",
167
+ )
168
+ return repo
169
+ except Exception as e:
170
+ print(f"[resolve] skip {repo}: {e}")
171
+ raise RuntimeError(f"No model resolved; tried {tried}")
172
+
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Langfuse checkpoint eval
176
+ # ---------------------------------------------------------------------------
177
+
178
+
179
+ def langfuse_log_ckpt_eval(
180
+ *,
181
+ step: int,
182
+ hf_revision: str,
183
+ train_model_id: str,
184
+ results: list[dict[str, Any]],
185
+ wandb_run_id: str | None,
186
+ ) -> dict[str, Any]:
187
+ from langfuse import Langfuse, get_client
188
+
189
+ Langfuse(
190
+ public_key=os.environ["LANGFUSE_PUBLIC_KEY"],
191
+ secret_key=os.environ["LANGFUSE_SECRET_KEY"],
192
+ host=os.getenv("LANGFUSE_HOST") or os.getenv("LANGFUSE_BASE_URL"),
193
+ )
194
+ lf = get_client()
195
+ if not lf.auth_check():
196
+ raise RuntimeError("Langfuse auth failed during ckpt eval")
197
+
198
+ n = len(results)
199
+ mean_pass = sum(r["pass_frac"] for r in results) / n if n else 0.0
200
+ mean_reward = sum(r["reward_total"] for r in results) / n if n else 0.0
201
+ n_perfect = sum(1 for r in results if r["pass_frac"] >= 1.0)
202
+
203
+ with lf.start_as_current_observation(
204
+ name="kaggle-ckpt-eval",
205
+ as_type="evaluator",
206
+ metadata={
207
+ "step": step,
208
+ "hf_revision": hf_revision,
209
+ "train_model_id": train_model_id,
210
+ "wandb_run_id": wandb_run_id,
211
+ "phase": "ckpt_eval",
212
+ },
213
+ input={"step": step, "hf_revision": hf_revision, "n": n},
214
+ ) as root:
215
+ for r in results:
216
+ with lf.start_as_current_observation(
217
+ name=f"task:{r['task_id']}",
218
+ as_type="span",
219
+ input={"prompt": r["prompt"][:2000]},
220
+ ) as span:
221
+ span.update(
222
+ output={
223
+ "completion": r["completion"][:2000],
224
+ "pass_frac": r["pass_frac"],
225
+ "reward_total": r["reward_total"],
226
+ }
227
+ )
228
+ summary = {
229
+ "mean_pass_frac": mean_pass,
230
+ "mean_reward_total": mean_reward,
231
+ "n_perfect": n_perfect,
232
+ "n_problems": n,
233
+ "step": step,
234
+ "hf_revision": hf_revision,
235
+ }
236
+ root.update(output=summary)
237
+ lf.score_current_trace(name="mean_pass_frac", value=mean_pass)
238
+ lf.score_current_trace(name="mean_reward_total", value=mean_reward)
239
+ lf.score_current_trace(name="n_perfect", value=float(n_perfect))
240
+ lf.score_current_trace(name="checkpoint_step", value=float(step))
241
+ trace_id = lf.get_current_trace_id() or ""
242
+ lf.flush()
243
+ print(f"[langfuse] ckpt-eval step={step} rev={hf_revision} mean_pass={mean_pass:.3f} trace={trace_id}")
244
+ return {"langfuse_trace_id": trace_id, **summary}
245
+
246
+
247
+ def run_heldout_eval(model, tokenizer, problems: list[dict], max_new_tokens: int = 512) -> list[dict]:
248
+ import torch
249
+
250
+ rows = []
251
+ model.eval()
252
+ for p in problems:
253
+ messages = [{"role": "user", "content": p["prompt"]}]
254
+ try:
255
+ prompt_text = tokenizer.apply_chat_template(
256
+ messages, tokenize=False, add_generation_prompt=True
257
+ )
258
+ except Exception:
259
+ prompt_text = p["prompt"]
260
+ inputs = tokenizer(prompt_text, return_tensors="pt").to(model.device)
261
+ with torch.no_grad():
262
+ out = model.generate(
263
+ **inputs,
264
+ max_new_tokens=max_new_tokens,
265
+ do_sample=False,
266
+ pad_token_id=tokenizer.eos_token_id,
267
+ )
268
+ gen = out[0][inputs["input_ids"].shape[-1] :]
269
+ completion = tokenizer.decode(gen, skip_special_tokens=True)
270
+ g = grade_completion(completion, list(p["hidden_tests"]))
271
+ rows.append(
272
+ {
273
+ "task_id": p["task_id"],
274
+ "prompt": p["prompt"],
275
+ "completion": completion,
276
+ "pass_frac": float(g["pass_frac"]),
277
+ "reward_total": float(g["pass_frac"]),
278
+ }
279
+ )
280
+ return rows
281
+
282
+
283
+ # ---------------------------------------------------------------------------
284
+ # Callback: eval + langfuse THEN save
285
+ # ---------------------------------------------------------------------------
286
+
287
+
288
+ def make_eval_before_save_callback(
289
+ *,
290
+ eval_problems: list[dict],
291
+ train_model_id: str,
292
+ hub_model_id: str,
293
+ save_steps: int,
294
+ n_eval: int,
295
+ ):
296
+ from transformers import TrainerCallback, TrainerControl, TrainerState, TrainingArguments
297
+
298
+ class EvalBeforeCheckpointCallback(TrainerCallback):
299
+ def __init__(self) -> None:
300
+ self._last_eval_step = -1
301
+
302
+ def on_save(
303
+ self,
304
+ args: TrainingArguments,
305
+ state: TrainerState,
306
+ control: TrainerControl,
307
+ **kwargs,
308
+ ):
309
+ # Transformers calls on_save AFTER saving locally. We also hook on_step_end
310
+ # to eval+langfuse BEFORE hub push. See on_step_end.
311
+ return control
312
+
313
+ def on_step_end(
314
+ self,
315
+ args: TrainingArguments,
316
+ state: TrainerState,
317
+ control: TrainerControl,
318
+ **kwargs,
319
+ ):
320
+ step = state.global_step
321
+ if save_steps <= 0 or step <= 0 or step % save_steps != 0:
322
+ return control
323
+ if step == self._last_eval_step:
324
+ return control
325
+ self._last_eval_step = step
326
+
327
+ model = kwargs.get("model")
328
+ tokenizer = kwargs.get("processing_class") or kwargs.get("tokenizer")
329
+ # GRPOTrainer may put tokenizer on trainer; fallback via kwargs["train"] not available
330
+ trainer = kwargs.get("trainer") # usually absent
331
+ if tokenizer is None:
332
+ # recover from module global set in main()
333
+ tokenizer = globals().get("_TOKENIZER")
334
+ if model is None or tokenizer is None:
335
+ print("[ckpt-eval] missing model/tokenizer; skipping")
336
+ return control
337
+
338
+ rev = f"grpo-step-{step}"
339
+ problems = eval_problems[:n_eval]
340
+ print(f"\n=== EVAL BEFORE CHECKPOINT step={step} rev={rev} n={len(problems)} ===")
341
+ try:
342
+ results = run_heldout_eval(model, tokenizer, problems)
343
+ import wandb
344
+
345
+ wandb_run_id = None
346
+ if wandb.run is not None:
347
+ wandb_run_id = wandb.run.id
348
+ mean_pass = sum(r["pass_frac"] for r in results) / max(len(results), 1)
349
+ wandb.log(
350
+ {
351
+ "eval/mean_pass_frac": mean_pass,
352
+ "eval/n_perfect": sum(1 for r in results if r["pass_frac"] >= 1.0),
353
+ "eval/checkpoint_step": step,
354
+ },
355
+ step=step,
356
+ )
357
+
358
+ summary = langfuse_log_ckpt_eval(
359
+ step=step,
360
+ hf_revision=rev,
361
+ train_model_id=train_model_id,
362
+ results=results,
363
+ wandb_run_id=wandb_run_id,
364
+ )
365
+ # Persist summary next to checkpoint
366
+ out = Path(args.output_dir) / f"ckpt_eval_step_{step}.json"
367
+ out.write_text(json.dumps(summary | {"results": results}, indent=2), encoding="utf-8")
368
+
369
+ # Push adapter AFTER langfuse success
370
+ try:
371
+ model.push_to_hub(
372
+ hub_model_id,
373
+ token=os.environ["HF_TOKEN"],
374
+ revision=rev,
375
+ commit_message=f"GRPO adapter {rev}; mean_pass={summary['mean_pass_frac']:.3f}",
376
+ private=False,
377
+ )
378
+ print(f"[hf] pushed {hub_model_id}@{rev}")
379
+ if wandb.run is not None:
380
+ wandb.log(
381
+ {
382
+ "hf/revision": rev,
383
+ "hf/mean_pass_at_push": summary["mean_pass_frac"],
384
+ "langfuse/ckpt_trace_id": summary.get("langfuse_trace_id"),
385
+ },
386
+ step=step,
387
+ )
388
+ except Exception as e:
389
+ print(f"[hf] push failed (local ckpt still ok): {e}")
390
+ traceback.print_exc()
391
+ except Exception:
392
+ print("[ckpt-eval] FAILED — blocking is policy; re-raising so bad ckpts aren't silent")
393
+ traceback.print_exc()
394
+ raise
395
+ return control
396
+
397
+ return EvalBeforeCheckpointCallback()
398
+
399
+
400
+ # ---------------------------------------------------------------------------
401
+ # Main
402
+ # ---------------------------------------------------------------------------
403
+
404
+
405
+ @dataclass
406
+ class Cfg:
407
+ dataset_id: str = "vanishingradient/deepagents-rl-mbpp"
408
+ hub_model_id: str = "vanishingradient/deepagents-rl-coding"
409
+ max_steps: int = 40
410
+ save_steps: int = 5
411
+ num_generations: int = 4
412
+ learning_rate: float = 1e-6
413
+ max_completion_length: int = 512
414
+ max_seq_length: int = 2048
415
+ n_eval: int = 16
416
+ lora_r: int = 16
417
+
418
+
419
+ def main(cfg: Cfg | None = None) -> None:
420
+ global _TOKENIZER
421
+ cfg = cfg or Cfg()
422
+ load_secrets()
423
+
424
+ import torch
425
+ import wandb
426
+ from datasets import load_dataset
427
+ from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
428
+ from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
429
+ from trl import GRPOConfig, GRPOTrainer
430
+
431
+ os.environ["WANDB_PROJECT"] = os.getenv("WANDB_PROJECT", "deepagents-rl-coding")
432
+ wandb.login(key=os.environ["WANDB_API_KEY"])
433
+
434
+ model_id = resolve_model()
435
+ ds = load_dataset(cfg.dataset_id, token=os.environ["HF_TOKEN"])
436
+ train = ds["train"]
437
+ ev = ds["eval"]
438
+ # Ensure list columns serializable for reward
439
+ def _ensure(example):
440
+ ht = example["hidden_tests"]
441
+ if not isinstance(ht, list):
442
+ ht = list(ht)
443
+ example["hidden_tests"] = ht
444
+ return example
445
+
446
+ train = train.map(_ensure)
447
+ eval_problems = [
448
+ {
449
+ "task_id": row["task_id"],
450
+ "prompt": row["prompt"],
451
+ "hidden_tests": list(row["hidden_tests"]),
452
+ }
453
+ for row in ev
454
+ ]
455
+
456
+ bnb = BitsAndBytesConfig(
457
+ load_in_4bit=True,
458
+ bnb_4bit_compute_dtype=torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16,
459
+ bnb_4bit_quant_type="nf4",
460
+ bnb_4bit_use_double_quant=True,
461
+ )
462
+ tokenizer = AutoTokenizer.from_pretrained(model_id, token=os.environ["HF_TOKEN"], trust_remote_code=True)
463
+ if tokenizer.pad_token is None:
464
+ tokenizer.pad_token = tokenizer.eos_token
465
+ _TOKENIZER = tokenizer
466
+
467
+ model = AutoModelForCausalLM.from_pretrained(
468
+ model_id,
469
+ quantization_config=bnb,
470
+ device_map="auto",
471
+ token=os.environ["HF_TOKEN"],
472
+ trust_remote_code=True,
473
+ )
474
+ model = prepare_model_for_kbit_training(model)
475
+ lora = LoraConfig(
476
+ r=cfg.lora_r,
477
+ lora_alpha=cfg.lora_r * 2,
478
+ lora_dropout=0.0,
479
+ bias="none",
480
+ task_type="CAUSAL_LM",
481
+ target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
482
+ )
483
+ model = get_peft_model(model, lora)
484
+
485
+ output_dir = "/kaggle/working/grpo-out"
486
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
487
+ globals()["_TOKENIZER"] = tokenizer
488
+
489
+ wandb.init(
490
+ project=os.environ["WANDB_PROJECT"],
491
+ name="kaggle-grpo-mbpp",
492
+ job_type="train",
493
+ config={
494
+ "train_model_id": model_id,
495
+ "dataset_id": cfg.dataset_id,
496
+ "max_steps": cfg.max_steps,
497
+ "save_steps": cfg.save_steps,
498
+ "n_eval": cfg.n_eval,
499
+ "hub_model_id": cfg.hub_model_id,
500
+ "eval_before_every_checkpoint": True,
501
+ },
502
+ )
503
+
504
+ # Baseline eval @ step 0 BEFORE any training / checkpoint
505
+ print("=== BASELINE EVAL (step 0) before any training ===")
506
+ baseline = run_heldout_eval(model, tokenizer, eval_problems[: cfg.n_eval])
507
+ base_summary = langfuse_log_ckpt_eval(
508
+ step=0,
509
+ hf_revision="grpo-step-0-baseline",
510
+ train_model_id=model_id,
511
+ results=baseline,
512
+ wandb_run_id=wandb.run.id if wandb.run else None,
513
+ )
514
+ Path(output_dir, "ckpt_eval_step_0.json").write_text(
515
+ json.dumps(base_summary | {"results": baseline}, indent=2), encoding="utf-8"
516
+ )
517
+ wandb.log(
518
+ {
519
+ "eval/mean_pass_frac": base_summary["mean_pass_frac"],
520
+ "eval/n_perfect": base_summary["n_perfect"],
521
+ "eval/checkpoint_step": 0,
522
+ "langfuse/ckpt_trace_id": base_summary.get("langfuse_trace_id"),
523
+ },
524
+ step=0,
525
+ )
526
+
527
+ args = GRPOConfig(
528
+ output_dir=output_dir,
529
+ run_name="kaggle-grpo-mbpp",
530
+ learning_rate=cfg.learning_rate,
531
+ per_device_train_batch_size=1,
532
+ gradient_accumulation_steps=4,
533
+ num_generations=cfg.num_generations,
534
+ max_completion_length=cfg.max_completion_length,
535
+ max_steps=cfg.max_steps,
536
+ logging_steps=1,
537
+ save_steps=cfg.save_steps,
538
+ save_total_limit=3,
539
+ report_to=["wandb"],
540
+ bf16=torch.cuda.is_bf16_supported(),
541
+ fp16=not torch.cuda.is_bf16_supported(),
542
+ remove_unused_columns=False,
543
+ hub_model_id=cfg.hub_model_id,
544
+ hub_token=os.environ["HF_TOKEN"],
545
+ push_to_hub=False, # push ONLY after Langfuse ckpt-eval succeeds
546
+ )
547
+
548
+ cb = make_eval_before_save_callback(
549
+ eval_problems=eval_problems,
550
+ train_model_id=model_id,
551
+ hub_model_id=cfg.hub_model_id,
552
+ save_steps=cfg.save_steps,
553
+ n_eval=cfg.n_eval,
554
+ )
555
+
556
+ trainer = GRPOTrainer(
557
+ model=model,
558
+ reward_funcs=coding_reward,
559
+ args=args,
560
+ train_dataset=train,
561
+ processing_class=tokenizer,
562
+ callbacks=[cb],
563
+ )
564
+
565
+ trainer.train()
566
+ # Final eval + push
567
+ final_step = int(trainer.state.global_step)
568
+ rev = f"grpo-step-{final_step}-final"
569
+ print(f"=== FINAL EVAL step={final_step} ===")
570
+ final_results = run_heldout_eval(model, tokenizer, eval_problems[: cfg.n_eval])
571
+ final_summary = langfuse_log_ckpt_eval(
572
+ step=final_step,
573
+ hf_revision=rev,
574
+ train_model_id=model_id,
575
+ results=final_results,
576
+ wandb_run_id=wandb.run.id if wandb.run else None,
577
+ )
578
+ model.push_to_hub(
579
+ cfg.hub_model_id,
580
+ token=os.environ["HF_TOKEN"],
581
+ revision=rev,
582
+ commit_message=f"Final GRPO {rev}; mean_pass={final_summary['mean_pass_frac']:.3f}",
583
+ )
584
+ # also push main
585
+ model.push_to_hub(
586
+ cfg.hub_model_id,
587
+ token=os.environ["HF_TOKEN"],
588
+ commit_message=f"Final GRPO main; mean_pass={final_summary['mean_pass_frac']:.3f}",
589
+ )
590
+ wandb.log(
591
+ {
592
+ "eval/final_mean_pass_frac": final_summary["mean_pass_frac"],
593
+ "hf/final_revision": rev,
594
+ }
595
+ )
596
+ wandb.finish()
597
+ print("TRAIN DONE", final_summary)
598
+
599
+
600
+ _TOKENIZER = None
601
+
602
+ if __name__ == "__main__":
603
+ main()