vanishingradient commited on
Commit
aa44576
·
verified ·
1 Parent(s): a5b37a7

Add kaggle/train_grpo.py

Browse files
Files changed (1) hide show
  1. kaggle/train_grpo.py +365 -168
kaggle/train_grpo.py CHANGED
@@ -1,89 +1,58 @@
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 pathlib import Path
22
- from typing import Any
23
 
24
  # ---------------------------------------------------------------------------
25
- # Inline reward helpers (also shipped in package; duplicated for notebook isolation)
26
  # ---------------------------------------------------------------------------
27
 
28
 
29
- def extract_python_code(completion: str) -> str:
30
- if "```python" in completion:
31
- start = completion.find("```python") + len("```python")
32
- end = completion.find("```", start)
33
- return completion[start:end if end != -1 else None].strip()
34
- if "```" in completion:
35
- start = completion.find("```") + 3
36
- end = completion.find("```", start)
37
- if end != -1:
38
- return completion[start:end].strip()
39
- return completion.strip()
40
-
41
-
42
- def run_hidden_tests(solution_code: str, hidden_tests: list[str], timeout_s: float = 5.0) -> dict[str, Any]:
43
- per_test = []
44
- passed = 0
45
- for i, assertion in enumerate(hidden_tests):
46
- script = f"{solution_code}\n{assertion}\n"
47
- try:
48
- proc = subprocess.run(
49
- [sys.executable, "-c", script],
50
- capture_output=True,
51
- text=True,
52
- timeout=timeout_s,
53
- )
54
- ok = proc.returncode == 0
55
- err = (proc.stderr or "")[:300]
56
- except Exception as e: # noqa: BLE001
57
- ok = False
58
- err = f"{type(e).__name__}: {e}"
59
- per_test.append({"i": i, "ok": ok, "stderr": err})
60
- if ok:
61
- passed += 1
62
- total = len(hidden_tests)
63
- return {
64
- "pass_frac": (passed / total) if total else 0.0,
65
- "passed": passed,
66
- "total": total,
67
- "per_test": per_test,
68
- }
69
-
70
-
71
- def grade_completion(completion: str, hidden_tests: list[str]) -> dict[str, Any]:
72
- code = extract_python_code(completion)
73
- g = run_hidden_tests(code, hidden_tests)
74
- g["extracted_code"] = code
75
- return g
76
-
77
-
78
- def coding_reward(completions: list[str], hidden_tests: list[Any], **kwargs: Any) -> list[float]:
79
- """TRL GRPO reward: fraction of hidden unit tests passed."""
80
- rewards = []
81
- for completion, tests in zip(completions, hidden_tests):
82
- if isinstance(tests, str):
83
- tests = json.loads(tests)
84
- g = grade_completion(completion, list(tests))
85
- rewards.append(float(g["pass_frac"]))
86
- return rewards
87
 
88
 
89
  # ---------------------------------------------------------------------------
@@ -103,6 +72,9 @@ def load_secrets() -> None:
103
  "LANGFUSE_SECRET_KEY",
104
  "LANGFUSE_HOST",
105
  "LANGFUSE_BASE_URL",
 
 
 
106
  ):
107
  try:
108
  val = us.get_secret(key)
@@ -113,6 +85,22 @@ def load_secrets() -> None:
113
  except Exception:
114
  pass
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  missing = [
117
  k
118
  for k in (
@@ -120,18 +108,20 @@ def load_secrets() -> None:
120
  "WANDB_API_KEY",
121
  "LANGFUSE_PUBLIC_KEY",
122
  "LANGFUSE_SECRET_KEY",
 
123
  )
124
  if not os.getenv(k)
125
  ]
126
  if missing:
127
  raise RuntimeError(
128
  f"Missing required secrets/env: {missing}. "
129
- "Add them in Kaggle Notebook Secrets (Add-ons -> Secrets)."
130
  )
131
  os.environ.setdefault("LANGFUSE_HOST", "https://cloud.langfuse.com")
132
  os.environ.setdefault("LANGFUSE_BASE_URL", os.environ["LANGFUSE_HOST"])
133
  os.environ.setdefault("WANDB_PROJECT", "deepagents-rl-coding")
134
  os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "0")
 
135
 
136
 
137
  # ---------------------------------------------------------------------------
@@ -160,7 +150,9 @@ def resolve_model() -> str:
160
  try:
161
  api.model_info(repo)
162
  print(f"[resolve] using {repo}")
163
- Path("/kaggle/working/resolved_model.json").write_text(
 
 
164
  json.dumps({"train_model_id": repo, "tried": tried}, indent=2),
165
  encoding="utf-8",
166
  )
@@ -170,6 +162,162 @@ def resolve_model() -> str:
170
  raise RuntimeError(f"No model resolved; tried {tried}")
171
 
172
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  # ---------------------------------------------------------------------------
174
  # Langfuse checkpoint eval
175
  # ---------------------------------------------------------------------------
@@ -195,9 +343,10 @@ def langfuse_log_ckpt_eval(
195
  raise RuntimeError("Langfuse auth failed during ckpt eval")
196
 
197
  n = len(results)
198
- mean_pass = sum(r["pass_frac"] for r in results) / n if n else 0.0
199
- mean_reward = sum(r["reward_total"] for r in results) / n if n else 0.0
200
- n_perfect = sum(1 for r in results if r["pass_frac"] >= 1.0)
 
201
 
202
  with lf.start_as_current_observation(
203
  name="kaggle-ckpt-eval",
@@ -208,6 +357,7 @@ def langfuse_log_ckpt_eval(
208
  "train_model_id": train_model_id,
209
  "wandb_run_id": wandb_run_id,
210
  "phase": "ckpt_eval",
 
211
  },
212
  input={"step": step, "hf_revision": hf_revision, "n": n},
213
  ) as root:
@@ -215,19 +365,21 @@ def langfuse_log_ckpt_eval(
215
  with lf.start_as_current_observation(
216
  name=f"task:{r['task_id']}",
217
  as_type="span",
218
- input={"prompt": r["prompt"][:2000]},
219
  ) as span:
220
  span.update(
221
  output={
222
- "completion": r["completion"][:2000],
223
- "pass_frac": r["pass_frac"],
224
- "reward_total": r["reward_total"],
 
225
  }
226
  )
227
  summary = {
228
  "mean_pass_frac": mean_pass,
229
  "mean_reward_total": mean_reward,
230
  "n_perfect": n_perfect,
 
231
  "n_problems": n,
232
  "step": step,
233
  "hf_revision": hf_revision,
@@ -236,49 +388,17 @@ def langfuse_log_ckpt_eval(
236
  lf.score_current_trace(name="mean_pass_frac", value=mean_pass)
237
  lf.score_current_trace(name="mean_reward_total", value=mean_reward)
238
  lf.score_current_trace(name="n_perfect", value=float(n_perfect))
 
239
  lf.score_current_trace(name="checkpoint_step", value=float(step))
240
  trace_id = lf.get_current_trace_id() or ""
241
  lf.flush()
242
- print(f"[langfuse] ckpt-eval step={step} rev={hf_revision} mean_pass={mean_pass:.3f} trace={trace_id}")
 
 
 
243
  return {"langfuse_trace_id": trace_id, **summary}
244
 
245
 
246
- def run_heldout_eval(model, tokenizer, problems: list[dict], max_new_tokens: int = 512) -> list[dict]:
247
- import torch
248
-
249
- rows = []
250
- model.eval()
251
- for p in problems:
252
- messages = [{"role": "user", "content": p["prompt"]}]
253
- try:
254
- prompt_text = tokenizer.apply_chat_template(
255
- messages, tokenize=False, add_generation_prompt=True
256
- )
257
- except Exception:
258
- prompt_text = p["prompt"]
259
- inputs = tokenizer(prompt_text, return_tensors="pt").to(model.device)
260
- with torch.no_grad():
261
- out = model.generate(
262
- **inputs,
263
- max_new_tokens=max_new_tokens,
264
- do_sample=False,
265
- pad_token_id=tokenizer.eos_token_id,
266
- )
267
- gen = out[0][inputs["input_ids"].shape[-1] :]
268
- completion = tokenizer.decode(gen, skip_special_tokens=True)
269
- g = grade_completion(completion, list(p["hidden_tests"]))
270
- rows.append(
271
- {
272
- "task_id": p["task_id"],
273
- "prompt": p["prompt"],
274
- "completion": completion,
275
- "pass_frac": float(g["pass_frac"]),
276
- "reward_total": float(g["pass_frac"]),
277
- }
278
- )
279
- return rows
280
-
281
-
282
  # ---------------------------------------------------------------------------
283
  # Callback: eval + langfuse THEN save
284
  # ---------------------------------------------------------------------------
@@ -291,13 +411,14 @@ def make_eval_before_save_callback(
291
  hub_model_id: str,
292
  save_steps: int,
293
  n_eval: int,
 
294
  ):
295
  from transformers import TrainerCallback, TrainerControl, TrainerState, TrainingArguments
296
 
297
  class EvalBeforeCheckpointCallback(TrainerCallback):
298
  def __init__(self) -> None:
299
  self._last_eval_step = -1
300
- self.trainer = None # set after GRPOTrainer construction
301
 
302
  def on_step_end(
303
  self,
@@ -326,23 +447,27 @@ def make_eval_before_save_callback(
326
  self.trainer, "tokenizer", None
327
  )
328
  if model is None or tokenizer is None:
329
- print("[ckpt-eval] missing model/tokenizer; aborting save pipeline")
330
  raise RuntimeError("ckpt-eval requires model+tokenizer")
 
331
  rev = f"grpo-step-{step}"
332
  problems = eval_problems[:n_eval]
333
- print(f"\n=== EVAL BEFORE CHECKPOINT step={step} rev={rev} n={len(problems)} ===")
334
  try:
335
- results = run_heldout_eval(model, tokenizer, problems)
 
 
336
  import wandb
337
 
338
  wandb_run_id = None
339
  if wandb.run is not None:
340
  wandb_run_id = wandb.run.id
341
- mean_pass = sum(r["pass_frac"] for r in results) / max(len(results), 1)
342
  wandb.log(
343
  {
344
- "eval/mean_pass_frac": mean_pass,
345
- "eval/n_perfect": sum(1 for r in results if r["pass_frac"] >= 1.0),
 
 
346
  "eval/checkpoint_step": step,
347
  },
348
  step=step,
@@ -355,17 +480,19 @@ def make_eval_before_save_callback(
355
  results=results,
356
  wandb_run_id=wandb_run_id,
357
  )
358
- # Persist summary next to checkpoint
359
  out = Path(args.output_dir) / f"ckpt_eval_step_{step}.json"
360
- out.write_text(json.dumps(summary | {"results": results}, indent=2), encoding="utf-8")
 
 
361
 
362
- # Push adapter AFTER langfuse success
363
  try:
364
  model.push_to_hub(
365
  hub_model_id,
366
  token=os.environ["HF_TOKEN"],
367
  revision=rev,
368
- commit_message=f"GRPO adapter {rev}; mean_pass={summary['mean_pass_frac']:.3f}",
 
 
369
  private=False,
370
  )
371
  print(f"[hf] pushed {hub_model_id}@{rev}")
@@ -373,7 +500,7 @@ def make_eval_before_save_callback(
373
  wandb.log(
374
  {
375
  "hf/revision": rev,
376
- "hf/mean_pass_at_push": summary["mean_pass_frac"],
377
  "langfuse/ckpt_trace_id": summary.get("langfuse_trace_id"),
378
  },
379
  step=step,
@@ -382,7 +509,7 @@ def make_eval_before_save_callback(
382
  print(f"[hf] push failed (local ckpt still ok): {e}")
383
  traceback.print_exc()
384
  except Exception:
385
- print("[ckpt-eval] FAILED — blocking is policy; re-raising so bad ckpts aren't silent")
386
  traceback.print_exc()
387
  raise
388
  return control
@@ -400,65 +527,105 @@ class Cfg:
400
 
401
  def __init__(
402
  self,
403
- dataset_id: str = "vanishingradient/deepagents-rl-mbpp",
404
  hub_model_id: str = "vanishingradient/deepagents-rl-coding",
405
- max_steps: int = 40,
 
 
406
  save_steps: int = 5,
407
- num_generations: int = 4,
408
  learning_rate: float = 1e-6,
409
- max_completion_length: int = 512,
410
- max_seq_length: int = 2048,
411
- n_eval: int = 16,
 
 
 
412
  lora_r: int = 16,
413
  ) -> None:
414
  self.dataset_id = dataset_id
415
  self.hub_model_id = hub_model_id
 
 
416
  self.max_steps = max_steps
417
  self.save_steps = save_steps
418
  self.num_generations = num_generations
419
  self.learning_rate = learning_rate
420
  self.max_completion_length = max_completion_length
421
  self.max_seq_length = max_seq_length
 
422
  self.n_eval = n_eval
 
 
423
  self.lora_r = lora_r
424
 
425
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  def main(cfg: Cfg | None = None) -> None:
427
  global _TOKENIZER
428
  cfg = cfg or Cfg()
429
  load_secrets()
 
430
 
431
  import torch
432
  import wandb
433
- from datasets import load_dataset
434
  from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
435
  from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
436
  from trl import GRPOConfig, GRPOTrainer
437
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  os.environ["WANDB_PROJECT"] = os.getenv("WANDB_PROJECT", "deepagents-rl-coding")
439
  wandb.login(key=os.environ["WANDB_API_KEY"])
440
 
441
  model_id = resolve_model()
442
- ds = load_dataset(cfg.dataset_id, token=os.environ["HF_TOKEN"])
443
- train = ds["train"]
444
- ev = ds["eval"]
445
- # Ensure list columns serializable for reward
446
- def _ensure(example):
447
- ht = example["hidden_tests"]
448
- if not isinstance(ht, list):
449
- ht = list(ht)
450
- example["hidden_tests"] = ht
451
- return example
452
-
453
- train = train.map(_ensure)
454
- eval_problems = [
455
- {
456
- "task_id": row["task_id"],
457
- "prompt": row["prompt"],
458
- "hidden_tests": list(row["hidden_tests"]),
459
- }
460
- for row in ev
461
- ]
462
 
463
  bnb = BitsAndBytesConfig(
464
  load_in_4bit=True,
@@ -466,7 +633,9 @@ def main(cfg: Cfg | None = None) -> None:
466
  bnb_4bit_quant_type="nf4",
467
  bnb_4bit_use_double_quant=True,
468
  )
469
- tokenizer = AutoTokenizer.from_pretrained(model_id, token=os.environ["HF_TOKEN"], trust_remote_code=True)
 
 
470
  if tokenizer.pad_token is None:
471
  tokenizer.pad_token = tokenizer.eos_token
472
  _TOKENIZER = tokenizer
@@ -485,7 +654,15 @@ def main(cfg: Cfg | None = None) -> None:
485
  lora_dropout=0.0,
486
  bias="none",
487
  task_type="CAUSAL_LM",
488
- target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
 
 
 
 
 
 
 
 
489
  )
490
  model = get_peft_model(model, lora)
491
 
@@ -495,22 +672,30 @@ def main(cfg: Cfg | None = None) -> None:
495
 
496
  wandb.init(
497
  project=os.environ["WANDB_PROJECT"],
498
- name="kaggle-grpo-mbpp",
499
  job_type="train",
500
  config={
501
  "train_model_id": model_id,
502
  "dataset_id": cfg.dataset_id,
 
503
  "max_steps": cfg.max_steps,
504
  "save_steps": cfg.save_steps,
505
  "n_eval": cfg.n_eval,
 
 
506
  "hub_model_id": cfg.hub_model_id,
 
507
  "eval_before_every_checkpoint": True,
508
  },
509
  )
510
 
511
- # Baseline eval @ step 0 BEFORE any training / checkpoint
512
- print("=== BASELINE EVAL (step 0) before any training ===")
513
- baseline = run_heldout_eval(model, tokenizer, eval_problems[: cfg.n_eval])
 
 
 
 
514
  base_summary = langfuse_log_ckpt_eval(
515
  step=0,
516
  hf_revision="grpo-step-0-baseline",
@@ -524,7 +709,8 @@ def main(cfg: Cfg | None = None) -> None:
524
  wandb.log(
525
  {
526
  "eval/mean_pass_frac": base_summary["mean_pass_frac"],
527
- "eval/n_perfect": base_summary["n_perfect"],
 
528
  "eval/checkpoint_step": 0,
529
  "langfuse/ckpt_trace_id": base_summary.get("langfuse_trace_id"),
530
  },
@@ -533,12 +719,13 @@ def main(cfg: Cfg | None = None) -> None:
533
 
534
  args = GRPOConfig(
535
  output_dir=output_dir,
536
- run_name="kaggle-grpo-mbpp",
537
  learning_rate=cfg.learning_rate,
538
  per_device_train_batch_size=1,
539
  gradient_accumulation_steps=4,
540
  num_generations=cfg.num_generations,
541
  max_completion_length=cfg.max_completion_length,
 
542
  max_steps=cfg.max_steps,
543
  logging_steps=1,
544
  save_steps=cfg.save_steps,
@@ -549,7 +736,7 @@ def main(cfg: Cfg | None = None) -> None:
549
  remove_unused_columns=False,
550
  hub_model_id=cfg.hub_model_id,
551
  hub_token=os.environ["HF_TOKEN"],
552
- push_to_hub=False, # push ONLY after Langfuse ckpt-eval succeeds
553
  )
554
 
555
  cb = make_eval_before_save_callback(
@@ -558,23 +745,30 @@ def main(cfg: Cfg | None = None) -> None:
558
  hub_model_id=cfg.hub_model_id,
559
  save_steps=cfg.save_steps,
560
  n_eval=cfg.n_eval,
 
561
  )
562
 
563
  trainer = GRPOTrainer(
564
  model=model,
565
- reward_funcs=coding_reward,
566
  args=args,
567
  train_dataset=train,
568
  processing_class=tokenizer,
569
  callbacks=[cb],
 
570
  )
571
  cb.trainer = trainer
572
 
573
- trainer.train() # Final eval + push
 
574
  final_step = int(trainer.state.global_step)
575
  rev = f"grpo-step-{final_step}-final"
576
- print(f"=== FINAL EVAL step={final_step} ===")
577
- final_results = run_heldout_eval(model, tokenizer, eval_problems[: cfg.n_eval])
 
 
 
 
 
578
  final_summary = langfuse_log_ckpt_eval(
579
  step=final_step,
580
  hf_revision=rev,
@@ -586,17 +780,20 @@ def main(cfg: Cfg | None = None) -> None:
586
  cfg.hub_model_id,
587
  token=os.environ["HF_TOKEN"],
588
  revision=rev,
589
- commit_message=f"Final GRPO {rev}; mean_pass={final_summary['mean_pass_frac']:.3f}",
 
 
590
  )
591
- # also push main
592
  model.push_to_hub(
593
  cfg.hub_model_id,
594
  token=os.environ["HF_TOKEN"],
595
- commit_message=f"Final GRPO main; mean_pass={final_summary['mean_pass_frac']:.3f}",
 
 
596
  )
597
  wandb.log(
598
  {
599
- "eval/final_mean_pass_frac": final_summary["mean_pass_frac"],
600
  "hf/final_revision": rev,
601
  }
602
  )
 
1
  """
2
+ Kaggle agent GRPO: Qwen + deepagents SWE harness + remote Docker reward.
3
 
4
  Hard requirements:
5
  - W&B metrics every step
6
+ - BEFORE every checkpoint: held-out agent eval -> Langfuse -> THEN save/push HF
7
  - HF adapter revisions tagged grpo-step-{n}
8
+ - SWE_GRADER_URL must reach local scripts/swe_grader_server.py (tunnel)
9
 
10
+ Secrets (dataset / User Secrets / env):
11
  HF_TOKEN, WANDB_API_KEY, LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY
12
+ SWE_GRADER_URL, optional SWE_GRADER_TOKEN
13
  """
14
  from __future__ import annotations
15
 
16
+ import inspect
17
  import json
18
  import os
19
  import re
 
20
  import sys
21
  import traceback
22
  from pathlib import Path
23
+ from typing import Any, Callable
24
 
25
  # ---------------------------------------------------------------------------
26
+ # Bootstrap package (notebook may only download this file)
27
  # ---------------------------------------------------------------------------
28
 
29
 
30
+ def _ensure_package() -> None:
31
+ try:
32
+ import deepagents_rl # noqa: F401
33
+
34
+ return
35
+ except ImportError:
36
+ pass
37
+
38
+ token = os.environ.get("HF_TOKEN")
39
+ if not token:
40
+ raise RuntimeError("HF_TOKEN required to download deepagents_rl sources")
41
+
42
+ from huggingface_hub import snapshot_download
43
+
44
+ root = Path("/kaggle/working/deepagents_rl_src")
45
+ snapshot_download(
46
+ repo_id=os.getenv("HF_REPO_ID", "vanishingradient/deepagents-rl-coding"),
47
+ repo_type="model",
48
+ token=token,
49
+ allow_patterns=["src/deepagents_rl/**"],
50
+ local_dir=str(root),
51
+ )
52
+ pkg_parent = root / "src"
53
+ if str(pkg_parent) not in sys.path:
54
+ sys.path.insert(0, str(pkg_parent))
55
+ import deepagents_rl # noqa: F401
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
 
58
  # ---------------------------------------------------------------------------
 
72
  "LANGFUSE_SECRET_KEY",
73
  "LANGFUSE_HOST",
74
  "LANGFUSE_BASE_URL",
75
+ "SWE_GRADER_URL",
76
+ "SWE_GRADER_TOKEN",
77
+ "WANDB_PROJECT",
78
  ):
79
  try:
80
  val = us.get_secret(key)
 
85
  except Exception:
86
  pass
87
 
88
+ # Private dataset secrets.json (API-friendly path)
89
+ secret_candidates = [Path("/kaggle/input/deepagents-rl-secrets/secrets.json")]
90
+ input_root = Path("/kaggle/input")
91
+ if input_root.exists():
92
+ secret_candidates.extend(input_root.rglob("secrets.json"))
93
+ for p in secret_candidates:
94
+ if p.exists():
95
+ try:
96
+ data = json.loads(p.read_text(encoding="utf-8"))
97
+ for k, v in data.items():
98
+ if v and not os.getenv(k):
99
+ os.environ[k] = str(v)
100
+ except Exception:
101
+ pass
102
+ break
103
+
104
  missing = [
105
  k
106
  for k in (
 
108
  "WANDB_API_KEY",
109
  "LANGFUSE_PUBLIC_KEY",
110
  "LANGFUSE_SECRET_KEY",
111
+ "SWE_GRADER_URL",
112
  )
113
  if not os.getenv(k)
114
  ]
115
  if missing:
116
  raise RuntimeError(
117
  f"Missing required secrets/env: {missing}. "
118
+ "Need SWE_GRADER_URL pointing at the local Docker grader tunnel."
119
  )
120
  os.environ.setdefault("LANGFUSE_HOST", "https://cloud.langfuse.com")
121
  os.environ.setdefault("LANGFUSE_BASE_URL", os.environ["LANGFUSE_HOST"])
122
  os.environ.setdefault("WANDB_PROJECT", "deepagents-rl-coding")
123
  os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "0")
124
+ os.environ.setdefault("TRL_EXPERIMENTAL_SILENCE", "1")
125
 
126
 
127
  # ---------------------------------------------------------------------------
 
150
  try:
151
  api.model_info(repo)
152
  print(f"[resolve] using {repo}")
153
+ out = Path("/kaggle/working/resolved_model.json")
154
+ out.parent.mkdir(parents=True, exist_ok=True)
155
+ out.write_text(
156
  json.dumps({"train_model_id": repo, "tried": tried}, indent=2),
157
  encoding="utf-8",
158
  )
 
162
  raise RuntimeError(f"No model resolved; tried {tried}")
163
 
164
 
165
+ # ---------------------------------------------------------------------------
166
+ # Lightweight HF agent rollout (checkpoint eval)
167
+ # ---------------------------------------------------------------------------
168
+
169
+ _TOOL_CALL_RE = re.compile(
170
+ r"<tool_call>\s*(\{.*?\})\s*</tool_call>",
171
+ re.DOTALL,
172
+ )
173
+
174
+
175
+ def _env_tool_callables(env: Any) -> list[Callable[..., str]]:
176
+ return [
177
+ getattr(env, name)
178
+ for name, _ in inspect.getmembers(type(env), predicate=inspect.isfunction)
179
+ if name not in ("reset", "get_reward") and not name.startswith("_")
180
+ ]
181
+
182
+
183
+ def _parse_tool_calls(text: str) -> list[dict[str, Any]]:
184
+ calls: list[dict[str, Any]] = []
185
+ for m in _TOOL_CALL_RE.finditer(text or ""):
186
+ try:
187
+ obj = json.loads(m.group(1))
188
+ name = obj.get("name") or obj.get("function", {}).get("name")
189
+ args = obj.get("arguments") or obj.get("parameters") or obj.get("function", {}).get("arguments") or {}
190
+ if isinstance(args, str):
191
+ try:
192
+ args = json.loads(args)
193
+ except json.JSONDecodeError:
194
+ args = {"input": args}
195
+ if name:
196
+ calls.append({"name": str(name), "arguments": args if isinstance(args, dict) else {}})
197
+ except json.JSONDecodeError:
198
+ continue
199
+ return calls
200
+
201
+
202
+ def run_heldout_agent_eval(
203
+ model,
204
+ tokenizer,
205
+ problems: list[dict],
206
+ *,
207
+ max_turns: int = 8,
208
+ max_new_tokens: int = 512,
209
+ ) -> list[dict]:
210
+ """Greedy multi-turn tool loop + remote Docker reward (checkpoint metric)."""
211
+ import torch
212
+
213
+ from deepagents_rl.train.swe_env import DeepAgentSWEEnv
214
+
215
+ rows = []
216
+ model.eval()
217
+ for p in problems:
218
+ env = DeepAgentSWEEnv()
219
+ try:
220
+ env.reset(**{k: v for k, v in p.items() if k != "prompt"})
221
+ tools = _env_tool_callables(env)
222
+ tool_map = {t.__name__: t for t in tools}
223
+ prompt = p.get("prompt")
224
+ if isinstance(prompt, list):
225
+ messages = list(prompt)
226
+ else:
227
+ messages = [{"role": "user", "content": str(prompt or "")}]
228
+
229
+ final_text = ""
230
+ for _turn in range(max_turns):
231
+ try:
232
+ prompt_text = tokenizer.apply_chat_template(
233
+ messages,
234
+ tools=tools,
235
+ tokenize=False,
236
+ add_generation_prompt=True,
237
+ )
238
+ except TypeError:
239
+ prompt_text = tokenizer.apply_chat_template(
240
+ messages, tokenize=False, add_generation_prompt=True
241
+ )
242
+ except Exception:
243
+ prompt_text = messages[-1]["content"] if messages else ""
244
+
245
+ inputs = tokenizer(prompt_text, return_tensors="pt")
246
+ inputs = {k: v.to(model.device) for k, v in inputs.items()}
247
+ with torch.no_grad():
248
+ out = model.generate(
249
+ **inputs,
250
+ max_new_tokens=max_new_tokens,
251
+ do_sample=False,
252
+ pad_token_id=tokenizer.eos_token_id,
253
+ )
254
+ gen = out[0][inputs["input_ids"].shape[-1] :]
255
+ completion = tokenizer.decode(gen, skip_special_tokens=True)
256
+ final_text = completion
257
+ calls = _parse_tool_calls(completion)
258
+ messages.append({"role": "assistant", "content": completion})
259
+ if not calls:
260
+ break
261
+ for call in calls:
262
+ fn = tool_map.get(call["name"])
263
+ if fn is None:
264
+ result = f"Unknown tool: {call['name']}"
265
+ else:
266
+ try:
267
+ result = fn(**call["arguments"])
268
+ except TypeError:
269
+ try:
270
+ result = fn(*call["arguments"].values())
271
+ except Exception as e: # noqa: BLE001
272
+ result = f"TOOL_ERROR: {e}"
273
+ except Exception as e: # noqa: BLE001
274
+ result = f"TOOL_ERROR: {e}"
275
+ messages.append(
276
+ {
277
+ "role": "tool",
278
+ "name": call["name"],
279
+ "content": str(result)[:4000],
280
+ }
281
+ )
282
+
283
+ reward = float(env.get_reward())
284
+ report = getattr(env, "_last_report", {}) or {}
285
+ rows.append(
286
+ {
287
+ "task_id": p.get("task_id") or p.get("instance_id"),
288
+ "prompt": (
289
+ prompt
290
+ if isinstance(prompt, str)
291
+ else json.dumps(prompt)[:2000]
292
+ ),
293
+ "completion": final_text[:4000],
294
+ "pass_frac": float(report.get("pass_frac") or reward),
295
+ "reward_total": reward,
296
+ "resolved": bool(report.get("resolved")),
297
+ "resolution": report.get("resolution"),
298
+ }
299
+ )
300
+ except Exception as e: # noqa: BLE001
301
+ traceback.print_exc()
302
+ rows.append(
303
+ {
304
+ "task_id": p.get("task_id") or p.get("instance_id"),
305
+ "prompt": str(p.get("prompt"))[:2000],
306
+ "completion": f"EVAL_ERROR: {e}",
307
+ "pass_frac": 0.0,
308
+ "reward_total": 0.0,
309
+ "resolved": False,
310
+ "resolution": "ERROR",
311
+ }
312
+ )
313
+ finally:
314
+ try:
315
+ env._cleanup()
316
+ except Exception:
317
+ pass
318
+ return rows
319
+
320
+
321
  # ---------------------------------------------------------------------------
322
  # Langfuse checkpoint eval
323
  # ---------------------------------------------------------------------------
 
343
  raise RuntimeError("Langfuse auth failed during ckpt eval")
344
 
345
  n = len(results)
346
+ mean_pass = sum(float(r.get("pass_frac") or 0) for r in results) / n if n else 0.0
347
+ mean_reward = sum(float(r.get("reward_total") or 0) for r in results) / n if n else 0.0
348
+ n_perfect = sum(1 for r in results if float(r.get("pass_frac") or 0) >= 1.0)
349
+ n_resolved = sum(1 for r in results if r.get("resolved"))
350
 
351
  with lf.start_as_current_observation(
352
  name="kaggle-ckpt-eval",
 
357
  "train_model_id": train_model_id,
358
  "wandb_run_id": wandb_run_id,
359
  "phase": "ckpt_eval",
360
+ "harness": "deepagents-swe",
361
  },
362
  input={"step": step, "hf_revision": hf_revision, "n": n},
363
  ) as root:
 
365
  with lf.start_as_current_observation(
366
  name=f"task:{r['task_id']}",
367
  as_type="span",
368
+ input={"prompt": str(r.get("prompt", ""))[:2000]},
369
  ) as span:
370
  span.update(
371
  output={
372
+ "completion": str(r.get("completion", ""))[:2000],
373
+ "pass_frac": r.get("pass_frac"),
374
+ "reward_total": r.get("reward_total"),
375
+ "resolved": r.get("resolved"),
376
  }
377
  )
378
  summary = {
379
  "mean_pass_frac": mean_pass,
380
  "mean_reward_total": mean_reward,
381
  "n_perfect": n_perfect,
382
+ "n_resolved": n_resolved,
383
  "n_problems": n,
384
  "step": step,
385
  "hf_revision": hf_revision,
 
388
  lf.score_current_trace(name="mean_pass_frac", value=mean_pass)
389
  lf.score_current_trace(name="mean_reward_total", value=mean_reward)
390
  lf.score_current_trace(name="n_perfect", value=float(n_perfect))
391
+ lf.score_current_trace(name="n_resolved", value=float(n_resolved))
392
  lf.score_current_trace(name="checkpoint_step", value=float(step))
393
  trace_id = lf.get_current_trace_id() or ""
394
  lf.flush()
395
+ print(
396
+ f"[langfuse] ckpt-eval step={step} rev={hf_revision} "
397
+ f"mean_reward={mean_reward:.3f} resolved={n_resolved}/{n} trace={trace_id}"
398
+ )
399
  return {"langfuse_trace_id": trace_id, **summary}
400
 
401
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  # ---------------------------------------------------------------------------
403
  # Callback: eval + langfuse THEN save
404
  # ---------------------------------------------------------------------------
 
411
  hub_model_id: str,
412
  save_steps: int,
413
  n_eval: int,
414
+ max_eval_turns: int,
415
  ):
416
  from transformers import TrainerCallback, TrainerControl, TrainerState, TrainingArguments
417
 
418
  class EvalBeforeCheckpointCallback(TrainerCallback):
419
  def __init__(self) -> None:
420
  self._last_eval_step = -1
421
+ self.trainer = None
422
 
423
  def on_step_end(
424
  self,
 
447
  self.trainer, "tokenizer", None
448
  )
449
  if model is None or tokenizer is None:
 
450
  raise RuntimeError("ckpt-eval requires model+tokenizer")
451
+
452
  rev = f"grpo-step-{step}"
453
  problems = eval_problems[:n_eval]
454
+ print(f"\n=== AGENT EVAL BEFORE CHECKPOINT step={step} rev={rev} n={len(problems)} ===")
455
  try:
456
+ results = run_heldout_agent_eval(
457
+ model, tokenizer, problems, max_turns=max_eval_turns
458
+ )
459
  import wandb
460
 
461
  wandb_run_id = None
462
  if wandb.run is not None:
463
  wandb_run_id = wandb.run.id
464
+ mean_r = sum(float(r["reward_total"]) for r in results) / max(len(results), 1)
465
  wandb.log(
466
  {
467
+ "eval/mean_reward_total": mean_r,
468
+ "eval/mean_pass_frac": sum(float(r["pass_frac"]) for r in results)
469
+ / max(len(results), 1),
470
+ "eval/n_resolved": sum(1 for r in results if r.get("resolved")),
471
  "eval/checkpoint_step": step,
472
  },
473
  step=step,
 
480
  results=results,
481
  wandb_run_id=wandb_run_id,
482
  )
 
483
  out = Path(args.output_dir) / f"ckpt_eval_step_{step}.json"
484
+ out.write_text(
485
+ json.dumps(summary | {"results": results}, indent=2), encoding="utf-8"
486
+ )
487
 
 
488
  try:
489
  model.push_to_hub(
490
  hub_model_id,
491
  token=os.environ["HF_TOKEN"],
492
  revision=rev,
493
+ commit_message=(
494
+ f"Agent GRPO {rev}; mean_reward={summary['mean_reward_total']:.3f}"
495
+ ),
496
  private=False,
497
  )
498
  print(f"[hf] pushed {hub_model_id}@{rev}")
 
500
  wandb.log(
501
  {
502
  "hf/revision": rev,
503
+ "hf/mean_reward_at_push": summary["mean_reward_total"],
504
  "langfuse/ckpt_trace_id": summary.get("langfuse_trace_id"),
505
  },
506
  step=step,
 
509
  print(f"[hf] push failed (local ckpt still ok): {e}")
510
  traceback.print_exc()
511
  except Exception:
512
+ print("[ckpt-eval] FAILED — re-raising")
513
  traceback.print_exc()
514
  raise
515
  return control
 
527
 
528
  def __init__(
529
  self,
530
+ dataset_id: str = "vanishingradient/deepagents-rl-swe-lite",
531
  hub_model_id: str = "vanishingradient/deepagents-rl-coding",
532
+ train_split: str = "dev",
533
+ eval_split: str = "test",
534
+ max_steps: int = 20,
535
  save_steps: int = 5,
536
+ num_generations: int = 2,
537
  learning_rate: float = 1e-6,
538
+ max_completion_length: int = 1024,
539
+ max_seq_length: int = 4096,
540
+ max_tool_calling_iterations: int = 12,
541
+ n_eval: int = 2,
542
+ max_eval_turns: int = 8,
543
+ max_train_instances: int = 16,
544
  lora_r: int = 16,
545
  ) -> None:
546
  self.dataset_id = dataset_id
547
  self.hub_model_id = hub_model_id
548
+ self.train_split = train_split
549
+ self.eval_split = eval_split
550
  self.max_steps = max_steps
551
  self.save_steps = save_steps
552
  self.num_generations = num_generations
553
  self.learning_rate = learning_rate
554
  self.max_completion_length = max_completion_length
555
  self.max_seq_length = max_seq_length
556
+ self.max_tool_calling_iterations = max_tool_calling_iterations
557
  self.n_eval = n_eval
558
+ self.max_eval_turns = max_eval_turns
559
+ self.max_train_instances = max_train_instances
560
  self.lora_r = lora_r
561
 
562
 
563
+ def _load_swe_splits(cfg: Cfg):
564
+ from datasets import Dataset, load_dataset
565
+
566
+ from deepagents_rl.train.swe_env import prepare_swe_grpo_rows
567
+
568
+ ds = load_dataset(cfg.dataset_id, token=os.environ["HF_TOKEN"])
569
+ train_split = cfg.train_split if cfg.train_split in ds else "train"
570
+ eval_split = cfg.eval_split if cfg.eval_split in ds else (
571
+ "test" if "test" in ds else train_split
572
+ )
573
+
574
+ train_recs = [dict(r) for r in ds[train_split]]
575
+ eval_recs = [dict(r) for r in ds[eval_split]]
576
+
577
+ # Keep only rows with docker image + eval script
578
+ def _ok(r: dict) -> bool:
579
+ img = r.get("image") or r.get("docker_image") or ""
580
+ return bool(img) and bool(r.get("eval_script"))
581
+
582
+ train_recs = [r for r in train_recs if _ok(r)][: cfg.max_train_instances]
583
+ eval_recs = [r for r in eval_recs if _ok(r)]
584
+
585
+ train_rows = prepare_swe_grpo_rows(train_recs)
586
+ eval_rows = prepare_swe_grpo_rows(eval_recs)
587
+ if not train_rows:
588
+ raise RuntimeError(f"No usable SWE train rows in {cfg.dataset_id}/{train_split}")
589
+ if not eval_rows:
590
+ eval_rows = train_rows[: max(1, cfg.n_eval)]
591
+
592
+ print(
593
+ f"[data] train={len(train_rows)} eval={len(eval_rows)} "
594
+ f"splits={train_split}/{eval_split} dataset={cfg.dataset_id}"
595
+ )
596
+ return Dataset.from_list(train_rows), eval_rows
597
+
598
+
599
  def main(cfg: Cfg | None = None) -> None:
600
  global _TOKENIZER
601
  cfg = cfg or Cfg()
602
  load_secrets()
603
+ _ensure_package()
604
 
605
  import torch
606
  import wandb
 
607
  from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
608
  from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
609
  from trl import GRPOConfig, GRPOTrainer
610
 
611
+ from deepagents_rl.train.swe_env import DeepAgentSWEEnv
612
+
613
+ # Probe grader
614
+ import requests
615
+
616
+ grader = os.environ["SWE_GRADER_URL"].rstrip("/")
617
+ headers = {}
618
+ if os.environ.get("SWE_GRADER_TOKEN"):
619
+ headers["Authorization"] = f"Bearer {os.environ['SWE_GRADER_TOKEN']}"
620
+ health = requests.get(f"{grader}/health", headers=headers, timeout=30)
621
+ health.raise_for_status()
622
+ print("[grader] health", health.json())
623
+
624
  os.environ["WANDB_PROJECT"] = os.getenv("WANDB_PROJECT", "deepagents-rl-coding")
625
  wandb.login(key=os.environ["WANDB_API_KEY"])
626
 
627
  model_id = resolve_model()
628
+ train, eval_problems = _load_swe_splits(cfg)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
629
 
630
  bnb = BitsAndBytesConfig(
631
  load_in_4bit=True,
 
633
  bnb_4bit_quant_type="nf4",
634
  bnb_4bit_use_double_quant=True,
635
  )
636
+ tokenizer = AutoTokenizer.from_pretrained(
637
+ model_id, token=os.environ["HF_TOKEN"], trust_remote_code=True
638
+ )
639
  if tokenizer.pad_token is None:
640
  tokenizer.pad_token = tokenizer.eos_token
641
  _TOKENIZER = tokenizer
 
654
  lora_dropout=0.0,
655
  bias="none",
656
  task_type="CAUSAL_LM",
657
+ target_modules=[
658
+ "q_proj",
659
+ "k_proj",
660
+ "v_proj",
661
+ "o_proj",
662
+ "gate_proj",
663
+ "up_proj",
664
+ "down_proj",
665
+ ],
666
  )
667
  model = get_peft_model(model, lora)
668
 
 
672
 
673
  wandb.init(
674
  project=os.environ["WANDB_PROJECT"],
675
+ name="kaggle-grpo-swe",
676
  job_type="train",
677
  config={
678
  "train_model_id": model_id,
679
  "dataset_id": cfg.dataset_id,
680
+ "harness": "deepagents-swe",
681
  "max_steps": cfg.max_steps,
682
  "save_steps": cfg.save_steps,
683
  "n_eval": cfg.n_eval,
684
+ "num_generations": cfg.num_generations,
685
+ "max_tool_calling_iterations": cfg.max_tool_calling_iterations,
686
  "hub_model_id": cfg.hub_model_id,
687
+ "swe_grader_url": grader,
688
  "eval_before_every_checkpoint": True,
689
  },
690
  )
691
 
692
+ print("=== BASELINE AGENT EVAL (step 0) ===")
693
+ baseline = run_heldout_agent_eval(
694
+ model,
695
+ tokenizer,
696
+ eval_problems[: cfg.n_eval],
697
+ max_turns=cfg.max_eval_turns,
698
+ )
699
  base_summary = langfuse_log_ckpt_eval(
700
  step=0,
701
  hf_revision="grpo-step-0-baseline",
 
709
  wandb.log(
710
  {
711
  "eval/mean_pass_frac": base_summary["mean_pass_frac"],
712
+ "eval/mean_reward_total": base_summary["mean_reward_total"],
713
+ "eval/n_resolved": base_summary.get("n_resolved", 0),
714
  "eval/checkpoint_step": 0,
715
  "langfuse/ckpt_trace_id": base_summary.get("langfuse_trace_id"),
716
  },
 
719
 
720
  args = GRPOConfig(
721
  output_dir=output_dir,
722
+ run_name="kaggle-grpo-swe",
723
  learning_rate=cfg.learning_rate,
724
  per_device_train_batch_size=1,
725
  gradient_accumulation_steps=4,
726
  num_generations=cfg.num_generations,
727
  max_completion_length=cfg.max_completion_length,
728
+ max_tool_calling_iterations=cfg.max_tool_calling_iterations,
729
  max_steps=cfg.max_steps,
730
  logging_steps=1,
731
  save_steps=cfg.save_steps,
 
736
  remove_unused_columns=False,
737
  hub_model_id=cfg.hub_model_id,
738
  hub_token=os.environ["HF_TOKEN"],
739
+ push_to_hub=False,
740
  )
741
 
742
  cb = make_eval_before_save_callback(
 
745
  hub_model_id=cfg.hub_model_id,
746
  save_steps=cfg.save_steps,
747
  n_eval=cfg.n_eval,
748
+ max_eval_turns=cfg.max_eval_turns,
749
  )
750
 
751
  trainer = GRPOTrainer(
752
  model=model,
 
753
  args=args,
754
  train_dataset=train,
755
  processing_class=tokenizer,
756
  callbacks=[cb],
757
+ environment_factory=DeepAgentSWEEnv,
758
  )
759
  cb.trainer = trainer
760
 
761
+ trainer.train()
762
+
763
  final_step = int(trainer.state.global_step)
764
  rev = f"grpo-step-{final_step}-final"
765
+ print(f"=== FINAL AGENT EVAL step={final_step} ===")
766
+ final_results = run_heldout_agent_eval(
767
+ model,
768
+ tokenizer,
769
+ eval_problems[: cfg.n_eval],
770
+ max_turns=cfg.max_eval_turns,
771
+ )
772
  final_summary = langfuse_log_ckpt_eval(
773
  step=final_step,
774
  hf_revision=rev,
 
780
  cfg.hub_model_id,
781
  token=os.environ["HF_TOKEN"],
782
  revision=rev,
783
+ commit_message=(
784
+ f"Final agent GRPO {rev}; mean_reward={final_summary['mean_reward_total']:.3f}"
785
+ ),
786
  )
 
787
  model.push_to_hub(
788
  cfg.hub_model_id,
789
  token=os.environ["HF_TOKEN"],
790
+ commit_message=(
791
+ f"Final agent GRPO main; mean_reward={final_summary['mean_reward_total']:.3f}"
792
+ ),
793
  )
794
  wandb.log(
795
  {
796
+ "eval/final_mean_reward_total": final_summary["mean_reward_total"],
797
  "hf/final_revision": rev,
798
  }
799
  )