"""
=============================================================================
CS349E · 第 5 讲 · 01_interference.py —— prefill 和 decode 挤在一起：互相干扰
=============================================================================

【这个脚本要回答的问题】
    DistServe（本讲阅读材料）的出发点：现有系统把 prefill 和 decode 放在同一组 GPU 上、混在一起批处理（共置），
    这会带来两个问题——互相干扰、资源和并行方式被绑在一起。这里先看第一个问题。

      演示 1：一步之内的干扰：一批 decode 里塞进一个 prefill，decode 这一步慢了多少；prefill 又慢了多少（论文图 2）。
      演示 2：请求一多会怎样：P90 TTFT 和 P90 TPOT 随请求速率的变化——共置、只做 prefill、只做 decode（论文图 1）。
      演示 3：论文图 1 的那笔账：把 prefill 和 decode 分到不同的 GPU 上，每块 GPU 的 goodput 能高多少。
      演示 4：分块 prefill（chunked prefill）：把长 prefill 切成小块、和 decode 拼在一起——它能缓解干扰吗，代价是什么。

【怎么读输出】Llama-3.1-8B、一块 H100，每步时间来自 serving_sim.StepModel（roofline × 效率折扣 + 每步 1 ms 固定开销，
    假设值）。到达过程是泊松过程，结果是确定性的（固定种子），每次运行相同。和论文一样，提示 512 个 token、输出 64 个。

【运行】python3 01_interference.py      （约 2 秒）
【依赖】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 fmt_time, 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, make_workload, max_rate, percentile, run_colocated,  # noqa: E402
                         run_decode_only, run_prefill_only)

STEP = StepModel(LLAMA31_8B, H100)
N = 1500
PROMPT, OUTPUT = 512, 64


# ---------------------------------------------------------------------------
# 演示 1：一步之内的干扰
# ---------------------------------------------------------------------------
def demo1_one_step():
    print_header("演示 1：一批 decode 里塞进一个 prefill——这一步慢了多少（论文图 2）")
    m = LLAMA31_8B
    B, ctx = 32, 700
    dec = STEP.decode_time(B, B * ctx)
    rows = []
    for L in (128, 512, 1024, 2048):
        pre = STEP.prefill_time([L])
        mixed = STEP.mixed_time(B, B * ctx, L, m.attn_flops_per_token(1) * L * (L + 1) / 2)
        rows.append([L, fmt_time(dec), fmt_time(mixed), f"{mixed / dec:.1f}×", fmt_time(pre), f"{mixed / pre:.2f}×"])
    print(f"Llama-3.1-8B、H100：{B} 个正在 decode 的序列（每个上下文 {ctx}）+ 一个长度为 L 的新提示，放在同一步里算：")
    print_table(["提示长度 L", "只有 decode", "decode + prefill", "decode 变慢", "只有 prefill", "prefill 变慢"], rows)
    print_observations([
        "decode 的一步本来只要约 7.7 ms（读权重和 KV 为主，算力几乎闲着）。塞进一个 512 token 的 prefill，这一步要算的 token "
        "从 32 个变成 544 个，变成 compute-bound，一步要 14.9 ms：这一步里所有 32 个用户的 token 都晚了约一倍。"
        "提示越长，decode 被拖得越慢（2048 时 7.2 倍）。",
        "128 token 的短提示几乎不添乱（7.70 ms，和只有 decode 一样）：它的计算正好藏在读权重的时间里。干扰主要来自长提示。",
        "反过来，prefill 和 decode 拼在一起时，prefill 也稍微变慢了（多读了 KV、多算了 decode 的 token），不过幅度小得多。",
        "这就是**干扰**（interference）：一个阶段的工作让另一个阶段的延迟变差。decode 每一步只做一点点事，最怕被一个大 prefill 卡住。",
    ])


# ---------------------------------------------------------------------------
# 演示 2：P90 延迟随速率变化
# ---------------------------------------------------------------------------
def demo2_latency_vs_rate():
    print_header(f"演示 2：P90 TTFT / P90 TPOT 随请求速率变化（一块 H100，提示 {PROMPT}、输出 {OUTPUT}，论文图 1）")
    rows = []
    for rate in (5, 10, 20, 30, 40, 50, 60):
        col = run_colocated(make_workload(N, rate, PROMPT, OUTPUT), STEP)
        pre = run_prefill_only(make_workload(N, rate, PROMPT, OUTPUT), STEP)
        dec = run_decode_only(make_workload(N, rate, PROMPT, OUTPUT), STEP)
        rows.append([rate, fmt_time(percentile(col, "ttft", 90)), fmt_time(percentile(col, "tpot", 90)),
                     fmt_time(percentile(pre, "ttft", 90)), fmt_time(percentile(dec, "tpot", 90))])
    print_table(["速率（请求/秒）", "共置：P90 TTFT", "共置：P90 TPOT", "只做 prefill：P90 TTFT", "只做 decode：P90 TPOT"], rows)
    print_observations([
        "共置时，速率一高，TPOT 先坏：每秒到达的 prefill 越多，decode 被打断得越频繁，P90 TPOT 从 7.6 ms 涨到几十毫秒。"
        "TTFT 也比'只做 prefill'差：新请求要等正在进行的 decode 步骤和排在前面的 prefill。",
        "同一块 GPU 只做 decode 时，P90 TPOT 几乎不随速率变化：decode 的批越大越划算（读一次权重给更多人用），"
        "而 64 个输出 token 的 decode 根本吃不满一块 H100。",
        "只做 prefill 时，TTFT 在低速率下就是一次 prefill 的时间（约 14 ms），速率接近 prefill 的处理能力时排队迅速变长（演示 3、03 脚本）。",
    ])


# ---------------------------------------------------------------------------
# 演示 3：论文图 1 的那笔账
# ---------------------------------------------------------------------------
def demo3_goodput_arithmetic():
    ttft, tpot = 0.1, 0.015
    print_header(f"演示 3：每块 GPU 的 goodput——共置 vs 分开（SLO：TTFT ≤ {ttft * 1e3:.0f} ms、TPOT ≤ {tpot * 1e3:.0f} ms，90% 达成）")

    def wl(r):
        return make_workload(N, r, PROMPT, OUTPUT)

    g_col = max_rate(lambda r: attainment(run_colocated(wl(r), STEP), ttft, tpot), hi=400)
    g_pre = max_rate(lambda r: attainment(run_prefill_only(wl(r), STEP), ttft, None), hi=400)
    g_dec = max_rate(lambda r: attainment(run_decode_only(wl(r), STEP), None, tpot), hi=2000)
    rows = [["共置（prefill 优先）", 1, f"{g_col:.1f}", f"{g_col:.1f}"],
            ["只做 prefill", 1, f"{g_pre:.1f}", "-"], ["只做 decode", 1, f"{g_dec:.1f}", "-"]]
    best = max((min(x * g_pre, y * g_dec) / (x + y), x, y) for x in range(1, 9) for y in range(1, 9))
    for x, y in ((1, 1), (2, 1), (best[1], best[2])):
        tot = min(x * g_pre, y * g_dec)
        rows.append([f"{x} 块 prefill + {y} 块 decode", x + y, f"{tot:.1f}", f"{tot / (x + y):.1f}"])
    print_table(["配置", "GPU 数", "整体 goodput（请求/秒）", "每块 GPU"], rows)
    print("\n论文图 1（OPT-13B、A100、提示 512 / 输出 64）：共置约 1.6 请求/秒；只做 prefill 5.6、只做 decode 10；"
          "2 块 prefill + 1 块 decode → 10 请求/秒，每块 GPU 3.3，是共置的 2.1 倍。")
    print_observations([
        f"共置时 goodput 被更严格的那个 SLO（这里是 TPOT）卡住，每块 GPU 约 {g_col:.0f} 请求/秒。",
        f"分开以后各自只对付自己的 SLO：一块只做 prefill 的 GPU 能扛约 {g_pre:.0f} 请求/秒，一块只做 decode 的 GPU 能扛约 {g_dec:.0f}——"
        "decode 的能力远远多余（输出只有 64 个 token）。",
        f"按需求配比例：{best[1]} 块 prefill 配 {best[2]} 块 decode 时每块 GPU 约 {best[0]:.0f} 请求/秒，是共置的 {best[0] / g_col:.1f} 倍。"
        "论文里的数字是 2.1 倍；具体倍数取决于模型、硬件和 SLO，但'分开、按比例配'的道理相同。",
        "这里的计算假设请求能平均分给各个实例、KV 传输不花时间——02 脚本用完整的模拟检查这一点。",
    ])


# ---------------------------------------------------------------------------
# 演示 4：分块 prefill
# ---------------------------------------------------------------------------
def demo4_chunked():
    rate = 40
    print_header(f"演示 4：分块 prefill（Sarathi-Serve）——每一步 = 全部 decode + 一块 prefill（速率 {rate} 请求/秒）")
    rows = []
    col = run_colocated(make_workload(N, rate, PROMPT, OUTPUT), STEP)
    rows.append(["不分块（prefill 优先）", "-", fmt_time(percentile(col, "ttft", 50)), fmt_time(percentile(col, "ttft", 90)),
                 fmt_time(percentile(col, "tpot", 50)), fmt_time(percentile(col, "tpot", 90))])
    for budget in (256, 512, 1024, 2048):
        c = run_colocated(make_workload(N, rate, PROMPT, OUTPUT), STEP, policy="chunked", chunk_budget=budget)
        rows.append(["分块", budget, fmt_time(percentile(c, "ttft", 50)), fmt_time(percentile(c, "ttft", 90)),
                     fmt_time(percentile(c, "tpot", 50)), fmt_time(percentile(c, "tpot", 90))])
    print_table(["策略", "每步 token 预算", "P50 TTFT", "P90 TTFT", "P50 TPOT", "P90 TPOT"], rows)
    print_observations([
        "分块之后，decode 不再被整段 prefill 卡住，每一步只多做一小块 prefill：P90 TPOT 明显下降。",
        "代价是 TTFT：一个提示要分几步才做完，而且每一步还要带着所有 decode。预算越小，TPOT 越好；TTFT 大体上越差"
        "（P90 从 2048 预算的 51 ms 到 256 预算的 59 ms，中间不完全单调）——"
        "DistServe 说的'分块 prefill 是用 TTFT 换 TPOT，干扰并没有消失'。",
        "分块 prefill 的另一个好处在这个模型里也看得到：decode 的 token 搭了 prefill 的便车（同一次读权重），每一步更'满'。"
        "所以它是一个很强的基线（02 脚本会和解耦正面比较）。",
    ])


if __name__ == "__main__":
    demo1_one_step()
    demo2_latency_vs_rate()
    demo3_goodput_arithmetic()
    demo4_chunked()
