温度、top-k、top-p、min-p 到底在改什么:把 LLM 采样从 softmax 到截断重归一化一步步讲清楚
很多人把「调温度」当成「调随机性」的旋钮,把 top-k / top-p / min-p 当成同类的「多还是少」旋钮,结果在 T=1.2 上配 top_p=0.95,又奇怪为什么输出忽而呆板忽而胡说。本文把这几个参数拆成两件事:温度只改分布的形状(把 logit 差值除以 T,等价于换一把量尺),而 top-k / top-p / min-p 只决定从形状里砍掉哪些候选,砍完还必须重新归一化。关键结论是三者与温度的关系完全不同:top-k 看名次、单独施加时对温度免疫(保留集合不随正温度变化,集合内的质量分布仍会变);top-p 看累积质量、与温度强耦合(本文构造的六候选例子、单独施加 top-p:同一个 top_p=0.9 在 T=0.3 时只留 2 个候选、在 T=3.0 时要留 5 个;这组 logits 是本文编的,非任何模型实测);min-p 看每个候选与最高分的相对差距(阈值 $$\alpha \cdot p_{\max}$$ 是它的等价写法,$$p_{\max}$$ 只是缩放代理)。文中参数默认值分别取自 vLLM 的 `SamplingParams` 与 Hugging Face 的 `GenerationConfig` 源码(两边默认值并不相同,正文有对照表),实现细节取自两个框架的源码,六候选数值例子为本文自行推导(logits 系本文构造,非任何模型实测),三类数字分别标注。
想象一间翻译工作室。桌上的模型刚刚读完半句话,它在心里排出了下一批候选词:某个词很合适,某个词勉强能用,还有一大串「语法上说得通、但语义上已经跑偏」的词排在后面。模型给这些候选各打了一个原始分数,然后在计算上要发生三件事:先把这批分数的差距按温度缩放(可以拉大,也可以压小),再决定哪些候选直接扔进纸篓,最后在活下来的候选里抽一个。
这三件事听起来是同一件事的不同说法,实际上是三个独立旋钮,各自改的是不同的东西:第一个改的是分布的形状,第二个改的是候选的集合,第三件事是抽签本身(它不对应任何参数)。工程师们最常犯的错,是把它们当成了同一根旋钮的不同刻度,于是造成「我明明把温度调低了,为什么还是乱说」这类困惑。
这篇文章只讲清一件事:当你写下 temperature / top_k / top_p / min_p 这四个参数时,模型内部到底对那批候选分数做了什么。 我们会从一个能精确推导的二选一例子出发,把温度对熵的影响推出来;再用一个六候选的小例子,把三类截断在同一个温度扫描下的行为摊在一张表上;最后把常见误解逐条拆开。全部实现细节以 Hugging Face Transformers 与 vLLM 的源码为准,版本锚定在 2026-10-15 读取的 main 分支(transformers f0705f9、vLLM 273a049;文末来源段给了具体文件与位置),数值例子则在文中明确标注为「本文自行推导」。
📐 符号与术语表
先把记号固定下来,后面所有推导只用这套记号。
| 符号 | 含义 |
|---|---|
| 模型对候选词 输出的 logit(未归一化的原始分数,可正可负,无量纲) | |
| 词表大小,即候选词总数 | |
| temperature,温度,严格正数 | |
| softmax 之后候选词 的概率, | |
| 该步里最大的那个概率,即 | |
| 分布 的熵,,单位 nats(用 则是 bits);直觉是「这个分布有多不确定」 | |
| 截断后保留下来的候选集合(survivors) | |
| 保留下来的总质量, | |
| 重归一化后的概率,(只对 定义) | |
| 两个 logit 的差, |
两个术语先说清:softmax 指把一串实数映射成概率分布的标准运算(不带温度时 ,带温度的版本见第一节);概率质量指一组概率之和(全部候选的质量合计为 1)。
需要先分清三层参数(本文重点讨论前两层),这是全文的骨架:
- 改分布的参数:temperature。它作用于 softmax 内部,改变概率形状。
- 改集合的参数:top_k、top_p、min_p(以及典型的 top_h、typical_p、epsilon、eta)。它们作用于已经算出的概率,删掉一部分候选,然后必须重新归一化。
- 直接改原始分数的参数:repetition_penalty、presence_penalty、frequency_penalty。它们改的是 softmax 之前的那批分数,改完之后形状与名次都会跟着变,所以既不属于「改形状」也不属于「改集合」,与前两类不是一回事(第八节展开)。
这一节想让你记住的一件事:温度改形状、截断改集合、惩罚改原始分数——三类参数的作用位置不同,所以不能互相「抵消」。
🎚️ 一、温度:它换的不是随机性,是量 logit 差值的那把尺子
模型最后一步只做一件事:把一批实数 变成一批概率。带温度的 softmax 写法是
看这个式子,最容易被忽略的一点是:温度只是把每个 logit 除以同一个数。 它不是随机数发生器上的一颗「更随机 / 更保守」的旋钮,它就是一把尺子——把 logit 差值放大或缩小之后,再交给 softmax 去指数化。
两个候选之间的概率比可以精确写出来:
这条式子把温度的全部作用暴露了:温度不改变次序,只改变「差距有多大」。 因为 是单调递增函数, 时无论 取多少都有 ——名次永远不变。变的是差距: 时 被放大,指数把差距拉得更开; 时被压缩,差距变小,分布变平。
这就是「温度 = 逆温度的倒数」这个说法的来处。在统计物理里,玻尔兹曼分布的写法是概率正比于 ;把 logit 当作(负的)能量、把 当作一把能量尺,就得到上面的式子。工程师常说的 (逆温度)就是同一个换尺动作的另一种写法。这个类比的实用价值在于:它告诉你温度不该被理解成「随机性大小」,而该被理解成「这批分数差几个单位才算差得多」。
生活化的说法:同样一组身高差,用「米」量显得很小,用「厘米」量显得很大,但人的高矮顺序不会变。温度就是切换这把单位尺( 相当于换更小的单位、把差距放大; 相反),softmax 是指数化的那一步——而且换尺之后读出来的概率值是真的变了,不只是读数变了,因为指数函数对刻度的变化极其敏感。
vLLM 的采样参数源码里,temperature 的默认值是 1.0,文档串写的正是「Controls the randomness of the sampling. Lower values make the model more deterministic, while higher values make the model more random. Zero means greedy sampling.」。Hugging Face 的 TemperatureLogitsWarper 文档串给了同一件事的另一半:「A value smaller than 1 decreases randomness (and vice versa), with 0 being equivalent to shifting all probability mass to the most likely token.」但这两处并不等价:vLLM 的 0 是一个被实现接受的约定(真正落进贪婪分支的条件是温度低于 _SAMPLING_EPS = 1e-5;而 0 < T < 1e-2 会被源码上抬到 _MAX_TEMP = 1e-2 并打一条告警,所以「极小但非零」的温度并不等于贪婪),而 Hugging Face 那句是与它自己的实现相反的描述——TemperatureLogitsWarper 要求 temperature 是严格正的 float,传 0.0 会直接抛 ValueError,并在报错信息里提示「如果你要贪婪解码,请设 do_sample=False」。这个细节第九节还要专门讲一次。
这一节想让你记住的一件事:温度是 logit 差值的缩放因子,它只改差距、不改名次。
🧮 二、温度对熵做了什么:一个可以精确算出来的情形
「调低温度输出更保守」这句直觉判断,能不能算出来?在只有两个候选的情形下可以完整推出来,而且结论很有用。
设两个候选的 logit 差为 (取 ),那么第一个候选的概率是
其中 是 logistic 函数。而两个候选的熵只依赖 :
现在把 对温度求导。先看熵对概率的导数:(最后一步用了 ,即 )。再看概率对温度的导数:。两者相乘:
这是个干净的结果,而且它说了三件事:
- 熵随温度单调递增: 恒成立,所以「调低温度输出更确定」在二选一情形下是可证的,不是经验之谈。
- 敏感度在两端都趋近于零,而不是「越低温越敏感」:要看清这一点,必须把 也看成温度的函数,不能只盯着 。固定 时, 会让分布饱和(), 则让 衰减——导数在两端都趋于 0,峰值出现在 (复算 3.2436,对应 ,注意不是 处)。举个本文自行推导的数值例子: 时,从 降到 0.3 引起的熵变化是 0.000476,而从 1.4 降到 1.3 是 0.030357,后者反而大 60 多倍——所以「低温区差一点就差很多」这句体感必须加条件:它只在分布还没饱和时成立。
- 这一项不是全部: 确实在 时最大,但整体敏感度还要乘 ,所以最大值并不落在 处(那里已经接近「谁都不占优」, 已经很小)——上面的复算给出的峰值在 、对应 。两端趋零的正确读法是:温度的边际影响在极端饱和与极端平坦两头都变小,在中间区间最大,而不是「温度只在某一小段区间里才起作用」。
多候选乃至整张词表的情形有同样的结构,而且这条恒等式是精确的:固定 logits 时,熵对温度的导数满足 ( 是在分布 下取的方差,本文复算确认它对任意有限 logits 成立)。它同时解释了上面的峰值:温度升高让分布更平、方差变大,但 衰减得更快,所以敏感度在高温端回落。数值例子仍用二选一的精确结果,因为那里能一条条算给人看。
对整张词表,这条恒等式同样精确成立,只是 由整个分布决定——所以判断敏感度不能只盯头两名。下面是本文构造的一组六候选 logits(the=2.0、a=1.6、car=1.2、bicycle=0.6、hovercraft=0.1、kubernetes=-1.4,这六个数就是本文编出来的一组原始分数),按上面公式逐个温度算出的表。这些数字是本文自行推导的,logits 是本文为了演示编的,不对应任何真实模型的实测输出,请只用来理解参数之间的关系。
| T | 熵(nats) | top_p=0.9 存活数 | top_k=3 覆盖质量 | min_p=0.1 存活数 | top_p=0.95 存活数 | |
|---|---|---|---|---|---|---|
| 0.3 | 0.7363 | 0.7439 | 2 | 0.9917 | 2 | 3 |
| 0.5 | 1.0960 | 0.5762 | 3 | 0.9514 | 3 | 3 |
| 0.7 | 1.3002 | 0.4778 | 4 | 0.9000 | 4 | 4 |
| 1.0 | 1.4734 | 0.3923 | 4 | 0.8315 | 5 | 5 |
| 1.5 | 1.6128 | 0.3194 | 5 | 0.7513 | 6 | 5 |
| 2.0 | 1.6783 | 0.2813 | 5 | 0.7001 | 6 | 6 |
| 3.0 | 1.7351 | 0.2427 | 5 | 0.6409 | 6 | 6 |
这张表的列依次是:温度 T、熵、最大概率 、top_p=0.9 下的存活候选数、top_k=3 覆盖的质量、min_p=0.1 下的存活候选数、top_p=0.95 下的存活候选数。请盯住其中两列:「top_p=0.9 存活数」从 2 变到 5,而 top_k=3 那一列给的是它覆盖的质量、集合大小按定义也是 3(并列或保底时可能更多,见第四节)。
这张表是全文后面所有讨论的地基。另外说明一处四舍五入:T=0.7 行的 0.9000 实际是 0.899971,严格小于 0.9,所以 top_p=0.9 仍需要第 4 个候选。同一批 logits、同一个 0.9,温度从 0.3 换到 3.0,保留的候选数翻了一倍还多;而 top_k=3 保留下来的永远是那三个候选,变的只是这三个候选加起来的质量从 99.17% 掉到 64.09%。这就是「top-k 看名次、top-p 看质量」在数字上的样子。
顺带把 T=1.0 那一列的完整概率列出来,后面要反复用到:
| 候选 | 概率 |
|---|---|
| the | 0.392282 |
| a | 0.262954 |
| car | 0.176264 |
| bicycle | 0.096736 |
| hovercraft | 0.058673 |
| kubernetes | 0.013092 |
这一节想让你记住的一件事:二候选情形下温度对熵的影响是 (整词表是 )——敏感度在两端趋于 0,峰值落在 (对应 ),而不是 处。
🔎 把「差距」量化出来:第一名到底领先多少
「温度只改差距、不改名次」这句话还可以再精确一点,用一个比值衡量:第 1 名与第 2 名的概率比。由前面的比值公式直接得到
本文构造的那组 logits 里,第一名与第二名的差距是 ,于是三个温度下的领先倍数是(本文自行推导):
| T | 读法 | |
|---|---|---|
| 0.3 | 3.7937 | 第一名领先近 4 倍,在这一步里它独占 74.39% 的概率 |
| 1.0 | 1.4918 | 领先约 1.5 倍,优势明显但不再是压倒性的 |
| 3.0 | 1.1426 | 只领先 14%,两个词几乎等概率 |
这张小表把「温度调高会变随机」翻译成了一个具体数字:温度从 0.3 升到 3.0,领先倍数掉到原来的三成左右。而顺序从头到尾没变——the 在三个温度下都是第一名,kubernetes 在三个温度下都是最后一名。所以严格的说法不是「温度让模型选别的词」,而是「温度让模型更敢偏离第一名」。
flowchart TD
A["同一组 logits<br/>the=2.0 a=1.6 car=1.2 ..."] --> B["T=0.3"]
A --> C["T=1.0"]
A --> D["T=3.0"]
B --> B1["p_max=0.7439<br/>第一名领先 3.79 倍<br/>top_p=0.9 只留 2 个候选"]
C --> C1["p_max=0.3923<br/>第一名领先 1.49 倍<br/>top_p=0.9 留 4 个候选"]
D --> D1["p_max=0.2427<br/>第一名领先 1.14 倍<br/>top_p=0.9 要留 5 个候选"]这张图说明了什么:温度一边改概率形状(领先倍数),一边顺带改掉截断参数的效果(存活候选数),但「谁排第一」这一层始终不动。这也说明为什么「温度」和「截断」必须放在一起看:两者作用在不同层,却会在同一层(最终输出)留下痕迹。
这一节想让你记住的一件事:头两名之间的领先倍数 是衡量「模型有多敢偏离自己的第一选择」的一个指标(它不描述第三名与长尾怎么分配,那是熵与截断的事)。
🌡️ 三、从 logits 到输出:完整流水线(图 1)
把上面两类参数放回执行顺序里,采样这一步其实是一条固定的流水线:
flowchart LR
A["模型输出<br/>logits z"] --> B["惩罚类处理器<br/>repetition / presence /<br/>frequency penalty"]
B --> C["温度缩放<br/>z 除以 T"]
C --> D["截断<br/>top_k / top_p / min_p"]
D --> E["重新归一化<br/>q = p / m"]
E --> F["采样<br/>按 q 抽一个 token"]这张图说明了什么:四个参数作用在不同位置、有先后顺序。 惩罚类在最前面(改原始分数),温度在中间(改指数化时的尺度),截断和重归一化在最后(改集合)。位置不同意味着:调 temperature 不会改变 top_k 保留的是哪几个词(因为名次不变),但会改变 top_p 和 min_p 保留的是哪几个词(因为质量分布变了)。顺序不是实现细节,而是参数语义的一部分。
Hugging Face 的当前实现(截至 2026-10-15,main 分支 commit f0705f9)把这条流水线里的每一步都做成独立的处理器,并按固定顺序装配。这段逻辑现在位于 _get_logits_processor——旧版本里的 _get_logits_warper 这个名字在当前 utils.py 里已经不存在(warper 装配已并入前者,源码注释写的是「Processors previously known as LogitsWarpers, only applied with sampling strategies」);processors.append(...) 的顺序是:TemperatureLogitsWarper → TopHLogitsWarper → TopKLogitsWarper → TopPLogitsWarper → MinPLogitsWarper → TypicalLogitsWarper → EpsilonLogitsWarper → EtaLogitsWarper。也就是说,温度先于所有截断类参数被施加,所以后面那些截断看到的概率,已经是温度缩放后的概率。这一点在复现别人的参数时经常被漏掉:同一个 top_p=0.9,在不同的温度下截出来的集合根本不一样。
还有一个组合层面的限定必须写清楚:因为 top_p 排在 top_k 之后,同时开启多个截断时,后一个截断看到的不是温度缩放后的原始分布,而是被前序过滤器删过、又重新归一化过的分布。本文后面所有「存活候选数 / 覆盖质量」的表,都是单独施加某一个截断、固定同一组原始 logits 得到的;「top-k 的集合与温度无关」「top-p 的候选数随温度单调增加」这两句话也只在这个意义下成立,它们不是任意参数组合下的普遍性质。
这一节想让你记住的一件事:参数不是并列的旋钮,它们在一条有先后顺序的流水线上,温度的施加早于所有截断。
✂️ 四、top-k:按名次截断,它对温度免疫
top-k 的定义最直白:只保留概率最大的前 个候选。形式化写法是先定义名次函数 (第 1 名是最大概率),然后
Hugging Face 的文档串把它写成「restricting to the k highest probability elements」,并在示例里给了一句经验值:「Pro tip: In practice, LLMs use top_k in the 5-50 range.」这个做法的出处是 Fan 等 2018 年的《Hierarchical Neural Story Generation》(arXiv:1805.04833)。
现在关键点来了:在单独施加 top-k 的前提下,它的集合与温度无关。 因为温度只把 logits 除以同一个正数,而除以正数是单调变换,名次不变,所以前 名还是同样那 个候选。第二节「六候选温度扫描表」的「top_k=3 覆盖质量」列就是这个事实的数字证据:温度从 0.3 升到 3.0,top_k=3 覆盖的质量从 99.17% 掉到 64.09%,但留下来的候选始终是同样三个。换句话说,top_k 决定「留几个」,温度决定「留下的这几个里面各占多少」。
这也解释了 top_k 的两个实际毛病,而且这两个毛病都能从第二节那张表的「top_k=3 覆盖质量」列读出来:
- 在模型很确定的位置(低温),留下的三个候选覆盖 99% 以上的质量,未入选的第 5、6 名合计不足 1%(本例 T=0.3 时 top_k=3 覆盖 99.17%)——此时设不设 top_k 对结果影响都很小。注意这只说明「低温下 top_k 的约束力弱」,不等于 top_k 没有用,也不等于可以把它省掉。
- 在模型很犹豫的位置(高温),留下的三个候选只覆盖 64% 的质量,剩下的 36% 质量本来分散在一大串候选上,现在被一刀切掉,然后按比例重新分给这三个人——这是「高温 + top_k 小」可能导致输出重复的一条机制猜测(注意:本例只剩 3 个候选,真实词表下被切掉的尾部要大得多、也分散得多;Mirostat 论文在其实验设置下报告过小 k / 小 p 与重复的相关性,但本文的构造例子不能证明端到端因果)。
还有两个容易混淆的边界: 且最高分没有并列时,集合里只有一个候选,它的重归一化概率必为 1,等价于贪婪解码(若最高分存在并列,所引实现会留下多于一个候选,此时不等价于贪婪); 在两个框架里都表示禁用、都表示「考虑全部候选」:vLLM 的文档串写得很明确「Set to 0 (or -1) to consider all tokens.」,Hugging Face 则在装配阶段直接跳过——只有 top_k != 0 时才会构造 TopKLogitsWarper,所以传 0 是静默不截断,既不报错、也不是「考虑零个」。
实现层还有两个边界值得记住:Hugging Face 的 TopKLogitsWarper 删除的是「分数严格小于第 k 名」的候选(scores < torch.topk(scores, k)[-1]),所以第 k 名存在并列时会留下多于 k 个候选;它的构造函数还会执行 max(top_k, min_tokens_to_keep),束搜索这类场景的保底可以让保留数大于 k。也就是说,「top_k=3 永远只留 3 个」只在无并列、保底不大于 k 的普通单束采样下成立;本文的六候选演示没有并列,不受影响。
这一节想让你记住的一件事:top-k 管数量、温度管分配;改温度不会改变 top-k 留下的候选,只会改变它们各自占多少。
🎯 五、top-p(nucleus):按累积质量截断,与温度强耦合
top-p 也叫 nucleus sampling,它的目标是:保留那些加起来刚好够 质量的最高分候选,一个都不多留、一个都不少留。(下文公式里出现的裸 指 top-p 的阈值取值,与 、 区分开。)形式化一点,就是取按概率降序的最短前缀:
Hugging Face 的文档串几乎是这个词的逐字翻译:「only the smallest set of most probable tokens with probabilities that add up to top_p or higher are kept for generation.」示例里给的经验值是 0.9 到 0.95。
它的实现方式很能说明问题。把概率升序排列后做累积求和,然后删掉累积值不超过 的那一段(也就是删掉低概率的尾巴,保留高概率的头),并且强制至少保留一个候选:
flowchart TD
A["logits z<br/>(已按温度缩放)"] --> B["softmax 得概率 p"]
B --> C["按概率升序排序"]
C --> D["前缀累积求和 cumsum"]
D --> E["删掉 cumsum 不超过 1-p 的候选"]
E --> F["保底:至少留 1 个候选"]
F --> G["重归一化 q = p / m"]
G --> H["按 q 采样"]这张图说明了什么:top-p 不是「留前 x% 的候选」,而是「留到够 p 质量为止」——候选个数是被质量预算反推出来的,所以它能随分布形状伸缩。这正是它被认为比 top-k 更「自适应」的原因。
砍完之后,数学上必须按条件分布来解释剩余概率,也就是写成除以 的形式;实现上是否显式做这次除法、在哪里做,取决于采样器(多项分布采样接受未归一化的非负权重,对数域实现用减 log-sum-exp 代替除法)。区别在于:「必须重归一化」要求的是结果等价于条件分布,而不是每次截断后都要手写一遍除法。设保留下来的总质量为 ,重归一化就是
两个直接推论:第一,每个存活者的概率都被统一放大 倍(相对比例不变,所以名次也不变);第二,砍掉的尾部质量并没有消失,而是按比例分给了幸存者。
现在把温度拉进来。熵的精确恒等式可以这样推:把 代入熵的定义,
其中 是「只看幸存者、不重归一化」的部分熵。这个式子把「截断 + 重归一化」拆成了两项: 是被放大后的头部贡献, 是尾部被删掉带来的熵下降。用 T=1.0 那一列的 top_p=0.9 验算一遍(以下为本文自行推导):保留下来的四个候选是 the / a / car / bicycle,,,于是
而截断前的原始熵是 。也就是说:这一步截断把采样的熵从 1.4734 降到 1.2724 nats(按 折算的「熵困惑度」,也就是该单步分布的有效候选数,从 4.3640 降到 3.5695;它与语言模型在整句上按交叉熵算的困惑度不是同一个量)——截断让输出更保守,而且这个「更保守」是可以算出来的,不是形容词。
最关键的一点在第二节那张表里:top_p=0.9 在 T=0.3 时只需 2 个候选,在 T=3.0 时需要 5 个候选。 同一个参数值,截出来的集合大小差了一倍多。原因就是温度把分布压平后,要凑够 90% 的质量就得拉更多人进来。所以「0.9」这个数字本身不携带信息,它必须和温度一起读。这也是第九节把「T 与 top_p 互相抵消」列为误解的原因:耦合存在,但不是固定配比。
这一节想让你记住的一件事:top-p 是质量预算驱动的截断,它的集合大小随温度变化——同一个 top_p 在不同温度下不是同一个采样器。
📊 top_p 的隐含候选数:把 p 取四档扫一遍
既然 top_p 的候选数随温度伸缩,那就把方向和幅度都扫出来。下面是同一组六候选 logits、四个 p 档位、七个温度档位的完整扫描(全部为本文自行推导,格式是「存活候选数 / 实际覆盖质量」)。注意 T=3.0 只是数学上的演示:vLLM 对 temperature 的校验区间是 (2026-10-15 读取的 main commit 273a049),超出会直接报错,所以这张表的最后一行不是一组能直接提交给 vLLM 的配置:
| T | top_p=0.8 | top_p=0.9 | top_p=0.95 | top_p=0.99 |
|---|---|---|---|---|
| 0.3 | 2 / 0.9400 | 2 / 0.9400 | 3 / 0.9917 | 3 / 0.9917 |
| 0.5 | 2 / 0.8351 | 3 / 0.9514 | 3 / 0.9514 | 5 / 0.9994 |
| 0.7 | 3 / 0.9000 | 4 / 0.9646 | 4 / 0.9646 | 5 / 0.9963 |
| 1.0 | 3 / 0.8315 | 4 / 0.9282 | 5 / 0.9869 | 6 / 1.0000 |
| 1.5 | 4 / 0.8769 | 5 / 0.9669 | 5 / 0.9669 | 6 / 1.0000 |
| 2.0 | 4 / 0.8398 | 5 / 0.9486 | 6 / 1.0000 | 6 / 1.0000 |
| 3.0 | 5 / 0.9219 | 5 / 0.9219 | 6 / 1.0000 | 6 / 1.0000 |
三个读法:
- 同一列从上往下看 = 温度升高的效果。 四列都没有反向行,top_p=0.8 与 0.9 两列从 2 个候选涨到 5 个。这就是「高温下 top_p 会变松」的量化版本。
- 同一行从左往右看 = 放宽 p 的效果,而它在低温区几乎无效。 T=0.3 那一行,p=0.8 和 p=0.9 都只留 2 个候选——因为第一名一个人就占了 74.39% 的质量,第二名一进来累计就超过 94%,把 p 从 0.8 调到 0.9 根本改不了决定。反过来,p=0.95 与 p=0.99 从 T=0.5 那一行起就已经分家(3 个 vs 5 个),T=1.0 时是 5 个 vs 6 个;到了 T=3.0 那一行,连 0.95 与 0.99 都只能留下全部 6 个候选,差别又消失了——因为分布已经平到必须留满。这就是「低温下调 top_p 没感觉、高温下调 top_p 感觉很大」这类体感的机制来源(本文只演示了这组构造 logits 上的形态,不能据此推断真实模型上的幅度)——不是参数坏了,是它作用的区间变了。
- 「实际覆盖质量」那一列暴露了 top_p 的真实行为。 因为规则是「加到够为止」,实际覆盖经常明显超过 p:T=0.5、p=0.8 时一次就覆盖到 0.8351;T=0.3、p=0.8 时甚至覆盖到 0.94。所以 p 不是「保留多少比例的质量」,而是「至少保留多少质量」的下界——候选必须按整数个加进来,所以超调不可避免。
关于第三点要补一句边界:本文只有 6 个候选,所以本文这 28 格的超调跨度足有两个数量级——最大 14 个百分点(T=0.3、p=0.8),最小只有 0.14 个百分点(T=0.5、p=0.95)。真实模型的词表比这大好几个数量级,但这不能推出「真实模型的超调通常很小」——超调幅度取决于跨过阈值的那个候选各自的概率,不由词表大小单独决定。这个例子的用途是让每一步都能被人手验算,不能拿来估真实模型的超调幅度;真要在自己的模型上量覆盖质量,前提是先确认拿到的 logprobs 是截断之前、且与当前温度一致的那批分数(并覆盖到判据需要的候选)——只拿到截断后归一化的前 k 个概率,是反推不出截断前覆盖质量的,接口语义要先核对。
这一节想让你记住的一件事:top_p 的实际覆盖总是 ≥ p(p 是下界不是精确比例),而在单独施加 top_p 时候选数随温度单调增加——两个方向叠加起来,就是「同一个 0.9 在不同温度下对应完全不同的集合」:低温时只留 2 个候选(砍得狠),高温时要留 5–6 个(留得宽)。
🆕 六、min-p:把「砍多少」交给模型的置信度
min-p 换了判据:不看累积质量,也不看名次,而是看每个候选与最高分候选的比值。保留条件写成
Hugging Face 的文档串把这个设计意图写得很清楚:它是「keeps all tokens that are above a minimum probability, scaled by the probability of the most likely token」,并且解释了为什么这样做——「the filter becomes more aggressive in the presence of high-probability tokens, which is a sign of a confident output that we shouldn’t deviate from」。
翻译成工程语言:**阈值是跟着置信度动的。**不过这里要说准确一层:把 两边同除 ,得到等价写法 ;再代入 softmax,就是 。也就是说,真正决定集合的是每个候选与第一名差了多远(以 为单位), 只是这个差距的缩放代理——往尾部加一批低分候选会改变 ,却不改变已经排在前面那些候选与第一名的概率比。所以下文表格里的「置信度」要读成「以 为代理的相对差距」,而不是「 独自决定砍多少」。
- 模型很确定时, 很大(第二节那张表里 T=0.3 时是 0.7439),阈值 也随之变大,砍得就狠:min_p=0.1 时只剩 2 个候选。
- 模型不确定时, 很小(T=3.0 时只有 0.2427),阈值同比缩小,留下的就宽:同样 min_p=0.1 时留 6 个候选。
先把 min-p 与 top-p 的区别说透,因为它们常被当成同一类:
| 维度 | top-p | min-p |
|---|---|---|
| 判据 | 累积质量够不够 | 单点概率够不够 |
| 谁决定砍多少 | 一个固定的质量预算 | 各候选与最高分的相对差距(以 为代理指标) |
| 参数方向 | 越大留得越多 | 越小留得越多(方向相反) |
| 典型取值 | 0.9 – 0.95 | 0.01 – 0.2 |
第三行「方向相反」不是文字游戏:Hugging Face 的文档串专门提醒「Typical values are in the 0.01-0.2 range, comparably selective as setting top_p in the 0.99-0.8 range (use the opposite of normal top_p values).」——把 top_p 的经验值直接搬到 min_p 上,会得到完全相反的松紧度,这是实际配置里最常见的错。
min-p 的实现细节也值得记一句:它的代码是先把分数变成概率、取出本步最大的那个概率、乘以 min_p 得到真正阈值,再删掉低于阈值的候选,并且和 top-p 一样有「至少保留 min_tokens_to_keep 个」的保底。保底这一条比看上去重要:它保证这些过滤器在输入合法、参数有效的前提下不会把候选集删成空集。但它做的事只有一件:不把候选集删成空集(对 top-p / min-p 这类规则,最高分候选本来就满足判据,保底只在极端配置下兜住)。保底不等于「集合被砍成单元素」:真正让温度失去作用的是最终集合恰好只剩一个候选(例如 top_k=1 且无并列、或 min_p=1),此时温度只改变那个唯一候选的未归一化权重,归一化后必然是 1——这是规则本身的结果,不是保底造成的。保底也救不了非法参数:Hugging Face 对 temperature=0 直接抛 ValueError(报错信息让你改设 do_sample=False),对负的 top_k(如 -1)也在构造 warper 时抛错;但 top_k=0 属于合法取值——装配阶段只在 top_k != 0 时才构造 TopKLogitsWarper,所以 0 的效果是「禁用 top_k、考虑全部候选」,既不报错也不截断。
关于 min-p 的效果,论文《Turning Up the Heat: Min-p Sampling for Creative and Coherent LLM Outputs》(arXiv:2407.01082,2024 年 7 月 1 日首次提交,本文读取的是 v8 修订版,arXiv 页面标注 v8 更新于 2025 年 11 月 20 日)报告:在高温度下,min-p 在 GPQA、GSM8K、AlpacaEval Creative Writing 等基准上同时改善了质量与多样性(测试模型覆盖 Mistral 与 Llama 3 系列、1B 到 123B 参数),人类评测也更偏好它的输出。这些是论文自报的结论,本文没有复现其实验;论文同时说明 min-p 已被 Hugging Face Transformers、vLLM 等框架采纳——这一点与本文查到的实现相互印证。
这一节想让你记住的一件事:min-p 的判据是「与最高分的相对差距」,阈值随模型置信度缩放( 是代理指标、不是独立决定因素),参数方向与 top-p 相反——两者不能互相套用经验值。
🧪 七、三类截断放在一起看:一张对照表
现在把前面所有细节压缩成一张表。注意最后一列,它回答的是「同一个输入下,什么决定了砍多少」——这才是选参数时真正该问的问题。
| 方法 | 判据 | 与温度的关系 | 候选数固定吗 | 典型取值 | 谁决定砍多少 |
|---|---|---|---|---|---|
| 贪婪解码 | 直接取 | 无关(不采样) | 固定 1 | — | 不适用 |
| top-k | 名次 ≤ | 免疫(单独施加、无并列、保底不大于 时):集合不变,质量变 | 固定为 (上述条件下) | 5 – 50 | 人工设定的固定名额 |
| top-p | 累积质量 ≥ 的最小集合 | 强耦合:温度升高→集合变大 | 随分布变化 | 0.9 – 0.95 | 固定的质量预算 |
| min-p | 耦合但方向自适应 | 随分布变化 | 0.01 – 0.2 | 与最高分的相对差距( 为代理) | |
| typical | 每个词的信息量与本步平均不确定性对齐 | 耦合 | 随分布变化 | warper 默认 0.9(库兜底 typical_p=1.0,即未启用) | 「这个词的信息量是否典型」 |
| η-sampling | , | 耦合(阈值本身依赖熵) | 随分布变化 | 任务相关 | 一个熵依赖的概率阈值 |
| Mirostat | 用反馈把困惑度控在目标值 | 耦合 | 随分布变化 | 任务相关 | 一个预先设定的困惑度目标 |
关于最后三行,得说清它们的出处与状态:typical(locally typical sampling,arXiv:2202.00666)的判据是「让每个词的负对数概率接近本步条件熵」——Hugging Face 的 TypicalLogitsWarper 文档串复述得很准确:「it prioritizes tokens whose log probability is close to the entropy of the token probability distribution. This means that the most likely tokens may be discarded in the process.」注意最后半句:典型采样会主动丢掉最高分的词,这和 top-k / top-p / min-p 只砍尾部是相反的取向。η-sampling 同样出自《Truncation Sampling as Language Model Desmoothing》(arXiv:2210.15191):该文把截断理解成「去平滑」,并把 η-sampling 定义为「截掉概率低于熵依赖阈值的词」(论文摘要原句是 truncates words below an entropy-dependent probability threshold);Hugging Face 的 EtaLogitsWarper 把同一条动态阈值实现为 ,即「固定上限与随熵变化的阈值取小」。(顺带一个可复现的实现细节:该类的文档串把它写成 sqrt(epsilon * e^-entropy),而代码里算的是 sqrt(epsilon) * exp(-entropy),两者差一个平方根指数、并不等价;本文以代码为准——这也正好说明「读文档串」不等于「读实现」。)Mirostat(arXiv:2007.14966)则不同——它不设一个固定阈值,而是用反馈让输出的困惑度落在目标值附近,论文报告在 Zipf 统计假设下 top-p 的交叉熵近似是 的线性函数、而 top-k 是 的非线性函数,这个差别正是它选择自适应路线的理由。
这一节想让你记住的一件事:选参数时先问「谁决定砍多少」——固定名额(top-k)、固定预算(top-p)、置信度自适应(min-p)、还是目标统计量(Mirostat),四种设计哲学不能混着调。
🌐 同一个参数名,两个框架的默认值不同
最后一个实践层面的问题:既然参数的作用位置这么关键,那么「什么都不设」到底等于设了什么?两个主流框架给出的答案并不一样。
| 参数 | Hugging Face Transformers(GenerationConfig 字段文档串;2026-10-15 读取 commit f0705f9) | vLLM(SamplingParams 源码;同日读取 commit 273a049) |
|---|---|---|
| temperature | 1.0(「If it isn’t set, the default value is 1.0」) | 1.0 |
| top_k | 50(「If it isn’t set, the default value is 50」) | 0,即禁用 |
| top_p | 1.0 | 1.0 |
| min_p | 默认 None(未启用);字段文档串给的是典型取值 0.01 – 0.2 | 0.0,即禁用 |
| repetition_penalty | 1.0 表示无惩罚 | 1.0(默认无惩罚) |
差别落在最容易被忽略的那一行:Hugging Face 的默认 top_k 是 50,vLLM 的默认 top_k 是 0(禁用、考虑全部候选)。 比较时还要把采样开关一起固定:下面说的都是「do_sample=True、单束(num_beams=1)、模型自带的 generation_config.json 没有覆盖这几个参数」的情形。同理,vLLM 的 SamplingParams 类默认值也不等于服务接口的最终生效值(服务侧可能被模型自带配置或启动参数覆盖),下文只说「直接构造 SamplingParams」时的行为。在这种条件下,同一份「只设了 temperature=0.8」的配置,在两个框架上跑出来的采样器并不是同一个:
- HF 侧:即使 top_p=1(不做质量截断),top_k=50 依然在生效——每个位置上只在前 50 名候选里抽。
- vLLM 侧:top_k=0 且 top_p=1 表示完全不截断,就是原分布采样。
这解释了一类常见困惑:「同一个 prompt、同样的 temperature,为什么本地 HF 跑出来的输出比推理框架上更规矩、更不容易跑偏」。一个常被忽略的来源就是默认参数不同(本文没有统计过这种情形在真实困惑里占多大比例,但它是排查时最先该排除的一项)。
还要顺着这条线补一个更隐蔽的坑:HF 的文档串明确写了这些默认值「is set in a model’s generation_config.json file」——也就是说 top_k=50 只是「模型作者没有覆盖时的兜底」,具体模型可能带一份自带的 generation_config.json,把 temperature、top_k、top_p 全部改掉。所以「HF 的默认值」其实有两层:库的兜底值,和模型自带的配置值。
另一个同源的坑是 HF 的 do_sample:「Whether or not to use sampling ; use greedy decoding otherwise.」——在 HF 上采样开关的生效值为 False 时不做多项分布采样,前面那些温度、top_k、top_p 全部不生效。(默认 do_sample=False 走贪婪;模型自带的 generation_config.json 也可能预先打开采样,num_beams>1 时还会走束搜索)这是「参数明明设了却看起来没用」的经典来源,第九节的误解五(把候选集砍到单元素导致温度失效)是同一类问题的另一半。
这一节想让你记住的一件事:比对两个框架的输出之前,先比对生效的默认值——参数名相同,默认行为可以不同,而且「默认」还要区分库兜底与模型自带配置。
🔁 八、另外三个旋钮不是采样参数:惩罚类改的是原始分数
repetition_penalty、presence_penalty、frequency_penalty 这三个经常和温度一起出现在同一行配置里,但它们在流水线的最前面,作用对象是 logits,不是概率。vLLM 的采样参数源码给了三者的语义:
repetition_penalty:默认 1.0,「Penalizes new tokens based on whether they appear in the prompt and the generated text so far. Values > 1 encourage the model to use new tokens, while values < 1 encourage the model to repeat tokens.」presence_penalty:默认 0.0,「Penalizes new tokens based on whether they appear in the generated text so far. Values > 0 encourage the model to use new tokens, while values < 0 encourage the model to repeat tokens.」frequency_penalty:默认 0.0,「Penalizes new tokens based on their frequency in the generated text so far. Values > 0 encourage the model to use new tokens, while values < 0 encourage the model to repeat tokens.」
三者的差别在两个维度上:惩罚的触发条件(出现在提示词里也算 / 只看出现在生成里)和惩罚是否随出现次数累积(presence 只看「出现过没有」,frequency 看「出现几次」)。repetition_penalty 的历史出处是 CTRL 论文(arXiv:1909.05858,2019 年 9 月 11 日提交),这也是它名字看起来和另外两个不同族的原因。
为什么必须把它们和采样参数分开看?因为它们在 softmax 之前就改了分数,于是同时改变形状与名次:一个候选被降分之后,可能直接从「排第二」掉到「被 top-p 砍掉」。这带来两个常见后果:
- 把 repetition_penalty 调大不保证消除重复,还可能伤及本该复现的词。重复的成因往往是解码方式本身(贪婪或温度过低导致在同几个词上打转),而不是「模型不知道别的词」。惩罚类参数会让模型主动回避所有高频词——包括这篇文本里正当需要的词,常见症状是语法代词、专有名词被替换成奇怪的同义词。关于「高概率文本为什么会平淡重复」,词表级(整分布层面)的分析可以看 nucleus sampling 原论文(arXiv:1904.09751)与 locally typical sampling(arXiv:2202.00666)的讨论。
- 它和温度叠加时会互相掩盖,但低温并不会让它失效。Hugging Face 的
RepetitionPenaltyLogitsProcessor直接改已出现 token 的 logit(正分数除以 penalty,负分数乘以 penalty),因此它可以改变名次:本文构造的两个 logit 2.0 与 1.6,若前者已出现且 penalty=2,处理后变成 1.0 与 1.6,第一名直接换人(本文自行推导)。所以在极低温度下惩罚依然可能是决定性的;反过来,温度高时惩罚把某些词压下去、又让另一些次优词浮上来,两者会互相掩盖。调参时最好一次只动一类。
这一节想让你记住的一件事:惩罚类参数改的是 softmax 之前的分数,会同时改变形状和名次,不能和温度、截断参数放在同一个「松紧度」坐标系里比较。
❓ 九、五个常见误解
误解一:「temperature=0 就是「温度为零的 softmax」」
数学上 的极限确实会让全部质量集中到最高分候选,但 本身会让 出现除零,softmax 无法直接定义。两个框架对这件事的处理并不相同:vLLM 明确接受 0,源码里温度低于极小阈值(_SAMPLING_EPS)时注释写的正是「Zero temperature means greedy sampling.」;Hugging Face 则直接拒绝——TemperatureLogitsWarper 只接受严格正的 temperature,传 0.0 会抛 ValueError,报错信息让你改设 do_sample=False。所以「把温度设成 0」不是通用写法:在 vLLM 上它是约定的贪婪;在 Hugging Face 上应当关掉采样。
误解二:「top_p=1 和默认一样,所以不用管」
前半句对,后半句会把人带偏——在 Hugging Face 上即使 top_p=1,默认的 top_k=50 依然在生效(前提是 do_sample=True;否则走贪婪,所有截断参数都不生效。见上文「同一个参数名,两个框架的默认值不同」一节)。vLLM 里 top_p 默认就是 1.0、top_k 默认 0、min_p 默认 0.0,三者合起来确实表示「不截断」;但 top_p=1 与 temperature=1 是完全不同的两件事:前者表示保留全部候选,后者表示指数化时不做缩放。「什么都不做」在 vLLM 上确实是四个参数取默认值,在 Hugging Face 上则不成立——库兜底的 top_k=50 依然在截断。
误解三:「温度高一点、top_p 低一点,可以互相抵消」 耦合是真的,但不是固定配比:温度升高会把分布压平,要让 top_p 重新收紧就得把 p 压得更低,而压得动还是压不动取决于当前落在哪个温区:本文的例子里,低温端怎么压都不动,中温端才有效。本文的六候选例子里,T=0.3 时 p=0.8 与 0.9 都只留 2 个候选(低温区几乎压不动),而 p=0.95 与 0.99 从 T=0.5 起就已经分家(3 个 vs 5 个),T=1.0 时是 5 个 vs 6 个。所以「互相抵消」只在「同一条等值线上」才有意义——它不意味着换一组参数就能得到同一个采样器,也不意味着两者的变动幅度可比。
误解四:「top_k 和温度一起调,效果是叠加的」 不是叠加,而是分层:top_k 决定集合,温度决定集合内的分配。表里 top_k=3 的质量从 99.17% 掉到 64.09%,而这三个候选从头到尾没变——这就是分层的直接证据。
误解五:「调了温度一定会更随机」 不一定。如果同时设了 top_k=1,候选集永远只有一个元素,无论温度取多少,采样结果都是同一个词(此时温度只改变那唯一的概率值,而它必然被归一化成 1)。同理,任何把集合砍到单元素的参数组合都会让温度「失效」。
这一节想让你记住的一件事:参数的语义来自具体框架的约定(同一个 temperature: 0,vLLM 接受、Hugging Face 报错)和参数之间的耦合关系,不能凭名字猜。
🧭 排障:从症状倒推该动哪个参数
前面讲的是「参数做什么」,实际工作里更常遇到的是反方向的问题:输出不对,该动哪个?下面这张对照表把常见症状映射回前面几节的机制。注意:这张表是本文根据参数语义整理的排查顺序,不是任何实验的结论,用它缩小范围,用 logprobs 与对比生成确认。
| 症状 | 机制上可以解释它的原因(假设) | 建议的排查顺序 |
|---|---|---|
| 输出保守、几乎每步都选同一个词 | 温度过低,或截断把集合砍到接近单元素 | 先看生效的 top_k / top_p(框架默认值可能不由你决定),再谈温度 |
| 输出在同几个词上打转、重复 | 高温 + 截断过紧:存活候选覆盖的质量太少,尾部被重归一化分给了少数几个词 | 先用 logprobs 看单步覆盖质量,再考虑放宽截断;惩罚类参数放到最后 |
| 输出跑偏、答非所问 | 温度过高,或 top_p / min_p 过松导致长尾被抽中 | 先降温度,再收紧 p(分布已经饱和时,降温度的边际效果会迅速减小,见第二节的推导) |
| 同一 prompt 两次输出差异巨大 | 温度偏高且截断没有约束力 | 加上 top_p 或 min_p,让集合先被收住 |
| 参数设了却像没生效 | HF 未打开 do_sample(走贪婪);或候选集被砍成 1 个,温度失去作用 | 先确认采样开关,再确认集合大小 |
| 换框架 / 换模型后行为变了 | 默认值不同,或模型自带 generation_config.json 覆盖了默认值 | 把实际生效值打印出来核对,别比对参数名 |
三个操作纪律,都是从前面几节的机制直接推出来的:
- 一次只改一类。 三类参数作用在不同位置,同时改会让「是哪一个起了作用」无法判断。
- 温度与截断要一起读。 想收紧输出时,先算一下当前截断覆盖了多少质量(存活集合的 ),再看温度——因为(二候选情形下) 在两端都趋于 0,降温的边际效果只在分布还没饱和时较大,饱和之后收紧截断更直接。
- 惩罚类参数放在最后。 它改的是 softmax 之前的分数,会同时改变名次与形状,一旦介入,前面两边的关系就都不好解释了。
这一节想让你记住的一件事:诊断顺序(先看生效的配置、再看单步覆盖质量)与调整顺序(先定形状(温度)→ 再定集合(截断)→ 最后才考虑动原始分数(惩罚))是两件事,不要混用;无论走哪条,都是一次只动一层。
🚧 十、边界与未解
- 本文的数值例子是构造出来的。 六候选 logits 只是为了把参数之间的关系展示清楚:这组 logits 未取自任何真实模型,本文也不主张它代表真实分布。结论(top-k 对温度免疫、top-p 与温度强耦合、min-p 判据随置信度缩放)是从公式推出来的,不依赖这组具体数字;但表里的具体取值不可外推。
- 「最佳组合」没有统一答案。 min-p、typical、η-sampling、Mirostat 都用各自的语言提出了「更好的截断」,各自的评测覆盖不同的模型家族、规模与任务。论文自报的优势不能直接当成跨模型的普遍结论,选参数仍需在自己的任务上量。
- 截断本身的代价没有免费的午餐。 「截断 = 去平滑」这个视角(arXiv:2210.15191)指出:语言模型可以看成一个「真分布 + 平滑分布」的混合,截断的目标是估计真分布的支撑集,估错就会砍掉本该保留的词。该论文给了一个很直观的反例:当文档以某个词开头、上下文强烈指向一个特定专名时,top-p 会把除了那个专名之外的所有词都砍掉——集合退化到单元素,采样和贪婪没区别。这个例子出自该论文,本文未独立复现。
- 不同框架的施加顺序需要各自核实。 本文查证的是 Hugging Face Transformers 在 2026-10-15 的源码顺序(温度先于全部截断类处理器,见第三节的组合限定);换到别的推理引擎时,同样的参数名不保证同样的施加次序与同样的边界处理(例如「保底至少一个候选」这一条)。复现别人的参数时,只看参数值是不够的。
- 保底规则会影响极端参数下的行为:本文提到的「至少保留一个候选」在 Hugging Face 的 top-p 与 min-p 实现里都存在,它意味着这些过滤器在有效输入下不会产出空集;这在参数配得过紧时是救命的,但也意味着参数配得过紧时不会报错,而是静默地越留越少(极端配置下只剩一个候选,看起来就像贪婪解码)。要区分的是:非法参数(如
temperature=0、负的top_k)会直接抛异常,保底救不了它们;而top_k=0是合法取值(表示禁用 top_k),效果是静默不截断。 - 「默认值」本身是两层结构:HF 的
GenerationConfig字段文档串把每个默认值都写成「模型自带的generation_config.json没设时才用它」,所以「HF 的默认 top_k=50」只对没有自带配置的模型成立。跨框架、跨模型复现别人的参数时,唯一可靠的做法是把实际生效值打印出来核对,而不是记住任何一组数字。 - 本文没有做任何生成实验:所有关于「输出会怎样」的判断都是从公式和实现读出来的推论,不是端到端实测。采样参数对最终文本质量的影响需要在自己的任务上量(困惑度、重复率、人工偏好),这类结论无法从参数语义直接推出。
- 真实分布的尾部形状决定 top-k 与 top-p 的差别有多大:Mirostat 论文报告,在 Zipf 统计假设下 top-p 的交叉熵近似随 线性变化,而 top-k 随 呈非线性。换句话说,同一个「收缩幅度」在两种参数上对应的取值并不等比,所以「把 top_k 从 50 调到 25」和「把 top_p 从 0.95 调到 0.90」不能当成同一件事来比较。
- 本文只覆盖了采样链路的一小部分参数:真实框架里还有 top_h、typical_p、epsilon / eta cutoff、logit_bias、bad_words、allowed_token_ids、结构化输出约束等,它们同样归入「改分数」或「改集合」这两层。判断一个陌生参数属于哪一层,最稳的方法是看它做的是哪一件事:缩放分数(温度)、重排或屏蔽候选(各类截断),还是直接改原始分数(惩罚类)。「排在 softmax 之前还是之后」只是常见的实现表象,不构成分类依据——例如 top-k 完全可以直接在 logits 上比较名次,并不需要先算出概率。
这一节想让你记住的一件事:本文的结论是公式层面的,数字例子是构造的;换框架、换任务都要重新核实现与量。
✅ 小结
四句话收束全文:
- 温度是尺子。它把 logit 差值除以 ,只改差距、不改名次;在二候选情形下对熵的影响是 (整词表是 ),敏感度在极低温和高温两端都趋于 0,峰值在 附近。
- top-k 看名次,top-p 看质量,min-p 看置信度。三者的候选集与温度的关系因此完全不同(下面三句都指单独施加该截断时):top-k 免疫,top-p 随温度升高而变大,min-p 由各候选与最高分的相对差距决定( 只是缩放代理),参数方向与 top-p 相反。
- 砍完必须重归一化。存活者概率统一放大 倍,相对比例不变,名次不变;这一步的熵变化有精确恒等式 。
- 惩罚类参数在流水线最前面,作用对象是 logits,会同时改变名次与形状,不要和采样参数混在一起调。
如果只记一句关于「为什么这件事值得花时间搞清楚」的话:采样参数是一类在模型之外、却能显著改变输出性格的开关(提示词、上下文与检索内容同样能改,但采样参数是最容易被忽略、也最难从输出里反推出来的一类)。模型权重定下来之后,同样的知识、同样的能力,经过不同参数组合会呈现出「背课文」和「聊天」两种完全不同的面貌。搞清它们各自作用在哪一层,比记住任何一组「推荐参数」都更耐用——因为推荐值会随模型换代而失效,作用位置不会。
🎯 如果你今天只想记一件事
你以为在调「随机性」,其实在调两个位置不同的东西:温度改的是 softmax 内部的刻度,截断参数改的是 softmax 之后的候选集合——而它们的耦合方式完全不同:在本文构造的六候选例子里、单独施加 top-p 时,同一个 top_p=0.9 在 T=0.3 只留 2 个候选(砍得狠),在 T=3.0 要留 5 个(留得宽)。
📋 来源与核验记录
以下每一条都是本轮实际打开并读取过的一手来源(论文页取摘要与元信息、代码取文档串与实现、官方文档取参数说明),核验日期 2026-10-15:
- Holtzman et al., The Curious Case of Neural Text Degeneration(nucleus sampling 的提出与「似然最大化解码会平淡重复」的现象),2019-04-22:https://arxiv.org/abs/1904.09751
- Fan, Lewis, Dauphin, Hierarchical Neural Story Generation(top-k 随机采样的出处,正文第四节引用),2018-05-13:https://arxiv.org/abs/1805.04833
- Keskar et al., CTRL: A Conditional Transformer Language Model for Controllable Generation(repetition penalty 的出处),2019-09-11:https://arxiv.org/abs/1909.05858
- Meister et al., Locally Typical Sampling(typical 解码的判据与「信息量接近条件熵」的表述),2022-02-01:https://arxiv.org/abs/2202.00666
- Nguyen et al., Turning Up the Heat: Min-p Sampling for Creative and Coherent LLM Outputs(min-p 的阈值定义、基准与人类偏好结论;本文标注为论文自报。arXiv 首次提交 2024-07-01,本文读取的是当前 v8 修订版):https://arxiv.org/abs/2407.01082
- Hewitt, Manning, Liang, Truncation Sampling as Language Model Desmoothing(「截断 = 去平滑」视角、η-sampling、top-p 误砍高概率词的反例),2022-10-27:https://arxiv.org/abs/2210.15191
- Basu et al., Mirostat: A Neural Text Decoding Algorithm that Directly Controls Perplexity(top-k / top-p / 温度下的困惑度解析关系与自适应解码),2020-07-29:https://arxiv.org/abs/2007.14966
- Su et al., A Contrastive Framework for Neural Text Generation(对比搜索解码;本文用于背景核验,正文未直接引用),2022-02-13:https://arxiv.org/abs/2202.06417
- Hugging Face 官方博客 How to generate text(温度 / top-k / top-p 的通俗说明与「取前 3 个词超过 92%」的例子):https://huggingface.co/blog/how-to-generate
- Hugging Face Transformers 源码
generation/logits_process.py(TopKLogitsWarper / TopPLogitsWarper / MinPLogitsWarper / TemperatureLogitsWarper / TypicalLogitsWarper / EtaLogitsWarper 的文档串与实现,含「至少保留 min_tokens_to_keep 个」、top-p 的升序累积删除逻辑、温度必须严格为正的校验、EtaLogitsWarper 的熵依赖动态阈值;对应main分支 commitf0705f9,2026-10-15 读取):https://github.com/huggingface/transformers/blob/f0705f991a71f05dad9569336b181119f5546f93/src/transformers/generation/logits_process.py - Hugging Face Transformers 源码
generation/utils.py中的_get_logits_processor(各处理器的装配顺序:温度 → top_h → top_k → top_p → min_p → typical → epsilon → eta;截至 2026-10-15 的main分支 commitf0705f9,旧名_get_logits_warper已不存在):https://github.com/huggingface/transformers/blob/f0705f991a71f05dad9569336b181119f5546f93/src/transformers/generation/utils.py - vLLM 源码
vllm/sampling_params.py(temperature / top_p / top_k / min_p / repetition_penalty / presence_penalty / frequency_penalty 的默认值与语义,含「Zero temperature means greedy sampling.」「Set to 0 (or -1) to consider all tokens」与 temperature 校验区间 ;对应main分支 commit273a049,2026-10-15 读取):https://github.com/vllm-project/vllm/blob/273a04964ed9d1e389c4a7cb9d660e8294782f8d/vllm/sampling_params.py - Hugging Face Transformers 源码
generation/configuration_utils.py(GenerationConfig各字段的文档串与默认值:temperature 默认 1.0、top_k 默认 50、top_p 默认 1.0、min_p 默认 None、repetition_penalty 1.0 表示无惩罚、do_sample 默认 False 时不做多项式采样,以及「默认值可被模型自带 generation_config.json 覆盖」的说明;对应main分支 commitf0705f9,2026-10-15 读取):https://github.com/huggingface/transformers/blob/f0705f991a71f05dad9569336b181119f5546f93/src/transformers/generation/configuration_utils.py
本文自行推导(与上述官方数字分列):第二节的二选一熵导数、第二节的六候选温度扫描表(含 top_p=0.9 存活数 2→5、top_k=3 覆盖 99.17%→64.09%、T=1.0 的六个概率值)、第五节的 top_p=0.9 验算(、、、 由 4.3640 降至 3.5695)、「把差距量化」一节的前两名概率比 3.7937 / 1.4918 / 1.1426、以及「top_p 四档扫描」一节的全部存活数与覆盖质量(0.8315 / 0.8351 / 0.8398 / 0.8769 / 0.9000 / 0.9219 / 0.9282 / 0.9400 / 0.9486 / 0.9514 / 0.9646 / 0.9669 / 0.9869 / 0.9917 / 0.9963 / 0.9994 / 1.0000,以及同一节末的超调跨度 0.14 – 14 个百分点、第四节 T=0.3 时未入选的第 5、6 名合计不足 1%(= 1 − 0.9917))。这些数字来自本文构造的 logits,用于演示参数之间的关系,不代表任何模型实测结果;文中的百分数(如 99%、64%、36%、90%、94%、74.39%、14%)均指这组构造分布上的质量占比或概率比值(例如 T=0.3 时第一名占 74.39%、top_p=0.8 实际覆盖 0.9400、前两名概率比 1.1426 对应领先 14%),不是任何线上服务的统计值。本次修订新增的自算项:第二节熵导数敏感度两端趋零的数值例( 时 H(0.4)-H(0.3)=0.000476、H(1.4)-H(1.3)=0.030357)与峰值位置 (复算 3.2436)、多候选情形的关系式 (仅作结构说明,未用于数值断言)、第二节 T=0.7 行 0.9000 的精确值 0.899971、第八节 repetition_penalty 的反例(logits 2.0 / 1.6 在 penalty=2 时变成 1.0 / 1.6)。