#模块五:推理优化、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_t 和 V_1...V_t 共 t 个。缓存 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% | 1× | 基准 |
| 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 是"重点保护"。
- 答:GPTQ 从"输出重建误差"角度出发,逐层优化量化后的权重,让
- "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 这类推理引擎你怎么比较?
#标准答案
比较 vLLM、TGI、TensorRT-LLM、SGLang 这类引擎时,不应该直接说谁最好,而要先讲比较维度:吞吐、首 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 三个模型做路由和部署,你会怎样设计推理层?
#标准答案
给 7B、32B、70B 三个模型做路由时,核心目标不是让最大模型包打天下,而是让“请求价值”和“模型成本”匹配。常见设计是多级路由:简单问答、低风险请求先走 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)长尾延迟。理想情况下,实验组在保持同等满意度的同时,成本显著降低。