推理与解码策略
摘要: 大语言模型在生成文本时,每一步都是从整个词表中选出下一个最合适的 token,而解码策略决定了这道选择题的求解方式。贪心解码永远选择概率最高的 token,效率高但容易陷入重复;Beam Search 维护多条候选序列,在翻译等确定性任务中表现优异,但会牺牲生成多样性。Top-k 与 Top-p(核采样)通过限制候选集大小引入可控随机性,温度系数则在 Softmax 之前缩放 logits 以调节概率分布的陡峭程度。本文从机制原理、适用场景和参数调优三个维度对比了各策略的优劣,并介绍了结合多种策略优势的对比搜索(Contrastive Search)等混合方案,为不同应用场景下的解码策略选型提供参考框架。
1. 引言
在前面的文章中,我们已经了解了模型如何被训练出来。但模型训练完成后,还有一个关键问题:给定一个已经训练好的模型,它如何"写出"一个个 token? 这就是推理与解码策略要解决的问题。
一个 LLM 在生成文本时,每一步实际上是在做一道选择题:给定当前的上下文,从整个词表(词汇量通常 5 万到 20 万)中选出下一个最合适的 token。
解码策略决定了这道选择题怎么做——是永远选最确定的那个,还是偶尔冒险尝试一些可能性?
2. 解码策略
2.1 Greedy Decoding(贪心解码)
贪心解码是最简单直观的策略:每一步都选择概率最高的那个 token。
时间步 1: "我" → 模型计算 → {的: 0.3, 想: 0.25, 要: 0.15, ...}
→ 选 "的" (概率最高)
时间步 2: "我的" → 模型计算 → {天: 0.4, 猫: 0.2, ...}
→ 选 "天" (概率最高)
时间步 3: "我的天" → ... 最终输出:"我的天哪!太棒了!"优点:速度极快,每一步只做一次 argmax 操作。
缺点:
- 确定性太强:同一个输入永远产生同一个输出,缺乏多样性
- 容易陷入重复循环:一旦模型进入一个高频短语模式,可能不断重复同一句话
- 没有全局最优保证:贪心算法在每一步选局部最优,未必得到整体最优序列
直观类比:贪心解码就像一个在迷宫里永远走"看起来最近的路"的人——每一步都是局部最优,但很可能走进死胡同。
2.2 Beam Search(束搜索)
束搜索是贪心解码的升级版:每步保留 k 个最有可能的候选序列,而不是只保留 1 个。k 称为 beam width(束宽)。
beam_width = 2 的示例:
时间步 1: 模型评估所有可能的第一个 token
候选序列:
["我"] score = -0.5
["今"] score = -1.2
保留 Top-2:["我", -0.5], ["今", -1.2]
时间步 2: 对每个候选序列,尝试所有可能的下一个 token
["我"] + "的" → score = -0.5 + (-0.3) = -0.8
["我"] + "想" → score = -0.5 + (-0.4) = -0.9
["今"] + "天" → score = -1.2 + (-0.2) = -1.4
["今"] + "晚" → score = -1.2 + (-0.5) = -1.7
保留 Top-2:["我的", -0.8], ["我想", -0.9]
时间步 3: ... 继续扩展,始终只保留分数最高的 2 条路径束宽的影响:
| beam_width | 行为特点 | 适用场景 |
|---|---|---|
| 1 | 等价于贪心解码 | 不需要 |
| 3-5 | 在速度与质量间取得平衡 | 翻译、摘要 |
| 10-20 | 更优的全局序列 | 高精度翻译 |
| 50+ | 几乎等于穷举搜索 | 极少使用 |
优点:比贪心解码更可能找到全局更优的序列,在翻译等需要精确输出的任务上效果显著。
缺点:
- 计算量翻倍:beam_width=5 意味着每一步的计算量是贪心的 5 倍
- 开放性生成表现差:束搜索倾向于产生"安全"的文本,缺乏惊喜和创造力。在对话和创意写作中远不如采样策略
2.3 Top-k 采样
Top-k 采样的思路:只从概率最高的 k 个候选 token 中随机采样,切断长尾的低概率 token。
原始概率分布(假设词表大小 50000):
"好的" ████████████████████ 0.40
"可以" ██████████ 0.20
"没问题" ██████ 0.12
"行" ████ 0.08
"嗯" ███ 0.06
"OK" ██ 0.05
"好滴" ██ 0.04
... (49993 个低概率 token) ...
"旮旯" ▏ 0.00001
Top-k (k=6): 只从前 6 个候选里按概率采样
→ 重归一化后:"好的" 44%, "可以" 22%, "没问题" 13%, "行" 9%, "嗯" 7%, "OK" 5%
→ 从这 6 个中随机抽取一个为什么需要 Top-k?
原始词表中大部分 token 在当前语境下完全不合理(比如对话中突然出现"旮旯")。Top-k 剪掉了这些"噪音",让模型在合理的候选集中产生多样性。
注意事项:固定的 k 值对于不同语境不够灵活。一个很确定的上下文(如 "法国的首都是")可能只有 2-3 个合理 token,而一个开放的上下文(如 "我认为")可能有 50+ 个合理选择。固定 k=50 可能在前者引入噪音,在后者限制创造力。
2.4 Top-p(Nucleus Sampling,核采样)
Top-p 采样解决了 Top-k 固定候选数不够灵活的问题:不是固定取 k 个,而是动态选取累计概率达到阈值 p 的最小候选集。
Top-p (p=0.9) 的工作方式:
概率分布(从高到低排序):
"好的" 0.40 ← 纳入,累计 0.40
"可以" 0.25 ← 纳入,累计 0.65
"没问题" 0.12 ← 纳入,累计 0.77
"行" 0.08 ← 纳入,累计 0.85
"嗯" 0.06 ← 纳入,累计 0.91 ← 累计超过 0.9,停止
-------- 阈值 ----------
"OK" 0.04 ← 丢弃
"好滴" 0.03 ← 丢弃
... 其余全部丢弃 ...
在入围的 5 个 token 中按原概率重归一化后采样直观类比
想象你在点外卖:
- Top-k 是说"只看评分前 20 的餐厅";
- Top-p 是说"只考虑能覆盖 90% 好评的餐厅";
后者的候选集大小会根据地区(语境)自动调整——美食街可能有 50 家入围,偏远地区可能只有 3 家。
p 值的调参指南:
| p 值 | 效果 | 适用 |
|---|---|---|
| 0.9-1.0 | 高多样性,更有创意 | 创意写作、头脑风暴 |
| 0.7-0.9 | 平衡 | 通用对话 |
| 0.5-0.7 | 更保守,更确定 | 事实性问答、代码生成 |
现代 LLM 推理中,Top-p + Top-k 经常联用:先用 Top-k 粗筛,再用 Top-p 精筛。
2.5 Temperature(温度系数)
Temperature 不是一种独立的采样策略,而是对概率分布的一个预处理变换。它在 softmax 之前对 logits 进行缩放,公式为:

缩放后的实际效果——以一组具体数值演示:
假设模型对下一个 token 的 Logits 为:
A: 5.0
B: 2.0
C: 1.0
D: 0.0
T = 0.5(低温):
Logits ÷ 0.5 → [10.0, 4.0, 2.0, 0.0]
Softmax → [0.982, 0.018, 0.000, 0.000]
→ A 几乎独占所有概率(98.2%),其他 token 基本没机会被选中
→ 模型极度保守,几乎等价于每次都选 A
T = 1.0(标准温度):
Logits ÷ 1.0 → [5.0, 2.0, 1.0, 0.0]
Softmax → [0.936, 0.047, 0.017, 0.006]
→ A 仍然主导,但 B 有 4.7% 的概率偶尔被选中
→ 保持原始分布的自然比例
T = 2.0(高温):
Logits ÷ 2.0 → [2.5, 1.0, 0.5, 0.0]
Softmax → [0.794, 0.134, 0.073, 0.033]
→ A 的概率从 93.6% 降到 79.4%,B 从 4.7% 升到 13.4%
→ 分布显著平坦化,低分 token 也有可观机会被选中
→ 输出更多样、更有创意(但也更容易跑偏)缩放到底改变了什么?
T < 1 时,Logits 的差异被放大——原本相差 3 分(A:5 vs B:2),除以 0.5 后差 6 分(10 vs 4),Softmax 的指数函数再进一步放大这个差距,最终概率极度集中。
T > 1 时,Logits 的差异被压缩——原本差 3 分,除以 2 后只差 1.5 分(2.5 vs 1.0),高分 token 的优势被削弱,低分 token 浮出水面。
极限情况:
T → 0: 概率分布极端尖锐 → 几乎等价于贪心解码(每次都选最高分)
T = 1: 原始概率分布(不做改变)
T → ∞: 概率分布趋于均匀 → 完全随机生成(每个 token 概率基本相同)不同温度的直观效果:
Prompt: "今天天气真"
T=0 (贪婪): "今天天气真好!阳光明媚,万里无云。" ← 最确定性输出
T=0.3: "今天天气真好!适合出去走走。" ← 比较确定
T=0.7: "今天天气真不错,不如去公园散步吧。" ← 适度多样
T=1.0: "今天天气真适合喝一杯热咖啡然后躺在沙发上看书。" ← 有创意
T=1.5: "今天天气真相是某种不可言说的隐喻。" ← 开始离谱
T=2.0: "今天天气真旮旯三才图会..." ← 完全随机最佳实践:
- 代码生成、数学推理:T=0 或接近 0,需要精确
- 日常对话:T=0.5-0.8,自然且有变化
- 创意写作:T=1.0-1.3,需要想象力
- T>1.5:通常不推荐,输出质量急剧下降
3. Logits / Logprobs
3.1 从 Logits 到概率
理解 logits 和 logprobs 是理解 LLM 内部运作的关键一步。模型最后一层(LM Head)输出的原始数值叫 logits,它们还需要经过一系列变换才能变成概率:
模型的原始输出 经过变换 最终结果
Logits (未归一化分数) → Softmax → 概率分布 → log → Logprobs (对数概率)
[2.3, 1.1, -0.5, ...] ↓ [0.58, 0.17, 0.03, ...]
每个 logit 概率之和 = 1
先做 exp(e^z_i)
再做归一化
具体计算:
z = [2.3, 1.1, -0.5]
e^z = [9.97, 3.00, 0.61]
sum = 9.97 + 3.00 + 0.61 = 13.58
softmax = [9.97/13.58, 3.00/13.58, 0.61/13.58]
= [0.734, 0.221, 0.045]
logprobs = log([0.734, 0.221, 0.045])
= [-0.309, -1.510, -3.101]为什么用对数概率?
- 数值稳定性:概率相乘很容易变成极小的浮点数(下溢),对数把乘法变加法
- 与损失函数一致:训练时用的交叉熵损失本质上就是负对数概率
- 更直观的比较:logprob=-2 比 logprob=-5 的概率大约高 20 倍 (e^3)
3.2 Logprobs 的应用
应用一:置信度评估
通过 token 级别的 logprobs 可以判断模型对自己输出的"把握"有多大:
场景:用户问"法国的首都是什么?"
输出序列及 logprobs:
"巴黎" logprob = -0.1 ← 非常确定
"是" logprob = -0.05 ← 非常确定
→ 模型对此答案非常有信心,可以高置信度使用
输出序列及 logprobs:
"让我想想..." logprob = -2.3 ← 不太确定
"可能是巴黎" logprob = -3.1 ← 很不确定
→ 模型在犹豫,这个答案可能需要人工复核应用二:模型路由选择
在混合专家路由或级联推理架构中,可以用 logprobs 决定是否需要调用更强的模型:
低成本模型 → 生成答案 + logprobs
│
├─ 平均 logprob > -1.0(高置信度)→ 直接返回答案
│
└─ 平均 logprob < -1.0(低置信度)→ 调用更强模型重新生成应用三:幻觉检测
当模型生成某个事实时,如果相关 token 的 logprobs 异常低(模型自己都不确定),可能是幻觉信号。
4. KV Cache
4.1 问题的起源
在自回归生成中,模型每生成一个新 token,都需要重新计算所有历史 token 的注意力。
这带来了巨大的计算浪费:
没有 KV Cache 的推理过程:
Step 1: 输入 ["我", "爱", "你"] → 计算 3 个 token 的 Q/K/V → 输出 token_4
Step 2: 输入 ["我", "爱", "你", token_4] → 重新计算全部 4 个 token 的 Q/K/V → 输出 token_5
↑
"我""爱""你" 的 K/V 已经在 Step 1 算过了!KV Cache 的核心思想:每一层的 Key 和 Value 矩阵一旦算出来就存下来,下次直接用,不再重复计算。
GPU 显存布局(单层 Attention )
┌──────────────────────────────────────┐
│ KV Cache │
│ │
│ token_1: [K₁₁...K₁₆₄][V₁₁...V₁₆₄] │ ← 已缓存,直接读取
│ token_2: [K₂₁...K₂₆₄][V₂₁...V₂₆₄] │ ← 已缓存,直接读取
│ token_3: [K₃₁...K₃₆₄][V₃₁...V₃₆₄] │ ← 已缓存,直接读取
│ token_4: [计算中... ][计算中... ] │ ← 新 token,需要计算
│ │
└──────────────────────────────────────┘
新 token 的 Attention 计算:
Q₄ · [K₁ K₂ K₃ K₄]ᵀ → 直接使用缓存的 K₁~K₃,只算 K₄为什么只缓存 K 和 V 而不缓存 Q?
因为在计算 token N 的注意力时,Q 只需要当前 token 的 Q_N,而 K 和 V 需要所有历史 token 的 K_{1..N} 和 V_{1..N}。
Q 是一次性的,K 和 V 需要反复使用。4.2 显存占用与上下文长度的关系
显存(Video RAM / VRAM):GPU 上专用于存储计算数据和中间结果的高速内存。
不同于电脑的系统内存(RAM),显存直接集成在显卡上,与 GPU 计算核心之间的数据传输带宽极高(通常 1-2 TB/s)。推理显存则特指模型在生成 token 过程中占用的显存,主要包括三大块:模型权重本身、KV Cache、以及推理过程中的临时激活值。其中 KV Cache 随上下文长度线性增长,通常是长文本场景下的最大瓶颈。
KV Cache 是推理显存的最大开销来源之一。以一个典型的 7B 模型为例:
模型:7B 参数,32 层,32 个注意力头,每个头维度 128(dim_head=128)
→ 每层 K 的维度 = 32 × 128 = 4096
→ 每层 V 的维度 = 32 × 128 = 4096
→ 每层 KV = 2 × 4096 = 8192 个元素
→ 每个元素在 FP16 下占 2 字节
单个 token 的 KV Cache 大小:
32 层 × 8192 元素 × 2 字节 = 524,288 字节 = 0.5 MB
上下文长度 → KV Cache 总大小:
1K tokens = 0.5 GB
4K tokens = 2 GB
32K tokens = 16 GB
128K tokens = 64 GB ← 已经超过绝大多数 GPU 的显存!这就是为什么长上下文推理如此昂贵——KV Cache 随上下文线性增长。在实际部署中,KV Cache 常常比模型权重本身占用更多显存。
4.3 PagedAttention(vLLM 核心技术)
传统的 KV Cache 管理方式是为每个请求预分配一整块连续显存,就像给每个客人一张固定大小的餐桌,无论他吃多少:
传统 KV Cache 管理:
请求 A: 预分配 4K token 空间 [████████░░░░░░░░░░░░] ← 大量碎片
请求 B: 预分配 4K token 空间 [████████████████░░░░] ← 浪费
请求 C: 预分配 4K token 空间 [██░░░░░░░░░░░░░░░░░░] ← 严重浪费
总分配 12GB,实际使用不到 6GB → 显存利用率 ~50%
碎片化严重,难以支持大批量请求PagedAttention 借鉴了操作系统中虚拟内存分页的思想——把 KV Cache 切分成固定大小的块(block/page),按需分配:
PagedAttention 的 KV Cache 管理:
物理块池(共 8 个 block):
Block 0 [████] ← 请求 A 占用
Block 1 [████] ← 请求 A 占用
Block 2 [████] ← 请求 B 占用
Block 3 [████] ← 请求 B 占用
Block 4 [████] ← 请求 C 占用
Block 5 [ 空 ] ← 空闲,任何请求需要时可分配
Block 6 [████] ← 请求 A 占用(A 的 block 不连续也没关系!)
Block 7 [████] ← 请求 B 占用
逻辑到物理映射:
请求 A 的逻辑块 [0,1,2] → 物理块 [0,1,6] ← 通过页表映射
请求 B 的逻辑块 [0,1,2] → 物理块 [2,3,7]
请求 C 的逻辑块 [0] → 物理块 [4]
显存利用率:从 ~50% 提升到接近 100%PagedAttention 的三大优势:
- 零浪费:按需分配,不会预留未使用的空间
- 灵活共享:多个请求可以共享同一物理块(如相同的 system prompt)
- 更高吞吐:同样的显存可以服务 2-4 倍的并发请求
这正是 vLLM 能够实现高吞吐推理的核心原因。
5. Streaming(流式输出)
流式输出是指模型生成一个 token 就立即返回一个 token,而不是等全部生成完毕再一次性返回:
非流式 vs 流式:
非流式(Batch):
用户发送请求 ────────────────────────────────→ 等待 5 秒 ──→ 一次性收到全部回复
模型内部生成 token1, token2, ..., token200
流式(Streaming):
用户发送请求 → 等 0.5s → "我" → "很" → "高" → "兴" → ... → "!"
↑ 每个 token 逐个到达,用户可以边看边等
TTFT ↓流式输出的意义:
| 维度 | 非流式 | 流式 |
|---|---|---|
| 用户感知延迟 | 高(等到全部完成) | 低(TTFT 后才开始有文字) |
| 可中断性 | 差 | 好(看到不对劲可以提前停止) |
| 交互体验 | 像等一封邮件 | 像在和人聊天 |
| 实现复杂度 | 低 | 需要 SSE/WebSocket |
OpenAI 的 API 使用 Server-Sent Events(SSE) 实现流式输出,每个事件包含一个 delta(增量 token)。
6. Speculative Decoding(推测解码)
推测解码是一种"用速度换正确性"的推理加速技术。核心思想很简单:让小模型(草稿模型)快速生成多个候选 token,大模型(目标模型)一次性验证这些 token 是否正确。
推测解码流水线:
Draft Model(草稿模型,如 0.5B 小模型)
│ 速度极快,但准确率不如大模型
│
▼
推测生成: ["我", "认为", "这", "是", "一", "个", "好", "主意"]
│
▼
Target Model(目标模型,如 70B 大模型)
│ 一次前向传播,同时验证全部 8 个 token
│
▼
┌─────────────────────────────────────┐
│ Token "我" "认为" "这" "是" "一" "个" "好" "主意" │
│ 小模型 ✓ ✓ ✓ ✓ ✓ ✓ ✗ ✗ │
│ 大模型 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ? │
│ │
│ 小模型在"个"之后预测"好",大模型认为应该是"非常好" │
│ 接受前 6 个 token (✓),拒绝第 7 个 (✗) │
└─────────────────────────────────────┘
│
▼
一次大模型前向传播 → 确认了 6 个 token,相当于吞吐量提升 6 倍关键前提——为什么这能加速?
大模型一次处理 8 个 token 的时间 ≈ 处理 1 个 token 的时间。这得益于 Transformer 的并行特性:attention 计算中,所有 token 的关注度矩阵是并行计算的。所以一次验证 N 个 token 和一次生成 1 个 token 的时间几乎相同。
适用条件:
- 草稿模型需要和目标模型使用相同的 tokenizer
- 草稿模型需要足够小(通常为目标模型的 1/10 到 1/100 参数量)
- 草稿模型的预测准确率不能太低,否则大量 token 被拒绝,加速效果有限
实际效果:在代码生成、翻译等"确定性较强"的任务上,推测解码可以实现 2-3 倍的推理加速,且生成的文本与原始大模型完全一致(因为最终由大模型验证)。
7. 推理性能指标
理解推理性能指标,才能科学地评估和优化推理系统。
7.1 TTFT(Time to First Token)
从用户发送请求到收到第一个 token 的时间间隔:
TTFT 的组成:
用户点击发送 → 网络传输 → 请求排队 → Prefill(预填充)→ 网络返回第一个 token
│ │
└──────────────────────── TTFT 通常 200ms ~ 2s ─────────────────────────────┘
Prefill 阶段:把所有输入 token 一次性送进模型,计算并填充 KV Cache
→ 这是 TTFT 中最耗时的部分
→ 输入越长,Prefill 越慢TTFT 为什么重要?
这是用户感知中的"响应速度"。就像点外卖时从下单到第一个骑手接单的时间——TTFT 越小,用户越不焦虑。通常目标:
- 好的体验:TTFT < 500ms
- 可接受:500ms ~ 2s
- 需要优化:> 2s
7.2 TPOT(Time per Output Token)
在生成阶段,每输出一个 token 的平均时间。
TPOT 决定了输出速度:
TPOT 50ms → 每秒 20 tokens → 就像普通人说话的速度
TPOT 20ms → 每秒 50 tokens → 快速阅读的速度
TPOT 10ms → 每秒 100 tokens → 极速输出
TPOT = (总生成时间) / (生成的 token 数)TPOT 受 KV Cache 大小影响:上下文越长,每次 Attention 计算的开销越大,TPOT 越高。
7.3 Throughput(吞吐量)与 QPS
吞吐量是系统整体每秒可以处理多少请求或生成多少 token。在生产环境中,吞吐量通常用 QPS(Queries Per Second,每秒查询数) 来衡量,两者密切相关但视角不同:
| Throughput(吞吐量) | QPS(每秒查询数) | |
|---|---|---|
| 定义 | 每秒处理的 token 总量 | 每秒完成的请求数量 |
| 单位 | tokens/s | 请求/s |
| 关注点 | 系统算力利用效率 | 用户感知的服务能力 |
| 关系 | 吞吐量 tokens/s = QPS × 平均每个请求的 token 数 | QPS = 吞吐量 / 平均每个请求的 token 数 |
计算示例:
场景 A:短回答(平均每个请求 20 token)
系统吞吐量 = 1000 tokens/s
→ QPS = 1000 / 20 = 50 请求/s
→ 服务能力:每秒 50 个用户
场景 B:长回答(平均每个请求 200 token)
系统吞吐量 = 1000 tokens/s
→ QPS = 1000 / 200 = 5 请求/s
→ 服务能力:每秒仅 5 个用户关键洞察
同样的吞吐量,长回答场景下的 QPS 会急剧下降。
这就是为什么评估 LLM 服务性能时,不能只看 QPS——一个系统可能在简单问候场景下 QPS 很高,但在生成长文档时 QPS 骤降。
吞吐量(tokens/s)才是更稳定的衡量标准。
吞吐量 = 并发请求数 / 平均每请求完成时间
或:吞吐量 = 每秒处理的总 token 数(含输入和输出)
高吞吐示例:
10 个请求并发,每个请求生成 100 token
总时间 5 秒
→ 吞吐量 = 10 × 100 / 5 = 200 tokens/sTTFT、TPOT、Throughput 三者之间的关系:
TTFT ↓ TPOT ↓
↓ ↓
用户请求 → [Prefill] → [Generate: token₁ → token₂ → ... → token_N] → 完成
↑ ↑
受输入长度 受 KV Cache 大小
和模型大小影响 和并发请求数影响
吞吐量 = f(模型并行策略, 批处理大小, 显存带宽, GPU 数量)优化优先级因场景而异:
- 聊天机器人:TTFT 最关键(用户等待首个字的耐心有限)
- 批量文本处理:吞吐量最关键(不关心单次延迟,关心整体效率)
- 实时翻译:TPOT 最关键(不能出现断续感)
| 指标 | 定义 | 优化方向 | 典型目标 |
|---|---|---|---|
| TTFT | 首 token 延迟 | 减小 Prefill 时间、提升 GPU 利用率 | < 500ms |
| TPOT | 每 token 生成时间 | 减小 KV Cache 计算开销 | < 50ms |
| Throughput | 系统整体吞吐 | Continuous Batching、PagedAttention | 依赖场景 |