404 lines
17 KiB
Python
404 lines
17 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""Audit and summarize the matched shared-gate vs KV-head-gate sweep."""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import argparse
|
|||
|
|
import csv
|
|||
|
|
import glob
|
|||
|
|
import hashlib
|
|||
|
|
import json
|
|||
|
|
import subprocess
|
|||
|
|
from collections import defaultdict
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
import matplotlib.pyplot as plt
|
|||
|
|
|
|||
|
|
|
|||
|
|
SUITES = ("RULER-local-full13", "BabiLong", "HELMET-ICL", "MRCR")
|
|||
|
|
THRESHOLDS = (0.45, 0.50, 0.525, 0.55, 0.575, 0.60, 0.625, 0.65)
|
|||
|
|
ARMS = ("token", "token_kv_head")
|
|||
|
|
SLUGS = {
|
|||
|
|
"RULER-local-full13": "ruler",
|
|||
|
|
"BabiLong": "babilong",
|
|||
|
|
"HELMET-ICL": "helmet",
|
|||
|
|
"MRCR": "mrcr",
|
|||
|
|
}
|
|||
|
|
SCORE_DEFINITIONS = {
|
|||
|
|
"RULER-local-full13": "unweighted macro of lm-eval 8192 string-match scores over the local 13-config set",
|
|||
|
|
"BabiLong": "unweighted macro exact-match accuracy over qa1-qa5",
|
|||
|
|
"HELMET-ICL": "unweighted macro label exact-match over five ICL configurations",
|
|||
|
|
"MRCR": "unweighted macro of prefix-check plus SequenceMatcher scores over 2/4/8-needle configurations",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def threshold_slug(value: float) -> str:
|
|||
|
|
return {
|
|||
|
|
0.45: "045",
|
|||
|
|
0.50: "050",
|
|||
|
|
0.525: "0525",
|
|||
|
|
0.55: "055",
|
|||
|
|
0.575: "0575",
|
|||
|
|
0.60: "060",
|
|||
|
|
0.625: "0625",
|
|||
|
|
0.65: "065",
|
|||
|
|
}[value]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def one(pattern: str) -> Path:
|
|||
|
|
paths = [Path(path) for path in glob.glob(pattern, recursive=True)]
|
|||
|
|
if len(paths) != 1:
|
|||
|
|
raise RuntimeError(f"expected one path for {pattern}, found {paths}")
|
|||
|
|
return paths[0]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def read_jsonl(path: Path) -> list[dict]:
|
|||
|
|
return [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def collect_scores(root: Path) -> dict[tuple[str, str], dict]:
|
|||
|
|
rows: dict[tuple[str, str], dict] = {}
|
|||
|
|
for split in ("a", "b"):
|
|||
|
|
result = one(str(root / f"ruler8k_splits/{split}/lm_eval/**/results_*.json"))
|
|||
|
|
payload = json.loads(result.read_text())
|
|||
|
|
for config, metrics in payload["results"].items():
|
|||
|
|
if config in payload["n-samples"]:
|
|||
|
|
rows[("RULER-local-full13", config)] = {
|
|||
|
|
"score": float(metrics["8192,none"]),
|
|||
|
|
"n": int(payload["n-samples"][config]["effective"]),
|
|||
|
|
}
|
|||
|
|
result = one(str(root / "babilong8k_qa1_qa5_n50/**/results_*.json"))
|
|||
|
|
payload = json.loads(result.read_text())
|
|||
|
|
for config, metrics in payload["results"].items():
|
|||
|
|
if config in payload["n-samples"]:
|
|||
|
|
rows[("BabiLong", config)] = {
|
|||
|
|
"score": float(metrics["acc,none"]),
|
|||
|
|
"n": int(payload["n-samples"][config]["effective"]),
|
|||
|
|
}
|
|||
|
|
for suite, filename in (
|
|||
|
|
("HELMET-ICL", "helmet_icl8k_n50.jsonl"),
|
|||
|
|
("MRCR", "mrcr_8k_2_4_8needle_n10.jsonl"),
|
|||
|
|
):
|
|||
|
|
grouped: dict[str, list[dict]] = defaultdict(list)
|
|||
|
|
for row in read_jsonl(root / filename):
|
|||
|
|
grouped[row["config"]].append(row)
|
|||
|
|
for config, values in grouped.items():
|
|||
|
|
rows[(suite, config)] = {
|
|||
|
|
"score": sum(float(row["score"]) for row in values) / len(values),
|
|||
|
|
"n": len(values),
|
|||
|
|
}
|
|||
|
|
return rows
|
|||
|
|
|
|||
|
|
|
|||
|
|
def sample_signatures(root: Path, score_rows: dict[tuple[str, str], dict]) -> dict[str, str]:
|
|||
|
|
signatures: dict[str, str] = {}
|
|||
|
|
for suite, rel_dirs in (
|
|||
|
|
("RULER-local-full13", ("ruler8k_splits/a/lm_eval", "ruler8k_splits/b/lm_eval")),
|
|||
|
|
("BabiLong", ("babilong8k_qa1_qa5_n50",)),
|
|||
|
|
):
|
|||
|
|
configs = [config for row_suite, config in score_rows if row_suite == suite]
|
|||
|
|
for config in configs:
|
|||
|
|
matches = []
|
|||
|
|
for rel_dir in rel_dirs:
|
|||
|
|
matches.extend(root.glob(f"{rel_dir}/**/samples_{config}_*.jsonl"))
|
|||
|
|
if len(matches) != 1:
|
|||
|
|
raise RuntimeError(f"expected one sample log for {suite}/{config}: {matches}")
|
|||
|
|
values = [
|
|||
|
|
(
|
|||
|
|
row.get("doc_id"),
|
|||
|
|
row.get("doc_hash"),
|
|||
|
|
row.get("prompt_hash"),
|
|||
|
|
row.get("target_hash"),
|
|||
|
|
)
|
|||
|
|
for row in read_jsonl(matches[0])
|
|||
|
|
]
|
|||
|
|
signatures[f"{suite}/{config}"] = hashlib.sha256(
|
|||
|
|
json.dumps(values, sort_keys=True).encode()
|
|||
|
|
).hexdigest()
|
|||
|
|
for suite, filename in (
|
|||
|
|
("HELMET-ICL", "helmet_icl8k_n50.jsonl"),
|
|||
|
|
("MRCR", "mrcr_8k_2_4_8needle_n10.jsonl"),
|
|||
|
|
):
|
|||
|
|
grouped: dict[str, list[tuple]] = defaultdict(list)
|
|||
|
|
for row in read_jsonl(root / filename):
|
|||
|
|
grouped[row["config"]].append(
|
|||
|
|
(row["row_id"], row.get("prompt_sha256"), row.get("target"))
|
|||
|
|
)
|
|||
|
|
for config, values in grouped.items():
|
|||
|
|
signatures[f"{suite}/{config}"] = hashlib.sha256(
|
|||
|
|
json.dumps(values, sort_keys=True).encode()
|
|||
|
|
).hexdigest()
|
|||
|
|
return signatures
|
|||
|
|
|
|||
|
|
|
|||
|
|
def read_sparsity(root: Path, suite: str) -> dict:
|
|||
|
|
if suite == "RULER-local-full13":
|
|||
|
|
parts = [
|
|||
|
|
json.loads((root / f"sparsity_ruler_{split}.json").read_text())
|
|||
|
|
for split in ("a", "b")
|
|||
|
|
]
|
|||
|
|
sparse = sum(int(part["sparse_decisions"]) for part in parts)
|
|||
|
|
total = sum(int(part["total_decisions"]) for part in parts)
|
|||
|
|
granularities = {part.get("router_granularity") for part in parts}
|
|||
|
|
native = sum(int(part.get("native_router_decisions", 0)) for part in parts)
|
|||
|
|
phases: dict[str, dict[str, int]] = defaultdict(lambda: {"sparse": 0, "total": 0})
|
|||
|
|
for part in parts:
|
|||
|
|
for phase, values in part.get("by_phase", {}).items():
|
|||
|
|
phases[phase]["sparse"] += int(values["sparse_decisions"])
|
|||
|
|
phases[phase]["total"] += int(values["total_decisions"])
|
|||
|
|
else:
|
|||
|
|
part = json.loads((root / f"sparsity_{SLUGS[suite]}.json").read_text())
|
|||
|
|
sparse = int(part["sparse_decisions"])
|
|||
|
|
total = int(part["total_decisions"])
|
|||
|
|
granularities = {part.get("router_granularity")}
|
|||
|
|
native = int(part.get("native_router_decisions", 0))
|
|||
|
|
phases = {
|
|||
|
|
phase: {
|
|||
|
|
"sparse": int(values["sparse_decisions"]),
|
|||
|
|
"total": int(values["total_decisions"]),
|
|||
|
|
}
|
|||
|
|
for phase, values in part.get("by_phase", {}).items()
|
|||
|
|
}
|
|||
|
|
return {
|
|||
|
|
"sparse_decisions": sparse,
|
|||
|
|
"total_decisions": total,
|
|||
|
|
"sparsity": sparse / total,
|
|||
|
|
"full_attention_usage": 1.0 - sparse / total,
|
|||
|
|
"router_granularities": sorted(value for value in granularities if value),
|
|||
|
|
"native_router_decisions": native or None,
|
|||
|
|
"by_phase": phases,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def sha256(path: Path) -> str:
|
|||
|
|
digest = hashlib.sha256()
|
|||
|
|
with path.open("rb") as handle:
|
|||
|
|
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
|||
|
|
digest.update(block)
|
|||
|
|
return digest.hexdigest()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def command_output(command: list[str], cwd: Path) -> str:
|
|||
|
|
try:
|
|||
|
|
return subprocess.run(
|
|||
|
|
command, cwd=cwd, check=True, text=True, capture_output=True
|
|||
|
|
).stdout.strip()
|
|||
|
|
except (OSError, subprocess.CalledProcessError):
|
|||
|
|
return "unavailable in downloaded HF snapshot"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main() -> None:
|
|||
|
|
parser = argparse.ArgumentParser()
|
|||
|
|
parser.add_argument(
|
|||
|
|
"--repo", type=Path, default=Path("/data/sjm/AHA/AHA-Qwen3")
|
|||
|
|
)
|
|||
|
|
parser.add_argument(
|
|||
|
|
"--eval-root",
|
|||
|
|
type=Path,
|
|||
|
|
default=Path("/data/sjm/AHA/AHA-Qwen3/experiments/qwen3_1p7b_router_granularity_20260714/eval"),
|
|||
|
|
)
|
|||
|
|
parser.add_argument(
|
|||
|
|
"--baseline-root",
|
|||
|
|
type=Path,
|
|||
|
|
default=Path("/data/sjm/AHA/AHA-Qwen3/experiments/qwen3_1p7b_fourbench_8k_broad_20260713/vanilla"),
|
|||
|
|
)
|
|||
|
|
parser.add_argument("--output-dir", type=Path)
|
|||
|
|
parser.add_argument("--input-dir", type=Path)
|
|||
|
|
args = parser.parse_args()
|
|||
|
|
output = args.output_dir or args.eval_root.parent / "summary"
|
|||
|
|
output.mkdir(parents=True, exist_ok=True)
|
|||
|
|
|
|||
|
|
baseline = collect_scores(args.baseline_root)
|
|||
|
|
baseline_signatures = sample_signatures(args.baseline_root, baseline)
|
|||
|
|
curve_rows = []
|
|||
|
|
per_config = []
|
|||
|
|
points = []
|
|||
|
|
alignment = {}
|
|||
|
|
done_files = ("ruler_a.DONE", "ruler_b.DONE", "babilong.DONE", "helmet.DONE", "mrcr.DONE")
|
|||
|
|
|
|||
|
|
for arm in ARMS:
|
|||
|
|
for threshold in THRESHOLDS:
|
|||
|
|
method = f"{arm}_t{threshold_slug(threshold)}"
|
|||
|
|
root = args.eval_root / method
|
|||
|
|
missing = [name for name in done_files if not (root / name).exists()]
|
|||
|
|
if missing:
|
|||
|
|
raise RuntimeError(f"incomplete {method}: missing {missing}")
|
|||
|
|
scores = collect_scores(root)
|
|||
|
|
if set(scores) != set(baseline):
|
|||
|
|
raise RuntimeError(f"configuration mismatch for {method}")
|
|||
|
|
if any(scores[key]["n"] != baseline[key]["n"] for key in baseline):
|
|||
|
|
raise RuntimeError(f"sample-count mismatch for {method}")
|
|||
|
|
signatures = sample_signatures(root, scores)
|
|||
|
|
mismatches = sorted(
|
|||
|
|
key for key in baseline_signatures if signatures.get(key) != baseline_signatures[key]
|
|||
|
|
)
|
|||
|
|
alignment[method] = {"aligned": not mismatches, "mismatches": mismatches}
|
|||
|
|
if mismatches:
|
|||
|
|
raise RuntimeError(f"prompt/target hash mismatch for {method}: {mismatches}")
|
|||
|
|
|
|||
|
|
suite_rows = []
|
|||
|
|
sparse_sum = total_sum = 0
|
|||
|
|
for suite in SUITES:
|
|||
|
|
keys = sorted(key for key in baseline if key[0] == suite)
|
|||
|
|
baseline_score = sum(baseline[key]["score"] for key in keys) / len(keys)
|
|||
|
|
score = sum(scores[key]["score"] for key in keys) / len(keys)
|
|||
|
|
sparsity = read_sparsity(root, suite)
|
|||
|
|
expected_granularity = arm
|
|||
|
|
if sparsity["router_granularities"] != [expected_granularity]:
|
|||
|
|
raise RuntimeError(
|
|||
|
|
f"{method}/{suite} recorded {sparsity['router_granularities']}, "
|
|||
|
|
f"expected {[expected_granularity]}"
|
|||
|
|
)
|
|||
|
|
sparse_sum += sparsity["sparse_decisions"]
|
|||
|
|
total_sum += sparsity["total_decisions"]
|
|||
|
|
row = {
|
|||
|
|
"arm": arm,
|
|||
|
|
"router_granularity": arm,
|
|||
|
|
"threshold": threshold,
|
|||
|
|
"benchmark": suite,
|
|||
|
|
"config_count": len(keys),
|
|||
|
|
"samples_per_config": ",".join(
|
|||
|
|
str(value) for value in sorted({baseline[key]["n"] for key in keys})
|
|||
|
|
),
|
|||
|
|
"total_samples": sum(baseline[key]["n"] for key in keys),
|
|||
|
|
"tuned_vanilla_score": baseline_score,
|
|||
|
|
"quality_floor_95pct": 0.95 * baseline_score,
|
|||
|
|
"score": score,
|
|||
|
|
"retention": score / baseline_score if baseline_score else None,
|
|||
|
|
"quality_pass": score >= 0.95 * baseline_score,
|
|||
|
|
**{key: sparsity[key] for key in (
|
|||
|
|
"sparse_decisions", "total_decisions", "sparsity",
|
|||
|
|
"full_attention_usage", "native_router_decisions", "by_phase"
|
|||
|
|
)},
|
|||
|
|
}
|
|||
|
|
curve_rows.append(row)
|
|||
|
|
suite_rows.append(row)
|
|||
|
|
for key in keys:
|
|||
|
|
per_config.append({
|
|||
|
|
"arm": arm,
|
|||
|
|
"threshold": threshold,
|
|||
|
|
"benchmark": suite,
|
|||
|
|
"config": key[1],
|
|||
|
|
"n": baseline[key]["n"],
|
|||
|
|
"tuned_vanilla_score": baseline[key]["score"],
|
|||
|
|
"score": scores[key]["score"],
|
|||
|
|
})
|
|||
|
|
points.append({
|
|||
|
|
"arm": arm,
|
|||
|
|
"threshold": threshold,
|
|||
|
|
"all_suite_quality_pass": all(row["quality_pass"] for row in suite_rows),
|
|||
|
|
"decision_weighted_sparsity": sparse_sum / total_sum,
|
|||
|
|
"full_attention_usage": 1.0 - sparse_sum / total_sum,
|
|||
|
|
"sparse_decisions": sparse_sum,
|
|||
|
|
"total_decisions": total_sum,
|
|||
|
|
"suites": suite_rows,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
headline = {}
|
|||
|
|
for arm in ARMS:
|
|||
|
|
valid = [
|
|||
|
|
point for point in points
|
|||
|
|
if point["arm"] == arm and point["all_suite_quality_pass"]
|
|||
|
|
]
|
|||
|
|
if valid:
|
|||
|
|
best = max(valid, key=lambda point: point["decision_weighted_sparsity"])
|
|||
|
|
headline[arm] = {"found": True, **best}
|
|||
|
|
else:
|
|||
|
|
headline[arm] = {
|
|||
|
|
"found": False,
|
|||
|
|
"statement": "No measured threshold preserved at least 95% of tuned vanilla on every suite.",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
input_dir = args.input_dir or args.baseline_root.parent / "inputs"
|
|||
|
|
frozen_inputs = [
|
|||
|
|
input_dir / "helmet_icl_8k_n50_per_config.jsonl",
|
|||
|
|
input_dir / "mrcr_8k_2_4_8needle_n10_per_config.jsonl",
|
|||
|
|
]
|
|||
|
|
payload = {
|
|||
|
|
"protocol": {
|
|||
|
|
"model": "Qwen3-1.7B tuned vanilla",
|
|||
|
|
"context_length": 8192,
|
|||
|
|
"arms": {
|
|||
|
|
"token": "L2A-style shared-gate: one native gate per token/layer",
|
|||
|
|
"token_kv_head": "AHA: one native gate per token/KV-head/layer",
|
|||
|
|
},
|
|||
|
|
"local_attention": {"sink_tokens": 64, "recent_tokens": 256},
|
|||
|
|
"inference": "strict AHA routing in both prefill and decode; no force-full heads or full-decode fallback",
|
|||
|
|
"thresholds": list(THRESHOLDS),
|
|||
|
|
"quality_rule": "every suite macro score >= 95% of aligned tuned-vanilla macro score",
|
|||
|
|
"headline_rule": "highest decision-weighted measured sparsity among thresholds passing every suite",
|
|||
|
|
"sparsity_definition": "hard local routes / token x KV-head x layer effective decisions",
|
|||
|
|
"suite_scope": "RULER local full13 means this repository's fixed 13-config set, not complete upstream RULER",
|
|||
|
|
},
|
|||
|
|
"score_definitions": SCORE_DEFINITIONS,
|
|||
|
|
"headline": headline,
|
|||
|
|
"points": points,
|
|||
|
|
"curves": curve_rows,
|
|||
|
|
"per_config": per_config,
|
|||
|
|
"alignment": {"all_aligned": all(value["aligned"] for value in alignment.values()), "methods": alignment},
|
|||
|
|
"appendix_note": (
|
|||
|
|
"Upstream L2A results are not merged into this table because architecture, "
|
|||
|
|
"training data, objective, and evaluation protocols differ."
|
|||
|
|
),
|
|||
|
|
"reproducibility": {
|
|||
|
|
"git_commit": command_output(["git", "rev-parse", "HEAD"], args.repo),
|
|||
|
|
"git_status_short": command_output(["git", "status", "--short"], args.repo),
|
|||
|
|
"container": "dmtd-repro",
|
|||
|
|
"container_image": command_output(
|
|||
|
|
["docker", "inspect", "-f", "{{.Config.Image}}", "dmtd-repro"], args.repo
|
|||
|
|
),
|
|||
|
|
"baseline_root": str(args.baseline_root),
|
|||
|
|
"eval_root": str(args.eval_root),
|
|||
|
|
"frozen_input_sha256": {
|
|||
|
|
str(path): sha256(path) for path in frozen_inputs if path.exists()
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
(output / "router_granularity_results.json").write_text(
|
|||
|
|
json.dumps(payload, indent=2) + "\n"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
flat_curve_rows = [
|
|||
|
|
{key: value for key, value in row.items() if key != "by_phase"}
|
|||
|
|
for row in curve_rows
|
|||
|
|
]
|
|||
|
|
with (output / "quality_sparsity_curves.csv").open("w", newline="") as handle:
|
|||
|
|
writer = csv.DictWriter(handle, fieldnames=list(flat_curve_rows[0]))
|
|||
|
|
writer.writeheader()
|
|||
|
|
writer.writerows(flat_curve_rows)
|
|||
|
|
with (output / "per_config_scores.csv").open("w", newline="") as handle:
|
|||
|
|
writer = csv.DictWriter(handle, fieldnames=list(per_config[0]))
|
|||
|
|
writer.writeheader()
|
|||
|
|
writer.writerows(per_config)
|
|||
|
|
|
|||
|
|
figure, axes = plt.subplots(2, 2, figsize=(12, 9), constrained_layout=True)
|
|||
|
|
colors = {"token": "#d62728", "token_kv_head": "#1f77b4"}
|
|||
|
|
labels = {"token": "L2A-style shared-gate", "token_kv_head": "AHA KV-head gate"}
|
|||
|
|
for axis, suite in zip(axes.flat, SUITES):
|
|||
|
|
for arm in ARMS:
|
|||
|
|
values = sorted(
|
|||
|
|
(row for row in curve_rows if row["arm"] == arm and row["benchmark"] == suite),
|
|||
|
|
key=lambda row: row["sparsity"],
|
|||
|
|
)
|
|||
|
|
axis.plot(
|
|||
|
|
[100 * row["sparsity"] for row in values],
|
|||
|
|
[100 * row["retention"] for row in values],
|
|||
|
|
marker="o", color=colors[arm], label=labels[arm],
|
|||
|
|
)
|
|||
|
|
for row in values:
|
|||
|
|
axis.annotate(f"{row['threshold']:.3g}", (100 * row["sparsity"], 100 * row["retention"]), fontsize=7)
|
|||
|
|
axis.axhline(95, color="black", linestyle="--", linewidth=1)
|
|||
|
|
axis.set_title(suite)
|
|||
|
|
axis.set_xlabel("Measured effective sparsity (%)")
|
|||
|
|
axis.set_ylabel("Retention vs tuned vanilla (%)")
|
|||
|
|
axis.grid(alpha=0.25)
|
|||
|
|
axes.flat[0].legend()
|
|||
|
|
figure.suptitle("Qwen3-1.7B matched router-granularity quality–sparsity curves")
|
|||
|
|
figure.savefig(output / "quality_sparsity_curves.png", dpi=180)
|
|||
|
|
plt.close(figure)
|
|||
|
|
print(json.dumps(headline, indent=2))
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|