| |
| """lalmodel 质量探针 — 离线报告生成器 |
| |
| 数据源 (二选一): |
| 1. HF ckpts 仓: gasschina/lal-ckpts 的 probe_samples.jsonl (Space 每 200 步上传) |
| 2. 本地文件: --jsonl path/to/probe_samples.jsonl (冒烟/调试) |
| |
| 输出 (默认 /home/z/my-project/download/): |
| quality_report.md 质量时间线: eval_loss 趋势 + 各 prompt 生成样本演进 + 指标表 |
| quality_curve.png eval_loss / distinct-1,2 / loop / E[step] 曲线 |
| |
| 用法: |
| python3 scripts/probe_report.py # 拉 HF 数据生成报告 |
| python3 scripts/probe_report.py --jsonl local.jsonl # 本地数据 |
| """ |
| import os |
| import sys |
| import json |
| import argparse |
| import datetime |
|
|
| def _load_token() -> str: |
| """HF token: 优先 env, 兜底读仓内 hfkey.txt (secret 扫描不允许硬编码 token).""" |
| t = os.environ.get("HF_TOKEN", "") |
| if t: |
| return t |
| for p in [os.path.join(os.path.dirname(__file__), "..", "hfkey.txt"), |
| "/home/z/my-project/lalmodel-code/hfkey.txt"]: |
| if os.path.exists(p): |
| return open(p).read().strip() |
| return "" |
|
|
|
|
| HF_TOKEN = _load_token() |
| CKPT_REPO = "gasschina/lal-ckpts" |
| OUT_DIR = "/home/z/my-project/download" |
|
|
| Q_LABELS = ["你好", "什么是火?", "中国的首都是", "从前有一座山"] |
|
|
|
|
| def fetch_jsonl_from_hf() -> str: |
| """从 HF ckpts 仓下载 probe_samples.jsonl, 返回本地路径.""" |
| from huggingface_hub import hf_hub_download |
| p = hf_hub_download(repo_id=CKPT_REPO, filename="probe_samples.jsonl", |
| repo_type="model", token=HF_TOKEN, |
| local_dir="/home/z/my-project/run-smoke/probe_cache") |
| return p |
|
|
|
|
| def load_events(path: str): |
| """解析 jsonl → [event dict], 跳过坏行.""" |
| events = [] |
| with open(path, encoding="utf-8") as f: |
| for line in f: |
| line = line.strip() |
| if not line: |
| continue |
| try: |
| events.append(json.loads(line)) |
| except json.JSONDecodeError: |
| continue |
| events.sort(key=lambda e: e.get("global", e.get("step", 0))) |
| return events |
|
|
|
|
| def render_markdown(events, out_path): |
| now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M UTC") |
| lines = [] |
| lines.append("# lalmodel 质量探针报告\n") |
| lines.append(f"*生成时间: {now} · 数据点: {len(events)} 个探针事件 · " |
| f"每事件 = held-out eval + 4 固定 prompt 生成*\n") |
|
|
| |
| lines.append("## 1. 客观指标趋势 (held-out eval loss)\n") |
| lines.append("| global step | eval_loss | Δ vs 上一点 | train avg |") |
| lines.append("|---:|---:|---:|---:|") |
| prev = None |
| for e in events: |
| ev = e.get("eval_loss", -1) |
| d = f"{ev - prev:+.3f}" if prev is not None else "—" |
| lines.append(f"| {e.get('global', e.get('step'))} | {ev:.4f} | {d} | " |
| f"{e.get('train_avg', float('nan')):.3f} |" if "train_avg" in e else |
| f"| {e.get('global', e.get('step'))} | {ev:.4f} | {d} | — |") |
| if ev > 0: |
| prev = ev |
| lines.append("") |
|
|
| |
| lines.append("## 2. 生成样本时间线 (temp=0.2, top_k=8, 无人工润色)\n") |
| for qi in range(4): |
| lines.append(f"### Prompt {qi+1}: 「{Q_LABELS[qi]}」\n") |
| for e in events: |
| gens = e.get("gens", []) |
| if qi >= len(gens): |
| continue |
| g = gens[qi] |
| text = (g.get("text") or "").strip() |
| if len(text) > 120: |
| text = text[:117] + "..." |
| lines.append(f"- **step {e.get('global', e.get('step'))}** " |
| f"`n={g.get('n')} d1={g.get('d1', 0):.2f} d2={g.get('d2', 0):.2f} " |
| f"loop={g.get('loop', 0)} E={g.get('e_step', 0):.2f}`") |
| lines.append(f" > {text if text else '*(空输出)*'}") |
| lines.append("") |
|
|
| |
| lines.append("## 3. 指标说明\n") |
| lines.append("- **eval_loss**: 数据集末尾 4 样本 (训练随机抽样几乎不触碰) 的下一 token " |
| "CE, 与训练同款前向语义 → 跨步可比的客观曲线, 持续下降 = 模型在学习") |
| lines.append("- **d1 / d2**: 生成 token 的 distinct-1/2 (0~1, 越高越多样; " |
| "早期模型通常低 = 高重复)") |
| lines.append("- **loop**: 末端循环周期长度 (0 = 无循环; >0 表示末尾陷入 k-token 循环)") |
| lines.append("- **E**: PonderNet 思考深度 E[step] (生成路径); " |
| "**关键观察**: 是否随训练分化 — 难 token 想得深、易 token 想得浅") |
| lines.append("- **exit**: 早退 token 占比 (生成路径提前停机的比例)") |
| with open(out_path, "w", encoding="utf-8") as f: |
| f.write("\n".join(lines) + "\n") |
| print(f"[report] markdown -> {out_path}") |
|
|
|
|
| def render_chart(events, out_path): |
| import matplotlib.font_manager as fm |
| for fp in ["/usr/share/fonts/truetype/chinese/SarasaMonoSC-Regular.ttf", |
| "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"]: |
| if os.path.exists(fp): |
| fm.fontManager.addfont(fp) |
| import matplotlib |
| matplotlib.use("Agg") |
| import matplotlib.pyplot as plt |
| plt.rcParams["font.sans-serif"] = ["Sarasa Mono SC", "DejaVu Sans"] |
| plt.rcParams["axes.unicode_minus"] = False |
|
|
| steps = [e.get("global", e.get("step", 0)) for e in events] |
| ev = [e.get("eval_loss", float("nan")) for e in events] |
| d1s = [[g.get("d1", 0) for g in e.get("gens", [])] for e in events] |
| d2s = [[g.get("d2", 0) for g in e.get("gens", [])] for e in events] |
| lps = [[g.get("loop", 0) for g in e.get("gens", [])] for e in events] |
| es = [[g.get("e_step", 0) for g in e.get("gens", [])] for e in events] |
| avg1 = [sum(x) / len(x) if x else 0 for x in d1s] |
| avg2 = [sum(x) / len(x) if x else 0 for x in d2s] |
| avgl = [sum(x) / len(x) if x else 0 for x in lps] |
| avge = [sum(x) / len(x) if x else 0 for x in es] |
|
|
| fig, axes = plt.subplots(2, 2, figsize=(11, 7), constrained_layout=True) |
| ax = axes[0][0] |
| ax.plot(steps, ev, "o-", color="#c0392b", lw=1.8, ms=4) |
| ax.set_title("held-out eval loss (越低越好)") |
| ax.set_xlabel("global step"); ax.set_ylabel("CE loss"); ax.grid(alpha=0.3) |
|
|
| ax = axes[0][1] |
| ax.plot(steps, avg1, "o-", color="#2980b9", lw=1.8, ms=4, label="distinct-1") |
| ax.plot(steps, avg2, "s-", color="#8e44ad", lw=1.8, ms=4, label="distinct-2") |
| ax.set_title("生成多样性 (越高越不重复)") |
| ax.set_xlabel("global step"); ax.legend(); ax.grid(alpha=0.3) |
|
|
| ax = axes[1][0] |
| ax.plot(steps, avgl, "o-", color="#d35400", lw=1.8, ms=4) |
| ax.set_title("末端循环周期 (0=无循环, 越低越好)") |
| ax.set_xlabel("global step"); ax.set_ylabel("loop k"); ax.grid(alpha=0.3) |
|
|
| ax = axes[1][1] |
| ax.plot(steps, avge, "o-", color="#16a085", lw=1.8, ms=4, label="E[step]") |
| ax2 = ax.twinx() |
| exitr = [] |
| for e in events: |
| gs = e.get("gens", []) |
| exitr.append(sum(g.get("exit", 0) for g in gs) / len(gs) if gs else 0) |
| ax2.plot(steps, exitr, "^--", color="#7f8c8d", lw=1.2, ms=4, alpha=0.7) |
| ax.set_title("PonderNet 思考深度 (分化 = 循环思考生效)") |
| ax.set_xlabel("global step"); ax.set_ylabel("E[step]", color="#16a085") |
| ax2.set_ylabel("早退占比", color="#7f8c8d"); ax.grid(alpha=0.3) |
|
|
| fig.suptitle("lalmodel · CCI2 训练质量探针", fontsize=13) |
| fig.savefig(out_path, dpi=140) |
| plt.close(fig) |
| print(f"[report] chart -> {out_path}") |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--jsonl", help="本地 probe_samples.jsonl (默认拉 HF)") |
| ap.add_argument("--out", default=OUT_DIR) |
| args = ap.parse_args() |
|
|
| if args.jsonl: |
| path = args.jsonl |
| else: |
| try: |
| path = fetch_jsonl_from_hf() |
| except Exception as e: |
| print(f"[!] HF 拉取失败: {e}\n (probe_samples.jsonl 每 200 步才上传一次, " |
| f"刚部署时可能还没有)") |
| sys.exit(1) |
|
|
| events = load_events(path) |
| if not events: |
| print("[!] 0 个探针事件 — Space 还没跑满第一个 probe 周期 (100 步)") |
| sys.exit(1) |
|
|
| os.makedirs(args.out, exist_ok=True) |
| render_markdown(events, os.path.join(args.out, "quality_report.md")) |
| try: |
| render_chart(events, os.path.join(args.out, "quality_curve.png")) |
| except Exception as e: |
| print(f"[!] 图表生成失败 (报告不受影响): {e}") |
| print(f"[report] {len(events)} 个探针事件, 步数范围 " |
| f"{events[0].get('global', 0)}..{events[-1].get('global', 0)}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|