预计阅读 7 分钟

采样选项:top-k / top-p / temperature / logprobs / penalties

谁该读这一篇? 想用好采样参数、不想瞎设的工程师;做 RL 训练 / Best-of-N 的研究员。 前置阅读: 00-prerequisites.md耗时: 30 分钟 学完能: 1. 用数学和直觉解释 temperature / top-k / top-p; 2. 区分 frequency / presence / repetition penalty; 3. 输出 logprobs 并解读; 4. 选对参数给定具体业务(chat / 代码 / RAG / 翻译); 5. 排查"输出突然乱码 / 重复"等采样相关 bug。


1. Logits → Token:完整流程

Hidden state ─▶ LM head ─▶ logits [vocab_size]
                              │
                              ▼
                       penalties (freq/pres/rep)
                              │
                              ▼
                       temperature scaling
                              │
                              ▼
                       top-k filter
                              │
                              ▼
                       top-p filter
                              │
                              ▼
                       constrained mask (regex/schema)
                              │
                              ▼
                       softmax
                              │
                              ▼
                       multinomial sample → token

每个滤波器在不同程度上"裁剪"候选 token 分布。

源码:srt/sampling/srt/layers/sampler.py


2. Temperature

$$p_i = \frac{e^{l_i / T}}{\sum_j e^{l_j / T}}$$
  • $T = 1$:原始分布。
  • $T \to 0$:变 argmax(greedy)。
  • $T \to \infty$:变均匀分布。

两个方向上的极端:T 越小越确定、越机械;T 越大越随机、越发散。

业务推荐:

业务 T
抽取 / 分类 / 数学 0.0 (greedy)
RAG / QA 0.1-0.3
Chat 0.7-1.0
创意写作 0.8-1.2
多样性采样(Best-of-N) 0.8-1.0

3. Top-k

只在 logits 最大的 k 个 token 里采样,其他全置 -inf。

top_k_mask = logits < kth_largest(logits, k)
logits[top_k_mask] = -inf
  • k=1:等价 greedy。
  • k=50:常用 baseline。
  • k=vocab_size:等价不裁剪。

适合:

  • 词表大(130k+)时防止低概率噪音。

4. Top-p (Nucleus)

按概率累积,取最少集合 S 使 $\sum_{i \in S} p_i \geq p$

sorted_idx = argsort(-probs)
cumsum = cumsum(probs[sorted_idx])
S = sorted_idx[cumsum < p]    # 加 1 让恰好超过
mask everything outside S
  • p=0.9:常用。
  • p=0.95:保留更多多样性。
  • p=0.5:保守。

top-p 比 top-k 更适应分布的尖锐度(尖时少 token,平时多 token)。

通常 temperature + top-p 组合,少用 top-k。


5. Min-p

$$\text{threshold} = p_{max} \times \text{min\_p}$$

只保留概率 ≥ threshold 的 token。

例:

  • min_p=0.1, p_max=0.5:阈值 0.05,保留概率 ≥ 0.05 的 token。
  • min_p=0.1, p_max=0.05:阈值 0.005,保留更多。

适应性强、参数少,渐成 chat 推荐选择。


6. Repetition / Frequency / Presence Penalties

OpenAI 协议三种 penalty:

6.1 Repetition penalty (HF 风格)

logits[token] /= rep_penalty       # 已出现过的 token

rep_penalty=1.1 是常用。 问题:对所有出现过 token 一视同仁,没区分次数。

6.2 Frequency penalty (OpenAI 风格)

logits[token] -= freq_penalty * count[token]

次数越多惩罚越大。freq_penalty=0.5 常用。

6.3 Presence penalty (OpenAI 风格)

logits[token] -= pres_penalty if token in seen else 0

只要出现过就扣一次。

三种各有适用:

  • Chat 防重复:presence_penalty=0.5
  • 长文创作:frequency_penalty=0.3
  • 代码生成:通常关掉所有 penalty(代码有合理重复)

7. DRY (Don't Repeat Yourself)

更新版的 repetition 抑制:

检测最近 N 个 token 里的 n-gram 重复
对重复 n-gram 的下一 token 大幅降低 logit

适合:长生成中防止"循环"。 SGLang 通过 dry_* 系列参数配。


8. Logprobs 输出

resp = client.chat.completions.create(
    model="...",
    messages=[...],
    logprobs=True,
    top_logprobs=5,
)

输出里每个 token 带:

  • logprob:选中 token 的对数概率。
  • top_logprobs:top-5 候选的 logprob。

用途:

  • Self-eval(模型对答案的"信心")。
  • 检测幻觉(低信心 token 多 → 可能瞎说)。
  • RAG 验证(看是否复述了 context)。

代价:每步多算一些,TPOT 增加 ~5%。


9. Best-of-N(拒绝采样)

# 客户端层:跑 N 次,选 logprob 最高的
candidates = []
for _ in range(N):
    r = client.chat.completions.create(..., temperature=1.0, logprobs=True)
    score = sum(t.logprob for t in r.choices[0].logprobs.content)
    candidates.append((r.choices[0].message.content, score))
best = max(candidates, key=lambda x: x[1])

适合:答案空间小、单次置信度低(数学、规划)。

DSL 等价物(07-hands-on/04-custom-frontend.md §4)用 fork 并 select。


10. 采样参数 cheatsheet

场景 T top_p top_k penalties 备注
分类 / 抽取 0 1 - - greedy
Chat 0.7 0.9 - pres=0.5 balance
代码 0.2 0.95 - 代码不要乱
翻译 0.3 0.9 - - 准确
创意 1.0 0.95 - freq=0.3 多样性
数学 / 规划 0.0 + best-of-5 - - - 多次投票
RAG 0.3 0.9 - - 准确性

11. 常见 bug

现象 排查
输出短得反常 检查 EOS 是否被 mask(受约束输出常见)
输出循环(同一短语反复) rep / dry penalty 没设
答案全选 A(多选题) choice 之间 logit 不均;用 select 而非 gen
Logprobs 全 0 触发了 greedy(T=0),logprob 都是 0(最大概率)
不同 batch 同 seed 不同结果 数值不可重现;某些 attention backend 有这问题

12. 数值确定性

seed=42 + temperature=0

  • 理论上完全确定。
  • 实际:不同 batch_size 下 attention kernel 走不同路径,浮点累加顺序不同,可能差 1 个 token。

SGLang 有 batch_invariant_opssrt/batch_invariant_ops/)让"同输入不同 batch 输出一致",开启后 throughput 略低。


13. 源码

内容 文件
Sampler srt/layers/sampler.py
SamplingParams srt/sampling/sampling_params.py
BatchInvariant srt/batch_invariant_ops/
Logits processor srt/layers/logits_processor.py

14. 小结

  • 采样流程:penalties → temperature → top-k → top-p → constrained → softmax → sample。
  • 不同业务采样参数差异大;查 cheatsheet。
  • Logprobs 可用于自信度评估、幻觉检测。
  • Best-of-N 数学题神器。
  • 数值确定性靠 batch_invariant_ops,代价是性能。

15. 自检

  1. T=0.1 和 T=1.0 在 top_p=0.9 下输出差异主要在哪?
答案 T 缩放 logits 后再 top_p 过滤。 T=0.1:logits 被放大 10×,**概率分布极尖锐**,top_p=0.9 实际只保留 1-2 个 token,输出近似 greedy。同问题反复跑结果几乎相同。 T=1.0:logits 不变,分布平坦,top_p=0.9 保留更多候选 token(可能 5-20 个),输出多样性高。同问题反复跑结果不同。 实际差异:T=0.1 时输出"理性"、保守、可能机械;T=1.0 时输出"有想法"、有惊喜也有错误。 选择:分类 / 抽取 T=0;chat T=0.7;创意写作 T=1.0。
  1. Frequency / presence / repetition penalty 各自的差异?
答案 三种"防重复"机制,公式不同: - **Repetition penalty**(HF 风格):`logits[token] /= rep_penalty if token in seen`。对**所有**出现过的 token 一视同仁惩罚(不区分次数);对代码 / 重复结构不友好。 - **Frequency penalty**(OpenAI):`logits[token] -= freq * count[token]`。**次数越多惩罚越大**;适合长生成防车轱辘话。 - **Presence penalty**(OpenAI):`logits[token] -= pres if token in seen`。出现过就扣固定一次;轻微鼓励多样性。 实战: - Chat:`presence=0.5`。 - 创意:`frequency=0.3`。 - 代码:全关(代码有合理重复)。 - 数学:全关(数字会复用)。 叠加用一种就行,不要三个全开。
  1. Logprobs 怎么用来判断幻觉?
答案 原理:模型对自己"瞎说"的 token 通常 logprob 低(自信度差)。 实战: (1) **平均 logprob**:`mean(logprobs)` 越低越不可信;阈值如 -2.0(每 token 概率 ~13%)。 (2) **最小 logprob**:找最不自信的 token,看是否在关键事实位置(人名、数字、日期)。 (3) **熵估计**:用 `top_logprobs=5` 看候选分布,熵高 = 模型在多个选项里犹豫。 (4) **数字字段 vs 文字**:JSON Schema 输出时数字字段 logprob 低 = 模型不确定具体数值。 局限:模型可能"自信地编"(high logprob 但内容假);logprobs 只是辅助信号,不是绝对判断。 配合 RAG 引用 / fact-check 工具用最稳。
  1. 同一 seed 不同 batch 结果不同怎么解?
答案 原因:attention kernel 在不同 batch size 下走不同代码路径(kernel autotune、tile 大小、tensor core 选择),浮点累加顺序不同,每层引入微小 numerical noise,几层后被放大成 logits 差异,sampler 在 boundary 处选不同 token。 解决: (1) **`--enable-batch-invariant-ops`** ([`srt/batch_invariant_ops/`](../sglang/python/sglang/srt/batch_invariant_ops/)):强制 attention / matmul 走 batch-invariant 路径,相同输入不同 batch 数值完全一致。代价:throughput 降 5-15%。 (2) **更高精度**:FP32 算 attention 噪声小,但成本高。 (3) **业务侧容忍**:很多场景"近似一致"足够(chat 不需要 byte-exact),不强求。 什么时候必须 deterministic:RL 训练、回归测试、合规审计。生产服务通常不需要。
  1. 翻译任务为什么 T 不能太高?
答案 翻译要求**准确 + 一致**: - 同源句应该有稳定译文,T 高会让相同输入每次产出不同译文,破坏一致性。 - 准确性要求选"最可能的"译法,T 低让模型选 argmax。 - 高 T 引入低概率 token,可能选择稀有词汇 / 错误同义词。 实战:T=0.2-0.3 是 sweet spot;T=0 偶尔有机械感(同句固定译,缺少自然变化),加一点点温度。 对比创意 / 自由生成:T=1.0 才能"有想法";翻译需要的是"准确"不是"想法"。 配合 `top_p=0.9` + `frequency_penalty=0` 即可。

16. 下一步

上游源码:sglang/python/sglang/srt/sampling/