27届大模型面试准备(二十五):推理服务化与投机解码——vLLM、PagedAttention、Medusa 与连续批处理

27届大模型面试准备(二十五):推理服务化与投机解码------vLLM、PagedAttention、Medusa 与连续批处理

上一篇《位置编码与长度外推》解决了"模型怎么读懂 128K 上下文"的问题,解决了"能装下"之后,下一个绕不开的工程问题是"怎么把模型高效的往外吐"。这是本系列第二十五篇。一个 70B 模型在单卡上跑不起来、在八卡上吞吐上不去、在百路并发下延迟爆炸,这些都是推理服务化的范畴。2025 年之后,几乎所有大模型团队的面试都会问到 vLLM、PagedAttention、连续批处理(continuous batching),以及越来越火的投机解码(speculative decoding)。本系列 A15 讲的是训练侧的高效推理(蒸馏、量化训练),A19 讲的是权重量化,这一篇是它们的"最后一公里"------把训好的、量化过的模型变成一个能扛住真实流量的服务。本文按"吞吐与延迟的权衡 → KV Cache 与显存碎片 → PagedAttention → 连续批处理 vs 静态批处理 → 调度架构 → 投机解码原理与变体 → 服务化工程 → 评测指标"展开,结尾给面试速答和高频追问清单。


一、推理服务化的核心矛盾:吞吐 vs 延迟

1.1 为什么训练集群跑得好好的,推理却很难

复制代码
             训练 (Training)                    推理 (Inference)
  目标        压低 loss,吞吐优先              既要高吞吐,又要低延迟
  批大小      大 batch,几千 token/步          单请求 1~N token/步
  显存占用    activations + 参数 + 优化器状态    参数 + KV Cache(随序列增长)
  计算模式    高度规整的矩阵乘                  自回归:decode 阶段每步只算 1 个 token
  关键瓶颈    算力 (FLOPS)                     显存带宽 (memory-bandwidth bound)

自回归生成的 decode 阶段有个残酷的事实:每生成一个 token,都要把全部参数从显存读一遍,但只做一次矩阵向量乘(matvec)。计算强度极低,瓶颈不在算力而在显存带宽。这决定了推理优化的两条主线:省显存(让 KV Cache 装得下更多并发)省带宽/增加并行度(让每次 decode 做更多有用功)

1.2 两个必须分清的指标

指标 含义 受什么影响 典型优化手段
吞吐 (Throughput) 单位时间生成的 token 数,常换算为 requests/s 或 tokens/s 批大小、显存利用率、调度效率 连续批处理、PagedAttention、多卡并行
延迟 (Latency) 单请求从发起到结束的耗时 首 token 延迟 TTFT、逐 token 间隔 ITL、队列等待 投机解码、KV Cache 复用、Prefill/Decode 分离
TTFT Time To First Token,用户最敏感的"开场白"速度 Prefill 长度、并发排队、批内优先级 Chunked Prefill、请求优先级
ITL Inter-Token Latency,逐 token 间隔,决定"打字机"是否流畅 decode 批大小、投机命中率 投机解码、受限批大小

面试常考的一个坑:吞吐高不等于体验好。把批大小开到极致能拉满吞吐,但每个请求的 ITL 会变差,用户看到的是"卡顿"。生产系统往往要在两者间找一个 SLA 下的工作点。


二、KV Cache 与显存碎片

2.1 自回归为什么必须缓存 KV

Transformer 每层的注意力要拿当前 token 的 query 去和前面所有 token 的 key/value 算分数。朴素做法每次都重算历史 K/V,复杂度 O(n^2)。实际做法是把每一步算出的 K、V 缓存下来:

复制代码
  第 t 步 decode:
      新 token x_t  ->  Q_t, K_t, V_t
      KV Cache 追加 (K_t, V_t)
      Attention(Q_t, [K_1..K_t], [V_1..V_t])

  显存占用 ≈ 2 * num_layers * seq_len * num_kv_heads * head_dim * batch * dtype_bytes
  例:7B 模型, seq=4096, batch=64, FP16
      ≈ 2 * 32 * 4096 * 32 * 128 * 64 * 2 ≈ 68 GB   <- 光 KV 就吃掉一张卡

这就是为什么"省 KV Cache 显存"是推理服务化的第一战场。KV Cache 越大,能并发的请求越多,吞吐越高。

2.2 传统显存管理的碎片问题

早期实现给每个请求预先分配一段连续、且等于 max_seq_len 的 KV 块。问题有两个:

复制代码
  请求 A 实际只用了 200 token,却占了 4096 的连续空间  ->  内部碎片
  请求 B 想增长,但旁边没有连续空闲块                ->  外部碎片
  结果:GPU 显存看着还有 30% 空闲,新请求却因"凑不出连续块"被拒

这正是操作系统当年面对的问题,vLLM 的解法是把"分页"思想搬过来:PagedAttention。


三、PagedAttention:把显存当虚拟内存管

3.1 核心思想

复制代码
  传统:每个序列一块连续 KV 显存 (预分配 max_len)
  PagedAttention:KV 被切成固定大小的 block (如 16 token/block)
                  序列的 KV 由一组逻辑 block 组成,物理上可离散分布
                  用一张 block table 做"逻辑块 -> 物理块"映射

  逻辑序列:  [block0 | block1 | block2 | block3]
                       |        |        |        |
  物理显存:  [P7]    [P2]    [P9]    [P4]     (离散、可复用)
             block_table = {0:P7, 1:P2, 2:P9, 3:P4}

好处:

  1. 消除外部碎片 :物理块按需分配,序列增长时找一个空闲块挂上即可。

  2. 消除内部碎片 :只分配实际用到的 block,不再预占 max_len。

  3. 支持 Copy-on-Write 共享:并行采样(同一个 prompt 出多个回答)、beam search 的共享前缀,KV 块可以引用计数共享,显存直接省下数倍。

3.2 一个 block table 的示意代码

python 复制代码
# 概念示意:PagedAttention 的块表与注意力寻址
class PagedKVCache:
    def __init__(self, num_blocks, block_size=16, kv_dim=4096):
        self.block_size = block_size
        self.free_blocks = list(range(num_blocks))   # 空闲物理块池
        self.block_table = {}                         # seq_id -> [物理块id...]
        self.kv = torch.empty(num_blocks, block_size, kv_dim)

    def append_token(self, seq_id, token_kv):
        blocks = self.block_table.setdefault(seq_id, [])
        if len(blocks) == 0 or self._is_last_full(seq_id):
            pid = self.free_blocks.pop()              # 按需分配新物理块
            blocks.append(pid)
        pid = blocks[-1]
        slot = (len(self._tokens(seq_id)) - 1) % self.block_size
        self.kv[pid, slot] = token_kv

    def _is_last_full(self, seq_id):
        n = len(self._tokens(seq_id))
        return n % self.block_size == 0

面试常追问:PagedAttention 和操作系统分页的区别? 答:粒度是 token block 而非 byte page;KV 是只读追加(append-only),没有随机写回;支持引用计数式的 CoW 共享,这是 OS 分页没有的"序列语义"。


四、连续批处理:让 GPU 永远不空转

4.1 静态批处理为什么会浪费算力

复制代码
  静态批处理 (Static Batching):
      凑齐 N 个请求 -> 一起跑 -> 等最慢的那个生成完 -> 整个 batch 一起返回
      问题:请求 A 在第 5 步就 EOS 了,但仍占着 batch 名额,
            后面 20 步 GPU 都在为等 B/C/D 空转(或算 padding)

  时间轴:
  Req A: ████EOS.......................... (浪费 20 步 slot)
  Req B: ████████████████████████████████
  Req C: ███████████████████EOS...........
  Req D: ████████████████████████████████

4.2 连续批处理(Continuous Batching / In-flight Batching)

核心:每个 decode 步(iteration)都重新决定"本步哪些序列参与计算"。结束的立即腾出位置,新的立即插入:

复制代码
  连续批处理:
  step1: [A B C D]
  step2: [A B C D E]      <- E 在 step1 刚到达,立即加入
  step3: [A B C D E F]    <- C 已 EOS 退出,F 补位
  step4: [A B   D E F G]  <- 每步动态重组 batch

  效果:GPU 利用率从 ~30% 提到 ~90%+,吞吐可翻 2~4 倍

连续批处理必须和 PagedAttention 配合:因为退出/加入会改变每个序列的 KV 布局,只有"块表 + 动态物理分配"才能支撑这种随时重组。这也是为什么 vLLM 是把这两者一起设计的------光有连续批处理而 KV 还按连续预分配,碎片问题会立刻反噬。


五、推理服务的调度架构

5.1 单实例内的调度队列

复制代码
                         ┌─────────────────────────────┐
   客户端请求 ──> 等待队列 │  Scheduler                  │
  (prompt, params)       │   - 优先级 / FCFS           │
                         │   - chunked prefill 切分     │
                         │   - 连续批处理重组            │
                         └──────────┬──────────────────┘
                                    │ 每步选出 running + 新加入
                         ┌──────────▼──────────────────┐
                         │  GPU Worker                  │
                         │   Prefill 阶段 (并行算 prompt)│
                         │   Decode  阶段 (逐 token)    │
                         │   PagedAttention KV 管理     │
                         └──────────┬──────────────────┘
                                    │ 生成的 token
                         ┌──────────▼──────────────────┐
                         │  采样 (temp/top_p/beam)       │
                         │  -> 流式返回 / 整体返回       │
                         └─────────────────────────────┘

5.2 Prefill 与 Decode 的算力特征不同

复制代码
  Prefill:一次性算整个 prompt,batch 内 token 多 -> 计算密集 (compute-bound)
  Decode :每步只算 1 个新 token      -> 访存密集 (memory-bound)

  混在一起批处理的副作用:
      Prefill 的大矩阵乘会"霸占" GPU,导致同批 decode 的 ITL 抖动。
  解法 A:Chunked Prefill  ------ 把长 prompt 切成 chunk,和 decode 交错,平滑延迟。
  解法 B:Prefill/Decode 分离 ------ 两套实例,prefill 算完把 KV 传给 decode 实例
          (如 DistServe / 腾讯Angel、阿里星河等架构)。

六、投机解码:用"草稿模型"换"大模型的逐 token 延迟"

6.1 为什么要投机

Decode 阶段每步只产 1 个 token,且受带宽限制。如果我们能让每步一次前向就验证多个候选 token,等效地把 decode 变成小批量 matmul,带宽利用率就上去了。这就是投机解码(Speculative Decoding)。

复制代码
  标准 decode (串行,每步 1 token):
      t1 -> t2 -> t3 -> t4 -> t5     (5 次大模型前向,每次 1 token)

  投机 decode (草稿并行,验证式接受):
      草稿模型一次出 5 个候选: [c1 c2 c3 c4 c5]
      大模型一次前向同时验证这 5 个  ->  接受若干 (如 c1 c2 c3)
      等价于 1 次大模型前向"白赚"了 3 个 token

6.2 数学保证:无损

投机解码的关键是输出分布与大模型自回归完全一致(无损加速)。机理是拒绝采样(rejection sampling):对草稿给出的每个候选 token x,以概率

复制代码
        p(x) - q(x)            p = 大模型分布, q = 草稿模型分布
  accept = min(1, ─────────)
            q(x)

  若拒绝,则从修正分布 (p - q 截断归一化) 重采样一个 token。

由于修正分布的设计,最终序列的边缘分布严格等于 p。也就是说加速不改变生成质量,这是它区别于普通"小模型直接替大模型答"的根本。

6.3 草稿模型怎么来

草稿来源 做法 优缺点
独立小模型 用 7B 给 70B 当草稿 简单;但要额外显存,且 q 与 p 偏差大时命中率低
同架构浅层 取大模型的前几层当草稿头 复用权重、显存友好、分布接近,命中率更高
自蒸馏草稿 训练一个专门匹配大模型的草稿 命中率最优,但要训练成本
无草稿 (Medusa / EAGLE) 见下文 不用串行小模型,靠"多头并行预测"

七、Medusa 与 EAGLE:不要串行草稿

7.1 Medusa:多头并行预测

标准投机是"草稿模型串行吐一串",瓶颈仍在草稿的串行。Medusa 给大模型接几个独立的预测头(head),每个 head 并行预测"未来第 k 个 token":

复制代码
  大模型最后一层表示 h
       ├─ Head1 预测 t+1
       ├─ Head2 预测 t+2
       ├─ Head3 预测 t+3
       └─ Head4 预测 t+4         (并行,一次前向出 4 个位置候选)

  再对每个位置做树状候选 + 大模型一次验证 (tree attention)
  接受后整段回写,类似投机但草稿是"宽"的不是"深"的

Medusa 的训练只训这几个轻量 head,冻结主干,成本很低。

7.2 EAGLE:基于上一层"嵌入"预测

EAGLE(Extrapolation Algorithm for Greater Language-model Efficiency)观察到:直接预测 token 分布不如预测下一层的隐藏状态来得准。它用一层小网络,输入"上一层的隐藏状态 + 刚采样的 token 嵌入",预测下一层隐藏状态,再接大模型剩下的层:

复制代码
  标准投机:草稿模型独立产出 token 分布
  EAGLE  :在 embedding 空间里做"下一步状态"预测,对齐更紧
        -> 接受率更高,加速比通常优于 Medusa
  权衡:依赖对大模型内部表示的访问,实现耦合度更高

7.3 几种方案对比

方案 草稿生成方式 是否无损 额外显存 典型加速
标准投机解码 小模型串行草稿 中(小模型权重) 2~3x
Medusa 多头并行预测头 低(仅 head) 2~3x
EAGLE / EAGLE-2 隐含状态外推 3~4x
普通小模型直答 小模型直接答 否(质量降) - 高但失真

八、服务化工程:从单机到集群

8.1 多卡并行推理的三种切分

复制代码
  TP (Tensor Parallel):单层权重切到多卡,all-reduce 拼结果。适合单请求内加速。
  PP (Pipeline Parallel):模型按层切到多卡,micro-batch 流水。适合超长模型。
  DP (Data Parallel):多份模型副本,不同请求走不同副本。配合连续批处理提吞吐。

  真实部署常见 TP=8 的单机 8 卡跑一个 70B,或 TP=4 x PP=2 的组合。
  注意:TP 通信密集,必须同机 NVLink;跨机只能 PP/DP。

8.2 一个最小化的本地服务封装

python 复制代码
# 用 vLLM 起一个 OpenAI 兼容服务(面试能默写启动参数很有用)
from vllm import LLM, SamplingParams

llm = LLM(
    model="Qwen2.5-72B-Instruct",
    tensor_parallel_size=8,        # TP=8
    gpu_memory_utilization=0.9,    # 显存占用上限
    max_model_len=32768,           # 支持长上下文
    enable_prefix_caching=True,    # 相同前缀的 prompt 复用 KV
    speculative_config={           # 投机解码配置
        "method": "medusa",
        "model": "Qwen2.5-72B-Instruct-Medusa",
        "num_speculative_tokens": 4,
    },
)

params = SamplingParams(temperature=0.7, top_p=0.9, max_tokens=1024)
outputs = llm.generate(prompts, params)

enable_prefix_caching 值得单独记:很多请求共享 system prompt 或 few-shot 示例,前缀 KV 命中后,新请求跳过这部分 Prefill,TTFT 直接下降一个量级。


九、评测指标与上线前 checklist

9.1 吞吐/延迟的衡量

场景 关注指标 说明
在线对话 TTFT + ITL + P99 延迟 用户体验敏感
离线批量 total tokens/s, QPS 成本敏感
压测 随并发上升的吞吐拐点 找显存/调度的瓶颈

9.2 上线前常被问的工程问题

  1. OOM 怎么排查 :先降 gpu_memory_utilization,再开 PagedAttention(默认开),看是否 KV 峰值过高;必要时限 max_model_len 或限并发。
  2. 长尾延迟高:多为 prefill 与 decode 争抢,开 chunked prefill 或 PD 分离。
  3. 投机反而变慢:草稿模型接受率低于某个阈值(如 <0.5)时,草稿开销盖过收益,应关掉或换草稿策略。
  4. 成本怎么算:按 token 计价时,量化(A19)+ 投机(本篇)+ 前缀缓存三者叠加,单位 token 成本可降数倍。

十、KV Cache 显存预算:把容量规划算成一道算术题

面试里最能区分"看过博客"和"真上过线"的问题,就是让你现场估一下:给定模型和并发,需要几张卡。这个不需要背,能推导出来就行。

10.1 KV Cache 的精确公式

每个 token、每一层,都要存一份 K 和一份 V:

复制代码
  单 token KV 显存 = 2 (K和V)
                   × num_layers          (层数)
                   × num_kv_heads        (KV 头数,注意不是 num_attention_heads)
                   × head_dim            (每头维度)
                   × dtype_bytes         (fp16=2, fp8=1, int8=1)

  总 KV 显存 = 单 token KV × 平均序列长度 × 并发请求数

关键陷阱:必须用 num_kv_heads 而不是 num_attention_heads。现代模型普遍用 GQA(Grouped Query Attention)或 MQA,KV 头数远小于 Q 头数。这是 KV Cache 优化最大的一笔红利,很多人算错就是错在这里。

10.2 拿 Llama-3-70B 实际算一遍

复制代码
  Llama-3-70B 配置:
      num_layers      = 80
      num_kv_heads    = 8        (GQA,注意 num_attention_heads=64,是 8 倍)
      head_dim        = 128
      dtype           = fp16 (2 bytes)

  单 token KV = 2 × 80 × 8 × 128 × 2 bytes
              = 327,680 bytes
              ≈ 320 KB / token

  单请求 8K 上下文 = 320KB × 8192 ≈ 2.5 GB
  并发 32 路 8K    = 2.5GB × 32   ≈ 80 GB   <-- 光 KV 就吃掉一整张 H100

  再加权重:70B × fp16 = 140 GB
  合计 220 GB -> 至少 4 张 80G 卡(还要留 activation 与碎片余量)

对比一下如果没有 GQA(即 MQA 之前的 MHA,num_kv_heads=64):单 token KV 会变成 2.5MB,并发 32 路 8K 需要 640GB KV------直接不可行。这就是为什么 GQA 是长上下文时代的必选项。

10.3 从预算反推最大并发

生产上更常见的是反过来问:4 张 H100(320GB),跑 70B,最多扛多少并发?

复制代码
  总显存        320 GB
  - 权重        140 GB  (fp16)
  - 框架/激活    ~20 GB  (经验值,含 CUDA context、临时 buffer)
  = KV 可用     160 GB

  平均上下文按 4K 估:单请求 KV = 320KB × 4096 ≈ 1.31 GB
  最大并发 = 160 / 1.31 ≈ 122 路

  若把权重换成 AWQ int4:权重降到 ~35 GB
  KV 可用 = 320 - 35 - 20 = 265 GB -> 并发 ≈ 202 路

这就直接回答了"量化的收益到底在哪":int4 量化省下的权重显存,转化成了更大的 KV 池,也就是更高的并发和吞吐------收益不只是单次推理变快,更是服务容量翻倍。这是 A19 量化那篇的工程落点。

10.4 KV Cache 量化:再省一半

当并发是瓶颈时,可以把 KV Cache 本身也量化到 fp8 或 int8:

方案 KV 显存 质量影响 备注
fp16 KV 基准 默认
fp8 KV 50% 极小(长文本略有累积误差) H100 原生支持,推荐
int8 KV(带 scale) 50% 小,需 per-token 量化 老卡可用
int4 KV 25% 明显,长序列退化 慎用

经验规则:K 比 V 更敏感。K 参与 softmax,量化误差会被指数放大;V 是加权求和,误差平均化。所以常见做法是 K 用 fp8、V 用 int4 的非对称配置。


十一、前缀缓存与 RadixAttention:把重复的 prefill 省掉

11.1 为什么前缀缓存值钱

真实流量里有大量重复前缀:同一个几千 token 的 system prompt、同一份 few-shot 示例、多轮对话里不变的历史、RAG 里同一批被反复命中的文档。这些内容每次请求都重新做一遍 prefill,是纯粹的浪费。

复制代码
  典型客服场景:
      system prompt + 工具定义   3000 token   <-- 每个请求都一样
      对话历史                    1500 token   <-- 同一会话内递增但前缀不变
      本轮用户输入                  50 token   <-- 唯一真正新的部分

  无前缀缓存:每轮 prefill 4550 token
  有前缀缓存:每轮 prefill 只需 50 token
  prefill 计算量降低约 98%,TTFT 从数百毫秒降到几十毫秒

11.2 RadixAttention:用前缀树管理 KV

vLLM 的 automatic prefix caching 和 SGLang 的 RadixAttention 思路一致:把所有活跃序列的 KV 块组织成一棵基数树(radix tree),公共前缀天然共享同一批物理块。

复制代码
  请求A: [system][few-shot][问题1]
  请求B: [system][few-shot][问题2]
  请求C: [system][工具定义][问题3]

  基数树:
                 root
                  |
              [system]              <-- A/B/C 共享,物理块只存一份
              /       \
        [few-shot]  [工具定义]       <-- A/B 共享左枝
         /     \         |
     [问题1] [问题2]   [问题3]        <-- 各自独占

  命中逻辑:新请求进来,沿树做最长前缀匹配,
            匹配到的部分直接复用物理块(引用计数+1),只 prefill 剩余部分。
  淘汰策略:LRU + 引用计数为 0 才可回收,热前缀常驻。

11.3 与 PagedAttention CoW 的区别(高频追问)

这两个概念很容易混淆,面试被追问时要分清:

维度 PagedAttention 的 CoW 共享 前缀缓存 / RadixAttention
作用范围 同一请求派生出的多个序列(如 beam search、parallel sampling 的 n>1) 跨请求、跨会话
生命周期 请求结束即释放 请求结束后仍保留,等待后续命中
触发方式 框架内部自动派生 前缀匹配命中
省的是什么 显存(同样内容不重复存) 显存 + prefill 计算(TTFT)

一句话概括:CoW 是"同一次请求内部的分叉共享",前缀缓存是"跨请求的历史复用"。两者都建立在 PagedAttention 的块级抽象之上,但解决的是不同问题。

11.4 工程注意事项

  1. 前缀顺序要稳定:把不变的内容(system prompt、工具定义)放最前面,把变化的(用户输入、时间戳)放最后。如果每次都在开头插一个当前时间,前缀缓存直接全部失效。
  2. 多租户下要隔离:不同租户的 KV 块共享会造成信息泄露(可通过时序侧信道推断他人 prompt)。生产上要按租户分区,这与 B21 多租户隔离那篇呼应。
  3. 命中率是核心指标 :要上报 prefix_cache_hit_rate,低于 30% 说明前缀设计有问题。

十二、投机解码的接受率:从数学到调参

12.1 加速比的定量模型

设草稿模型一次投机 k 个 token,接受率(每个位置被接受的概率)为 α,草稿模型与目标模型的单步耗时比为 c = T_draft / T_target

复制代码
  期望接受长度(含最后大模型必定补的 1 个 token):
      E[接受数] = (1 - α^(k+1)) / (1 - α)

  一轮投机的成本 = k × c × T_target  (草稿串行跑 k 步)
                 + 1 × T_target      (大模型一次并行验证)

  加速比 S = E[接受数] / (k × c + 1)

代入实际数字体会一下:

α(接受率) k=4, c=0.1 时的加速比 结论
0.9 (1-0.9^5)/(1-0.9) / 1.4 = 4.10/1.4 ≈ 2.93x 草稿很准,接近理论上限
0.8 3.36/1.4 ≈ 2.40x 典型良好水平
0.6 2.18/1.4 ≈ 1.56x 仍有收益
0.4 1.50/1.4 ≈ 1.07x 几乎白干
0.3 1.30/1.4 ≈ 0.93x 负收益,反而变慢

这张表就是"投机反而变慢"的定量解释:当 α 低于约 0.35 时,草稿开销盖过收益。生产上必须监控实时接受率,低于阈值自动降级为普通解码。

12.2 k 怎么选

k 不是越大越好。α^k 随 k 指数衰减,后面的位置几乎必然被拒绝,但草稿的串行成本是线性增长的。

复制代码
  最优 k 的直觉:
      α 高(0.85+)-> k 可以开到 5~8,吃满长接受
      α 中(0.6~0.8)-> k = 3~5 最稳
      α 低(<0.5)  -> k = 1~2 或直接关闭

  现代框架(vLLM 的 dynamic speculative decoding)会在线估计 α,
  动态调整 k,甚至按请求粒度决定是否启用投机。

12.3 什么因素决定接受率

  1. 草稿与目标的分布相似度:同家族、同 tokenizer、同训练数据的小模型(如 Llama-3-8B 草稿 + Llama-3-70B 目标)α 最高。跨家族基本不可用。
  2. 任务类型:代码、结构化输出(JSON)、有大量模板重复的文本,α 很高(0.85+),因为下一个 token 高度可预测;开放创作、诗歌类 α 低。
  3. 采样温度:temperature 越高,目标分布越平,拒绝概率越大,α 下降。贪心解码(T=0)下 α 最高。这解释了为什么"投机在 T=0 时收益最大"。
  4. 批大小 :注意一个反直觉的点------大 batch 下投机的收益会下降 。因为 decode 阶段在大 batch 时已经从访存密集转向算力密集,GPU 本来就不空闲了,投机带来的"一次多算几个 token"不再是免费午餐。所以投机解码主要服务于低并发、低延迟场景(如单用户对话),而不是高吞吐离线批处理。

12.4 拒绝采样的实现要点

python 复制代码
# 概念示意:投机解码的验证与拒绝采样(保证输出分布与目标模型完全一致)
import torch

def speculative_verify(draft_tokens, q_probs, p_probs):
    """
    draft_tokens: 草稿模型产出的 k 个候选 token
    q_probs: 草稿模型在各位置给这些 token 的概率
    p_probs: 目标模型一次前向后,在各位置给这些 token 的概率
    返回: 接受的 token 列表
    """
    accepted = []
    for i, tok in enumerate(draft_tokens):
        p, q = p_probs[i][tok], q_probs[i][tok]
        # 核心:以 min(1, p/q) 的概率接受
        if torch.rand(1).item() < min(1.0, (p / q).item()):
            accepted.append(tok)
        else:
            # 拒绝后必须从残差分布 norm(max(0, p - q)) 重采样一个 token
            # 这一步是"无损"的数学保证所在,不能简单丢弃后重来
            residual = torch.clamp(p_probs[i] - q_probs[i], min=0)
            residual = residual / residual.sum()
            accepted.append(torch.multinomial(residual, 1).item())
            break  # 一旦拒绝,后面的草稿全部作废(因为上下文已偏离)
    else:
        # k 个全部接受,则大模型的"免费"下一个 token 也可直接采用
        accepted.append(torch.multinomial(p_probs[-1], 1).item())
    return accepted

两个必须记住的细节:一是拒绝后要从残差分布 max(0, p-q) 归一化后重采样,不是直接用 p 重采样 ,这是无损性的数学关键;二是一旦某个位置被拒绝,其后所有草稿 token 立即作废,因为它们是基于被拒绝的那个 token 继续生成的,上下文已经不成立了。


十三、落地决策路径与常见误区

13.1 一套推理服务该怎么从零搭起

面试里经常有一道开放题:给你一个模型和一批预算,你会怎么把服务搭起来。回答这类问题最忌讳的是上来就报框架名字,正确的路径是先明确约束,再倒推技术选型。

第一步是确定业务形态。在线对话类业务对首 token 延迟极其敏感,用户在屏幕前等着,超过一秒就会觉得卡;离线批处理类业务完全不在乎单条延迟,只关心单位成本和总吞吐。这两类的技术选型几乎是相反的:前者要限制批大小、开投机解码、必要时牺牲吞吐保延迟;后者要把批大小拉满、关掉投机、用最激进的量化换更大的并发。如果一个系统里两类流量混在一起,正确做法是物理隔离成两个集群,而不是试图用一套参数同时满足,因为调度器无法在同一个批次里同时优化两个相互冲突的目标。

第二步是估算显存与卡数,也就是第十节那套算术。这一步的产出是一个明确的结论:需要几张什么卡、能扛多少并发、上下文上限设多少。这里最容易犯的错误是只算权重不算 KV Cache,结果上线后一压测就 OOM。经验上,KV Cache 在长上下文高并发场景下占用的显存经常超过权重本身,把它当作事后再考虑的细节,是新手和老手最明显的分水岭。

第三步是选择优化手段的施加顺序。这个顺序不是随意的,而是由收益确定性和实施风险共同决定的。最先做的应该是那些几乎无损、纯赚的项:开启 PagedAttention 和连续批处理,这是现代框架的默认能力,收益巨大且没有质量代价;接着是前缀缓存,只要把 prompt 结构设计对,就能白拿 prefill 的节省。之后才轮到有质量风险的项:权重量化会带来轻微精度损失,需要在业务评测集上验证;KV Cache 量化的风险更高一些,长序列场景要重点测。最后才是投机解码,因为它的收益高度依赖场景和接受率,属于"锦上添花但需要持续调参"的项,过早引入会增加系统复杂度却不一定见效。

第四步是建立可观测与压测基线。没有基线的优化都是自我感动。至少要能持续看到首 token 延迟的分位数、逐 token 间隔、批内平均序列数、KV Cache 利用率、前缀缓存命中率、投机接受率这几个指标。特别是最后两个,它们直接决定了对应优化是否还在产生收益------前缀命中率跌下来通常意味着有人在 prompt 开头加了变化内容,投机接受率跌下来通常意味着流量结构变了,两者都需要及时发现并调整。

13.2 四个高频误区

第一个误区是认为吞吐和延迟可以同时最优。这是所有推理服务问题的根源性矛盾。把批大小调大,GPU 利用率上去了,总吞吐上去了,但批内每个请求的逐 token 间隔必然变长,用户看到的打字机效果就会变慢。反过来限制批大小保延迟,卡就会有大量空转。工程上没有免费午餐,只有在给定服务质量约束下寻找最优工作点。面试时如果能主动说出"我需要先知道这个业务的延迟 SLA 是什么",基本就答对了一半。

第二个误区是把投机解码当成万能加速开关 。前面的定量分析已经说明,接受率低于某个阈值时投机是负收益。更微妙的是,投机的收益在高并发下会自然衰减,因为大批量解码时 GPU 已经不再是访存瓶颈,"顺手多算几个 token"不再免费。所以投机解码本质上是一个用空闲算力换延迟的手段,只有在算力有富余的低并发场景才成立。很多团队在压测时开着投机测出好数据,上线后并发一高收益就消失了,原因就在这里。

第三个误区是忽视 prompt 结构对性能的影响。工程师习惯把注意力放在框架参数和硬件上,但前缀缓存的命中率完全由业务侧的 prompt 拼装方式决定。一个在 system prompt 里插入当前时间戳的改动,可以让整个集群的前缀命中率从八成掉到零,而这个改动在代码评审里看起来人畜无害。所以推理性能不只是基础设施团队的事,prompt 的组织规范应该作为团队约定沉淀下来:不变内容在前、可变内容在后、绝不在前缀里放随机值。

第四个误区是孤立看待各项优化。量化、投机、前缀缓存、连续批处理之间存在复杂的相互作用。量化省下的显存会转化为更大的 KV 池,从而支持更高并发,而更高并发又会削弱投机解码的收益;前缀缓存提高了 KV 复用率,间接也提升了有效并发。这些耦合意味着不能把每项优化的收益简单相乘来估算总收益,必须在真实流量上端到端压测。面试时能说出"这几项优化之间不是独立的,需要联合调优",会比逐个罗列技术名词显得深入得多。


十四、面试速答 + 高频追问清单

面试速答(60 秒版)

推理服务化围绕两件事:省 KV Cache 显存提高 GPU 利用率。PagedAttention 把 KV 切块、用 block table 做逻辑-物理映射,消除内外碎片并支持共享;连续批处理在每个 decode 步动态重组 batch,让结束的请求立刻腾位、新请求立刻补位,把利用率从 ~30% 拉到 90%+,且必须配合 PagedAttention。Decode 阶段是访存密集而非算力密集。投机解码用草稿模型一次产出多个候选、大模型一次前向验证并接受(拒绝采样保证无损),把串行 decode 变成小批量并行;Medusa 用多头并行预测、EAGLE 在隐含状态空间外推,都不需要串行小模型且加速更高。Prefill/Decode 分离(chunked prefill 或 PD 分离)解决两者算力特征冲突导致的延迟抖动。

高频追问清单

  1. PagedAttention 的 block table 在 GPU 上怎么存?注意力计算时怎么 gather 物理块?
  2. 连续批处理下,不同序列长度混在同一 batch,padding 怎么处理?flash attention 在这里的作用是什么?
  3. 投机解码的接受率由什么决定?为什么草稿模型越像大模型,加速越明显?
  4. 拒绝采样公式里 p(x) - q(x) < 0 的情况(草稿概率高于大模型)怎么处理?
  5. EAGLE 比 Medusa 快的根本原因?它为什么必须访问大模型内部表示?
  6. 前缀缓存(prefix caching)和 PagedAttention 的 CoW 共享是同一回事吗?区别在哪?
  7. Prefill/Decode 分离架构里,KV Cache 怎么在两套实例间传输?跨机带宽会是瓶颈吗?
  8. tensor parallel 和 pipeline parallel 各自的通信模式与适用场景?为什么 TP 必须同机?
  9. 怎么给一个 70B 模型定 SLA:TTFT < 500ms、ITL < 50ms、并发 100,显存和卡数怎么估?
  10. 量化(AWQ/GPTQ)和投机解码能叠加吗?两者的加速是否独立、会不会相互抵消?

下一篇预告:本篇让模型"吐得快",但还有个更扎心的问题------它吐出来的到底是真还是编的。下一篇《幻觉成因与缓解》从校准、不确定性估计到弃权机制,讲怎么让模型的"不确定"变得可测量、可利用。

相关推荐
NutShell Wang1 小时前
国产全模态开源潮:从语言模型到视频模型,中国开源生态再升级
人工智能·语言模型·开源·大模型·音视频·多模态·vibe coding
码字的特恩3 小时前
微软确认暂不为 Windows 11 加入透明效果自定义功能,建议用户使用第三方工具
人工智能·windows·算法·microsoft·计算机·大模型·编程
程序猿编码3 小时前
不用PyTorch,不用CUDA,我用C++手写了一个能跑GPT和Whisper的推理库
c++·pytorch·gpt·大模型·whisper
AI大佬的小弟4 小时前
大模型名词精讲06:Top-P(核采样)
大模型·top-p·temperature·ai入门·核采样·采样策略·ai调参
thesky12345620 小时前
27届大模型面试准备(二十三):数据工程与合成数据——配方、去重、质量过滤与数据飞轮
大模型·合成数据·数据去重·数据工程·minhash·数据配比·模型坍缩
寻道码路21 小时前
大模型工程化实战(一):概率坍塌的救赎 - 给LLM输出加锁
大模型·agent·langgraph·ai工程化·llm确定性
m0_547486661 天前
《人工智能导论:深度学习大模型基础》全套PPT课件2026
人工智能·深度学习·大模型
@Mr_LiuYang1 天前
大模型提示注入攻防实验--《深入理解 AI Agent:设计原理与工程实践 》实验2-5
人工智能·大模型·提示词注入·攻防实验