大模型算法工程师面试题精讲(三):模型推理优化高频15问
文章目录
- 大模型算法工程师面试题精讲(三):模型推理优化高频15问
-
- 前言
- [一、KV Cache与注意力优化](#一、KV Cache与注意力优化)
-
- [1. KV Cache的原理是什么?为什么能加速自回归生成?占用多少显存?](#1. KV Cache的原理是什么?为什么能加速自回归生成?占用多少显存?)
- [2. 什么是PagedAttention?解决了KV Cache的什么问题?](#2. 什么是PagedAttention?解决了KV Cache的什么问题?)
- [6. Flash Attention的原理是什么?为什么能减少HBM访问?](#6. Flash Attention的原理是什么?为什么能减少HBM访问?)
- 二、量化与压缩
-
- [3. 模型量化有哪些方法?INT8 vs INT4的区别和精度损失?](#3. 模型量化有哪些方法?INT8 vs INT4的区别和精度损失?)
- [8. 什么是KV Cache量化?有什么风险?](#8. 什么是KV Cache量化?有什么风险?)
- [9. 模型蒸馏有哪些方法?软标签蒸馏 vs 蒸馏到小模型?](#9. 模型蒸馏有哪些方法?软标签蒸馏 vs 蒸馏到小模型?)
- [14. 模型剪枝有哪些策略?结构化 vs 非结构化?](#14. 模型剪枝有哪些策略?结构化 vs 非结构化?)
- 三、解码加速
-
- [4. 什么是投机解码(Speculative Decoding)?能加速多少?](#4. 什么是投机解码(Speculative Decoding)?能加速多少?)
- [5. 连续批处理(Continuous Batching)和静态批处理有什么区别?](#5. 连续批处理(Continuous Batching)和静态批处理有什么区别?)
- [10. 推理时prefill和decode阶段有什么区别?为什么decode是memory-bound?](#10. 推理时prefill和decode阶段有什么区别?为什么decode是memory-bound?)
- [11. 什么是Chunked Prefill?解决了什么问题?](#11. 什么是Chunked Prefill?解决了什么问题?)
- 四、调度与服务化
-
- [7. TensorRT-LLM做了哪些推理优化?](#7. TensorRT-LLM做了哪些推理优化?)
- [12. vLLM的PagedScheduler是怎么工作的?](#12. vLLM的PagedScheduler是怎么工作的?)
- [13. 什么是投机采样(Speculative Sampling)?和投机解码的区别?](#13. 什么是投机采样(Speculative Sampling)?和投机解码的区别?)
- [15. 推理服务中如何做请求调度和优先级管理?](#15. 推理服务中如何做请求调度和优先级管理?)
- 总结
前言
大模型从训练到上线,推理优化是算法工程师必须跨越的最后一公里。无论模型多强,推理延迟、显存占用、吞吐量直接决定了产品体验和部署成本。本篇整理推理优化方向 15 道高频面试题,覆盖 KV Cache、量化、解码加速、调度服务化等核心主题,帮助你在面试中从容应对。
一、KV Cache与注意力优化
1. KV Cache的原理是什么?为什么能加速自回归生成?占用多少显存?
参考答案:
在自回归生成中,每生成一个 token 都需要对前面所有 token 做 Self-Attention。如果没有缓存,第 t t t 步要重新计算前 t − 1 t-1 t−1 个 token 的 K 和 V,计算量为 O ( t 2 ) O(t^2) O(t2)。
KV Cache 的核心思想:把每一步算出的 K 和 V 缓存下来,下一步只需计算新 token 的 Q、K、V,把新 K、V 追加到缓存中,然后用新 Q 对全部缓存 K 做 Attention。
计算复杂度从 O ( t 2 ) O(t^2) O(t2) 降为 O ( t ) O(t) O(t)(每步只算一个 token),大幅减少重复计算。
显存占用估算公式:
KV Cache Size = 2 × L × n kv_heads × d head × seq_len × batch_size × dtype_size \text{KV Cache Size} = 2 \times L \times n_{\text{kv\heads}} \times d{\text{head}} \times \text{seq\_len} \times \text{batch\_size} \times \text{dtype\_size} KV Cache Size=2×L×nkv_heads×dhead×seq_len×batch_size×dtype_size
以 Llama-2-7B(32 层,32 头,d=128,FP16)为例,batch=1,seq_len=2048:
2 × 32 × 32 × 128 × 2048 × 1 × 2 bytes = 1 GB 2 \times 32 \times 32 \times 128 \times 2048 \times 1 \times 2 \text{ bytes} = 1 \text{ GB} 2×32×32×128×2048×1×2 bytes=1 GB
python
def estimate_kv_cache(model_layers, kv_heads, head_dim, seq_len, batch_size, dtype_bytes=2):
return 2 * model_layers * kv_heads * head_dim * seq_len * batch_size * dtype_bytes
# Llama-2-7B, batch=4, seq=4096
print(estimate_kv_cache(32, 32, 128, 4096, 4) / 1e9, "GB")
# 输出: 8.59 GB
| 模型 | 层数 | KV头数 | 头维度 | seq=2048 | seq=8192 |
|---|---|---|---|---|---|
| Llama-2-7B | 32 | 32 | 128 | 1.0 GB | 4.0 GB |
| Llama-2-13B | 40 | 40 | 128 | 1.6 GB | 6.4 GB |
| Llama-2-70B (GQA) | 80 | 8 | 128 | 0.8 GB | 3.2 GB |
GQA(Grouped Query Attention)通过减少 KV 头数,显著降低了 KV Cache 显存。
面试加分点: 能指出 KV Cache 是以空间换时间,且 GQA/MQA 是当前降低 KV Cache 显存的主流方案,长上下文场景下 KV Cache 显存可能超过模型权重本身。
2. 什么是PagedAttention?解决了KV Cache的什么问题?
参考答案:
PagedAttention 是 vLLM 提出的注意力机制,灵感来自操作系统的虚拟内存分页机制。
传统 KV Cache 的痛点在于显存碎片化:
- 内部碎片:预分配最大序列长度,实际序列短,大量显存浪费。预分配 2048 但只生成 200 token,浪费 90%。
- 外部碎片:不同请求序列长度不一,频繁分配/释放后显存空洞散布,无法容纳新请求。
PagedAttention 的方案:
-
将 KV Cache 划分为固定大小的 Block(通常 16 个 token 为一个 block)
-
每个请求通过 Block Table 映射到物理 block,逻辑连续但物理可不连续
-
按需分配 block,生成过程中动态扩展
逻辑视图: [Block 0][Block 1][Block 2][Block 3]
│ │ │ │
Block Table: 7 12 3 18 ← 物理block编号(可不连续)
│ │ │ │
物理显存: ...[7]...[12]...[3]...[18]... ← 散落在显存各处
| 指标 | 传统KV Cache | PagedAttention |
|---|---|---|
| 显存利用率 | 20%-40% | 90%+ |
| 碎片问题 | 严重 | 几乎消除 |
| 请求接纳率 | 受碎片限制 | 接近理论上限 |
| Copy开销 | 无 | 逻辑→物理映射(极小) |
python
class BlockManager:
def __init__(self, num_blocks, block_size=16):
self.num_blocks = num_blocks
self.block_size = block_size
self.free_blocks = list(range(num_blocks))
def allocate(self, num_tokens):
"""按需分配block"""
num_blocks_needed = (num_tokens + self.block_size - 1) // self.block_size
blocks = [self.free_blocks.pop() for _ in range(num_blocks_needed)]
return blocks
面试加分点: 能说出 PagedAttention 让 vLLM 的吞吐量比 HuggingFace Transformers 高 2-4 倍,核心就是显存利用率从约 30% 提升到约 96%,从而支持更大的 batch size。
6. Flash Attention的原理是什么?为什么能减少HBM访问?
参考答案:
Flash Attention 的核心是通过分块计算(Tiling)减少 GPU HBM(高带宽显存)的读写次数,将注意力计算从 HBM-bound 变为 SRAM-bound。
标准 Attention 的瓶颈:
python
# 标准Attention: S = Q @ K^T, P = softmax(S), O = P @ V
# S矩阵大小: [seq_len, seq_len] ------ 序列长时巨大,必须写入HBM
S = Q @ K.T # [N, d] @ [d, N] → [N, N] 写入HBM
P = softmax(S) # 读出S, 计算softmax, 写回P到HBM
O = P @ V # 读出P, 计算O, 写入HBM
# 共3次大矩阵的HBM读写
Flash Attention 的优化:
-
分块(Tiling):将 Q、K、V 分成小块(block),每块大小可以放进 SRAM(约 20MB)
-
在线 Softmax:逐块计算 softmax,无需等待完整 S 矩阵。通过跟踪 running max 和 running sum 实现数值等价
-
融合(Kernel Fusion):QK^T → softmax → PV 在一个 CUDA kernel 内完成,中间结果不写回 HBM
Flash Attention 分块示意:
Q分块: [Q1] [Q2] [Q3] ...
K分块: [K1] [K2] [K3] ...
V分块: [V1] [V2] [V3] ...对每个Qi:
遍历所有Kj, Vj:
Sij = Qi @ Kj^T (在SRAM中)
更新running max & sum (在线softmax)
Oi += Pij @ Vj (在SRAM中)
最终Oi写回HBM
| 对比 | 标准Attention | Flash Attention |
|---|---|---|
| HBM读写 | O(N²d) | O(Nd) |
| 额外显存 | O(N²) | O(N) |
| IO复杂度 | HBM-bound | SRAM-bound |
| 数值结果 | 完全相同 | 完全相同(数学等价) |
面试加分点: Flash Attention v2 进一步优化了并行度(沿序列维度并行)和减少非矩阵乘法 FLOPs,v3 针对 Hopper 架构利用了 FP8 Tensor Core 和异步拷贝,面试中提到版本差异会加分。
二、量化与压缩
3. 模型量化有哪些方法?INT8 vs INT4的区别和精度损失?
参考答案:
模型量化是将 FP16/FP32 权重映射到低比特表示,减少显存和加速推理。主要分两类:
PTQ(Post-Training Quantization,训练后量化):
- 不需要重新训练,直接对权重做量化
- 代表方法:GPTQ、AWQ、SmoothQuant、RTN(Round-to-Nearest)
QAT(Quantization-Aware Training,量化感知训练):
- 训练过程中模拟量化误差,让模型适应低比特
- 精度更好但成本高
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| RTN | 直接四舍五入 | 最简单 | 精度损失大 |
| GPTQ | 基于二阶Hessian信息逐层量化 | 精度好,INT4可用 | 需校准数据 |
| AWQ | 保护重要权重(激活异常值对应的权重) | 精度好,推理快 | 需校准数据 |
| SmoothQuant | 将激活异常值平滑到权重上 | 激活量化友好 | 需调参数 |
INT8 vs INT4 对比:
python
# INT8量化: 每个权重用8bit表示
# 显存: 7B模型 14GB(FP16) → 7GB(INT8) → 3.5GB(INT4)
# GPTQ INT8量化示例
from auto_gptq import AutoGPTQForCausalLM
model = AutoGPTQForCausalLM.from_quantized(
"model_path",
quantize_config={"bits": 8, "group_size": 128}
)
# AWQ INT4量化
from awq import AutoAWQForCausalLM
model = AutoAWQForCausalLM.from_quantized(
"model_path",
quant_config={"w_bit": 4, "q_group_size": 128}
)
| 指标 | FP16 | INT8 | INT4 |
|---|---|---|---|
| 显存(7B) | 14 GB | 7 GB | 3.5 GB |
| 精度损失 | 基准 | <1% | 1-3% |
| 推理速度 | 1x | 1.5-2x | 2-3x |
| 适用场景 | 训练/高精度 | 生产推理 | 边缘部署/大batch |
INT4 的精度损失主要来自权重分布的尾部值被截断、小 group_size 下量化噪声累积、以及激活值与低比特权重的乘法误差放大。
面试加分点: 能指出 AWQ 的核心创新是观察到"激活幅度大的通道对应的权重更重要",因此对这些权重不做量化或用更高精度保护,从而在 INT4 下几乎无损。
8. 什么是KV Cache量化?有什么风险?
参考答案:
KV Cache 量化是将缓存中的 K 和 V 从 FP16 压缩到 INT8 或 INT4,减少显存占用,从而支持更长上下文或更大 batch size。
以 Llama-2-7B 为例,batch=8、seq=4096 时,KV Cache 占用 16 GB,和模型权重相当。长上下文场景(如 32K)KV Cache 可达数十 GB。
主要方法:
python
# 方案1: 直接INT8量化KV Cache
# K和V分别做per-token或per-channel量化
def quantize_kv_cache_fp8(kv_cache):
# per-token量化: 每个token的KV向量单独计算scale
scale = kv_cache.abs().max(dim=-1, keepdim=True).values / 127
quantized = (kv_cache / scale).round().clamp(-128, 127).to(torch.int8)
return quantized, scale
# 方案2: 混合精度 ------ K用INT4, V用INT8
# 因为V对精度更敏感(直接影响Attention输出)
| 量化方案 | 显存节省 | 精度影响 | 适用性 |
|---|---|---|---|
| KV INT8 | 2x | 极小 | 通用 |
| KV INT4 | 4x | 可感知 | 需校准 |
| K-INT4/V-INT8 | 3x | 较小 | 折中方案 |
| FP8 (Hopper) | 2x | 几乎无 | H100/H200 |
风险点:
- 精度退化在长序列上放大:KV Cache 累积误差会随序列长度增长而放大,长文本生成时出现重复、丢失信息
- K比V更敏感:K 直接参与 softmax 计算,量化误差会影响注意力分布;V 只是加权求和,容错性更好
- 异常值问题:某些 token 的 K/V 值远大于其他,per-tensor 量化会压缩大部分正常值
- 与 GQA 的交互:GQA 下 KV 头数少,每个 KV 的精度影响被多个 Q 头共享,量化误差传播更广
面试加分点: 能指出 FP8 KV Cache(Hopper 架构原生支持)是当前最优解,精度几乎无损且硬件加速,INT4 KV Cache 目前仍处于研究阶段,生产环境慎用。
9. 模型蒸馏有哪些方法?软标签蒸馏 vs 蒸馏到小模型?
参考答案:
模型蒸馏(Knowledge Distillation)是将大模型(Teacher)的知识迁移到小模型(Student),降低部署成本。
1. 软标签蒸馏(Soft Label / Logit Distillation):
L = α ⋅ CE ( y , p s ) + ( 1 − α ) ⋅ KL ( p t ( τ ) ∥ p s ( τ ) ) \mathcal{L} = \alpha \cdot \text{CE}(y, p_s) + (1-\alpha) \cdot \text{KL}(p_t^{(\tau)} \| p_s^{(\tau)}) L=α⋅CE(y,ps)+(1−α)⋅KL(pt(τ)∥ps(τ))
其中 p t p_t pt 为 teacher 软标签, p s p_s ps 为 student 输出, τ \tau τ 为温度参数, α \alpha α 为权重。
python
def distillation_loss(student_logits, teacher_logits, labels, T=2.0, alpha=0.5):
# 软标签损失: KL散度
soft_teacher = F.softmax(teacher_logits / T, dim=-1)
soft_student = F.log_softmax(student_logits / T, dim=-1)
distill_loss = F.kl_div(soft_student, soft_teacher, reduction='batchmean') * (T ** 2)
# 硬标签损失
hard_loss = F.cross_entropy(student_logits, labels)
return alpha * hard_loss + (1 - alpha) * distill_loss
2. 序列级蒸馏(Sequence-Level Distillation):
用 Teacher 生成训练数据,Student 在这些数据上做 SFT。适合生成任务,比 logit 蒸馏更实用。
3. 黑盒蒸馏(API Distillation):
只能调用 Teacher API(如 GPT-4),无法获取 logits。用 Teacher 生成多样化指令-回答对,Student 做指令微调。Alpaca、Vicuna 都是此路线。
| 方法 | 需要Teacher权重 | 数据量 | 效果 | 代表 |
|---|---|---|---|---|
| 软标签蒸馏 | 是 | 中 | 好 | MiniLM |
| 序列蒸馏 | 是 | 大 | 很好 | DistilBERT |
| API蒸馏 | 否 | 大 | 中-好 | Alpaca |
| 特征蒸馏 | 是 | 中 | 好 | TinyBERT |
面试加分点: 能指出大模型时代 API 蒸馏是最常用方案,但要注意"模型坍缩"风险------纯蒸馏数据训练的模型多样性下降,且可能违反 Teacher 模型的使用条款。
14. 模型剪枝有哪些策略?结构化 vs 非结构化?
参考答案:
模型剪枝(Pruning)通过移除不重要的权重或结构来减少模型大小和计算量。
非结构化剪枝(Unstructured Pruning):
- 逐个权重置零,得到稀疏矩阵
- 理论上可减 90% 参数,但需要稀疏矩阵硬件支持才能加速
- 代表:Magnitude Pruning(按绝对值大小剪)
python
def magnitude_prune(weight, sparsity=0.5):
threshold = torch.quantile(weight.abs().flatten(), sparsity)
mask = weight.abs() > threshold
return weight * mask
结构化剪枝(Structured Pruning):
- 整个通道/头/层移除,权重矩阵仍密集
- 无需特殊硬件即可加速
- 代表:LLM-Pruner、Sheared LLaMA、Wanda
python
def channel_prune(conv_layer, num_prune):
# 计算每个通道的重要性(如L1 norm)
importance = conv_layer.weight.norm(p=1, dim=(1,2,3))
# 剪掉最不重要的通道
prune_indices = importance.argsort()[:num_prune]
mask = torch.ones(conv_layer.out_channels)
mask[prune_indices] = 0
return conv_layer.weight * mask.view(-1,1,1,1)
| 维度 | 非结构化 | 结构化 |
|---|---|---|
| 粒度 | 单个权重 | 通道/头/层 |
| 稀疏性 | 细粒度 | 粗粒度 |
| 硬件加速 | 需稀疏支持 | 直接加速 |
| 精度损失 | 较小 | 较大 |
| 通用性 | 理论优美 | 工程实用 |
大模型剪枝的挑战:剪枝后模型能力骤降需要重新预训练恢复,注意力头之间存在耦合,且不同任务依赖不同的头/层。
面试加分点: 能指出 Wanda(Weights and Activations)是 2024 年热门的大模型剪枝方法,仅用权重幅度和激活范数的乘积作为重要性指标,无需重训练即可在 50% 稀疏度下保持性能。
三、解码加速
4. 什么是投机解码(Speculative Decoding)?能加速多少?
参考答案:
投机解码的核心思想:用一个小的 Draft Model 快速生成若干候选 token,再用大 Target Model 并行验证,如果候选正确则一次接受多个 token,实现加速。
标准自回归解码每步只能生成 1 个 token(大模型前向一次)。投机解码让小模型先"猜" k k k 个 token,大模型一次前向验证全部:
标准解码:
大模型: t1 → t2 → t3 → t4 → t5 (5次前向)
投机解码:
小模型: t1' → t2' → t3' → t4' → t5' (很快)
大模型: 一次性验证 t1't2't3't4't5' (1次前向)
如果前3个正确, 后2个错误:
接受t1't2't3', 拒绝t4't5'
大模型从t3'重新生成t4'', 小模型继续猜t5''
接受概率与加速比:
如果每个 token 的接受概率为 α \alpha α,则期望接受长度为:
E accepted = 1 − α k + 1 1 − α E\\text{accepted} = \frac{1 - \alpha^{k+1}}{1 - \alpha} Eaccepted=1−α1−αk+1
当 α = 0.7 , k = 4 \alpha = 0.7, k = 4 α=0.7,k=4 时,期望接受约 3.3 个 token,加速约 2-3x。
python
def speculative_decode(target_model, draft_model, prompt, max_tokens, k=4):
tokens = prompt
while len(tokens) < max_tokens:
# 1. 小模型生成k个候选token
draft_tokens = []
for _ in range(k):
draft_token = draft_model.generate_one(tokens + draft_tokens)
draft_tokens.append(draft_token)
# 2. 大模型一次性验证
target_logits = target_model.forward(tokens + draft_tokens)
# 3. 逐个验证, 找到第一个拒绝位置
accepted = 0
for i, dt in enumerate(draft_tokens):
target_dist = softmax(target_logits[len(tokens) + i])
draft_dist = softmax(draft_model.last_logits)
r = random.random()
if r < target_dist[dt] / draft_dist[dt]:
accepted += 1
else:
residual = (target_dist - draft_dist).clamp(min=0)
residual = residual / residual.sum()
new_token = sample(residual)
break
tokens += draft_tokens[:accepted]
if accepted < k:
tokens.append(new_token)
return tokens
| 场景 | 加速比 | 限制 |
|---|---|---|
| 代码生成 | 2-3x | 小模型需与任务匹配 |
| 对话 | 1.5-2x | 多样性高的任务接受率低 |
| 重复性任务 | 3-4x | 分布差异大时退化 |
面试加分点: 投机解码的关键约束是小模型和大模型的分布要对齐(通常用同系列不同规模的模型),且验证阶段大模型的前向计算可以完全并行化。Medusa 则用多个头替代独立小模型,避免了维护两个模型的复杂度。
5. 连续批处理(Continuous Batching)和静态批处理有什么区别?
参考答案:
静态批处理(Static Batching):
- 请求组成一个 batch 后,等所有请求生成完毕才释放
- 短请求要等长请求完成,GPU 空转严重
- batch 内位置固定,无法动态插入新请求
连续批处理(Continuous Batching / In-Flight Batching):
-
每一步迭代后检查:有请求完成则移出,有新请求则插入
-
batch 大小动态变化,GPU 始终满载
-
配合 PagedAttention,新请求可以立即分配 block 开始生成
时间 →
Static Batching:
Req A: [==========================] done
Req B: [============] waiting... done ← B完成后空等A
Req C: [===============================] doneContinuous Batching:
Req A: [==========================] done
Req B: [============] done
Req D: [================] done ← B完成后D立即插入
Req E: [===========] done ← D运行中E插入
Req C: [===============================] done
| 指标 | 静态批处理 | 连续批处理 |
|---|---|---|
| GPU利用率 | 30%-50% | 70%-90% |
| 吞吐量 | 基准 | 2-4x |
| 尾延迟 | 高 | 低 |
| 实现复杂度 | 简单 | 需精细调度 |
python
class ContinuousBatchScheduler:
def __init__(self, max_batch_size):
self.max_batch_size = max_batch_size
self.running = []
self.waiting = []
def step(self):
# 1. 移出已完成的请求
self.running = [r for r in self.running if not r.finished]
# 2. 填充新请求
while len(self.running) < self.max_batch_size and self.waiting:
self.running.append(self.waiting.pop(0))
# 3. 前向
if self.running:
tokens = [r.current_token for r in self.running]
outputs = model.forward(tokens)
for r, out in zip(self.running, outputs):
r.update(out)
面试加分点: 连续批处理的核心挑战是不同请求的 prefill 和 decode 阶段混合调度,prefill 是 compute-bound,decode 是 memory-bound,混合时 prefill 会阻塞 decode,这正是 Chunked Prefill 要解决的问题。
10. 推理时prefill和decode阶段有什么区别?为什么decode是memory-bound?
参考答案:
大模型推理分两个阶段:
Prefill 阶段:
- 处理输入 prompt 的所有 token,计算 KV Cache
- 一次性并行处理所有 prompt token
- 计算量大,GPU 算力充分利用
Decode 阶段:
- 逐个生成输出 token,每步用 KV Cache 做注意力
- 每步只处理 1 个 token(batch 内每个请求)
- 计算量小,但需要读取全部 KV Cache
| 维度 | Prefill | Decode |
|---|---|---|
| 输入token数 | N(prompt长度) | 1(逐token) |
| 计算量 | O(N²·d) | O(N·d) |
| 访存量 | O(N·d) | O(N·d) |
| 瓶颈类型 | Compute-bound | Memory-bound |
| GPU算力利用 | 高 | 极低(<5%) |
为什么 Decode 是 Memory-bound?
算术强度(Arithmetic Intensity)= FLOPs / Bytes:
Decode 阶段每步 FLOPs 约 2 N d 2Nd 2Nd,Bytes 约 2 N d × dtype_size 2Nd \times \text{dtype\_size} 2Nd×dtype_size。以 FP16 为例,算术强度约 1 FLOP/Byte,而 A100 的峰值算术强度为 312 TFLOPS / 2 TB/s = 156 FLOP/Byte。计算量远不够"喂饱"算力单元,瓶颈在显存带宽。
A100 GPU:
计算能力: 312 TFLOPS (FP16)
显存带宽: 2.0 TB/s
平衡点: 312/2.0 = 156 FLOP/Byte
Decode算术强度: ~1 FLOP/Byte → 利用率 < 1%
Prefill算术强度: ~数十到数百 FLOP/Byte → 利用率较高
这意味着 decode 阶段加 batch size 几乎不影响延迟(因为算力空闲),但能线性提升吞吐量------这是连续批处理有效的基础。
面试加分点: 正因为 decode 是 memory-bound,INT4/INT8 量化能显著加速 decode(减少访存量),而 prefill 阶段量化加速不明显。这也是为什么一些系统采用"W8A8"策略时主要收益在 decode 阶段。
11. 什么是Chunked Prefill?解决了什么问题?
参考答案:
Chunked Prefill 是将长 prompt 的 prefill 计算分成多个 chunk 逐步执行,而不是一次性处理整个 prompt。
要解决的问题:
在连续批处理中,当一个新请求到达时,其 prefill 阶段需要一次性处理整个 prompt(可能几千 token)。这段计算期间:
-
decode 被阻塞:正在 decode 的请求必须等待 prefill 完成
-
长 prompt 拖慢全局:一个 4096 token 的 prefill 可能需要 100+ms,期间所有 decode 请求停滞
-
尾延迟恶化:短请求的 token 生成出现"卡顿"
无Chunked Prefill:
时间: |----prefill(4000 tokens)----|--decode--|--decode--|
其他decode请求: |######## 等待 ########|正常|正常|有Chunked Prefill (chunk_size=512):
时间: |prefill_512|decode|prefill_512|decode|prefill_512|decode|...
其他decode请求: |正常|正常|正常|正常|正常|正常|...
python
class ChunkedPrefillScheduler:
def __init__(self, chunk_size=512):
self.chunk_size = chunk_size
def schedule_step(self, running_requests):
# 优先处理decode请求
decode_batch = [r for r in running_requests if r.phase == 'decode']
# 如果有空余batch容量, 插入一个prefill chunk
prefill_batch = []
for r in running_requests:
if r.phase == 'prefill':
chunk = r.next_chunk(self.chunk_size)
prefill_batch.append((r, chunk))
break
self.execute(decode_batch, prefill_batch)
| 参数 | 无Chunked | Chunked (512) |
|---|---|---|
| Prefill延迟 | 一次性完成 | 略增加(分步开销) |
| Decode尾延迟 | 高 | 低 |
| 整体吞吐 | 降低 | 提升10-20% |
| TTFT(首token延迟) | 长prompt高 | 略增加但可接受 |
面试加分点: Chunked Prefill 的本质是将 compute-bound 的 prefill 切碎,与 memory-bound 的 decode 交错执行,使 GPU 算力在两个阶段之间更均衡分配。vLLM 默认开启此功能,chunk_size 通常设为 512-2048。
四、调度与服务化
7. TensorRT-LLM做了哪些推理优化?
参考答案:
TensorRT-LLM 是 NVIDIA 推出的大模型推理优化库,基于 TensorRT 引擎,整合了多种优化技术:
1. 算子融合(Kernel Fusion):
- 将 QKV 投影、多头注意力、MLP 等算子融合为单个 CUDA kernel
- 减少中间结果写入 HBM 和 kernel launch 开销
2. Continuous Batching: 原生支持动态 batch,请求级别动态调度。
3. 多种量化支持: INT8 SmoothQuant、INT4 Weight-Only、FP8(Hopper 架构),自动选择最优量化方案。
4. KV Cache 管理: TensorRT 引擎内置 KV Cache 池化管理,支持 PagedAttention 类似的分块管理。
5. In-Flight Batching + Paged KV Cache:
python
import tensorrt_llm
from tensorrt_llm.models import LLaMAForCausalLM
builder = tensorrt_llm.Builder()
config = builder.create_builder_config(
max_batch_size=32,
max_seq_len=4096,
kv_cache_type=tensorrt_llm.KVCacheType.INT8,
quant_mode=tensorrt_llm.QuantMode.INT8_WEIGHT,
use_paged_context_fmha=True,
use_inflight_batching=True,
)
model = LLaMAForCausalLM.from_hugging_face("meta-llama/Llama-2-7B")
engine = builder.build(model, config)
| 优化技术 | 加速效果 | 适用阶段 |
|---|---|---|
| 算子融合 | 1.3-1.5x | Prefill + Decode |
| Continuous Batching | 2-4x吞吐 | 整体 |
| FP8量化 | 1.5-2x | Prefill + Decode |
| KV Cache INT8 | 减少显存50% | Decode |
| Paged Attention | 显存利用率90%+ | 整体 |
其他优化还包括 Plugin 机制(自定义高性能注意力 kernel)、Graph Optimization(计算图级别的常量折叠、死代码消除)、多 GPU 并行(Tensor Parallel + Pipeline Parallel)。
面试加分点: TensorRT-LLM 的最大优势是"NVIDIA 官方"------针对每一代 GPU 架构(Ampere/Hopper/Blackwell)都有专门的 kernel 优化,是 NVIDIA GPU 上的性能天花板。缺点是绑定 NVIDIA 硬件,且构建引擎过程较长,灵活性不如 vLLM。
12. vLLM的PagedScheduler是怎么工作的?
参考答案:
vLLM 的 PagedScheduler 是结合 PagedAttention 的请求调度器,核心流程:
1. 请求生命周期:
请求到达 → 加入WAITING队列
→ 调度器分配KV Cache block → 进入RUNNING队列
→ 生成完成或触发abort → 回收block → 释放
2. 调度策略(FIFO + Preemption):
python
class PagedScheduler:
def __init__(self, block_manager, max_num_seqs):
self.block_manager = block_manager
self.waiting = [] # 等待prefill的请求
self.running = [] # 正在decode的请求
self.swapped = [] # 被换出的请求
def schedule(self):
# 1. 优先调度RUNNING中的decode请求
num_new_tokens = len(self.running)
# 2. 检查是否有足够KV Cache接纳新请求
while self.waiting and len(self.running) < self.max_num_seqs:
req = self.waiting[0]
if self.block_manager.can_allocate(req):
self.block_manager.allocate(req)
self.running.append(req)
self.waiting.pop(0)
else:
break
# 3. 显存不足时的换出策略
if not self.can_schedule():
self._preempt()
3. Preemption(抢占)机制:
当 KV Cache 不足时,vLLM 有两种策略:
-
SWAP:将请求的 KV Cache 换到 CPU 内存,腾出 GPU 显存
-
RECOMPUTE:丢弃 KV Cache,只保留已生成 token,之后重新 prefill
显存不足时:
RUNNING: [A, B, C, D] ← 显存不够SWAP策略: D的KV Cache → CPU内存, D进入SWAPPED队列 RUNNING: [A, B, C] ← 腾出空间接纳新请求 RECOMPUTE策略: D的KV Cache丢弃, D回到WAITING队列 RUNNING: [A, B, C] ← 腾出更多空间 D重新调度时需重新prefill (但只需算已生成token的KV)
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| SWAP | 不需重算 | CPU↔GPU传输开销 | 显存紧张但不太极端 |
| RECOMPUTE | 立即释放显存 | 需重新prefill | 显存极度紧张 |
面试加分点: 能指出 vLLM 默认使用 RECOMPUTE 策略(比 SWAP 更简单且在大多数场景下更好),因为重新 prefill 的计算成本通常低于 PCIe 传输开销,且现代 GPU 算力充裕。
13. 什么是投机采样(Speculative Sampling)?和投机解码的区别?
参考答案:
投机采样(Speculative Sampling)和投机解码(Speculative Decoding)在广义上常被混用,但严格来说有细微区别:
投机解码(Speculative Decoding):
- 强调"解码过程"的加速
- Draft Model 生成候选序列,Target Model 验证
- 验证通过则接受多个 token,否则从拒绝点重新生成
- 保证输出分布与 Target Model 完全一致
投机采样(Speculative Sampling):
- 更强调"采样策略"层面的改进
- 核心是修改接受/拒绝的采样规则,使得最终分布等价于 Target Model
- 数学基础是拒绝采样(Rejection Sampling)
两者在工程实现上通常是一体的,投机采样提供了分布等价的数学保证:
python
# 投机采样的接受规则
# 对于draft模型采样的token x, 概率为q(x)
# target模型的概率为p(x)
# 接受概率 = min(1, p(x)/q(x))
def accept_reject_sampling(target_prob, draft_prob, draft_token):
"""拒绝采样确保分布等价"""
ratio = target_prob[draft_token] / draft_prob[draft_token]
if random.random() < min(1.0, ratio):
return True, draft_token # 接受
else:
# 拒绝, 从残差分布采样
residual = (target_prob - draft_prob).clamp(min=0)
residual = residual / residual.sum()
return False, torch.multinomial(residual)
| 维度 | 投机解码 | 投机采样 |
|---|---|---|
| 关注点 | 整体加速流程 | 采样等价性证明 |
| 输出保证 | 与Target Model分布一致 | 数学证明等价 |
| Draft Model | 可独立模型/多头 | 可独立模型/多头 |
| 工程关系 | 通常包含投机采样 | 投机解码的数学基础 |
变种方法:
- Stochastic Speculative Sampling:在拒绝时不是从残差分布采样,而是引入随机性减少偏差
- Lossless Acceleration:投机采样最核心的卖点是"无损"------输出分布与 Target Model 完全一致,不牺牲质量
面试加分点: 能指出投机采样的"无损"特性是相对其他加速方法(如量化、剪枝)的最大优势,理论上只要 Draft Model 与 Target Model 分布有足够重叠,就能获得加速且输出质量完全不变。
15. 推理服务中如何做请求调度和优先级管理?
参考答案:
生产级推理服务需要处理多租户、不同优先级的请求,调度策略直接影响服务质量和成本。
核心调度维度:
- 优先级队列(Priority Queue)
- 公平调度(Fair Scheduling)
- SLA 感知调度(SLA-Aware)
- 资源感知调度(Resource-Aware)
python
class PriorityScheduler:
def __init__(self, max_batch_size, max_kv_cache_blocks):
self.max_batch_size = max_batch_size
self.queues = {
'high': [], # 高优先级(付费用户、实时对话)
'medium': [], # 中优先级(批量API调用)
'low': [] # 低优先级(后台任务、预热)
}
self.running = []
self.block_manager = BlockManager(max_kv_cache_blocks)
def add_request(self, request, priority='medium'):
request.priority = priority
request.arrival_time = time.time()
self.queues[priority].append(request)
def schedule(self):
# 1. 移出已完成请求, 回收资源
self.running = [r for r in self.running if not r.finished]
for r in self.running:
if r.finished:
self.block_manager.free(r.blocks)
# 2. 按优先级填充batch
while len(self.running) < self.max_batch_size:
req = self._get_next_request()
if req is None:
break
if self.block_manager.can_allocate(req):
self.block_manager.allocate(req)
self.running.append(req)
else:
# 资源不足, 尝试抢占低优先级请求
self._preempt_low_priority()
def _get_next_request(self):
# 优先级 + FIFO
for priority in ['high', 'medium', 'low']:
if self.queues[priority]:
return self.queues[priority].pop(0)
return None
def _preempt_low_priority(self):
"""抢占低优先级请求的资源"""
for i, r in enumerate(self.running):
if r.priority == 'low':
self.block_manager.free(r.blocks)
r.phase = 'waiting'
self.queues['low'].insert(0, r) # 重新入队
self.running.pop(i)
return True
return False
SLA 感知调度:
python
def sla_aware_priority(request):
"""根据SLA剩余时间动态调整优先级"""
if request.deadline:
remaining = request.deadline - time.time()
if remaining < 1.0: # 临近超时
return 'critical'
elif remaining < 5.0:
return 'high'
return request.base_priority
| 调度策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 严格优先级 | 高优先级体验好 | 低优先级饥饿 | 付费/免费分层 |
| 加权公平 | 无饥饿 | 高优先级不突出 | 多租户平等 |
| SLA感知 | 动态适应 | 实现复杂 | 对延迟敏感场景 |
| 资源感知 | 最大化利用率 | 需准确预估 | 混合长短请求 |
面试加分点: 能指出生产环境通常采用"加权公平 + SLA 感知"的混合策略,并配合请求预热(Speculative Prefill)、前缀缓存(Prefix Caching)等手段降低 TTFT。前缀缓存是指多个请求共享相同前缀 prompt 时复用 KV Cache,在 RAG、系统 prompt 等场景下可减少 30-50% 的 prefill 计算。
总结
模型推理优化是大模型算法工程师从"能训模型"到"能上线模型"的关键能力。本文 15 道高频面试题覆盖了四大核心方向:
| 方向 | 核心问题 | 代表技术 |
|---|---|---|
| KV Cache与注意力优化 | 显存效率、计算效率 | KV Cache、PagedAttention、Flash Attention |
| 量化与压缩 | 减少显存、加速推理 | GPTQ、AWQ、KV Cache量化、蒸馏、剪枝 |
| 解码加速 | 降低延迟、提升吞吐 | 投机解码、连续批处理、Chunked Prefill |
| 调度与服务化 | 最大化GPU利用率 | TensorRT-LLM、vLLM、优先级调度 |
核心面试主线: 理解 prefill 是 compute-bound 而 decode 是 memory-bound 这一根本区别,几乎所有推理优化技术都可以沿着这条线理解------量化加速 decode(减少访存)、连续批处理利用 decode 的空闲算力、Flash Attention 加速 prefill 的 HBM 瓶颈、PagedAttention 解决 KV Cache 显存碎片。掌握这条主线,面试中遇到任何推理优化问题都能找到分析框架。
下一篇文章将进入 RAG 与工具调用方向的高频面试题,敬请关注。