"""
=============================================================================
CS349E · 第 5 讲 · 02_goodput_workloads.py —— 三种工作负载下的 per-GPU goodput：共置、分块 prefill、解耦
=============================================================================

【这个脚本要回答的问题】
    DistServe 用三类应用评测：聊天（ShareGPT）、代码补全（HumanEval）、文档摘要（LongBench），各有不同的 SLO。
    它报告：比 vLLM（共置、prefill 优先）多服务 2.0–4.6 倍（聊天）、5.7 倍（代码）、4.3 倍（摘要）的请求；
    比 DeepSpeed-MII（共置 + 分块 prefill）多 1.6–7.4 倍。这里用模拟器在三类合成负载上做同样的比较。

      演示 1：单块 GPU 的能力：只做 prefill、只做 decode 各能扛多少请求——三种负载需要的 prefill : decode 比例完全不同。
      演示 2：公平的正面比较：同样 4 块 H100，三种系统的 per-GPU goodput（完整模拟，含 KV 传输）：
              共置（prefill 优先）×4、共置（分块 prefill）×4、解耦（1P3D / 2P2D / 3P1D 里最好的）。
      演示 3：SLO 收紧到多少还能撑住（论文图 8 第二行的"SLO Scale"）：固定每块 GPU 的速率，把 TTFT 和 TPOT 的要求同比例缩小。

【怎么读输出】负载是合成的（对数正态分布的长度，均值见表头），不是论文的数据集；每步时间是 roofline 模型（假设值）。
    goodput 是对速率二分查找得到的，受有限请求数影响有约 ±10% 的模拟噪声。结果是确定性的，每次运行相同。

【运行】python3 02_goodput_workloads.py      （约 40 秒）
【依赖】numpy；同目录的 serving_sim；../lecture01 的 console、hw_specs、llm_math。
"""

import os
import sys

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

from console import print_header, print_observations, print_table  # noqa: E402
from hw_specs import H100  # noqa: E402
from llm_math import LLAMA31_8B  # noqa: E402
from serving_sim import (StepModel, attainment, lognormal_len, make_workload, max_rate, run_colocated,  # noqa: E402
                         run_decode_only, run_disaggregated, run_prefill_only)

STEP = StepModel(LLAMA31_8B, H100)
N = 4000
K = 4                                   # 演示 2、3：每种系统都用 4 块 GPU
WORKLOADS = {
    # 名字: (提示长度分布, 输出长度分布, TTFT SLO, TPOT SLO, 分块 prefill 的预算, 说明)
    "聊天": (lognormal_len(700, 1.0, 16, 8000), lognormal_len(250, 1.0, 2, 2000), 0.2, 0.02, 512,
           "提示约 700、输出约 250；TTFT ≤ 200 ms、TPOT ≤ 20 ms"),
    "代码补全": (lognormal_len(300, 0.8, 16, 4000), lognormal_len(100, 0.8, 2, 1000), 0.05, 0.02, 2048,
             "提示约 300、输出约 100；TTFT ≤ 50 ms（很紧）、TPOT ≤ 20 ms"),
    "摘要": (lognormal_len(4000, 0.5, 500, 16000), lognormal_len(200, 0.6, 2, 1000), 2.0, 0.015, 256,
           "提示约 4000、输出约 200；TTFT ≤ 2 s（宽松）、TPOT ≤ 15 ms（很紧）"),
}


def wl(name, rate):
    p, o, *_ = WORKLOADS[name]
    return make_workload(N, rate, p, o, seed=0)


def run_system(name, system, rate):
    """在 K 块 GPU 上跑一种系统。system："pf"、"chunk"，或者 (x, y) 表示 x 个 prefill 实例 + y 个 decode 实例。"""
    _, _, _, _, budget, _ = WORKLOADS[name]
    if system == "pf":
        return run_colocated(wl(name, rate), STEP, n_instances=K)
    if system == "chunk":
        return run_colocated(wl(name, rate), STEP, n_instances=K, policy="chunked", chunk_budget=budget)
    x, y = system
    return run_disaggregated(wl(name, rate), STEP, STEP, x, y)


def goodput(name, system):
    """K 块 GPU 上一种系统的 per-GPU goodput（二分查找速率，90% 的请求同时满足两个 SLO）。"""
    _, _, ttft, tpot, _, _ = WORKLOADS[name]
    return max_rate(lambda r: attainment(run_system(name, system, r), ttft, tpot), hi=2000, iters=10) / K


# ---------------------------------------------------------------------------
# 演示 1：单块 GPU 的能力
# ---------------------------------------------------------------------------
def demo1_single_gpu():
    print_header("演示 1：一块 H100 只做 prefill / 只做 decode 各能扛多少（Llama-3.1-8B，90% 满足各自的 SLO）")
    rows = []
    for name, (p, o, ttft, tpot, _, desc) in WORKLOADS.items():
        g_p = max_rate(lambda r: attainment(run_prefill_only(make_workload(N // 2, r, p, o), STEP), ttft, None), hi=400)
        g_d = max_rate(lambda r: attainment(run_decode_only(make_workload(N // 2, r, p, o), STEP), None, tpot), hi=2000)
        rows.append([name, desc, f"{g_p:.1f}", f"{g_d:.1f}", f"{g_d / g_p:.1f}"])
    print_table(["负载", "设置", "只做 prefill（请求/秒）", "只做 decode（请求/秒）", "每块 decode GPU 需要几块 prefill GPU"], rows)
    print_observations([
        "三种负载对两个阶段的需求比例完全不同：代码补全的 decode 很轻（输出短、TPOT 宽松），一块 decode GPU 能对付好几块 prefill GPU 的产出；"
        "摘要的提示很长、TPOT 很紧，两边需求接近。",
        "共置系统里这个比例是固定的（每块 GPU 两种活都干）；解耦以后可以按负载配比，这是 DistServe 说的'资源和并行方式解耦'的第一层含义。",
        "代价是配比只能取整数，而且负载一变，最优配比就变了（DistServe 要定期按历史负载重新搜索）。",
    ])


# ---------------------------------------------------------------------------
# 演示 2：公平的正面比较
# ---------------------------------------------------------------------------
def demo2_fair():
    print_header(f"演示 2：同样 {K} 块 H100——三种系统的 per-GPU goodput（完整模拟；解耦含 KV 传输：InfiniBand 50 GB/s、α = 5 µs）")
    rows, best = [], {}
    for name, (*_, budget, desc) in WORKLOADS.items():
        pf = goodput(name, "pf")
        ch = goodput(name, "chunk")
        dis = max((goodput(name, (x, K - x)), f"{x}P{K - x}D", (x, K - x)) for x in range(1, K))
        best[name] = dis
        rows.append([name, f"{pf:.1f}", f"{ch:.1f}（预算 {budget}）", f"{dis[0]:.1f}（{dis[1]}）",
                     f"{dis[0] / pf:.2f}×", f"{dis[0] / ch:.2f}×"])
    print_table(["负载", "共置：prefill 优先", "共置：分块", "解耦（最好的配比）", "解耦 / prefill 优先", "解耦 / 分块"], rows)
    print_observations([
        "和'共置 + prefill 优先'（vLLM 早期的做法）比，解耦在聊天、代码补全、摘要上都更好（1.2–1.5 倍）："
        "decode 不再被 prefill 打断，prefill 和 decode 的 GPU 可以按需求配比。方向和论文一致（论文比 vLLM 好 2.0–5.7 倍）。",
        "但'共置 + 分块 prefill'是强得多的对手：在这个模型里，只有 TTFT 要求很紧的代码补全，解耦才明显更好（3P1D）；"
        "聊天和摘要上分块 prefill 反而更好。原因有两个：分块让 decode 的 token 搭上了 prefill 的便车（同一次读权重）；"
        "而 4 块 GPU 的解耦只能在 1P3D、2P2D、3P1D 里选，总有一边的能力被浪费（演示 1 的理想比例不是整数）。",
        "这和论文的结论（比使用分块 prefill 的 DeepSpeed-MII 好 1.6–7.4 倍）不完全一致。我们的每步时间是理想化的 roofline："
        "混合批次一点效率都不损失、注意力内核对混合批次一样高效——真实系统里分块 prefill 没有这么便宜。"
        "**结论高度依赖于'每步要多久'这个模型**，这也是 DistServe 要先校准模拟器（和真实系统的误差 < 2%）再做搜索的原因。",
        "这组结果和后来业界的经验一致：中小模型、负载平稳时，分块 prefill 的共置系统已经很好；解耦的价值在 TTFT 很紧、"
        "两个阶段想用不同的并行方式或不同的硬件（04、05 脚本）、或者规模大到可以细调配比的时候。",
    ])
    return best


# ---------------------------------------------------------------------------
# 演示 3：SLO 能收得多紧
# ---------------------------------------------------------------------------
def demo3_slo_scale(best):
    name = "代码补全"
    _, _, ttft, tpot, budget, _ = WORKLOADS[name]
    g_pf = goodput(name, "pf")
    per_gpu = 0.8 * g_pf
    x, y = best[name][2]
    print_header(f"演示 3：{name}，{K} 块 GPU、每块固定 {per_gpu:.1f} 请求/秒（= 0.8 × prefill 优先的 goodput），把两个 SLO 同比例收紧")
    rows = []
    for system, label in (("pf", "共置：prefill 优先"), ("chunk", f"共置：分块（预算 {budget}）"), ((x, y), f"解耦 {x}P{y}D")):
        cells = [label]
        for scale in (1.0, 0.8, 0.6, 0.5, 0.4, 0.3):
            cells.append(f"{attainment(run_system(name, system, per_gpu * K), ttft * scale, tpot * scale) * 100:.0f}%")
        rows.append(cells)
    print_table(["系统", "SLO × 1.0", "× 0.8", "× 0.6", "× 0.5", "× 0.4", "× 0.3"], rows)
    print_observations([
        "能撑住 90% 达成率的最紧 SLO：解耦 3P1D 是 0.6 倍（92%），两种共置系统在 0.8 倍时就只剩 89%——解耦能撑住约 1.7 倍紧的 SLO。"
        "论文在代码补全上报告比 vLLM 紧 1.4 倍。原因：prefill 实例上没有 decode 和它抢，TTFT 只取决于 prefill 自己的排队；"
        "decode 实例上没有 prefill 打断，TPOT 稳定。",
        "prefill 优先的共置系统崩得最快：0.6 倍时只有 29% 的请求达标。",
        "但解耦系统的配比是**按原 SLO 选的**：收到 0.5 倍时它掉到 30%，反而不如分块 prefill（62%）。SLO 变了，配比和并行方式要重新搜索——"
        "DistServe 的放置算法正是以 SLO 为输入的。分块 prefill 没有'配比'这个旋钮，退化得最平缓。",
    ])


if __name__ == "__main__":
    demo1_single_gpu()
    b = demo2_fair()
    demo3_slo_scale(b)
