minimind/scripts/aggregate_experiment_report.py

279 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import argparse
import csv
import json
import os
from collections import OrderedDict
TRAIN_FINAL_KEYS = [
"loss",
"logits_loss",
"aux_loss",
"reward",
"f1",
"em",
"cite",
"format",
"kl_ref",
"avg_len",
"lr",
]
EVAL_KEYS = [
"answer_f1",
"exact_match",
"citation_precision",
"citation_recall",
"citation_valid",
"format_score",
"reward_total",
]
def fmt(value, digits=4):
if value is None or value == "":
return ""
if isinstance(value, float):
return f"{value:.{digits}f}"
return str(value)
def load_json(path):
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def ensure_run(runs, name):
if name not in runs:
runs[name] = OrderedDict(
[
("experiment_name", name),
("stage", ""),
("backend", ""),
("model_profile", ""),
("attention_type", ""),
("hidden_size", ""),
("num_layers", ""),
("world_size", ""),
("per_gpu_batch", ""),
("accumulation_steps", ""),
("effective_batch", ""),
("max_seq_len", ""),
("tokens_per_optimizer_step", ""),
("peak_memory_gb", ""),
("last_step", ""),
]
)
return runs[name]
def merge_train_summary(runs, obj):
name = obj.get("experiment_name") or os.path.splitext(os.path.basename(obj.get("source_log", "run")))[0]
row = ensure_run(runs, name)
setup = obj.get("setup", {})
summary = obj.get("summary", {})
final = summary.get("final", {})
best = summary.get("best", {})
row["stage"] = setup.get("stage", row.get("stage", ""))
row["backend"] = setup.get("distributed", row.get("backend", ""))
row["model_profile"] = setup.get("model_profile", row.get("model_profile", ""))
row["attention_type"] = setup.get("attention_type", row.get("attention_type", ""))
row["hidden_size"] = setup.get("hidden_size", row.get("hidden_size", ""))
row["num_layers"] = setup.get("num_layers", row.get("num_layers", ""))
row["world_size"] = setup.get("world_size", row.get("world_size", ""))
row["per_gpu_batch"] = setup.get("per_gpu", row.get("per_gpu_batch", ""))
row["accumulation_steps"] = setup.get("accumulation_steps", row.get("accumulation_steps", ""))
row["effective_batch"] = setup.get("effective_batch", row.get("effective_batch", ""))
row["max_seq_len"] = setup.get("max_seq_len", row.get("max_seq_len", ""))
row["tokens_per_optimizer_step"] = setup.get("tokens_per_optimizer_step", row.get("tokens_per_optimizer_step", ""))
row["peak_memory_gb"] = obj.get("peak_memory_gb") or row.get("peak_memory_gb", "")
row["last_step"] = summary.get("last_step", row.get("last_step", ""))
for key in TRAIN_FINAL_KEYS:
if key in final:
row[f"train_final_{key}"] = final[key]
if key in best:
row[f"train_best_{key}"] = best[key]
def merge_eval_summary(runs, obj):
name = obj.get("experiment_name") or os.path.splitext(os.path.basename(obj.get("pred", "eval")))[0]
row = ensure_run(runs, name)
metrics = obj.get("metrics", obj)
row["eval_num_samples"] = obj.get("num_samples", "")
row["eval_num_bad_cases"] = obj.get("num_bad_cases", "")
for key in EVAL_KEYS:
if key in metrics:
row[f"eval_{key}"] = metrics[key]
def merge_input(runs, path):
obj = load_json(path)
kind = obj.get("kind", "")
if kind == "train_log_summary":
merge_train_summary(runs, obj)
elif kind == "search_shortqa_eval":
merge_eval_summary(runs, obj)
elif "summary" in obj and "setup" in obj:
merge_train_summary(runs, obj)
else:
merge_eval_summary(runs, obj)
def metric_delta(row, baseline, key):
value = row.get(key)
base = baseline.get(key) if baseline else None
if not isinstance(value, (int, float)) or not isinstance(base, (int, float)):
return ""
return value - base
def collect_fieldnames(rows):
preferred = [
"experiment_name",
"stage",
"backend",
"model_profile",
"attention_type",
"hidden_size",
"num_layers",
"world_size",
"per_gpu_batch",
"accumulation_steps",
"effective_batch",
"max_seq_len",
"tokens_per_optimizer_step",
"peak_memory_gb",
"last_step",
"train_final_loss",
"train_best_loss",
"train_final_reward",
"train_best_reward",
"train_final_f1",
"train_best_f1",
"train_final_cite",
"train_best_cite",
"train_final_format",
"train_final_kl_ref",
"eval_answer_f1",
"eval_exact_match",
"eval_citation_valid",
"eval_format_score",
"eval_reward_total",
]
all_keys = {key for row in rows for key in row.keys()}
return [key for key in preferred if key in all_keys] + sorted(all_keys - set(preferred))
def write_csv(path, rows, fieldnames):
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
with open(path, "w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for row in rows:
writer.writerow(row)
def markdown_table(rows, fieldnames):
header = "| " + " | ".join(fieldnames) + " |"
sep = "| " + " | ".join(["---"] * len(fieldnames)) + " |"
lines = [header, sep]
for row in rows:
lines.append("| " + " | ".join(fmt(row.get(key)) for key in fieldnames) + " |")
return "\n".join(lines)
def write_markdown(path, title, rows, fieldnames, baseline_name):
baseline = next((row for row in rows if row.get("experiment_name") == baseline_name), None)
report_fields = [key for key in fieldnames if key in {
"experiment_name",
"stage",
"backend",
"model_profile",
"attention_type",
"hidden_size",
"world_size",
"effective_batch",
"peak_memory_gb",
"train_final_loss",
"train_final_reward",
"train_final_f1",
"train_final_cite",
"eval_answer_f1",
"eval_exact_match",
"eval_citation_valid",
"eval_format_score",
"eval_reward_total",
}]
lines = [
f"# {title}",
"",
"## Summary",
"",
markdown_table(rows, report_fields),
"",
]
if baseline:
lines.extend(["## Deltas Vs Baseline", ""])
delta_fields = ["experiment_name"]
for key in ("eval_answer_f1", "eval_citation_valid", "eval_format_score", "eval_reward_total", "train_final_reward", "peak_memory_gb"):
if any(key in row for row in rows):
delta_fields.append(f"delta_{key}")
delta_rows = []
for row in rows:
delta_row = {"experiment_name": row.get("experiment_name", "")}
for field in delta_fields[1:]:
key = field.replace("delta_", "", 1)
delta_row[field] = metric_delta(row, baseline, key)
delta_rows.append(delta_row)
lines.extend([markdown_table(delta_rows, delta_fields), ""])
lines.extend(
[
"## Resume Metric Candidates",
"",
"- DeepSpeed/显存:填写 `peak_memory_gb`,对比 DDP baseline 的 `delta_peak_memory_gb`,可写峰值显存下降比例。",
"- GRPO/效果:填写 `eval_answer_f1`、`eval_citation_valid`、`eval_format_score`,对比 SFT baseline 的提升。",
"- MLA/效率:结合 `scripts/estimate_searchlm_scale.py` 和推理 benchmark,写 KV Cache 压缩比例、吞吐或延迟变化。",
"",
]
)
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
def main():
parser = argparse.ArgumentParser(description="Aggregate training/evaluation JSON summaries into a resume-ready report")
parser.add_argument("--inputs", nargs="+", required=True, help="JSON files from parse_train_log.py or eval_search_shortqa.py")
parser.add_argument("--output_md", default="reports/search_shortqa_experiment_report.md")
parser.add_argument("--output_csv", default="reports/search_shortqa_experiment_report.csv")
parser.add_argument("--title", default="SearchShortQA Experiment Report")
parser.add_argument("--baseline", default="", help="Experiment name used for delta calculation")
args = parser.parse_args()
runs = OrderedDict()
for path in args.inputs:
merge_input(runs, path)
rows = list(runs.values())
fieldnames = collect_fieldnames(rows)
if args.output_csv:
write_csv(args.output_csv, rows, fieldnames)
if args.output_md:
write_markdown(args.output_md, args.title, rows, fieldnames, args.baseline)
print(f"Aggregated {len(rows)} experiments")
if args.output_md:
print(f"Markdown report: {args.output_md}")
if args.output_csv:
print(f"CSV table: {args.output_csv}")
if __name__ == "__main__":
main()