"""
=============================================================================
第 2 讲 · 03：Archon —— 把"怎样组合推理技巧"当作一个架构搜索问题
=============================================================================

对应讲义：lectures/lecture02_test_time_compute.md 第 5 节（Saad-Falcon et al. 2024）

-----------------------------------------------------------------------------
【问题】
-----------------------------------------------------------------------------
test-time 的技巧很多：多采样、投票、verifier 排序、融合（fuse）、批评、生成 unit test……
可用的模型也很多：便宜的小模型、昂贵的大模型。给定一个预算，怎样**组合**它们？
每个技巧的效果取决于模型和任务，技巧之间还会相互影响，手工设计很难找到最好的组合。

Archon 的做法：把一个推理系统看成一个"神经网络式"的多层结构（生成层 → 排序层 → 融合层……），
定义好可选的组件和参数，然后在开发集上**自动搜索**最好的配置（论文用的是贝叶斯优化）。

-----------------------------------------------------------------------------
【这里的简化版】
-----------------------------------------------------------------------------
可选的组件：
    生成器 ：1B / 10B / 100B 三个模型之一，采样 n 次（n ∈ {1, 4, 16}）
    最后一层：first（直接用第一个）/ vote（多数投票）/ rank（weighted verifier）/
              fuse（用某个模型逐步融合全部候选）/ rank + fuse（先用 verifier 挑出前 4 个，再融合）
成本（以"10B 模型生成一次"为 1 个单位）：
    生成：1B = 0.1，10B = 1，100B = 10（≈ 和参数量成正比）
    verifier 给一个候选打分：0.1
    融合：融合模型生成一次的成本 × (1 + 候选数 / 4)（要读入所有候选，输入更长）
在 100 道开发题上评估全部配置，对每个预算选出开发集上最好的配置，再到 100 道测试题上检验。

运行时间：约 3 秒（只用标准库）。
"""

import random

from tts_tools import (CHECK_TEMPERATURE, ToyLLM, fuse, majority_vote, make_problem_set, mean, pad,
                       select_weighted, verifier_score)

TEMPERATURE = 0.7
MODELS = {"1B": ToyLLM(1e9), "10B": ToyLLM(1e10), "100B": ToyLLM(1e11)}
COST = {"1B": 0.1, "10B": 1.0, "100B": 10.0}
VERIFY_COST = 0.1
REPEATS = 2


def config_cost(gen, n, final, fuser):
    """
    一个配置的成本。

    【参数】gen：生成器的名字；n：采样数；final：最后一层的类型；fuser：融合模型的名字（不融合时为 None）
    【返回】float（单位：10B 模型生成一次）
    """
    cost = COST[gen] * n
    if final in ("rank", "rank+fuse"):
        cost += VERIFY_COST * n
    if final in ("fuse", "rank+fuse"):
        n_in = min(n, 4) if final == "rank+fuse" else n
        cost += COST[fuser] * (1 + n_in / 4)
    return cost


def run_config(problem, rng, gen, n, final, fuser):
    """
    按配置解一道题，返回是否答对。

    【思路】先用生成器采样 n 个候选，再按最后一层的类型给出答案：
        first     ：第一个候选
        vote      ：出现最多的答案
        rank      ：weighted verifier（tts_tools.select_weighted）
        fuse      ：融合模型读入全部候选，逐步投票、必要时自己（以低温 0.3）算（tts_tools.fuse）
        rank+fuse ：先按 verifier 分数挑出前 4 个候选，再融合
    """
    cands = [MODELS[gen].generate(problem, rng, TEMPERATURE) for _ in range(n)]
    if final == "first":
        return cands[0].correct
    if final == "vote":
        answer, _ = majority_vote([c.answer for c in cands])
        return answer == problem.answer()
    if final == "rank":
        return select_weighted(problem, cands).correct
    if final == "rank+fuse":
        cands = sorted(cands, key=lambda c: -verifier_score(problem.pid, c.answer, c.correct))[:4]
    return fuse(MODELS[fuser], problem, cands, rng, CHECK_TEMPERATURE).correct   # 融合时"仔细"地算（低温）


def all_configs():
    """列出搜索空间里的全部配置：(生成器, n, 最后一层, 融合模型)。"""
    configs = []
    for gen in MODELS:
        configs.append((gen, 1, "first", None))
        for n in (4, 16):
            configs.append((gen, n, "vote", None))
            configs.append((gen, n, "rank", None))
            for fuser in MODELS:
                configs.append((gen, n, "fuse", fuser))
                configs.append((gen, n, "rank+fuse", fuser))
    return configs


def describe(config):
    gen, n, final, fuser = config
    text = f"{gen} × {n} → {final}"
    return text + f"（{fuser}）" if fuser else text


def main():
    rng = random.Random(51)
    problems = make_problem_set(random.Random(50), n_problems=200, min_steps=3, max_steps=8)
    dev, test = problems[:100], problems[100:]
    results = []
    for config in all_configs():
        dev_acc = mean(run_config(p, rng, *config) for p in dev for _ in range(REPEATS))
        test_acc = mean(run_config(p, rng, *config) for p in test for _ in range(REPEATS))
        results.append((config_cost(*config), dev_acc, test_acc, config))

    print("=" * 78)
    print(f"搜索空间：{len(results)} 个配置。几个有代表性的配置：")
    print("=" * 78)
    print(pad("配置", 34) + pad("成本", 8, "right") + pad("开发集", 9, "right") + pad("测试集", 9, "right"))
    for want in [("1B", 1, "first", None), ("10B", 1, "first", None), ("100B", 1, "first", None),
                 ("10B", 16, "vote", None), ("10B", 16, "rank", None), ("10B", 16, "fuse", "10B"),
                 ("10B", 16, "fuse", "100B"), ("10B", 16, "rank+fuse", "100B"),
                 ("1B", 16, "fuse", "10B"), ("1B", 16, "rank+fuse", "100B")]:
        cost, d, t, c = next(r for r in results if r[3] == want)
        print(pad(describe(c), 34) + pad(f"{cost:.1f}", 8, "right") + pad(f"{d:.3f}", 9, "right")
              + pad(f"{t:.3f}", 9, "right"))

    print("\n" + "=" * 78)
    print("按预算选配置：开发集上最好的配置 vs 预算内最好的\"单个模型采样一次\"")
    print("=" * 78)
    print(pad("预算", 6) + pad("搜索出的配置", 34) + pad("成本", 7, "right") + pad("开发集", 9, "right")
          + pad("测试集", 9, "right") + pad("单个模型", 11, "right"))
    singles = [r for r in results if r[3][2] == "first"]
    for budget in (1, 3, 10, 30, 100):
        affordable = [r for r in results if r[0] <= budget + 1e-9]
        best = max(affordable, key=lambda r: (r[1], -r[0]))
        single = max((r for r in singles if r[0] <= budget + 1e-9), key=lambda r: r[1])
        print(pad(str(budget), 6) + pad(describe(best[3]), 34) + pad(f"{best[0]:.1f}", 7, "right")
              + pad(f"{best[1]:.3f}", 9, "right") + pad(f"{best[2]:.3f}", 9, "right")
              + pad(f"{single[2]:.3f}", 11, "right"))

    gaps = [d - t for _, d, t, _ in results]
    winners = sorted(results, key=lambda r: -r[1])[:10]
    print(f"\n全部配置：开发集 − 测试集的平均差 {mean(gaps):+.3f}；"
          f"开发集前 10 名的平均差 {mean(d - t for _, d, t, _ in winners):+.3f}")

    print("\n观察：")
    print("  · 同一组 10B × 16 的候选，最后一层不同，结果差别很大：vote ≈ 和 10B 融合（0.6 左右）< rank ≈ 和 100B 融合")
    print("    （0.8 左右）< 先排序再用 100B 融合（开发集 0.875）。")
    print("  · 在这个\"答案可以验证\"的数学类任务上，verifier（rank）是最有用的单个组件。Archon 论文的发现相同：")
    print("    MATH 这类推理任务上 verifier 最有效，代码任务上 unit test 最有效；融合（fusion）的大收益出现在")
    print("    指令遵循这类开放式任务上 —— 那里没有对错分明的答案，\"综合多个回答的优点\"更有价值。")
    print("  · 融合的效果取决于融合模型本身：和生成器一样强的融合模型不比投票好，强的融合模型才有帮助；")
    print("    \"先排序、只融合前几名\"最好，这也是 Archon 在指令遵循任务上找到的最佳结构。")
    print("  · 候选太差时，什么组件都救不了：1B 的候选即使用 100B 排序再融合，也只有 0.5 左右。")
    print("  · 预算小时，最好的往往是\"预算内最大的模型采样一次\"；预算足够大时，组合技巧才明显胜出")
    print("    （预算 100：100B × 4 → rank 在测试集上 0.955，单个模型 0.825）。")
    print("  · 挑选偏差：开发集前 10 名在测试集上平均低一点点。预算 30 时，开发集上 100B × 1（0.815）")
    print("    以微弱优势胜过 10B × 16 → rank（0.805），但测试集上后者更好 —— 开发集上的\"接近平手\"由噪声决定。\n")


if __name__ == "__main__":
    main()
