El-Mouden Moncif Claude Opus 5 commited on
Commit
4f41179
·
1 Parent(s): 9a6c537

Add RepoPeftBench corpus merge

Browse files

Joins Code2LoRA's benchmark (~500K assertion-completion items over 512 repos)
to aligned6 (~27K prose QA over 2066 repos). The two are complementary: the
benchmark showed the head learns a repo's stack and conventions but not what the
project does, and exact-recall data is what addresses that.

Carries RepoPeftBench's own by-repo partition through unchanged, and lets an
eval assignment always win over a train one, so no repo can leak between train
and cross-repo eval. Caps QA per repo (evo alone has ~1000/repo) so the exact
recall task cannot swamp the prose QA.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

app/scripts/benchmark_repo.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Benchmark a generated adapter against the frozen base model on ONE repo.
3
+
4
+ Builds the question set from the repo's ACTUAL contents (deps, test framework,
5
+ packaging, layout, license, entry points) rather than hand-written guesses, so
6
+ the gold answers are ground truth rather than opinion. Then scores base vs
7
+ adapted on the same model instance, adapter toggled with `disable_adapter()`.
8
+
9
+ Three task families, because an adapter can help one and hurt another -- which
10
+ is exactly what happened here (QA improved hugely while raw-text modelling
11
+ regressed), and a single number would have hidden it:
12
+
13
+ FACT - "Q: <question>\\nA:" -> short factual answer. The trained format.
14
+ CODE - completion of real lines taken from the repo's own source.
15
+ TEXT - plain continuation of repo prose (README/docstrings).
16
+
17
+ Metrics per family:
18
+ loss teacher-forced cross-entropy on the gold answer (lower better)
19
+ win rate fraction of items where adapted loss < base loss
20
+ keyword fraction of greedy generations containing the gold keyword
21
+
22
+ Usage:
23
+ python benchmark_repo.py --job <jobId> --checkpoint ../../runs/h200_run/head.best.pt
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import json
29
+ import re
30
+ import sys
31
+ from pathlib import Path
32
+
33
+ import numpy as np
34
+ import torch
35
+
36
+ HERE = Path(__file__).resolve().parent
37
+ sys.path.insert(0, str(HERE.parent / "engine"))
38
+ import config # noqa: E402
39
+ from generate_and_merge import (TARGET_MODULES, load_head, # noqa: E402
40
+ _lora_modules_by_spec_name)
41
+ from memory_lora.core import DEFAULT_ROOT_PREFIX, get_module_specs # noqa: E402
42
+
43
+ SKIP = {".git", "__pycache__", ".venv", "node_modules", "build", "dist", ".tox"}
44
+
45
+
46
+ # --------------------------------------------------------------------------
47
+ # Build the benchmark from repo ground truth
48
+ # --------------------------------------------------------------------------
49
+
50
+ def _read(p: Path, n: int = 20000) -> str:
51
+ try:
52
+ return p.read_text(errors="ignore")[:n]
53
+ except OSError:
54
+ return ""
55
+
56
+
57
+ def build_fact_items(repo: Path) -> list[dict]:
58
+ """Derive Q/A pairs whose answers are verifiable from the repo itself."""
59
+ items: list[dict] = []
60
+ pyproject = _read(repo / "pyproject.toml")
61
+ setup_py = _read(repo / "setup.py")
62
+ setup_cfg = _read(repo / "setup.cfg")
63
+ build = pyproject + setup_py + setup_cfg
64
+
65
+ # packaging backend
66
+ if "setuptools" in build:
67
+ items.append(dict(q="What packaging tool does this project use?",
68
+ a=" setuptools", kw="setuptools"))
69
+ elif "poetry" in build.lower():
70
+ items.append(dict(q="What packaging tool does this project use?",
71
+ a=" poetry", kw="poetry"))
72
+ if "hatchling" in build:
73
+ items.append(dict(q="What build backend does this project declare?",
74
+ a=" hatchling", kw="hatchling"))
75
+
76
+ # test framework
77
+ test_files = [p for p in repo.rglob("*.py")
78
+ if not any(s in p.parts for s in SKIP)
79
+ and ("test" in p.name.lower() or "tests" in p.parts)]
80
+ joined = " ".join(_read(p, 4000) for p in test_files[:12])
81
+ if "pytest" in joined or "pytest" in build:
82
+ items.append(dict(q="What testing framework does this repository use?",
83
+ a=" pytest", kw="pytest"))
84
+ elif "unittest" in joined:
85
+ items.append(dict(q="What testing framework does this repository use?",
86
+ a=" unittest", kw="unittest"))
87
+
88
+ # license
89
+ lic = _read(repo / "LICENSE") + _read(repo / "LICENSE.txt")
90
+ for name, key in (("Apache", "Apache"), ("MIT", "MIT"),
91
+ ("BSD", "BSD"), ("GNU", "GPL")):
92
+ if name.lower() in lic.lower()[:400]:
93
+ items.append(dict(q="What license does this project use?",
94
+ a=f" {key}", kw=key))
95
+ break
96
+
97
+ # top-level package
98
+ EXCL = {"tests", "test", "docs", "doc", "examples", "example", "scripts",
99
+ "benchmarks", "ext"}
100
+ pkgs = [d.name for d in repo.iterdir()
101
+ if d.is_dir() and (d / "__init__.py").exists()
102
+ and d.name not in SKIP and d.name.lower() not in EXCL]
103
+ if pkgs:
104
+ # Prefer the package named after the repo (src/<name> layouts included).
105
+ best = next((k for k in pkgs if k.lower() == repo.name.lower()), pkgs[0])
106
+ items.append(dict(q="What is the name of the main Python package in this repository?",
107
+ a=f" {best}", kw=best))
108
+
109
+ # dependencies
110
+ # Parse only INSIDE a dependency list, otherwise setup.py keywords such as
111
+ # `install_requires=` / `python_requires=` get matched as package names.
112
+ dep_block = ""
113
+ for pat in (r"install_requires\s*=\s*\[(.*?)\]",
114
+ r"dependencies\s*=\s*\[(.*?)\]",
115
+ r"\[project\.dependencies\](.*?)(?:\n\[|\Z)"):
116
+ m = re.search(pat, build, re.S)
117
+ if m:
118
+ dep_block = m.group(1)
119
+ break
120
+ NOT_PKG = {"python", "name", "version", "requires", "install", "extras",
121
+ "setup", "packages", "classifiers"}
122
+ deps = re.findall(r"['\"]([A-Za-z][A-Za-z0-9_.-]{2,})\s*[><=~!\[]", dep_block)
123
+ deps = [d for d in deps if d.lower() not in NOT_PKG]
124
+ if deps:
125
+ items.append(dict(q="Name a runtime dependency of this project.",
126
+ a=f" {deps[0]}", kw=deps[0]))
127
+
128
+ # CI
129
+ ci = list((repo / ".github" / "workflows").glob("*.y*ml")) if (repo / ".github" / "workflows").exists() else []
130
+ if ci:
131
+ items.append(dict(q="What CI system does this repository use?",
132
+ a=" GitHub Actions", kw="GitHub Actions"))
133
+
134
+ # docs
135
+ if (repo / "docs").is_dir():
136
+ conf = _read(repo / "docs" / "conf.py")
137
+ if "sphinx" in conf.lower() or (repo / "docs" / "conf.py").exists():
138
+ items.append(dict(q="What documentation tool does this project use?",
139
+ a=" Sphinx", kw="Sphinx"))
140
+ return items
141
+
142
+
143
+ def build_code_items(repo: Path, n: int = 12) -> list[dict]:
144
+ """Split real source lines: prefix -> the rest of the line."""
145
+ items: list[dict] = []
146
+ srcs = [p for p in repo.rglob("*.py")
147
+ if not any(s in p.parts for s in SKIP)
148
+ and "test" not in p.name.lower()]
149
+ for p in srcs[:40]:
150
+ text = _read(p, 12000)
151
+ lines = [l for l in text.splitlines()
152
+ if 30 < len(l) < 110 and not l.strip().startswith("#")
153
+ and ("(" in l or "=" in l or "import" in l)]
154
+ for l in lines[:2]:
155
+ cut = max(len(l) // 2, l.find("(") + 1 if "(" in l else len(l) // 2)
156
+ prefix, target = l[:cut], l[cut:]
157
+ if len(target.strip()) < 4:
158
+ continue
159
+ items.append(dict(prefix=f"# file: {p.name}\n{prefix}", target=target))
160
+ if len(items) >= n:
161
+ return items
162
+ return items
163
+
164
+
165
+ def build_text_items(repo: Path, n: int = 6) -> list[dict]:
166
+ items: list[dict] = []
167
+ for name in ("README.md", "README.rst", "HISTORY.md", "CHANGELOG.md"):
168
+ t = _read(repo / name, 6000)
169
+ if len(t) < 600:
170
+ continue
171
+ chunks = [c for c in t.split("\n\n") if len(c) > 200][:3]
172
+ for c in chunks:
173
+ half = len(c) // 2
174
+ items.append(dict(prefix=c[:half], target=c[half:half + 300]))
175
+ if len(items) >= n:
176
+ return items
177
+ return items
178
+
179
+
180
+ # --------------------------------------------------------------------------
181
+ # Scoring
182
+ # --------------------------------------------------------------------------
183
+
184
+ @torch.no_grad()
185
+ def item_loss(model, tok, prefix: str, target: str, device: str) -> float:
186
+ pid = tok(prefix, add_special_tokens=False)["input_ids"]
187
+ tid = tok(target, add_special_tokens=False)["input_ids"]
188
+ if not tid or not pid:
189
+ return float("nan")
190
+ ids = torch.tensor([pid + tid], device=device)
191
+ labels = torch.tensor([[-100] * len(pid) + tid], device=device)
192
+ return float(model(input_ids=ids, labels=labels).loss)
193
+
194
+
195
+ @torch.no_grad()
196
+ def generate(model, tok, prompt: str, device: str, max_new: int = 24) -> str:
197
+ enc = tok(prompt, return_tensors="pt").to(device)
198
+ out = model.generate(**enc, max_new_tokens=max_new, do_sample=False,
199
+ pad_token_id=tok.pad_token_id or tok.eos_token_id)
200
+ return tok.decode(out[0][enc["input_ids"].shape[1]:],
201
+ skip_special_tokens=True).split("\n")[0]
202
+
203
+
204
+ def run_family(model, tok, items, device, name, generate_kw=False):
205
+ rows = []
206
+ for it in items:
207
+ prefix = it.get("prefix") or f"Q: {it['q']}\nA:"
208
+ target = it.get("target") or it["a"]
209
+ with model.disable_adapter():
210
+ lb = item_loss(model, tok, prefix, target, device)
211
+ gb = generate(model, tok, prefix, device) if generate_kw else ""
212
+ la = item_loss(model, tok, prefix, target, device)
213
+ ga = generate(model, tok, prefix, device) if generate_kw else ""
214
+ rows.append(dict(prefix=prefix, target=target, kw=it.get("kw", ""),
215
+ base=lb, adapted=la, gen_base=gb, gen_adapted=ga))
216
+ valid = [r for r in rows if not (np.isnan(r["base"]) or np.isnan(r["adapted"]))]
217
+ if not valid:
218
+ return None
219
+ mb = float(np.mean([r["base"] for r in valid]))
220
+ ma = float(np.mean([r["adapted"] for r in valid]))
221
+ wins = sum(1 for r in valid if r["adapted"] < r["base"])
222
+ out = dict(family=name, n=len(valid), base=mb, adapted=ma,
223
+ delta=ma - mb, win_rate=wins / len(valid))
224
+ if generate_kw:
225
+ kb = sum(1 for r in valid if r["kw"] and r["kw"].lower() in r["gen_base"].lower())
226
+ ka = sum(1 for r in valid if r["kw"] and r["kw"].lower() in r["gen_adapted"].lower())
227
+ nk = sum(1 for r in valid if r["kw"])
228
+ out["kw_base"] = kb / max(nk, 1)
229
+ out["kw_adapted"] = ka / max(nk, 1)
230
+ out["nk"] = nk
231
+ return out, rows
232
+
233
+
234
+ def main() -> None:
235
+ ap = argparse.ArgumentParser()
236
+ ap.add_argument("--job", required=True)
237
+ ap.add_argument("--checkpoint", default=str(config.DEFAULT_CHECKPOINT))
238
+ ap.add_argument("--device", default="cpu")
239
+ ap.add_argument("--show-generations", action="store_true")
240
+ args = ap.parse_args()
241
+
242
+ from peft import LoraConfig, get_peft_model
243
+ from transformers import AutoModelForImageTextToText, AutoTokenizer
244
+
245
+ ws = config.workspace(args.job)
246
+ repo = ws / "repo"
247
+ if not repo.exists():
248
+ print(f"repo clone missing at {repo}", file=sys.stderr)
249
+ sys.exit(1)
250
+ repo_url = json.loads((ws / "status.json").read_text()).get("repo_url", "?")
251
+ emb = np.load(ws / "embedding.npy").astype("float32")
252
+ device = config.resolve_device(args.device)
253
+
254
+ facts = build_fact_items(repo)
255
+ codes = build_code_items(repo)
256
+ texts = build_text_items(repo)
257
+ print(f"repo: {repo_url}")
258
+ print(f"checkpoint: {args.checkpoint}")
259
+ print(f"benchmark: {len(facts)} FACT, {len(codes)} CODE, {len(texts)} TEXT items\n",
260
+ flush=True)
261
+
262
+ head, cfg, alpha = load_head(Path(args.checkpoint))
263
+ with torch.no_grad():
264
+ head_out = head(torch.from_numpy(emb).unsqueeze(0))
265
+
266
+ tok = AutoTokenizer.from_pretrained(config.BASE_MODEL)
267
+ if tok.pad_token is None:
268
+ tok.pad_token = tok.eos_token
269
+ base = AutoModelForImageTextToText.from_pretrained(
270
+ config.BASE_MODEL, torch_dtype=torch.float32, low_cpu_mem_usage=True)
271
+ specs = get_module_specs(base, TARGET_MODULES, root_prefix=DEFAULT_ROOT_PREFIX)
272
+ type_of = {s.full_name: s.type for s in specs}
273
+ model = get_peft_model(base, LoraConfig(
274
+ r=cfg["rank"], lora_alpha=alpha,
275
+ target_modules=[s.full_name for s in specs], lora_dropout=0.0, bias="none"))
276
+ mods = _lora_modules_by_spec_name(model)
277
+ with torch.no_grad():
278
+ for sp in specs:
279
+ m = mods.get(sp.full_name)
280
+ if m is None:
281
+ continue
282
+ t = type_of[sp.full_name]
283
+ m.lora_A["default"].weight.copy_(head_out["A"][t][0].float())
284
+ m.lora_B["default"].weight.copy_(head_out["B"][t][0].float())
285
+ model.to(device)
286
+ model.eval()
287
+
288
+ results = []
289
+ all_rows = {}
290
+ for items, name, gk in ((facts, "FACT", True), (codes, "CODE", False),
291
+ (texts, "TEXT", False)):
292
+ if not items:
293
+ continue
294
+ r = run_family(model, tok, items, device, name, generate_kw=gk)
295
+ if r:
296
+ res, rows = r
297
+ results.append(res)
298
+ all_rows[name] = rows
299
+
300
+ print(f"{'family':<7} {'n':>3} {'base':>8} {'adapted':>8} {'delta':>9} {'win%':>6}")
301
+ print("-" * 46)
302
+ for r in results:
303
+ print(f"{r['family']:<7} {r['n']:>3} {r['base']:>8.4f} {r['adapted']:>8.4f} "
304
+ f"{r['delta']:>+9.4f} {100*r['win_rate']:>5.0f}%")
305
+ for r in results:
306
+ if "kw_base" in r:
307
+ print(f"\nFACT keyword accuracy over {r['nk']} verifiable answers:")
308
+ print(f" base {100*r['kw_base']:.0f}%")
309
+ print(f" adapted {100*r['kw_adapted']:.0f}%")
310
+
311
+ if args.show_generations and "FACT" in all_rows:
312
+ print("\n--- FACT generations ---")
313
+ for row in all_rows["FACT"]:
314
+ q = row["prefix"].replace("Q: ", "").replace("\nA:", "")
315
+ print(f"Q: {q}\n gold: {row['target'].strip()}")
316
+ print(f" base: {row['gen_base'].strip()[:110]}")
317
+ print(f" adapted: {row['gen_adapted'].strip()[:110]}\n")
318
+
319
+
320
+ if __name__ == "__main__":
321
+ main()
app/scripts/test_repo_qa.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Test a generated adapter the way it was TRAINED: repo Q&A.
3
+
4
+ `diagnose_head.py` scores raw repo text, which is the wrong distribution for a
5
+ head trained on "Q: <question>\\nA:" -> answer. It is the right tool for
6
+ detecting an adapter that is inert or collapsed (bad at everything), but a
7
+ working head specializes on the QA format and can look worse on plain text
8
+ while being dramatically better at the task it was trained for.
9
+
10
+ This scores the trained format, on the same model instance, adapter on vs off:
11
+ * loss on held-out QA pairs for the repo (if the repo is in the corpus)
12
+ * greedy generations for hand-written questions, base vs adapted
13
+
14
+ Usage:
15
+ python test_repo_qa.py --job <jobId> --checkpoint ../../runs/h200_run/head.best.pt
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import json
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ import numpy as np
25
+ import torch
26
+
27
+ HERE = Path(__file__).resolve().parent
28
+ sys.path.insert(0, str(HERE.parent / "engine"))
29
+ import config # noqa: E402
30
+ from generate_and_merge import (TARGET_MODULES, load_head, # noqa: E402
31
+ _lora_modules_by_spec_name)
32
+ from memory_lora.core import DEFAULT_ROOT_PREFIX, get_module_specs # noqa: E402
33
+
34
+ QUESTIONS = [
35
+ "What is the core purpose of this repository?",
36
+ "What is the main entry point or primary public API of this project?",
37
+ "How is this project's source code organized?",
38
+ "What testing framework and conventions does this repository use?",
39
+ "How is this project built, packaged, or deployed?",
40
+ ]
41
+
42
+
43
+ @torch.no_grad()
44
+ def gen(model, tok, prompt: str, device: str, max_new: int = 40) -> str:
45
+ enc = tok(prompt, return_tensors="pt").to(device)
46
+ out = model.generate(**enc, max_new_tokens=max_new, do_sample=False,
47
+ pad_token_id=tok.pad_token_id or tok.eos_token_id)
48
+ txt = tok.decode(out[0][enc["input_ids"].shape[1]:], skip_special_tokens=True)
49
+ return txt.split("\nQ:")[0].strip()
50
+
51
+
52
+ @torch.no_grad()
53
+ def qa_loss(model, tok, pairs, device: str) -> float:
54
+ tot, ntok = 0.0, 0
55
+ for p in pairs:
56
+ prefix, target = p["prefix"], p["target"]
57
+ pid = tok(prefix, add_special_tokens=False)["input_ids"]
58
+ tid = tok(target, add_special_tokens=False)["input_ids"]
59
+ if not tid:
60
+ continue
61
+ ids = torch.tensor([pid + tid], device=device)
62
+ labels = torch.tensor([[-100] * len(pid) + tid], device=device)
63
+ out = model(input_ids=ids, labels=labels)
64
+ n = len(tid)
65
+ tot += float(out.loss) * n
66
+ ntok += n
67
+ return tot / max(ntok, 1)
68
+
69
+
70
+ def main() -> None:
71
+ ap = argparse.ArgumentParser()
72
+ ap.add_argument("--job", required=True)
73
+ ap.add_argument("--checkpoint", default=str(config.DEFAULT_CHECKPOINT))
74
+ ap.add_argument("--qna-path", default="data/qna/aligned6_qna.jsonl")
75
+ ap.add_argument("--device", default="cpu")
76
+ ap.add_argument("--max-qa", type=int, default=20)
77
+ args = ap.parse_args()
78
+
79
+ from peft import LoraConfig, get_peft_model
80
+ from transformers import AutoModelForImageTextToText, AutoTokenizer
81
+
82
+ ws = config.workspace(args.job)
83
+ emb = np.load(ws / "embedding.npy").astype("float32")
84
+ repo_url = json.loads((ws / "status.json").read_text()).get("repo_url", "?")
85
+ device = config.resolve_device(args.device)
86
+
87
+ head, cfg, alpha = load_head(Path(args.checkpoint))
88
+ with torch.no_grad():
89
+ head_out = head(torch.from_numpy(emb).unsqueeze(0))
90
+
91
+ print(f"repo: {repo_url}\ncheckpoint: {args.checkpoint}\n", flush=True)
92
+ tok = AutoTokenizer.from_pretrained(config.BASE_MODEL)
93
+ if tok.pad_token is None:
94
+ tok.pad_token = tok.eos_token
95
+ base = AutoModelForImageTextToText.from_pretrained(
96
+ config.BASE_MODEL, torch_dtype=torch.float32, low_cpu_mem_usage=True)
97
+ specs = get_module_specs(base, TARGET_MODULES, root_prefix=DEFAULT_ROOT_PREFIX)
98
+ type_of = {s.full_name: s.type for s in specs}
99
+ model = get_peft_model(base, LoraConfig(
100
+ r=cfg["rank"], lora_alpha=alpha,
101
+ target_modules=[s.full_name for s in specs],
102
+ lora_dropout=0.0, bias="none"))
103
+ mods = _lora_modules_by_spec_name(model)
104
+ with torch.no_grad():
105
+ for sp in specs:
106
+ m = mods.get(sp.full_name)
107
+ if m is None:
108
+ continue
109
+ t = type_of[sp.full_name]
110
+ m.lora_A["default"].weight.copy_(head_out["A"][t][0].float())
111
+ m.lora_B["default"].weight.copy_(head_out["B"][t][0].float())
112
+ model.to(device)
113
+ model.eval()
114
+
115
+ # 1. Held-out QA loss in the trained format, if this repo is in the corpus.
116
+ doc_id = repo_url.replace("https://github.com/", "").rstrip("/")
117
+ pairs = []
118
+ qp = Path(args.qna_path)
119
+ if qp.exists():
120
+ with open(qp) as f:
121
+ for line in f:
122
+ d = json.loads(line)
123
+ if d.get("doc_id") == doc_id:
124
+ pairs.append(d)
125
+ if len(pairs) >= args.max_qa:
126
+ break
127
+ if pairs:
128
+ with model.disable_adapter():
129
+ lb = qa_loss(model, tok, pairs, device)
130
+ la = qa_loss(model, tok, pairs, device)
131
+ print(f"QA loss on {len(pairs)} pairs (the TRAINED format):")
132
+ print(f" base {lb:.4f}")
133
+ print(f" adapted {la:.4f} delta {la-lb:+.4f} "
134
+ f"{'HELPS' if la < lb else 'HURTS'}\n", flush=True)
135
+ else:
136
+ print(f"(no QA rows for doc_id '{doc_id}' — generation comparison only)\n",
137
+ flush=True)
138
+
139
+ # 2. Generation comparison.
140
+ for q in QUESTIONS:
141
+ prompt = f"Q: {q}\nA:"
142
+ with model.disable_adapter():
143
+ b = gen(model, tok, prompt, device)
144
+ a = gen(model, tok, prompt, device)
145
+ print(f"Q: {q}")
146
+ print(f" base: {b[:200]}")
147
+ print(f" adapted: {a[:200]}\n", flush=True)
148
+
149
+
150
+ if __name__ == "__main__":
151
+ main()
runs/h200_best/head.best.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:48804c71508e1d96381b33acf0a331237a9ba2c5f2a2e1dd6759ae5212ac4f31
3
+ size 3001716868
scripts/merge_repopeft_corpus.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Merge RepoPeftBench (Code2LoRA's own benchmark) into the aligned6 corpus.
3
+
4
+ RepoPeftBench contributes ~500K *assertion-completion* items over 512 repos --
5
+ short, exact code targets. aligned6 contributes ~27K *prose* QA over 2066 repos
6
+ -- conventions and architecture. They are complementary: the benchmark run
7
+ showed the head learns a repo's stack and conventions but not what it actually
8
+ does, and exact-recall data is what addresses that.
9
+
10
+ Split integrity is the thing to get right. A repo must never appear in both a
11
+ training split and an eval split, or cross-repo evaluation becomes meaningless.
12
+ RepoPeftBench already partitions BY REPO (cr_val / cr_test hold out whole
13
+ repositories), so we carry its partition through unchanged and only ever add
14
+ repos to `train` when the benchmark itself calls them training repos.
15
+
16
+ doc split : which repos the hypernetwork trains on (train/cr_val/cr_test)
17
+ qna_split : within a train repo, held-out QA for in-repo eval (ir_*)
18
+
19
+ Usage:
20
+ python scripts/merge_repopeft_corpus.py \
21
+ --repopeft-emb data/embeddings/repopeft_6view.parquet \
22
+ --out-emb data/embeddings/all_lora_embeddings.parquet \
23
+ --out-qna data/qna/all_lora_qna.jsonl
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import json
29
+ import sys
30
+ from collections import Counter, defaultdict
31
+ from pathlib import Path
32
+
33
+ import pyarrow.parquet as pq
34
+
35
+ HERE = Path(__file__).resolve().parent
36
+ REPO_ROOT = HERE.parent
37
+ sys.path.insert(0, str(REPO_ROOT))
38
+
39
+ # file stem -> (doc split contributed, qna_split)
40
+ # cr_* hold out whole repos; ir_* are held-out QA of repos that stay in train.
41
+ SPLIT_MAP = {
42
+ "train": ("train", "train"),
43
+ "cr_val": ("cr_val", "train"),
44
+ "cr_test": ("cr_test", "train"),
45
+ "ir_val": ("train", "held_out"),
46
+ "ir_test": ("train", "held_out"),
47
+ }
48
+
49
+
50
+ def main() -> None:
51
+ ap = argparse.ArgumentParser()
52
+ ap.add_argument("--repopeft-emb", default="data/embeddings/repopeft_6view.parquet")
53
+ ap.add_argument("--aligned-emb", default="data/embeddings/aligned6_embeddings.parquet")
54
+ ap.add_argument("--aligned-qna", default="data/qna/aligned6_qna.jsonl")
55
+ ap.add_argument("--repopeft-glob", default="data/real_code2lora/*/qna/*.parquet")
56
+ ap.add_argument("--out-emb", default="data/embeddings/all_lora_embeddings.parquet")
57
+ ap.add_argument("--out-qna", default="data/qna/all_lora_qna.jsonl")
58
+ ap.add_argument("--max-qna-per-repo", type=int, default=400,
59
+ help="cap per repo: evo alone has ~1000/repo, which would "
60
+ "swamp the prose QA and bias the head toward one task")
61
+ ap.add_argument("--max-target-chars", type=int, default=400)
62
+ args = ap.parse_args()
63
+
64
+ import glob as _glob
65
+ import pyarrow as pa
66
+
67
+ # ---------------- embeddings ----------------
68
+ ali = pq.read_table(args.aligned_emb)
69
+ ali_dim = len(ali.column("doc_embedding")[0].as_py())
70
+ print(f"aligned6: {ali.num_rows} repos, dim {ali_dim}")
71
+
72
+ rp_path = Path(args.repopeft_emb)
73
+ if not rp_path.exists():
74
+ print(f"!! missing {rp_path} -- run build_repo_multiview.py first",
75
+ file=sys.stderr)
76
+ sys.exit(1)
77
+ rp = pq.read_table(rp_path)
78
+ rp_dim = len(rp.column("doc_embedding")[0].as_py())
79
+ print(f"repopeft: {rp.num_rows} repos, dim {rp_dim}")
80
+ if rp_dim != ali_dim:
81
+ print(f"!! dim mismatch {rp_dim} != {ali_dim}; the head cannot consume both",
82
+ file=sys.stderr)
83
+ sys.exit(1)
84
+
85
+ # Which split does each RepoPeftBench repo belong to? Derived from the QA
86
+ # files it appears in, so we inherit the benchmark's own repo partition.
87
+ repo_split: dict[str, str] = {}
88
+ for f in sorted(_glob.glob(args.repopeft_glob)):
89
+ stem = Path(f).stem
90
+ if stem not in SPLIT_MAP:
91
+ continue
92
+ doc_split, _ = SPLIT_MAP[stem]
93
+ ids = set(pq.read_table(f, columns=["repo_id"]).column("repo_id").to_pylist())
94
+ for r in ids:
95
+ # An eval assignment always wins: if a repo is used to hold out
96
+ # cross-repo performance anywhere, it must never be trained on.
97
+ if repo_split.get(r) in ("cr_val", "cr_test"):
98
+ continue
99
+ repo_split[r] = doc_split
100
+
101
+ rp_ids = rp.column("doc_id").to_pylist()
102
+ rp_embs = rp.column("doc_embedding").to_pylist()
103
+ ali_ids = set(ali.column("doc_id").to_pylist())
104
+
105
+ out_ids, out_ver, out_split, out_cat, out_emb = [], [], [], [], []
106
+ for c, col in (("doc_id", out_ids), ("doc_version", out_ver),
107
+ ("split", out_split), ("category", out_cat)):
108
+ if c in ali.column_names:
109
+ col.extend(ali.column(c).to_pylist())
110
+ else:
111
+ col.extend(["v1"] * ali.num_rows if c == "doc_version"
112
+ else ["aligned6"] * ali.num_rows)
113
+ out_emb.extend(ali.column("doc_embedding").to_pylist())
114
+
115
+ added = 0
116
+ for rid, emb in zip(rp_ids, rp_embs):
117
+ if rid in ali_ids: # already covered by aligned6
118
+ continue
119
+ sp = repo_split.get(rid)
120
+ if sp is None: # embedded but no QA -> useless
121
+ continue
122
+ out_ids.append(rid)
123
+ out_ver.append("head")
124
+ out_split.append(sp)
125
+ out_cat.append("repopeftbench")
126
+ out_emb.append(emb)
127
+ added += 1
128
+ print(f"merged embeddings: {len(out_ids)} repos (+{added} from RepoPeftBench)")
129
+ print(" split counts:", dict(Counter(out_split)))
130
+
131
+ pq.write_table(pa.table({
132
+ "doc_id": out_ids, "doc_version": out_ver, "split": out_split,
133
+ "category": out_cat, "doc_embedding": out_emb,
134
+ }), args.out_emb)
135
+
136
+ # ---------------- QA ----------------
137
+ have_emb = set(out_ids)
138
+ split_of = dict(zip(out_ids, out_split))
139
+ per_repo: dict[str, int] = defaultdict(int)
140
+ n_written = 0
141
+ src_counts: Counter = Counter()
142
+
143
+ with open(args.out_qna, "w") as out:
144
+ # aligned6 first, verbatim
145
+ with open(args.aligned_qna) as f:
146
+ for line in f:
147
+ line = line.strip()
148
+ if not line:
149
+ continue
150
+ out.write(line + "\n")
151
+ n_written += 1
152
+ src_counts["aligned6"] += 1
153
+
154
+ for f in sorted(_glob.glob(args.repopeft_glob)):
155
+ stem = Path(f).stem
156
+ if stem not in SPLIT_MAP:
157
+ continue
158
+ _, qna_split = SPLIT_MAP[stem]
159
+ t = pq.read_table(f, columns=["repo_id", "prefix", "target"])
160
+ rid_c = t.column("repo_id").to_pylist()
161
+ pre_c = t.column("prefix").to_pylist()
162
+ tgt_c = t.column("target").to_pylist()
163
+ kept = 0
164
+ for rid, pre, tgt in zip(rid_c, pre_c, tgt_c):
165
+ if rid not in have_emb:
166
+ continue
167
+ if not pre or not tgt:
168
+ continue
169
+ if len(tgt) > args.max_target_chars:
170
+ continue
171
+ if per_repo[rid] >= args.max_qna_per_repo:
172
+ continue
173
+ per_repo[rid] += 1
174
+ out.write(json.dumps({
175
+ "doc_id": rid,
176
+ "doc_version": "head",
177
+ "split": split_of[rid],
178
+ "qna_split": qna_split,
179
+ "question": "",
180
+ "prefix": pre,
181
+ "target": tgt,
182
+ }) + "\n")
183
+ kept += 1
184
+ n_written += 1
185
+ src_counts[Path(f).parent.parent.name + "/" + stem] += kept
186
+
187
+ print(f"\nmerged QA: {n_written} rows -> {args.out_qna}")
188
+ for k, v in src_counts.most_common():
189
+ print(f" {k:<34} {v}")
190
+ print(f"\nrepos with QA: {len(per_repo)}")
191
+
192
+
193
+ if __name__ == "__main__":
194
+ main()