#模块五:推理优化、Serving 与部署工程

#49. 什么是 KV cache?

#标准答案

KV cache 的本质,是把历史 token 在每一层算出来的 K/V 保留下来。这样在自回归生成下一个 token 时,不需要每一步都把整段历史重新做一遍 attention,而是只算新 token 对历史缓存的交互。

它为什么重要?因为没有 KV cache,生成第 t 个 token 时要反复重算前 t-1 个 token 的表示,成本会非常高;有了 cache 之后,推理才真正具备可用速度。所以 KV cache 可以理解成 Decoder-only 推理高效化的核心基础设施。


#深度解析

1. KV Cache 的具体工作原理(以单头为例)

假设正在生成第 5 个 token,前面已经生成了 4 个 token。

阶段 操作 K/V 缓存状态
生成 token 1 计算 token 1 的 K/V cache = {K1, V1}
生成 token 2 计算 token 2 的 K/V;attention 用 Q2 查 {K1,K2} cache = {K1,V1, K2,V2}
生成 token 3 计算 token 3 的 K/V;attention 用 Q3 查 {K1,K2,K3} cache = {K1,V1, K2,V2, K3,V3}
生成 token 4 计算 token 4 的 K/V;attention 用 Q4 查 {K1..K4} cache = {K1,V1, K2,V2, K3,V3, K4,V4}
生成 token 5 计算 token 5 的 K/V;attention 用 Q5 查 {K1..K5} cache = {K1,V1, K2,V2, K3,V3, K4,V4, K5,V5}

关键:每一步只需要计算当前新 token的 K/V,然后用这个新的 Q 去跟所有历史 K做点积。历史 K/V 不用重算。

2. 为什么不缓存 Q?

因为 Q 是"当前 token 想查什么",每个新生成的 token 都有自己的 Q,不需要复用之前的。而 K/V 是"历史 token 能提供什么信息",这些信息对后续所有 token 都有用,所以需要缓存。

换个角度说:生成 token t 时,query 只有 Q_t 一个;但 key/value 有 K_1...K_tV_1...V_tt 个。缓存 K/V 是因为它们多且重复用,不缓存 Q 是因为它只有一个且只用一次。

3. Prefill vs Decode 阶段的 KV Cache 行为

  • Prefill 阶段(处理输入 prompt):一次性计算整个 prompt 中所有 token 的 K/V,把它们全部写入 cache。这是 cache 的"初始化"。
  • Decode 阶段(自回归生成):每步只计算新 token 的 K/V,追加到 cache 末尾。cache 长度逐 token 增长。

Prefill 阶段没有"历史缓存"可以利用,但可以利用矩阵乘法的并行性一次性算完;Decode 阶段算量小,但全靠 cache 避免重复计算。

4. KV Cache 的显存占用公式(再强调)

KV_cache_size = 2 × B × L × n_kv_heads × head_dim × seq_len × dtype_bytes

其中 2 是 K 和 V 两个矩阵。以 LLaMA-2 7B、batch=1、seq_len=4096、FP16 为例:

  • 2 × 1 × 32 × 32 × 128 × 4096 × 2 = 2.1 GB

如果 batch=16,就是 33.6 GB。一个 A100 80GB 卡,模型权重占 14GB,KV cache 占 33.6GB,只剩 32GB 给激活和系统开销,已经很紧张了。

5. 面试官常见深挖追问

  • "KV cache 可以量化吗?怎么量化?"
    • 答:可以。常见的有 INT8 KV cache(把 FP16 的 K/V 压到 INT8,显存减半)和更激进的 4-bit 方案。量化 K/V 的挑战在于:K/V 的数值范围比权重更大(因为激活值分布更动态),需要 per-channel 或 per-token 的缩放因子。K cache 通常比 V cache 对量化更敏感。
  • "为什么 PagedAttention 能缓解 KV cache 的显存碎片问题?"
    • 答:标准 KV cache 是连续分配的——每个请求按最大可能长度预分配一块连续显存。不同请求长度差异大,短请求结束后留下的空洞无法被长请求利用。PagedAttention 把 KV cache 分成固定大小的"页"(block),像操作系统虚拟内存一样非连续分配,显存利用率从 50-70% 提升到 90%+。
  • "如果用户输入 1000 token,输出 100 token,KV cache 的增长过程是怎样的?"
    • 答:Prefill 阶段先一次性写入 1000 个 token 的 K/V;然后 decode 阶段每步追加 1 个,共追加 100 次,最终 cache 长度是 1100。Prefill 的 cache 初始化是"一次性 burst",decode 是"线性增长"。

#50. 为什么大模型推理时显存会越用越多?

#标准答案

大模型推理显存会越用越多,核心原因不是权重在变大,而是生成过程中历史 KV cache 在持续累积。每多生成一个 token,每一层都要多存一份新的 K/V,所以上下文越长、输出越长,缓存就越涨。

如果再叠加大 batch、多层网络、较多 kv_heads,显存压力会放大得很快。所以推理显存一定要分开看:静态部分是模型权重,动态部分是 KV cache、中间激活和调度开销。线上很多 OOM,根因其实都在动态缓存而不是模型文件本身。


#深度解析

1. 显存构成的具体分解

以 LLaMA-2 7B 模型、batch_size=8、seq_len=4096、FP16 推理为例:

显存来源 计算公式 大小
模型权重 7B × 2 bytes ~14 GB
KV Cache 2 × 8 × 32 × 32 × 128 × 4096 × 2 ~16.8 GB
中间激活 取决于 batch 和序列长度 ~2-4 GB
系统开销 CUDA context、驱动等 ~1-2 GB
总计 ~34-36 GB

注意:KV cache(16.8 GB)已经超过了模型权重(14 GB)。这就是"推理显存越用越多"的核心原因。

2. 生成过程中显存增长的曲线

生成进度 累计 token 数 KV cache 大小 总显存
Prefill 完成 4096 16.8 GB ~34 GB
生成 500 token 4596 18.9 GB ~36 GB
生成 1000 token 5096 20.9 GB ~38 GB
生成 2000 token 6096 24.9 GB ~42 GB

显存随输出长度线性增长。如果用户要求生成长文本(如写一篇 5000 字文章),显存可能从 34GB 涨到 50GB+。

3. 为什么权重不会增长,但 KV cache 会?

模型权重在推理前就已经全部加载到显存,之后不会再变。但 KV cache 每生成一个新 token 就要追加新的 K/V。

形象比喻:权重是"厂房"(固定大小),KV cache 是"仓库里的货物"(越堆越多)。OOM 通常不是因为厂房太小,而是因为货物爆仓。

4. 面试官常见深挖追问

  • "如果显存已经不够了,除了减少 batch size,还有什么办法?"
    • 答:按优先级:1)启用 GQA/MQA(如果还没用);2)KV cache 量化到 INT8(显存减半);3)限制最大生成长度;4)用 PagedAttention 提高显存利用率;5)分段生成(chunked generation),每段结束后释放 cache 重新 prefill。
  • "Prefill 阶段和 Decode 阶段的显存占用有什么区别?"
    • 答:Prefill 阶段需要存储整个输入序列的 KV cache(一次性 burst),同时中间激活也很大(因为并行计算 attention)。Decode 阶段中间激活很小(只算一个 token),但 KV cache 持续累积。Prefill 的峰值通常比单个 decode 步高,但 decode 的持续平均值更高。
  • "为什么 beam search 比 greedy 解码更耗显存?"
    • 答:beam search 同时维护多个候选序列(如 beam_width=4),每个候选都有自己的 KV cache。所以显存占用是 greedy 的 beam_width 倍。这也是大模型推理中很少用大 beam 的原因之一。

#51. quantization(INT8 / INT4 / FP16 / BF16)的核心作用是什么?

#标准答案

量化的核心,不是“为了把数值变丑一点”,而是把权重、激活或者缓存用更低比特表示,从而减少显存占用、降低带宽压力,并在某些硬件上换到更高吞吐。对推理系统来说,这往往直接对应更低成本和更高并发。

但量化一定有代价:数值表示变粗后,会引入近似误差,所以需要在资源收益和效果损失之间做权衡。也正因为如此,量化不是越低比特越好,而是看模型、任务、硬件和精度容忍度能接受到什么程度。


#深度解析

1. 四种精度格式的本质区别

格式 比特数 动态范围 精度 适用场景
FP32 32 极大 最高 训练主权重(master weights)
FP16 16 训练/推理,需配合 loss scaling
BF16 16 极大(与 FP32 相同) 训练首选(不用 loss scaling)
INT8 8 推理权重量化
INT4 4 极小 很低 极端压缩(GPTQ/AWQ)

BF16 vs FP16 的关键差异

  • FP16:1 位符号 + 5 位指数 + 10 位尾数 → 范围 ~6×10⁻⁵ 到 6×10⁴
  • BF16:1 位符号 + 8 位指数 + 7 位尾数 → 范围与 FP32 相同,但精度更低

BF16 的指数位数与 FP32 相同,所以不容易溢出,训练时不需要复杂的 loss scaling。这就是为什么现代大模型训练普遍用 BF16 而不是 FP16。

2. 量化的数学原理:对称 vs 非对称

  • 对称量化q = round(x / scale),零点为 0。适合权重分布以 0 为中心的情况。
  • 非对称量化q = round((x - zero_point) / scale)。适合激活值分布不关于 0 对称的情况。

scale 的计算:scale = (max - min) / (2^b - 1),其中 b 是比特数。

3. 主流量化方案对比

方案 量化对象 方法 精度损失 推理加速
LLM.int8() 权重 混合精度(大值 FP16,小值 INT8) 很小 一般
GPTQ 权重 逐层量化 + 最小化输出误差
AWQ 权重 保护"重要"权重通道(activation-aware) 很小
GGUF 权重 多种量化组合(Q4_K_M, Q5_K_M 等) 很好
SmoothQuant 权重+激活 将激活难度转移到权重 很好

GPTQ 的核心思想:不是独立量化每个权重,而是逐层优化,让量化后的输出 W_q @ x 尽可能接近原输出 W @ x。通过贪心算法逐步量化并调整未量化权重来补偿误差。

AWQ 的核心洞察:不是所有权重通道同等重要。那些对应大激活值的权重通道更敏感,应该保留更高精度(如 FP16),其他通道可以大胆量化到 INT4。

4. 量化收益的定量感受

以 LLaMA-2 7B 为例:

精度 权重大小 相对 FP16 显存节省 推理速度 效果损失(perplexity)
FP16 13.5 GB 0% 基准
INT8 6.8 GB 50% 1.2-1.5× +0-2%
INT4 (GPTQ) 3.4 GB 75% 1.5-2× +2-5%
INT4 (AWQ) 3.4 GB 75% 1.5-2× +1-3%

5. 面试官常见深挖追问

  • "为什么 KV cache 量化比权重量化更难?"
    • 答:权重量化只发生在加载时,量化参数(scale, zero_point)可以离线计算好;KV cache 是动态生成的,每层的 KV 值分布随输入变化,需要在线统计或用运行时校准。而且 KV cache 的量化误差会逐层累积,对长序列影响更大。
  • "GPTQ 和 AWQ 的本质区别是什么?"
    • 答:GPTQ 从"输出重建误差"角度出发,逐层优化量化后的权重,让 W_q @ x 接近 W @ x;AWQ 从"通道重要性"角度出发,发现某些权重通道(对应大激活值)更敏感,对这些通道保留高精度。GPTQ 是"全局补偿",AWQ 是"重点保护"。
  • "INT4 量化后模型效果掉了 10%,可能是什么原因?"
    • 答:1)量化方案不合适(如对敏感层用了太激进的量化);2)校准数据分布和实际推理分布不匹配;3)模型本身对某些权重变化很敏感(如小模型比大模型更难量化);4)任务本身对精度要求高(如数学推理 > 创意写作)。解决:尝试 AWQ(保护重要通道)、混合精度(敏感层用 INT8)、或增加校准数据多样性。

#52. batching 为什么能提吞吐,但不一定能降延迟?

#标准答案

batching 能提吞吐,是因为 GPU 更擅长并行处理一批请求,而不是一个请求一个请求地喂。把多个请求合成 batch 后,硬件利用率会更高,单位时间能处理的 token 数也更大。

但它不一定能降延迟,因为单个请求可能要先等待“凑批”,而且 batch 变大后,每一步计算时间也会变长。所以吞吐优化和延迟优化不是同一个目标,生产系统里经常要在二者之间折中,而不是默认 batch 越大越好。


#深度解析

1. 吞吐和延迟的数学关系

  • 吞吐 (Throughput):单位时间处理的请求数或 token 数
  • 延迟 (Latency):单个请求从发送到收到完整响应的时间

当 batch_size 增大时:

  • Batch_time 增加(每步算更多 token)
  • Throughput = Batch_size / Batch_time 通常增加(GPU 利用率更高)
  • 单个请求的 Queue_time 可能增加(等凑批)

2. 具体数值感受

假设每个请求生成 100 token:

Batch Size 每步时间 总时间 吞吐 (token/s) 单个请求延迟
1 10 ms 1000 ms 100 1000 ms
4 15 ms 1500 ms 267 1500 ms
8 25 ms 2500 ms 320 2500 ms
16 50 ms 5000 ms 320 5000 ms

batch=16 时吞吐和 batch=8 一样(GPU 已饱和),但延迟翻倍。

3. 为什么线上更关注延迟?

  • 延迟 < 200ms:用户感觉"即时"
  • 延迟 200ms-1s:用户感觉"快"
  • 延迟 1-3s:用户开始不耐烦
  • 延迟 > 3s:用户可能放弃

在线服务通常用较小 batch(4-8),牺牲一点吞吐换低延迟。

4. 面试官常见深挖追问

  • "Continuous batching 怎么解决 batch 大小的动态变化?"
    • 答:continuous batching 允许请求随时加入和退出。调度器每轮重新组织活跃请求成新 batch。短请求完成后立刻退出,新请求马上加入。这样 GPU 几乎始终满负荷,吞吐比静态 batching 高 5-10 倍。
  • "如果有些请求很短(10 token)、有些很长(1000 token),batching 怎么处理?"
    • 答:简单 batching 下短请求要等长请求完成,造成"尾部延迟"。解决方案:1)按长度分组;2)设置最大长度限制,超长请求拆分;3)用 continuous batching,短请求完成后立即释放资源。

#53. speculative decoding 的基本思想是什么?

#标准答案

speculative decoding 的思路可以概括成一句话:让便宜的小模型先打草稿,让昂贵的大模型负责验收。具体做法是小模型先一次提议多个 token,大模型再并行检查这些 token 是否能接受;如果大部分都通过,就相当于把原本大模型必须逐 token 做的工作,提前批量推进了。

它能提速的前提,是小模型草稿质量足够高、通过率足够好。否则大模型总在否掉草稿,反而会增加额外开销。所以这类方法的关键不是“有个小模型就行”,而是草拟模型和目标模型之间要有合适的匹配度。


#深度解析

1. 数学原理:为什么 speculative decoding 不会改分布?

核心思想来自 rejection sampling:

小模型(draft)生成 K 个候选 token:d_1, d_2, ..., d_K 大模型(target)并行计算这 K 个 token 的概率:p_1, p_2, ..., p_K 小模型对这 K 个 token 的概率:q_1, q_2, ..., q_K

对于每个位置 i,接受概率是 min(1, p_i / q_i)

  • 如果 p_i >= q_i,一定接受
  • 如果 p_i < q_i,以 p_i/q_i 的概率接受

如果拒绝,则从修正分布中采样替代 token。

这个机制保证:最终输出的分布严格等于大模型自己逐 token 生成的分布。是无损加速

2. 加速效果的关键:接受率

草稿长度 K 接受率 理论加速比 实际加速比
4 80% 3.2x ~2.5x
4 60% 2.4x ~1.8x
8 80% 6.4x ~4x

草稿模型和目标模型越匹配,接受率越高。常见选择:目标 70B + 草稿 7B(同系列)。

3. 进阶方法:Medusa / EAGLE

  • Medusa:在目标模型上加多个轻量"头",每个头预测未来某个位置的 token,不需要额外模型。
  • EAGLE:用目标模型自身的浅层特征预测未来 token,比独立小模型更准确。

4. 面试官常见深挖追问

  • "speculative decoding 的 overhead 在哪里?"
    • 答:1)小模型生成草稿需要额外计算;2)大模型并行验证增加了每步矩阵乘法大小;3)接受率低时,拒绝后的重新采样增加了浪费。接受率 < 50% 时,overhead 可能抵消加速收益。
  • "为什么 speculative decoding 只适合 decode 阶段?"
    • 答:prefill 阶段是并行处理整个输入,没有逐 token 生成的串行瓶颈。speculative decoding 的核心价值是打破 decode 阶段的串行性。

#54. continuous batching 和静态 batching 的区别是什么?

#标准答案

静态 batching 更像“开整点班车”:先凑一车请求,再统一发车,中途不再变化。它实现简单,但在线场景下会浪费很多空闲,因为不同请求长度差异很大,短请求很快结束后,剩余算力不一定能及时被新请求补上。

continuous batching 则更像”动态拼车”:请求可以随时进入,已完成的也随时退出,所以系统能更持续地填满 GPU。它更适合线上 serving,因为能显著提高利用率和吞吐,但调度器、cache 管理和请求状态管理也会复杂很多。


#深度解析

1. 静态 batching 的浪费到底有多少?

请求 输入长度 输出长度 在静态 batching 中的实际计算
A 10 tokens 50 tokens 需要等 B、C 全部完成(假设 max=200 tokens)
B 100 tokens 150 tokens 同上
C 500 tokens 200 tokens 决定整批完成时间

假设 3 个请求同时到达:

关键问题:A 和 B 早就生成了 50/150 个 token,但 batch 里还有”空位”不能释放,直到 C 的 200 个 token 全部生成完。这些空位对应的 GPU 算力被浪费了。

2. continuous batching 的核心机制

时间线示意(每个格子代表一个 forward step):

静态 batching:
├─ A: [PPPPPPPPPP][GGGGGGGGGG][      空位      ] ── 请求A完成,但batch不能释放
├─ B: [PPPPPPPPPPPPPPPPPPPP][GGGGGG...150个...][  空位  ]
└─ C: [PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP][GGGG...200个...]

continuous batching:
Step 1: A(prefill) + B(prefill) + C(prefill)  → 3个请求同时处理输入
Step 2: A(decode_1) + B(decode_1) + C(decode_1) → 同时生成第一个token
Step 3: A(decode_2) + B(decode_1) + C(decode_1) → A已结束,新请求D进来
Step 4: B(decode_2) + C(decode_1) + D(prefill)   → D进来做prefill,其他decode

核心调度规则

  • 每个 forward step 前,调度器检查哪些请求已完成
  • 完成的请求释放 KV cache,新请求可以立即插入
  • Prefill 和 Decode 可以在同一个 batch 中混合(vLLM 的关键创新)

3. 量化收益:GPU 利用率提升多少?

场景 静态 batching GPU 利用率 continuous batching GPU 利用率 提升
短查询为主(<50 tokens) 40-60% 75-90% ~50%
混合查询 50-70% 80-95% ~40%
长查询为主(>1K tokens) 70-85% 85-98% ~20%

利用率提升来自:

  • 没有”等待整批结束”的空转
  • batch size 保持更稳定(新请求不断补位)
  • memory 效率更高(PagedAttention 动态分配)

4. continuous batching 的实现复杂度

调度器需要处理:

问题 解决方案
KV cache 管理 PagedAttention:按 block 分配,请求完成即释放
不同长度请求的 attention mask 动态构造 causal mask,确保新请求看不到已生成 token
Prefill + Decode 混合 同一 batch 中,新请求做 full prefill,老请求只做 single decode
优先级/抢占 高优先级请求可抢占低优先级请求的 slot
内存不足保护 预留内存 buffer,避免 OOM 导致整批失败

5. 静态 vs continuous:决策树

应用场景判断:
├─ 离线批量推理(如 benchmark、数据处理)
│   └── 静态 batching 更简单、可复现
├─ 在线服务(如 chatbot、API)
│   └── continuous batching 必须上,否则吞吐浪费严重
└─ 实时性要求极高(如游戏 NPC)
    └── continuous batching + 短请求优先队列

6. 面试官常见深挖追问

  • ”continuous batching 里,Prefill 和 Decode 混合会不会互相拖慢?”
    • 答:会。Prefill 是大矩阵乘法(compute-heavy),Decode 是小矩阵+KV cache读取(memory-heavy)。两者混合时,Prefill 的计算会”掩盖” Decode 的内存等待,反而提升整体利用率。但如果 Prefill 太长(如 10K+ tokens),会阻塞同一 batch 中其他请求的 Decode。解决方案:设置 prefill chunk size,超长 prefill 拆分到多个 step。
  • ”如果一个请求生成了一半 OOM 了,怎么处理?”
    • 答:continuous batching 需要实现”请求级隔离”。OOM 时只 kill 该请求,释放其 KV cache,其他请求继续。vLLM 通过 PagedAttention 的 block-level 管理实现这一点——每个请求有自己独立的 block table,不会互相污染。
  • ”continuous batching 和 dynamic batching 有什么区别?”
    • 答:dynamic batching 是在 batch 开始前动态组合请求(如把长度相近的请求放一起),但 batch 开始后不变;continuous batching 是 batch 运行过程中也能增删请求。前者是”动态组队”,后者是”动态拼车”。

#55. 你详细讲一下 Prefill 和 Decode 两个阶段,瓶颈为什么不一样?

#标准答案

Prefill 和 Decode 的差别,不能只理解成“一个在前一个在后”,而要理解成它们的硬件画像完全不同。Prefill 阶段要把整段输入上下文一次性过模型,所以矩阵计算多、并行度高,更偏 compute-heavy;Decode 阶段则是逐 token 生成,每一步算量没那么大,但高度串行,而且强依赖 KV cache 读取,所以更容易受显存带宽和调度影响。

也正因为两者瓶颈不同,优化手段也不同:Prefill 更关注大矩阵算子效率、长上下文 attention 优化;Decode 更关注 cache 管理、batch 调度、prefix 复用、speculative decoding 等。所以面试里最好把它讲成”两个不同系统问题”。


#深度解析

1. 用 Roofline 模型理解两个阶段的瓶颈

Roofline 模型把性能瓶颈分为两类:

  • Compute-bound:算力是瓶颈,增加并行度能直接提速
  • Memory-bound:带宽是瓶颈,算力再强也喂不饱
阶段 计算特征 瓶颈类型 原因
Prefill 大矩阵乘法 (seq_len, d) × (d, d) Compute-bound 矩阵大,并行度高,GPU 浮点单元能饱和
Decode 小矩阵 (1, d) × (d, d) + KV cache 读取 Memory-bound 每步算量小,但要从显存读大量 K/V 缓存

Prefill 阶段就像一个”大数据中心”,数据量大、计算密集,GPU 能全力运转。 Decode 阶段就像一个”快递分拣站”,每次只处理一个小包裹,但要从大仓库(显存)频繁取货,取货速度成了瓶颈。

2. 具体数字感受

假设模型隐藏维度 d=4096,层数 L=32

Prefill 阶段(输入 1024 token):

  • 每层 Attention: QK^T 计算量 ≈ 2 × 1024² × 4096 ≈ 8.6 TFLOPs
  • 每层 FFN: ≈ 16 × 1024 × 4096² ≈ 275 TFLOPs
  • 总计: 大量浮点运算,GPU 计算单元忙

Decode 阶段(生成 1 个新 token):

  • 每层 Attention: QK^T 计算量 ≈ 2 × 1 × 1024 × 4096 ≈ 8.4 GFLOPs(比 Prefill 小 1000 倍)
  • 但 KV cache 读取: 2 × 32 × 1024 × 4096 × 2 bytes ≈ 512 MB 要从显存读到 SM
  • 512 MB 数据搬运 vs 8 GFLOPs 计算 → 明显的 memory-bound

3. 为什么 Prefill 可以并行,Decode 只能串行?

Prefill 处理的是已知的输入 prompt,所有 token 同时存在,可以并行计算它们的表示。这属于”前向传播”的标准模式。

Decode 生成的是未知输出——第 t+1 个 token 是什么,取决于第 t 个 token 是什么。这种数据依赖性强制串行。除非用 speculative decoding(小模型先猜多个 token,大模型并行验证),否则无法打破串行性。

4. 两个阶段的优化策略差异

优化方向 Prefill 阶段 Decode 阶段
核心问题 大矩阵效率、长序列 attention 显存带宽、cache 管理、调度
关键技术 FlashAttention、算子融合、TP 并行 PagedAttention、continuous batching、GQA、量化
吞吐指标 tokens/sec (处理速度) tokens/sec (生成速度)
延迟敏感 首 token 延迟 (TTFT) 每 token 延迟 (TBT)

5. 面试官常见深挖追问

  • ”为什么 Decode 阶段不能用更大的 batch 来提速?”
    • 答:Decode 阶段 batching 确实能提升吞吐(多个请求同时生成),但有个关键约束:batch 中不同请求的序列长度不同,短的生成完了还要等长的。continuous batching(也叫 in-flight batching)解决这个问题——短请求结束后立刻把新请求塞进来,而不是等整批结束。vLLM 和 TGI 都用这个技术。
  • ”TTFT (Time To First Token) 和 TPOT (Time Per Output Token) 分别由什么决定?”
    • 答:TTFT 主要由 Prefill 阶段决定——输入 prompt 越长、模型越大,首 token 出来越慢。TPOT 主要由 Decode 阶段决定——每生成一个 token 的时间,受 KV cache 带宽、batch 大小、模型层数影响。用户体感”响应快”主要看 TTFT;”生成流畅”主要看 TPOT。
  • ”如果线上既有短查询(10 token 输入)又有长查询(10K token 输入),你怎么调度?”
    • 答:应该把长短请求分开处理。短查询的 Prefill 很快,可以走快速通道;长查询的 Prefill 慢,如果跟短查询混在一起,短查询会被长查询拖慢(head-of-line blocking)。常见做法是:设置输入长度阈值,超过阈值的请求走独立队列或拆分处理。

#56. KV cache 为什么既能提速又能成为显存灾难?你会怎么优化?

#标准答案

KV cache 之所以既能提速又会变成显存灾难,是因为它解决的是“算太多”的问题,却引入了“存太多”的问题。缓存之后,历史 token 不用反复重算,所以 decode 速度显著提升;但代价是每层、每个历史 token 的 K/V 都要常驻显存,长度一上去就会很夸张。

优化思路通常分几类:一类是直接减少 cache 体积,比如 cache 量化、GQA/MQA;一类是改善内存管理,比如 PagedAttention;还有一类是在系统层减少不必要的长上下文和无效 batch。面试时这样回答,能体现你知道”快”和”贵”其实是同一个机制的两面。


#深度解析

1. KV Cache 优化的完整工具箱

优化方向 方法 原理 收益 代价
减少存储 GQA/MQA 减少 K/V 头数 显存降 1/4~1/32 轻微效果损失
减少存储 Cache 量化 (INT8/INT4) 降低 K/V 精度 显存减半或更多 效果损失(K 比 V 敏感)
内存管理 PagedAttention 分页非连续分配 显存利用率 50%→90%+ 实现复杂
内存管理 Prefix Caching 缓存可复用前缀 相同 system prompt 跳过 prefill 需要前缀匹配
动态淘汰 H2O / SnapKV 只保留重要 token 的 K/V 长上下文显存降 30-50% 需要重要性估计
系统层 Continuous Batching 动态组合请求 吞吐提升 10-20× 实现复杂
系统层 请求路由 短请求优先/分级 降低长尾延迟 需要调度策略

2. PagedAttention 的分页机制详解

标准 KV cache 分配的问题:每个请求按最大长度预分配连续显存。比如最大 8K,就分配 8K 空间,即使实际只生成了 100 token,剩下的 7.9K 也浪费了。多个请求并行时,这些碎片累积,显存利用率可能只有 50-70%。

PagedAttention 的解决:把 KV cache 分成固定大小的 block(如每 block 16 个 token)。请求需要多少就分配多少 block,block 之间不连续。就像一个进程的虚拟内存——逻辑上连续,物理上不连续。

额外收益:

  • Copy-on-Write:多个请求共享相同前缀(如 system prompt)时,可以共享 block,只在分叉后才复制。
  • 快速清理:请求结束后只需释放 block,不需要整理碎片。

3. H2O(Heavy Hitter Oracle)的直觉

H2O 发现:在自回归生成中,只有约 20% 的历史 token 获得了大部分 attention 权重(”heavy hitters”),其余 80% 的 token 几乎被忽略。

基于这个观察,H2O 只保留:

  • 最近的几个 token(局部上下文)
  • 历史上 attention 权重最高的少数 token(heavy hitters)

丢弃其他 token 的 K/V,显存大幅减少,但效果损失很小。

SnapKV 是类似思路的改进:在 prefill 阶段通过观察 attention 模式,提前决定哪些 token 的 K/V 值得保留。

4. 量化 KV Cache 的具体做法

KV cache 量化的挑战:K 和 V 的数值分布比权重更动态(per-token 差异大)。

常见策略:

  • Per-channel 量化:对每个 head、每个维度独立计算缩放因子
  • Per-token 量化:对每个 token 的 K/V 向量单独量化
  • 混合精度:K 用 INT8(更敏感),V 用 INT4 或 INT8

实践经验:4-bit KV cache 通常可以承受,但某些层(如 early layers)对量化更敏感,可以保留 FP16。

5. 面试官常见深挖追问

  • ”PagedAttention 的 block size 怎么选?大了好还是小了好?”
    • 答:block 太小(如 1 token)→ 管理开销大,内存分配频繁;block 太大(如 1024 token)→ 内部碎片多(一个请求只用了 block 的前 10%)。通常选 16 或 32 token,在管理开销和碎片率之间平衡。
  • ”Prefix Caching 怎么实现?什么场景收益最大?”
    • 答:用哈希表缓存不同前缀(如 system prompt + few-shot examples)的 KV cache。新请求来时,先匹配前缀哈希,如果命中就直接复用缓存,跳过 prefill。收益最大的是:多轮对话(复用历史上下文)、批量相同 prompt(如批量代码生成)、Agent 循环(复用 system prompt 和工具定义)。
  • ”如果 KV cache 已经爆了,但用户还要求更长上下文,你能做什么?”
    • 答:分层策略:1)先启用 cache 量化(INT8/INT4),显存减半;2)再启用 GQA(如果还没用),进一步降低;3)再用 H2O/SnapKV 淘汰低价值 token;4)如果还爆,考虑把长文档分段处理(chunked prefill)或用 RAG 减少输入长度;5)最后才考虑换更大显存的硬件。

#57. vLLM、TGI、TensorRT-LLM、SGLang 这类推理引擎你怎么比较?

#标准答案

比较 vLLMTGITensorRT-LLMSGLang 这类引擎时,不应该直接说谁最好,而要先讲比较维度:吞吐、首 token 时延、动态 batch 能力、KV cache 管理方式、量化支持、硬件绑定程度、部署和运维复杂度、生态成熟度。

例如有的框架在 NVIDIA 生态上极致压榨性能,但部署链路更重;有的框架在线服务体验和兼容性更好,但极限性能未必最强。所以正确答法通常是:按模型规模、硬件条件、流量画像和团队维护能力去选,而不是做一个脱离场景的排名。


#深度解析

1. 四个引擎的核心差异

维度 vLLM TGI TensorRT-LLM SGLang
核心创新 PagedAttention 安全/易用/生态 极致性能优化 RadixAttention + 编译优化
KV Cache 管理 块级分页(物理块复用) 传统连续分配 优化内存布局 RadixAttention(前缀树复用)
Continuous Batching 支持 支持 支持 支持
量化支持 AWQ/GPTQ/FP8 GPTQ/AWQ FP8/INT8/INT4(最强) 基础支持
硬件绑定 NVIDIA/AMD NVIDIA NVIDIA only NVIDIA/AMD
易用性 中(需调参) 高(HuggingFace 生态) 低(需编译)
适用场景 高并发在线服务 快速原型/社区项目 极致性能生产环境 复杂程序推理(Agent/工具)

2. vLLM 的 PagedAttention 核心原理

传统推理系统为每个请求预先分配一块连续的 KV cache 内存(大小 = max_seq_len)。这导致两个问题:

  • 内部碎片:请求实际生成长度 < max_seq_len,预分配的空间浪费了
  • 外部碎片:不同请求释放后留下不连续的小块,无法被大请求复用

PagedAttention 把 KV cache 分成固定大小的 block(如 16 tokens),像操作系统的虚拟内存一样管理。请求的 KV cache 可以是不连续的 block 列表,通过 block table 映射。这消除了内部和外部碎片,允许:

  • 请求动态扩展(无需预先分配 max_seq_len)
  • 内存共享(copy-on-write,如 beam search 多个候选共享前缀)
  • 更高的 batch size(因为内存利用率高了 2-4 倍)

3. TensorRT-LLM 为什么能做到极致性能?

TensorRT-LLM 基于 NVIDIA TensorRT,核心优势:

  • Kernel 融合:把多个小 op 融合成一个大 kernel,减少 launch overhead
  • 自定义 CUDA Kernel:针对特定模型架构手写最优 kernel(如 FlashAttention 变体)
  • 量化深度优化:支持 FP8(Hopper 架构)、INT4/INT8,且与 Tensor Core 高度匹配
  • 图优化:常量折叠、死代码消除、内存布局优化

代价:模型需要编译(可能耗时数分钟到数小时),且每次改模型结构都要重新编译。灵活性远低于 vLLM/TGI。

4. SGLang 的 RadixAttention 是什么?

SGLang 的核心创新是 RadixAttention,它把 KV cache 组织成前缀树(Radix Tree):

  • 多个请求如果前缀相同(如相同的 system prompt),可以共享前缀的 KV cache
  • 新请求只需计算不同的后缀部分
  • 在 Agent/多轮对话场景中,前缀共享率可达 80-90%,极大降低 prefill 成本

5. 面试官常见深挖追问

  • "如果我的场景是单一模型、固定 batch、追求极致吞吐,选哪个?"
    • 答:TensorRT-LLM。它在固定场景下 kernel 最优,量化支持最深,能压榨出硬件极限性能。但前提是模型不常变、团队有 CUDA 调优能力。
  • "vLLM 的 PagedAttention 和操作系统虚拟内存有什么区别?"
    • 答:思想类似,但实现更简单:1)block size 固定(如 16 tokens),不像 OS 页大小可能变化;2)没有磁盘换出(GPU 无法换出到磁盘);3)block table 由调度器管理,不需要硬件 MMU 支持。核心共同点:物理内存不连续、按需分配、copy-on-write 共享。
  • "如果团队没有 GPU 专家,但又需要高并发服务,选哪个?"
    • 答:vLLM 或 TGI。vLLM 性能更强但需要调参(block size、调度策略);TGI 更易用,HuggingFace 生态成熟,适合快速上线。如果追求"开箱即用"选 TGI,追求"性能天花板"选 vLLM。

#58. 如果线上服务要求首 token 延迟很低,但吞吐也不能差,你会怎么折中?

#标准答案

如果线上既要很低的首 token 延迟,又不能把吞吐做得太差,通常要靠调度层做折中,而不是指望一个单点技巧解决。常见做法包括:把短请求和长请求拆开处理、做分级路由、小模型优先返回、缓存可复用前缀、针对 prefill 和 decode 分别优化,必要时再引入 speculative decoding。

这类问题最怕回答成“上大 batch 就好了”或者“把模型换小一点”。真正成熟的答法是说明:延迟和吞吐来自不同流量画像,需要用调度策略把不同请求分层,而不是用一个全局默认策略硬顶。


#深度解析

1. 首 token 延迟(TTFT)和吞吐(TPOT)的本质矛盾

  • TTFT(Time To First Token):从请求到达,到第一个输出生成的时间。主要由 prefill 阶段决定——模型需要一次性处理整个输入序列的 attention。
  • TPOT(Time Per Output Token):生成每个后续 token 的时间。主要由 decode 阶段决定——逐 token 自回归生成。

矛盾点:

  • 降低 TTFT:需要小 batch、短输入、快速 prefill
  • 提高吞吐:需要大 batch、充分利用 GPU 并行度

两者不是完全对立,但优化方向不同。

2. 分层调度策略

策略 原理 效果
请求分级 简单请求走小模型/缓存,复杂请求走大模型 简单请求 TTFT 极低,复杂请求保证质量
长短分离 短请求(<1K)快速通道,长请求排队批量处理 短请求不被长请求阻塞
前缀缓存 相同 system prompt / 多轮对话前缀复用 KV cache prefill 时间降低 50-90%
prefill-decode 分离 prefill 和 decode 放在不同 GPU / 不同 batch 各自优化,互不干扰
speculative decoding 小模型草拟、大模型验证 降低 TPOT,对 TTFT 影响小

3. 具体数值感受

假设 7B 模型,输入 2K tokens,输出 500 tokens:

策略 TTFT 总延迟 吞吐 (req/s)
基础配置(batch=1) 200ms 5.2s 0.19
batch=8 200ms 5.5s 1.45
前缀缓存(50% 命中) 100ms 5.0s 0.20
长短分离 + 缓存 80ms 4.8s 1.60
prefill-decode 分离 60ms 4.5s 2.00

前缀缓存对 TTFT 改善最显著;prefill-decode 分离对整体吞吐提升最大。

4. 为什么 "prefill-decode 分离" 越来越流行?

Prefill 是 compute-bound(大矩阵乘法),decode 是 memory-bound(小矩阵、大量 KV cache 读取)。两者混在同一个 batch 中时:

  • prefill 请求让 batch 变大,拖慢 decode
  • decode 请求让 prefill 无法全力并行

分离后:

  • Prefill 集群:大 batch、高并行、快速处理输入
  • Decode 集群:小 batch、低延迟、专注逐 token 生成
  • 中间通过 KV cache 传递状态

5. 面试官常见深挖追问

  • "如果 TTFT 要求 < 100ms,但模型 prefill 本身就需 300ms,怎么办?"
    • 答:1)用更小的模型做首层路由/分类,把简单请求直接用小模型回答;2)前缀缓存(如果请求有共享前缀);3)预加载常见查询的结果(缓存最终答案而非 KV cache);4)如果以上都不行,考虑模型蒸馏,用 1-3B 小模型服务简单请求。
  • "前缀缓存和最终答案缓存有什么区别?"
    • 答:前缀缓存复用的是 KV cache(中间状态),适用于"相同前缀、不同后缀"的场景(如多轮对话);最终答案缓存复用的是完整输出,适用于"完全相同的请求"场景(如固定 FAQ)。前缀缓存粒度更细、命中率更高,但实现更复杂(需要维护 Radix Tree 或哈希表)。
  • "speculative decoding 对 TTFT 有帮助吗?"
    • 答:几乎没有。speculative decoding 加速的是 decode 阶段(逐 token 生成),而 TTFT 由 prefill 决定。它降低的是 TPOT 和总生成时间,不是首 token 时间。要降低 TTFT,需要优化 prefill(更短输入、前缀缓存、小模型 prefill)。

#59. 量化为什么有时几乎不掉效果,有时却会明显掉点?

#标准答案

量化有时几乎不掉效果,有时却掉得很明显,核心原因是不同模型、不同层、不同任务对数值误差的容忍度差很多。有些场景只要保持大体语义正确就行,比如常规聊天或简单分类,量化误差不容易被放大。

但在代码、数学、长链推理、工具调用、长上下文这些任务里,小误差可能层层累积,最后放大成明显质量下降。所以量化效果不是一个统一常数,而是“模型结构 x 任务类型 x 量化方案”共同决定的。


#深度解析

1. 量化误差从何而来?

量化是把连续值映射到离散值的过程。例如 FP16 的权重 w = 0.137,量化到 INT8 后变成 w_q = round(0.137 / scale) = 35(假设 scale=0.004)。反量化时 w' = 35 * 0.004 = 0.140

误差:|w - w'| = 0.003。单个权重误差很小,但:

  • 一层有数百万权重,误差累积
  • 深层网络的误差会逐层放大(因为每层的输入已经带上了前层的量化误差)
  • 某些任务对数值精度极度敏感(如 if (x == 0.5) 这种边界判断)

2. 不同精度格式的对比

格式 每参数字节 7B 模型大小 相对精度 常见场景
FP32 4 28 GB 最高 训练
FP16/BF16 2 14 GB 推理标准
INT8 1 7 GB 中高 推理加速
INT4/FP4 0.5 3.5 GB 边缘部署
NF4 (QLoRA) ~0.5 ~4 GB 中高 微调底座

BF16 和 FP16 都是 2 字节,但 BF16 用 8 位指数、7 位尾数;FP16 用 5 位指数、10 位尾数。BF16 范围更大(不容易溢出),FP16 精度更高。

3. 为什么不同任务对量化敏感度不同?

任务 敏感度 原因
自由聊天 语义正确即可,小误差不影响理解
分类/情感分析 低-中 决策边界附近可能受影响
代码生成 变量名、语法结构需要精确
数学计算 数值误差会传播放大
长链推理 每步的小误差累积成大偏差
工具调用 参数格式必须精确匹配 schema

4. 量化方案的选择

  • PTQ (Post-Training Quantization):训练后直接量化,最简单,但效果损失较大
  • QAT (Quantization-Aware Training):训练时模拟量化,让模型适应低精度,效果最好但成本高
  • GPTQ/AWQ:针对 LLM 优化的 PTQ 方法,通过分析权重的重要性做差异化量化(重要权重用更高精度)

5. 面试官常见深挖追问

  • "GPTQ 和 AWQ 有什么区别?"
    • 答:GPTQ 是基于 Optimal Brain Surgeon 框架,逐层量化权重,通过更新未量化的权重来补偿已量化部分的误差;AWQ(Activation-aware Weight Quantization)则认为"对激活值影响大的权重更重要",给这些权重更高的量化精度。AWQ 通常比 GPTQ 更快、效果更稳。
  • "INT8 量化时,缩放因子怎么确定?"
    • 答:常见策略:1)per-tensor:整个张量用一个缩放因子,简单但可能不够精细;2)per-channel:每个输出通道一个缩放因子,更精细;3)per-token:每个输入 token 一个缩放因子,适合动态激活。实践中 per-channel 对权重、per-token 对激活通常是最佳组合。
  • "为什么 4-bit 量化有时效果比 8-bit 还好?"
    • 答:这种情况通常是因为 4-bit 用了更聪明的量化算法(如 GPTQ/AWQ),而 8-bit 用的是简单的逐张量化。4-bit 的"聪明"体现在:1)差异化处理重要权重;2)分组量化减少范围差异;3)补偿更新减少累积误差。所以如果 8-bit 是"笨方法"、4-bit 是"聪明方法",确实可能出现 4-bit 反超。

#60. 如果你要给 7B、32B、70B 三个模型做路由和部署,你会怎样设计推理层?

#标准答案

7B32B70B 三个模型做路由时,核心目标不是让最大模型包打天下,而是让“请求价值”和“模型成本”匹配。常见设计是多级路由:简单问答、低风险请求先走 7B;中等复杂度请求升到 32B;真正高价值、长推理、复杂工具调用或疑难样本再交给 70B

系统层通常还要加上失败回退、缓存复用、成本预算、SLA 约束和灰度策略。这样设计的本质,是把大模型能力当成稀缺资源分配,而不是默认所有请求都值得烧最贵的算力。


#深度解析

1. 路由策略的具体实现

方案 A:基于规则的路由

if 请求长度 < 100 token and 不含专业术语:
    route_to(7B)
elif 请求包含代码 or 数学 or 工具调用:
    route_to(70B)
else:
    route_to(32B)

优点:简单、可解释、延迟低 缺点:规则维护成本高、无法处理边界情况

方案 B:基于小模型分类的路由 用一个极小的模型(如 0.5B)做意图分类和复杂度评分,然后根据分数路由:

  • 复杂度 < 0.3 → 7B
  • 0.3 <= 复杂度 < 0.7 → 32B
  • 复杂度 >= 0.7 → 70B

优点:自适应、可学习 缺点:增加一层延迟、分类器本身也需要维护

方案 C:级联路由(Cascade) 先走 7B,如果置信度低(如输出概率 entropy 高),再升级到 32B,仍不满足再升级到 70B。

优点:简单请求成本低 缺点:长尾请求延迟高(要过多个模型)

2. 成本与效果的权衡

假设三个模型的成本和效果:

模型 每 1K token 成本 通用问答准确率 代码任务准确率
7B $0.001 75% 40%
32B $0.01 88% 70%
70B $0.05 93% 85%

如果 80% 的请求可以用 7B 处理,15% 用 32B,5% 用 70B:

  • 平均成本 = 0.8*0.001 + 0.15*0.01 + 0.05*0.05 = $0.0043
  • 相比全部走 70B($0.05),成本降低 91%

3. 面试官常见深挖追问

  • "路由模型的分类准确率需要达到多少才值得部署?"
    • 答:取决于路由错误的代价。如果把复杂请求错分给 7B,用户得到低质量回答,代价高;如果把简单请求错分给 70B,只是多花了钱,代价低。通常要求"复杂请求不能漏给大模型"的召回率 > 95%,"简单请求不浪费"的精确率 > 80% 就可以上线。
  • "如果 70B 模型挂了,系统怎么降级?"
    • 答:1)自动切换到 32B(如果已加载);2)如果所有大模型都不可用,返回 7B 的结果并标注"模型降级中";3)如果完全不可用,返回缓存的相似历史回答;4)最终兜底:转人工或返回"服务暂时不可用"。
  • "怎么评估路由策略的效果?"
    • 答:A/B 测试:一组用户走固定路由(对照组),一组走智能路由(实验组)。对比指标:1)平均成本;2)用户满意度;3)任务完成率;4)长尾延迟。理想情况下,实验组在保持同等满意度的同时,成本显著降低。