Skip to content
🔗 分享本题
查看我的学习进度 →

草稿模型提案、目标模型并行验证及拒绝校正的投机解码图

🧠 记忆锚点:草稿模型负责猜,目标模型并行验;只有高接受率且草稿够便宜,才真正减少目标模型串行步数。

💡 答案要点

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(草稿长度)接受率加速比
290%1.8x
470%2.8x
850%4.0x
1630%4.8x

小模型选择:

大模型小模型速度差异
Llama 2 70BLlama 2 7B10x
GPT-4DeepSeek V4-Flash5x
CodeLlama 34BCodeLlama 7B5x

优缺点:

优点缺点
加速 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解决方案:

  1. 分页管理

    • 把KV Cache切成固定大小的Block(如256 tokens)
    • 按需分配Block,用多少分多少
    • 类似操作系统的虚拟内存分页
  2. 动态分配

    请求生成第1个token → 分配 Block 1
    请求生成第257个token → 分配 Block 2
    请求结束 → 回收所有Block
  3. 内存共享

    • 多个请求共享相同的System Prompt
    • 只存储一份,多个逻辑地址指向同一物理Block

vLLM性能提升:

指标传统服务vLLM提升
显存利用率~50%~90%1.8x
吞吐量(QPS)1002402.4x
平均延迟2.5s1.8s1.4x

vLLM核心特性:

python
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: 小模型快速"猜测"

python
# 小模型(1B参数)快速生成N个候选token
draft_model = "small-1B"  # 快10倍
draft_tokens = draft_model.generate(
    prompt,
    n=5  # 一口气生成5个token
)
# 输出: ["今天", "天气", "很", "不错", ","]

Step 2: 大模型并行"验证"

python
# 大模型(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 行)
python
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) - 猜测长度

python
# γ太小: 加速不明显
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%)

性能分析

加速比计算:

python
# 传统生成
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单独180ms1.0x-
70B+7B (γ=3)95ms1.9x72%
70B+7B (γ=5)68ms2.6x68%
70B+7B (γ=8)75ms2.4x55%

最优: γ=5, 加速2.6倍

进阶: Medusa优化

问题: 需要两个模型,部署麻烦

Medusa方案: 一个模型+多个预测头

展开 Python 代码示例(30 行)
python
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 行)
python
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的动态内存管理

python
# 传统预分配
# 问题: 不知道生成多长,保守估计浪费内存
max_len = 2048
kv_cache = torch.zeros(batch_size, num_layers, max_len, hidden_size)
# → 如果实际只生成50 tokens,浪费97.5%内存!

PagedAttention方案: 分页管理

展开 Python 代码示例(42 行)
python
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)1245%3.2s
静态批处理(batch=32)2868%5.8s
Continuous Batching15692%1.1s
CB + PagedAttention24595%0.9s

提升倍数:

  • 吞吐量: 20倍 (12 → 245)
  • GPU利用率: 2.1倍 (45% → 95%)
  • 延迟降低: 3.5倍 (3.2s → 0.9s)

vLLM完整示例

python
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始终满载

最佳实践

python
# 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(原论文)