
1. 从一个看似简单的需求说起1.1 为什么“加个 temperature”会变成一个值得写文章的问题事情起源于一个很朴素的需求给扩散语言模型diffusion language model简称 dLLM的推理过程加一个 temperature 参数让采样结果可控。做过自回归模型推理的人第一反应都是——这不就是改一行 argmax 的事吗把logits.argmax()换成torch.multinomial(softmax(logits / temperature))收工。但真正动手之后你会发现扩散语言模型的解码路径和自回归模型完全不是一回事。argmax 在自回归里是“每一步选一个 token”而在扩散语言模型里它出现在去噪迭代的每一个时间步上而且每一步的 argmax 结果会作为下一步的输入被重新加噪、重新预测。你改的不是一个终点决策而是整条轨迹上的一个中间算子。这就好比你以为自己在调水龙头的开关结果发现这个开关同时控制着整条水管里每一段的水压。我最近在 SGLang 上做 dLLM 的推理适配就踩了这个坑。SGLang 本身对自回归模型的支持非常成熟调度、KV cache、continuous batching 都做得很干净但 dLLM 的推理范式和它原有的假设有本质冲突。temperature 这件事看起来小实际上牵出了采样策略、去噪调度、并行解码、缓存复用一整条链路。这篇文章就把我这几周折腾下来的东西完整梳理一遍包括为什么不能只改一行 argmax、正确的改法应该长什么样、以及在实际工程里怎么落地。适合读这篇的人对扩散语言模型有基本了解、想自己动手做推理适配的工程师正在用 SGLang 或类似框架做非自回归推理的开发者以及单纯好奇“扩散模型怎么做文本生成”的读者。我会尽量把原理讲透同时给出可以直接抄的代码思路。1.2 先厘清扩散语言模型到底在“扩散”什么很多人第一次听到“扩散语言模型”会懵因为图像扩散那套加高斯噪声、学反向去噪搬到离散的 token 空间上并不直接成立。文本是离散的你没法给一个 token id 加高斯噪声。目前主流的 dLLM 走的是离散扩散或者掩码扩散masked diffusion路线。核心思想是定义一个前向过程把一段完整的 token 序列逐步“破坏”成完全随机的状态然后训练一个模型学习反向过程从随机状态逐步恢复出原始序列。破坏的方式有很多种最常见的是随机掩码——每一步以一定概率把 token 替换成[MASK]最终整段序列全是 mask。反向过程就是模型看着当前带 mask 的序列预测哪些位置应该填什么 token。这里有个关键点反向去噪不是一步到位的而是分多个时间步迭代。假设总共 T 个去噪步模型在第 t 步看到的是一个部分被 mask 的序列它要预测出每个 mask 位置上的 token 分布。然后根据某种调度策略决定这一步“确认”哪些位置的预测、保留哪些位置继续 mask进入第 t1 步。这个过程一直持续到所有位置都被填满。所以一次完整的 dLLM 生成本质上是 T 轮迭代每轮都要跑一次完整的 forward。这和自回归模型“一次 forward 出一个 token”的节奏完全不同。理解了这一点你就能明白为什么 temperature 不能只改一行——它影响的是每一轮迭代里“从预测分布到实际填入 token”这个映射而这个映射的结果会作为下一轮的输入误差和随机性会沿着迭代链累积。2. argmax 在 dLLM 里到底扮演什么角色2.1 自回归里的 argmax 与 dLLM 里的 argmax 不是同一个东西在自回归模型里argmax 是最终输出决策。模型算出 logitsargmax 选出 token这个 token 直接进入输出序列然后作为下一步的输入。它的语义很清晰我要最可能的那个 token。在 dLLM 里argmax 出现在两个不同的地方语义完全不同。第一个地方是每步去噪时的 token 填充。模型对每个 mask 位置输出一个分布你需要从这个分布里选一个 token 填进去。这里可以用 argmax也可以用采样。第二个地方是置信度判断。很多 dLLM 的解码策略比如 confidence-based decoding会计算每个位置预测的置信度然后决定这一步要“解锁”哪些位置。置信度通常就是 max probability也就是 argmax 对应的概率值。这个值决定了去噪的调度节奏。问题来了如果你只把第一个地方的 argmax 换成采样第二个地方的置信度计算还是基于 argmax 的概率那么采样引入的随机性和调度逻辑就会打架。你采样出一个低概率的 token但调度器以为这个位置置信度很高直接把它锁定了后面再也没机会改。结果就是生成质量断崖式下跌。2.2 去噪迭代中的“确认”机制才是核心要理解 temperature 该怎么加得先理解 dLLM 的去噪调度。我用一个具体的例子来说明。假设序列长度 L8总去噪步数 T8。初始状态全是[MASK]。第 1 步模型看到 8 个 mask输出 8 个位置的 token 分布。假设调度策略是“每步确认置信度最高的 1 个位置”那么这一步只填 1 个 token其余 7 个保持 mask。第 2 步模型看到 1 个已填 token 7 个 mask再预测再确认 1 个。以此类推8 步之后全部填满。这个机制意味着每一步的采样决策只影响一个或少数几个位置但这个位置一旦被确认就不可逆。这和自回归的“每步出一个 token 且不可逆”有点像但区别在于 dLLM 每一步能看到全局信息所有未确认位置一起预测而且确认顺序是由置信度动态决定的不是从左到右。所以 temperature 的作用点就很明确了它应该作用在每个位置被确认时的 token 选择上而不是作用在置信度计算上。置信度应该始终用原始分布的 max probability因为它是调度器的依据不能被 temperature 扭曲。如果你把 temperature 也加到置信度上高温会让所有位置的置信度都变得差不多调度器就失去了判断依据可能一次性确认太多低质量位置生成直接崩掉。2.3 一个反直觉的结论temperature 不该改 argmax 本身很多人包括我一开始的想法是把 argmax 换成multinomial(softmax(logits / T))就完事了。但实测下来这样做有两个问题。第一argmax 在置信度计算里还要用。你如果把 argmax 那行代码直接替换掉置信度就没法算了。正确的做法是保留 argmax 用于计算置信度另外加一条采样路径用于实际填充 token。第二temperature 应该只作用于“被选中确认”的位置。如果你对所有位置都做采样包括那些这一步不打算确认的位置那采样结果就被浪费了而且可能干扰调度。更合理的做法是先用原始分布算出每个位置的置信度和 argmax token调度器决定这一步确认哪些位置然后只对这些位置用 temperature 采样出最终 token。这个区别在代码上可能只是几行的差异但在生成质量上差距巨大。我做过对比实验同样的模型、同样的 prompt全局采样 vs 仅确认位置采样后者的连贯性和多样性平衡明显更好。3. 在 SGLang 上落地 temperature 的完整思路3.1 为什么选 SGLang 而不是自己写推理循环自己写一个 dLLM 的推理循环其实不难几百行就能跑通。但一旦要上生产问题就来了batch 怎么组、不同请求的序列长度怎么对齐、去噪步数不同怎么调度、显存怎么复用。这些正是 SGLang 的强项。SGLang 的核心抽象是RadixAttention和continuous batching。它把请求组织成一棵树共享前缀的请求可以复用 KV cache。对于自回归模型这个前缀是“已经生成的 token 序列”。对于 dLLM前缀的概念需要重新定义——因为 dLLM 的中间状态是带 mask 的序列不同请求的 mask 模式不同共享前缀的条件更苛刻。但 SGLang 的调度框架本身是通用的它不关心你的模型是自回归还是扩散只要你把“一次前向计算”和“一次状态更新”定义清楚它就能帮你做 batching 和调度。所以我的思路是在 SGLang 的框架内实现 dLLM 的去噪循环把 temperature 作为采样策略的一个参数注入进去。3.2 关键改动点从 logits 到 token 的映射层在 SGLang 的推理流程里模型 forward 输出 logits 之后会经过一个 sampler 层决定最终 token。自回归模型的 sampler 是“选一个 token 追加到序列末尾”。dLLM 的 sampler 需要改成“对当前所有 mask 位置根据调度策略选择性地填充 token”。这个 sampler 层就是 temperature 的注入点。具体来说需要做三件事从 logits 计算每个 mask 位置的原始概率分布和置信度max probability。根据置信度和调度策略决定这一步确认哪些位置。对确认的位置用带 temperature 的采样从分布中选出 token未确认的位置保持 mask。这三步里第 1 步用原始 logits不加 temperature第 3 步才加 temperature。这个分离是保证生成质量的关键。3.3 调度策略的选择每步确认多少位置调度策略直接决定了生成质量和速度的权衡。常见的策略有几种固定步数总步数 T 固定每步确认 L/T 个位置。简单但不够灵活。置信度阈值每步确认所有置信度超过阈值的位置。阈值高则步数多、质量好阈值低则步数少、速度快。Top-K 确认每步确认置信度最高的 K 个位置。K 可以是固定值也可以随步数衰减。自适应根据当前已确认比例动态调整 K。我在实测里发现Top-K 加衰减是比较稳的方案。前期 K 大一点快速填充高置信位置后期 K 小一点精细调整。temperature 在这个策略下的作用是当 K 较大时采样多样性帮助避免早期错误固化当 K 较小时低温采样保证最终质量。这里有个经验值可以参考初始 K 设为 L/4每步衰减 0.9最低不低于 1。temperature 在前期设 1.0-1.2后期降到 0.6-0.8。这个组合在多个模型上表现都比较均衡。4. 代码层面的具体实现4.1 核心采样函数的改造假设你已经在 SGLang 里跑通了 dLLM 的基础推理现在有一个函数负责从 logits 生成下一步的 token 状态。改造前的伪代码大概是这样def decode_step(logits, current_tokens, mask_positions): # logits: [L, vocab_size] # 只关心 mask 位置 mask_logits logits[mask_positions] probs softmax(mask_logits, dim-1) confidence, pred_tokens probs.max(dim-1) # 简单策略全部确认 current_tokens[mask_positions] pred_tokens return current_tokens改造后def decode_step(logits, current_tokens, mask_positions, temperature, top_k): mask_logits logits[mask_positions] probs softmax(mask_logits, dim-1) confidence, pred_tokens probs.max(dim-1) # 调度选置信度最高的 top_k 个位置确认 if top_k len(mask_positions): _, selected_idx confidence.topk(top_k) else: selected_idx torch.arange(len(mask_positions)) selected_positions mask_positions[selected_idx] selected_logits mask_logits[selected_idx] # 只对确认的位置做带 temperature 的采样 if temperature 0: scaled_logits selected_logits / temperature sampled_tokens multinomial(softmax(scaled_logits, dim-1)) else: # temperature0 退化为 argmax sampled_tokens selected_logits.argmax(dim-1) current_tokens[selected_positions] sampled_tokens return current_tokens注意几个细节。第一confidence和pred_tokens始终用原始probs算不受 temperature 影响。第二top_k的选择基于原始置信度也不受 temperature 影响。第三只有最后一步采样用了 temperature。这个分离是整段代码的灵魂。4.2 temperature 与 top_k 的联动调参单独调 temperature 或单独调 top_k 都不够这两个参数是耦合的。我整理了一个调参对照表基于实际测试的经验值场景temperature初始 top_k衰减系数说明追求质量0.6-0.8L/60.95慢但稳适合最终输出平衡模式0.9-1.1L/40.9默认推荐追求速度1.2-1.5L/30.85快但可能重复创意生成1.3-1.6L/50.92多样性优先这里的 L 是序列长度。衰减系数指每步 top_k 乘以这个系数向下取整最低为 1。有个坑要注意temperature 太高超过 1.5时即使 top_k 很小采样出的 token 也可能偏离太远导致后续步骤的上下文变得奇怪模型预测质量下降。我试过 temperature2.0生成结果基本不可读。所以高温要配合更小的 top_k让每一步只确认最确定的位置把不确定性留给后续步骤。4.3 与 SGLang 调度器的对接SGLang 的调度器需要知道每个请求当前处于哪个去噪步、还有多少位置未确认。这些信息要作为请求的元数据传给调度器。具体来说需要在请求对象里加几个字段current_step当前去噪步数remaining_masks剩余未确认位置数temperature采样温度top_k_scheduletop_k 的衰减计划调度器根据remaining_masks决定这个请求还需要多少轮 forward从而决定是否继续留在当前 batch 里。当remaining_masks为 0 时请求完成可以释放资源。这里有个工程上的优化点不同请求的去噪进度不同有的快有的慢。如果严格按步同步快的请求要等慢的浪费算力。SGLang 的 continuous batching 允许动态加入和退出所以可以让完成的请求立即退出新请求立即加入。但 dLLM 的中间状态带 mask 的序列需要保留在显存里这对显存管理提出了更高要求。我的做法是给每个请求分配固定的中间状态缓冲区请求完成时回收避免频繁分配释放。5. 实测中遇到的坑与排查记录5.1 生成结果重复、循环这是最常见的症状。原因通常是 temperature 太低加上 top_k 太大导致早期确认了太多高置信但实际错误的 token后续步骤只能在错误上下文里打转。排查思路先看是不是 top_k 初始值太大。把初始 top_k 降到 L/6 试试。如果还重复检查 temperature 是不是低于 0.5。低于 0.5 时采样几乎退化为 argmax多样性丧失。建议 temperature 不要低于 0.6。另一个可能原因是置信度计算被 temperature 污染了。检查代码里confidence是不是从原始probs算的而不是从softmax(logits/temperature)算的。这个错误很隐蔽因为生成不会崩只是质量慢慢变差。5.2 生成结果发散、不连贯和重复相反这是 temperature 太高或 top_k 太小的症状。高温让每一步都采样出低概率 token上下文迅速偏离模型后续预测失去依据。解决办法降低 temperature 到 1.0 以下同时增大 top_k 让每步确认更多位置减少迭代次数。迭代次数少了误差累积的机会也少。还有一个容易被忽略的点去噪步数 T 和序列长度 L 的关系。如果 T 远小于 L每步要确认很多位置采样压力大容易出错。建议 T 至少等于 L/2让每步确认的位置数不超过 2 个。这样 temperature 的影响被限制在很小的范围内整体更稳定。5.3 不同请求之间相互干扰在 batch 推理时如果多个请求共享了不该共享的状态会出现一个请求的 temperature 影响到另一个请求的情况。这通常是因为采样用的随机数生成器没有按请求隔离。SGLang 里每个请求应该有独立的随机种子。检查你的 sampler 是不是用了全局的torch.manual_seed或者共享的 generator。正确做法是给每个请求分配一个torch.Generator用请求 id 作为种子初始化。这样即使 batch 在一起跑采样结果也是独立的。5.4 性能不升反降加了 temperature 和 top_k 调度之后如果发现推理速度反而变慢了大概率是因为 top_k 太小导致去噪步数增加。每步只确认 1 个位置L512 的序列就要 512 步每步一次完整 forward开销巨大。优化方向增大初始 top_k让前期快速填充后期再减小 top_k 做精细调整。另外可以设置一个最小确认数比如每步至少确认 2 个位置避免步数爆炸。实测下来L512 的序列总步数控制在 64-128 之间比较合理对应每步平均确认 4-8 个位置。6. 一些值得记录的工程经验6.1 temperature 的语义在 dLLM 里需要重新定义在自回归模型里temperature 的语义很直观控制下一个 token 的随机性。在 dLLM 里temperature 控制的是“每个被确认位置的 token 选择随机性”而确认哪些位置是由另一套逻辑决定的。这意味着同样的 temperature 值在 dLLM 里的效果和自回归里完全不同。我建议在文档里明确写清楚dLLM 的 temperature 只作用于确认位置的采样不影响调度。这样使用者才不会误以为调 temperature 能控制生成速度或长度。6.2 调度策略比 temperature 更重要折腾了几周之后我的最大体会是在 dLLM 里调度策略对生成质量的影响远大于 temperature。temperature 只是在既定调度下微调多样性而调度决定了整个去噪轨迹的形状。一个好的调度策略比如自适应 top_k 衰减能让 temperature 的作用变得很温和甚至用默认值 1.0 就能出不错的结果。反过来调度策略糟糕时怎么调 temperature 都救不回来。所以如果你刚开始做 dLLM 推理适配建议先把调度策略调好再考虑 temperature。顺序反了会浪费很多时间。6.3 关于 SGLang 源码阅读的一点建议SGLang 的代码结构比较清晰但 dLLM 相关的支持还在演进中。如果你想深入改建议从sampler和scheduler两个模块入手。sampler 负责 logits 到 token 的映射scheduler 负责请求的批次管理。temperature 的注入点在 sampler调度策略的实现在 scheduler。把这两个模块的接口摸清楚改动就有章可循。另外SGLang 的测试用例里有不少自回归模型的采样测试可以参照着写 dLLM 的测试。重点测边界情况temperature0、temperature 很大、top_k1、top_kL、序列全 mask、序列无 mask。这些边界跑通了基本逻辑就稳了。6.4 一个实用的小技巧分阶段 temperature如果不想搞复杂的调度可以用一个简单但有效的技巧分阶段 temperature。把去噪过程分成三段前 1/3 步用较高 temperature1.2中间 1/3 用中等1.0后 1/3 用较低0.7。这样前期探索、后期收敛不需要动态调整 top_k 也能有不错的效果。这个技巧的好处是实现简单只需要在循环里根据当前步数查表设置 temperature 即可。缺点是灵活性不如自适应调度但在快速原型阶段很好用。我在早期验证想法时就用这个方案半天就能跑通后面再逐步替换成更精细的调度。6.5 关于置信度归一化的一个细节有些实现会对置信度做归一化比如除以所有 mask 位置置信度的和。这个操作在 temperature 存在时要特别小心。归一化会改变置信度的相对大小可能影响 top_k 的选择。我的建议是置信度归一化只在调度阶段用且始终基于原始分布。采样阶段用未归一化的 logits 加 temperature两者不要混。如果确实需要归一化确保归一化系数是从原始 probs 算的而不是从 temperature 缩放后的 probs 算的。这个细节很容易写错而且错了之后症状不明显只是生成质量略微下降很难排查。7. 回到最初的问题“给扩散语言模型加一个 temperature为什么不是改一行 argmax”——现在答案应该清楚了。因为 dLLM 的 argmax 不只是输出决策它同时承担着置信度计算和调度依据的职责。temperature 只能作用于最终的 token 采样不能污染置信度和调度。这个分离需要在代码结构上明确体现而不是简单替换一行。从工程角度看这件事的复杂度不在于 temperature 本身而在于它牵出的整条链路采样策略、调度策略、批次管理、随机数隔离、性能调优。每一环都有坑每一环都需要实测验证。我这几周踩过的坑基本都写在上面了希望能帮到正在做类似适配的人。如果你也在折腾 dLLM 推理我的建议是先把调度策略跑通用一个固定的 temperature比如 1.0验证生成质量然后再逐步引入 temperature 调参。顺序对了事半功倍。另外多准备几组对比实验同样的 prompt 用不同参数跑肉眼对比生成结果比看任何指标都直观。扩散语言模型的评估指标目前还不成熟人眼判断反而是最可靠的。