← 返回学习路线 ◆ 贯穿项目
应用工程与上线 · 阶段 16 · 推理优化与工程部署
Stage 16 / 17 · 应用工程与上线

推理优化与工程部署 Inference Optimization & Deployment

当模型能力不再是瓶颈,决定项目生死的就是推理成本与延迟。这一阶段的目标很实际:把同样的模型跑得更快、更省、更稳。2026 年的就业市场上,「有大模型推理优化经验」是明确的加分项,因为这部分能力直接对应真金白银的账单。

⏱ 4–5 周 🎯 高级 · 工程壁垒 ◆ 里程碑 M16 2026-09-29
vLLMSGLang量化KV CacheTP/PP/EPFastAPIMLOps

阶段总览

✔
学完你能做到
  • 能用第一性原理判断一个推理负载是显存带宽受限还是算力受限
  • 掌握 vLLM / SGLang / TensorRT-LLM 的适用场景与关键参数
  • 理解量化方法(GPTQ / AWQ / FP8 / KV Cache 量化)的取舍与验证方式
  • 能解释并配置连续批处理、投机解码、前缀缓存、chunked prefill
  • 理解 PD 分离架构与多卡并行(TP / PP / EP)的部署取舍
  • 能写一个生产级推理服务:异步、流式、限流、重试、降级、灰度
  • 能做容量规划与单位经济核算,回答「要几张卡、每千次请求多少钱」
阶段知识结构总览 · 从指标定义到成本核算的完整链路
阶段 16 · 推理优化与工程部署Inference Optimization & Deployment · 9 大章 · 180+ 知识点
1. 推理指标体系TTFT / TPOT / P99goodput 有效吞吐延迟预算分解P99/P50 健康度
2. 推理第一性原理prefill vs decoderoofline 强度判据KV Cache 分页连续批处理chunked prefill
3. 推理引擎与优化vLLM / SGLang 选型PagedAttentionRadixAttentionGGUF 边缘部署
4. 推测解码draft-then-verify接受率 αEAGLE / Medusan-gram 查表
5. 量化GPTQ / AWQFP8 / INT4KV Cache 量化掉点诊断
6. 并行策略TP / PP / EPPD 分离流水线气泡率NVLink vs 以太网
7. 服务化SSE 流式输出限流熔断降级就绪探针幂等键
8. 压测方法论开环 vs 闭环warmup 编译开销容量拐点 ×0.8
9. 成本与 MLOps每百万 token 成本自托管 vs API容量规划算例HPA 排队扩容
贯穿项目 · M16 推理优化、部署与压测第 93–99 周vLLM 后端接入(连续批处理、PagedAttention、前缀缓存复用)FP16 / INT8 / AWQ 或 GPTQ 的质量-速度-显存三角权衡表K8s 部署清单 + HPA 自动扩缩 + 灰度发布(按流量比例切流)与回滚脚本Locust/k6 压测报告——QPS、TTFT、TPOT、P99、错误率、GPU …
学习路径
★
一句话抓住推理的本质:大模型推理是显存带宽受限的:每生成一个 token,都要把模型权重(以及 KV Cache)从显存读一遍。所以「快」的关键不是算得更快,而是让每次读出来的数据服务更多计算——这就是批处理、量化、KV Cache 管理、投机解码共同的方向。
周次主题交付物
第 1 周推理性能原理一份性能瓶颈分析(roofline 判断)
第 2 周推理引擎与关键优化vLLM / SGLang 压测对比报告
第 3 周量化与验证不同量化方案的精度–速度–显存对比
第 4 周服务化与容错生产级推理服务(流式 / 限流 / 降级)
第 5 周容量规划与 MLOps成本模型 + 上线与回滚流程
★
2026 就业形势:推理成本下降正在引爆应用:2026 年最确定的趋势之一是单位推理成本持续下降(FP8/4bit 量化、PD 分离、更高效的注意力内核),直接催生了Agentic AI 岗位的爆发式增长——企业不再满足于「能对话」,而要「能替我连续干活」。同时企业级落地需求从 PoC 走向生产,对「稳定、可控、可观测、可压测」的推理基建人才需求激增。会用 vLLM 做吞吐–延迟调优、能给出成本模型、能设计高可用服务的人,是当下最稀缺的那一档。

1. 推理指标体系:先定义再优化

知识结构图 · 推理指标体系
推理指标体系4 大知识域 · 18 个知识点
核心延迟指标TTFT 首 token 延迟TPOT / ITL 间隔端到端 ≈ TTFT+(n−1)·TPOT吞吐 tokens/s 与 QPS并发度与排队
学习路径
  1. 读 1.1:用 measure_stream 对一次流式响应算出 TTFT / TPOT(体感 20–60ms)
  2. 跑 latency_budget 分解代码,观察 decode 占端到端 80%+
  3. 完成 1.4 自测:对给定样本算 P50/P95/P99 并判断 P99/P50 健康度
  4. 对接 M16:压测时同时记录 TTFT 与分位数,作为 vLLM 调优基线
✔ 能现写代码算出 TTFT/TPOT 与分位数,并说出哪个物理资源主导每项延迟
核心知识点详解
  • 端到端 = TTFT + (n−1)×TPOT:交互产品把延迟拆三截看:TTFT 首 token 决定「响应快不快」,TPOT 每步间隔决定「生成稳不稳」;参考量级 TPOT 20–60ms(约 16–50 tokens/s)。用 measure_stream 对一次流式响应逐 token 打点即可同时算出两项与分位数。
  • 分位数用线性插值:P99 表示 99% 请求都不超过的延迟:先 sorted,再取 rank=p×(n−1) 在相邻两点间插值。看健康度用 P99/P50 ≤ 2–4×,超过就怀疑长尾(排队 / KV 碎片 / GC)。
  • 常见坑:超时请求被剔除:统计分位数时把超时 / 失败请求丢掉,P99 显得很健康——用户其实早已超时。超时请求要按 SLO 记为失败并参与计数,否则 goodput 与压测结论全是幻觉。
goodput 与 SLOgoodput 有效吞吐SLO 门限筛请求拐点并发设限流名义吞吐是幻觉
学习路径
  1. 读 1.2:理解 goodput 是满足 SLO 的有效吞吐,名义吞吐是幻觉
  2. 跑 goodput 与 sweep 代码,观察并发 128 时 goodput 腰斩
  3. 完成 1.4 自测:构造名义吞吐高但 goodput 低的数据,说明限流设在哪
  4. 对接 M16:按 SLO 门限给 vLLM 并发档位设限流参考
✔ 能在数据上指出限流阈值设在 goodput 见顶处而非名义吞吐峰值
核心知识点详解
  • goodput 是满足 SLO 的吞吐:名义吞吐是系统塞多少算多少;goodput 只统计达到延迟 SLO(如 P99<2s 且无超时)的请求。并发 128 时名义吞吐还在涨,goodput 可能腰斩。
  • 限流设在 goodput 顶点:并发从 1 扫到过载,goodput 先升后降,拐点处就是限流阈值(常再 ×0.8 留安全余量)。慢请求挤压排队让更多请求超时,goodput 塌得比名义吞吐更快。
  • 常见坑:拿名义吞吐定容量:按峰值名义吞吐买卡、设限流,进真实流量后大量请求因排队超时,SLA 破口。决策永远看 goodput 拐点,而不是理论上限。
分位数与尾延迟P50 / P95 / P99线性插值分位数P99/P50 健康度 2–4×KV 碎片拖尾延迟
学习路径
  1. 读 1.3:理解平均值骗人、用户体感由 P95/P99 决定
  2. 跑 tail_ratio 代码,对比平稳与长尾两种分布的 P99/P50
  3. 完成 1.4 自测:解释 P99 涨到 9s 而 P50 几乎不变时先怀疑什么
  4. 对接 M16:监控告警把 TTFT 分位数当指标并设阈值告警
✔ 能解释长尾来源(排队 / KV 碎片 / GC)并让监控单独盯 P99
核心知识点详解
  • 平均值骗人,体感由 P99 决定:接口平均 800ms 看着不错,但 P99=6s 用户早已流失。要分位数就得留数据:每请求打点落 Prometheus Histogram,才能还原 P50/P95/P99。
  • 长尾逐一排查:P99 涨到 9s 而 P50 几乎不变,优先怀疑三件事:并发超标引起排队、KV 碎片挤压 batch、GC / 调度抖动。先按预算分解定位段,再调参,别盲目加卡。
  • 常见坑:只盯平均值:只监控 avg latency,长尾恶化毫无察觉,直到用户大量投诉。监控必须单独看分位数并设 P99 告警,如 TTFT P99 > 1s 持续 5min。
延迟预算分解网络 50–150ms网关鉴权 5–20ms检索 100–800msdecode 占端到端 80%+
学习路径
  1. 读 1.1:把端到端预算拆成网络 / 网关 / 检索 / prefill / decode
  2. 跑 latency_budget,确认输出长度决定端到端(decode 占比大)
  3. 完成 1.4 自测:把自己服务拆成预算表并指出占比最大一项
  4. 对接 M16:在架构文档里写清预算分解与优化优先级
✔ 能给出各段的毫秒预算并指出此刻该优化哪一项
核心知识点详解
  • 预算 = 网络+网关+检索+prefill+decode:端到端拆成:网络往返 50–150ms、网关鉴权 5–20ms、检索/工具 100–800ms、prefill 100–600ms(TTFT 主体)、decode (n−1)×TPOT。把每段写进 latency_budget 表,找到占比最大一项才有优化抓手。
  • decode 决定端到端:TPOT=40ms、输出 300 token 时,decode 占 (299)×40ms≈12s——端到端几乎由输出长度线性决定。想压延迟先问「能不能让模型少写点」,再谈引擎调参。
  • 常见坑:全链路一起优化:不知道每段占多少就乱改,往往优化了最便宜的网关还自我感动。先量化分解、优先占比最大的段,每个优化都用预算表佐证收益。
学习路径

1.1 先定义指标,再谈优化

优化的第一原则是「先定义指标,再动手」——否则你无法判断一次改动是变好还是变差。大模型服务天然分成 prefill 与 decode 两个阶段,因此指标体系也要分成「首 token」与「生成中」两条线。下面这张表是所有后续讨论的词汇表;面试与实际调优里,能准确说出每个指标受什么影响,比背公式更重要。

指标定义主要受什么影响经验目标
TTFT(Time To First Token)请求发起到收到首个输出 token 的延迟输入长度、prefill 排队、前缀缓存命中率交互场景 < 0.5s,批量 < 2s
TPOT / ITL相邻两个输出 token 的间隔(Time / Inter-Token Per Output Token)decode 并发 batch、量化、KV 是否放得下20–60ms(约 16–50 tokens/s 体感)
端到端延迟TTFT + (输出长度 − 1) × TPOT长度分布、TPOT、TTFT与输出长度近似线性
吞吐 tokens/s单位时间生成 token 总量(系统容量)并发度、batch、量化、权重体积越大越省,是容量核心
吞吐 requests/s(QPS)单位时间完成的请求数单请求耗时、并发上限直接决定所需副本数
并发度同一时刻正在处理的请求数限流、排队、调度由 SLA 与显存共同决定
goodput满足全部 SLO 的有效吞吐(剔除超时/失败)SLO 阈值、尾延迟、错误率比名义吞吐更反映真实价值
P50 / P95 / P99延迟分位数(从小到大第 50/95/99 百分位)排队、GC、KV 碎片、调度抖动体验由 P99 决定

prefill 与 decode 的资源特性差异是后面所有优化的根源:prefill 算力密集(一次算出整个 prompt 的 KV,矩阵乘并行度高,算术强度高,接近 compute-bound),decode 带宽密集(每步只产 1 个 token,却要把全部权重与 KV 读一遍,算术强度低,严重 memory-bound)。正因 decode 是带宽受限,优化它的关键永远是两条——提高 batch(让一次权重读取服务更多 token)与降低显存访问(量化权重与 KV、减少读写量)。理解这一点,后面所有技术(批处理、量化、KV 管理、投机解码)都是这两条的推论。

python# 从一个流式响应里直接算出 TTFT / TPOT / 分位数(压测与线上监控同款逻辑)
import time, statistics

def measure_stream(chunks):
    """chunks: 按时间顺序到达的 (timestamp, token_text) 列表"""
    t0 = chunks[0][0]
    first = chunks[1][0] if len(chunks) > 1 else t0      # 首个生成 token 时刻
    ttft = first - t0
    gaps = [chunks[i][0] - chunks[i-1][0] for i in range(1, len(chunks))]
    tpot = statistics.median(gaps)                        # 用中位数抗抖动
    return {"ttft_s": round(ttft, 3), "tpot_ms": round(tpot*1000, 1),
            "n_tokens": len(chunks) - 1}

def percentile(xs, p):
    if not xs: return None
    s = sorted(xs); n = len(s)
    if n == 1: return s[0]
    rank = p * (n - 1); lo = int(rank); hi = min(lo + 1, n - 1); frac = rank - lo
    return s[lo] * (1 - frac) + s[hi] * frac

# 用法:把一批请求的端到端延迟传进来
# print("P50/P95/P99 =", percentile(ends, .5), percentile(ends, .95), percentile(ends, .99))

把指标变成「预算」才可执行。交互式产品的端到端预算通常这样拆:网络往返 50–150ms + 网关鉴权 5–20ms + 检索/工具 100–800ms + prefill 100–600ms(TTFT 主体)+ decode (n−1)×TPOT。当 TPOT=40ms、输出 300 token 时,decode 就占 (299)×40ms ≈ 12s——端到端延迟几乎由输出长度线性决定,想压延迟先问「能不能少写点」。

python# 延迟预算分解:找出「该优化的那一项」,而不是盲目调引擎
def latency_budget(in_tok, out_tok, ttft_ms=350, tpot_ms=40,
                   net_ms=80, gateway_ms=15, retrieve_ms=250, tool_ms=0):
    prefill_decode = ttft_ms + (out_tok - 1) * tpot_ms
    total = net_ms + gateway_ms + retrieve_ms + tool_ms + prefill_decode
    return {"total_ms": total,
            "ttft_ms": net_ms + gateway_ms + retrieve_ms + tool_ms + ttft_ms,
            "decode_pct": round(prefill_decode / total * 100, 1)}

print(latency_budget(800, 300))
# 预期输出(约):{'total_ms': 12305, 'ttft_ms': 695, 'decode_pct': 94.3}
# 关键结论:输出 300 token、TPOT=40ms 时 decode 占端到端约 94%,
#           优化 prefill / 检索都是小事,降 TPOT 或缩短输出才是大头。

1.2 goodput:满足 SLO 的有效吞吐

名义吞吐(tokens/s)会骗人:一个系统可能「生成得很快」但大量请求超时或报错,这些 token 对用户毫无价值。goodput = 在 SLO 内成功完成的请求数 / 时间,才是真正衡量服务价值的指标。调优目标应当是「在给定 SLO 下最大化 goodput」,而不是单纯拉高名义吞吐——因为后者可以通过牺牲尾延迟换到,而用户感知不到那部分。

SLO 维度宽松配置(批处理/离线)严格配置(实时交互)
TTFT< 5s< 0.5s
TPOT< 200ms< 50ms
请求成功率> 99%> 99.9%
单请求最大长度32K4K
python# goodput:把 SLO 当作筛选条件,而非平均值
SLO = {"ttft_max": 0.8, "tpot_max": 0.06, "err_max": 0.01}

def goodput(records):
    """records: [{ttft, tpot, ok, out_tokens, start, end}]"""
    ok = [r for r in records
          if r["ok"] and r["ttft"] <= SLO["ttft_max"] and r["tpot"] <= SLO["tpot_max"]]
    good_rate = len(ok) / max(1, len(records))
    good_tokens = sum(r.get("out_tokens", 0) for r in ok)
    wall = max(r["end"] for r in records) - min(r["start"] for r in records)
    return {"good_rate": round(good_rate, 3),
            "goodput_tps": round(good_tokens / wall, 1),
            "nominal_tps": round(sum(r.get("out_tokens",0) for r in records)/wall, 1)}

# 典型现象:把并发拉到拐点之上,nominal_tps 还涨,但 good_rate 暴跌
#           —— 这时名义吞吐是幻觉,goodput 才是真相,限流阈值应按 goodput 设。
python# 并发扫描下 goodput 与名义吞吐的分叉 —— 限流阈值应设在 goodput 最大处
def sweep(records_by_conc):
    """records_by_conc: {concurrency: [rec, ...]}"""
    for c, recs in records_by_conc.items():
        g = goodput(recs)
        print(f"conc={c:>4} nominal={g['nominal_tps']:>7} "
              f"good={g['goodput_tps']:>7} rate={g['good_rate']}")

# 预期(8B / FP8 / H100,SLO 门限 p95 TTFT 0.8s):
# conc=   8 nominal= 420 good= 415 rate=0.99
# conc=  32 nominal=1180 good=1150 rate=0.98
# conc=  64 nominal=1620 good=1480 rate=0.91
# conc= 128 nominal=1850 good=1010 rate=0.55   <- 名义还在涨,goodput 已腰斩
# 结论:名义吞吐到 128 并发仍上升,但真实价值在 64 附近见顶 -> 限流设在 64。

1.3 分位数与尾延迟:为什么平均值会骗人

平均值会被「大量很快的请求」掩盖「少数极慢的请求」。但用户体验由最慢的那批决定——一个 P99 为 8s 的服务,意味着每 100 个用户里有 1 个等了 8 秒。线上监控与 SLA 一律看 P95/P99,不看平均。尾延迟的来源通常是:请求排队、KV Cache 碎片触发重分配、CUDA 内核启动抖动、垃圾回收,以及某个长请求把整个 batch 拖慢。

python# 分位数的稳健实现(线性插值,与 numpy/perf 一致)
def percentile(xs, p):
    if not xs: return None
    s = sorted(xs); n = len(s)
    if n == 1: return s[0]
    rank = p * (n - 1)                 # 线性插值位置
    lo = int(rank); hi = min(lo + 1, n - 1)
    frac = rank - lo
    return s[lo] * (1 - frac) + s[hi] * frac

# 示例:同样的均值,分布天差地别
a = [0.2]*90 + [3.0]*10               # 均值 0.48,P99 = 3.0(长尾)
b = [0.45]*100                        # 均值 0.45,P99 = 0.45(平稳)
print("A: mean=%.2f p99=%.2f" % (sum(a)/len(a), percentile(a, .99)))
print("B: mean=%.2f p99=%.2f" % (sum(b)/len(b), percentile(b, .99)))
# 输出:A 均值更低但 P99 是 B 的 6.7 倍 —— 用户体感由 A 的尾巴决定
python# 用 P99/P50 比值当「尾延迟健康度」告警指标(比绝对 P99 更抗流量波动)
def tail_ratio(lat):
    p50, p99 = percentile(lat, .5), percentile(lat, .99)
    health = "OK" if p99 <= 3 * p50 else ("WARN" if p99 <= 6 * p50 else "BAD")
    return round(p99 / p50, 2), health

print(tail_ratio([0.45]*100))            # (1.0, 'OK')   平稳
print(tail_ratio([0.2]*90 + [3.0]*10))   # (13.5, 'BAD') 长尾
# 经验:健康的在线服务 P99/P50 常在 2–4 倍;一旦 > 6 倍,
#       通常是排队、KV 碎片或 GC 抖动,先查并发与显存再谈扩容。
⚠
尾延迟的隐藏来源:① KV 碎片:未分页的实现里,长请求释放后留下空洞,后续请求被迫串行化;② 抢占/重调度:优先级调度在低优请求上造成停顿;③ 编译与图捕获:首次或输入形状变化触发 CUDA graph / 内核编译,单请求骤慢;④ 跨 NUMA 或显存碎片导致的分配延迟。这些都让 P99 远高于 P50,必须在压测里单独观察并作为容量拐点的主要信号。

1.4 动手练习与自测

  1. 用 measure_stream 对一次真实流式响应算出 TTFT / TPOT,并解释为什么用中位数而非均值(判据:能指出前几个 token 常因编译/首块抖动而偏大)。
  2. 给定 500 次请求延迟样本,写出 P50/P95/P99,并判断 P99/P50 是否健康(判据:比值 2–4 健康,> 6 需先查排队与显存)。
  3. 把某个你熟悉的服务拆成延迟预算表,指出占比最大的一项(参考答案:输出长度通常让 decode 占 80%+)。
  4. 构造一组「名义吞吐高但 goodput 低」的数据,说明限流阈值该设在哪(参考答案:goodput 见顶处,本书示例为 64 并发)。
  5. 如果线上 P99 从 3s 涨到 9s 而 P50 几乎没变,最先怀疑什么?(参考答案:KV 碎片 / 长请求阻塞 / 抢占,而非整体变慢)。
✔
自测判据:能把每个指标归因到一个物理资源(带宽 / 算力 / 显存 / 排队),并说出它随并发与长度如何变化,就说明你已经从「记名词」升级到「会诊断」。
指标经验量级 / 阈值说明
TTFT0.2–2 s受 prompt 长度与 prefill 并行度影响;前缀缓存可数倍下降
TPOT20–60 ms/tokendecode 每 token 时间;人眼流畅阈值约 < 50 ms
P95 / P501.5–3 ×健康区;P99 / P50 > 6 需先查排队与显存
goodput拐点并发 × 0.8有效吞吐见顶处即限流阈值参考

2. 推理性能的第一性原理

知识结构图 · 推理性能的第一性原理
推理性能的第一性原理6 大知识域 · 27 个知识点
两阶段资源特性prefill 算力受限decode 带宽受限算术强度 ≈ 1 FLOP/Byteroofline 拐点判定理论 TPS ≈ 带宽/模型大小
学习路径
  1. 读 2.1:理解 prefill 算力受限、decode 带宽受限
  2. 跑 roofline 代码,观察 decode batch=1 强度约 1 FLOP/Byte 属 memory-bound
  3. 估理论最快生成速度 ≈ 显存带宽 / 模型大小
  4. 对接 M16:据此决定 vLLM 更该靠增大 batch 摊薄权重读取
✔ 能说出 decode 为何 memory-bound 并算出某个模型的理论最高 TPS
核心知识点详解
  • prefill 算力密集、decode 带宽密集:prefill 一次算出整个 prompt 的 KV,矩阵乘并行度高、算术强度高,接近 compute-bound;decode 每步只产 1 个 token 却要把权重全读一遍,算术强度约 1 FLOP/Byte,严重 memory-bound。
  • 理论最快 TPS ≈ 带宽 / 模型大小:decode 吞吐上界 ≈ 显存带宽 ÷ 权重体积。H100 约 3.35TB/s、8B FP16 权重 16GB:3.35e12/16e9≈210 tokens/s;INT8 一半权重则理论 TPS 近翻倍,这是量化与 KV 优化的物理依据。
  • 常见坑:拿算力卡跑 decode:小生成负载买「算力爆炸」的卡,结果带宽打满、算力闲置,钱花错地方。先判断负载受带宽还是算力约束,再决定买带宽 or 上 batch。
KV Cache 管理PagedAttention 分页前缀缓存命中复用RadixAttention 前缀树块表 block table碎片浪费 60%–80%→95%+
学习路径
  1. 读 2.2:理解 PagedAttention 分页与块表映射、前缀缓存命中复用
  2. 跑前缀缓存命中示例,观察命中复用时 TTFT 显著下降
  3. 完成 2.6 自测:估算单序列 KV 显存并解释碎片如何被分页抹平
  4. 对接 M16:在 vLLM 确认 prefix caching 开启并记录命中率
✔ 能算出 KV 显存并讲清 PagedAttention 如何把显存利用率提到 95% 量级
核心知识点详解
  • KV 显存按 token 算:每 token KV = 2 × 层数 × (KV_heads×head_dim) × 字节数。PagedAttention 把 KV 切成固定小块按需分配、非连续存放,把显存利用率从典型 20% 提到 95% 量级。
  • 前缀缓存命中复用:共享前缀(System Prompt / 多轮开头)命中时无需重算,TTFT 显著下降;命中率 80% 时 TTFT 可降约 5×。vLLM 用 --enable-prefix-caching 开启并记录命中率。
  • 常见坑:给每序列预分配连续显存:不切块时按 max_len 预留,窄分布下空间被碎片浪费,可并发数被压到十分之一。选引擎务必确认内存管理是分页式(PagedAttention)的。
批处理调度静态批 vs 连续批FCFS / 优先级 / 抢占吞吐 ~ 并发^0.72TPOT ~ log(并发)max-num-seqs 权衡
学习路径
  1. 读 2.3:理解连续批处理与 max-num-seqs 的权衡
  2. 跑并发扫描示例,观察吞吐 ~ 并发^0.72、TPOT ~ log(并发)
  3. 完成 2.6 自测:解释为什么并发数不是越大越好
  4. 对接 M16:调 vLLM max-num-seqs 找吞吐与 P99 的平衡点
✔ 能根据吞吐 / P99 曲线选 max-num-seqs 并说清连续批的提升来源
核心知识点详解
  • 连续批 vs 静态批:静态批等整批都完才换下一批;连续批不等完整序列,谁先完成谁先进。经验曲线:吞吐 ~ 并发^0.72、TPOT ~ log(并发)——并发涨 4 倍吞吐只涨约 2.4 倍,还换来 P99 上涨。
  • max-num-seqs 是核心旋钮:vLLM --max-num-seqs 决定单次 batch 家数:调大摊薄权重读取、提吞吐,代价是单请求排队变长。应在压测曲线上选「吞吐已见顶而 P99 还能忍」的平衡点,不是越大越好。
  • 常见坑:并发无限涨:排队论下并发过高时吞吐饱和、延迟线性恶化,P99 崩掉。记住「并发数不是越大越好」,超限多副本横向拆,别让同一副本死扛全量。
分块预填充长 prompt 分块交织max-num-batched-tokens改善 P99 尾延迟峰值吞吐略降
学习路径
  1. 读 2.4:理解长 prompt 分块交织如何避免阻塞 decode
  2. 跑长短 prompt 对比,观察 TTFT 与 P99 尾延迟变化
  3. 完成 2.6 自测:说明 chunked prefill 为何峰值吞吐略降
  4. 对接 M16:为长上下文场景配置 max-num-batched-tokens
✔ 能说清 chunked prefill 用峰值吞吐换尾延迟的机制
核心知识点详解
  • chunked prefill 交错长 prompt:把长 prefill 切块与 decode 交错执行,避免一个大 prompt 独占整卡几十毫秒、卡住别的 decode。代价是峰值吞吐略降,换来 P99 尾延迟明显改善。
  • max-num-batched-tokens:vLLM 用 --max-num-batched-tokens 控制单步最多处理的 token 数,长上下文尤其敏感;块太大又滑回「堵 decode」,块太小则 prefill 步数变多、TTFT 变高,需压测取中间值。
  • 常见坑:长短均匀也乱开:负载长短均匀时过度切块反而增加调度开销、降低有效吞吐。只有存在长 prompt 拖累尾部时才开,按你的输入长度分布决定。
KV 显存与量化KV 显存公式GQA / MQA 压缩FP8 / INT4 KV 量化分层滑窗驱逐KV 卸载到 CPU
学习路径
  1. 读 2.5:按公式估算 KV 显存,理解 GQA / MQA 压缩原理
  2. 跑 FP8 / INT4 KV 量化示例,对比显存节省与精度变化
  3. 完成 2.6 自测:口算 8B GQA 32K 单序列约 2GB
  4. 对接 M16:显存受限时量化 KV 并复核是否掉点
✔ 能口算单序列 KV 显存并决定何时量化 KV / 卸载到 CPU
核心知识点详解
  • KV 显存公式口算:每 token 每层 KV = 2×KV_heads×head_dim,再 ×层数×字节。GQA 把 KV heads 缩到 1/2–1/4;8B GQA 在 32K 上下文下单序列约 2GB——这决定单卡能并多少并发。
  • KV 量化:FP8 / INT4:KV Cache 是最吃显存的之一,量化到 FP8/INT4 可把 KV 显存几乎减半 / 减到 1/4,从而多塞并发(vLLM 参数如 --kv-cache-dtype fp8)。记得量化后复核长上下文质量。
  • 常见坑:只量化权重不量 KV:长上下文场景瓶颈常是 KV 显存而非权重。只动权重、KV 仍 FP16,并发照样上不去。先算权重与 KV 各占比重,再决定量化哪一个。
经验值与自测8B GQA 32K ≈ 2GB/序列prefix 命中 80% 降 TTFT 5×80GB 单卡约 7 并发
学习路径
  1. 完成 2.6 自测:给出 8B GQA 32K 单序列 KV 显存估算与 80GB 单卡并发上限
  2. 复述 prefix 命中 80% 为何能把 TTFT 降约 5 倍
  3. 把三个经验值(KV 显存 / 并发 / prefix 加速)并入一张速查卡
✔ 能把三个经验值一口讲清,并当场给出某个模型这些数的估算依据
核心知识点详解
  • 三张速查卡:8B GQA 32K ≈2GB/序列;prefix 命中 80% 令 TTFT 降约 5×;80GB 单卡短上下文约 7 并发。面试被问「这模型要几卡」第一反应就是查这三条。
  • 估算骨架:给任意模型估容量:先权重(参数×bit/8),再 KV(公式),再除以可用显存得单卡并发上界,进而折算出 QPS。能把推导途讲出来比背结论可信。
  • 常见坑:背结论不背推导:只记得「8B 要多少」,换个模型就懵。把公式与量级记牢、能口算任意规模,才有含金量。
学习路径

1.1 Prefill 与 Decode:两个完全不同的阶段

阶段做什么计算特征瓶颈优化方向
Prefill(预填充)一次性处理整个输入 prompt,计算并缓存 KV矩阵乘为主,并行度高算力受限(compute-bound)大 batch、chunked prefill、FP8
Decode(解码)逐 token 生成,每步复用 KV Cache每步只算 1 个 token显存带宽受限(memory-bound)增大并发 batch、量化权重与 KV、投机解码
首 token 延迟 TTFT主要取决于 prefill与输入长度近似线性长 prompt 卡首字前缀缓存、chunked prefill 与 decode 交织
生成速度 TPOT / 吞吐主要取决于 decode与并发数强相关显存带宽连续批处理提高并发、量化权重
python# 用算术强度判断瓶颈:算力 / 访存量
def roofline(flops, bytes_moved, gpu_tflops=990, gpu_bw_gbs=3350):
    """H100 SXM 参考值:约 990 TFLOPs(BF16稠密) / 3.35 TB/s"""
    intensity = flops / bytes_moved                      # FLOPs per Byte
    ridge = gpu_tflops * 1e12 / (gpu_bw_gbs * 1e9)       # 拐点强度
    return {"intensity": round(intensity, 2), "ridge": round(ridge, 1),
            "bound": "compute" if intensity > ridge else "memory"}

# Decode 阶段:batch=1,每 token 需要读全部权重
N, d = 8e9, 8192                          # 8B 模型
flops  = 2 * N                            # 每 token 约 2N FLOPs
bytes_ = 2 * N                            # BF16 权重读一遍:2N 字节
print("decode batch=1 ->", roofline(flops, bytes_))
# 强度约 1 FLOP/Byte,远低于拐点 -> 严重 memory-bound
# 结论:单请求推理时 GPU 算力大量闲置,必须靠「增大并发 batch」摊薄权重读取成本

# 同一个权重读一遍服务 32 条请求,算术强度提升约 32 倍
print("decode batch=32 ->", roofline(flops * 32, bytes_))

# 另一个视角:理论最快生成速度 ≈ 显存带宽 / 模型大小
bw, model_gb = 3350e9, 16               # 8B BF16 约 16GB
print(f"batch=1 理论上限 ≈ {bw / model_gb:.0f} tokens/s(实际会更低)")
# 这解释了为什么「量化到 4bit」能显著提速:权重变小 -> 读得更快

prefill 与 decode 的算力差异可用一个数字刻死:处理 L 个输入 token 的 prefill 近似 2·N·L FLOPs,而 decode 每步只有 2·N FLOPs(N 为参数量)。因此 L=8192 的一次 prefill ≈ 8192 次 decode 步的算力——这解释了为什么长 prompt 的 TTFT 昂贵,也让「prefix caching 把已算过的 L 直接归零」成为最大杠杆之一。

python# prefill 与 decode 的算力/带宽对照(N 参数、L 输入、B 并发 decode)
N, L, B = 8e9, 8192, 32
prefill_flops = 2 * N * L                  # 一次性
decode_flops  = 2 * N                       # 每个 decode step
print("prefill FLOPs:", f"{prefill_flops:.2e}")       # 约 1.3e+14
print("decode  step FLOPs:", f"{decode_flops:.2e}")   # 约 1.6e+10
print("等价 decode 步数:", int(prefill_flops / decode_flops))   # 8192
print("decode 每步读权重 GB:", round(2*N/1024**3, 1))          # 约 14.9
# 结论:prefill 用算力换时间,decode 用带宽换时间;两者的最优硬件与
#       batching 策略完全不同 —— 这正是 PD 分离的动机。
✔
一个立刻可用的诊断方法:推理慢,先问三个问题:① 是首 token 慢还是后续 token 慢?前者看 prefill 与输入长度;后者看 decode 与并发。② GPU 利用率是多少?利用率低而延迟高,基本就是 memory-bound,需要提高并发或量化。③ 显存被什么占了?权重、KV Cache、还是框架开销?很多时候是 KV Cache 顶掉了可用 batch,导致并发上不去。

1.2 KV Cache 管理:PagedAttention 与前缀缓存

KV Cache 是推理显存的主要占用方,也是吞吐的关键约束。传统实现按最大长度预分配连续显存,造成严重碎片与浪费(通常浪费 60%–80%)。PagedAttention 借鉴操作系统分页思想,把 KV Cache 切成固定大小的块按需分配,几乎消除碎片,使可用 batch 大幅增加——这是 vLLM 高吞吐的核心。

python# 显存预算测算:决定你能开多大并发(容量规划的第一张表)
def vram_plan(params_b, weight_bits, layers=32, kv_heads=8, d_head=128,
              dtype_kv=2, max_len=8192, gpu_gb=80, reserve_gb=6):
    weights = params_b * weight_bits / 8                      # 权重占用 GB
    per_seq_kv = (layers * max_len * kv_heads * d_head * 2 * dtype_kv) / 1024**3
    free = gpu_gb - reserve_gb - weights
    max_concurrency = max(0, int(free / per_seq_kv))
    return {"weights_gb": round(weights, 1), "kv_per_seq_gb": round(per_seq_kv, 2),
            "free_gb": round(free, 1), "max_concurrency": max_concurrency}

print("8B BF16, 8K 上下文:", vram_plan(8, 16))
print("8B INT4, 8K 上下文:", vram_plan(8, 4))
print("8B BF16, 32K 上下文:", vram_plan(8, 16, max_len=32768))
# 观察:量化把权重从 16GB 降到 4GB,多出来的 12GB 直接变成并发能力;
#       而上下文从 8K 拉到 32K,并发能力下降到 1/4 —— 长上下文是昂贵的。
python# 前缀缓存收益估算:命中率 h 对 TTFT 的直接影响
def prefix_cache_effect(prompt_len, hit_ratio, prefill_ms_per_1k=90):
    cached   = int(prompt_len * hit_ratio)
    fresh    = prompt_len - cached
    ttft_before = prompt_len / 1000 * prefill_ms_per_1k
    ttft_after  = fresh / 1000 * prefill_ms_per_1k
    return {"ttft_before_ms": round(ttft_before),
            "ttft_after_ms": round(ttft_after),
            "speedup": round(ttft_before / max(1, ttft_after), 2)}

print(prefix_cache_effect(8192, 0.80))
# 预期:{'ttft_before_ms': 737, 'ttft_after_ms': 147, 'speedup': 5.0}
# 经验:多轮对话 / 同一文档问答命中率常 60%–90%;命中率 <20% 时收益
#       会被哈希与块管理开销吃掉,不要为了它增加复杂度。

2.3 批处理:从静态批处理到连续批处理

批处理是推理吞吐的命脉。最简单的是静态批处理(static batching):攒够 N 个请求一起算,但要等最慢的那个生成完才释放——短请求被长请求拖死,GPU 在等待时大量闲置。现代引擎几乎都用连续批处理(continuous batching / in-flight batching):每个请求独立排队、生成完一步就立即离开、新请求随时填充空位,GPU 始终有活干。

维度静态批处理连续批处理
请求聚合方式凑满固定大小再算动态调度,完成即离开
短请求体验被同批长请求拖慢不受影响,生成完即返回
GPU 利用率低(等待期闲置)高(始终有 sequence 可算)
实现复杂度低高(需管理变长 batch)
代表实现早期 Triton 动态批、简单封装vLLM / SGLang / TensorRT-LLM 默认
python# 连续批处理的调度骨架(直觉版):每个 step 只做「凑当前活着的序列然后跑一步」
class ContinuousScheduler:
    def __init__(self, max_seqs=128):
        self.waiting = []          # 已到达但未开始
        self.running = []          # 正在 decode 的序列
        self.max_seqs = max_seqs

    def step(self):
        # 1) 从 waiting 补充到 running,直到达到并发上限
        while self.waiting and len(self.running) < self.max_seqs:
            self.running.append(self.waiting.pop(0))
        # 2) 对 running 里所有序列执行一步 decode(一次前向,batch 推理)
        logs = engine.forward(self.running)
        # 3) 完成的序列立即离场,腾出名额给 waiting(这是与静态批处理的关键区别)
        done = [s for s in self.running if s.finished]
        self.running = [s for s in self.running if not s.finished]
        return logs, done

# 调度策略的三种常见选择:
#  FCFS(先到先服务):公平、实现简单,但一个超长请求会占用名额;
#  优先级:按租户等级/付费/紧急度排序,高优先调度,需配合配额防饿死;
#  抢占:低优序列跑到预算上限时被暂停(KV 暂存),高优插队,恢复时续跑。
python# 连续批处理的收益曲线:吞吐随并发次线性上升,TPOT 近似对数变差
import math
def cb_curve(max_seqs_list, tps_base=1.0, tpot_base=25):
    for m in max_seqs_list:
        tps  = tps_base * m ** 0.72                  # 经验拟合:吞吐 ~ 并发^0.72
        tpot = tpot_base * (1 + 0.9 * math.log2(max(1, m)))
        print(f"max_seqs={m:>4} 相对吞吐={tps:5.1f} TPOT={tpot:6.1f}ms")

cb_curve([1, 8, 32, 128, 256])
# 预期输出(约):
# max_seqs=   1 相对吞吐=  1.0 TPOT=  25.0ms
# max_seqs=  32 相对吞吐= 11.9 TPOT=  79.0ms
# max_seqs= 128 相对吞吐= 30.7 TPOT= 137.0ms
# max_seqs= 256 相对吞吐= 51.4 TPOT= 162.0ms
# 结论:吞吐 ~ 并发^0.72(次线性),延迟 ~ log(并发)(超线性变差);
#       最优 max_seqs 由 SLA 定,通常落在 64–256。
调度策略优点代价 / 风险
FCFS 先到先服务公平、可预测、实现简单长请求阻塞短请求(队头阻塞)
优先级调度保障付费/高优用户体感需配额机制防止低优饿死
抢占(preemption)应对突发高优流量需保存/恢复 KV,增加复杂度与碎片
✔
调 max-num-seqs 的本质:它就是在调「连续批处理的目标并发」。调大 → 吞吐上升、单请求 TPOT 变差(每次权重读取要服务更多序列);调小 → 延迟好但 GPU 吃不饱。最优值由你的 SLA 决定,不是越大越好。压测时把它当自变量扫描(见第 8 章)。

2.4 Chunked Prefill:让长 prompt 不再阻塞 decode

连续批处理解决了 decode 侧,但 prefill 仍是一个「大块」:一个 8K token 的 prompt 要一次性算完才能开始出字,期间 batch 里的其他短请求 decode 被卡住,尾延迟飙升。Chunked Prefill 把长 prompt 切成若干小块,每块算完就把槽位让给 decode 请求,二者交织调度,既不让长请求独占、也不让短请求饿死。代价是 prefill 本身略有开销(小块间的调度与拼接),但尾延迟改善显著。

python# Chunked Prefill 的直觉:把 prefill 按 token 块切分,与 decode 交织
PREFILL_CHUNK = 512                      # 每个 prefill 块最大 token 数

def schedule(running_prefills, running_decodes):
    plan = []
    # 先给等待中的 decode 各一步(保障 TPOT)
    for d in running_decodes:
        plan.append(("decode", d))
    # 再给每个未完成 prefill 喂一个 chunk(不一次喂完,避免占满)
    for p in running_prefills:
        chunk = p.take_chunk(PREFILL_CHUNK)
        plan.append(("prefill", p, chunk))
        if p.done: running_prefills.remove(p)
    return plan                            # decode 与 prefill 混合进同一次前向

# 效果:长 prompt 用户仍要走多个 step 才出首字(TTFT 略升),
#       但同 batch 的短请求 decode 几乎不被影响(P99 大幅下降)。
bash# vLLM 开启 chunked prefill 并控制其与 decode 的 token 预算配比
vllm serve Qwen/Qwen3-8B \
  --enable-chunked-prefill \
  --max-num-batched-tokens 4096 \      # 单次迭代总 token 预算(prefill+decode 共享)
  --max-num-seqs 128                    # 并发序列上限
# max-num-batched-tokens 是核心旋钮:
#   调小(如 2048) -> 每次迭代算得更少,decode 抖动量小、P99 更好,吞吐略降;
#   调大(如 8192) -> prefill 更快、吞吐更高,但长 prompt 更易阻塞 decode。
# 经验:交互服务取 2048–4096;批量离线可取 8192 以上。
# 配合 prefix caching 时,已缓存的前缀块不再计入本预算,TTFT 进一步下降。
⚠
Chunked Prefill 的副作用:它把 prefill 的算力需求摊薄到多个 step,因此单次前向的 compute 占比下降、memory 占比上升,对纯 decode 密集的负载峰值吞吐略降。实践中通常设一个 chunk 大小(如 512–2048),并与 prefix caching 配合:已缓存的前缀块无需再算,进一步缩短 TTFT。

2.5 KV Cache 显存公式、量化与驱逐

KV Cache 的显存占用直接决定你能开多大并发。公式为:每序列 KV 字节 = 层数 × 序列长度 × KV 头数 × 每头维度 × 2(K 和 V)× 每元素字节。当模型用 GQA/MQA 时,KV 头数远小于注意力头数,从而大幅压缩 KV——这是长上下文能跑起来的前提。

python# KV Cache 显存公式 + 实例(注意 GQA/MQA 的压缩效果)
def kv_per_seq_gb(layers, seq, kv_heads, d_head, bytes_per=2):
    return layers * seq * kv_heads * d_head * 2 * bytes_per / 1024**3

# Llama-3-8B: 32 层, 32 注意力头, GQA 下 kv_heads=8, d_head=128
print("8B GQA 8K :", round(kv_per_seq_gb(32, 8192, 8, 128), 3), "GB/序列")
# 对比若用 MHA (kv_heads=32): 4 倍 -> 约 2.0 GB/序列,并发直接砍到 1/4

# 实例:单卡 80GB,权重 16GB(BF16 8B),保留 6GB 框架开销
free = 80 - 6 - 16
print("8B GQA 8K  最大并发:", int(free / kv_per_seq_gb(32,8192,8,128)))   # 约 29
print("8B GQA 32K 最大并发:", int(free / kv_per_seq_gb(32,32768,8,128)))  # 约 7
python# KV 量化后的显存账本:FP16 -> FP8 -> INT4 的并发增益
def kv_quant_gain(layers=32, seq=32768, kv_heads=8, d_head=128, free_gb=39):
    base = layers*seq*kv_heads*d_head*2*2/1024**3          # FP16
    return {f"{b}bit": {"kv_gb": round(base*b/16, 2),
                        "max_conc": int(free_gb/(base*b/16))}
            for b in (16, 8, 4)}
print(kv_quant_gain())
# 预期(约):
# 16bit: kv_gb≈2.0  max_conc≈19
#  8bit: kv_gb≈1.0  max_conc≈39
#  4bit: kv_gb≈0.5  max_conc≈78
# 风险:FP8 KV 通常几乎无损;INT4 KV 在长依赖 / 低资源语言上掉点明显,
#       必须用长上下文评测集专门验证,不要只看短问答。
注意力类型KV 头数(示例)相对 KV 体积说明
MHA(多头)= 注意力头数(如 32)1×(基准)早期模型,KV 最大
GQA(分组查询)如 8(Llama-3-8B)/ 4(70B)1/4–1/82024 后主流,几乎无损
MQA(多查询)11/32极致压缩,质量略损

KV 量化是长上下文的关键手段:把 KV 从 FP16 压到 FP8(体积减半)或 INT8(减半),4bit 进一步减半但精度风险明显上升——长依赖、低资源语言、数值敏感任务容易掉点,必须专门验证。当显存仍不够时,还有两类兜底:① 分层/滑窗驱逐(如 H2O、StreamingLLM 类的重计算策略):保留最近若干层或最近 token 的 KV,丢弃不重要历史,用少量质量损失换显存;② KV 卸载到 CPU 内存 / SSD:显存省了,但每次访问要多走 PCIe / NVMe,带宽下降一到两个数量级,只适合「偶尔访问的历史 KV」而非热路径。

✔
KV 管理的优先级:显存压力下正确的降级顺序是:先开 PagedAttention 消除碎片 → 再开 prefix caching 复用 → 再 KV 量化到 FP8 → 仍不够才考虑驱逐/卸载。每一步都先用业务评测集验证质量,不要一次性全上。

2.6 动手练习与自测

  1. 用 roofline 判断 batch=1 与 batch=32 的 decode 分别是 compute 还是 memory bound,并说出结论(判据:能指出 batch=1 强度约 1 FLOP/Byte,远低于拐点)。
  2. 写出 KV Cache 显存公式,并算出 8B / GQA(8 头, d=128) / 32K 的单序列占用与 80GB 单卡可开并发(参考答案:约 2GB/序列,约 7 并发)。
  3. 说明为什么 chunked prefill 能改善尾延迟、代价是什么(参考答案:把长 prompt 分块与 decode 交织;代价是峰值吞吐略降)。
  4. 给定 prefix 命中率 80%、prompt 8K,估算 TTFT 的下降倍数(参考答案:约 5 倍)。
  5. 解释 max-num-seqs 从 64 调到 256 时吞吐与 TPOT 各如何变化(参考答案:吞吐次线性上升,TPOT 近似对数变差)。
✔
自测判据:能从「带宽 / 算力 / 显存 / 碎片」四个物理约束出发推导每项优化的收益与代价,就真正掌握了推理第一性原理。
量公式 / 经验值
KV / 序列2 × L × seq × kv_heads × d_head × bytes
8B GQA(8,128) 32K≈ 2 GB/序列 → 80 GB 单卡约 7 并发
batch=1 算术强度≈ 1 FLOP/Byte(memory bound)
prefix 命中 80%TTFT 约降 5 ×
max-num-seqs 64→256吞吐次线性上升,TPOT 近似对数变差

3. 推理引擎与关键优化

知识结构图 · 推理引擎与关键优化
推理引擎与关键优化5 大知识域 · 25 个知识点
引擎选型vLLM 通用首选SGLang 前缀树复用TensorRT-LLM 极致性能TGI / llama.cpp / Ollamamax-model-len 按 p99gpu-mem-util 0.90–0.92
学习路径
  1. 读 3.1:按表对比 vLLM / SGLang / TensorRT-LLM / llama.cpp 取舍
  2. 起一个 vLLM 服务,设 max-model-len 与 gpu-mem-util 0.90–0.92
  3. 完成 3.6 自测:说明本项目该选哪个引擎及理由
  4. 对接 M16:用 vLLM 后端替换朴素推理
✔ 能按负载在引擎间做选型并讲清每档核心参数的依据
核心知识点详解
  • vLLM 通用首选:自带 PagedAttention、连续批处理、量化与分布式,接口 OpenAI 兼容,vllm serve Qwen2.5-7B-Instruct --gpu-mem-util 0.92 一行起服务,绝大多数生产场景选它就够了。
  • SGLang 强在共享前缀:RadixAttention 前缀树对多轮 / Agent / 结构化输出的共享前缀复用更强(多轮 TTFT 降 40%–70%);TensorRT-LLM 求极致吞吐但开发重。
  • 关键参数别拍脑袋:--max-model-len 按 P99 请求长度设置而非模型上限,--gpu-mem-util 0.90–0.92 给 KV 与调度留余量。设太满会 OOM,设太低浪费显存、并发上不去。
  • 常见坑:不量输入就设长度:先统计输入长度 P50/P99 再设 max-model-len,否则要么吃光 KV 要么浪费显存,是工程基本功。
四类加速技术连续批处理投机解码前缀缓存PD 分离Chunked Prefill
学习路径
  1. 读 3.2:理解连续批处理 / 投机解码 / 前缀缓存 / PD 分离四类的适用条件
  2. 跑每类技术开与关的对比,观察吞吐与 TTFT 的变化
  3. 完成 3.6 自测:逐条说出每类技术适用与不适用场景
  4. 对接 M16:在 vLLM 开启前缀缓存复用
✔ 能按场景挑加速技术并解释其代价与不适用边界
核心知识点详解
  • 四类技术各有所长:连续批处理提平均吞吐;投机解码加速 decode 且不损精度;前缀缓存复用历史 KV 降 TTFT;Chunked prefill 保 P99;PD 分离把两阶段分开订制。
  • 不是全开都收益:投机解码在低接受率 / compute-bound 场景是负收益,PD 分离在小规模是净亏。每项技术都要开与关对比压测,用「吞吐↑ / TTFT↓」双指标拍板。
  • 常见坑:全功能拉满:把 speculative、前缀缓存、chunked prefill 一起开,互相抢显存与调度,复杂度暴涨收益互相抵消。按负载特征逐项启用并各留基线。
PagedAttention 架构逻辑块→物理块映射block table 块表Scheduler / Block Manager显存利用率 20%→95%吞吐提升 2–4×
学习路径
  1. 读 3.3:跟代码走一遍逻辑块到物理块的块表映射
  2. 理解 Scheduler / Block Manager 分工,观察显存利用率 20%→95%
  3. 完成 3.6 自测:解释吞吐为何可提升 2–4×
  4. 对接 M16:压测中对比开启分页前后的显存占用
✔ 能画出块表映射并解释显存利用率与吞吐提升的来源
核心知识点详解
  • 逻辑块→物理块映射:KV 按 16/32 token 的块切,维护 block table 记录逻辑块到物理位置,按需分配、非连续存储。Scheduler 管请求调度,Block Manager 管显存分配。
  • 显存利用率 20%→95%:传统按最大长度预分配连续缓冲区,典型浪费 60%–80%;分页几乎消除碎片。配合连续批处理,吞吐常在 2–4× 量级提升,是 vLLM 的核心卖点。
  • 常见坑:把注意力算法当瓶砖:吞吐上不去常不是注意力算得慢,而是显存放不下更多并发。先看显存利用率与并发数,再谈内核优化。
前缀树与约束解码RadixAttention 基数树多轮对话 TTFT 降 40%–70%JSON schema 约束解码Agent 树状探索
学习路径
  1. 读 3.4:理解 RadixAttention 前缀树如何复用共享前缀
  2. 跑多轮对话与 JSON schema 约束解码示例
  3. 完成 3.6 自测:说明 Agent 多轮场景为何受益
  4. 对接 M16:若用 SGLang,配置结构化输出并测多轮 TTFT
✔ 能解释前缀树把多轮 TTFT 降 40%–70% 的机制与约束解码的用途
核心知识点详解
  • RadixAttention 前缀树:以 token 序列的前缀建树节点,共享前缀(System Prompt、多轮开头、Agent 工具上下文)只存一份、多请求复用,多轮对话 TTFT 可降 40%–70%。
  • 结构化输出:SGLang / vLLM 支持 JSON schema 约束解码,让输出必然符合 schema(return_schema=... 指定),省去解析容错,是工具调用与 Agent 场景的关键。
  • 常见坑:前缀不稳定导致命中趋零:前缀混入随机 uid / 时间戳会让树分支爆炸、复用率归零。把 System Prompt 做成稳定常量,才能吃到前缀缓存收益。
边缘部署GGUF 量化格式Q4_K_M 甜点Q8_0 近乎无损-ngl 层放 GPU本地 / 隐私场景
学习路径
  1. 读 3.5:理解 GGUF 量化与 Q4_K_M 精度-速度甜点
  2. 跑 llama.cpp 示例,用 -ngl 把层放到 GPU 并观察加速
  3. 完成 3.6 自测:说明本地 / 隐私场景的取舍
  4. 完成本章自测:对比 Q4_K_M 与 Q8_0 的质量损失
✔ 能在消费级设备用 GGUF 跑起模型并说清 Q4/Q8 的取舍
核心知识点详解
  • GGUF 量化格式:llama.cpp 生态用 GGUF 单文件跑本地。Q4_K_M 是精度/速度甜点,Q8_0 近乎无损;Q8 文件更大但质量更好,按内存预算取舍。
  • 把层放 GPU:llama-server -m model.gguf -ngl 999 把尽可能多层放 GPU,带宽快得多;CPU-only 或 -ngl 20 适合低配,速度差一个数量级。
  • 常见坑:无脑 Q4 到底:数学 / 长链推理 / 代码对 Q4 敏感,低资源语言掉点明显。本地先用 Q8_0 定基线,确认 Q4 不掉点再下放。
学习路径

2.1 引擎选型与核心参数

引擎核心特色适合注意
vLLMPagedAttention + 连续批处理,生态最成熟通用自托管、开源模型首选参数众多,需按负载调优
SGLangRadixAttention 前缀复用 + 结构化输出优化多轮对话、共享前缀、Agent 工作流生态略小但增长快
TensorRT-LLMNVIDIA 深度优化,极致性能固定模型、追求极限吞吐编译复杂、迭代慢、绑定 NVIDIA
TGIHuggingFace 出品,易用快速上线、HF 生态极限性能略逊
llama.cpp / OllamaCPU / 端侧推理、GGUF 量化本地开发、边缘部署大规模服务不适合
云端服务各家托管推理无运维、弹性成本与数据合规需评估
bash# vLLM 生产启动示例(关键参数逐个说明)
vllm serve Qwen/Qwen3-8B \
  --host 0.0.0.0 --port 8000 \
  --tensor-parallel-size 2 \            # 张量并行:单卡放不下或想降延迟时用
  --max-model-len 32768 \               # 必须按真实需求设置,设太大会吃光 KV 空间
  --gpu-memory-utilization 0.92 \       # 留给 KV Cache 的比例,太高易 OOM
  --max-num-seqs 128 \                  # 最大并发序列数:吞吐与延迟的核心权衡
  --enable-prefix-caching \             # 共享前缀场景必开(多轮/同文档问答)
  --enable-chunked-prefill \            # 改善长 prompt 造成的尾延迟
  --quantization fp8 \                  # 权重 FP8 量化(需硬件支持)
  --kv-cache-dtype fp8 \                # KV 量化,长上下文关键
  --speculative-model Qwen/Qwen3-0.6B \ # 投机解码草稿模型
  --num-speculative-tokens 5

# 关键权衡:max-num-seqs 越大吞吐越高,但单请求延迟(TPOT)会变差。
# 必须按业务 SLA 找平衡点,而不是一味拉大。
bash# SGLang 生产启动:RadixAttention + 结构化输出 + 投机解码
python -m sglang.launch_server --model-path Qwen/Qwen3-8B \
  --port 30000 --tp 1 --mem-fraction-static 0.88 \
  --enable-torch-compile \          # 编译加速,首请求慢、后续快
  --speculative-algorithm EAGLE --speculative-draft-model-path ./eagle-qwen3 \
  --schedule-policy lpm \            # 最长前缀匹配优先 -> 提升前缀复用命中率
  --max-running-requests 256
# 经验对比(8B / 单 H100 / 8K 上下文,社区与实测常见量级):
#   vLLM 连续批处理 + PagedAttention:吞吐基线,生态最稳;
#   SGLang RadixAttention:共享前缀场景 TTFT 降 30%–60%;
#   两者裸吞吐同档,差异主要在「前缀复用结构」与「结构化生成」。
⚠
两个最常见的错误配置:① --max-model-len 设成模型上限(如 128K)却没做长度校验——少量长请求会把 KV 空间吃光,导致其他请求排队或 OOM。正确做法是按实际 p99 输入长度 × 安全系数设置,并对超长请求做截断或拒绝。② --gpu-memory-utilization 设到 0.98——框架本身需要显存,容易在压力下 OOM。0.90–0.92 通常更稳。

2.2 四类加速技术的适用条件

技术原理收益前提条件
连续批处理请求按到达顺序动态进出 batch,而非等整批生成完吞吐可提升数倍引擎内置(vLLM 默认开);需要足够并发
投机解码小模型草拟多个 token,大模型一次并行验证延迟降 1.5–2.5 倍有同族小模型;草稿命中率高才有效
前缀缓存共享前缀的 KV 只算一次首 token 延迟大幅下降请求间前缀重复率高(系统提示、同文档)
PD 分离prefill 与 decode 部署在不同节点,各自优化资源利用率与尾延迟改善架构复杂、需 KV 传输;中大规模才划算
Chunked Prefill长 prompt 分块与 decode 交织尾延迟显著改善引擎支持;轻微影响峰值吞吐
python# 用压测找到「吞吐–延迟」的平衡点(容量规划的核心实验)
import asyncio, time, statistics

async def bench(client, concurrency, n_requests=200, prompt=None):
    sem = asyncio.Semaphore(concurrency)
    lat, ttft = [], []

    async def one():
        async with sem:
            t0 = time.perf_counter()
            stream = await client.chat_stream(prompt or DEFAULT_PROMPT)
            first = await anext(stream)                    # 首 token
            ttft.append(time.perf_counter() - t0)
            async for _ in stream: pass
            lat.append(time.perf_counter() - t0)

    t0 = time.perf_counter()
    await asyncio.gather(*[one() for _ in range(n_requests)])
    wall = time.perf_counter() - t0
    return {"concurrency": concurrency,
            "throughput_rps": round(n_requests / wall, 1),
            "p50_latency": round(statistics.median(lat), 3),
            "p95_latency": round(sorted(lat)[int(len(lat) * 0.95)], 3),
            "ttft_p95": round(sorted(ttft)[int(len(ttft) * 0.95)], 3)}

async def sweep(client):
    for c in (1, 4, 16, 32, 64, 128):
        print(await bench(client, c))
# 典型曲线:并发上升 -> 吞吐上升、延迟上升;到某点吞吐不再涨但延迟急剧上升。
# 那个拐点就是你的最优服务并发;线上按它配置限流与扩容阈值。
python# 实测投机解码是否划算:比较接受率与端到端 TPOT
def spec_gain(accept_rate, n_draft=5, draft_cost_ratio=0.15):
    # 期望接受 token 数(见 4.1 推导)
    e = sum(k * accept_rate**(k-1) * (1-accept_rate) for k in range(1, n_draft+1)) \
        + n_draft * accept_rate**n_draft
    # 每次「主模型前向」总成本 = 1(验证)+ draft 草稿成本
    cost = 1 + draft_cost_ratio * n_draft
    return {"accepted_tokens": round(e, 2), "speedup": round(e / cost, 2)}

for a in (0.4, 0.6, 0.8):
    print(a, spec_gain(a))
# 预期:0.4 -> speedup≈1.0;0.6 -> ≈1.7;0.8 -> ≈2.6
# 判据:实测 speedup < 1.2 就不要上线——草稿模型同样吃显存与算力。

3.3 PagedAttention 深入:块表映射与 vLLM 架构

PagedAttention 借鉴操作系统的虚拟内存分页:把每个序列的 KV Cache 切成固定大小的逻辑块,再映射到非连续的物理块,用一张块表(block table)记录映射。逻辑上连续、物理上离散,分配按需进行,几乎消除内部碎片(预留块)与外部碎片(空洞)。vLLM 论文报告:传统实现显存利用率通常只有 20%–40%,PagedAttention 可提升到 接近 95%–100%,对应并发容量提升数倍、整体吞吐提升 2–4 倍。

实现方式典型显存利用率后果
连续预分配(最大长度)20%–40%并发被严重限制,尾延迟高
PagedAttention(分页)95%–100%并发容量大幅提升,吞吐 2–4×

vLLM 的核心架构组件可以拆成几条协作的线:Scheduler(调度器)决定哪些序列进 batch、是否抢占;Block Manager(块管理器)负责物理块的分配/释放/共享;GPU Worker / Model Runner 执行实际前向;KV Cache Manager 维护分页 KV;对外是兼容 OpenAI 的 API Server。理解这条链路,你才能在出问题时定位是调度、内存还是计算。

python# 分页 KV 的块表映射(PagedAttention 的核心数据结构)
BLOCK = 16                                   # 每块 16 个 token 的 KV
class BlockTable:
    def __init__(self): self.pages = []      # 逻辑块 -> 物理块号
    def append(self, physical_id): self.pages.append(physical_id)
    def slot_of(self, pos):                  # 第 pos 个 token 落在哪
        return self.pages[pos // BLOCK], pos % BLOCK
bt = BlockTable()
for pid in (7, 2, 9): bt.append(pid)         # 物理块可以不连续
print("logical->physical:", bt.pages)        # [7, 2, 9]
print("token 20 ->", bt.slot_of(20))         # (2, 4):物理块 2,块内偏移 4
# 关键收益:逻辑连续即可、物理可离散 -> 按需分配,几乎零碎片;
#           相同内容的逻辑块可共享同一物理块(prefix caching 的基础)。

3.4 SGLang 的 RadixAttention 与结构化生成

SGLang 的 RadixAttention 用一棵基数树(radix tree)组织所有请求的 KV 前缀,不仅支持线性前缀共享,还能表达树状分支(如一个 prompt 的多个采样分支、多轮对话的分叉),自动在树中找到可复用节点。它在「多轮对话、同一文档反复提问、Agent 的树状探索」场景收益明显。此外 SGLang 对结构化生成(JSON / 正则约束解码)做了内核级加速,让 constrained decoding 几乎不掉速。

引擎核心机制最擅长的场景主要代价
vLLMPagedAttention + 连续批处理,生态最成熟通用自托管、开源模型首选、社区大超长共享前缀的树状复用不如 SGLang
SGLangRadixAttention 前缀树复用 + 结构化生成加速多轮对话、共享前缀、Agent 树状探索、结构化输出生态略小,调优资料较少
TensorRT-LLMNVIDIA 深度编译优化(CUDA graph、 fused 内核)固定模型追求极限吞吐/延迟编译复杂、迭代慢、绑定 NVIDIA 硬件
llama.cpp / OllamaGGUF 量化 + CPU/GPU 混合执行本地开发、边缘、消费级显卡大规模高并发服务不适合
TGIHuggingFace 出品,开箱即用快速上线 HF 生态模型极限性能略逊于 vLLM/TRT-LLM
python# 结构化生成:让模型只在合法 JSON 空间里输出(SGLang / 多数引擎支持)
# 用 JSON schema 约束解码,比「让模型自己写 JSON 再校验」更稳更快
import json
from sglang import function, gen, set_default_backend, RuntimeEndpoint

@function
def extract(s, question):
    s += "请只输出符合 schema 的 JSON:\n"
    s += gen("answer", json_schema={
        "type": "object",
        "properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
        "required": ["name", "age"]})
    return s["answer"]

# 约束解码的价值:① 输出 100% 可解析,省去「解析失败重试」;
#                 ② 不需要额外提示词描述格式,省 token;
#                 ③ 在 SGLang 里有内核加速,延迟接近非约束生成。
场景前缀复用机制典型收益说明
多轮对话历史前缀整段复用TTFT 降 40%–70%轮数越多,累积命中越高
同文档多问文档前缀共享prefill 算力降 60%+配合 chunked prefill 更佳
Agent 树状探索分支共享公共路径分支越多收益越大RadixAttention 强项
结构化 JSON 输出约束解码内核加速约束下几乎不掉速对比朴素实现省 30%+ 时间
n 选 1 / beam多候选共享前缀 KV显存只存一份前缀PagedAttention 块级共享

3.5 llama.cpp 与边缘 / 消费级部署

llama.cpp 用 C++ 实现、以 GGUF 格式打包量化权重,支持纯 CPU、CPU+GPU 混合(Metal / CUDA / Vulkan),是本地开发、消费级显卡与边缘设备的事实标准(Ollama 即基于它)。它的价值不是极致吞吐,而是极低门槛地把模型跑在笔记本、手机、树莓派上,适合数据不出端、离线、隐私敏感的场景。

GGUF 等级相对体积质量适用
Q8_0约 1/2(相对 FP16)几乎无损质量要求高的本地部署
Q5_K_M约 1/3轻微损失大多数本地场景的甜点
Q4_K_M约 1/4可接受显存/内存紧张时的默认
Q2_K约 1/6明显损失仅验证可行性,不用于生产
bash# llama.cpp 本地量化推理:一条命令跑起 GGUF
llama-cli -m Qwen3-8B-Q4_K_M.gguf \
  -p "用一句话解释 PagedAttention" -n 128 \
  -ngl 99            # 尽量多的层放到 GPU(-ngl 0 则纯 CPU)
# 让 Ollama 也能本地 serving(OpenAI 兼容)
ollama run qwen3:8b-q4_K_M
# 实测量级(M2 Pro 32G / 8B Q4_K_M):约 15–25 tokens/s,显存占用 ~5.5GB;
# 同模型 Q8_0 约 9GB、质量几乎无损但更慢;Q2_K 可跑但明显掉智,
# 只用于「验证可行性」,不要用于生产。
✔
什么时候用 llama.cpp 而不是 vLLM:判断标准很简单:是否需要高并发服务?是 → vLLM/SGLang;否,且要「在任意机器上能跑、零运维、数据不出端」→ llama.cpp/Ollama。很多团队的真实架构是两者并存:云端 vLLM 扛高并发,本地/边缘 llama.cpp 做离线或隐私场景。

3.6 动手练习与自测

  1. 为你的模型写一份 vLLM 启动参数清单,并逐条说明为什么这么设(判据:max-model-len 按 p99 而非模型上限、gpu-memory-utilization 取 0.90–0.92)。
  2. 对比 vLLM 与 SGLang 在你负载下的选择依据(参考答案:共享前缀/结构化输出/Agent 树状探索选 SGLang,否则 vLLM 更稳)。
  3. 画出 PagedAttention 的块表映射示例,说明为什么逻辑连续、物理离散能消除碎片。
  4. 给定接受率 0.4 / 0.6 / 0.8,估算投机解码加速比并判断是否上线(参考答案:≈1.0 / 1.7 / 2.6,<1.2 不上线)。
  5. 为「本地隐私场景」选一个 GGUF 等级并说明理由(参考答案:Q4_K_M 甜点、Q8_0 近乎无损但更慢)。
✔
自测判据:能根据负载特征(前缀重复率、结构化需求、并发规模、隐私要求)选出引擎与参数,而不是照抄一份配置,就说明这一章真正掌握。
场景推荐引擎 / 参数要点
共享前缀多SGLang(RadixAttention)前缀缓存命中率高
通用 / 稳定vLLMPagedAttention + 连续批处理
结构化 / AgentSGLang约束解码 + 树状探索
本地隐私llama.cpp GGUF Q4_K_M甜点;Q8_0 近无损但更慢
启动参数gpu-mem-util 0.90–0.92max-model-len 按 p99 而非上限

4. 推测解码:用草稿模型换延迟

知识结构图 · 推测解码
推测解码4 大知识域 · 17 个知识点
draft-then-verify草稿模型预测 n token主模型一次并行验证贪心接受 + 重采样期望收益公式 E边际验证成本极低
学习路径
  1. 读 4.1:理解草稿模型预测 n token、主模型一次并行验证的流程
  2. 代期望收益公式 E,观察不同接受率下的加速比
  3. 完成 4.4 自测:解释贪心接受与重采样的规则
  4. 对 M16:在 vLLM 评估是否值得开 speculative 解码
✔ 能算期望加速并根据接受率判断要不要上推测解码
核心知识点详解
  • 草稿 - 验证流程:草稿模型一次预测 n 个候选 token,主模型并行验证:连续匹配的前缀直接采纳,首次不一致处用主模型分布重采样,保证输出等价于主模型自回归。
  • 期望收益与加速比:加速 ~ 接受率 α 相关,α 越高、n 越大、验证边际成本越低收益越好(α=0.7、n=5 约 2.5×)。用 tokens_per_forward 实测开与关的吞吐差,比估算更可靠。
  • 常见坑:compute-bound 也硬开:收益只在 decode 带宽瓶颈成立;prefill 主导时验证开销反超收益、吞吐下降。压测验证再开,别拍脑袋。
接受率与加速比α=0.7 n=5 ≈ 2.5×α=0.9 ≈ 4.0×α=0.5 ≈ 1.9×tokens_per_forward 实测
学习路径
  1. 读 4.1:记下接受率与加速比的对照(α=0.7 n=5 ≈ 2.5× 等)
  2. 跑加速比敏感性示例,观察接受率与 n 的敏感度
  3. 完成 4.4 自测:用 tokens_per_forward 实测一台服务
  4. 对接 M16:压测时实测加速比并做性价比判断
✔ 能读压测的加速比并解释为何加速比 <1.2 不上线
核心知识点详解
  • 接受率 - 加速对照:经验值:α=0.7、n=5 约 2.5×;α=0.9 可到 4.0×;α=0.5 仅 1.9×。接受率是杠杆核心,决定值得投入多少。
  • 加速比 <1.2 不上线:上线要承担草稿模型的额外显存与调度复杂度;实测加速比低于 1.2 时收益撑不起成本,直接关闭。
  • 常见坑:照搬论文加速比:加速比高度依赖你的 prompt 分布与输出长度。必须在自己的负载上开与关各压一遍,用实测数说话。
四种草稿来源独立小模型Medusa 多头预测EAGLE 特征层预测n-gram 查表零训练
学习路径
  1. 读 4.2:对比独立小模型 / Medusa / EAGLE / n-gram 四种草稿来源
  2. 跑 n-gram 查表示例(零训练)并观察接受率
  3. 完成 4.4 自测:针对自己的模型选草稿来源
  4. 完成本章自测:说明四种来源的训练成本差异
✔ 能按训练成本与收益给自己的模型选草稿来源
核心知识点详解
  • 四种来源训练成本递增:n-gram 查表零训练直接可用;独立小模型要额外训一个;Medusa 在主干上加多头预测头;EAGLE 在特征层预测更准但更复杂。按「要不要动原模型」选。
  • 选型看分布与预算:有预算训一个好的草稿模型抬高接受率;不想动原模型就上 n-gram,代价是接受率平平。草稿与主模型分布越接近越好。
  • 常见坑:为省事硬用 n-gram:通用生成场景 n-gram 命中率低、几乎无收益。至少评估独立小模型,用实测接受率决定值不值。
适用条件仅 decode 带宽瓶颈划算草稿与主模型分布接近长生成累计收益大加速比 <1.2 不上线
学习路径
  1. 读 4.3:理解仅 decode 带宽瓶颈时才划算的前提
  2. 跑草稿与主模型分布差异对比,观察接受率受影响
  3. 完成 4.4 自测:说明长生成场景累计收益大
  4. 完成本章自测:判断当前负载是否值得上推测解码
✔ 能用一句话说清当前负载适不适合推测解码
核心知识点详解
  • 三个前提缺一不可:① 仅 decode 带宽瓶颈划算;② 草稿与主模型分布足够接近(接受率高);③ 长生成累计收益大——输出才 20 token 就算 4× 也省不了多少。
  • 一句话判断:输出足够长 + 带宽受限 + 草稿够准,三条件都满足才开,否则按成本收益关掉。
  • 常见坑:短输出也开:输出极短的分类/抽取,投机解码省不了半毫秒还引入延迟与显存开销。先看平均输出长度再决定。
学习路径

4.1 draft-then-verify 与期望加速

推测解码(speculative decoding)的核心是 draft-then-verify:用一个小而快的草稿模型连续预测 n 个 token,再交给大模型一次并行验证。验证时采用「贪心接受 + 重采样」:大模型只要在草稿 token 上的分布概率不低于草稿模型,就接受;否则从修正分布中采样一个。由于大模型一次前向就能验证 n 个 token,而 decode 是 memory-bound(权重只读一遍),当接受率足够高时,等效于免费生成多个 token。

python# 期望加速的直觉:给定接受率 alpha,草稿步数 n
# 一次大模型前向「产出」的有效 token 期望约为:
#   E = sum_{k=1..n} k * alpha**(k-1) * (1-alpha)  +  n * alpha**n
# 单次前向成本 ~ 1(一次 decode),朴素 decode 每次前向只产 1 个 token
# 期望加速比 ~ E / 1 = E(忽略草稿模型成本,草稿远小于主模型)
import numpy as np
def expected_speedup(alpha, n=5):
    k = np.arange(1, n+1)
    e = np.sum(k * (alpha**(k-1)) * (1-alpha)) + n * (alpha**n)
    return round(float(e), 2)

for a in (0.5, 0.7, 0.9):
    print(f"alpha={a} n=5 -> 期望加速约 {expected_speedup(a)}x")
# alpha=0.7 -> 约 2.5x;alpha=0.9 -> 约 4x;alpha=0.5 -> 约 1.9x
# 结论:接受率每提升一点,加速比接近线性放大,所以「草稿模型要像主模型」是核心。
接受率 αn=5 期望加速说明
0.5约 1.9×收益有限,接近临界
0.7约 2.5×多数同族小模型可达
0.9约 4.0×高质量草稿(EAGLE 类)可达
python# 实测一次投机解码会话:统计接受率与真实加速(而非只看理论)
def analyze_spec(log):
    """log: [{'drafted': k, 'accepted': a}] 每次主模型前向记录"""
    tot_d = sum(r["drafted"] for r in log)
    tot_a = sum(r["accepted"] for r in log)
    return {"accept_rate": round(tot_a / max(1, tot_d), 3),
            "tokens_per_forward": round((tot_a + len(log)) / len(log), 2)}
# tokens_per_forward 含每次前向至少 1 个「自主」token
print(analyze_spec([{"drafted":5,"accepted":4},{"drafted":5,"accepted":3},
                    {"drafted":5,"accepted":5}]))
# 预期:{'accept_rate': 0.8, 'tokens_per_forward': 5.0}
# 判据:tokens_per_forward / (1 + 草稿成本) > 1.2 才值得开。
✔
为什么它有效:关键在于 decode 每步的瓶颈是「读权重」而非「算」。一次大模型前向不论验证 1 个还是 5 个 token,权重都只读一遍——边际验证成本极低。所以只要草稿命中的 token 多,验证就近似免费。反之若接受率低,反而白算草稿,收益归零甚至为负。

4.2 Medusa / EAGLE / n-gram:四种常见形态

方法草稿来源是否需要训练接受率/特点
独立小模型另训一个同族小模型做 draft需要(小模型)实现简单,但分布差异大时接受率一般
Medusa在主干末层挂多个 MLP 头,各自预测未来第 1/2/3… 个 token需要(加头微调)多头并行预测,加速稳定
EAGLE在特征层(而非 token 层)预测,用自回归头迭代需要(轻量微调)接受率更高,接近上界
n-gram 查表从输入/历史 n-gram 直接查下一个 token不需要零成本,适合重复性强的任务(模板、代码)

选型直觉:n-gram 查表 零成本、无需训练,是「先免费试」的首选,特别适合代码补全、模板化输出;Medusa / EAGLE 需要少量微调但接受率更高,适合追求极致延迟的生产服务。EAGLE 在特征层预测之所以更强,是因为它利用了主干已经算出的隐藏状态,比单纯从 token 预测更准。

bash# vLLM 里三种投机解码后端的启用方式(2026 常见)
# 1) 独立草稿模型
vllm serve Qwen/Qwen3-32B --speculative-model Qwen/Qwen3-1.7B \
  --num-speculative-tokens 5
# 2) EAGLE / EAGLE-3(特征层预测,接受率最高)
vllm serve Qwen/Qwen3-32B --speculative-model ./eagle3-qwen3-32b \
  --speculative-model-draft-tensor-parallel-size 1
# 3) n-gram 查表(零训练,代码补全 / 模板场景)
vllm serve Qwen/Qwen3-32B --speculative-model "[ngram]" \
  --num-speculative-tokens 4 --ngram-prompt-lookup-max 4
# 经验接受率:EAGLE-3 > 独立小模型 > n-gram(强重复域除外)。
# EAGLE-3 尤其适合「长生成 + 分布稳定」的推理与代码场景。

4.3 适用条件:什么时候该上推测解码

python# 该不该上投机解码:先估收益再决定(避免为「看起来快」买单)
def should_spec(avg_accept, n=5, draft_vram_gb=1.6, free_vram_gb=8):
    e = sum(k*avg_accept**(k-1)*(1-avg_accept) for k in range(1, n+1)) + n*avg_accept**n
    gain = e / (1 + 0.15*n)
    fits = draft_vram_gb <= free_vram_gb
    return {"speedup": round(gain, 2), "fits_vram": fits, "go": gain > 1.2 and fits}
print(should_spec(0.65))   # {'speedup': 1.75, 'fits_vram': True, 'go': True}
print(should_spec(0.35))   # speedup≈0.9 -> go False(反而变慢)
# 不适合:纯 prefill 重负载、创意写作(低接受率)、草稿与主模型差距大。
⚠
一个常见误用:有人为了「看起来更快」给所有服务都开推测解码。但草稿模型也要占显存与算力,且在接受率低时反而增加每 token 成本。正确做法是:先在目标负载上实测接受率与端到端 TPOT,确认加速比 >1.2 再上线。

4.4 动手练习与自测

  1. 推导接受率 α=0.7、n=5 时的期望加速(参考答案:约 2.5×),并说明为什么用「一次前向验证多个」等效免费。
  2. 对比独立小模型 / Medusa / EAGLE / n-gram 四种草稿来源的训练成本与接受率(判据:能说 EAGLE 在特征层预测、接受率最高)。
  3. 给定一次投机解码日志,算出接受率与 tokens_per_forward(判据:能判断是否 >1.2 才上线)。
  4. 举一个「不该上投机解码」的场景并说明原因(参考答案:纯 prefill 重负载或创意写作低接受率)。
  5. 解释为什么草稿模型不能太大(参考答案:草稿成本计入分母,太大反而拉低有效加速)。
✔
自测判据:能把接受率、草稿成本、主模型前向成本三者放进同一个收益公式再决定是否上线,而不是只看「用了投机解码」这个标签。
接受率 αn=5 期望加速(约)结论
0.41.0–1.2 ×不上线
0.6≈ 1.7 ×视负载上线
0.8≈ 2.5 ×明显收益
草稿来源独立小模型 / Medusa / EAGLE / n-gramEAGLE 在特征层预测、接受率最高

5. 量化:用精度换速度与显存

知识结构图 · 量化
量化5 大知识域 · 23 个知识点
量化方法选型GPTQ 误差补偿AWQ 激活感知FP8 原生张量核心KV Cache 量化QAT 量化感知训练
学习路径
  1. 读 5.1:对比 GPTQ / AWQ / FP8 的取舍与适用场景
  2. 跑不同方法量化同一模型,记录质量、显存与速度变化
  3. 完成 5.6 自测:按自己的负载选量化方案
  4. 对接 M16:产出 FP16 / INT8 / AWQ 的质量-速度-显存三角权衡表
✔ 能用一张三角权衡表对比各方法并按负载选型
核心知识点详解
  • 方法目的不同:GPTQ 用二阶 Hessian 补偿逐层量化误差;AWQ 感知激活、保护重要通道;FP8 走原生张量核心是全面折中(E4M3 范围 ±448);QAT 要训练。
  • 三角权衡表三栏:同一模型做 FP16 / INT8 / AWQ-INT4,测质量(评测集)、显存(权重体积)、速度(吞吐/QPS)三栏。FP16 最准最重,INT4 最省最快但可能掉点。
  • 常见坑:用公开任务单点测精度:公开 benchmark 代表不了你的业务。量化方案必须在自建业务评测集上按任务子类看掉点,否则上生产埋雷。
PTQ 与 QATPTQ 仅需校准QAT 需训练W8A16 / W4A16W8A8 激活量化离群值 outlier
学习路径
  1. 读 5.2:理解 PTQ 仅需校准、QAT 需训练的区别
  2. 跑 W8A16 / W8A8 示例,对比显存与精度变化
  3. 完成 5.6 自测:说明何时该上 QAT
  4. 完成本章自测:用校准集覆盖真实分布的一个检查点收尾
✔ 能说清 PTQ/QAT 流程与何时需要重训
核心知识点详解
  • PTQ 仅需校准:PTQ 跑一小段校准集算缩放 / 零点 / 补偿即可,零训练几乎零成本;QAT 要在训练中插入伪量化、重训一轮,成本高但精度好。
  • 何时上 QAT:当 PTQ 掉了不可接受的点、又无法退出量化时上 QAT。一般先 PTQ 试水,W8A8 / W8A16 通常够用,极端才 QAT。
  • 常见坑:校准集与业务分布不符:拿通用语料校准、一上业务就崩。校准集必须覆盖真实分布:中文 / 代码 / 长文本的比例贴近线上。
机制差异GPTQ 二阶 Hessian 补偿AWQ 保护重要通道SmoothQuant 难度迁移alpha=0.5 平衡
学习路径
  1. 读 5.3:理解 GPTQ 补偿 / AWQ 保护通道 / SmoothQuant 难度迁移
  2. 跑各方法的敏感度对比示例
  3. 完成 5.6 自测:解释 alpha=0.5 的平衡作用
  4. 完成本章自测:说出三种方法各自保护的对象
✔ 能讲清三种量化各自保护的对象(权重建模误差 / 活性通道 / 激活难度)
核心知识点详解
  • 三种保护对象:GPTQ 保护「权重建模误差」——用 Hessian 给敏感权重更小步长;AWQ 保护「活性通道」——少数激活大的通道不量化;SmoothQuant 把激活难度迁移到权重侧。
  • alpha=0.5 的平衡:SmoothQuant 的 alpha 决定把量化难度往权重还是激活迁移,0.5 表示均衡分配,两边的量化都更容易。
  • 常见坑:三种方法随便挑一个:优化点不同。对着「哪个退化在你任务上最痛」选:改建模误差→GPTQ,活性离群→AWQ,激活要量化→SmoothQuant。
数值格式INT8 / INT4FP8 E4M3 范围 ±448MXFP4 块级缩放能上 FP8 就上 FP8
学习路径
  1. 读 5.4:记住 INT8 / INT4 / FP8 / MXFP4 的精度速度取舍
  2. 用量化引擎对比 INT8 / FP8 的速度与显存
  3. 完成 5.6 自测:解释为何能上 FP8 就上 FP8
  4. 对接 M16:在部署里定权重格式并复核质量
✔ 能按精度-速度-显存三角选出数值格式并给出理由
核心知识点详解
  • 格式决定折中:INT8 稳定安全;FP8 E4M3 用硬件张量核心、范围 ±448,W8A8 再量化激活更省;INT4 / MXFP4(块级缩放)更省但更易掉点。
  • 能上 FP8 就上 FP8:现代卡原生支持 E4M3/E5M2,精度接近 FP16(多数任务掉 0–2 个点)而省一半显存、吞吐近翻倍;硬件不支才退而考虑 INT8。
  • 常见坑:把位宽当唯一标准:「INT8 一定更快」是错觉——没走 Tensor Core 的 INT8 可能更慢。要实测才能定格式,不能只看位宽。
掉点诊断按任务子类评估逐层敏感度回退 FP16校准集覆盖真实分布group-wise / per-channel输出稳定性
学习路径
  1. 读 5.5:理解按任务子类评估与逐层敏感度回退 FP16
  2. 跑校准集覆盖性检查与分组回退 FP16 示例
  3. 完成 5.6 自测:给出一套量化掉点后的诊断流程
  4. 对接 M16:在量化对比表里记录掉点并给出回退方案
✔ 能量化对比掉点并用逐层回退定位哪个模块该回归 FP16
核心知识点详解
  • 按任务子类评估:别只给一个总分。量化前后分别测各任务子类(中文 / 代码 / 数学 / 长上下文),才看得见「数学类掉 8 个点」这种隐藏损伤。
  • 逐层敏感度回退:对层做敏感度扫描,找出掉点最多的层单独回退到 FP16(group-wise / per-channel 粒度),用最小显存代价补回精度。
  • 常见坑:上线前不设精度红线:不定义「可接受掉点阈值」,上线后才知道损失。先定红线(如业务指标掉点 ≤2%),越线就回退或换方案。
学习路径

5.1 方法对比与选型

方法类型精度损失适用注意
GPTQ训练后量化(PTQ),逐层误差补偿低(4bit 通常可接受)权重 4bit,通用需要校准数据;不同任务表现差异需实测
AWQ激活感知的权重量化,保护重要通道较低,通常优于 GPTQ权重 4bit,主流选择工程实现依赖特定内核
FP88bit 浮点极低H100/Blackwell 原生支持,大模型首选需硬件支持;可同时用于权重与 KV
KV Cache 量化把 KV 从 FP16 压到 FP8 / INT8低但需验证长上下文、高并发场景对长依赖任务要专门验证
QAT(量化感知训练)训练时模拟量化最低极致部署要求(端侧、大规模服务)需要训练资源,成本高
NVFP4 等低比特4bit 浮点(新硬件)中(需验证)新一代 GPU 的极致吞吐生态与工具链仍在成熟
python# 量化必须验证:不能只看速度,要测「精度是否仍满足业务要求」
from lm_eval import simple_evaluate

def eval_quant(model_path, tasks=("gsm8k", "arc_easy"), limit=None):
    return simple_evaluate(model="vllm", model_args=f"pretrained={model_path}",
                           tasks=list(tasks), limit=limit)

base = eval_quant("./Qwen3-8B")                     # BF16 基线
for tag, path in [("int8", "./Qwen3-8B-int8"),
                  ("awq-int4", "./Qwen3-8B-AWQ"),
                  ("fp8", "./Qwen3-8B-FP8")]:
    r = eval_quant(path)
    print(tag, {t: r["results"][t]["acc,none"] for t in r["results"]})
    # 判断标准:相对基线下降在可接受范围内(通常 <1-2 个点),
    #           并且要在「你自己的业务评测集」上复验,而不只看公开任务。
    # 特别要注意:量化对长链推理、代码生成、低资源语言的损伤常大于平均值。
bash# 量化的两条路:现成量化权重直接下载 vs 自己量化(AWQ/GPTQ)
# 路线 A:直接用社区量化版本(推荐先验证质量)
vllm serve Qwen/Qwen3-8B-AWQ --quantization awq --max-model-len 8192
# 路线 B:自己用校准集量化(autoawq / llm-compressor)
pip install autoawq
python -m awq.entry --model_path ./Qwen3-8B --w_bit 4 --q_group_size 128 \
  --calib_data ./calib.jsonl --calib_n 128 --export_path ./Qwen3-8B-AWQ
# 关键:校准集必须覆盖真实输入分布(含长文本与专业术语),calib_n 经验取
#       128–512;太少会让激活统计失真、掉点。之后务必跑 5.5 节的掉点诊断。
✔
量化的实践顺序:① 先量化权重(收益最大、风险最低);② 显存仍不够再量化 KV Cache;③ 只在确有极致需求时才考虑 QAT 或 4bit 以下。每一步都要在业务评测集上复验,并特别关注长上下文、推理链、代码、低资源语言这四类对量化更敏感的能力。

5.2 PTQ 与 QAT、权重量化与激活量化

维度PTQ(训练后量化)QAT(量化感知训练)
是否需要训练否,仅校准是,需反向与微调
成本低(几小时校准)高(需训练资源与数据)
精度好(4bit 通常可接受)最好(可到更低比特)
适用绝大多数部署端侧/极致压缩/精度敏感

另一个关键区分是量化谁:权重量化(如 W8A16、W4A16)只压缩权重,decode 阶段每步都要读权重,所以收益直接体现在显存与读取带宽上,实现最简单;激活量化(W8A8)还要量化激活,能进一步加速 compute-bound 的 prefill,但激活里有离群值(outlier),处理不好会严重掉点,通常需要在线统计或 SmoothQuant 这类平滑手段。

python# 混合精度的最简决策:先量化权重,不够再量化激活,最后才动 KV
# W8A16  -> 权重 INT8,激活保持 BF16(最稳,decode 显存减半)
# W4A16  -> 权重 INT4(如 AWQ),激活 BF16(长上下文常用)
# W8A8   -> 权重与激活都 INT8(prefill 加速最大,需处理离群值)
# 经验:从 W8A16 起步验证质量,再逐步下探到 W4A16;W8A8 留给
#       prefill 占比高、且已确认离群值被妥善处理的场景。
量化配置权重激活decode 收益prefill 收益风险
W16A16(基线)BF16BF16——无
W8A16INT8BF16显存减半,速度 ~1.3–1.6×小低
W4A16(AWQ)INT4BF16速度 ~1.8–2.5×小中(需校准)
W8A8(SmoothQuant)INT8INT8中大(compute-bound 提升)中(离群值)
FP8(E4M3)FP8FP8高高(原生张量核心)极低

5.3 GPTQ / AWQ / SmoothQuant 的机制差异

python# SmoothQuant 的缩放直觉:平滑系数 s 平衡激活与权重的量化难度
#   X' = X / s,  W' = W * s    使得 X' 与 W' 的数值范围都更友好
# s 通常取 per-channel:s_j = (max(|X_j|)^alpha) / (max(|W_j|)^(1-alpha))
#   alpha 默认 0.5:激活与权重各承担一半难度
# 效果:原本激活离群值大、无法 INT8 的层,平滑后可整层 W8A8 而不掉点。
python# GPTQ 误差补偿的直觉(标量例子,展示「把误差摊回未量化列」)
import numpy as np
w = np.array([0.12, -0.98, 0.55, 0.03])        # 一行的权重
scale = w.max() / 7                              # INT4 对称量化步长(范围 -8..7)
q = np.round(w / scale).clip(-8, 7)
deq = q * scale
print("原权重:", w)
print("反量化:", deq)
print("误差  :", np.round(w - deq, 4))          # GPTQ 会把 err 按 Hessian 摊给未量化列
# 关键:GPTQ 不是「独立取整」,而是「取整后补偿」,因此 4bit 掉点远小于朴素 round。
# AWQ 换个思路:不补偿误差,而是按激活幅度保护「重要通道」。
✔
三者的选择逻辑:纯权重压缩、要 4bit 且不想训练 → AWQ(或 GPTQ);prefill 重、要 W8A8 提速 → SmoothQuant / 原生 FP8;追求端侧极限且能承受训练 → QAT。无论选哪个,最终判定标准是「业务评测集上的掉点幅度」,不是论文数字。

5.4 INT8 / INT4 / FP8 / MXFP4 的精度与速度取舍

格式相对 FP16 显存相对速度精度风险适用硬件
INT81/2高低几乎所有 GPU / CPU
FP8(E4M3)1/2很高(原生张量核心)极低H100 / H200 / Blackwell
INT41/4中–高中(需校准/补偿)Ampere+(需内核支持)
MXFP41/4高(新硬件极致)中高(需验证)Blackwell 及后续

优先级建议:能上 FP8 就上 FP8——它在 H100 级硬件上有原生张量核心支持,显存减半且精度损失极小,是 2025–2026 年大模型部署的默认选择;硬件不支持 FP8 时再退回 INT8/INT4(AWQ)。4bit 以下(含 MXFP4)务必在业务集上验证,它对长推理链和数值敏感任务的损伤不成比例地大。

python# 各格式的数值范围与精度(决定为什么 FP8 比 INT8 更「稳」)
import numpy as np
print("FP16 范围约", np.finfo(np.float16).min, "~", np.finfo(np.float16).max)
print("INT8 均匀量化,范围固定 -128~127,动态范围小、依赖 scale")
print("FP8-E4M3 范围约 -448 ~ 448,尾数 3 位(相对精度 ~2^-3)")
# 结论:FP8 有指数位、动态范围大,权重里的极小/极大值都不易溢出,因此
#       「几乎无损」;INT8 靠 per-channel scale 才够用;NVFP4 用块级缩放
#       (micro-scaling) 弥补 4bit 动态范围的不足。

5.5 量化后掉点的诊断方法

python# 逐层敏感度分析:找出「一量化就掉点」的层,只对它们回退高精度
def layer_sensitivity(eval_fn, model_layers):
    """对每层分别回退 FP16,观察指标回升幅度"""
    base = eval_fn(model_layers)                 # 全量化基线分
    for i in range(len(model_layers)):
        mixed = model_layers.copy(); mixed[i] = "FP16"
        gain = eval_fn(mixed) - base             # 该层回退带来的回升
        if gain > 0.01:                          # 回升 >1 个点视为敏感层
            print(f"layer {i}: sensitive (+{gain:.3f})")
# 预期:敏感层通常集中在少数几层(经验上 10%–25% 的层贡献大部分掉点),
#       对这些层保留 FP16/BF16 即可「混合精度」回收大部分精度。
# 注意:必须用真实业务评测集,公开任务常掩盖这种层间差异。
⚠
掉点后的处置顺序:① 先确认是「真掉点」还是「评测集偏差」(换一份独立集复验);② 做混合精度(敏感层回退 FP16);③ 换校准集或换方法(AWQ→GPTQ 或反之);④ 仍不行再退回更高比特。不要第一步就放弃量化——多数掉点可通过前两步解决。

5.6 动手练习与自测

  1. 为同一模型设计 BF16 / FP8 / AWQ-INT4 三套部署,列出各自的显存、预期速度与风险(判据:能说 FP8 近乎无损、INT4 需校准)。
  2. 解释 GPTQ 的误差补偿与 AWQ 的激活感知差异,并说明为什么 AWQ 在 4bit 常优于 GPTQ。
  3. 给定一份量化后评测结果,判断掉点集中在哪些任务子类,给出处置顺序(参考答案:先复验集、再混合精度、后换方法)。
  4. 写出 W8A16 / W4A16 / W8A8 的适用场景与 prefill/decode 收益差异。
  5. 说明为什么 FP8 比 INT8 更「稳」(参考答案:有指数位、动态范围大、不易溢出)。
✔
自测判据:量化决策必须落到「业务评测集掉点幅度」这一条判据上;能用「权重 / 激活 / KV 谁先量化」的顺序讲清优先级,才算真正掌握。
方案显存 / 速度风险
BF16基准无(但显存与带宽要求最高)
FP8(E4M3)显存约半、速度约 1.5–2 ×近乎无损,Hopper/Blackwell 首选
W8A16权重减半激活仍 16bit,收益有限
AWQ-INT4速度约 2–3 ×需校准,掉点须看业务评测集

6. 并行策略:从单卡到集群

知识结构图 · 并行策略
并行策略5 大知识域 · 23 个知识点
张量并行 TP切权重矩阵每层 all-reduce依赖 NVLink限单机卡数降单请求延迟
学习路径
  1. 读 6.1:理解 TP 切权重矩阵 + 每层 all-reduce 的通信模型
  2. 跑单机多卡 TP 示例,对比单请求延迟
  3. 完成 6.6 自测:解释为何 TP 只在单机 NVLink 内用
  4. 对接 M16:多卡部署时确定 TP 度数
✔ 能在多卡上做 TP 并讲清其通信代价
核心知识点详解
  • 切权重、每层 all-reduce:TP 把一层权重按列切成 TP 份,每卡算一部分,层末 all-reduce 通信一次。NVLink 快、开销可控,但通信/计算比限制它只适合单机。
  • 降单请求延迟:TP 把一次请求摊到多卡、单请求加速,适合延迟敏感负载;跨卡带宽不足时吞吐反而受限,TP 度数一般 2–8。
  • 常见坑:跨节点开大 TP:TP 依赖 NVLink 高带宽,跨节点后每层 all-reduce 都走网络、延迟爆炸。TP 只在单机卡力内,大模型要靠 PP/EP。
流水线并行 PP按层切 stage气泡率 (P−1)/(m+P−1)micro-batch 填充跨节点扩展
学习路径
  1. 读 6.2:理解 PP 按层切 stage 与气泡率公式
  2. 跑 micro-batch 填充示例,观察气泡占比
  3. 完成 6.6 自测:计算 (P−1)/(m+P−1) 的气泡比例
  4. 完成本章自测:评估 PP 相对 TP 的适用规模
✔ 能用气泡率公式评估 PP 收益并说出如何用 micro-batch 填充泄压
核心知识点详解
  • 按层切 stage:PP 按层切成 P 段分给 P 卡,前向一条条过。气泡率 (P−1)/(m+P−1)(m 为 micro-batch 数):P=8、m=16 时仍约有 43% 空转。
  • micro-batch 填充:把请求切成 micro-batch 流水化,延后每段的空隙,气泡率随之下降。PP 允许跨节点廉价网络,是卡间扩展的保底方案。
  • 常见坑:不切 micro-batch 直接跑:一个 batch 整段同步,气泡率拉满、PP 吞吐优势归零。必须切 micro-batch 并靠流水把空隙填上。
专家与序列并行MoE 专家并行all-to-all 通信top-k 路由负载均衡防热点序列并行切长上下文
学习路径
  1. 读 6.3:理解 MoE 专家并行 + all-to-all 通信
  2. 跑 top-k 路由与负载均衡示例,观察热点专家
  3. 完成 6.6 自测:说明序列并行如何切长上下文
  4. 完成本章自测:评估 EP 的通信与均衡取舍
✔ 能讲清 EP 的 all-to-all 通信与负载均衡取舍
核心知识点详解
  • MoE 专家并行:MoE 的专家经 top-k 路由到不同卡,卡间 all-to-all 交换 token;负载均衡防止热点专家拖垮整体。
  • 序列并行切长上下文:把单个长序列的 attention 沿序列维度切开到多卡,规避单卡装不下超长 KV 的墙,常与 PP 配合使用。
  • 常见坑:路由不均衡:部分专家被热点 token 打爆、别的卡闲,延迟由最忙卡决定。要做均衡约束(如每个 token 落到不同卡),否则 EP 收益被热点吃掉。
PD 分离prefill / decode 解耦KV 跨网络传输prefill 高算力卡decode 高带宽卡小规模不划算
学习路径
  1. 读 6.4:理解 prefill 高算力卡 / decode 高带宽卡的解耦部署
  2. 跑 PD 分离对比,观察 KV 跨网络传输开销
  3. 完成 6.6 自测:判断小规模为何不划算
  4. 对接 M16:压测时评估 PD 分离是否值得上
✔ 能用数据判断 PD 分离的收益边界
核心知识点详解
  • prefill 算力卡 + decode 带宽卡:把 prefill 放算力型卡、decode 放带宽型卡,各自打满对应瓶颈;中间需把 KV 跨网络传输,有显式开销。
  • 小规模不划算:QPS 不够高时,KV 传输开销、双份调度与运维复杂度大于收益。只有两阶段真的各自受限、单卡寸显时才值得上。
  • 常见坑:负载不大硬上 PD:额外 KV 传输与部署复杂度让成本反升。用压测数据判断两阶段是否各自受限,别被「先进架构」带节奏。
互联与通信NVLink ≈ 900GB/sPCIe 5.0 ≈ 64GB/sIB / 400Gb RoCE ≈ 50GB/s通信/计算比判据
学习路径
  1. 读 6.5:记住 NVLink / PCIe / IB 的带宽量级
  2. 用通信/计算比判据评估某个并行方案的可行性
  3. 完成 6.6 自测:选互联时给吞吐依据
  4. 完成本章自测:判断某张卡通信是否会成瓶颈
✔ 能按带宽数字选择互联并判断通信何时成为瓶颈
核心知识点详解
  • 带宽量级速记:NVLink ≈900GB/s、PCIe 5.0 ≈64GB/s、IB / 400Gb RoCE ≈50GB/s。通信量过大时就要降并行度或换互联。
  • 通信/计算比判据:若每层通信量 ÷ 计算量比值过高,all-reduce 会成为瓶颈,据此判断给定互联下一个并行方案可不可行。
  • 常见坑:给以太网配 TP=8:TP 每层全量传输,走 50GB/s 以太网直接变网络等待。通信密集的并行(TP/EP)必须配 NVLink 或高带宽 IB/InfiniBand。
学习路径

6.1 张量并行(TP):切矩阵

张量并行(Tensor Parallelism)把每一层的权重矩阵沿特定维度切到多张卡,每个设备算一部分,再在层内做 all-reduce 汇总。它的通信极其频繁(每层前向/反向各一次 all-reduce),因此严重依赖高带宽互联(NVLink),一般只在单机多卡内使用,跨节点 TP 会因为网络延迟迅速恶化。TP 的两个典型用途:① 单卡放不下模型;② 降低单请求延迟(把一次大矩阵乘分摊到多卡并行)。

python# 张量并行的直觉:把一个矩阵乘切成 N 份,各卡算一份再汇总
# 以列并行 Linear 为例(Megatron 风格)
#   Y = [X @ W1, X @ W2, ...]  (沿输出维切 W),各卡算一段
#   若后面紧跟需要完整 Y 的操作(如 GeLU/RMSNorm),则需 all-gather
#   若后面是行并行 Linear,则可直接用分段结果做 reduce-scatter,省一次通信
#
# 通信量近似:每层 2 * (参数 / TP) 的激活要在卡间传递(forward + backward)
#   → TP 越大,单卡算力/显存省,但通信量近似不变(与层数成正比)
#   → 所以 TP 受限于「互联带宽 / 单层计算量」的比值
bash# 张量并行启动与「是否值得」的判据
# 8 卡单机(NVLink),70B BF16 单卡放不下 -> TP=4 或 8
vllm serve Qwen/Qwen3-72B --tensor-parallel-size 4
# 判据:TP 的代价是每层一次 all-reduce,通信时间 ~ 2*(激活字节)/带宽
#   NVLink ~900GB/s: 8K 激活 all-reduce 约几十微秒,可忽略 -> 值
#   PCIe  ~64GB/s : 同样 all-reduce 要十几倍时间 -> 常拖慢整体
# 经验:TP 只用于「单机内」且务必配 NVLink;跨机请改 PP/EP 或多副本。
# 一个反例:在 PCIe-only 机器上开 tp=4,吞吐反而比 tp=1 + 多副本低。
✔
TP 的合理上限:经验法:TP 大小不要超过单机卡数(如 8 卡机用 TP=8 是上限),且优先保证「每层通信量 ≪ 该层计算量」。若互联是 PCIe 而非 NVLink,TP 收益会大打折扣,此时应优先用多副本而非大 TP。

6.2 流水线并行(PP):切层

流水线并行(Pipeline Parallelism)把模型按层切成若干 stage,每个 stage 在一组卡上,激活在 stage 间传递。问题是气泡(bubble):前几个 stage 等第一个 micro-batch 流到末尾才能开始第二个,期间空闲。用 micro-batch 把大 batch 拆细、交错执行,可以填补气泡,但气泡率仍约为 (P−1)/m 量级(P 为 stage 数、m 为 micro-batch 数),需要足够多的 micro-batch 才能压低。PP 适合跨节点扩展(通信少,只在 stage 边界传激活),但对延迟不友好。

并行方式通信模式主要代价何时用
TP 张量并行每层 all-reduce,频繁依赖 NVLink单节点、降延迟、放不下
PP 流水线并行stage 间传激活,少气泡、延迟高跨节点扩展
EP 专家并行专家间 all-to-all路由通信MoE 必需
多副本无模型内通信冗余资源提吞吐/可用性的首选
python# 流水线气泡率:micro-batch 越多,气泡越小(但有上限)
def bubble_rate(P, m):
    # 近似:气泡占比 = (P-1)/(m+P-1)
    return round((P-1)/(m+P-1), 3)
for P in (2, 4, 8):
    print(f"P={P}:", [bubble_rate(P, m) for m in (1, 4, 16, 64)])
# 预期:P=4 时 m=1 -> 0.75(!),m=16 -> 0.158,m=64 -> 0.045
# 结论:PP 必须配足够多 micro-batch 才划算;这也意味着 PP 对
#       「大 batch 离线」友好,对「小 batch 低延迟在线」不友好。

6.3 专家并行(EP)与序列并行

MoE 模型每层有多个专家,专家并行(Expert Parallelism)把不同专家放到不同卡上,token 按路由结果被送到对应卡——带来 all-to-all 通信,是 MoE 部署的主要成本。与之相对,序列并行(Sequence Parallelism)把长序列的激活沿序列维切到多卡,专门解决单卡放不下超长上下文激活的问题,常与 TP 配合(TP 切隐藏维、SP 切序列维)。两者都是「单卡放不下」时的必备手段。

python# MoE 专家并行的通信量估算:all-to-all 是主要成本
def ep_alltoall(tokens, hidden=4096, bytes_per=2, top_k=2):
    # 每个 token 送 top_k 个专家,每个方向 hidden 维激活
    per_dir = tokens * top_k * hidden * bytes_per / 1024**2
    return {"send_MB": round(per_dir, 1), "total_MB": round(per_dir*2, 1)}
print(ep_alltoall(tokens=4096))
# 预期:{'send_MB': 64.0, 'total_MB': 128.0}
# 结论:batch 越大 all-to-all 越大,且它是同步点 -> 对跨机带宽敏感;
#       DeepSeek 系 MoE 用 EP + 负载均衡(辅助损失 / 无辅助损失路由)控制热点。
# 经验失败模式:专家负载不均 -> 少数卡成瓶颈,整体吞吐被最慢专家拖死。

6.4 PD 分离:prefill 与 decode 解耦部署

传统部署把 prefill 与 decode 放在同一批卡上,但二者资源特性相反(prefill 算力密集、decode 带宽密集),混部会让 GPU 在两种模式间反复切换、利用率都上不去。PD 分离(prefill/decode disaggregation)把二者部署在不同节点:prefill 集群用高算力卡 + 较大 TP,decode 集群用高带宽/大显存卡 + 高并发 batch。请求先在 prefill 节点算完 KV,再把 KV 传输到 decode 节点继续生成。代价是一次额外的 KV 跨网络传输,但当规模足够大时,资源利用率与尾延迟的改善显著。

阶段资源特性推荐硬件倾向并行策略
Prefill算力密集(compute-bound)高 FLOPs 卡(如 H100 密集)较大 TP,少副本
Decode带宽密集(memory-bound)高带宽/大显存卡高并发 batch,多副本
传输KV 跨节点搬运高带宽网络(如 400Gb RoCE)异步传输 + 流水线重叠
python# PD 分离到底值不值:把「KV 传输成本」算清楚再决定
def pd_worth(kv_gb_per_req, reqs_per_s, link_gbps=400, prefill_tps_gain=0.35):
    link_gb_s  = link_gbps / 8                          # 400Gb/s -> 50GB/s
    transfer_ms = kv_gb_per_req / link_gb_s * 1000
    bw_used    = reqs_per_s * kv_gb_per_req / link_gb_s
    return {"transfer_ms": round(transfer_ms, 2),
            "link_utilization": round(bw_used, 3),
            "prefill_gain": prefill_tps_gain}
print(pd_worth(kv_gb_per_req=0.2, reqs_per_s=20))
# 预期:{'transfer_ms': 4.0, 'link_utilization': 0.08, 'prefill_gain': 0.35}
# 结论:单请求 KV ~0.2GB、20 QPS 时,400Gb 链路只用 8%,传输成本可忽略,
#       而 prefill/decode 各自优化可提效 ~35% -> 大规模值得。
#       反例:单机 8 卡小规模,两套调度+传输的复杂度 > 收益,先调单机。
⚠
PD 分离不是银弹:它增加架构复杂度(KV 传输、两套调度、负载均衡)与一次网络开销。中低规模(如单机 8 卡撑一个模型)往往不划算,先把单机的连续批处理、量化、prefix caching 用到极限再说;只有当「prefill 与 decode 相互拖累导致利用率上不去」时,PD 分离才值得上。

6.5 通信量与互联:NVLink 还是以太网

互联典型带宽对哪种并行友好说明
NVLink / NVSwitch约 900 GB/s(H100)TP(频繁 all-reduce)节点内首选,TP 几乎必须
PCIe 5.0约 64 GB/s弱(TP 会受限)无 NVLink 时的退路
InfiniBand / 400Gb RoCE约 50 GB/sPP / EP / PD 传输跨节点主力
以太网(普通)10–25 GB/s仅多副本/控制面不适合模型内通信

一句话判断:TP 吃互联带宽,所以 TP 必须配 NVLink 且限单机;PP/EP/PD 的通信稀疏,可以跨节点用 IB/RoCE。很多团队「多卡反而更慢」的根因,就是把需要 NVLink 的 TP 铺到了跨节点以太网上。

python# 互联带宽选型:把「通信/计算比」算出来,决定能用多大 TP
def tp_viable(activation_mb, layer_tflops, bw_gbs, peak_tflops=990):
    comm_ms = activation_mb * 2 / (bw_gbs * 1000)          # 双向 all-reduce 近似
    comp_ms = layer_tflops / peak_tflops * 1000            # H100 峰值折算
    return {"comm_ms": round(comm_ms, 4), "comp_ms": round(comp_ms, 4),
            "ratio": round(comm_ms / comp_ms, 4), "ok": comm_ms / comp_ms < 0.3}
print("NVLink:", tp_viable(32, 300, 900))
print("PCIe  :", tp_viable(32, 300, 64))
# 预期:NVLink ratio≈0.0001;PCIe ratio≈0.0014(单层都很小)
# 说明:单层看 PCIe 也还行,但每层累积 + 激活随序列/并发增长会放大;
#       工程判据仍取「TP 限单机、优先 NVLink」,跨机走 PP/EP 或多副本。

6.6 动手练习与自测

  1. 给定「单卡放不下 70B BP16」的约束,选出并行策略并说明理由(参考答案:单机内 TP=4/8 配 NVLink;跨机走 PP/EP)。
  2. 计算 P=4、m=1/16/64 的流水线气泡率(参考答案:0.75 / 0.158 / 0.045),说明 PP 需要多少 micro-batch。
  3. 估算 4096 token、top-2 专家、hidden=4096 的 all-to-all 通信量(参考答案:约 128MB 双向)。
  4. 判断「单机 8 卡、KV 0.2GB/请求、20 QPS」要不要上 PD 分离(参考答案:链路利用率仅 8%,可上;但小规模更应先调单机)。
  5. 解释为什么在 PCIe-only 机器上开大 TP 反而更慢(参考答案:all-reduce 通信时间被互联带宽拖爆)。
✔
自测判据:能把「放不下 / 降延迟 / 提吞吐 / 省成本」四类诉求分别映射到 TP/PP/EP/多副本,并报出通信量与气泡率量级,才算真正会做并行选型。
并行通信 / 代价适用
TP每层 all-reduce,量大单机 NVLink
PP点对点,量小有空泡,适合跨机
EPall-to-allMoE 专家
气泡率 P=4m=1/16/64 → 0.75/0.158/0.045micro-batch 越大越低

7. 服务化:让它稳定地跑在线上

知识结构图 · 服务化
服务化3 大知识域 · 17 个知识点
生产必备要素SSE / WebSocket 流式超时与取消生成限流与并发控制重试与熔断优雅降级Prometheus 指标暴露
学习路径
  1. 读 7.1:理解流式 / 超时取消 / 限流 / 熔断 / 降级五项必备
  2. 跑 SSE 流式 + 停止生成的示例
  3. 完成 7.4 自测:列全生产环境必备要素
  4. 对接 M16:让 vLLM 后端支持流式输出与取消
✔ 能列全生产要素并说清每项对应的故障场景
核心知识点详解
  • 五项必备:① SSE/WebSocket 流式输出;② 超时与取消生成(断开即取消,避免烧 GPU);③ 限流与并发控制(令牌桶);④ 重试与熔断(保护下游);⑤ 优雅降级 + Prometheus 暴露指标。
  • 每条对应一个故障:没有超时→请求永远挂起;没有取消→GPU 空转烧钱;没有限流→被突发打死;没有熔断→雪崩传染;没有指标→问题无法定位。
  • 常见坑:只做流式不做取消:SSE 流出了但客户端关页面后端还在生成。必须实现 cancel:断开即终止循环,省 token 省显存。
探针与部署liveness / readiness / startupinitialDelay 120sK8s GPU 资源声明多副本 + 负载均衡纵向优化优先
学习路径
  1. 读 7.2:理解 liveness / readiness / startup 与 initialDelay 120s
  2. 写 K8s GPU 资源声明与探针配置
  3. 完成 7.4 自测:配置多副本 + 负载均衡示例
  4. 对接 M16:产出 K8s 部署清单 + HPA 自动扩缩
✔ 能写出可直接用的 K8s GPU 部署清单
核心知识点详解
  • 三种探针各司其职:startup 解决冷启动(权重加载几十秒,initialDelaySeconds 给足,参考 120s);readiness 控制是否接流量;liveness 决定是否重启(加载阶段设 liveness 会反复重启)。
  • GPU 资源声明 + 多副本:K8s 声明 resources.requests[gpu]=1,Service 做负载均衡。初步优化优先纵向(把单副本调满)而非急着横向多副本。
  • 常见坑:liveness 误杀推理服务:长请求冻住指标采集窗口,被 liveness 杀掉反而更糟。liveness 阈值要容忍长推理与 GC,快失败逻辑放 readiness 与超时。
网关与容错API 网关鉴权令牌桶限流幂等键 Idempotency-KeySSE 分帧与心跳多模型路由与灰度优雅停机 drain
学习路径
  1. 读 7.3:理解网关鉴权 / 令牌桶限流 / SSE 心跳
  2. 跑 Idempotency-Key 幂等与优雅停机 drain 示例
  3. 完成 7.4 自测:写灰度路由与回滚流程
  4. 对接 M16:实现按流量比例切流的灰度发布与回滚脚本
✔ 能实现灰度 + 回滚并讲清限流与幂等机制
核心知识点详解
  • 令牌桶与幂等:网关用令牌桶限流(突发可容忍、均值可控);写操作带 Idempotency-Key,重试不重复计费;SSE 用分帧 + 心跳保活防代理超时断流。
  • 灰度与优雅停机:按流量比例切流灰度(先 5%)、核心链路可回滚;优雅停机走 drain:先停接新连接、把在途请求跑完再关进程。
  • 常见坑:灰度没有回滚脚本:灰开头容易,出问题却没一键回滚只能手工改配置。回滚脚本要跟灰度一起写好,故障时 1 分钟内切旧版本。
学习路径

7.1 生产级推理服务的必备要素

python# 生产级推理接口的骨架:流式 + 取消 + 超时 + 限流 + 指标
import asyncio, time
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from prometheus_client import Counter, Histogram

app = FastAPI()
REQS = Counter("llm_requests_total", "total", ["status"])
TTFT = Histogram("llm_ttft_seconds", "time to first token")

@app.post("/v1/chat")
async def chat(req: Request, body: ChatBody):
    if not limiter.allow(body.user_id):                 # 1) 限流
        REQS.labels("rate_limited").inc()
        return {"error": "too_many_requests", "retry_after": 1}

    async def gen():
        t0 = time.perf_counter()
        first = True
        try:
            async with asyncio.timeout(60):             # 2) 总超时
                async for chunk in engine.stream(body):
                    if await req.is_disconnected():      # 3) 客户端断开就取消
                        log.info("client disconnected, cancelling")
                        return
                    if first:
                        TTFT.observe(time.perf_counter() - t0)
                        first = False
                    yield f"data: {chunk.model_dump_json()}\n\n"
            REQS.labels("ok").inc()
            yield "data: [DONE]\n\n"
        except asyncio.TimeoutError:
            REQS.labels("timeout").inc()
            yield 'data: {"error":"timeout"}\n\n'
        except Exception as e:
            REQS.labels("error").inc()
            log.exception("generation failed")
            yield 'data: {"error":"internal"}\n\n'   # 4) 失败也要给结构化响应

    return StreamingResponse(gen(), media_type="text/event-stream")
python# 分级超时与降级:不同下游给不同预算,超时即降级而非硬等
import asyncio
async def with_budget(coro, ms, fallback):
    try:
        return await asyncio.wait_for(coro, ms/1000)
    except asyncio.TimeoutError:
        return fallback                            # 降级值(缓存/小模型/提示)
async def answer(q):
    retrieved = await with_budget(retrieve(q), 300, fallback=[])       # 检索 300ms
    tools     = await with_budget(call_tools(q), 500, fallback=None)   # 工具 500ms
    return await generate(q, ctx=retrieved, tool=tools)
# 原则:① 每个下游独立预算;② 超时返回可用降级(宁可少检索也别超时);
#       ③ 总预算再兜一层(见 sv-api 主体)。避免一个慢下游拖垮整链。

7.2 多卡部署与选型

并行方式何时用代价
单卡模型能放下且吞吐足够最简单,首选
张量并行 TP单卡放不下,或需要降低单请求延迟每层都要通信,必须有高速互联(NVLink);建议不超过单机卡数
流水线并行 PP跨机扩展有气泡,需要调微批次;延迟敏感场景不友好
专家并行 EPMoE 模型部署token 路由的通信开销大;需要专门支持
多副本 + 负载均衡提升总吞吐与可用性最简单可靠的扩展方式,优先于复杂并行
bash# K8s 部署 vLLM:GPU 资源 + 就绪探针 + 分副本负载均衡(Deployment 片段)
#   containers:
#   - name: vllm
#     image: vllm/vllm-openai:latest
#     args: ["--model","Qwen/Qwen3-8B","--max-num-seqs","128",
#            "--enable-prefix-caching","--gpu-memory-utilization","0.90"]
#     resources: { limits: { nvidia.com/gpu: 1 } }
#     readinessProbe:
#       httpGet: { path: /health, port: 8000 }
#       initialDelaySeconds: 120        # 大模型加载慢,探测过早会反复重启
#     startupProbe:
#       httpGet: { path: /health, port: 8000 }
#       failureThreshold: 60            # 给最长约 10 分钟加载窗口
# Service 用 ClusterIP + Ingress;副本间由 Service 轮询,无需模型内并行。
# 关键:readiness 没过之前绝不可导流量,否则首个用户吃到「冷启动」超时。
★
扩展的顺序(很重要):实际工程中正确的扩展顺序是:先纵向优化单卡(量化、批处理、KV 管理)→ 再加副本 + 负载均衡 → 最后才考虑多卡并行。很多团队跳过前两步直接上多卡并行,结果是复杂度大幅上升而单位成本没降。多卡并行解决的是「放不下」与「单请求延迟」问题,不是「吞吐不够」问题。

7.3 服务化工程:网关、限流、熔断与发布

python# 幂等重试:客户端必须带幂等键,服务端去重,否则抖动会重复扣费/重复动作
import uuid
def call_with_idempotency(client, payload, idem_key=None):
    idem_key = idem_key or str(uuid.uuid4())          # 每个业务动作一个稳定键
    for attempt in range(4):
        try:
            return client.post("/v1/chat", json=payload,
                               headers={"Idempotency-Key": idem_key})
        except (TimeoutError, ConnectionError):
            continue                                   # 仅网络类可安全重试
    raise RuntimeError("exhausted retries")

# 服务端:用 key -> 结果缓存做去重;同一 key 的并发/重复请求返回首次结果,
#         而不是重复消费 GPU 与计费。这是「计费/下单类」接口的上线硬要求。
python# 令牌桶限流 + 租户配额(生产网关的最小实现)
import time
class TokenBucket:
    def __init__(self, rate, burst):
        self.rate, self.burst = rate, burst           # 每秒补充速率、桶容量
        self.tokens, self.ts = burst, time.monotonic()
    def allow(self, n=1):
        now = time.monotonic()
        self.tokens = min(self.burst, self.tokens + (now-self.ts)*self.rate)
        self.ts = now
        if self.tokens >= n:
            self.tokens -= n
            return True
        return False                                   # 返回 429 + retry-after

buckets = {}                                           # 按租户隔离
def allow(tenant, rate=5, burst=10):
    return buckets.setdefault(tenant, TokenBucket(rate, burst)).allow()
# 经验:交互服务按租户 5–20 rps、突发 2 倍;批量任务走独立队列与配额,
#       避免用同一桶把在线与离线混在一起导致互相挤占。

7.4 动手练习与自测

  1. 为一个推理服务列出「生产必备要素」清单,并各写一句你的实现方式(判据:含流式、超时取消、限流、熔断降级、探针、指标)。
  2. 写出 liveness / readiness / startup 三种探针的差异,并给出大模型服务的 initialDelay 与 failureThreshold 经验值。
  3. 说明为什么客户端断开后必须取消后端计算(参考答案:否则 GPU 空转烧钱,且占用 KV 空间)。
  4. 给「按租户 5rps、突发 2 倍」实现一个令牌桶并说明 429 与 retry-after 的处理。
  5. 解释为什么重试接口必须带幂等键(参考答案:网络抖动会导致重复扣费/重复副作用,需服务端去重)。
✔
自测判据:能把「可用性」拆成探针、限流、熔断、降级、灰度、幂等六个可落地机制并各给出参数,就说明达到生产级工程要求。
机制参数 / 要点
探针startup 给足权重加载时间;readiness 控流量;liveness 只判死锁
限流令牌桶,租户 5 rps、突发 ×2,超限返 429 + retry-after
取消客户端断开即取消后端计算,省 GPU 与 KV 空间
幂等重试接口带幂等键,服务端去重防重复副作用

8. 压测方法论:找到真实容量

知识结构图 · 压测方法论
压测方法论4 大知识域 · 15 个知识点
开环 vs 闭环闭环掩盖排队开环按速率到达暴露真实拐点
学习路径
  1. 读 8.1:理解开环按速率到达、闭环掩盖排队
  2. 跑 open_loop 负载生成代码,观察积压
  3. 完成 8.5 自测:说明压测该用哪种负载模型
  4. 对接 M16:用开环负载做容量压测
✔ 能用开环模型压出真实拐点而非假健康曲线
核心知识点详解
  • 闭环掩盖排队:闭环「上一个完成才发起下一个」,请求率跟着吞吐走、掩盖积压。开环按固定 RPS 打,能暴露真实拐点与积压。
  • 压测用开环:开环固定速率灌流量,观察延迟与超时如何随速率恶化,才能画出真实容量曲线。工具如 locust、wrk、ghz。
  • 常见坑:拿闭环基准当容量:闭环给的是「服务自身节奏」,开环才是用户真实到达。两者差别大时容量规划会严重偏乐观。
warmup 与编译权重加载数十秒CUDA graph 捕获内核 JIT 编译丢弃前 N≥50 样本
学习路径
  1. 读 8.2:理解权重加载 / CUDA graph 捕获 / 内核 JIT 编译
  2. 压测前做 warmup 并丢弃前 N≥50 个样本
  3. 完成 8.5 自测:说明丢弃前样本的原因
  4. 对接 M16:在 loadtest.md 里记录 warmup 处理
✔ 能说明 warmup 必要性并正确处理(丢弃前样本)
核心知识点详解
  • 三大启动开销:权重加载数十秒、CUDA graph 捕获、内核 JIT 编译,都让首批请求奇慢。压测不 warmup 得出的 P99 全是脏数据。
  • 正确做法:压测前先发若干热请求做 warmup,并丢弃前 N≥50 个样本再统计,同时避免 CPU/GPU 频率冷启动影响。
  • 常见坑:头 50 条也进报告:把冷启动慢请求混进样本,P99 被严重拉高误导调优。先 warmup + 丢前样本,报告才可信。
指标与报告并发档位扫描吞吐 + P99 双轴GPU 利用率错误 / 超时分类
学习路径
  1. 读 8.3:理解并发档位扫描与吞吐 + P99 双轴报告
  2. 跑多档位扫描,记录 GPU 利用率与错误分类
  3. 完成 8.5 自测:写一份完整压测报告
  4. 对接 M16:产出 bench/loadtest.md(QPS/TTFT/TPOT/P99/GPU 利用)
✔ 能产出一份可复现的压测报告并说清每个数的来源
核心知识点详解
  • 并发档位扫描:并发从低到高逐档压,记录档位 → 吞吐(QPS)、P50/P95/P99、错误/超时率、GPU 利用率,画 x 并发、左轴吞吐右轴 P99 的双轴图。
  • 报告要素要齐:写清每个数怎么来:负载模型、warmup 处理、样本数、GPU 利用率(几卡、多少)。产出 bench/loadtest.md 存档。
  • 常见坑:报告只有平均值:没有档位扫描与分位数、GPU 利用率、错误分类,读到的人无法判断该不该信。可复现性比美观更重要。
容量拐点二阶差分找 knee限流阈值 ×0.8安全区 vs 过载区HPA 扩容阈值
学习路径
  1. 读 8.4:用二阶差分在并发扫描数据上找 knee
  2. 定限流阈值(拐点 ×0.8)并划分安全区与过载区
  3. 完成 8.5 自测:设定 HPA 扩容阈值
  4. 对接 M16:给出单卡容量结论与 HPA 阈值
✔ 能在数据上找到拐点并给出安全区与过载区边界
核心知识点详解
  • 二阶差分找 knee:对吞吐~并发曲线做二阶差分,变化骤降的点就是 knee(拐点)。拐点前的吞吐增长有意义,拐点后进入过载区。
  • 限流阈值 ×0.8:把限流设到拐点 80%,留 20% 余量应对波动;在安全区内跑、永远不进过载区。HPA 扩容阈值同理参考拐点。
  • 常见坑:把最大吞吐当容量:在吞吐巅峰跑等于在悬崖边跑,一个波动就超时崩 SLA。容量真值是「能稳定维持的 goodput」,以拐点 ×0.8 为准。
学习路径

8.1 开环 vs 闭环负载模型

压测最重要的一个选择是负载模型。闭环(closed loop):一个虚拟用户必须等上一个请求返回,才发下一个——这会掩盖排队问题,因为系统变慢时到达率自动下降,曲线看起来「很健康」。开环(open loop):请求按泊松过程独立到达(如每秒固定发 N 个),不管是否返回——这才符合真实线上流量,能暴露排队与饱和。

负载模型到达方式能否暴露排队适用
闭环完成才发下一个否(慢时到达率自动降)测单请求延迟上限
开环按速率独立到达是(慢时积压)测容量与拐点(正确选择)
python# 开环负载生成:按固定速率发放,不等待返回(暴露排队)
import asyncio, time
async def open_loop(client, rps, duration):
    tasks, t0, n = [], time.perf_counter(), 0
    while time.perf_counter() - t0 < duration:
        n += 1
        tasks.append(asyncio.create_task(client.one(tag=n)))  # 不等返回,立即发下一个
        await asyncio.sleep(1.0 / rps)                        # 恒定到达率
    await asyncio.gather(*tasks)
    return [t.result() for t in tasks]
# 关键差异:闭环(等返回再发)会在系统变慢时自动降压 -> 掩盖排队;
#           开环在系统变慢时会持续积压 -> P99 迅速暴露真实拐点。
⚠
闭环压测的经典误导:用闭环压测,你会看到「并发越高、吞吐还涨」,误以为系统没瓶颈;切换开环后,同一并发下排队迅速堆积、P99 飙升,真实拐点才暴露。任何容量结论都必须用开环得出。

8.2 warmup 与编译开销

现代引擎首次请求很慢:要加载权重、分配 KV、捕获 CUDA graph、编译内核(尤其 TRT-LLM 与变长输入)。压测前必须 warmup(先跑几十到几百个请求把热路径跑热),并且排除 warmup 阶段的样本,否则会把一次性开销算进稳态延迟,得出错误的容量。

python# 压测骨架:先 warmup,再统计稳态
async def load_test(client, rps, duration_s=60, warmup=50):
    # 1) warmup:跑一批但不计入统计
    for _ in range(warmup):
        await client.one(priority="warmup")
    # 2) 稳态:开环按 rps 发放,记录时间戳
    start = time.perf_counter()
    tasks = []
    while time.perf_counter() - start < duration_s:
        tasks.append(asyncio.create_task(client.one()))
        await asyncio.sleep(1.0 / rps)          # 泊松到达的近似:固定间隔发
    results = await asyncio.gather(*tasks)
    # 3) 剔除 warmup 与异常,计算 TTFT/TPOT/P99/错误率
    return summarize([r for r in results if r.ok])
warmup 来源一次性开销处理方式
权重加载 + KV 分配数十秒到数分钟就绪探针挡住流量,不计入统计
CUDA graph 捕获首次若干形状各一次用代表性长度各 warmup 一轮
内核 / JIT 编译(TRT-LLM / torch.compile)可达数分钟启动后自动预热,压测前先跑满
KV 池增长到稳态前数百请求丢弃前 N 个样本(N ≥ 50)
缓存冷启动(prefix cache 为空)直到命中率稳定用真实 prompt 分布预热缓存

8.3 指标采集与报告写法

一份能用来决策的压测报告至少包含:测试配置(模型、量化、max-num-seqs、硬件、并发档位)、核心曲线(横轴并发、纵轴吞吐与 P99 两条线)、拐点标注、错误率与超时分布、GPU 利用率/显存、以及结论与建议配置。结论要明确回答「单副本支撑多少并发、P99 多少、每千次请求成本多少」。

报告字段为什么要错误写法
并发档位扫描找出拐点只测一个并发就下结论
吞吐 + P99 双轴看饱和点只看平均延迟
GPU 利用率判断是否还有余量只看 QPS 不看利用率
错误/超时分类定位瓶颈类型只报总错误率
python# 自动生成压测报告的核心字段(JSON -> Markdown 表)
def report(rows):
    lines = ["| 并发 | 吞吐(tps) | P50(ms) | P99(ms) | 错误率 | GPU% |",
             "|---|---|---|---|---|---|"]
    for r in rows:
        lines.append(f"| {r['c']} | {r['tps']} | {r['p50']} | {r['p99']} "
                     f"| {r['err']:.2%} | {r['gpu']} |")
    lines.append("\n拐点并发 ≈ %d(此后吞吐停滞而 P99 上升)" % find_knee(rows))
    return "\n".join(lines)
# 报告必须回答三个问题:单副本安全并发=?  对应 P99=?  每千次请求成本=?
# 缺失任何一个,报告都只是「数据罗列」而非「可决策结论」。

8.4 找到容量拐点

容量拐点(knee)是:继续加并发,吞吐几乎不再涨,但 P99 延迟开始指数级上升的那一点。这个点之前的并发是「安全区」,之后是「过载区」。线上限流阈值应设在拐点稍下方,并据此配置扩容触发条件。

python# 用二阶差分找拐点:吞吐增量骤减、延迟增量骤增的位置
def find_knee(rows):
    """rows: [{concurrency, tps, p99}],按并发升序"""
    best = None
    for i in range(1, len(rows)):
        d_tps = rows[i]["tps"] - rows[i-1]["tps"]
        d_p99 = rows[i]["p99"] - rows[i-1]["p99"]
        # 吞吐增量 < 5% 且延迟增量 > 50% -> 进入过载区
        if d_tps < 0.05 * rows[i-1]["tps"] and d_p99 > 0.5 * rows[i-1]["p99"]:
            return rows[i]["concurrency"]            # 拐点并发
    return rows[-1]["concurrency"]

# 实践:把拐点并发 × 安全系数(0.8) 作为单副本目标并发,
#       并作为 HPA 扩容的触发阈值(而非凭 GPU 利用率拍脑袋)。
并发吞吐 tpsP50 msP99 ms状态
8420180320安全区
321180260540安全区
641480420980逼近拐点
12816209003200过载区(P99 爆炸)
256165018008900严重过载(吞吐不再涨)
★
压测的最终产出:不是「我们的服务很快」,而是三张可执行的表:① 拐点并发与对应 P99;② 每千次请求成本;③ 扩容触发条件与冗余策略。有了这三张表,容量规划与成本模型才不是拍脑袋。

8.5 动手练习与自测

  1. 写一个开环负载生成器,并说明它与闭环压测在「暴露排队」上的差异(判据:能指出闭环会掩盖拐点)。
  2. 列举 warmup 的三类一次性开销,并给出各自的处理方式(参考答案:权重加载、CUDA graph 捕获、内核编译)。
  3. 给定一组并发–吞吐–P99 数据,用二阶差分找拐点并给出限流阈值(参考答案:拐点×0.8)。
  4. 写出一份压测报告必须回答的三个问题(单副本安全并发 / 对应 P99 / 每千次请求成本)。
  5. 解释为什么「只看平均延迟」会得出错误容量结论。
✔
自测判据:能用开环压测产出「拐点并发 + P99 + 每千次成本」三张可执行表,而不是只报一句「服务很快」。
项判据
开环压测暴露排队;闭环会掩盖拐点
warmup权重加载 / CUDA graph 捕获 / 内核编译三类一次性开销
拐点二阶差分找拐点,限流阈值取拐点 × 0.8
报告三问单副本安全并发 / 对应 P99 / 每千次请求成本

9. 容量规划、成本与 MLOps

知识结构图 · 容量规划、成本与 MLOps
容量规划、成本与 MLOps4 大知识域 · 22 个知识点
单位经济核算每千次请求成本每百万 token 成本利用率决定成本自托管 vs API 盈亏平衡
学习路径
  1. 读 9.1:理解每千次请求成本与每百万 token 成本口径
  2. 跑成本公式,观察利用率如何决定单位成本
  3. 完成 9.5 自测:算一次自托管 vs 商业 API 的盈亏平衡点
  4. 对接 M16:产出每千次请求成本模型
✔ 能算出每千次请求成本并与商业 API 做 TCO 对比
核心知识点详解
  • 每千次请求成本:成本 = (输入/输出 token × 每 token 单价) + 部署固定成本 ÷ 请求数。利用率是分母,利用率 40% 时闲置 60% 也按满付钱、单位成本翻倍。
  • 自托管 vs API 盈亏平衡:比较自托管机器成本 ÷ (理想吞吐 × 利用率) 与 API 每 token 单价:请求量大、利用率高、自托管跨过平衡点后更省,反之买 API。
  • 常见坑:忽略固定成本与利用率:把 7×24 闲置的 GPU 当「免费」,模型严重失真。把机器折旧、电、运维都算进分母。
LLMOps版本化一切评测门禁灰度与 A/B一键回滚监控告警数据飞轮HPA 排队时长扩容
学习路径
  1. 读 9.2:理解版本化 / 评测门禁 / 灰度 A/B / 一键回滚
  2. 跑监控告警 + HPA 排队时长扩容示例
  3. 完成 9.5 自测:设计一套上线与回滚流程
  4. 对接 M16:配监控告警与灰度发布流程
✔ 能设计可回滚的发布与评测门禁流程
核心知识点详解
  • 可回滚的发布闭环:版本化模型/权重/配置 → 评测门禁(没达基线不发)→ 灰度 5%/A/B → 一键回滚,让一次模型或 Prompt 变更可安全上线并快速撤回。
  • 监控告警 + HPA:Prometheus + Grafana 盯 P99 / 排队时长 / GPU 利用率 / 错误率;HPA 按排队时长扩容而非只看 GPU,避免用户超时才扩。
  • 常见坑:回滚没有快速通道:出事后靠手工重发旧镜像恢复要半小时。一键回滚与版本清单要提前配好,故障时 1 分钟内切回去。
容量规划算例先算权重显存再算 KV 显存压测定单副本 QPS副本数 = 目标/单副本N+1 冗余模型路由省卡
学习路径
  1. 读 9.3:先算权重显存再算 KV 显存
  2. 压测定单副本 QPS,副本数 = 目标负载 / 单副本容量
  3. 完成 9.5 自测:重算一整套容量规划的数值算例
  4. 对接 M16:给出副本数与 N+1 冗余结论
✔ 能手算给定负载需要多少张卡及最少副本数
核心知识点详解
  • 先权重后 KV:权重显存 = 参数量×bit/8;再加每并发 KV(公式)。排 GPU 可用显存得单卡并发上界 → 单卡 QPS = 并发 ÷ 平均耗时。
  • 副本数 = 目标 ÷ 单副本:所需副本 = 目标 QPS ÷ 压测的单副本容量,再加 N+1 冗余应对峰值与故障。模型路由(多模型共享卡)可进一步省卡。
  • 常见坑:只按权重定卡:只算权重以为一张卡够了,长上下文下 KV 把显存吃光。必须 权重 + KV + 并发换算 QPS 才是完整算例。
降本杠杆批处理 API 打 5 折前缀缓存语义缓存利用率 30%→80% 降本 2.6×量化
学习路径
  1. 读 9.4:对比批处理打折 / 前缀缓存 / 语义缓存 / 量化四类杠杆
  2. 跑成本对比示例,观察利用率 30%→80% 降本约 2.6×
  3. 完成 9.5 自测:给自己的服务列出降本空间
  4. 对接 M16:在成本模型里列出各降本杠杆
✔ 能算出各杠杆的降本倍数并排出优先级
核心知识点详解
  • 四类杠杆:① 批处理 API 打 5 折(离线可异步);② 前缀缓存命中免去重复 prefill;③ 语义缓存命中直接返旧结果;④ 量化减权重与 KV。
  • 利用率是最大杠杆:利用率 30%→80%,单位成本约降 2.6×——比任何算法优化都狠。先想办法把卡跑满,再谈级别省。
  • 常见坑:一上来就量化:先查利用率并填缓存,往往比量化收益大得多、风险小得多。降本优先级:利用率 > 缓存 > 量化。
学习路径

9.1 单位经济核算

python# 单位经济核算:回答「每千次请求要多少钱」
def unit_economics(gpu_hour_price, gpus, throughput_rps, p95_latency_s, n_requests=1000):
    """throughput_rps 来自压测(在满足 SLA 的并发下测得)"""
    qps_per_gpu = throughput_rps / gpus
    seconds_needed = n_requests / qps_per_gpu
    cost = (seconds_needed / 3600) * gpu_hour_price * gpus
    return {"cost_per_1k_requests": round(cost, 3),
            "cost_per_1m_requests": round(cost * 1000, 1),
            "gpus_needed_for_10rps": round(10 / qps_per_gpu + 0.999, 1),
            "p95_latency_s": p95_latency_s}

# 自托管(参考量级,实际按供应商报价替换)
print("8B on 1xH100:", unit_economics(gpu_hour_price=2.5, gpus=1,
                                      throughput_rps=12, p95_latency_s=2.1))
# 与 API 报价对比:把 API 的每 token 价换算成每千次请求成本,做同口径比较。
# 决策要点:请求量大、延迟要求稳定、数据敏感 -> 自托管更划算;
#           请求量小、波动大、要快速上线 -> API 更划算(省运维与闲置成本)。
python# 自托管 vs API 的盈亏平衡:把「利用率」纳入决策
def breakeven(gpu_hour=2.5, gpus=4, api_per_m_out=1.5, util=0.6, tps_per_gpu=30):
    self_host_per_m = (gpus*gpu_hour) / (gpus*tps_per_gpu*util*3600) * 1e6
    return {"self_host_per_m_usd": round(self_host_per_m, 2),
            "api_per_m_usd": api_per_m_out,
            "cheaper": "self" if self_host_per_m < api_per_m_out else "api"}
print(breakeven(util=0.6))   # 约 self=3.09 vs api=1.5 -> 用 API
print(breakeven(util=0.9))   # 约 self=2.06 vs api=1.5 -> 仍 API
print(breakeven(gpus=1, util=0.9, tps_per_gpu=60))  # 自托管才可能更省
# 关键:利用率与单卡吞吐同时决定结论;盲目自托管最容易在「闲置」上亏钱。
场景推荐方案理由
低流量(< 1 万次/天)直接用 API自托管的固定成本与运维成本高于节省
中高流量、延迟敏感自托管 + 多副本单位成本随量下降,可精细控制延迟
流量波动大API + 自托管混合(按负载切换)用 API 兜峰,自托管承担基座流量
数据敏感 / 合规要求自托管或私有云数据不出域是硬约束
端侧 / 离线场景小模型量化后本地部署无网络依赖,隐私最好

9.2 LLMOps:让迭代可控

版本化一切
模型版本、Prompt 版本、检索索引版本、配置版本、评测集版本——全部纳入版本管理并在每次请求的 trace 中记录。没有这一步,出问题无法归因。
评测门禁
任何改动(Prompt / 模型 / 参数)在合并前必须通过评测集与红队用例。指标下降或安全用例失败则阻止上线。
灰度与 A/B
新版本先给小流量(如 5%),对比质量指标、延迟、成本、人工介入率,确认后再全量。
快速回滚
模型与 Prompt 都要能一键回滚到上一版本。回滚演练要定期做,否则真出事时不会用。
监控与告警
质量(抽样评估或用户反馈)、延迟分位数、错误率、成本、GPU 利用率、安全事件(越权尝试、注入命中)。
数据飞轮
把线上失败案例沉淀进评测集与训练数据,形成「发现问题 → 进入评测 → 修复 → 验证」的闭环。这是长期竞争力所在。
bash# K8s 部署的两个关键点:GPU 调度与就绪探针
# 1) 就绪探针要区分「进程活着」和「模型已加载可服务」
#    readinessProbe:
#      httpGet: { path: /health/ready, port: 8000 }
#      initialDelaySeconds: 120      # 大模型加载慢,探测过早会反复重启
#      periodSeconds: 10
#      failureThreshold: 30
#    livenessProbe:
#      httpGet: { path: /health/live, port: 8000 }
#      initialDelaySeconds: 300      # 给足加载时间,避免启动被误杀

# 2) GPU 资源与节点选择
#    resources:
#      limits: { nvidia.com/gpu: 2 }     # 声明 GPU 数量,K8s 才会调度到带卡的节点
#    nodeSelector: { gpu-type: h100 }

# 3) 扩容:LLM 的扩容指标不应只看 CPU/GPU 利用率
#    用「排队时长」或「TTFT p95」驱动 HPA,比 GPU 利用率更贴近用户体感
bash# HPA 用「自定义指标」扩容:排队时长比 GPU 利用率更贴近体感
# vLLM 暴露指标后,用 Prometheus Adapter 让 HPA 读取
kubectl apply -f - <<EOF
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: vllm-hpa }
spec:
  scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: vllm }
  minReplicas: 2
  maxReplicas: 12
  metrics:
  - type: Pods
    pods:
      metric: { name: vllm_num_requests_waiting }
      target: { type: AverageValue, averageValue: "4" }   # 平均排队 >4 就扩容
  behavior:
    scaleDown: { stabilizationWindowSeconds: 300 }        # 冷启动慢,缩容要保守
EOF
# 关键:① LLM 冷启动慢(1–3 分钟),缩容必须留足稳定窗口;
#       ② 扩容指标用「排队/等待」而非 GPU 利用率,否则用户已超时还没扩容。
bash# 补充:GPU 节点的调度基础(GPU Operator 让 nvidia.com/gpu 可被 K8s 感知)
kubectl apply -f https://raw.githubusercontent.com/NVIDIA/gpu-operator/latest/deployments/gpu-operator.yaml
kubectl get nodes -o custom-columns=NAME:.metadata.name,GPU:.status.allocatable
# 经验:GPU Operator 统一提供驱动 / 容器运行时 / 设备插件;再配 MIG 可把一张
#       H100 切成多个隔离实例,供小模型或低优先级任务共享,提升利用率。
python# 扩容阈值测算:从压测拐点反推 HPA 目标值
def hpa_target(knee_concurrency, qps_per_gpu, safe=0.8):
    target_conc = knee_concurrency * safe
    return {"target_concurrency_per_replica": int(target_conc),
            "recommended_waiting_threshold": round(target_conc * 0.1, 1),
            "note": "排队超过阈值即扩容;缩容用 5 分钟稳定窗口避免抖动"}
print(hpa_target(knee_concurrency=64, qps_per_gpu=20))
# 预期:{'target_concurrency_per_replica': 51, 'recommended_waiting_threshold': 5.1, ...}
# 结论:把「拐点×0.8」作为单副本目标,排队阈值取它的 ~10%,而非拍脑袋。

9.3 容量规划:一个完整的数值算例

给定:一个 70B 模型(BF16 权重约 140GB)、上下文 8K、目标并发 64、单卡 80GB H100。容量规划要回答:需要多少张卡、每张卡能开多大并发、P99 是否达标。

python# 完整数值算例:70B BF16, ctx 8K, 目标并发 64, 单卡 80GB
GB = 1024**3
weights = 140                                  # 70B BF16 权重(GB)
kv = 80 * 8192 * 8 * 128 * 2 * 2 / GB          # 假设 GQA kv_heads=8, d_head=128
#   -> 单序列 KV 约 0.20 GB
reserve = 6                                    # 框架/激活保留
free_per_gpu = 80 - reserve - weights           # 负数!单卡根本放不下 140GB 权重
# 结论1:70B BF16 单卡放不下,必须张量并行(TP>=2,甚至 TP=4/8)
# 选 TP=4:每卡权重 35GB,free_per_gpu = 80-6-35 = 39GB
free_tp4 = 80 - reserve - weights/4
max_conc_tp4 = int(free_tp4 / kv)              # 39 / 0.20 ≈ 190 -> 单副本可撑很高
# 但并发还受 max-num-seqs 与延迟约束,不是显存满了就开满
# 结论2:目标并发 64 远小于显存单副本上限,单副本(TP=4)即可;
#        若要更高可用性与吞吐,再加副本(replicas)而非加大 TP。
print("TP=4 单卡可容纳并发上限 ≈", max_conc_tp4)
# 成本:4 卡 × H100 × 小时价 2.5 美元 = 10 美元/小时;
#      若压测得满足 SLA 的单副本 QPS=20,则撑 64 并发需约 4 副本 = 16 卡。
python# 多模型混合场景的容量规划(大模型做难例、小模型做简单例)
import math
def plan_mix(total_qps, big_ratio, big_qps_per_gpu, small_qps_per_gpu, peak=3):
    peak_qps = total_qps * peak
    big, small = peak_qps * big_ratio, peak_qps * (1 - big_ratio)
    g_big = math.ceil(big / big_qps_per_gpu)
    g_small = math.ceil(small / small_qps_per_gpu)
    ha = lambda n: n + max(1, n // 5)            # N+1 冗余
    return {"gpus_big": ha(g_big), "gpus_small": ha(g_small),
            "total": ha(g_big) + ha(g_small)}
print(plan_mix(total_qps=12, big_ratio=0.2, big_qps_per_gpu=3, small_qps_per_gpu=18))
# 预期:{'gpus_big': 4, 'gpus_small': 3, 'total': 7}
# 结论:把 80% 的简单请求路由到小模型,卡数从「全上大模型」的十余张降到 7 张;
#       这就是模型路由的价值 —— 用少量质量换取巨大成本下降。

9.4 每百万 token 成本与盈亏平衡

把成本落到「每百万(M)token」是横向比较的通用口径。公式:每 M token 成本 = GPU 小时单价 × 单卡耗时(小时) ÷ 该时段生成 token 数 × 1e6。自托管看似便宜,但GPU 利用率才是真实成本——一张卡空闲也照常计费,利用率 30% 时单位成本是直接按满负荷算的 3 倍以上。

python# 每百万 token 成本:把利用率与吞吐都考虑进来
def cost_per_million_tokens(gpu_hour_price, gpus, tps_per_gpu, utilization=1.0):
    """tps_per_gpu: 满负荷单卡生成速度;utilization: 实际利用率"""
    effective_tps = tps_per_gpu * utilization
    gpu_hours = gpus * (1e6 / max(1, effective_tps))
    return round(gpu_hours * gpu_hour_price, 2)

# H100 自托管:单卡约 30 tps(decode, 8B 级),利用率 0.6
print("自托管 8B @60% util:", cost_per_million_tokens(2.5, 1, 30, 0.6))   # 约 139 美元/M
# 对比商业 API:通常按 token 直接报价(如 0.5–3 美元/M 输出),无需运维
# 盈亏平衡点:自托管成本 = API 成本 时的月 token 量
#   月 token > (固定运维+卡成本)/单位差  -> 自托管更省
#   关键变量是「利用率」:利用率越高,自托管越划算;闲置越多越亏。
降本杠杆机制注意
批处理 API把异步请求攒批,价格常打 5 折仅适合非实时场景
语义缓存 / 前缀缓存命中率 20% 即省 20% 算力需请求有可复用前缀
提升 GPU 利用率从 30% 到 80%,单位成本降 2.6×靠稳定流量与排队,而非盲目堆卡
量化权重/激活/KV 压缩,吞吐近似线性提升要验证质量
python# 语义缓存 + 前缀缓存叠加后的成本下降(把杠杆量化)
def with_caches(base_per_m, prefix_hit=0.35, sem_hit=0.15,
                prefix_save=0.3, sem_save=0.95):
    # 前缀缓存省的是 prefill(约占成本 30%);语义缓存直接命中跳过生成
    after_prefix = base_per_m * (1 - prefix_hit*prefix_save)
    after_sem    = after_prefix * (1 - sem_hit*sem_save)
    return {"base": base_per_m, "after_prefix": round(after_prefix, 2),
            "after_semantic": round(after_sem, 2),
            "total_cut": f"{(1-after_sem/base_per_m)*100:.0f}%"}
print(with_caches(1.8))
# 预期:{'base': 1.8, 'after_prefix': 1.61, 'after_semantic': 1.35, 'total_cut': '25%'}
# 经验:前缀命中 30%–50%、语义缓存命中 10%–30% 是常见区间;
#       两者叠加通常能把单位成本压掉 20%–40%,且几乎不改质量。
★
成本决策的核心公式:自托管 vs API 的盈亏平衡不是「谁单价低」,而是:当你的稳定流量足够高、利用率足够高、且数据合规要求自托管时,自托管才划算;流量波动大或偏低时,API(含批处理与缓存)几乎总是更优。把利用率写进成本模型,才不会算出「看起来很美的自托管方案」。

9.5 动手练习与自测

  1. 给定日请求量与长度分布,算出平均 QPS 与峰值 QPS(峰值系数取 3–5),再除以单卡 QPS 得卡数并加冗余。
  2. 用 breakeven 函数比较自托管与 API,说明为什么「利用率」是决定性变量(判据:利用率低则自托管不划算)。
  3. 写出每百万 token 成本公式,并说明利用率从 30% 提到 80% 对单位成本的影响(参考答案:降约 2.6×)。
  4. 把前缀缓存 + 语义缓存叠加,估算单位成本下降幅度(参考答案:常见 20%–40%)。
  5. 为一个混合(大模型难例 + 小模型简单例)场景做容量规划,说明模型路由如何减少卡数。
★
自测判据:能用「显存 → 并发 → 单副本 QPS → 副本数 → 小时成本 → 每百万 token 成本」完整链路算出一个方案,并给出自托管 vs API 的明确结论,就达到了容量规划的要求。
量经验值 / 结论
峰值系数3–5 ×峰值 QPS = 平均 QPS × 峰值系数
冗余N+1(约 +20%)避免单副本故障打满
盈亏平衡利用率是关键利用率低则自托管不划算,优先 API
优化叠加前缀 + 语义缓存降 20%–40%模型路由进一步减卡数

项目里程碑

贯穿项目 · Hamauls Orion
M16 推理优化、部署与压测 第 93–99 周

把 Hamauls Orion 真正跑起来:用 vLLM / SGLang 换掉朴素推理,做量化与批调度优化,容器化 + K8s 部署,灰度发布,压测出吞吐/延迟曲线与成本模型,并配好监控告警。

本阶段产出(直接进入项目仓库)
验收标准:压测给出明确的容量结论(单卡支撑多少并发、P99 多少毫秒、每千次请求成本多少);灰度发布与回滚演练成功一次;有可视化的监控面板。

阶段练习项目

PROJECT 1
推理性能压测与调优
用 vLLM 部署一个 7B–8B 模型,系统性扫描 max-num-seqs、量化方案、是否开前缀缓存等配置,输出「吞吐–延迟–成本」三维对比报告与最优配置建议。
要达成的效果
  • 对同一 7B–8B 模型完成 ≥3 组配置组合的压测,覆盖不同量化方案与是否开前缀缓存
  • 输出可复现的 QPS / TTFT / TPOT / P99 / GPU 利用率对比,并给出最优配置建议
  • 给出容量拐点与推荐并发档位,注明 warmup 与样本舍弃规则
功能需求
  • 用 vllm serve 部署 7B–8B 模型,设置 --gpu-mem-util 与 --max-num-seqs
  • 对 max-num-seqs(如 64 / 128 / 256)做并发档位扫描,用开环负载记录每档 QPS 与 P99(工具如 locust 或自研脚本)
  • 对比是否开启前缀缓存(--enable-prefix-caching)对未来命中率与 TTFT 的影响
  • 压测前 warmup 并丢弃前 N≥50 个样本,产出 bench/loadtest.md 报告
  • 记录 GPU 利用率与错误 / 超时分类,定位容量拐点并给出推荐并发
交付物
  • bench/loadtest.md:QPS / 分位数 / TTFT / TPOT / 利用率完整报告
  • 配置对比表与「最优配置建议」(含理由与依据)
  • 可复现的压测脚本与参数清单
边界 · 不做

不做量化精度验证与成本核算(交给同阶段其他卡);不做跨机多卡并行压测。

PROJECT 2
量化精度验证
对同一模型做 BF16 / FP8 / AWQ-INT4 三种部署,在公开任务与自建业务评测集上对比精度、显存、吞吐,给出可上线的量化方案。
要达成的效果
  • BF16 / FP8 / AWQ-INT4 三种部署各产出质量、显存、吞吐三项量化对比
  • 给出「可上线」结论:掉点不超过设定红线(如业务指标 ≤2%)并说明依据
功能需求
  • 对同一模型做 FP16(BF16) / FP8 / AWQ-INT4 三种部署,用 vLLM 或 SGLang 加载
  • 在公开任务 + 自建业务评测集上按任务子类分别统计掉点
  • 对比权重与 KV 显存占用,记录各自可同时并发的 batch 数与吞吐
  • 覆盖长上下文 / 长输出专项,量化后复核是否掉点
  • 掉点超线时做逐层敏感度回退 FP16 并重新验证
交付物
  • 三维权衡对比表(FP16 / FP8 / INT4)
  • 量化掉点诊断报告与回退方案
  • 可上线的量化配置建议(含部署命令)
边界 · 不做

不做 QAT 训练;不做内核级编译优化。

PROJECT 3
生产级推理服务
实现一个完整服务:SSE 流式、超时与取消、限流、熔断降级、指标暴露、健康探针,并用压测验证 SLA。
要达成的效果
  • 服务具备 SSE 流式 / 超时取消 / 限流 / 熔断降级 / 指标暴露 / 健康探针六项
  • 压测验证 SLA:给定错误率与 P99 下稳定达到目标 QPS,并在过载时正确降级
功能需求
  • 实现 SSE 流式输出与客户端断开后的生成取消(不烧 token)
  • 实现令牌桶限流、重试分类(可重试 5xx / 不可重试 4xx)与熔断降级
  • 暴露 Prometheus 指标:QPS / TTFT / TPOT / 错误率 / 排队时长,并配 Grafana 面板
  • 配置 liveness / readiness / startup 探针,参考 initialDelaySeconds 120,写 K8s 部署清单
  • 实现 Idempotency-Key 幂等与优雅停机 drain
交付物
  • 可运行的流式推理服务代码与时序图
  • K8s 部署清单(含探针、GPU 资源声明、多副本)
  • 压测验证报告(SLA 达成情况)
边界 · 不做

不做跨节点多卡并行与 PD 分离;不做鉴权计费之外的增值网关。

PROJECT 4
成本模型与方案对比
给定一个业务场景(日请求量、输入输出长度分布、延迟 SLA),分别计算 API、自托管单卡、自托管多卡三种方案的年成本,输出决策建议文档。
要达成的效果
  • 对给定场景输出 API / 自托管单卡 / 自托管多卡三种方案的年成本与每千次请求成本
  • 给出决策建议文档,明确盈亏平衡点与推荐方案及依据
功能需求
  • 以日请求量、输入 / 输出长度分布、延迟 SLA 作为输入假设
  • 先算权重显存再算每并发 KV 显存,折算单卡 / 单副本容量与 QPS
  • 用「副本数 = 目标 QPS ÷ 单副本容量 + N+1 冗余」推算所需卡数与利用率
  • 分别测算三种方案的年成本,含机器折旧 / 电 / 运维 / API token 单价
  • 列出降本杠杆(批处理打折 / 前缀缓存 / 语义缓存 / 量化)并给出优先级
交付物
  • 成本模型(脚本或表格)与假设清单
  • 三方案对比报告与盈亏平衡分析
  • 决策建议文档(给评审的口径)
边界 · 不做

不做真实采购下单;不做财务审计,只做工程口径的成本估算。

PROJECT 5
M16 · 推理优化、部署与压测(里程碑交付)
把 Hamauls Orion 的推理后端从朴素实现换为 vLLM:接入连续批处理、PagedAttention 与 prefix caching;做 FP16/INT8/AWQ 量化质量–速度–显存三角对比;编写 K8s 部署清单 + HPA 自动扩缩 + 灰度发布与回滚脚本;用开环压测输出 TTFT/TPOT/P99/错误率/GPU 利用率报告;给出每千次请求成本与商业 API 的 TCO 对比,并配好监控告警面板。
要达成的效果
  • 把 Hamauls Orion 推理后端从朴素实现替换为 vLLM 并稳定跑通
  • 产出可复现的压测、量化、K8s、成本全套交付物,验证达到设定 SLA 与成本目标
功能需求
  • 接入 vLLM:连续批处理、PagedAttention、prefix caching,接口保持流式兼容
  • 完成 FP16 / INT8 / AWQ 质量–速度–显存三角对比,记录掉点与回退方案
  • 编写 K8s 部署清单 + HPA(按排队时长扩容)自动扩缩
  • 实现按流量比例切流的灰度发布与一键回滚脚本
  • 开环压测输出 TTFT/TPOT/P99/错误率/GPU 利用率,配监控告警;给出每千次请求成本与商业 API 的 TCO 对比
交付物
  • vLLM 服务化后端 + K8s 与 HPA 清单
  • bench/loadtest.md、量化对比表、成本模型与监控面板
  • 灰度回滚脚本 + 上线 / 回滚流程文档
边界 · 不做

不做多卡分布式并行(TP/PP/EP)与 PD 分离;不做训练侧改动。

常见误区

面试高频问题速答

为什么大模型推理是显存带宽受限的?

Decode 阶段每生成一个 token 都要把全部权重(以及 KV Cache)从显存读一遍,而这一步的浮点运算量很小(约 2N FLOPs 对应 2N 字节权重)。因此算术强度约为 1 FLOP/Byte,远低于 GPU 的算力/带宽拐点(H100 约 300 FLOP/Byte),GPU 的算力大量闲置而带宽打满。结论:提升吞吐的关键是让每次权重读取服务更多 token——即增大并发批处理,或减小权重体积(量化)。

PagedAttention 解决了什么问题?

传统 KV Cache 按最大长度预分配连续显存,实际使用长度远小于最大长度,导致大量内部碎片与外部碎片(典型浪费 60%–80%),直接压缩了可用并发数。PagedAttention 把 KV Cache 划分为固定大小的块,按需分配、非连续存储,配合页表定位,几乎消除碎片,使可容纳的并发序列数大幅提升,从而提高吞吐。它不影响计算结果的正确性,只是更高效的内存管理。

什么情况下投机解码有效?为什么?

原理是用一个小而快的草稿模型连续预测多个 token,再由主模型一次并行验证,接受匹配的前缀。有效条件是:① 草稿模型与主模型行为足够一致(接受率高,通常要 60%+ 才有明显收益);② 主模型 decode 阶段是 memory-bound,一次验证多个 token 的边际成本低(权重只读一遍)。如果接受率低或场景是 compute-bound 的 prefill,收益会很有限甚至为负。

量化会伤害哪些能力?怎么验证?

量化对「长链推理、代码生成、数学计算、低资源语言、需要精细数值判断」的任务损伤通常更大,因为这些任务对权重微小扰动更敏感;对短文本分类、抽取等任务影响较小。验证方式:① 在公开任务上对比基线,看是否掉点;② 更关键的是在自建业务评测集上按任务子类分别统计;③ 做长上下文与长输出的专项对比;④ 设定可接受的精度损失阈值,超过阈值就不用该量化方案,或退回更高比特。

线上推理服务突然延迟飙升,你怎么排查?

按这条顺序看:① 是 TTFT 还是 TPOT 变差?TTFT 变差多为输入变长、排队变多(并发超限);TPOT 变差多为显存压力导致 batch 下降或 KV 空间不足。② 看排队时长与并发数:是否有突发流量或某个租户刷量(限流是否生效)。③ 看显存与 GPU 利用率:是否有长请求吃光 KV、是否有 OOM 后的重试风暴。④ 看下游依赖:检索、工具调用、模型网关是否有超时。⑤ 看版本变更:最近是否有模型、Prompt、配置或索引的变更。最后一步永远是「有没有版本记录可回溯」。

学习资源

vLLM 官方文档文档 docs.vllm.ai/ PagedAttention、连续批处理、量化与分布式部署的权威参考。 PagedAttention 论文(vLLM)论文 arxiv.org/abs/2309.06180 理解 KV Cache 分页管理为什么能把吞吐提升数倍。 SGLang 官方文档文档 docs.sglang.ai/ RadixAttention 前缀复用与结构化输出,Agent 与多轮场景优势明显。 TensorRT-LLMGitHub github.com/NVIDIA/TensorRT-LLM NVIDIA 极致性能优化,固定模型追求极限吞吐时使用。 GPTQ / AWQ / SmoothQuant 论文线索论文 arxiv.org/abs/2210.17323 从 GPTQ 开始理解训练后量化的原理与误差补偿思路。 FastAPI 文档文档 fastapi.tiangolo.com/ 异步、流式响应、依赖注入,AI 服务化的主力框架。 NVIDIA Triton Inference ServerGitHub github.com/triton-inference-server/server 多模型、动态批处理的企业级推理服务框架。 Prometheus + Grafana文档 prometheus.io/docs/introduction/overview/ 指标采集与可视化,延迟分位数与成本监控的基础设施。
★
2026 形势提示:2026 年一个关键的工程现实是:「Agent 的默认行为就是遗忘和跑偏」——它会在长流程中丢失上下文、混淆指令。而推理基建(上下文管理、缓存、稳定的服务化、可观测)正是让它能连续运行一个月的保障。这意味着推理与部署能力不再只是「省钱的技能」,而是Agent 能否落地的前提。掌握 vLLM 调优 + 量化 + 服务化的人,在任何 AI 团队里都是刚需。