llm: first real-LLM prototype — recombining specialist LLMs (C2/C4)
First step from toy models toward real language models, on one 16 GB GPU. New src/llm/ package: procedural task families + exact-match verifier (tasks.py), batched eval (evaluate.py), LoRA specialisation (specialise.py, manual answer-only SFT), weight-space merge via peft add_weighted_adapter (merge.py: soup = averaged deltas, ties = sign-reconciled union), runner (experiment.py, kind llm_merge). Base Qwen2.5-0.5B-Instruct (Apache-2.0); three disjoint hard families (lists/strings/arith); one LoRA specialist each (~90s total). Result (seed 1), reported honestly: - STRONG/robust: the merges are the ONLY models competent across ALL families -- worst-family ~0.25 vs <0.16 for every single specialist (the Fisher-Muller "generalist assembled from specialists" signature, in real LoRA weights). - MARGINAL: "exceeds every parent overall" is only marginal at this scale (soup 0.64 vs best specialist 0.63; ties 0.61 below it). - CAVEAT VISIBLE: averaging dilutes peaks (lists specialist 0.43 -> merge 0.26) -- Layer-1's "merge, don't average" (E4) appearing in real weights. The pipeline works end-to-end; the balance/retention half reproduces; the strict overall-exceeds and soup-vs-ties distinction need scale (bigger base, more/cleaner families, seeds, a dilution-resistant / offspring-selected merge) -- the HPC step. Env: Python 3.14 + transformers 5.13 works; note transformers-5.x apply_chat_template returns a dict. make env-llm / make llm; adapters under gitignored models/llm/, base in the HF cache. figures/plot_llm_merge.py, README, tests/test_llm.py (+3, 125 green). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6bca1db61e
commit
809e45a5e0
18 changed files with 702 additions and 2 deletions
81
figures/plot_llm_merge.py
Normal file
81
figures/plot_llm_merge.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
"""llm_merge figure — recombining specialist LLMs (blueprint C2/C4, the real-LLM image of E8).
|
||||
|
||||
LoRA specialists on disjoint task families are merged (weight-space) into one deployable model. The
|
||||
recombined model beats any single specialist overall and — the sharper signature — is competent
|
||||
across *all* families, which no single parent is. Two panels: (A) per-family accuracy for the base,
|
||||
each specialist, and the merges (each specialist spikes on its own family; the merges are high
|
||||
everywhere); (B) overall vs worst-family accuracy (the merges dominate both, especially worst-family).
|
||||
Reads only the committed bundle.
|
||||
|
||||
Usage: python figures/plot_llm_merge.py [results/llm_merge]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from _figlib import load_bundle, savefig # noqa: E402
|
||||
|
||||
_FAMS = ["lists", "strings", "arith"]
|
||||
|
||||
|
||||
def _acc(df, model, metric):
|
||||
r = df[(df["model"] == model) & (df["metric"] == metric)]["accuracy"]
|
||||
return float(r.iloc[0]) if len(r) else float("nan")
|
||||
|
||||
|
||||
def main(results_dir: str = "results/llm_merge") -> None:
|
||||
df, cfg = load_bundle(results_dir)
|
||||
specialists = sorted(m for m in df["model"].unique() if m.startswith("spec_"))
|
||||
merges = sorted(m for m in df["model"].unique() if m.startswith("merge_"))
|
||||
models = ["base"] + specialists + merges
|
||||
labels = {"base": "base", **{s: s.replace("spec_", "spec:") for s in specialists},
|
||||
**{m: m.replace("merge_", "merge:") for m in merges}}
|
||||
colors = {"base": "#7f7f7f"}
|
||||
for s in specialists:
|
||||
colors[s] = "#1f77b4"
|
||||
for m in merges:
|
||||
colors[m] = "#2ca02c"
|
||||
|
||||
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
|
||||
|
||||
# Panel A: per-family accuracy, grouped by model.
|
||||
ax = axes[0]
|
||||
x = np.arange(len(_FAMS))
|
||||
w = 0.8 / len(models)
|
||||
for i, mdl in enumerate(models):
|
||||
vals = [_acc(df, mdl, f) for f in _FAMS]
|
||||
ax.bar(x + (i - (len(models) - 1) / 2) * w, vals, w, label=labels[mdl],
|
||||
color=colors[mdl], alpha=0.9 if mdl.startswith("merge_") else 0.7)
|
||||
ax.set_xticks(x); ax.set_xticklabels(_FAMS)
|
||||
ax.set(ylabel="accuracy", title="Per-family: each specialist spikes on its own family; the merges\n"
|
||||
"(green) are competent everywhere (but averaging dilutes some peaks)")
|
||||
ax.legend(frameon=False, fontsize=8, ncol=2)
|
||||
|
||||
# Panel B: overall vs worst-family, per model.
|
||||
ax = axes[1]
|
||||
x2 = np.arange(len(models))
|
||||
for off, metric, hatch, lab in [(-0.2, "overall", "", "overall"),
|
||||
(0.2, "worst_family", "//", "worst family")]:
|
||||
ax.bar(x2 + off, [_acc(df, m, metric) for m in models], 0.38,
|
||||
color=[colors[m] for m in models], hatch=hatch, alpha=0.85, label=lab,
|
||||
edgecolor="white")
|
||||
ax.set_xticks(x2); ax.set_xticklabels([labels[m] for m in models], rotation=25, ha="right",
|
||||
fontsize=8)
|
||||
ax.set(ylabel="accuracy", title="Overall (solid) vs worst-family (hatched): the merge "
|
||||
"clearly wins\nworst-family (balance); overall it matches the best specialist")
|
||||
ax.legend(frameon=False, fontsize=9)
|
||||
|
||||
fig.suptitle("llm_merge — recombining decorrelated specialist LLMs gives the only model competent "
|
||||
f"across all families (balance); overall parity ({cfg['base_model']})", y=1.0, fontsize=12)
|
||||
fig.tight_layout()
|
||||
savefig(fig, results_dir, "llm_merge")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(*sys.argv[1:])
|
||||
Loading…
Add table
Add a link
Reference in a new issue