"""
=============================================================================
CS349E · 第 16 讲 · 03_verify_with_simulation.py —— 用模拟器检验规划器：解析模型哪里会错
=============================================================================

【这个脚本要回答的问题】
    02 脚本的方案来自解析模型。它们在"真的"有排队、请求长短不一、prefill 和 decode 互相干扰的系统里还满足 SLO 吗？
    用第 5 讲的离散事件模拟器（serving_sim）检验，检验不过就加实例（planner.repair）。
      演示 1：一个失败的教训——"写代码的智能体"场景里，按平均提示长度筛选出的方案在模拟器里不达标，有一个加多少实例都没用。
      演示 2：四个场景：解析模型挑的前几个方案 → 模拟检验 → 修复，最后选的方案和成本。
      演示 3：解析模型说的容量 vs 模拟器里量到的最大流量。

【关于数字】模拟器的每步时间仍然是 roofline 模型（第 5 讲 StepModel），所以这里检验的是"排队、长短不一、干扰"这些解析模型没算的东西，
    不是硬件的真实性能。SLO 达成率按中间 60% 的请求统计（第 5 讲的做法），目标是 90%。
【运行】python3 03_verify_with_simulation.py      （约 25 秒）
【依赖】numpy；本目录的 planner；../lecture01 的 console；第 5 讲的 serving_sim（经 planner 导入）。
"""

import os
import sys

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

from planner import SCENARIOS, plan, repair, simulate, with_counts  # noqa: E402
from serving_sim import max_rate  # noqa: E402
from console import print_header, print_observations, print_table  # noqa: E402

TOP = 6


def demo1_lesson():
    sc = SCENARIOS["agent"]
    print_header(f"演示 1：一个失败的教训——{sc.name}（提示平均 {sc.prompt:,}，P90 {sc.prompt_quantile(0.9):,}；TTFT ≤ {sc.ttft:g} s）")
    rows = []
    for tag, ta in (("按平均提示长度筛选", False), ("按 P90 提示长度筛选", True)):
        for p in plan(sc, tail_aware=ta)[:3]:
            r = simulate(p, sc, duration=300)
            q, _, _ = repair(p, sc)
            fix = "加机器也没用（判定不可行）" if q is None else (f"→ {q.gpus} 块 GPU，{q.usd_per_mtok:.3f} 美元/百万" if q.gpus != p.gpus else "不用修")
            rows.append([tag, p.label(), str(p.gpus), f"{r['attain']:.0%}", f"{r['ttft_p90']:.2f} s", f"{r['tpot_p90'] * 1e3:.0f} ms", fix])
    print_table(["筛选", "方案", "GPU", "SLO 达成率", "TTFT P90", "TPOT P90", "修复"], rows)
    print_observations([
        "按平均长度筛选的前三个方案，在模拟器里都不达标：TTFT 的 P90 远超 3 秒。SLO 是按 P90 定的，而 P90 的 TTFT 主要由**长**提示决定——"
        f"这个负载的提示长度是长尾的（平均 {sc.prompt:,}，P90 {sc.prompt_quantile(0.9):,}，最长的十几万）。",
        "其中 H200 单卡的方案加多少实例都救不回来：单卡、每步 256 个 token 的分块 prefill，把一个一万多 token 的提示 prefill 完就要好几秒——"
        "这不是排队的问题，加机器不能让单个请求变快。只有换更快的配置（更大的张量并行、更大的块）才行。",
        "按 P90 长度筛选去掉了这类不可行的方案；剩下的仍然要加一两个实例：解析模型的排队近似（M/D/1 + 尾部系数）对长短悬殊的请求偏乐观。"
        "这就是为什么 DistServe 的放置算法用模拟器而不是公式来算 goodput（第 5 讲）。",
    ])


def demo2_all():
    print_header(f"演示 2：四个场景——解析模型的前 {TOP} 个方案 → 模拟检验 → 修复 → 选最便宜的")
    rows, summary = [], {}
    for key, sc in SCENARIOS.items():
        ps = plan(sc, tail_aware=True)[:TOP]
        best = None
        n_fixed = n_bad = 0
        for p in ps:
            q, r, _ = repair(p, sc)
            if q is None:
                n_bad += 1
                continue
            n_fixed += q.gpus != p.gpus
            if best is None or q.usd_per_mtok < best[0].usd_per_mtok:
                best = (q, r, p)
        q, r, p = best
        summary[key] = (ps[0], q)
        rows.append([sc.name, ps[0].label(), f"{ps[0].usd_per_mtok:.3f}", q.label(), f"{q.usd_per_mtok:.3f}", f"{r['attain']:.0%}",
                     f"{r['ttft_p90']:.2f} s / {r['tpot_p90'] * 1e3:.0f} ms", f"{n_fixed} / {n_bad}"])
    print_table(["场景", "解析模型最便宜的方案", "解析成本", "检验 + 修复后选的方案", "实际成本", "达成率", "TTFT / TPOT 的 P90",
                 f"前 {TOP} 个里要加机器 / 不可行"], rows)
    worse = [SCENARIOS[k].name for k, (a, b) in summary.items() if b.usd_per_mtok > a.usd_per_mtok + 1e-9]
    print_observations([
        "聊天、离线批处理、低延迟这三个场景，解析模型挑的方案直接通过了检验；"
        + ("、".join(worse) + "的方案要加机器，实际成本比解析模型说的高。" if worse else "所有场景都直接通过。"),
        "规划器的正确用法是\"解析模型筛选 + 模拟（或者真机）验证\"：解析模型便宜，可以扫几百个组合；模拟器慢，但看得见排队和长尾。"
        "解析模型给出的成本是**下界**的估计，最后的数要以验证后的为准。",
    ])
    return summary


def demo3_capacity(summary):
    print_header("演示 3：解析模型说的容量 vs 模拟器里量到的最大流量（固定实例数，二分查找 SLO 达成率 ≥ 90% 的最大流量）")
    sc = SCENARIOS["batch"]
    p = summary["batch"][1]
    cap = p.capacity()
    rows, seen = [], []
    for dur in (None, 120, 300, 600):
        m = max_rate(lambda rate: simulate(p, sc, rate=rate, duration=dur)["attain"], lo=0.5, hi=4 * cap, iters=8)
        seen.append(m)
        rows.append(["固定 1,500 个请求" if dur is None else f"至少 {dur} 秒的到达", ("≥ " if m >= 4 * cap * 0.999 else "") + f"{m:.1f}"])
    print(f"先看模拟本身：{sc.name}（{p.label()}，解析容量 {cap:.1f} 请求/s），模拟多长才可信？")
    print_table(["模拟的长度", "量到的最大流量（请求/s）"], rows)
    print()
    rows = []
    for key in ("chat", "batch"):
        sc = SCENARIOS[key]
        p = summary[key][1]
        cap = p.capacity()
        m = max_rate(lambda rate: simulate(p, sc, rate=rate, duration=600)["attain"], lo=0.5, hi=4 * cap, iters=8)
        rows.append([sc.name, p.label(), f"{cap:.1f}", f"{m:.1f}", f"{m / cap:.2f}"])
    sc = SCENARIOS["agent"]
    p0 = summary["agent"][0]
    p_one = with_counts(p0, sc, [1 for _ in p0.instances])
    cap = p_one.capacity()
    m = max_rate(lambda rate: simulate(p_one, sc, rate=rate, duration=600)["attain"], lo=0.1, hi=4 * cap, iters=8)
    rows.append([sc.name + "（1 个实例）", p_one.label(), f"{cap:.2f}", f"{m:.2f}", f"{m / cap:.2f}"])
    print_table(["场景", "方案", "解析容量（请求/s）", "模拟的最大流量（600 秒）", "模拟 / 解析"], rows)
    print_observations([
        "模拟太短会把一个过载的系统误判成\"满足 SLO\"：离线批处理的 TTFT 限制是 60 秒，过载时队列要过很久才会长到 60 秒；"
        f"只模拟 1,500 个请求（十几秒的到达），量到的\"容量\"是解析值的 4 倍以上；模拟越长，量到的值越小（120 秒 {seen[1]:.1f}、300 秒 {seen[2]:.1f}、"
        f"600 秒 {seen[3]:.1f}），逐渐接近真正的稳态容量。CS349D 第 17 讲 §5.2 讲过同一件事：实验要跑到稳态。下面的比较都用 600 秒。",
        "比值接近 1 说明解析模型的估计大致可用；小于 1 说明它乐观（排队、长尾没算够，写代码的智能体最明显）；"
        "大于 1 说明它保守（它给 TPOT 留了 15% 的余量、TTFT 只用一半的预算）。同一个解析模型，在不同的负载上偏差的方向都可能不同——"
        "这正是\"必须验证\"的原因。",
        "做课程项目时也一样：模型（不管是解析的还是模拟的）用来理解和筛选，结论要用更接近真实的方法确认，并且报告两者的差距。",
    ])


if __name__ == "__main__":
    demo1_lesson()
    s = demo2_all()
    demo3_capacity(s)
