Skip to content

推理与解码策略

摘要: 大语言模型在生成文本时,每一步都是从整个词表中选出下一个最合适的 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 进行缩放,公式为:

figure

缩放后的实际效果——以一组具体数值演示

假设模型对下一个 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]

为什么用对数概率?

  1. 数值稳定性:概率相乘很容易变成极小的浮点数(下溢),对数把乘法变加法
  2. 与损失函数一致:训练时用的交叉熵损失本质上就是负对数概率
  3. 更直观的比较: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 的三大优势

  1. 零浪费:按需分配,不会预留未使用的空间
  2. 灵活共享:多个请求可以共享同一物理块(如相同的 system prompt)
  3. 更高吞吐:同样的显存可以服务 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 的时间几乎相同

适用条件

  1. 草稿模型需要和目标模型使用相同的 tokenizer
  2. 草稿模型需要足够小(通常为目标模型的 1/10 到 1/100 参数量)
  3. 草稿模型的预测准确率不能太低,否则大量 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/s

TTFT、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依赖场景