大模型算法工程师面试题精讲(三):模型推理优化高频15问

大模型算法工程师面试题精讲(三):模型推理优化高频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 的方案:

  1. 将 KV Cache 划分为固定大小的 Block(通常 16 个 token 为一个 block)

  2. 每个请求通过 Block Table 映射到物理 block,逻辑连续但物理可不连续

  3. 按需分配 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 的优化:

  1. 分块(Tiling):将 Q、K、V 分成小块(block),每块大小可以放进 SRAM(约 20MB)

  2. 在线 Softmax:逐块计算 softmax,无需等待完整 S 矩阵。通过跟踪 running max 和 running sum 实现数值等价

  3. 融合(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

风险点:

  1. 精度退化在长序列上放大:KV Cache 累积误差会随序列长度增长而放大,长文本生成时出现重复、丢失信息
  2. K比V更敏感:K 直接参与 softmax 计算,量化误差会影响注意力分布;V 只是加权求和,容错性更好
  3. 异常值问题:某些 token 的 K/V 值远大于其他,per-tensor 量化会压缩大部分正常值
  4. 与 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: [===============================] done

    Continuous 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)。这段计算期间:

  1. decode 被阻塞:正在 decode 的请求必须等待 prefill 完成

  2. 长 prompt 拖慢全局:一个 4096 token 的 prefill 可能需要 100+ms,期间所有 decode 请求停滞

  3. 尾延迟恶化:短请求的 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. 推理服务中如何做请求调度和优先级管理?

参考答案:

生产级推理服务需要处理多租户、不同优先级的请求,调度策略直接影响服务质量和成本。

核心调度维度:

  1. 优先级队列(Priority Queue)
  2. 公平调度(Fair Scheduling)
  3. SLA 感知调度(SLA-Aware)
  4. 资源感知调度(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 与工具调用方向的高频面试题,敬请关注。