"""
=============================================================================
第 4 讲 · 代码 3：拟合一条规模定律，用小模型预测大模型——以及它会怎样失败
=============================================================================

对应讲义：lectures/lecture04_scaling_laws.md 第 7 节（以及 §6.2 的"方法 3"）

-----------------------------------------------------------------------------
【这个文件要回答的问题 What this file answers】
-----------------------------------------------------------------------------
规模定律的用处是**外推**：在小模型上量出曲线，预测还没训练的大模型。这个文件读取代码 02 的扫描结果
（没有缓存时先训练，约 2 分钟），在一个"知道真正的 E"的世界里检查各种拟合方式：

    演示 1：只拟合一个切片（固定 D 看 N，固定 N 看 D）：带下限的幂律找到的"下限"是不是 E？
    演示 2：Chinchilla 的方法 3——联合拟合 L(N, D) = E + A/N^α + B/D^β；拟合出的 E 和真值比；
            由拟合式推出"算力该怎样分给 N 和 D"。
    演示 3：外推：只用小模型（d ≤ 32）拟合，预测 d = 48、64 的损失；比较
            "单切片 + 下限"、"单切片纯幂律"、"联合拟合"三种做法的误差；纯幂律为什么一定会预测出低于熵的损失。
    演示 4：为什么外推这么脆弱：E 和 α 互相"抵消"；去掉一个模型重新拟合，指数变多少；和训练噪声比一比。

-----------------------------------------------------------------------------
【运行方式】
-----------------------------------------------------------------------------
    python3 03_fit_and_extrapolate.py      # 有代码 02 的缓存时约 3 秒；没有缓存时先训练（约 2 分钟）

需要 numpy、PyTorch；依赖本目录的 scaling_tools.py 和第 1 讲的 console.py（只读）。
"""

import math
import os
import sys

import numpy as np

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "lecture01"))

from console import pad, print_header                                       # noqa: E402
from scaling_tools import (DATA_SIZES, EXPONENT_GRID, WIDTHS, build_language, compute_optimal,  # noqa: E402
                           ensure_runs, fit_joint, fit_power_law, fit_power_law_with_floor, grid_specs,
                           grid_table, joint_predict)

D_MIN = 500_000          # 联合拟合只用 D ≥ 0.5M 的点：D = 0.25M（244 步）属于"小数据区"（代码 02 演示 2）
SMALL = (8, 16, 24, 32)  # "小模型"：用来拟合；d = 48、64 留作"还没训练的大模型"


def points(table, sizes, widths, d_min=D_MIN):
    """把网格里满足条件的格子摊平成 (N, D, L) 三个数组。"""
    N, D, L = [], [], []
    for d in widths:
        for x in DATA_SIZES:
            if x >= d_min:
                N.append(sizes[d][0])
                D.append(x)
                L.append(table[d][x])
    return np.array(N, float), np.array(D, float), np.array(L)


# =============================================================================
# 演示 1：单个切片
# =============================================================================
def demo_slices(E, table, sizes):
    print_header("演示 1：只拟合一个切片 —— 带下限的幂律找到的'下限'是不是 E？")
    D = DATA_SIZES[-1]
    Ns = [sizes[d][0] for d in WIDTHS]
    Ls = [table[d][D] for d in WIDTHS]
    f = fit_power_law_with_floor(Ns, Ls)
    A0, a0 = fit_power_law(Ns, Ls)
    pred_pure = [A0 * n ** (-a0) for n in Ns]
    rmse_pure = math.sqrt(np.mean((np.log(Ls) - np.log(pred_pure)) ** 2))
    print(f"(a) L(N)，固定 D = {D / 1e6:g}M，6 个宽度：")
    print(f"    带下限 L = E + A·N^(−α)：E = {f['E']:.4f}，α = {f['alpha']:.3f}，对数误差 rmse = {f['rmse']:.4f}")
    print(f"    纯幂律 L = A·N^(−α)：     α = {a0:.3f}，rmse = {rmse_pure:.4f}")

    d = WIDTHS[-1]
    Ds = list(DATA_SIZES)
    g = fit_power_law_with_floor(Ds, [table[d][x] for x in Ds])
    print(f"(b) L(D)，固定 d = {d}（N = {sizes[d][0]:,}），4 个数据量：")
    print(f"    带下限：E = {g['E']:.4f}，β = {g['alpha']:.3f}，rmse = {g['rmse']:.4f}")
    print(f"\n    真正的熵率 E = {E:.4f}")
    print("""
观察 Observations：
  · 两个切片拟合出的"下限"都比真正的 E 高。原因不是拟合坏了，而是问题问错了：
    固定 D 时，N → ∞ 的极限是"E + 数据不够造成的损失"，不是 E；固定 N 时，D → ∞ 的极限是"E + 容量不够造成的损失"。
    Kaplan et al. 的单变量定律 L(N) 是在"数据足够、提前停止"的条件下量的，正是为了避开这个问题。
  · 带下限的拟合比纯幂律好得多（rmse 小几倍）：在这个范围里，曲线明显在"弯向一个下限"。
  · 数据方向的指数（这里 β ≈ 1.6）比参数方向（α ≈ 0.84）大，和代码 02 演示 3 的局部斜率一致；
    真实 LLM 的指数小一个数量级（Kaplan：0.076 和 0.095）。""")
    return f


# =============================================================================
# 演示 2：联合拟合（Chinchilla 方法 3）
# =============================================================================
def demo_joint(E, table, sizes):
    print_header("演示 2：联合拟合 L(N, D) = E + A/N^α + B/D^β（Chinchilla 的方法 3）")
    N, D, L = points(table, sizes, WIDTHS)
    p = fit_joint(N, D, L)
    print(f"用 {len(L)} 个点（6 个宽度 × D ≥ {D_MIN / 1e6:g}M）拟合：")
    print(f"    E = {p['E']:.4f}（真值 {E:.4f}）   A = {p['A']:.1f}   α = {p['alpha']:.3f}   "
          f"B = {p['B']:.3g}   β = {p['beta']:.3f}   rmse = {p['rmse']:.4f}")
    print("\n残差（观测 − 拟合）：")
    print("  " + pad("宽度 d", 8) + "".join(pad(f"D={x / 1e6:g}M", 10) for x in DATA_SIZES if x >= D_MIN))
    for d in WIDTHS:
        row = [table[d][x] - float(joint_predict(p, sizes[d][0], x)) for x in DATA_SIZES if x >= D_MIN]
        print("  " + pad(d, 8) + "".join(pad(f"{r:+.4f}", 10) for r in row))
    a = p["beta"] / (p["alpha"] + p["beta"])
    print(f"\n由拟合式推出的计算最优分配：N_opt ∝ C^{a:.2f}，D_opt ∝ C^{1 - a:.2f}")
    print("  " + pad("算力 C", 10) + pad("N_opt", 10) + pad("D_opt", 12) + "token/参数")
    for C in (1e10, 1e11, 1e12):
        n, dd = compute_optimal(p, C)
        print("  " + pad(f"{C:.0e}", 10) + pad(f"{n:,.0f}", 10) + pad(f"{dd:,.0f}", 12) + f"{dd / n:.0f}")
    print(f"""
观察 Observations：
  · 用上两个方向的全部点，拟合出的 E = {p['E']:.3f}，比单切片的"下限"接近真值得多，但仍然低了 {E - p['E']:.3f}
    （约 {(E - p['E']) / E:.0%}）。我们最好的模型离 E 还有 0.1 以上，"损失最后停在哪里"只能靠外推猜。
    Chinchilla 拟合出的 E = 1.69 nats/token 同样是这样外推出来的——没有人知道它离"英语真正的熵"有多远。
  · 18 个残差里 14 个在 ±0.05 以内。最大的两个都在 d = 16 那一行（0.5M 处 +0.19、2M 处 −0.11）：
    d = 16 恰好是噪声最大的设定——只换初始化种子，0.5M 处的损失就差 0.13（代码 02 演示 4）。
    拟合式是光滑的，单次训练的噪声不是；残差的大小告诉你"这条定律最多能准到哪里"。
  · 由拟合式推出：N_opt ∝ C^{a:.2f}。在这个玩具里，数据的回报衰减得快（β 大），所以算力增加时更多地给参数；
    "token/参数"随算力下降。真实 LLM 上 Chinchilla 得到的是约 0.5 / 0.5、约 20 token/参数——
    数字不能搬，**方法**可以：同样的拟合、同样的推导，换一个世界就给出那个世界的答案。""")
    return p


# =============================================================================
# 演示 3：外推
# =============================================================================
def demo_extrapolate(E, table, sizes):
    print_header("演示 3：外推 —— 只用 d ≤ 32 的模型拟合，预测 d = 48、64")
    big = [d for d in WIDTHS if d not in SMALL]
    D = DATA_SIZES[-1]
    Ns = [sizes[d][0] for d in SMALL]
    Ls = [table[d][D] for d in SMALL]
    f = fit_power_law_with_floor(Ns, Ls)
    A0, a0 = fit_power_law(Ns, Ls)
    N, Dv, L = points(table, sizes, SMALL)
    q = fit_joint(N, Dv, L)
    print(f"(a) 三种拟合（都只用 d ≤ 32）：")
    print(f"    单切片 + 下限（D = 2M）：E = {f['E']:.4f}，α = {f['alpha']:.3f}")
    print(f"    单切片纯幂律（D = 2M）： α = {a0:.3f}")
    print(f"    联合拟合（{len(L)} 个点）：   E = {q['E']:.4f}，α = {q['alpha']:.3f}，β = {q['beta']:.3f}")
    print("\n(b) 预测 vs 实际（bit/字符）：")
    print("  " + pad("模型", 18) + pad("实际", 9) + pad("单切片+下限", 14) + pad("单切片纯幂律", 14) + "联合拟合")
    errs = {"floor": [], "pure": [], "joint": [], "joint_1M": []}
    for d in big:
        for x in DATA_SIZES:
            if x < D_MIN:
                continue
            n = sizes[d][0]
            yj = float(joint_predict(q, n, x))
            if x == D:
                yf = f["E"] + f["A"] * n ** (-f["alpha"])
                yp = A0 * n ** (-a0)
                errs["floor"].append(yf - table[d][x])
                errs["pure"].append(yp - table[d][x])
                cf, cp = f"{yf:.4f}", f"{yp:.4f}"
            else:
                cf, cp = "—", "—"
            errs["joint"].append(yj - table[d][x])
            if x >= 1_000_000:
                errs["joint_1M"].append(yj - table[d][x])
            print("  " + pad(f"d={d}, D={x / 1e6:g}M", 18) + pad(f"{table[d][x]:.4f}", 9) + pad(cf, 14) + pad(cp, 14)
                  + f"{yj:.4f}")
    mae = {k: float(np.mean(np.abs(v))) for k, v in errs.items()}
    print(f"\n    平均绝对误差（D = 2M 的两格）：单切片+下限 {mae['floor']:.3f}，单切片纯幂律 {mae['pure']:.3f}；"
          f"联合拟合：全部 6 格 {mae['joint']:.3f}，D ≥ 1M 的 4 格 {mae['joint_1M']:.3f}")

    print("\n(c) 再往外推：N 很大、数据也足够时，损失会到多少？（真正的下限是 E = {:.4f}）".format(E))
    print("  " + pad("N", 10) + pad("单切片纯幂律", 14) + "联合拟合（D = 1e9）")
    for n in (1e5, 1e6, 1e7, 1e8):
        print("  " + pad(f"{n:.0e}", 10) + pad(f"{A0 * n ** (-a0):.4f}", 14) + f"{float(joint_predict(q, n, 1e9)):.4f}")
    n_cross = (A0 / E) ** (1 / a0)
    print(f"    纯幂律在 N ≈ {n_cross:,.0f} 时就预测出低于熵率的损失——这在信息论上是不可能的（第 1 讲 §2.4）。")
    print("""
观察 Observations：
  · 单切片的两种做法错在相反的方向：带下限的拟合把 d ≤ 32 时"曲线变平"当成了"快到下限了"，预测偏高；
    纯幂律假设曲线永远是直线，预测偏低，而且很快会预测出比熵还低的损失——没有下限项的规模定律
    只能在一个有限的范围里用。
  · 联合拟合在 D ≥ 1M 的格子上误差只有百分之几 bit，和种子噪声相当；在 D = 0.5M 上明显偏高——
    那里训练还在快速变化（只有 488 步），大模型在那里的表现和小模型不一样，小模型的数据给不出它的形状。
    外推只在"和拟合数据同一个区域"里可靠。
  · (c) 联合拟合不会掉到熵以下太多，但它的下限（{:.3f}）也比真值低：N 很大时它预测的损失同样低于 E，
    只是错得少得多。"损失最终停在哪里"是外推里最不可靠的部分。
  · GPT-4 技术报告用"带不可约项的幂律 L(C) = aC^b + c"，从用不到 GPT-4 万分之一算力的模型预测了它的最终损失；
    Llama 3 用 IsoFLOP 实验（最大 1e22 FLOPs）外推到 3.8e25 FLOPs。他们的成功依赖于大范围、密集、
    超参数调好的小实验——我们只有 6 个宽度、跨 1.7 个数量级、每格一次训练。""".format(q["E"]))
    return f, q


# =============================================================================
# 演示 4：为什么外推这么脆弱
# =============================================================================
def fit_with_fixed_floor(x, y, E_fixed):
    """E 固定时拟合 y = E + A·x^(−α)：对每个 α 解出最好的 A（相对误差的最小二乘），挑对数误差最小的 α。"""
    x = np.asarray(x, float)
    y = np.asarray(y, float)
    best = None
    for a in EXPONENT_GRID:
        col = x ** (-a) / y
        A = float(((y - E_fixed) / y * col).sum() / (col ** 2).sum())
        if A <= 0:
            continue
        err = float(((np.log(y) - np.log(E_fixed + A * x ** (-a))) ** 2).sum())
        if best is None or err < best[0]:
            best = (err, A, float(a))
    return dict(A=best[1], alpha=best[2], rmse=math.sqrt(best[0] / len(x)))


def demo_fragile(E, table, sizes, runs):
    print_header("演示 4：为什么外推这么脆弱 —— E 和 α 互相抵消；少一个点，指数就变")
    D = DATA_SIZES[-1]
    Ns = [sizes[d][0] for d in WIDTHS]
    Ls = [table[d][D] for d in WIDTHS]
    print(f"(a) 把 L(N)（D = {D / 1e6:g}M）的下限 E 固定成不同的值，再拟合 A、α：")
    print("  " + pad("固定的 E", 14) + pad("拟合出的 α", 12) + pad("rmse", 9) + "预测 N = 1e6 时的 L")
    for Ef, name in ((0.0, "0（纯幂律）"), (0.4, "0.4"), (0.6, "0.6"), (E, f"{E:.4f}（真值）"), (0.85, "0.85")):
        g = fit_with_fixed_floor(Ns, Ls, Ef)
        print("  " + pad(name, 14) + pad(f"{g['alpha']:.3f}", 12) + pad(f"{g['rmse']:.4f}", 9)
              + f"{Ef + g['A'] * 1e6 ** (-g['alpha']):.4f}")

    print("\n(b) 联合拟合时去掉一个宽度（"
          "leave-one-out），参数变多少：")
    print("  " + pad("去掉", 8) + pad("E", 8) + pad("α", 8) + pad("β", 8) + "N_opt ∝ C^a 的 a")
    rows = []
    for drop in WIDTHS:
        keep = [d for d in WIDTHS if d != drop]
        N, Dv, L = points(table, sizes, keep)
        p = fit_joint(N, Dv, L)
        a = p["beta"] / (p["alpha"] + p["beta"])
        rows.append((p["E"], p["alpha"], p["beta"], a))
        print("  " + pad(f"d={drop}", 8) + pad(f"{p['E']:.3f}", 8) + pad(f"{p['alpha']:.3f}", 8)
              + pad(f"{p['beta']:.3f}", 8) + f"{a:.3f}")
    arr = np.array(rows)
    print("  范围：E {:.3f}–{:.3f}，α {:.3f}–{:.3f}，β {:.3f}–{:.3f}，a {:.3f}–{:.3f}".format(
        arr[:, 0].min(), arr[:, 0].max(), arr[:, 1].min(), arr[:, 1].max(), arr[:, 2].min(), arr[:, 2].max(),
        arr[:, 3].min(), arr[:, 3].max()))

    n16a = runs["L2_d16_D500000_S500000_seed0"]["loss"]
    n16b = runs["L2_d16_D500000_S500000_seed1"]["loss"]
    print(f"\n(c) 对照：d = 16、D = 0.5M 只换初始化种子，损失就从 {n16a:.4f} 变成 {n16b:.4f}（差 {abs(n16a - n16b):.3f}）。")
    print("""
观察 Observations：
  · (a) 除了 E = 0（纯幂律）明显更差，其余几行的 rmse 只差几个百分点，和种子噪声（代码 02 演示 4）
    同一个量级；α 却从 0.4 左右变到 0.8 左右，对 N = 1e6 的预测从 0.69 到 0.86。只看拟合范围内"拟合得好不好"，
    分辨不出哪一个是对的——
    这就是"范围窄的时候 E 和 α 互相抵消"（参数不可辨识）。这个切片上误差最小的反而是偏高的 E，
    因为它把"数据不够造成的那一块"也算进了下限（演示 1）。
  · (b) 只去掉 6 个宽度中的一个，β 在 1.15–1.56 之间变，a 在 0.66–0.73 之间变；E 在 0.64–0.72 之间——
    6 次拟合的 E 全都低于真值。Chinchilla 自己的三种方法给出 0.46–0.50；后来的复现
    （Besiroglu et al. 2024）发现方法 3 报告的置信区间窄得不合理。
  · (c) 训练本身的噪声和这些差别是同一个量级。真实的规模定律研究用几百次训练（Chinchilla 超过 400 个模型）、
    跨好几个数量级、为每个规模调好超参数，才把这些不确定性压下去。""")


def main():
    lang = build_language()
    E = lang.entropy_rate
    runs = ensure_runs(grid_specs())
    table, sizes = grid_table(runs)
    demo_slices(E, table, sizes)
    demo_joint(E, table, sizes)
    demo_extrapolate(E, table, sizes)
    demo_fragile(E, table, sizes, runs)


if __name__ == "__main__":
    main()
