Spaces:
Running
Running
File size: 10,111 Bytes
898a901 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | """DPO retrain via AgentTune's closed-loop retrain path (run inside an HF GPU Job).
Consumes the healer's verified pairs, builds agenttune TrainingExample objects, and
trains an adapter-only LoRA through agenttune's own
examples_to_dpo_dataset -> build_retrainer (create_agentic_trainer) -> train()
exactly as the closed loop's run_retrain does (we call build_retrainer so we can save
the adapter explicitly afterwards).
Train/eval consistency: the student evaluates with the Cohere chat template
(student.build_prompt). examples_to_dpo_dataset flattens prompt messages as
"role: content", BUT with role="" it emits content verbatim - so we pre-apply the
chat template here and hand it the final string. Training sees exactly the text the
model sees at inference.
Launched by launch_job.py, which injects --data/--out/--push-repo/--push-path.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--data", default=None) # injected by launcher; unused
ap.add_argument("--out", required=True)
ap.add_argument("--push-repo", default=None)
ap.add_argument("--push-path", default=None)
ap.add_argument("--pairs-repo", default="HideIron/selfheal-sql-runs")
ap.add_argument("--pairs-path", default="heal/baseline-pool600/pairs.jsonl")
ap.add_argument("--model", default="CohereLabs/tiny-aya-global")
ap.add_argument("--adapter-repo", default="HideIron/tiny-aya-global-sql-selfheal")
ap.add_argument("--epochs", type=float, default=3.0)
ap.add_argument("--lr", type=float, default=1e-5)
ap.add_argument("--beta", type=float, default=0.1)
ap.add_argument("--lora-r", type=int, default=16)
ap.add_argument("--batch-size", type=int, default=2)
ap.add_argument("--grad-accum", type=int, default=8)
ap.add_argument("--max-steps", type=int, default=None, help="smoke-test cap")
ap.add_argument("--max-prompt-len", type=int, default=1408)
ap.add_argument("--rpo-alpha", type=float, default=0.0)
ap.add_argument("--algorithm", default="dpo", choices=["dpo", "bco"])
ap.add_argument("--replay-run", default=None,
help="BCO only: student run whose CORRECT answers are added as "
"positive (label=True) replay rows against forgetting")
ap.add_argument("--no-push-adapter", action="store_true")
args = ap.parse_args()
from huggingface_hub import HfApi, hf_hub_download
from transformers import AutoTokenizer
from agenttune.decide.closed_loop.contracts import TrainingExample
from agenttune.decide.closed_loop.retrain_config import RetrainConfig, build_retrainer
pp = hf_hub_download(args.pairs_repo, args.pairs_path, repo_type="dataset")
pairs = [json.loads(l) for l in open(pp, encoding="utf-8")]
healed = [p for p in pairs if p.get("healed") and p.get("chosen_sql")]
print(f"[dpo] {len(healed)} verified pairs of {len(pairs)} failures", flush=True)
tok = AutoTokenizer.from_pretrained(args.model)
def templ(user_content: str) -> str:
return tok.apply_chat_template(
[{"role": "user", "content": user_content}],
add_generation_prompt=True, tokenize=False,
)
examples = [
TrainingExample(
trajectory_id=p["qid"],
original_failure_type=p["status_before"],
root_cause=p["root_cause"],
# role "" -> _messages_to_text emits content verbatim (no "user: " prefix)
prompt=[{"role": "", "content": templ(p["prompt_user_content"])}],
chosen=[{"role": "", "content": p["chosen_sql"]}],
rejected=[{"role": "", "content": p["rejected_sql"]}],
)
for p in healed
]
if args.replay_run:
assert args.algorithm == "bco", "success replay needs BCO's independent rows"
import re as _re
def _extract(t): # minimal extract_sql (fences + first statement)
t = _re.sub(r"<think>.*?</think>", "", t, flags=_re.S)
m = _re.search(r"```(?:sql)?\s*(.*?)```", t, _re.S | _re.I)
if m:
t = m.group(1)
m = _re.search(r"\b(select|with)\b", t, _re.I)
return t[m.start():].split(";")[0].strip() if m else t.strip()
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from student import PROMPT # the exact student prompt builder
split_rows = {}
sp = os.path.join(args.data or "/data", "splits", "pool.jsonl")
for s in map(json.loads, open(sp, encoding="utf-8")):
split_rows[s["qid"]] = s
rp2 = hf_hub_download(args.pairs_repo, f"runs/{args.replay_run}/results.jsonl",
repo_type="dataset")
n0 = len(examples)
for r in map(json.loads, open(rp2, encoding="utf-8")):
if r["status"] == "correct" and r["qid"] in split_rows:
s = split_rows[r["qid"]]
examples.append(TrainingExample(
trajectory_id=r["qid"] + "-replay",
original_failure_type="success_replay", root_cause="none",
prompt=[{"role": "", "content": templ(
PROMPT.format(schema=s["schema"], question=s["question"]))}],
chosen=[{"role": "", "content": _extract(r["raw_output"])}],
))
print(f"[dpo] +{len(examples)-n0} success-replay rows (label=True)", flush=True)
cfg = RetrainConfig(
model=args.model,
algorithm=args.algorithm,
output_dir=os.path.join(args.out, "adapter"),
lora_r=args.lora_r,
lora_alpha=args.lora_r * 2,
lora_dropout=0.05,
lora_target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
num_train_epochs=args.epochs,
per_device_train_batch_size=args.batch_size,
gradient_accumulation_steps=args.grad_accum,
learning_rate=args.lr,
beta=args.beta,
max_steps=args.max_steps,
extra={
"bf16": True,
"logging_steps": 2,
"save_strategy": "no",
"report_to": "none",
"gradient_checkpointing": True,
},
)
if args.algorithm == "dpo":
# The BCO wrapper instantiates the model itself and rejects model_init_kwargs.
cfg.extra["model_init_kwargs"] = {"dtype": "bfloat16"}
# Cohere vocab is ~256k: full-sequence fp32 logits are ~4 GiB per copy and OOM a
# 24 GB card. use_logits_to_keep restricts logits to completion tokens (~60 here).
# If this TRL lacks it, shrink sequence caps hard instead.
import inspect
import trl
cfg_keys = set(inspect.signature(trl.DPOConfig.__init__).parameters)
if "use_logits_to_keep" in cfg_keys:
cfg.extra.update({"use_logits_to_keep": True,
"max_prompt_length": 2944, "max_length": 3072})
print("[dpo] use_logits_to_keep=True (trl", trl.__version__, ")", flush=True)
else:
cfg.extra.update({"max_prompt_length": args.max_prompt_len,
"max_length": args.max_prompt_len + 128})
print("[dpo] trl lacks use_logits_to_keep -> caps", args.max_prompt_len, flush=True)
# rpo_alpha adds an NLL term on the chosen completion (DPO+SFT hybrid): the model
# must LEARN to produce the verified fix, not merely prefer it. v1 (pure DPO,
# 30 steps) failed to move pool accuracy at all - this is the main v2 fix.
if args.rpo_alpha and "rpo_alpha" in cfg_keys:
cfg.extra["rpo_alpha"] = args.rpo_alpha
print("[dpo] rpo_alpha =", args.rpo_alpha, flush=True)
elif args.rpo_alpha:
print("[dpo] WARNING: trl lacks rpo_alpha; pure DPO", flush=True)
lens = sorted(len(tok(templ(p["prompt_user_content"]))["input_ids"]) for p in healed)
print(f"[dpo] prompt tokens: median={lens[len(lens)//2]} p90={lens[int(len(lens)*.9)]} "
f"max={lens[-1]} | cap={cfg.extra['max_prompt_length']}", flush=True)
trainer = build_retrainer(examples, cfg)
stats = trainer.train()
print("[dpo] train() returned:", str(stats)[:500], flush=True)
# Save the adapter explicitly (find the underlying HF trainer/model).
os.makedirs(cfg.output_dir, exist_ok=True)
inner = None
for attr in ("trainer", "_trainer", "dpo_trainer"):
inner = getattr(trainer, attr, None)
if inner is not None and hasattr(inner, "model"):
break
model_obj = getattr(inner, "model", None) or getattr(trainer, "model", None)
if model_obj is None:
raise RuntimeError(
f"cannot locate trained model on {type(trainer).__name__}; attrs: "
+ ", ".join(a for a in dir(trainer) if not a.startswith("__"))[:800]
)
model_obj.save_pretrained(cfg.output_dir) # PeftModel -> adapter only
print("[dpo] adapter saved:", os.listdir(cfg.output_dir), flush=True)
with open(os.path.join(args.out, "train_stats.json"), "w", encoding="utf-8") as f:
json.dump({"n_pairs": len(healed), "stats": str(stats)[:2000],
"config": {k: v for k, v in vars(args).items()}}, f, indent=2)
api = HfApi()
if not args.no_push_adapter:
api.create_repo(args.adapter_repo, private=True, exist_ok=True)
api.upload_folder(repo_id=args.adapter_repo, folder_path=cfg.output_dir,
commit_message=f"DPO LoRA ({len(healed)} self-heal pairs)")
print("[dpo] adapter pushed:", args.adapter_repo, flush=True)
if args.push_repo:
api.upload_folder(repo_id=args.push_repo, repo_type="dataset",
folder_path=args.out, path_in_repo=args.push_path or "dpo",
commit_message="dpo training stats")
if __name__ == "__main__":
main()
|