高级文本生成 高级文本生成超越了朴素的自回归解码,旨在提升质量、可控性和速度。本文件涵盖文本扩散模型(D3PM、MDLM)、OCR、用于对齐的 RLHF 和 DPO、长上下文方法(RoPE 缩放、环形注意力)、检索增强生成,以及用于加速推理的投机解码。 标准的自回归生成(第 4 节)从左到右一次一个词元地产出文本。这简单有效,但它本质上是串行的,无法做全局规划,对输出的控制也很有限。本文件讲的是超越朴素自回归解码的那些方法:用于文本的扩散模型、光学字符识别、通过人类反馈实现的可控生成、处理长上下文、检索增强生成,以及用于加速推理的投机解码。 文本扩散模型(text diffusion models)把扩散框架(为图像引入于第 8 章)应用到离散文本上。
高级文本生成超越了朴素的自回归解码,旨在提升质量、可控性和速度。本文件涵盖文本扩散模型(D3PM、MDLM)、OCR、用于对齐的 RLHF 和 DPO、长上下文方法(RoPE 缩放、环形注意力)、检索增强生成,以及用于加速推理的投机解码。
标准的自回归生成(第 4 节)从左到右一次一个词元地产出文本。这简单有效,但它本质上是串行的,无法做全局规划,对输出的控制也很有限。本文件讲的是超越朴素自回归解码的那些方法:用于文本的扩散模型、光学字符识别、通过人类反馈实现的可控生成、处理长上下文、检索增强生成,以及用于加速推理的投机解码。
**文本扩散模型(text diffusion models)**把扩散框架(为图像引入于第 8 章)应用到离散文本上。核心挑战在于文本是离散的:你不能像给像素加噪声那样给词元加上连续的高斯噪声。有几种方法应对这一问题。
D3PM(Discrete Denoising Diffusion Probabilistic Models,Austin 等,2021)直接在离散词元上用转移矩阵定义了一个前向破坏过程。在前向的每一步,一个词元都有一定概率被替换成另一个词元(均匀噪声)、被掩蔽(吸收态),或保持不变。反向过程学习去噪,从被破坏的词元预测干净的词元。第 t 步的转移矩阵 Q_t 控制破坏:
第一项确保完全被破坏的分布与先验(均匀或全掩蔽)匹配。KL 项之和训练模型去逆转每一个破坏步骤:真实的反向后验 q(x_{t-1} \mid x_t, x_0) 可以用贝叶斯法则和已知的转移矩阵以闭式算出,而模型 p_\theta(x_{t-1} \mid x_t) 被训练去匹配它。
由于两个分布都是类别分布,KL 散度就是对词表各项的一个简单求和。最后一项衡量从被破坏最轻的状态重建的质量。
MDLM(Masked Diffusion Language Models,Sahoo 等,2024)用掩蔽作为唯一的破坏操作简化了 D3PM:前向过程逐步把词元替换成 [MASK] 词元,反向过程预测原始词元。这把文本扩散和掩码语言建模(BERT,第 4 节)联系起来,其中扩散时间步控制被掩蔽词元的比例。在 t = 0 时文本完全干净;在 t = T 时完全被掩蔽。
**连续文本扩散(continuous text diffusion)**通过在连续的嵌入空间中工作,绕开了离散问题。词元先被映射到它们的嵌入向量(第 6 章),在这个连续空间里加噪声,一个去噪模型(通常是 Transformer)学习逆转这个过程。生成时,模型产出连续向量,再通过找最近的嵌入映射回离散词元。挑战在于连续空间中的小误差可能映射到完全错误的词元,所以需要仔细的取整和钳位。
文本扩散的吸引力在于它能通过迭代精修同时生成所有词元,而不是从左到右。这带来了全局连贯性和方便的填充(在一段文字中间生成缺失部分),但目前的文本扩散模型在长篇文本的生成质量上仍落后于自回归模型。
文本 OCR(Optical Character Recognition,光学字符识别)是从图像中提取文本的任务。虽然传统上不被归入语言生成,但现代 OCR 系统与 NLP 深度集成,并越来越多地使用语言模型组件。
**场景文本检测(scene text detection)**在自然图像(路标、产品标签、车牌)中定位文本区域。这很有挑战性,因为野外文本出现在任意角度、尺度、字体下,而且背景杂乱。检测方法通常用 CNN 或 Transformer 主干来产出围绕文本区域的边界框或分割掩码。
CRNN(Convolutional Recurrent Neural Network,Shi 等,2017)是一种经典的文本识别架构。一个 CNN 从文本图像中提取视觉特征,特征图被切成一列列的列(每个水平位置一列),一个双向 LSTM 读取这个序列以建模上下文。输出用 CTC(Connectionist Temporal Classification,连接时序分类)解码,它处理输入列与输出字符之间的对齐,不需要显式的分割。
CTC 解决的根本问题是:模型产出 T 个输出分布(每个输入列一个),但目标文本只有 L \leq T 个字符。
我们不知道哪些列对应哪些字符。CTC 引入一个空白词元(blank token) \epsilon,并定义一个多对一映射 \mathcal{B},它坍缩重复字符并去掉空白:\mathcal{B}(\text{"HH-ee-ll-ll-oo"}) = \text{"Hello"}(其中 "-" 是空白)。
目标序列 y 的概率是所有能坍缩到 y 的输入对齐路径之和:
其中 \pi 是长度为 T 的一条对齐路径(每列一个标签,包括空白)。朴素地对所有路径求和是指数级的,但前向算法(第 5 章 HMM)用动态规划在 O(T \cdot L) 时间内高效地算出这个和。
空白词元至关重要:没有它,"Hello" 里像 "ll" 这样的重复字符就和单个 "l" 无法区分。训练最大化 \log P(y \mid x),推理时则通过束搜索或在 CTC 输出上贪心解码找到最佳路径。
**文档 OCR(document OCR)**处理结构化文档(发票、表格、科学论文),除了识别字符,还必须理解版面。像 LayoutLM 这样的现代系统把文本识别和空间位置特征结合起来:每个词元同时拿到它的文本嵌入和一个编码其在页面上 (x, y) 坐标的位置嵌入。这让模型能理解:出现在 "Total:" 下面的数字就是总金额。
**视觉-语言 OCR(vision-language OCR)**模型如 TrOCR 把文本识别当作图像到文本的生成:一个 Vision Transformer 编码器处理图像,一个语言模型解码器逐字符地生成文本。这利用了预训练视觉和语言模型的威力,无需手工特征工程就能处理各种文字、字体和版面。
**可控生成(controllable generation)**是引导语言模型产出具有期望属性的输出的挑战:某种特定的风格、主题、情感、安全级别或事实准确性。模型应当遵循指令,同时保持流畅和连贯。
**无分类器引导(classifier-free guidance,CFG)**用于文本时借鉴了图像生成的一项技术。训练时,条件信号(例如提示)有一定比例被随机丢弃,从而在同一个模型里同时训练条件和无条件两种情况。推理时,输出 logits 被插值:
其中 w > 0 放大条件的影响。w 越大,输出越紧随提示,但多样性降低。
RLHF(Reinforcement Learning from Human Feedback,Ouyang 等,2022)是把语言模型与人类偏好对齐的主流方法。这个过程有三个阶段:
第一,有监督微调(supervised fine-tuning,SFT):在一个由人类撰写的高质量提示-回复数据集上微调基础语言模型。
第二,奖励模型训练(reward model training):收集人类比较(给定提示 x 和两个回复 y_1, y_2,哪个更好?),训练一个奖励模型 r_\phi(x, y) 来预测人类偏好。奖励模型用成对排序损失训练:
其中 y_w 是被偏好的回复,y_l 是不被偏好的回复。
第三,强化学习微调(RL fine-tuning):优化语言模型以最大化奖励,同时贴近 SFT 模型(以防止模式崩溃)。这用 PPO(Proximal Policy Optimisation,第 6 章)加一个 KL 惩罚:
这在数学上等价于 RLHF,但把奖励模型和 RL 训练坍缩成了单步有监督训练。
sigmoid 里那个表达式可以这样读:"相对于参考模型,提高被偏好回复的相对概率,降低不被偏好回复的相对概率。"
\beta 参数控制策略能偏离参考模型多远。在实践中,DPO 更容易实现(只需为两个回复分别算出当前模型和参考模型下的对数概率),并避免了 PPO 训练的不稳定性。
Constitutional AI(Bai 等,2022)把对齐过程的一部分自动化了。它不收集人类比较,而是用语言模型自己按照一套原则("宪法")来批判和修订自己的输出,比如"选择那个危害较小的回复"。然后用 AI 生成的比较来做偏好训练(RLAIF:RL from AI Feedback,来自 AI 反馈的强化学习)。
**长上下文方法(long-context methods)**应对的是标准自注意力 O(n^2) 的内存和计算开销,正是它限制了序列长度。当 n 增长到数万乃至数十万词元时,标准注意力就变得不可行。
稀疏注意力(sparse attention)用一个稀疏模式取代稠密的 n \times n 注意力矩阵,其中每个词元只注意到其他词元的一个子集。常见模式包括局部注意力(local attention)(每个词元注意到固定大小的邻域窗口)、跨步注意力(strided attention)(注意到每第 k 个词元)、随机注意力(random attention)(注意到一个随机子集)。这些模式的组合(用于 BigBird、Longformer)在 O(n) 或 O(n \sqrt{n}) 复杂度下,仍能捕捉局部和全局依赖。
**滑动窗口注意力(sliding window attention)**把每个词元限制为只注意到它之前的 w 个词元(它的局部窗口)。这是 O(nw) 而不是 O(n^2),但长程信息必须通过跨层的重叠窗口层层传递。有 L 层、窗口大小 w 时,有效感受野是 L \times w 个词元。
**环形注意力(ring attention)**把长序列分布到多个设备上,把它们排列成环形拓扑。每个设备持有序列的一块,在为本块计算注意力的同时,把键值块发给环里的下一个设备。这把计算和通信重叠起来,允许任意长度的序列(上限是所有设备的总内存,而不是任何单个设备的内存)。
**记忆增强模型(memory-augmented models)**通过给 Transformer 配备一个外部记忆库来扩展上下文。在每一层,模型可以用注意力从这个记忆库读取和写入。Memorizing Transformers 缓存之前各块的键值对,并在后续块中注意到它们,从而有效地把上下文扩展到训练窗口之外。检索是近似的(对缓存的键做 k 近邻),以保持高效。
上面这些是长上下文的架构解决方案。同样重要的是模型如何被训练来有效使用长上下文。
渐进式上下文扩展(progressive context extension)是标准做法。从一开始就在很长的序列上训练代价过高(O(n^2) 的注意力开销),所以模型在短的上下文长度(通常 4K-8K 词元)上预训练,然后再通过继续预训练分阶段扩展到目标长度。
Llama 3.1 在 8000 亿词元上从 8K 扩展到 128K,序列长度逐步增加。DeepSeek-V3 在 4K 上训练,然后扩展到 32K,再到 128K。
每个阶段用的词元数适中(相对于完整的预训练预算),因为模型只需要学会如何使用更长的位置,而不需要重新学习语言本身。
扩展时必须调整位置编码。**RoPE 插值(RoPE interpolation)**把位置索引缩小,使模型看到与训练时相同的旋转角度,只是铺展到更长的序列上。如果模型在长度 L 上训练,而你想扩展到 L' = 4L,就把所有位置索引除以 4。
这意味着模型永远不会遇到没见过的旋转角度,但相邻位置之间的有效分辨率下降了。
**RoPE 外推(RoPE extrapolation)**保持原始位置索引不变,直接对超出 L 的位置应用 RoPE,依赖模型泛化到未见过的角度。
插值要稳定得多;外推在不调整基频(ABF)的情况下会迅速退化。
YaRN(Yet another RoPE extensioN)通过认识到并非所有 RoPE 维度都应一视同仁,改进了朴素的插值。
高频维度(\theta_i = \theta_{\text{base}}^{-2i/d} 中 i 较小)在训练长度内旋转很多次,能很好地外推。
低频维度(i 较大)旋转缓慢,对长度扩展更敏感。
YaRN 只对低频维度做插值,对高频维度做外推,并对注意力 logits 施加一个温度缩放 t,以补偿分布的偏移:
其中 t > 1 把注意力分布压平,防止模型在位置信号被压缩时过尖锐地注意到附近的词元。
**长上下文数据策展(long-context data curation)**是一个关键却常被低估的挑战。大多数预训练语料由短文档构成(新闻文章、网页、社交媒体帖子)。
长上下文训练需要一个真正能锻炼整个上下文窗口的数据混合:书籍、代码仓库、长篇科学论文、多轮对话日志,以及把主题相关的文档拼接在一起。
如果模型只在被填充或打包到上下文窗口的短文档上训练,它就会学会忽略远处的词元,因为它们从来都不相关。
**序列打包(sequence packing)**是一种训练效率技术:把多个文档拼接成一条训练序列以避免填充浪费,并用注意力掩码阻止跨文档注意力。
对长上下文训练来说,打包策略很关键:把许多不相关的短文档打包在一起,会教模型把远处的词元当噪声;而打包更少、真正长的文档,则教会它使用完整的上下文。
一个已知的失败模式是**"迷失在中间"(lost in the middle)**现象(Liu 等,2023):语言模型倾向于有效利用上下文窗口开头和结尾的信息,却难以处理放在中间的信息。
这类似于人类记忆中的系列位置效应(首因和近因)。
它部分源于训练数据分布(重要信息常常在文档的开头或结尾),部分源于注意力模式集中于附近和起始的词元。
用关键信息多样放置的长上下文训练可以缓解但无法完全解决这一问题。
**大海捞针(needle-in-a-haystack)**评测测试的是模型能否检索出被放在一段长长的干扰上下文("草堆")中各个位置上的某个具体事实("针")。
一个具有真正长上下文能力的模型,无论针放在哪里,都应该达到近乎完美的检索。
这个测试清楚地揭示了迷失在中间的效应,被用来给上下文扩展方法打分。
预训练之后的**长上下文微调(long-context fine-tuning)**使用有针对性的 SFT 数据:长多轮对话、证据散布在数千词元中的文档问答、长篇摘要,以及仓库级别的代码理解。
Qwen3 在这一阶段使用双块注意力(Dual Chunk Attention,DCA),它把长序列作为成对的块来处理:块内注意力是完整的,块间注意力是高效的,在微调期间实现了 4 倍的有效序列容量。
**状态空间模型(State Space Models,SSM)**为长序列建模提供了一条根本不同的路线。它不是修改注意力,而是用一个受连续时间控制理论启发的线性动力系统完全取代注意力。
一个 SSM 通过一个由下式支配的潜在状态 x(t) \in \mathbb{R}^N,把输入序列 u(t) 映射到输出 y(t):
其中 A \in \mathbb{R}^{N \times N} 是状态转移矩阵,B \in \mathbb{R}^{N \times 1} 是输入投影,C \in \mathbb{R}^{1 \times N} 是输出投影,D 是跳跃连接。
要把它应用到离散序列(词元)上,连续系统要用一个步长 \Delta 离散化。零阶保持离散化给出:
离散递推就变成 x_k = \bar{A} x_{k-1} + \bar{B} u_k,y_k = C x_k + D u_k,看起来就像一个 RNN:用一个隐藏状态一次处理一个词元。
与 RNN 不同,这个递推也可以展开为一个全局卷积:因为系统是线性的,输出为 y = \bar{K} \ast u,其中核 \bar{K} = (C\bar{B}, \, C\bar{A}\bar{B}, \, C\bar{A}^2\bar{B}, \ldots) 只依赖于固定的参数。
这种双重视角——用递推做高效的自回归推理(每步 O(1))、用卷积做高效的并行训练(经 FFT 为 O(n \log n))——正是 SSM 的核心洞见。
S4(Structured State Spaces for Sequence Modeling,Gu 等,2022)通过解决关键的数值挑战让 SSM 变得实用:状态矩阵 A 必须捕捉长程依赖,但朴素地参数化它会导致梯度消失或爆炸(和朴素 RNN 同样的问题)。
S4 用 HiPPO(High-order Polynomial Projection Operators)矩阵初始化 A,它源自连续信号最优多项式逼近的理论。HiPPO 矩阵有一种特定的结构,可证明能让状态以优雅的衰减维持整个输入历史的压缩表示:
A_{nk} = -\begin{cases} (2n+1)^{1/2}(2k+1)^{1/2} & \text{if } n > k \\ n+1 & \text{if } n = k \\ 0 & \text{if } n < k \end{cases}
这种下三角结构确保状态作为输入信号的一种在线逼近,使用勒让德多项式。计算长核的 \bar{A}^k 很昂贵,所以 S4 利用 HiPPO 矩阵可分解为低秩项和对角项之和这一事实,实现 O(n \log n) 的核计算。
Mamba(Gu 和 Dao,2023)引入了**选择性状态空间(selective state spaces)**这一关键创新:让 SSM 的参数依赖于输入。在 S4 中,矩阵 A、B、C 以及步长 \Delta 都是固定的——对每个词元都施加同样的动力学,与内容无关。Mamba 把 B、C、\Delta 变成输入的函数:
这种选择性让模型能在每个位置决定:什么信息要存入状态、什么要忽略——类似于注意力选择相关词元的方式,但没有平方开销。步长 \Delta_k 控制"门":大的 \Delta 让状态强烈地积分当前输入(连续动力学前进一大步,实际上是重置状态),小的 \Delta 则保留现有状态、忽略当前输入。
代价是依赖输入的参数打破了卷积视角(核不再固定),所以 Mamba 不能用基于 FFT 的训练。它转而用一个**硬件感知的并行扫描(hardware-aware parallel scan)**算法,利用递推的结合律:状态更新 (x_k, u_k) \mapsto x_{k+1} 可以表示成一串结合性运算,并用前缀和(扫描)并行化,类似于硬件设计中的并行前缀加法。它在 GPU 上以 O(n) 时间、O(\log n) 深度运行,几乎匹配卷积的效率。
Mamba 实现了真正每词元 O(1) 的推理(只更新固定大小的状态,没有随上下文增长的 KV 缓存),使它在长序列长度上从根本上比 Transformer 更省内存。状态大小 N(通常为 16)远小于 Transformer 的 KV 缓存,后者存储 O(n \cdot d) 个值。在实践中,Mamba 在相同参数量下的语言建模基准上匹配或超过 Transformer 质量,并在长序列上推理显著更快。
**混合架构(hybrid architectures)**把 SSM 层和注意力层结合起来,多数层用 SSM(高效的长程传播),再穿插少量注意力层(精确的基于内容的检索)。像 Jamba 和 Zamba 这样的模型交错排列 Mamba 和 Transformer 块,在保持大部分推理效率优势的同时,取得了比纯 SSM 更好的质量。这暗示注意力和 SSM 捕捉的是互补的能力:SSM 擅长平滑的长程状态传播,而注意力擅长精确的、依赖内容的查找。
**检索增强生成(Retrieval-Augmented Generation,RAG)**通过让语言模型在推理时访问外部知识库,来解决它的知识局限性。RAG 不只依赖训练期间编码进模型参数的知识,而是检索相关文档并以它们为条件来生成。
经典的**检索器-阅读器架构(retriever-reader architecture)**有两个组件。**检索器(retriever)**接收一个查询,从语料中取回 top-k 个最相关的段落。阅读器(reader)(一个语言模型)以查询和检索到的段落共同为条件生成答案。检索器可以用稀疏方法(BM25,它扩展了第 2 节的 TF-IDF)或稠密方法。
**稠密段落检索(Dense Passage Retrieval,DPR)**用双编码器架构:一个编码器把问题映射成向量,另一个把段落映射成向量。两者通常都基于 BERT。索引时,所有段落都被编码并存储。查询时,问题被编码,再用近似最近邻搜索(如 FAISS)找到最近的段落。相似度度量是问题向量与段落向量的点积。
**分块策略(chunking strategies)**显著影响检索质量。文档必须被切成检索器能处理的段落,但又不能小到容纳不了一个完整想法。固定大小分块(例如 256 个词元、50 词元重叠)简单但可能生硬地切断句子。语义分块在段落或小节边界处切分。层次化分块在不同粒度上构建一棵摘要树。
RAG 有几个优势:知识库可以在不重训模型的情况下更新;模型可以引用来源;因为模型能把答案奠基在检索到的文本上,幻觉减少了。主要挑战是检索质量(如果检索到错误的段落,模型可能自信地给出错误答案)和延迟(检索给推理增加了一步)。
**投机解码(speculative decoding)通过用一个又小又快的草稿模型(draft model)并行地提议多个词元,再由大的目标模型(target model)**在一次前向传播中验证它们,来加速自回归生成。
算法是这样工作的:草稿模型自回归地生成 k 个候选词元(这很快,因为草稿模型小)。
然后目标模型在一次前向传播中同时给所有 k 个词元打分(这很高效,因为工作是批处理的)。
对每个从草稿分布 p_d(t) 采样的候选词元 t,它以概率 \min(1, \, p_{\text{target}}(t) / p_d(t)) 被接受。如果被拒绝,就从调整后的分布 p_{\text{adj}}(t) = \max(0, \, p_{\text{target}}(t) - p_d(t))(归一化后)重新采样一个修正词元。
这个接受-拒绝方案保证了输出分布与单独用目标模型完全相同。
要看明白为什么,考虑发射词元 t 的有效概率。它可以直接被接受(概率 p_d(t) \cdot \min(1, p_{\text{target}}(t)/p_d(t))),也可以通过重新采样产生。
对 p_{\text{target}}(t) \leq p_d(t) 的词元,直接接受贡献 p_{\text{target}}(t)。对 p_{\text{target}}(t) > p_d(t) 的词元,直接接受贡献 p_d(t),重新采样贡献余下的 p_{\text{target}}(t) - p_d(t)(在考虑了拒绝概率之后)。
在两种情况下,发射 t 的总概率都等于 p_{\text{target}}(t)。草稿模型只影响速度,不影响质量。
加速取决于接受率:如果草稿模型与目标模型对齐得好,大多数词元都被接受,墙上时间大致就是草稿模型的时间。典型的加速是 2-3 倍,且没有质量损失。
Medusa(Cai 等,2024)走了一条不同的路:它不用单独的草稿模型,而是给目标模型本身加上多个轻量级预测头。每个头同时预测一个不同的未来词元位置(k = 1, 2, 3, \ldots 步之后)。每一步,Medusa 用一个树结构提议若干候选续写,目标模型的注意力层一次前向传播就验证哪些候选是一致的。这完全避免了单独的草稿模型。
更广泛地说,**并行生成(parallel generation)**方法旨在打破自回归解码的串行瓶颈。Jacobi 解码用猜测初始化所有位置,然后并行迭代精修直到收敛,把生成当作不动点迭代。非自回归模型(NAT)在单次前向传播中同时生成所有词元,但通常会遭受质量退化,需要迭代精修、CTC 损失或从自回归教师蒸馏等技术来缩小差距。
上面这些技术——对齐、长上下文、检索、高效解码、状态空间模型——在 modern 生产级 LLM 中汇合到了一起。
本节余下部分综述前沿模型中的架构创新,展示第 1-4 节的理论思想和上面的方法是如何在实践中结合的。
**分组查询注意力(Grouped Query Attention,GQA)**是被最广泛采用的注意力效率技术。标准的多头注意力(MHA)为每个头维护单独的键和值投影,每个词元需要缓存 n_{\text{heads}} \times d_{\text{head}} 个值。GQA 把多个查询头分组,让它们共享一个键值头。
在 64 个查询头和 8 个 KV 头的配置下(Llama 3、Qwen、Gemma 中的常见配置),每个 KV 头被 8 个查询头共享,相比 MHA 把 KV 缓存减少了 8 倍。
输出质量与 MHA 几乎相同,因为查询仍然可以关注不同的模式,只是共享同一个键值子空间。多查询注意力(Multi-Query Attention,MQA)是所有查询共用单个 KV 头的极端情形,但 GQA 提供了更好的质量-效率折中。
多头潜在注意力(Multi-head Latent Attention,MLA),在 DeepSeek-V2 中引入,实现了更激进的 KV 缓存压缩。它不再缓存完整的键值投影(即便用了 GQA),而是把隐藏状态降维到一个低秩的潜在向量(latent vector) c_t \in \mathbb{R}^{d_c},其中 d_c \ll n_{\text{heads}} \times d_{\text{head}}:
只缓存这个压缩向量。注意力时,完整的键和值表示通过升维重建:k_t = W_{\text{up}}^K c_t,v_t = W_{\text{up}}^V c_t。在 DeepSeek-V3(总参数 671B、激活 37B)中,压缩维度 d_c = 512,而完整 MHA 是 128 \times 128 = 16{,}384,KV 缓存减少了 93%。
一个微妙之处:标准 RoPE 依赖于位置,与共享压缩不兼容,所以 MLA 使用解耦 RoPE(decoupled RoPE):查询和键的一小股单独支路(每个头 64 维)通过 RoPE 承载位置信息,而表示的主体部分则流经压缩的潜在路径。
大规模下的位置编码已经与原始的正弦方案大相径庭。所有前沿模型都用 RoPE(第 4 节),但针对长上下文做了关键修改。原始 RoPE 公式 \theta_i = \theta_{\text{base}}^{-2i/d} 中的基频 \theta_{\text{base}} 通常是 10,000,这限制了超出训练长度的外推。
**调整基频(Adjusted Base Frequency,ABF)**就是把 \theta_{\text{base}} 提高到 500,000(Llama 3)或 1,000,000(Qwen3、Gemma 3),拉伸旋转周期,使模型在训练期间经历更少的完整旋转,从而能外推得更远。
YaRN(Yet another RoPE extensioN)施加依赖频率的插值:低频维度被插值(缩小),高频维度被外推,再用一个温度因子调整注意力分布。DeepSeek-V3、Qwen 和 Kimi K2 都使用基于 YaRN 的扩展,从在 4K-8K 上预训练的模型达到 128K 上下文。
iRoPE(interleaved RoPE,交错 RoPE),在 Llama 4 中引入,走了一条更激进的路:每第 4 层注意力完全不使用位置编码(NoPE),其余层使用带分块注意力的标准 RoPE。
NoPE 层可以在没有任何位置偏置的情况下关注所有位置,而 RoPE 层提供局部排序。再结合推理时的温度缩放,这让 Llama 4 Scout 拥有 1000 万词元的上下文窗口——比任何纯 RoPE 方案都高出几个数量级。
大规模混合专家已经成为前沿模型的主导架构(第 4 节介绍了 MoE 基础)。关键设计选择是专家数量、路由稀疏度和负载均衡。
路由稀疏度差异显著:DeepSeek-V3 用 256 个专家加 top-8 路由(32 倍稀疏),Qwen3 用 128 个专家加 top-8(16 倍稀疏),Mixtral 用 8 个专家加 top-2(4 倍稀疏),Llama 4 Maverick 用 128 个专家加 top-1 再加一个共享专家(128 倍稀疏)。
稀疏度更高意味着在相同激活计算量下总参数更多,但需要更细致的负载均衡和通信基础设施。
无辅助损失的负载均衡(auxiliary-loss-free load balancing)(DeepSeek-V3)取代了传统的负载均衡损失(第 4 节),后者被发现会损害模型质量。取而代之的是,每个专家维护一个动态偏置项,每个训练步调整:过载的专家把偏置调低(接收更少词元),欠载的专家把偏置调高。这在不让任何辅助损失污染主训练信号的情况下实现了均衡路由。
**共享专家(shared experts)**出现在大多数 MoE 设计中:一个或多个无论路由如何都处理每个词元的专家 FFN。它们处理所有词元都需要的常见模式(基本句法、功能词),把被路由的专家解放出来去专门化。Llama 4 用 1 个共享专家加 1 个路由专家(非常稀疏);DeepSeek-V3 用 1 个共享加 8 个路由。
稠密层与 MoE 层交替提供了另一个设计维度。Gemma 2 和 3 交替使用局部/全局注意力层(Gemma 3 中比例 5:1,局部层用 1024 词元的滑动窗口,只有全局层缓存完整的 128K 上下文)。
Llama 4 Maverick 在稠密 FFN 层和 MoE 层之间交错。Kimi K2 使用混合稀疏度层(在专家层之间穿插一个稠密层)。这种异构设计让不同层能承担不同功能。
多词元预测(Multi-Token Prediction,MTP),用于 DeepSeek-V3,训练模型不仅预测下一个词元,还预测再下一个。在每个位置,一个次要预测模块(共享主模型的嵌入)预测一个额外的未来词元。MTP 损失相对于主下一词元损失加权为 0.1-0.3。除了在训练期间改善表示质量,MTP 头还可以在推理时作为投机解码的草稿头,提供免费的加速。
**知识蒸馏(knowledge distillation)**是一种训练策略,由一个大的"教师"模型的输出来指导一个更小的"学生"模型的训练。Gemma 2 和 3 大量使用蒸馏:较小的模型(2B、4B)在 50 倍于计算最优量的数据上训练,以教师的概率分布作为软目标。这就是为什么 Gemma 3-4B 在质量上能匹配 Gemma 2-27B。
蒸馏损失取代或补充标准的交叉熵:学生最小化其输出分布与教师分布之间的 KL 散度:
DeepSeek-R1 用 80 万条精选的思维链样本,把它 671B 的推理模型蒸馏成小到 1.5B 的稠密模型,产出了推理能力强得出奇的小模型。
基于强化学习的推理代表了 LLM 能力最近最重要的进展。DeepSeek-R1 证明,在基础模型上做纯强化学习(不经过有监督微调)就能催生思维链推理、自我验证和错误纠正——这些行为在模型因正确最终答案而获得奖励时会自发涌现。
DeepSeek-R1 使用 GRPO(Group Relative Policy Optimisation,组相对策略优化),它去掉了 PPO 所需的价值网络。对每个提示,GRPO 采样一组 G 个输出,计算它们的奖励,并在组内归一化优势:
策略梯度随后用这些组相对优势配合一个裁剪目标(类似于 PPO 的裁剪)。
去掉 critic 网络把 RL 训练的内存和计算需求减半,让用 RL 训练 671B 参数模型变得可行。
一个关键设计选择:DeepSeek-R1 使用基于规则的奖励(rule-based rewards)(把数学答案对照标准答案、运行代码测试用例),而不是神经奖励模型,因为发现在这个规模下神经奖励模型容易被 reward hacking。
Qwen3 的混合思考模式把推理(用 <think> 标签做逐步思维链)和快速直接回复集成进同一个模型,让用户可以控制一个"思考预算",在延迟和推理深度之间权衡。
这是通过在思考和非思考数据上共同训练实现的,而不是通过分离的模型检查点。
大规模下的训练稳定化需要超越常规做法的新技术。Logit 软顶(logit soft-capping)(Gemma 2)把注意力分数过一遍 s \cdot \tanh(\text{logits} / s),软顶 s(通常 30-50)防止无界增长。
QK-Norm(Qwen3)在计算注意力分数之前对查询和键向量施加 RMSNorm,取代了对 QKV 偏置的需求。QK-Clip(Kimi K2 的 MuonClip 优化器)在训练期间监控最大注意力 logit,并在其超过阈值时重新缩放查询-键权重矩阵,从而实现 1T 参数模型的稳定预训练,零不稳定事件。
FP8 混合精度训练(DeepSeek-V3)在前向和反向传播中计算密集的矩阵乘法时使用 8 位浮点,同时把主权重保持在更高精度。
这相比 BF16/FP16 训练大约把吞吐量翻倍,而质量损失可以忽略。DeepSeek-V3 只用 280 万 H800 GPU 小时就训练完了它的 671B 参数模型——只是可比模型的一小部分——这在很大程度上归功于这项技术和其他工程优化。
import jax.numpy as jnp import math from collections import Counter # 知识库:一组短段落 knowledge_base = [ "The Eiffel Tower is a wrought-iron lattice tower in Paris, France. It was constructed from 1887 to 1889 as the centerpiece of the 1889 World's Fair.", "The Great Wall of China is a series of fortifications built along the northern borders of China. Construction began in the 7th century BC.", "Photosynthesis is the process by which plants convert sunlight, water, and carbon dioxide into glucose and oxygen using chlorophyll.", "The theory of general relativity, published by Albert Einstein in 1915, describes gravity as the curvature of spacetime caused by mass and energy.", "Python is a high-level programming language known for its simple syntax and readability. It was created by Guido van Rossum and released in 1991.", "The mitochondria are organelles found in eukaryotic cells. They generate most of the cell's supply of ATP, used as a source of chemical energy.", ] # 构建 TF-IDF 索引(复用第 2 节的概念) def tokenise(text): return text.lower().split() vocab = sorted(set(w for doc in knowledge_base for w in tokenise(doc))) word2idx = {w: i for i, w in enumerate(vocab)} V = len(vocab) N = len(knowledge_base) # 文档频率 doc_freq = Counter() for doc in knowledge_base: for w in set(tokenise(doc)): doc_freq[w] += 1 def tfidf_vector(text): words = tokenise(text) counts = Counter(words) vec = jnp.zeros(V) for w, c in counts.items(): if w in word2idx: tf = 1 + math.log(c) idf = math.log(N / (doc_freq.get(w, 0) + 1)) vec = vec.at[word2idx[w]].set(tf * idf) return vec # 索引所有文档 doc_vectors = jnp.stack([tfidf_vector(doc) for doc in knowledge_base]) def cosine_sim(a, b): return jnp.dot(a, b) / (jnp.linalg.norm(a) * jnp.linalg.norm(b) + 1e-8) def retrieve(query, top_k=2): """为查询检索 top-k 个最相关的段落。""" q_vec = tfidf_vector(query) sims = jnp.array([cosine_sim(q_vec, doc_vectors[i]) for i in range(N)]) top_indices = jnp.argsort(-sims)[:top_k] return [(int(i), float(sims[i]), knowledge_base[int(i)]) for i in top_indices] # 测试检索 queries = [ "Who built the Eiffel Tower?", "How do plants make food?", "What did Einstein discover?", ] for query in queries: results = retrieve(query, top_k=1) print(f"\nQuery: '{query}'") for idx, sim, passage in results: print(f" Retrieved (sim={sim:.3f}): '{passage[:80]}...'") # RAG 风格的提示构造 context = results[0][2] rag_prompt = f"Context: {context}\n\nQuestion: {query}\nAnswer:" print(f" RAG prompt:\n {rag_prompt[:120]}...")
import jax import jax.numpy as jnp # 模拟草稿模型(快,精度较低)和目标模型(慢,精度高) vocab_size = 8 seq_len = 5 key = jax.random.PRNGKey(42) # 目标模型:给定序列返回 logits def target_model(seq, key): """模拟的目标模型:产生词元 logits(昂贵)。""" # 实际中这会是一次大型 Transformer 前向传播 k1, k2 = jax.random.split(key) logits = jax.random.normal(k1, (len(seq), vocab_size)) * 2 # 让它有一定可预测性:偏向词元 (seq[-1] + 1) % vocab_size for i in range(len(seq)): logits = logits.at[i, (seq[i] + 1) % vocab_size].add(3.0) return logits def draft_model(seq, key): """模拟的草稿模型:类似但噪声更大(便宜)。""" k1, k2 = jax.random.split(key) logits = jax.random.normal(k1, (len(seq), vocab_size)) for i in range(len(seq)): logits = logits.at[i, (seq[i] + 1) % vocab_size].add(2.0) return logits def sample_token(logits, key): return jax.random.categorical(key, logits) def speculative_decode(prefix, draft_steps=3, key=jax.random.PRNGKey(0)): """投机解码:草稿提议,目标验证。""" seq = list(prefix) total_accepted = 0 total_proposed = 0 for _ in range(4): # 生成 4 轮 key, *subkeys = jax.random.split(key, draft_steps + 3) # 草稿模型提议 draft_steps 个词元 draft_tokens = [] draft_probs = [] draft_seq = list(seq) for i in range(draft_steps): d_logits = draft_model(jnp.array(draft_seq), subkeys[i]) d_probs = jax.nn.softmax(d_logits[-1]) tok = sample_token(d_logits[-1], subkeys[i]) draft_tokens.append(int(tok)) draft_probs.append(d_probs) draft_seq.append(int(tok)) # 目标模型一次前向传播给所有草稿词元打分 target_logits = target_model(jnp.array(draft_seq), subkeys[draft_steps]) target_start = len(seq) - 1 # 最后一个前缀词元的位置 # 接受/拒绝每个草稿词元 accepted = 0 for i in range(draft_steps): t_probs = jax.nn.softmax(target_logits[target_start + i]) d_prob = draft_probs[i][draft_tokens[i]] t_prob = t_probs[draft_tokens[i]] # 以概率 min(1, target_prob / draft_prob) 接受 accept_prob = jnp.minimum(1.0, t_prob / (d_prob + 1e-10)) key, accept_key = jax.random.split(key) if jax.random.uniform(accept_key) < accept_prob: seq.append(draft_tokens[i]) accepted += 1 else: # 拒绝:从调整后的分布采样 key, resample_key = jax.random.split(key) adjusted = jnp.maximum(0, t_probs - draft_probs[i]) adjusted = adjusted / (adjusted.sum() + 1e-10) new_tok = jax.random.categorical(resample_key, jnp.log(adjusted + 1e-10)) seq.append(int(new_tok)) break total_accepted += accepted total_proposed += draft_steps return seq, total_accepted, total_proposed # 运行投机解码 prefix = [0, 1] result_seq, accepted, proposed = speculative_decode(prefix) acceptance_rate = accepted / proposed if proposed > 0 else 0 print(f"Prefix: {prefix}") print(f"Generated sequence: {result_seq}") print(f"Draft proposals: {proposed}") print(f"Accepted: {accepted}") print(f"Acceptance rate: {acceptance_rate:.1%}") print(f"Speedup potential: {(accepted + proposed) / proposed:.2f}x")
import jax import jax.numpy as jnp # 极小的语言模型:从 one-hot 到 logits 的线性投影 vocab_size = 10 seq_len = 4 key = jax.random.PRNGKey(42) k1, k2 = jax.random.split(key) # 当前策略参数(可训练) theta = jax.random.normal(k1, (vocab_size, vocab_size)) * 0.1 # 参考策略参数(初始 theta 的冻结副本) theta_ref = theta.copy() def log_prob_sequence(params, sequence): """在一个简单的自回归模型下计算 log P(sequence)。""" total = 0.0 for t in range(1, len(sequence)): # 简化:位置 t 的 logits 依赖于 t-1 处的词元 logits = params[sequence[t-1]] log_probs = jax.nn.log_softmax(logits) total += log_probs[sequence[t]] return total def dpo_loss(theta, theta_ref, preferred, dispreferred, beta=0.1): """一对样本的直接偏好优化损失。""" log_pi_w = log_prob_sequence(theta, preferred) log_pi_l = log_prob_sequence(theta, dispreferred) log_ref_w = log_prob_sequence(theta_ref, preferred) log_ref_l = log_prob_sequence(theta_ref, dispreferred) # DPO 目标 return -jax.nn.log_sigmoid( beta * ((log_pi_w - log_ref_w) - (log_pi_l - log_ref_l)) ) # 偏好数据集:(提示前缀, 偏好补全, 不偏好补全) preferences = [ (jnp.array([1, 3, 5, 7]), jnp.array([1, 3, 5, 2])), # 结尾偏好 7 而非 2 (jnp.array([0, 2, 4, 6]), jnp.array([0, 2, 4, 9])), # 偏好 6 而非 9 (jnp.array([3, 3, 3, 3]), jnp.array([3, 3, 3, 0])), # 偏好重复而非 0 (jnp.array([5, 6, 7, 8]), jnp.array([5, 6, 7, 1])), # 偏好 8 而非 1 ] grad_fn = jax.jit(jax.grad(dpo_loss)) lr = 0.05 print("Training DPO...") for epoch in range(100): total_loss = 0.0 for preferred, dispreferred in preferences: loss = dpo_loss(theta, theta_ref, preferred, dispreferred) grads = grad_fn(theta, theta_ref, preferred, dispreferred) theta = theta - lr * grads total_loss += loss if (epoch + 1) % 20 == 0: avg_loss = total_loss / len(preferences) print(f" Epoch {epoch+1}: avg DPO loss = {avg_loss:.4f}") # 检查:训练后模型应该更偏好那些被偏好的补全 print("\nPreference check after DPO training:") for preferred, dispreferred in preferences: lp_w = log_prob_sequence(theta, preferred) lp_l = log_prob_sequence(theta, dispreferred) print(f" Preferred {list(preferred.astype(int))}: logP={lp_w:.3f} " f"Dispreferred {list(dispreferred.astype(int))}: logP={lp_l:.3f} " f"{'correct' if lp_w > lp_l else 'WRONG'}")