
🧠 记忆锚点:草稿模型负责猜,目标模型并行验;只有高接受率且草稿够便宜,才真正减少目标模型串行步数。
💡 答案要点
Speculative Decoding = 用小模型猜,用大模型验证
核心思想: 并行生成多个 token,减少大模型调用次数
传统解码的问题:
每生成 1 个 token,需要调用 1 次大模型
生成 100 tokens → 调用 100 次 → 串行,慢Speculative Decoding 流程:
┌─────────────────────────────────────────────────────────┐
│ Speculative Decoding │
└─────────────────────────────────────────────────────────┘
1. 小模型(draft model)快速生成 K 个 tokens
输入:"今天天气" → 输出:"真好啊!"(4个tokens)
2. 大模型(target model)一次性验证这 K 个 tokens
并行计算 4 个 token 的概率分布
3. 接受/拒绝
如果小模型的 token 符合大模型的分布 → 接受
否则 → 拒绝,用大模型的 token
4. 从第一个拒绝位置重新开始
结果:
传统:4 次大模型调用
推测:1 次大模型调用(4 倍加速)数学原理(接受准则):
小模型生成 token t,概率 p_draft(t)
大模型对 token t 的概率 p_target(t)
接受概率:
α = min(1, p_target(t) / p_draft(t))
随机决策:
if random() < α:
接受 token t
else:
用大模型重新采样示例:
输入:"请用一句话描述"
小模型生成(draft):
"今天 天气 真 好"
概率:[0.8, 0.6, 0.7, 0.5]
大模型验证:
Token p_draft p_target α=min(1, p_t/p_d) 接受?
"今天" 0.8 0.9 1.0 ✓
"天气" 0.6 0.7 1.0 ✓
"真" 0.7 0.3 0.43 ✗(随机拒绝)
"好" - - - 跳过
大模型重新生成:
"真" → "非常"
最终输出:"今天 天气 非常" + 继续...
统计:
接受了 2 个 token,拒绝了 1 个
平均接受率:67%性能分析:
| K(草稿长度) | 接受率 | 加速比 |
|---|---|---|
| 2 | 90% | 1.8x |
| 4 | 70% | 2.8x |
| 8 | 50% | 4.0x |
| 16 | 30% | 4.8x |
小模型选择:
| 大模型 | 小模型 | 速度差异 |
|---|---|---|
| Llama 2 70B | Llama 2 7B | 10x |
| GPT-4 | DeepSeek V4-Flash | 5x |
| CodeLlama 34B | CodeLlama 7B | 5x |
优缺点:
| 优点 | 缺点 |
|---|---|
| 加速 2-4 倍 | 需要额外的小模型 |
| 无损(输出分布不变) | 小模型不好时加速有限 |
| 易于实现 | 显存占用增加(两个模型) |
面试话术:
"Speculative Decoding 的思路是先猜再验证。小模型快速生成多个候选,大模型并行验证。接受率 60-70% 时,能加速 2-3 倍,而且输出分布完全不变。"
工程补充:vLLM 中的 PagedAttention 工作流
💡 答案要点
PagedAttention = 虚拟内存管理应用到KV Cache
传统KV Cache问题:
请求 1: 申请 2048 tokens 的 KV 空间,实际只用了 500 → 浪费 75%
请求 2: 申请 1024 tokens,实际用满 → 效率高
请求 3: 申请 4096 tokens,实际用了 3000 → 浪费 27%
总体显存利用率: 约 50% ❌PagedAttention解决方案:
分页管理
- 把KV Cache切成固定大小的Block(如256 tokens)
- 按需分配Block,用多少分多少
- 类似操作系统的虚拟内存分页
动态分配
请求生成第1个token → 分配 Block 1 请求生成第257个token → 分配 Block 2 请求结束 → 回收所有Block内存共享
- 多个请求共享相同的System Prompt
- 只存储一份,多个逻辑地址指向同一物理Block
vLLM性能提升:
| 指标 | 传统服务 | vLLM | 提升 |
|---|---|---|---|
| 显存利用率 | ~50% | ~90% | 1.8x |
| 吞吐量(QPS) | 100 | 240 | 2.4x |
| 平均延迟 | 2.5s | 1.8s | 1.4x |
vLLM核心特性:
from vllm import LLM, SamplingParams
# 初始化vLLM
llm = LLM(
model="meta-llama/Llama-2-7b",
tensor_parallel_size=2, # 多卡推理
dtype="float16",
max_num_seqs=256, # 最大并发请求数
max_model_len=4096, # 最大上下文长度
gpu_memory_utilization=0.9, # 显存利用率
)
#批量推理
prompts = ["问题1", "问题2", ...]
sampling_params = SamplingParams(
temperature=0.8,
top_p=0.95,
max_tokens=100
)
outputs = llm.generate(prompts, sampling_params)vLLM vs 其他推理框架:
| 框架 | 显存利用率 | 吞吐量 | 易用性 |
|---|---|---|---|
| Hugging Face | ~40% | 低 | ⭐⭐⭐ |
| TGI(Text Generation Inference) | ~60% | 中 | ⭐⭐⭐⭐ |
| vLLM | ~90% | 高 | ⭐⭐⭐⭐⭐ |
| TensorRT-LLM | ~85% | 很高 | ⭐⭐(配置复杂) |
PagedAttention工作流程:
1. 用户请求到达
↓
2. 调度器分配 Block
↓
3. Prefill 阶段:并行计算 prompt 的 KV,写入分配的 Block
↓
4. Decode 阶段:每生成 1 个token,检查是否需要新 Block
↓
5. 请求完成,释放所有 Block
↓
6. Block 重新进入空闲池,供新请求使用面试话术:
"vLLM 的代表性机制是 PagedAttention:用块表把逻辑 KV 块映射到可非连续的物理块,按需分配,以减少碎片和按最大长度预留造成的浪费。它能为连续批处理容纳更多序列,但吞吐和显存收益依模型、长度分布、并发、块大小及版本而变,必须在目标硬件上压测。"
工程补充:投机采样实现与参数调优
💡 答案要点
核心思想: 用小模型"猜",大模型"验证",并行生成多个token
问题背景
传统LLM推理的瓶颈:
Autoregressive生成: 必须串行,一个token接一个token
GPT-4生成100个token = 100次前向传播
每次都要加载全部参数 → 慢!
Time Per Output Token (TPOT):
- 7B模型: ~50ms/token
- 70B模型: ~200ms/token
→ 生成100个token需要5-20秒投机采样原理
Step 1: 小模型快速"猜测"
# 小模型(1B参数)快速生成N个候选token
draft_model = "small-1B" # 快10倍
draft_tokens = draft_model.generate(
prompt,
n=5 # 一口气生成5个token
)
# 输出: ["今天", "天气", "很", "不错", ","]Step 2: 大模型并行"验证"
# 大模型(70B参数)一次性并行验证这5个token
target_model = "large-70B"
# 并行计算这5个位置的概率分布
probs = target_model.forward(
prompt + draft_tokens # 一次前向传播
)
# 逐个验证:目标模型的top-1是否等于草稿token
accepted = []
for i, draft_token in enumerate(draft_tokens):
target_top1 = probs[i].argmax()
if target_top1 == draft_token:
accepted.append(draft_token) # 接受
else:
# 拒绝,用目标模型的预测
accepted.append(target_top1)
break # 从拒绝处重新开始
# 结果: ["今天", "天气", "很"] 接受, "不错" 拒绝核心优势:
1次大模型前向传播 = 验证5个token
→ 相当于5倍加速(如果全接受)
实际接受率: 60-80% → 2-3倍加速完整实现
展开 Python 代码示例(71 行)
class SpeculativeDecoding:
def __init__(self, draft_model, target_model, gamma=5):
self.draft = draft_model # 小模型(1B)
self.target = target_model # 大模型(70B)
self.gamma = gamma # 每次猜测的token数
def generate(self, prompt, max_tokens=100):
output_tokens = []
current_prompt = prompt
while len(output_tokens) < max_tokens:
# Step 1: 小模型猜测γ个token
draft_tokens = self.draft.generate(
current_prompt,
max_new_tokens=self.gamma,
do_sample=True # 采样生成
)
# Step 2: 大模型并行验证
# 构造验证序列
verify_seq = current_prompt + draft_tokens
# 一次前向传播得到所有位置的logits
with torch.no_grad():
target_logits = self.target(verify_seq)
# Step 3: 逐个验证
accepted_count = 0
for i in range(self.gamma):
draft_token = draft_tokens[i]
target_dist = torch.softmax(target_logits[len(current_prompt) + i], dim=-1)
draft_dist = torch.softmax(
self.draft(current_prompt + draft_tokens[:i+1])[-1],
dim=-1
)
# 采样验证(保持分布一致性)
adjusted_dist = torch.max(
torch.zeros_like(target_dist),
target_dist - draft_dist
)
adjusted_dist = adjusted_dist / adjusted_dist.sum()
if torch.rand(1) < (target_dist[draft_token] / draft_dist[draft_token]):
# 接受
output_tokens.append(draft_token)
accepted_count += 1
else:
# 拒绝,从调整分布中采样
new_token = torch.multinomial(adjusted_dist, 1).item()
output_tokens.append(new_token)
break
# 更新prompt
current_prompt += output_tokens[-accepted_count:]
print(f"接受 {accepted_count}/{self.gamma} 个token")
return output_tokens
# 使用
sd = SpeculativeDecoding(
draft_model=load_model("1B-draft"),
target_model=load_model("70B-target"),
gamma=5
)
result = sd.generate("今天天气")
# 输出: 接受 3/5 个token
# 接受 4/5 个token
# ...关键参数优化
γ (gamma) - 猜测长度
# γ太小: 加速不明显
gamma = 1 # 每次只猜1个,退化成普通生成
# γ太大: 接受率低,浪费计算
gamma = 20 # 猜太多,大概率被拒绝
# 最优: 4-6
gamma = 5 # 经验最优值
# 理论最优: γ = sqrt(小模型速度/大模型速度)小模型选择:
方案1: 同族小模型
- 大模型: LLaMA-70B
- 小模型: LLaMA-7B
- 优势: 输出风格一致,接受率高(80%)
方案2: 蒸馏模型
- 大模型: GPT-4
- 小模型: GPT-4蒸馏到1B
- 优势: 模仿能力强,接受率高(75%)
方案3: 任意快速模型
- 大模型: Claude
- 小模型: Phi-2
- 劣势: 风格不一致,接受率低(50%)性能分析
加速比计算:
# 传统生成
traditional_time = N_tokens * T_large
# 100 tokens * 200ms = 20秒
# 投机采样
speculative_time = (N_tokens / (gamma * accept_rate)) * T_large + N_tokens * T_small
# (100 / (5 * 0.7)) * 200ms + 100 * 20ms
# = 5.7秒 + 2秒 = 7.7秒
# 加速比 = 20 / 7.7 ≈ 2.6x实测数据(LLaMA-2):
| 配置 | TPOT | 加速比 | 接受率 |
|---|---|---|---|
| 70B单独 | 180ms | 1.0x | - |
| 70B+7B (γ=3) | 95ms | 1.9x | 72% |
| 70B+7B (γ=5) | 68ms | 2.6x | 68% |
| 70B+7B (γ=8) | 75ms | 2.4x | 55% |
最优: γ=5, 加速2.6倍
进阶: Medusa优化
问题: 需要两个模型,部署麻烦
Medusa方案: 一个模型+多个预测头
展开 Python 代码示例(30 行)
class MedusaModel(nn.Module):
def __init__(self, base_model, num_heads=4):
self.base = base_model # 原始大模型
# 添加4个预测头,并行猜测未来4个token
self.medusa_heads = nn.ModuleList([
nn.Linear(hidden_size, vocab_size)
for _ in range(num_heads)
])
def forward(self, x):
# 基础模型输出
hidden = self.base(x)
# 主预测(t+1)
main_pred = self.base.lm_head(hidden)
# Medusa预测(t+2, t+3, t+4, t+5)
medusa_preds = [
head(hidden) for head in self.medusa_heads
]
return main_pred, medusa_preds
# 推理
model = MedusaModel(llama_70B)
main, medusa = model(prompt)
# 主预测 + 4个medusa预测 = 5个候选token
# 然后统一验证优势:
- ✅ 单模型部署
- ✅ 无需额外小模型
- ✅ 接受率更高(共享底层表示)
劣势:
- ❌ 需要微调训练Medusa头
- ❌ 略微增加模型大小(+1-2%)
适用场景
| 场景 | 是否适用 | 原因 |
|---|---|---|
| 代码生成 | ✅推荐 | 高度结构化,小模型猜测准 |
| 翻译 | ✅推荐 | 格式固定,接受率高 |
| 闲聊对话 | ⚠️一般 | 随机性大,接受率中等 |
| 创意写作 | ❌不推荐 | 需要高创造性,猜测难 |
| 批量推理 | ❌不推荐 | 用Continuous Batching更好 |
面试话术:
示例表达(仅在能用本人经历或可复现实验佐证时使用): "投机采样用小模型快速生成5个候选token,大模型一次并行验证,接受就跳过,拒绝就用大模型预测。实测LLaMA-70B+7B组合,γ=5时接受率68%,TPOT从180ms降到68ms,加速2.6倍。适合代码生成等结构化任务,闲聊等高随机性场景效果一般。进阶版Medusa用单模型+多预测头,避免部署两个模型。"
工程补充:Continuous Batching 调度器实现
💡 答案要点
核心思想: 动态批处理,一个请求完成立即补充新请求,GPU永不空闲
问题背景
传统静态批处理的低效:
批次大小: 8
请求长度: [10, 20, 50, 100, 15, 30, 200, 25] tokens
处理过程:
┌─────────────────────────────────────┐
│ Req1 (10) ████░░░░░░░░░░░░░░░░░░░░│ 完成后空等
│ Req2 (20) ████████░░░░░░░░░░░░░░░░│ 完成后空等
│ Req3 (50) ████████████████████░░░░│ 完成后空等
│ Req4 (100) ████████████████████████│ 最长
│ Req5 (15) ██████░░░░░░░░░░░░░░░░░░│ 完成后空等
│ Req6 (30) ████████████░░░░░░░░░░░░│ 完成后空等
│ Req7 (200) ████████████████████████│ ← 等它!
│ Req8 (25) ██████████░░░░░░░░░░░░░░│ 完成后空等
└─────────────────────────────────────┘
所有请求必须等Req7完成(200 steps)才能释放GPU
→ 前7个请求完成后GPU大量空闲 → 浪费!
GPU利用率: 约45% (很多时候只有1-2个请求还在生成)Continuous Batching原理
动态补充,永不等待:
初始批次: [Req1, Req2, Req3, Req4]
Step 10: Req1完成
→ 立即补充 Req5
批次: [Req2, Req3, Req4, Req5]
Step 20: Req2完成
→ 立即补充 Req6
批次: [Req3, Req4, Req5, Req6]
...持续滚动,GPU始终满载
GPU利用率: 约90% ✅实现细节
展开 Python 代码示例(90 行)
class ContinuousBatchingEngine:
def __init__(self, model, max_batch_size=32):
self.model = model
self.max_batch_size = max_batch_size
# 运行中的请求
self.running_requests = []
# 等待队列
self.waiting_queue = deque()
# KV Cache管理(关键!)
self.kv_cache_manager = PagedKVCacheManager()
def add_request(self, request):
"""接收新请求"""
self.waiting_queue.append(request)
def schedule_iteration(self):
"""每个生成步骤的调度"""
# Step 1: 移除已完成的请求
completed = []
for req in self.running_requests:
if req.is_finished():
completed.append(req)
# 释放KV Cache
self.kv_cache_manager.free(req.kv_blocks)
for req in completed:
self.running_requests.remove(req)
print(f"请求{req.id}完成,释放{len(req.kv_blocks)}个KV块")
# Step 2: 从等待队列补充新请求
while (len(self.running_requests) < self.max_batch_size
and self.waiting_queue):
new_req = self.waiting_queue.popleft()
# 分配KV Cache
if self.kv_cache_manager.can_allocate(new_req.estimated_tokens):
new_req.kv_blocks = self.kv_cache_manager.allocate(
new_req.estimated_tokens
)
self.running_requests.append(new_req)
print(f"新请求{new_req.id}加入批次")
else:
# 显存不足,放回队列
self.waiting_queue.appendleft(new_req)
break
# Step 3: 准备批次输入
batch_input_ids = []
batch_position_ids = []
batch_kv_caches = []
for req in self.running_requests:
# 每个请求只输入最新的token
batch_input_ids.append([req.next_token])
batch_position_ids.append([req.current_position])
batch_kv_caches.append(req.kv_blocks)
# Step 4: 批量前向传播
outputs = self.model.forward(
input_ids=torch.tensor(batch_input_ids),
position_ids=torch.tensor(batch_position_ids),
kv_caches=batch_kv_caches
)
# Step 5: 更新每个请求的状态
for i, req in enumerate(self.running_requests):
next_token = outputs[i].argmax().item()
req.append_token(next_token)
req.current_position += 1
def run(self):
"""主循环"""
while self.running_requests or self.waiting_queue:
self.schedule_iteration()
time.sleep(0.001) # 避免忙等
# 使用
engine = ContinuousBatchingEngine(model, max_batch_size=32)
# 不断接收请求
for user_request in incoming_requests():
engine.add_request(user_request)
# 后台持续调度
engine.run()关键技术: PagedAttention
问题: KV Cache的动态内存管理
# 传统预分配
# 问题: 不知道生成多长,保守估计浪费内存
max_len = 2048
kv_cache = torch.zeros(batch_size, num_layers, max_len, hidden_size)
# → 如果实际只生成50 tokens,浪费97.5%内存!PagedAttention方案: 分页管理
展开 Python 代码示例(42 行)
class PagedKVCacheManager:
def __init__(self, block_size=16):
"""
block_size: 每个KV块存储的token数
类似操作系统的内存分页
"""
self.block_size = 16
self.free_blocks = list(range(1000)) # 1000个空闲块
self.allocated_blocks = {} # req_id -> [block_ids]
def allocate(self, req_id, num_tokens):
"""按需分配"""
# 需要几个块?
num_blocks = (num_tokens + self.block_size - 1) // self.block_size
if len(self.free_blocks) < num_blocks:
raise OutOfMemoryError()
# 分配
blocks = [self.free_blocks.pop() for _ in range(num_blocks)]
self.allocated_blocks[req_id] = blocks
return blocks
def free(self, req_id):
"""释放"""
blocks = self.allocated_blocks.pop(req_id)
self.free_blocks.extend(blocks)
print(f"释放{len(blocks)}个块")
def extend(self, req_id):
"""需要更多空间时扩展"""
if not self.free_blocks:
raise OutOfMemoryError()
new_block = self.free_blocks.pop()
self.allocated_blocks[req_id].append(new_block)
return new_block
# 优势
# ✅ 按需分配,零浪费
# ✅ 碎片化少
# ✅ 支持任意长度生成性能对比
实测数据(vLLM,A100-80GB):
| 配置 | 吞吐量(req/s) | GPU利用率 | 平均延迟 |
|---|---|---|---|
| 静态批处理(batch=8) | 12 | 45% | 3.2s |
| 静态批处理(batch=32) | 28 | 68% | 5.8s |
| Continuous Batching | 156 | 92% | 1.1s |
| CB + PagedAttention | 245 | 95% | 0.9s |
提升倍数:
- 吞吐量: 20倍 (12 → 245)
- GPU利用率: 2.1倍 (45% → 95%)
- 延迟降低: 3.5倍 (3.2s → 0.9s)
vLLM完整示例
from vllm import LLM, SamplingParams
# 初始化(自动启用Continuous Batching)
llm = LLM(
model="meta-llama/Llama-2-7b-hf",
tensor_parallel_size=1,
max_num_seqs=128, # 最大批次大小
)
# 采样参数
sampling_params = SamplingParams(
temperature=0.7,
max_tokens=100
)
# 批量推理(内部自动用Continuous Batching)
prompts = [
"什么是AI?",
"写一个Python函数计算斐波那契数列...", # 长输出
"1+1=", # 短输出
# ... 1000个请求
]
outputs = llm.generate(prompts, sampling_params)
# vLLM会:
# 1. 短请求完成后立即从队列补充新请求
# 2. PagedAttention动态管理KV Cache
# 3. GPU始终满载最佳实践
# 1. 设置合理的max_num_seqs
# 太小: 吞吐量低
# 太大: OOM
max_num_seqs = 128 # 示例起点;需按模型、序列长度分布和显存压测调整
# 2. 结合Prefix Caching
# 共享System Prompt的KV Cache
llm = LLM(
model="...",
enable_prefix_caching=True # 共享前缀比例高时可能受益,需实测命中率和开销
)
# 3. 监控指标
metrics = llm.get_metrics()
print(f"GPU利用率: {metrics.gpu_utilization}%")
print(f"KV Cache利用率: {metrics.kv_cache_utilization}%")
print(f"等待队列长度: {metrics.waiting_queue_size}")面试话术:
"Continuous Batching 让已完成序列及时退出、等待请求动态加入,避免整个静态批次被最长请求拖住;再配合分页 KV Cache 管理,可以提高有效并发。实际收益取决于请求到达率、长短分布、调度配置和硬件,应同时比较吞吐、TTFT、TPOT 与 P95/P99,不能用单个公开系统或一组固定倍数作通用结论。"
📚 参考:Fast Inference from Transformers via Speculative Decoding(原论文)