AI/ML 详解:扩散语言模型

Day 57 · 2026-10-03
面向:有编程经验的非 AI 方向工程师 · 难度:前沿

掩码扩散语言模型Masked Diffusion Language Model

生成机制非自回归
一句话类比

GPT 这类自回归(autoregressive,逐个往后猜下一个词)模型像一个游标:fetch 一行、再 fetch 一行,第 i 行必须等第 i-1 行回来。掩码扩散换了个写法:先 INSERT 一条定长记录、所有字段全是 NULL,然后跑若干轮批量 UPDATE,每轮把最有把握的几个字段填成实值,直到没有 NULL。文本不是"写出来"的,是从一张空画布上显影出来的。

它解决什么问题 + 工作机制

痛点是一面顺序依赖的硬墙:自回归生成 N 个 token 就得做 N 次前向,因为第 i 个 token 的输入里含着第 i-1 个。加再多 GPU 也压不成 10 步——延迟被串行步数锁死,不是被算力锁死。

扩散的思路来自图像生成,但文本是离散的,加高斯噪声没有意义("猫"加一点噪声不会变成"狗")。D3PM(Austin et al., 2021,arXiv:2107.03006)给出了可用的离散版本:噪声不是抖动而是遗忘——前向过程按噪声水平 t 把一部分真 token 换成 [MASK],而 [MASK] 是吸收态(absorbing state,进去出不来);t=0 是原文,t=1 全是 mask。反向过程训一个 Transformer,去掉 causal mask——这是和 GPT 最本质的结构差别:它能同时看见左右两边,一次前向把所有 [MASK] 位置的分布全预测出来。

前向=遗忘(t: 0→1) 反向=显影(t: 1→0)

t=0.0 缓存击穿的解法 原文
t=0.4 缓■击■的■法 按比例 t 替换成 [MASK]
t=1.0 ■■■■■■■ 全 mask = 纯噪声

双向 Transformer→一次前向预测所有■ 的分布→定稿置信度最高的 k 个

训练目标更让人意外。MDLM(Sahoo et al., NeurIPS 2024,arXiv:2406.07524)证明这套扩散的 ELBO 可以化简成一个加权的 masked LM 损失:

L(θ) = Et~U(0,1] [ (1/t) · Σi∈被mask的位置 −log pθ(xi | xt) ]

逐项读:t 是本次采样的 mask 比例(即噪声水平);xt 是挖掉 t 比例后的序列;括号里就是"预测被挖掉的字"——和 BERT 的训练任务一模一样,只是 mask 比例每个样本随机抽,不是固定 15%。那个 1/t 权重是关键:t 很小时只挖掉几个字、上下文几乎全在,样本太容易、信号太稀疏,1/t 把它放大,让各噪声水平对总损失的贡献被摆平——否则模型只擅长"几乎没挖空"的简单情形,而采样恰恰从 t=1 开始。换句话说:训练扩散语言模型,就是训练一个能承受任意 mask 比例的 BERT。LLaDA(Nie et al., arXiv:2502.09992)用这条路把掩码扩散推到 8B,在 in-context learning 上做到与同规模 LLaMA3 可比。

代码示例
import torch
from transformers import AutoTokenizer, AutoModelForMaskedLM

name = "bert-base-chinese"          # 任何 masked LM 都能当"一步去噪器"
tok = AutoTokenizer.from_pretrained(name)
mdl = AutoModelForMaskedLM.from_pretrained(name).eval()

L, STEPS = 16, 8                    # 画布长 16,但只跑 8 步(AR 要 16 次前向)
ids = torch.full((1, L), tok.mask_token_id)   # t=1:整张画布全 [MASK]
ids[0, 0], ids[0, -1] = tok.cls_token_id, tok.sep_token_id

for s in range(STEPS):
    masked = (ids == tok.mask_token_id)
    if not masked.any(): break
    with torch.no_grad():
        logits = mdl(input_ids=ids).logits       # 双向:一次预测所有槽位
    prob, pred = logits.softmax(-1).max(-1)      # 每槽的最佳 token + 置信度
    prob[~masked] = -1                           # 已定稿的不再改(吸收态)
    k = max(1, int(masked.sum()) // (STEPS - s))  # 剩余 mask 均摊到剩余步
    for i in prob[0].topk(k).indices:            # 置信度最高的 k 个 → 写实
        ids[0, i] = pred[0, i]
    print(s, tok.decode(ids[0]))                 # 看文本从噪声里显影
常见误区 + 实践场景
"扩散语言模型就是把图像扩散的高斯噪声搬到文本上"——错。主流离散扩散的"噪声"是把 token 换成 [MASK],是遗忘而非抖动。它和 BERT 的血缘比和 Stable Diffusion 更近:BERT 一直是个只跑一步的扩散模型,只是当年没人这么用。
📌 BigCat 场景:本地跑一个能双向填空的小模型做模板/代码中段补全——给定函数签名和 return 语句,补中间的实现。自回归模型天生做不好这件事(它看不见右边),而掩码扩散的训练任务本身就是填空。
Takeaway + 思考题
💡 离散扩散把"逐个写"换成"整张画布多轮收敛",噪声=mask,训练=任意比例的 BERT。
🤔 如果生成不再有"顺序",那"流式输出 token"这个你用惯的 UX 契约还成立吗?

任意顺序生成与灵活性陷阱Any-Order Generation & the Flexibility Trap

采样机制反直觉
一句话类比

自回归像固定拓扑排序的任务调度:顺序写死,没得选。扩散像一个能任意挑任务的调度器:哪个槽位最有把握就先做。听上去是调度器的胜利——但真实调度器都有这个毛病:一直先捡简单任务,把关键路径节点拖到最后。等你终于去做它,周围全已定稿,选择空间被挤没了。

它解决什么问题 + 工作机制

掩码扩散的 ELBO 实际上是在所有生成顺序上取期望——它学的是一个 any-order(任意顺序)模型,自回归只是其中一种固定顺序的特例。采样时最常用的策略是 置信度优先解码(confidence-based unmasking):每轮把预测熵最低、也就是模型最笃定的几个槽位定稿。直觉上很合理:先确定能确定的,让它们给后面当上下文。

ICML 2026 Outstanding Paper《The Flexibility Trap》(Ni et al., arXiv:2601.15165)指出这里有个结构性毛病。推理任务里真正的决策点恰恰是高熵 token——"因此"/"所以"/"设 x =",它们熵高正是因为下面挂着好几条都合法的推理分支。置信度优先会系统性绕开它们,先把周边确定无疑的词填满;等绕回来,左右文已把这个位置夹死,几条分支只剩一条还说得通。

置信度优先解码如何绕开决策点

第1轮 ■■■熵高■■ → 先填两端最笃定的
第2轮 小明■■熵高■8个 → 继续填确定的
第3轮 小明有3+5■熵高一共8个 → 决策点最后才填
第4轮 小明有3+5所以一共8个 → 只剩一条路可走

后果:分支在被"考虑"之前就已经被上下文剪掉 → 解的多样性塌缩 → RL 探索不出更好的推理轨迹

作者的修法很简洁:RL 训练时强制按左到右顺序探索(把自回归顺序当训练脚手架),推理时完全保留双向注意力和并行解码。这套 JustGRPO 在 GSM8K 上 89.1%、MATH-500 上 45.1%(生成长度 256)。结论反直觉但干净:更大的假设空间配一个贪心的顺序选择器,可以比更小的假设空间更差。

代码示例
import torch
from transformers import AutoTokenizer, AutoModelForMaskedLM
tok = AutoTokenizer.from_pretrained("bert-base-chinese")
mdl = AutoModelForMaskedLM.from_pretrained("bert-base-chinese").eval()

M = tok.mask_token
# 中间两格留给"因此/所以"这类连接词,两端是确定的算式
text = f"小明有 3 个苹果又买了 5 个, {M} {M} 他一共有 8 个。"
ids = tok(text, return_tensors="pt").input_ids
with torch.no_grad():
    p = mdl(input_ids=ids).logits.softmax(-1)

ent = -(p * p.clamp_min(1e-9).log()).sum(-1)[0]   # 每个位置的预测熵
for i in (ids[0] == tok.mask_token_id).nonzero().flatten():
    top3 = tok.convert_ids_to_tokens(p[0, i].topk(3).indices)
    print(f"槽位{i.item()} 熵={ent[i]:.2f} 候选={top3}")
# 熵高的槽位 = 有多条合法分支的决策点;
# 置信度优先策略恰恰把它们排在最后 —— 这就是"灵活性陷阱"
常见误区 + 实践场景
"任意顺序一定不比固定顺序差,因为自回归是它的特例"——这是把表达能力和可优化性搞混了。模型类的包含关系只说"存在一组参数能做到",不保证"训练和采样能找到它"——和你见过无数次的"更通用的框架跑得更慢"同类。
📌 BigCat 场景:这个陷阱不只属于扩散模型。你让 AI agent 拆一个复杂任务时,它也倾向于先做"最有把握"的子任务,把真正含糊的核心决策(选哪个方案、定哪个口径)一路后推;等终于面对时,前面的产出已经把答案夹定了。对抗办法和 JustGRPO 一样:人为规定顺序,先逼它把最含糊的那一步定下来。
Takeaway + 思考题
💡 置信度优先解码会绕开高熵决策点,等绕回来时分支已被上下文剪掉——灵活性反而成了约束。
🤔 你自己的工作流里,有哪个"先做容易的"的习惯,实际上在悄悄替你砍掉选项?

块并行去噪Block Diffusion

架构延迟
一句话类比

自回归解码是教科书级的 N+1 查询问题:要 512 个 token 就发 512 次串行请求,每次付一个完整往返。纯扩散是另一极端:一次全发出去,但要重试很多轮,且每轮都得把整批重算。块扩散是中间那个所有 DBA 都会告诉你的答案——分页批处理:按 256 一页,页内并行、页间顺序。

它解决什么问题 + 工作机制

纯掩码扩散有两个硬伤,都不在数学上而在工程上。一、必须定长:得先 allocate 一张固定长度的画布,而真实请求的输出长度事先不知道。二、KV cache 失效:双向注意力意味着每个 token 都依赖所有其他 token,上一轮的 key/value 下一轮全部作废——自回归省时间的最大法宝没了。

Block Diffusion(Arriola et al., ICLR 2025,arXiv:2503.09573)两刀都解了:把序列切块,块之间自回归(前块已定稿,可缓存、可想生成多少块就多少块),块内部扩散(并行去噪)。块大小成了连续旋钮:块=1 退化成纯自回归,块=全长退化成纯扩散。

DiffusionGemma(Google DeepMind,2026-06,Apache 2.0 开放权重)走的就是这条路:基于 Gemma 4 的 26B 级 MoE、每步只激活约 4B 参数,一次并行去噪 256 token 的块。官方口径:解码比 Gemma 4 快约 4×,单张 H100 上超过 1000 tokens/s。

速度到底从哪来?全篇最值得记住的一句:延迟 ≈ NFE(网络前向次数)× 单次前向耗时。自回归的 NFE 等于 token 数,一步都省不掉;块扩散的 NFE = 块数 × 块内去噪步数,而块内步数远小于块长(256 个位置可能只跑三四十步)。注意它没减少 FLOPs——每次前向算的是整块 256 个位置,总计算量不降甚至略升。省掉的是"必须等上一个 token 回来"的那 N−1 次往返。

为什么这笔买卖划得来?因为自回归解码时 GPU 其实在等内存:batch=1 下每生成一个 token 都要把全部权重从显存搬进计算单元,算力大半空转(memory-bound)。块扩散把闲着的算力拿去一次处理整块,把 memory-bound 的解码推回 compute-bound——和"把 N+1 查询改成一条批量 SQL"同理:往返次数才是瓶颈。

512 token 的 NFE 账本(单次前向按 10ms 估)

纯自回归   512 次 ≈ 5.1s
块扩散(256块/32步)  64 次 ≈ 0.6s
纯扩散(全长/128步)  128 次,但无 KV cache 且定长

块1 扩散→定稿→KV 缓存→块2 扩散→定稿→KV 缓存→…
块间自回归(可缓存·可变长) 块内扩散(可并行)
这笔账是理想化的(8×);真实只到约 4×,因为块扩散每次前向更贵
代码示例
import math

def nfe_ar(n):                      # 自回归:一个 token 一次前向,无法并行
    return n

def nfe_block(n, block=256, steps=32):   # 块间顺序、块内并行去噪
    return math.ceil(n / block) * steps

N = 512
for fwd_ms in (10, 14):              # 单次前向耗时(MoE 稀疏激活后更小)
    a, d = nfe_ar(N), nfe_block(N)
    print(f"前向{fwd_ms}ms | AR {a}次={a*fwd_ms/1000:.2f}s | "
          f"块扩散 {d}次={d*fwd_ms/1000:.2f}s | 加速{a/d:.1f}x")

# 调 block/steps 看旋钮效果:block=1 就退化成自回归。
# 关键:块扩散每次前向算的是"整块 256 个位置",FLOPs 并没少,
# 省掉的是 N-1 次"必须等上一个 token"的往返。算力换延迟。
常见误区 + 实践场景
"快 4 倍就是便宜 4 倍"——不成立。降的是延迟(步数),不是算力(FLOPs):单请求变快,但每次前向都在算整块位置,GPU 做的功没少。成本账要落在吞吐和利用率上,不是单请求时延上。
📌 BigCat 场景:本地部署这类模型做"即时改写/补全"等人在等着的交互——延迟就是体验,快 4× 比准 5% 更值。反过来跑夜间批处理时优势几乎为零:那里你本来就能把 batch 堆满,GPU 早已 compute-bound。

工程对应 → super-individual D16 (Cost:稀疏激活与推理成本账)

Takeaway + 思考题
💡 延迟 ≈ NFE × 单次前向耗时;块扩散砍的是 NFE 不是 FLOPs,本质是用闲置算力换串行往返。
🤔 你负责过的系统里,有多少次"性能优化"其实只是把 N+1 往返改成一次批量?这件事在模型解码上刚刚才被做掉。

三方取舍:似然 · 可控性 · 延迟Likelihood / Controllability / Latency

对比诚实边界
一句话类比

自回归像单主强一致写入:顺序明确、可缓存、每步都看得见前面已提交的全部内容,代价是写入串行。扩散像多主并发写:吞吐高,但同一轮里各槽位互相看不见对方这轮写了什么,于是可能写出"每格单看都合理、合起来不一致"的组合。

它解决什么问题 + 工作机制

并行解码要付的根本税就藏在这个类比里:模型一次前向给出的是每个 mask 位置的边缘分布 p(xi | xt),而你一次定稿 k 个位置,等于悄悄假设这 k 个位置在当前上下文下条件独立。可语言里相邻位置强相关——"纽"和"约"各自单看都可能是别的字,一起看才只有一种读法。k 越大,这个假设离真实联合分布越远。所以"并行度 ↔ 质量"是一条真实存在的曲线,不是实现问题;靠置信度门限自适应控制 k,就是在这条曲线上找工作点。

三个轴上的真实位置

似然打分 自回归 精确链式分解 扩散 只有 ELBO 下界
双向可控 自回归 需专门训 FIM 扩散 填空是原生任务
解码延迟 自回归 NFE = token 数 扩散 NFE = 块数 × 步数

厂商公布的同代对比(DiffusionGemma vs Gemma 4)
MMLU Pro 77.6 vs 82.6  AIME 2026 69.1 vs 88.3
质量确实还有差距——这条路线目前买到的是延迟,不是能力

逐轴说清楚。似然:自回归的链式分解 log p(x) = Σ log p(xi|x<i) 是恒等式,给的是精确值,能直接用来打分、比困惑度、做重排序;扩散的顺序是随机的,只能给一个对顺序取期望的下界(ELBO),两者的 perplexity 不可直接比大小。可控性:扩散天生支持双向约束——给定开头和结尾填中间、固定格式槽位、从右往左补全,这些自回归要靠 fill-in-the-middle 专门训练才勉强做到;LLaDA 还报告它缓解了反转诅咒(reversal curse,学了"A 是 B"却答不出"B 是 A"),那正是单向训练留下的伤。延迟:扩散赢,代价是定长画布和 KV cache 重建。

什么时候不要用,说实话:需要精确似然去打分或比模型时;追求长链推理的极限正确率时(上面那组数字还在差距里);以及需要逐 token 流式输出时——块内 token 不按左到右定稿,"打字机效果"这个 UX 契约得重新设计。目前的主流判断不是替代,而是互补并融合:块扩散本身就是两极之间的插值,块大小就是那个滑块。

代码示例
import torch
from transformers import AutoTokenizer, AutoModelForMaskedLM
tok = AutoTokenizer.from_pretrained("bert-base-chinese")
mdl = AutoModelForMaskedLM.from_pretrained("bert-base-chinese").eval()

# 双向约束填空:前缀和后缀同时给定 —— 自回归看不见右边,天生做不到
M = tok.mask_token
prompt = f"缓存击穿的常见解法是 {M} {M} {M} {M} ,代价是写入延迟变高。"
ids = tok(prompt, return_tensors="pt").input_ids
slots = (ids[0] == tok.mask_token_id).nonzero().flatten().tolist()

for _ in range(len(slots)):           # 一次只定稿 1 个 → 绕开条件独立假设
    with torch.no_grad():
        p = mdl(input_ids=ids).logits.softmax(-1)[0]
    left = [i for i in slots if ids[0, i] == tok.mask_token_id]
    if not left: break
    best = max(left, key=lambda i: p[i].max().item())  # 最有把握的槽位先写
    ids[0, best] = p[best].argmax()
print(tok.decode(ids[0], skip_special_tokens=True))
# 把循环改成"一轮定稿全部 4 个",就能亲眼看到条件独立的代价
常见误区 + 实践场景
"扩散语言模型会取代自回归"——目前没有证据支持。两条路线在融合:块扩散是显式插值,而《Flexibility Trap》给出的最佳配方干脆是"训练用自回归顺序、推理用并行解码"。更可能的未来不是替代,而是块大小变成一个可调的部署参数。
📌 BigCat 场景:需要"填进固定模板"的任务(周报槽位、结构化字段、代码骨架补中段)可以试掩码扩散模型,而不是硬凑 prompt 让自回归模型别越界——双向约束在这里是模型的原生能力,不是你要和它搏斗的东西。

工程对应 → super-individual D16 (Cost:延迟、吞吐与成本的三角)

Takeaway + 思考题
💡 并行解码的根本税是"同一步内条件独立";扩散用似然精度和推理质量,换到了延迟和双向可控性。
🤔 如果块大小是个可调参数,你会按什么信号在线调它——输入长度、任务类型,还是用户在不在等?

深入资源

  • Austin et al., Structured Denoising Diffusion Models in Discrete State-Spaces (D3PM), arXiv:2107.03006 — 离散扩散的奠基工作,吸收态变体是今天掩码扩散的源头
  • Sahoo et al., Simple and Effective Masked Diffusion Language Models (MDLM), NeurIPS 2024, arXiv:2406.07524 — 把 ELBO 化简成加权 MLM 损失,是读懂训练目标的最短路径
  • Nie et al., Large Language Diffusion Models (LLaDA), arXiv:2502.09992 — 掩码扩散首次推到 8B 并与同规模 LLaMA3 对标,含反转诅咒的讨论
  • Arriola et al., Block Diffusion: Interpolating Between Autoregressive and Diffusion Language Models (BD3-LM), ICLR 2025 Oral, arXiv:2503.09573 — 块结构如何同时救回 KV cache 和可变长度
  • Ni et al., The Flexibility Trap: Rethinking the Value of Arbitrary Order in Diffusion Language Models, ICML 2026 Outstanding Paper, arXiv:2601.15165 — 任意顺序为何伤推理,以及 JustGRPO 的修法
  • Google DeepMind, DiffusionGemma(2026-06,Apache 2.0 开放权重)— 首个开放权重的文本扩散模型,官方模型卡与推理文档

深入思考

如果 BERT 一直是个"只跑一步的扩散模型",为什么 2018 年没人用它生成文本?
缺的不是直觉,是三件具体的东西。一、训练目标:BERT 固定 mask 15%,而采样必须从"全 mask"起步,模型从没见过高 mask 比例的输入,第一步就崩——MDLM 的随机 t 加 1/t 加权正是补这个。二、理论形式:需要把多步去噪写成一个有 ELBO 的生成过程,否则你不知道每步该定稿多少、该选哪些位置,这要等 D3PM。三、工程动机:那几年自回归的质量优势压倒一切,解码延迟还不值得换掉整个范式——是长输出和 agent 时代才把延迟推成一等公民。
卡 2 的"灵活性陷阱"和卡 4 的"条件独立税",是同一个问题吗?
不是,它们在不同的轴上。条件独立税是一步之内的空间误差:同时定稿 k 个位置,用边缘分布近似联合分布,k 降到 1 就消失。灵活性陷阱是跨步之间的策略偏差:即便每步只定稿一个位置、完全没有独立性误差,"按置信度挑位置"这个顺序本身就会把高熵决策点系统性后置,从而剪掉分支。证据正是 JustGRPO——它没降低并行度,只换了探索顺序就拿到提升。两者可以独立缓解,也可以同时存在。
为什么"给不出精确似然"比"benchmark 低几分"更值得在意?
因为精确似然是很多上层机制的地基,不只是一个指标。重排序(生成 n 条按 log p 选)、推测解码的接受判据、置信度校准与弃答、带评分的检索重排,都要一个可比的、精确的"这段文本有多可能"。换成 ELBO 下界,这些机制要么失效,要么引入一个你不知道多大的偏差——而下界的松紧本身随文本变化,同一个阈值在不同输入上含义就不同。benchmark 差几分能靠训练追;缺一个精确可比的概率量级,是范式层面的缺口。
"快 4× 但 FLOPs 没降"在什么条件下会从优势变成劣势?
当 GPU 已经 compute-bound 的时候。自回归 batch=1 解码是典型 memory-bound,算力大半空转,这时"多算一些、少走几趟"是纯赚。但把 batch 堆到几十上百(离线批处理、高并发服务),权重搬运已被多请求摊薄,GPU 真在满负荷算,扩散多算的那些位置就变成真实成本,吞吐可能反而下降。推论是:最优块大小取决于当下负载,所以它更像一个运行时参数,而不是模型超参。