taurusduan's picture
Duplicate from pfeifferj/DeepSeek-V4.1-Flash-GSQ-RCO-GGUF
0b10848
Raw
History Blame Contribute Delete
14.9 kB
#!/usr/bin/env python3
"""Strict, dependency-free analysis of the pinned Spark MMLU-Pro protocol.
Usage: python3 analyze_mmlu.py --config results/mmlu/analysis-config.json
All input paths are relative to the config file. No partial denominators,
duplicate overwrites, dropped rows, or implicit cross-model comparisons.
"""
import argparse
from collections import Counter, defaultdict
from datetime import datetime, timezone
import hashlib
import json
import math
from pathlib import Path
import sys
class ValidationError(ValueError):
pass
def require(condition, message):
if not condition:
raise ValidationError(message)
def sha256(path):
digest = hashlib.sha256()
with Path(path).open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def load_index(index_path, subset_path, expected_n=2000):
rows = json.loads(Path(index_path).read_text())
subset = json.loads(Path(subset_path).read_text())
require(isinstance(rows, list), "Task index must be a JSON list")
require(len(rows) == expected_n, f"Index has {len(rows)} tasks, expected {expected_n}")
require(subset.get("n") == expected_n, "Subset count differs from required task count")
index = {}
question_ids = []
for position, row in enumerate(rows):
require(isinstance(row, dict), f"Index row {position} is not an object")
for key in ("i", "question_id", "n_options", "answer_index"):
require(type(row.get(key)) is int, f"Index row {position}: {key} must be an integer")
i = row["i"]
require(i not in index, f"Duplicate task index {i}")
require(i == position, f"Index row {position} has task ID {i}: order must match prompts")
require(2 <= row["n_options"] <= 10, f"Task {i}: unsupported option count")
require(0 <= row["answer_index"] < row["n_options"], f"Task {i}: gold label out of range")
require(isinstance(row.get("category"), str) and row["category"], f"Task {i}: invalid category")
index[i] = row
question_ids.append(row["question_id"])
require(len(set(question_ids)) == expected_n, "Duplicate dataset question ID")
require(question_ids == subset.get("question_ids"), "Task question IDs/order differ from pinned subset")
categories = dict(sorted(Counter(r["category"] for r in rows).items()))
require(categories == subset.get("allocation"), "Category counts differ from pinned subset")
return index, subset
def load_scores(path, index):
"""Validate every row before returning any scores; accept rounded argmax ties."""
rows = {}
ties = []
path = Path(path)
with path.open(encoding="utf-8") as handle:
for line_number, line in enumerate(handle, 1):
where = f"{path.name}:{line_number}"
require(bool(line.strip()), f"{where}: blank row")
fields = line.rstrip("\r\n").split("\t")
require(len(fields) >= 5, f"{where}: expected task, prediction, gold and option scores")
try:
i, pred, gold = map(int, fields[:3])
logprobs = tuple(map(float, fields[3:]))
except ValueError as error:
raise ValidationError(f"{where}: malformed numeric field") from error
require(i in index, f"{where}: unknown task index {i}")
require(i not in rows, f"{where}: duplicate task index {i}")
task = index[i]
n_options = task["n_options"]
require(len(logprobs) == n_options, f"{where}: {len(logprobs)} scores, expected {n_options}")
require(gold == task["answer_index"], f"{where}: gold label {gold} differs from index")
require(0 <= pred < n_options, f"{where}: prediction out of range")
require(all(math.isfinite(x) for x in logprobs), f"{where}: nonfinite logprob")
require(max(logprobs) <= 0, f"{where}: positive logprob")
require(sum(math.exp(x) for x in logprobs) <= 1.00001, f"{where}: option probabilities exceed one")
maximum = max(logprobs)
require(logprobs[pred] == maximum, f"{where}: prediction is not an argmax of recorded logprobs")
if logprobs.count(maximum) > 1:
ties.append(i)
rows[i] = {"prediction": pred, "gold": gold, "logprobs": logprobs}
missing = sorted(set(index) - set(rows))
require(not missing, f"{path.name}: missing {len(missing)} tasks; first missing IDs: {missing[:10]}")
require(len(rows) == len(index), f"{path.name}: invalid total task count")
return rows, {
"passed": True,
"n_rows": len(rows),
"unique_indices": len(rows),
"complete_index_coverage": True,
"gold_labels_match": True,
"option_counts_match": True,
"finite_nonpositive_logprobs": True,
"prediction_is_recorded_argmax": True,
"rounded_argmax_tie_indices": ties,
"tie_note": "TSV has rounded logprobs; prediction must attain the recorded maximum. Exact pre-rounding tie-breaking cannot be recovered.",
}
def summarize(rows, index):
n = len(rows)
correct = sum(r["prediction"] == r["gold"] for r in rows.values())
accuracy = correct / n
by_category = defaultdict(list)
for i in rows:
by_category[index[i]["category"]].append(i)
per_category = {}
for category, ids in sorted(by_category.items()):
c = sum(rows[i]["prediction"] == rows[i]["gold"] for i in ids)
p = c / len(ids)
per_category[category] = {
"n": len(ids), "correct": c, "accuracy_pct": 100 * p,
"binomial_se_pp": 100 * math.sqrt(p * (1 - p) / len(ids)),
}
return {
"n": n, "correct": correct, "accuracy_pct": 100 * accuracy,
"binomial_se_pp": 100 * math.sqrt(accuracy * (1 - accuracy) / n),
"per_category": per_category,
"prediction_counts": dict(sorted(Counter(str(r["prediction"]) for r in rows.values()).items())),
"gold_counts": dict(sorted(Counter(str(r["gold"]) for r in rows.values()).items())),
}
def exact_mcnemar(a_only, b_only):
require(type(a_only) is int and a_only >= 0, "a_only must be a nonnegative integer")
require(type(b_only) is int and b_only >= 0, "b_only must be a nonnegative integer")
n = a_only + b_only
if not n:
return 1.0
# Exact integer arithmetic before final conversion: no factorial overflow.
numerator = 2 * sum(math.comb(n, k) for k in range(min(a_only, b_only) + 1))
return min(1.0, numerator / (1 << n))
def compare(a, b):
require(set(a) == set(b), "Paired comparison requires identical complete task IDs")
n = len(a)
both = a_only = b_only = neither = agree = 0
max_logprob_difference = 0.0
for i in a:
require(a[i]["gold"] == b[i]["gold"], f"Paired task {i}: mismatched gold labels")
require(len(a[i]["logprobs"]) == len(b[i]["logprobs"]), f"Paired task {i}: mismatched options")
ac = a[i]["prediction"] == a[i]["gold"]
bc = b[i]["prediction"] == b[i]["gold"]
both += ac and bc
a_only += ac and not bc
b_only += bc and not ac
neither += not ac and not bc
agree += a[i]["prediction"] == b[i]["prediction"]
max_logprob_difference = max(max_logprob_difference, *(abs(x-y) for x, y in zip(a[i]["logprobs"], b[i]["logprobs"])))
discordant = a_only + b_only
delta = (a_only - b_only) / n
return {
"n": n,
"delta_definition": "accuracy(a) minus accuracy(b)",
"delta_pp": 100 * delta,
"paired_se_pp": 100 * math.sqrt(max(0.0, discordant / n - delta * delta) / (n - 1)) if n > 1 else None,
"paired_se_definition": "SE of per-question correctness differences using sample variance (n-1)",
"mcnemar_null_se_pp": 100 * math.sqrt(discordant) / n,
"both_correct": both, "a_only_correct": a_only, "b_only_correct": b_only, "neither_correct": neither,
"prediction_agreement_count": agree,
"prediction_agreement_pct": 100 * agree / n,
"correctness_agreement_count": both + neither,
"correctness_agreement_pct": 100 * (both + neither) / n,
"max_abs_logprob_difference": max_logprob_difference,
"mcnemar_exact_two_sided_p": exact_mcnemar(a_only, b_only),
"p_value_adjustment": "none; pairwise descriptive comparisons",
}
def markdown(report):
out = ["# MMLU-Pro evaluation results", "", f"Generated {report['generated_at']}. Every included TSV passed strict validation on all {report['dataset']['n']:,} pinned questions.", "",
"Zero-shot raw prompt; no chat template or reasoning generation. One prompt decode, vocabulary log-softmax at the single-token continuations ` A` through ` J`, argmax over available options.", "",
f"Uniform random choice accuracy averaged over actual option counts: **{report['dataset']['chance_floor_pct']:.4f}%**. Accuracy SE is binomial; this is a fixed stratified subset, and SE does not include runtime or dataset-selection uncertainty.", "",
"| Run | Status | Correct | Accuracy | Binomial SE |", "|---|---|---:|---:|---:|"]
for name, run in report["runs"].items():
s = run["summary"]
out.append(f"| {name} | {run['status']} | {s['correct']}/{s['n']} | {s['accuracy_pct']:.2f}% | {s['binomial_se_pp']:.2f} pp |")
if report["comparisons"]:
out += ["", "Every delta below is **A minus B**. Prediction agreement compares chosen option indices; correctness agreement is recorded separately in JSON. McNemar p-values are exact, two-sided and unadjusted.", "",
"| A | B | Context | Delta | Paired SE | A-only / B-only correct | Prediction agreement | McNemar p |", "|---|---|---|---:|---:|---:|---:|---:|"]
for pair in report["comparisons"]:
s = pair["statistics"]
out.append(f"| {pair['a']} | {pair['b']} | {pair['classification']} | {s['delta_pp']:+.2f} pp | {s['paired_se_pp']:.2f} pp | {s['a_only_correct']} / {s['b_only_correct']} | {s['prediction_agreement_pct']:.2f}% | {s['mcnemar_exact_two_sided_p']:.6g} |")
out += ["", "## Per-category accuracy", "", "Category estimates have small denominators; do not select a runtime using these scores.", "",
"| Category | n | " + " | ".join(report["runs"]) + " |",
"|---|---:|" + "---:|" * len(report["runs"])]
for category, n in report["dataset"]["category_counts"].items():
values = [f"{run['summary']['per_category'][category]['accuracy_pct']:.2f}%" for run in report["runs"].values()]
out.append(f"| {category} | {n} | " + " | ".join(values) + " |")
out += ["", "## Interpretation and provenance", ""]
out.extend(f"- {note}" for note in report["notes"])
for pair in report["comparisons"]:
if pair.get("interpretation"):
out.append(f"- {pair['a']} versus {pair['b']}: {pair['interpretation']}")
out += ["", "Input SHA256 hashes, model provenance, validation flags, prediction counts, paired cells and category counts are in the companion JSON. Valid TSV structure does not establish model/runtime equivalence.", ""]
return "\n".join(out)
def analyze(config_path):
config_path = Path(config_path).resolve()
config = json.loads(config_path.read_text())
base = config_path.parent
index_path = base / config["index"]
subset_path = base / config["subset"]
index, subset = load_index(index_path, subset_path)
report = {
"schema_version": 1,
"generated_at": datetime.now(timezone.utc).isoformat(),
"analysis_script_sha256": sha256(__file__),
"config_sha256": sha256(config_path),
"dataset": {
"id": subset["dataset"], "split": subset["split"], "n": len(index), "seed": subset["seed"],
"source_parquet_sha256": subset["file_sha256"],
"task_index_sha256": sha256(index_path), "subset_sha256": sha256(subset_path),
"category_counts": dict(sorted(Counter(r["category"] for r in index.values()).items())),
"option_count_distribution": dict(sorted(Counter(r["n_options"] for r in index.values()).items())),
"chance_floor_pct": 100 * sum(1 / r["n_options"] for r in index.values()) / len(index),
},
"runs": {}, "comparisons": [], "notes": config.get("notes", []),
}
scores = {}
require(bool(config["runs"]), "At least one run is required")
for run in config["runs"]:
name = run["name"]
require(isinstance(name, str) and name, "Run name must be a nonempty string")
require(name not in scores, f"Duplicate run name: {name}")
require(run.get("status") in ("current", "historical"), f"Run {name}: set status current or historical")
path = base / run["path"]
rows, validation = load_scores(path, index)
scores[name] = rows
report["runs"][name] = {
"status": run["status"], "source_path": run["path"], "source_sha256": sha256(path),
"provenance": run.get("provenance", {}), "validation": validation, "summary": summarize(rows, index),
}
for pair in config.get("comparisons", []):
a, b = pair["a"], pair["b"]
require(a in scores and b in scores and a != b, f"Invalid comparison: {a} versus {b}")
require(bool(pair.get("classification")), f"Comparison {a} versus {b} needs a classification")
historical = any(report["runs"][name]["status"] == "historical" for name in (a, b))
require(not historical or "historical" in pair["classification"], f"Comparison {a} versus {b} must be labeled historical")
report["comparisons"].append({**pair, "contains_historical_run": historical, "statistics": compare(scores[a], scores[b])})
return report
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--config", type=Path, required=True)
parser.add_argument("--output-prefix", type=Path, help="Default: analysis beside config")
args = parser.parse_args()
try:
report = analyze(args.config)
except (ValidationError, KeyError, TypeError, OSError, json.JSONDecodeError) as error:
print(f"VALIDATION FAILED: {error}", file=sys.stderr)
return 1
prefix = args.output_prefix or args.config.parent / "analysis"
prefix.parent.mkdir(parents=True, exist_ok=True)
# Do not write a success report until every run and comparison has validated.
json_path = prefix.with_suffix(".json")
md_path = prefix.with_suffix(".md")
json_path.write_text(json.dumps(report, indent=2, allow_nan=False) + "\n")
md_path.write_text(markdown(report))
print(f"Validated {len(report['runs'])} run(s), {report['dataset']['n']} questions each: {json_path}, {md_path}")
return 0
if __name__ == "__main__":
sys.exit(main())