"""
=============================================================================
CS349E · 第 5 讲 · 03_queueing_prefill.py —— prefill 实例是一个 M/D/1 队列：排队、并行方式与利用率
=============================================================================

【这个脚本要回答的问题】
    解耦以后，prefill 实例只做一件事：来一个提示，算一个提示。DistServe §3.1 指出它就像排队论里的 M/D/1 队列
    （M：泊松到达；D：服务时间固定；1：一个服务台），于是可以用公式回答：
        - 请求速率越来越接近处理能力时，TTFT 为什么会爆炸？
        - 两块 GPU 做 prefill，用张量并行（intra-op，"一个请求算得更快"）还是流水线并行（inter-op，"同时算两个请求"）？

      演示 1：M/D/1 公式（论文公式 1）vs 模拟：平均 TTFT = 服务时间 + 排队时间。
      演示 2：两块 GPU：张量并行 vs 流水线并行（论文公式 2、3）——低速率时张量并行好，高速率时流水线好；交叉点和加速系数 K 有关。
      演示 3：利用率与尾延迟：为什么不能把 GPU 用到 100%；到达越"扎堆"，同样利用率下排队越长。

【怎么读输出】提示长度统一 512（和论文一样），不做批处理（一个提示就能吃满 GPU）。每步时间来自 serving_sim.StepModel（假设值）。
    结果是确定性的（固定种子），每次运行相同。

【运行】python3 03_queueing_prefill.py      （约 1 秒）
【依赖】numpy；同目录的 serving_sim；../lecture01 的 console、hw_specs、llm_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 fmt_time, print_header, print_observations, print_table  # noqa: E402
from hw_specs import H100  # noqa: E402
from llm_math import LLAMA31_8B, LLAMA31_70B  # noqa: E402
from serving_sim import StepModel, make_workload, run_prefill_only  # noqa: E402

N = 6000
L = 512


def mean_ttft(reqs, warmup=0.2, cooldown=0.2):
    """平均 TTFT（只统计中间那部分请求，理由同 serving_sim.attainment）。"""
    k0, k1 = int(len(reqs) * warmup), len(reqs) - int(len(reqs) * cooldown)
    return float(np.mean([r.ttft for r in reqs[k0:k1]]))


def tail_ttft(reqs, q, warmup=0.2, cooldown=0.2):
    k0, k1 = int(len(reqs) * warmup), len(reqs) - int(len(reqs) * cooldown)
    return float(np.percentile([r.ttft for r in reqs[k0:k1]], q))


def mdl_ttft(R, D):
    """论文公式 1（M/D/1）：平均 TTFT = D + R·D² / (2(1 − R·D))，要求 R·D < 1。"""
    return D + R * D * D / (2 * (1 - R * D))


def inter_op_ttft(R, D):
    """论文公式 2（两段流水线）：一个请求要 D，但每 D/2 就能进一个新的。= D + R·D² / (4(2 − R·D))。"""
    return D + R * D * D / (4 * (2 - R * D))


def intra_op_ttft(R, D, K):
    """论文公式 3（两块 GPU 张量并行，加速 K 倍，1 < K < 2）：= D/K + R·D² / (2K(K − R·D))。"""
    return D / K + R * D * D / (2 * K * (K - R * D))


# ---------------------------------------------------------------------------
# 演示 1：M/D/1
# ---------------------------------------------------------------------------
def demo1_md1():
    step = StepModel(LLAMA31_8B, H100)
    D = step.prefill_time([L])
    print_header(f"演示 1：M/D/1——一块 H100 做 Llama-3.1-8B 的 prefill，每个提示 {L} 个 token，服务时间 D = {fmt_time(D)}")
    rows = []
    for rho in (0.2, 0.5, 0.7, 0.8, 0.9, 0.95):
        R = rho / D
        sim = mean_ttft(run_prefill_only(make_workload(N, R, L, 1), step, max_tokens=L))
        f = mdl_ttft(R, D)
        rows.append([f"{rho:.2f}", f"{R:.1f}", fmt_time(D), fmt_time(f - D), fmt_time(f), fmt_time(sim)])
    print_table(["利用率 ρ = R·D", "速率 R（请求/秒）", "服务时间", "公式：排队", "公式：平均 TTFT", "模拟：平均 TTFT"], rows)
    print_observations([
        "ρ ≤ 0.8 时公式和模拟吻合：平均 TTFT = 服务时间 D + 排队时间 R·D² / (2(1 − ρ))。ρ = 0.9、0.95 时模拟偏低："
        "越接近满负荷，队列要越久才能'涨'到稳态，有限的模拟时间（几千个请求）看不到最坏的排队——公式说的是无限长时间的平均。",
        "排队时间在 ρ → 1 时趋于无穷：ρ = 0.5 时排队只有 D 的一半，ρ = 0.9 时是 4.5 倍，ρ = 0.95 时是 9.5 倍。"
        "**速率逼近处理能力时，延迟不是线性变差，而是爆炸**。",
        "所以 TTFT 的 SLO 决定了 prefill 实例能跑到多高的利用率：SLO 越紧，留的余量越大，需要的 prefill GPU 越多。",
    ])


# ---------------------------------------------------------------------------
# 演示 2：张量并行 vs 流水线并行
# ---------------------------------------------------------------------------
def demo2_intra_vs_inter():
    m = LLAMA31_70B
    one = StepModel(m, H100)                         # "假想的单卡"：整个模型在一块 GPU 上的时间（实际放不下，只用来定义 D）
    tp2 = StepModel(m, H100, tp=2)
    pp2 = StepModel(m, H100, pp=2)
    D = one.prefill_time([L])
    D_tp = tp2.prefill_time([L])
    K = D / D_tp
    print_header(f"演示 2：两块 H100 做 Llama-3.1-70B 的 prefill——张量并行（intra-op）vs 流水线并行（inter-op）")
    print(f"单卡时间 D = {fmt_time(D)}；张量并行一个请求 {fmt_time(D_tp)}（加速系数 K = {K:.2f}，不到 2：要做 all-reduce）；"
          f"流水线一个请求仍是 {fmt_time(pp2.prefill_time([L]))}，但每 {fmt_time(D / 2)} 就能进一个新的")
    rows = []
    for R in (2, 5, 8, 11, 13, 14, 15):
        if R * D >= K or R * D >= 2:
            continue
        s_tp = mean_ttft(run_prefill_only(make_workload(N, R, L, 1), tp2, max_tokens=L))
        s_pp = mean_ttft(run_prefill_only(make_workload(N, R, L, 1), pp2, max_tokens=L))
        f_tp, f_pp = intra_op_ttft(R, D, K), inter_op_ttft(R, D)
        rows.append([R, fmt_time(f_tp), fmt_time(s_tp), fmt_time(f_pp), fmt_time(s_pp), "张量并行" if s_tp < s_pp else "流水线"])
    print_table(["速率 R", "张量并行：公式 3", "模拟", "流水线：公式 2", "模拟", "更好的"], rows)
    rows = []
    for k in (1.5, 1.7, 1.9, K):
        grid = np.linspace(0.1, min(k, 2) / D * 0.999, 2000)
        cross = next((R for R in grid if inter_op_ttft(R, D) < intra_op_ttft(R, D, k)), None)
        rows.append([f"{k:.2f}" + ("（本例）" if k == K else ""), "-" if cross is None else f"{cross:.1f}",
                     f"{k / D:.1f}", f"{2 / D:.1f}"])
    print("\n交叉点（流水线开始更好的速率）与 K 的关系（按公式）：")
    print_table(["K", "交叉速率（请求/秒）", "张量并行的处理能力 K/D", "流水线的处理能力 2/D"], rows)
    print_observations([
        "低速率时排队几乎为零，平均 TTFT ≈ 一个请求的执行时间：张量并行把它缩短到 D/K，所以更好。",
        "速率升高后排队主导：流水线的处理能力是 2/D（两段同时在忙），张量并行只有 K/D（K < 2，通信和更小的矩阵乘法让它达不到 2 倍），"
        "所以高速率时流水线更好。公式和模拟都给出同样的交叉现象（论文图 4）；接近张量并行的处理能力（约 15.5 请求/秒）时，"
        "模拟的排队比公式短，原因同演示 1。",
        "K 越接近 2（张量并行越高效），交叉点越靠右，张量并行适用的范围越大。K 取决于输入长度、模型结构、链路带宽——"
        "这就是 DistServe 不靠手算、而是用模拟器搜索并行方式的原因。",
        "TTFT 的 SLO 越紧，越偏向张量并行（它能把单个请求的时间压下来，这是流水线做不到的）。",
    ])


# ---------------------------------------------------------------------------
# 演示 3：利用率、尾延迟与"扎堆"
# ---------------------------------------------------------------------------
def demo3_tail_and_burstiness():
    step = StepModel(LLAMA31_8B, H100)
    D = step.prefill_time([L])
    print_header(f"演示 3：利用率 vs 尾延迟，以及到达的'扎堆'程度（服务时间 D = {fmt_time(D)}）")
    rows = []
    for rho in (0.5, 0.7, 0.9):
        R = rho / D
        for label, kw in (("等间隔（D/D/1）", {"arrivals": "uniform"}), ("泊松（M/D/1）", {}),
                          ("扎堆（间隔的变异系数 2）", {"arrivals": "bursty", "cv": 2.0})):
            reqs = run_prefill_only(make_workload(N, R, L, 1, **kw), step, max_tokens=L)
            rows.append([f"{rho:.1f}", label, fmt_time(mean_ttft(reqs)), fmt_time(tail_ttft(reqs, 90)),
                         fmt_time(tail_ttft(reqs, 99)), f"{tail_ttft(reqs, 99) / D:.1f}"])
    print_table(["利用率 ρ", "到达方式", "平均 TTFT", "P90", "P99", "P99 / D"], rows)
    print_observations([
        "如果请求完全等间隔地来（D/D/1），利用率 0.9 也不排队：TTFT 就是 D。排队完全是**随机性**造成的。",
        "泊松到达时，ρ = 0.9 的 P99 已经是 D 的十几倍；到达更'扎堆'（真实流量常常如此），同样的利用率下尾延迟再翻好几倍。",
        "所以 SLO 用 P90 / P99 而不是平均值来定，系统要留足余量；DistServe 用历史流量拟合到达分布来做模拟，也是这个原因。"
        "突发流量还会让 decode 实例的 KV 一下子涌入，论文用'decode 实例主动来拉 KV'（pull）而不是 prefill 推（push）来缓冲。",
    ])


if __name__ == "__main__":
    demo1_md1()
    demo2_intra_vs_inter()
    demo3_tail_and_burstiness()
