Instructions to use moncefem/memory-lora-gemma4 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use moncefem/memory-lora-gemma4 with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
El-Mouden Moncif Claude Opus 5 commited on
Commit ·
4f41179
1
Parent(s): 9a6c537
Add RepoPeftBench corpus merge
Browse filesJoins 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 +321 -0
- app/scripts/test_repo_qa.py +151 -0
- runs/h200_best/head.best.pt +3 -0
- scripts/merge_repopeft_corpus.py +194 -0
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()
|