斯坦福-CS336-从零构建大语言模型笔记-全-
斯坦福 CS336 从零构建大语言模型笔记(全)



课程01:大语言模型基础与分词技术 🧠


在本节课中,我们将要学习大语言模型的基础知识,并深入探讨其构建流程中的第一个关键环节:分词技术。我们将了解为什么需要分词,以及如何通过字节对编码(BPE)算法将文本转换为模型可以处理的整数序列。




概述
语言模型是当今人工智能领域的核心。要理解并构建它们,我们需要从最基础的部分开始。本节课将介绍课程的整体目标,并聚焦于文本处理的第一步——分词。我们将看到,一个高效的分词器对于模型的性能和效率至关重要。

课程背景与目标
我们正面临研究人员与底层技术脱节的危机。过去,研究人员会自行实现并训练模型,而现在许多人仅通过提示专用模型来工作。虽然这开启了大量研究,但理解底层技术对于开展基础研究仍然必要。本课程的理念是“要理解就得构建”。

然而,语言模型的产业化带来了挑战。前沿模型规模巨大、训练成本高昂,且构建细节往往不公开。因此,本课程将构建小型语言模型,并专注于传授三种知识:
- 事物运作的原理:例如Transformer架构和模型并行。
- 一种心态:充分利用硬件、重视扩展的思维方式。
- 部分直觉:关于数据和建模决策的直觉,尽管在小尺度与大尺度下可能不同。
正确的思路是:在给定的计算资源和数据预算下,构建最佳模型。这意味着要最大化算法效率。
课程结构简介
课程围绕“效率”这一核心原则,分为五个单元:

- 基础:实现分词器、模型架构并进行训练。
- 系统:深入优化,发挥硬件极致性能。
- 缩放定律:通过小规模实验预测大规模超参数和损失。
- 数据:探讨数据获取、整理、评估及其对模型能力的决定性影响。
- 对齐:通过监督微调、从反馈中学习等技术,使基础模型变得有用、安全、符合指令。
分词技术详解





上一节我们介绍了课程的整体框架,本节中我们来看看构建流程的第一个具体步骤:分词。

分词是将原始文本(Unicode字符串)转换为整数序列的过程,每个整数代表一个“词元”(Token)。模型接收并处理这些整数序列。


为什么需要分词?
模型无法直接理解文本字符。我们需要一种将文本数字化并表示成固定维度向量的方法。分词器就是完成这个映射的组件。



简单的分词方法及其问题
以下是几种简单的分词方法及其局限性:
- 基于字符的分词:将每个Unicode字符映射为其码点(整数)。
- 问题:词汇表可能很大(约14万),且对罕见字符的利用效率低。压缩率(每个词元代表的字节数)不理想。
- 基于字节的分词:将文本转换为UTF-8字节序列,每个字节(0-255)作为一个词元。
- 问题:序列过长。因为每个词元只代表1个字节,压缩率为1,导致计算效率低下(注意力机制计算量与序列长度成二次方关系)。
- 基于单词的分词:使用正则表达式将文本拆分成单词或片段。
- 问题:词汇表近乎无限,会遇到未知词(UNK)问题,影响模型性能评估。
字节对编码(BPE)
BPE是一种在1994年提出的数据压缩算法,后被应用于自然语言处理。它能在原始文本上“训练”出一个分词器,自动地将频繁共现的字节对合并为新的词元。
BPE算法核心步骤:
- 将输入文本转换为字节序列(基础词汇表为0-255)。
- 统计所有相邻字节对的出现频率。
- 找到出现频率最高的字节对(A, B)。
- 创建一个新的词元C,将其加入词汇表。
- 在训练数据中,将所有出现的(A, B)对替换为C。
- 重复步骤2-5,直到达到预设的合并次数或词汇表大小。

通过这个过程,常见的字符序列会被合并成单个词元,而罕见序列则由多个词元表示,从而实现了自适应和高效的压缩。
一个简单的BPE合并示例:
假设要对“cat”和“heat”进行编码。
- 初始字节序列(示例):
[99, 97, 116, 104, 101, 97, 116](对应 ‘c’, ‘a’, ‘t’, ‘h’, ‘e’, ‘a’, ‘t’) - 统计发现,字节对
(116, 104)(即 ‘t’ 和 ‘h’)出现频率高。 - 创建新词元256,代表 ‘th’。序列变为:
[99, 97, 256, 101, 97, 256] - 继续合并下一个高频对,例如
(256, 101)(即 ‘th’ 和 ‘e’)合并为 ‘the’(词元257)。 - 最终序列变短,压缩率提高。
在实际实现中(如GPT-2),通常会先进行“预分词”,使用基于规则的方法(如正则表达式)将文本初步拆分成片段(如按空格、标点),然后在每个片段上独立运行BPE算法,这有助于提升效率和处理能力。
总结


本节课中我们一起学习了构建大语言模型的起点。我们了解了本课程的目标是深入理解技术原理,并树立重视效率与扩展的思维方式。我们重点探讨了分词技术,认识到简单的基于字符、字节或单词的方法各有缺陷,而字节对编码(BPE) 算法提供了一种自适应、高效的分词方案。它通过统计语料库中字节对的共现频率,逐步构建词汇表,从而在压缩率和序列长度之间取得良好平衡,为后续的模型训练奠定了基础。在接下来的课程中,我们将基于分好的词元序列,深入模型架构的内部世界。


课程10:推理 (Inference) 🚀
在本节课中,我们将学习语言模型推理的核心概念。推理是指使用一个已经训练好的固定模型,根据给定的提示(prompt)生成回应(response)。我们将探讨推理的含义、其带来的计算挑战,以及如何通过各种技术来加速推理过程。




概述:什么是推理?
推理是语言模型实际应用的核心环节。当你与聊天机器人对话、使用代码补全工具,或者让模型处理批量数据任务时,都需要进行推理。此外,评估模型性能(如指令遵循能力)、进行测试时计算(如思维链推理),甚至在使用强化学习进行训练时对响应进行采样,都离不开推理。因此,推理是支撑语言模型多种功能的基础。

推理的效率至关重要。虽然训练是一次性的大规模投入,但推理会反复进行无数次。例如,OpenAI每天生成约1000亿个单词,代码补全工具Cursor每天生成大量代码行。这些数据表明,推理成本在模型总成本中的占比正在不断上升。


衡量推理效率的指标
评估推理性能主要有三个关键指标:

- 首次标记时间 (TTFT):用户从发出请求到收到模型生成的第一个标记(token)所需的时间。这对交互式应用(如聊天)的体验至关重要。
- 延迟 (Latency):在收到第一个标记后,后续标记的生成速度。这也直接影响交互体验。
- 吞吐量 (Throughput):系统在单位时间内能够生成的总标记数量。这对批处理任务非常重要。
高吞吐量不一定意味着低延迟,因为系统可能通过处理大量并发请求来实现高吞吐量,但单个请求的完成时间可能较长。

推理的挑战:为何它如此困难?

上一节我们介绍了衡量推理的指标,本节我们来看看推理面临的核心挑战。与训练不同,推理(特别是生成阶段)有一个关键特征:必须按顺序生成标记。

在训练Transformer时,我们可以对整个输入序列进行并行计算。但在自回归生成时,要生成下一个标记,必须依赖于之前生成的所有标记。这种顺序依赖性使得我们无法像训练那样充分利用并行计算资源。
更重要的是,这种顺序生成模式使得计算受限于内存带宽,而非GPU的计算能力。我们接下来会通过“算术强度”这个概念来深入理解这一点。
核心概念:算术强度与内存瓶颈
要理解推理为何受内存限制,我们需要引入“算术强度”这个概念。算术强度衡量的是每从内存中读取1字节数据,能执行多少次浮点运算。
我们以一个简单的矩阵乘法为例:计算 X * W,其中 X 的形状是 (B, D),W 的形状是 (D, F)。
- 浮点运算次数 (FLOPs) ≈
2 * B * D * F - 内存读写字节数 (Bytes) ≈
2 * B * D + 2 * D * F + 2 * B * F(读取X和W,写入结果) - 算术强度 (Arithmetic Intensity) =
FLOPs / Bytes≈B(当D和F远大于B时)
算术强度 B 在这里就是批量大小(batch size)。
对于像NVIDIA H100这样的GPU,其“加速器强度”(峰值算力/内存带宽)约为300。这意味着:
- 如果算术强度
B > 300,计算受限于GPU的算力(计算受限)。 - 如果算术强度
B < 300,计算受限于从内存中读取数据的速度(内存受限)。

在推理的生成阶段,我们通常一次只生成一个标记(或为少量并发用户生成),即 B 很小(例如1)。这使得算术强度极低(约等于1),远低于GPU的加速器强度。因此,GPU强大的计算能力无法被充分利用,大部分时间都在等待数据从内存中读取出来,从而造成了内存瓶颈。

Transformer推理分解:预填充 vs. 生成

理解了内存瓶颈的根源后,我们具体分析Transformer的推理过程。它分为两个阶段:

- 预填充阶段:给定一个提示(长度为S),模型并行计算整个提示,并生成第一个输出标记的概率。同时,它会计算并缓存每个Transformer层中注意力机制所需的键(Key)和值(Value)向量,形成KV缓存。
- 生成阶段:基于已生成的标记和KV缓存,自回归地生成后续标记。每生成一个新标记,只需计算其对应的新查询(Query)向量,并结合KV缓存计算注意力,然后更新缓存。
KV缓存是内存消耗的大头。其大小公式为:
KV缓存大小 = 2 * 批次大小(B) * 序列长度(S) * 层数(L) * 注意力头数(N) * 头维度(H) * 2字节(bf16)
对于生成阶段(T=1):
- MLP层的算术强度 ≈
B。提升并发请求数(B)可以提高计算效率。 - 注意力层的算术强度 ≈
1。这与批次大小B无关,因为每个序列的KV缓存是独立的,无法通过批处理共享内存读取。这导致了固有的内存瓶颈。


结论:预填充阶段通常是计算受限的,可以高效并行。而生成阶段是内存受限的,速度受限于KV缓存的读写速度。

理论分析:延迟、吞吐量与内存的权衡



基于上述分析,我们可以对推理的延迟和吞吐量进行理论估算。我们以Llama 2 13B模型在H100 GPU上为例。


我们需要考虑两部分内存占用:
- 模型参数:13B参数,以bf16格式存储,约占26GB。
- KV缓存:大小由公式决定,与批次大小B和序列长度S成正比。





延迟主要由将所需数据(参数+KV缓存)传输到GPU计算核心的时间决定,即 总数据量 / 内存带宽。
吞吐量是单位时间内处理的标记总数,约为 B / 延迟。


通过计算可以发现:
- 小批量(B=1):延迟很低(8毫秒/标记),但吞吐量也低(124标记/秒)。适合追求低延迟的交互场景。
- 大批量(B=256):延迟升高,但吞吐量大幅提升。适合批处理任务。
- 权衡与极限:吞吐量随B增长,但受GPU内存容量限制(无法无限增大B),并且延迟也会增加。此外,简单增加模型副本(数据并行)可以线性提升吞吐量且不增加延迟。




优化策略一:减小KV缓存大小(无损/微损)



既然KV缓存是内存瓶颈的关键,最直接的优化思路就是减小它。以下是几种架构层面的改进方法:


1. 分组查询注意力
GQA将多个查询头(Query Head)分组,共享一个键值头(Key-Value Head)。这显著减少了需要存储的KV向量数量,从而减小缓存。例如,Llama 2/3 大型模型就采用了GQA。在保持精度接近的同时,能大幅提升吞吐量和降低延迟。


2. 多头潜在注意力
MLA将原始的键值向量投影到一个低维空间(如从16000维降至512维)再进行存储和计算。这是DeepSeek-V2采用的技术,能极大压缩KV缓存大小。

3. 跨层注意力
CLA让Transformer不同层共享同一套键值投影权重。这相当于在层维度上压缩了KV缓存,也是一种有效的优化手段。




4. 局部注意力与稀疏注意力
仅关注最近的部分历史标记(滑动窗口),或采用块稀疏注意力。这能直接将KV缓存大小从与序列长度S相关变为与固定窗口大小相关,特别适合长序列。Mistral、Character.ai等模型采用了此类技术。



这些方法都是在模型架构设计时引入的,旨在从源头降低推理对内存的需求。





优化策略二:突破Transformer架构




更激进的优化是设计全新的、天生高效的架构,以绕过Transformer自回归+全注意力的根本瓶颈。

1. 状态空间模型
SSM(如Mamba)使用线性递归机制替代注意力,理论上可以实现恒定的KV缓存(与序列长度无关)和线性计算复杂度。Mamba及其变体已在语言建模上展现出与Transformer媲美的性能,同时推理速度更快。




2. 线性注意力
通过数学变换,将标准的Softmax注意力近似为线性计算,从而避免序列长度的二次方复杂度。像Minimax的Abel模型等已成功将线性注意力扩展到数百亿参数规模。




3. 扩散模型用于文本
与自回归逐个生成不同,扩散模型尝试并行生成所有标记,然后通过多轮迭代逐步优化。这完全打破了自回归的顺序瓶颈,在代码生成等任务上已展现出极高的吞吐量潜力。



这些新架构代表了推理加速的前沿方向,它们从第一性原理出发,试图重新定义文本生成的方式。



优化策略三:量化与模型减枝

除了改变架构,我们也可以对已有的模型进行“瘦身”。
1. 量化
将模型权重和激活值从高精度(如FP16)转换为低精度(如INT8、INT4)。这能直接减少内存占用和带宽压力,从而提升速度。常用技术包括:
- 训练后量化:对训练好的模型直接量化,可能需要校准来减少精度损失。
- 量化感知训练:在训练过程中模拟量化效应,使模型更能适应低精度。


2. 模型减枝
移除模型中不重要的部分(如权重、神经元、注意力头)。流程通常是:评估重要性 -> 剪枝 -> 对剪枝后的模型进行微调或知识蒸馏以恢复精度。这可以产生更小、更快的模型。

量化与减枝是有损压缩,需要在速度和精度之间做出权衡。





优化策略四:推测性解码



推测性解码是一种巧妙的“无损加速”方法。它利用了一个观察:用小模型(草稿模型)生成多个候选标记的速度,远快于用大模型(目标模型)自回归生成一个标记;而用大模型并行验证这些候选标记的速度,又远快于它自己生成。

算法流程:
- 使用快速但能力较弱的草稿模型,自回归地生成一长串候选标记(γ个)。
- 使用强大但缓慢的目标模型,并行地对这整段候选序列进行计算,得到每个位置“真实”的概率分布。
- 通过一个精心设计的接受/拒绝算法(基于两个模型的概率),逐个检查候选标记。如果被接受,则白嫖了一个快速生成的标记;如果被拒绝,则从目标模型的修正分布中采样一个,然后停止本轮推测。
- 重复此过程。


这种方法的美妙之处在于,它严格保证了输出分布与直接使用目标模型生成是完全一致的,同时又能获得显著的加速(加速比取决于草稿模型与目标模型的匹配程度)。Medusa、EAGLE等工作是该方向的进一步发展。



系统优化:服务与调度

在实际部署中,请求是动态、实时到达的,长度各异,完成时间也不同。这需要复杂的系统调度。

1. 连续批处理
传统的批处理需要等一批请求都准备好才开始,会造成资源闲置。连续批处理允许动态地将新到达的请求加入正在运行的批次中,并让已完成的请求离开,从而最大化GPU利用率。


2. 页面注意力
受到操作系统虚拟内存管理的启发,vLLM提出了页面注意力。它将KV缓存分割成固定大小的“块”,像管理内存页一样管理它们。这可以有效解决由于请求长度可变和生命周期不同导致的内存碎片化问题,提升内存利用率高达数倍。


这些系统级优化对于高并发、高效率的推理服务至关重要。




总结



本节课我们一起深入探讨了语言模型推理的方方面面:

- 推理的重要性与挑战:推理是模型应用的核心,其顺序生成特性导致严重的内存瓶颈,与训练截然不同。
- 效率指标:我们学习了衡量推理的TTFT、延迟和吞吐量指标。
- 理论分析:通过算术强度概念,我们理解了生成阶段受内存限制的根本原因,并分析了延迟、吞吐量与内存的权衡关系。
- 优化策略:我们系统性地学习了四大类优化技术:
- 减小KV缓存:GQA、MLA、CLA、局部注意力等架构改进。
- 新架构探索:状态空间模型(Mamba)、线性注意力、扩散模型,旨在从根本上改变生成范式。
- 模型压缩:量化和减枝,在精度和速度间权衡。
- 无损加速:推测性解码,利用草稿模型进行“猜测-验证”来实现加速。
- 系统部署:连续批处理和页面注意力等系统优化,是构建高效推理服务的关键。



推理是一个融合了模型架构、算法设计和系统工程的广阔领域。随着大模型应用的普及,推理优化将继续是研究和工程实践的热点。



斯坦福 CS336 课程笔记:从零构建 ChatGPT - P11:缩放定律2 🚀

在本节课中,我们将深入探讨缩放定律的实际应用。我们将通过分析几个现代大语言模型的案例研究,了解构建者如何在设计过程中运用缩放定律。此外,我们还将详细解析一种名为“最大更新参数化”的技术,它旨在使模型的超参数在不同规模下保持稳定,从而简化扩展过程。


案例研究:现代大语言模型的扩展策略

上一节我们介绍了缩放定律的基本概念和动机。本节中,我们来看看现代大语言模型构建者如何在实际中应用这些定律。我们将重点分析三个模型:Cerebras-GPT、MiniCPM 和 DeepSeek。它们各自采用了不同的扩展策略组合。

Cerebras-GPT:验证最大更新参数化

Cerebras-GPT 是早期对缩放定律进行严谨研究的模型之一。该模型使用 Chinchilla 算法进行训练,参数规模在 1 亿到 130 亿之间,并遵循了最优的 token 与参数比例。


该团队的一个核心发现是,通过采用 最大更新参数化,模型的扩展变得更加稳定和可预测。以下是他们的关键验证结果:
- 标准参数化 vs. MP:在测试损失对比中,采用 MP 的模型扩展曲线比标准参数化更平滑,性能至少与 GPT-J 等模型相当。
- 稳定超参数:使用 MP 后,模型在预测的缩放点附近不再出现大幅震荡,使得超参数调整更容易,并能更准确地达到预测性能。
这个案例是对 MP 方法的早期公开验证,表明正确的参数化和初始化对于稳定扩展至关重要。
MiniCPM:激进的扩展与创新调度

MiniCPM 的目标是使用大量算力训练出优秀的小型语言模型。他们在扩展过程中进行了大量精确计算,并同样使用了 MP 来稳定扩展。

以下是他们采用的核心策略:

- 超参数选择:在小规模代理模型上进行广泛的超参数搜索,然后利用 MP 的稳定性将结果扩展到大规模模型。
- WSD 学习率调度:团队推广了 热身-稳定-衰减 学习率调度方法。与传统的余弦调度不同,WSD 的“稳定”阶段是平坦的,允许在单次训练运行中通过“回滚并重新衰减”来高效地评估不同数据量下的模型性能,成本接近于单次训练。
- 数据缩放分析:利用 WSD,他们能够高效地进行 Chinchilla 风格的数据缩放分析。他们发现最优的 token 与参数比例远高于 Chinchilla 提出的 20:1,达到了 192:1,这表明通过精心优化可以大幅超越早期的经验法则。


DeepSeek:直接的缩放定律拟合
DeepSeek 在扩展策略上采取了更直接的方法。他们没有依赖 MP,而是直接通过实验拟合缩放定律来确定最优超参数。
他们的做法如下:

- 网格搜索:在不同规模的小模型上,对批量大小和学习率进行网格搜索,以确定最优值。
- 外推定律:他们发现最优批量大小和学习率随计算规模(FLOPs)的变化呈现可预测的趋势。通过拟合这些趋势线,他们可以将最优超参数外推到计划训练的大型模型(如 70B 和 670B 参数)。
- 复用成熟方法:他们同样采用了 Chinchilla 风格的 FLOPs 分析来确定模型规模与数据量之间的最优权衡,并使用了 WSD 风格的调度来降低成本。





DeepSeek 的结果表明,即使不采用 MP,通过严谨的缩放定律分析也能实现可预测且成功的模型扩展。


深入理解:最大更新参数化

从上述案例中我们看到,控制学习率等超参数在不同规模下的稳定性是一个核心问题。接下来,我们深入探讨 最大更新参数化 背后的数学原理。
MP 旨在实现两个目标:
- 初始化稳定:当网络宽度增加时,各层激活值的范数应大致保持恒定,既不爆炸也不消失。
- 更新稳定:在初始化后进行一步梯度更新时,激活值的变化量也应大致保持恒定。

MP 的推导概要
考虑一个简化的深度线性网络。我们希望其激活值在初始化时稳定。

- 初始化条件:对于第
l层的权重矩阵W_l,我们使用高斯初始化,其标准差σ_l需要精心选择。 - 推导结果:为了满足激活值稳定的条件,应选择
σ_l = 1 / sqrt(fan_in),其中fan_in是该层的输入维度。这正是何凯明初始化的思想。

我们希望单步梯度更新后的激活值变化也稳定。

- 更新条件:这涉及到学习率
η的选择。 - 推导结果:通过分析梯度更新大小与损失变化的关系,可以推导出,对于 Adam 优化器,每层的最佳学习率应缩放为
η_l ∝ 1 / fan_in。这与标准参数化中通常使用全局恒定学习率的做法不同。

MP 的实践要点
总结 MP 的指导方针:


- 初始化:权重初始化的标准差设为
1 / sqrt(fan_in),并根据fan_in和fan_out的比例进行微调。 - 学习率:对于 Adam 优化器,每层的学习率应设为与
1 / fan_in成正比。

在 Cerebras-GPT 的实践中,他们正是按照宽度(fan_in)的倒数来缩放每层的初始化权重和学习率。


MP 的实证评估



一篇独立的预印本对 MP 进行了大规模的消融实验,验证了其有效性和局限性。

- 何时有效:MP 在学习率迁移方面对许多架构变体(如不同的激活函数、批量大小、初始化技巧)表现出稳健性。
- 何时失效:当添加可学习的偏置项、使用非常规优化器(如 SignSGD)或采用较强的权重衰减时,MP 的稳定性可能会被破坏。
尽管如此,实证表明,在标准的 Transformer 架构上使用 MP,可以在从中小规模扩展到百亿参数规模时,保持最优学习率基本不变,这大大简化了超参数搜索。




总结与核心要点


本节课我们一起学习了缩放定律在实际构建大语言模型中的应用。

我们通过分析 Cerebras-GPT、MiniCPM 和 DeepSeek 等模型的案例,看到了几种常见的扩展策略:
- 使用 最大更新参数化 来稳定超参数。
- 采用 热身-稳定-衰减 学习率调度来高效进行数据缩放分析。
- 直接通过实验 拟合缩放定律 来确定最优批量大小和学习率,并外推到大规模。

这些案例表明,虽然 Chinchilla 定律提供了一个起点,但通过精心优化(如更好的数据质量、更高效的架构),模型可以承受远高于 20:1 的 token 与参数比例。

此外,我们深入探讨了 MP 的数学原理和实证效果。MP 通过控制初始化和每层学习率的缩放,旨在实现超参数的尺度不变性,从而让扩展过程更可预测、更高效。尽管它不是万能的,但在许多标准场景下被证明是有效的工具。



掌握这些扩展策略和原理,对于从零开始设计和训练大型语言模型至关重要。




斯坦福 CS336 课程笔记:第12讲 - 评估 (Evaluation) 🧪


在本节课中,我们将要学习如何评估大型语言模型。评估看似简单,实则涉及许多复杂问题。我们将探讨评估的目的、常用基准测试、评估框架以及当前评估领域面临的挑战。



概述




评估的核心是给定一个固定的模型,通过提问来衡量其性能。这听起来相当容易,但实际操作中需要考虑许多因素。我们经常看到各种基准分数、排行榜和模型间的比较,但这些数字背后的含义是什么?如何解读它们?本节课将深入探讨这些问题。

评估的目的

上一节我们介绍了评估的基本概念,本节中我们来看看我们为何要进行评估。评估的目的并非绝对,它取决于你想要回答的具体问题。
以下是评估可能服务的几种不同目标:
- 用户或公司的购买决策:针对特定用途,选择最合适的模型(例如,在 Claude、Grok、Gemini 或 O3 之间选择)。
- 研究人员的科学探索:了解模型的原始能力,衡量人工智能领域的科学进展。
- 政策制定者和企业的客观了解:在特定时间点,客观了解模型的利弊、进展和价值。
- 模型开发者的反馈循环:通过评估获得反馈,以指导模型的改进和开发。
在每种情况下,评估者都有特定的目标,这需要转化为具体的评估方法。你选择的评估方式将取决于你想要达成的目标。
评估框架



为了系统地进行评估,我们可以思考一个简单的框架。这个框架主要关注三个环节:输入、模型调用和输出评估。

输入提示
首先,你需要一组提示。这些提示从哪里来?它们覆盖了哪些用例?它们是否具有代表性,是否包含能挑战模型的复杂输入,还是任何模型都能处理的常规简单情形?在多轮对话场景下,输入实际上依赖于模型的回复,这引入了复杂性。即使在单轮情境下,你也可能需要选择适合该模型的输入。

调用模型
有多种方式可以提示语言模型。你可以采用零样本、少样本或思维链的方式。模型对提示非常敏感,这意味着评估时必须将此考虑在内。你还需要决定是否使用工具(如代码解释器)、进行检索增强生成,或者在进行知识查询时使用联网搜索功能。此外,我们评估的对象是语言模型本身,还是包含工具和框架的整个智能体系统?这是一个重要的区分。

评估输出


通常,你会将模型的输出与参考答案进行比较。这些参考答案是否干净、没有错误?在代码生成中,你使用什么指标(例如,单元测试通过率)?你是否考虑成本?在许多排行榜中,成本因素常被边缘化。显然,在某些应用场景中,并非所有错误都是等价的,你如何将其纳入评估标准?开放式生成(如写故事)则很难评估,因为没有标准答案。
假设你完成了所有这些步骤并得到了各项指标,如何解读它们?例如,91分意味着模型很不错吗?如果你是一家公司,这个分数足够部署给用户吗?如果你是一名研究人员,如何确定模型真的学到了特定类型的泛化能力?这要求我们面对训练集与测试集可能重叠的问题。

总之,评估不仅仅是拿一堆提示输入模型那么简单,它需要仔细思考上述所有问题。




困惑度评估




在深入下游任务基准之前,我们先来看看一种基础的评估方法:困惑度。困惑度衡量的是语言模型对某个数据集赋予高概率的情况。
语言模型是标记序列上的一种分布。困惑度本质上衡量的是模型对某个数据集(通常是验证集或测试集)的预测不确定性。在预训练时,我们最小化训练集的困惑度。标准做法是在独立的测试集上评估困惑度。



在2010年代,有多种用于语言建模的标准数据集(如 Penn Treebank、WikiText)。研究人员在指定的训练集上训练,在测试集上评估困惑度。GPT-2 和 GPT-3 改变了人们看待困惑度评估的方式。它们在海量网络文本上训练,然后在维基文本等分布外数据集上评估,展示了强大的泛化能力。
困惑度相比下游任务准确性有一些优势:
- 它提供了关于每个词元的精细概率分数,而不仅仅是二元的对错判断。
- 它允许更平滑地拟合缩放定律曲线。
- 它具有通用性,因为它关注数据集中的每一个词元。

但困惑度评估也需注意:
- 你需要信任模型提供的概率是有效的(概率之和为1)。
- 它可能过度关注分布中不重要的部分。



一些看似像困惑度测试的任务(如完形填空任务、HellaSwag常识推理)本质上也是基于概率的评估。


常用基准测试介绍



现在,让我们来看看评估语言模型常用的一些基准测试。每个基准都有其侧重点和数据来源。





知识密集型基准
以下是几个测试模型知识能力的著名基准:

- MMLU (大规模多任务语言理解):包含57个学科的多项选择题,源自网络。它通常使用少样本提示进行评估。目前顶级模型在该基准上的分数已接近饱和(90%以上)。
- MMLU Pro:MMLU的改进版,将选项从四选一改为十选一,并剔除了一些简单问题,使得分数更具区分度。
- GPQA (谷歌-proof问答):由博士水平专家编写和验证的高难度选择题,旨在抵御通过谷歌搜索30分钟找到答案。它测试的是深度的专业知识。
- 人类最后一场考试 (HLE):一个多模态(文本+图表)的基准,包含多项选择题和简答题。它通过奖金池征集难题,旨在提出当前模型难以解决的问题。

指令遵循与开放式生成基准
对于遵循指令和开放式生成的能力,评估更具挑战性,因为没有标准答案。
- Chatbot Arena:一个流行的众包评估平台。用户输入提示,会随机收到两个模型的匿名回复,然后评判哪个更好。根据这些两两偏好计算出Elo评分进行排名。其优点是使用实时、动态的用户提示。
- AlpacaEval:使用一个强大的语言模型(如GPT-4)作为评判员,自动计算被评估模型相对于参考模型(如text-davinci-003)的胜率。它是快速、自动化的,但可能存在基于评判模型的偏差。
- IFEval (指令遵循评估):测试模型遵循复杂、人为约束的能力(例如,“用 exactly 10个单词写一个关于狗的故事”)。评估可以通过简单脚本自动完成,但只评估格式而非内容质量。
智能体基准测试



智能体基准测试评估模型使用工具、执行多步任务的能力。
- SWE-bench:给定一个代码库和一个GitHub问题描述,要求模型提交能让单元测试通过的拉取请求。评估智能体的代码理解和修改能力。
- CYBENCH:一个网络安全基准。智能体可以访问一个服务器终端,目标是入侵服务器并获取密钥。它评估智能体在受限环境下的规划和命令执行能力。
- MLAgentBench:包含75场Kaggle竞赛。智能体需要分析数据集、编写代码、训练模型、调试并提交结果。它评估端到端的机器学习项目执行能力。
在这些智能体基准测试中,即使是最好的模型,准确率也相对较低(约20%或以下)。


推理与安全基准





还有一些基准专注于更纯粹的推理能力或模型的安全性。



- ARC (抽象推理语料库):提供抽象的模式(如图形矩阵),要求模型找出规律并填空。它旨在测试不依赖世界知识的纯推理能力。GPT-4在此任务上表现不佳,但O3等最新模型有所提升。
- 危害基准 (HarmBench):梳理了510种有害行为,测试模型是否会遵循这些有害指令。用于评估模型的安全对齐程度。
- 越狱攻击:通过优化提示(例如,在有害指令后添加特定乱码)来绕过模型的安全限制。这揭示了安全防护的潜在脆弱性。






评估面临的挑战






在介绍了各种基准后,我们必须认识到当前的评估体系面临诸多挑战。




现实性与实用性




许多基准测试(尤其是标准化考试)与实际应用场景相去甚远。真实的用户提示更多是“提问”(用户不知道答案,寻求信息),而非“测验”(用户知道答案,测试系统)。收集和分析真实世界的对话数据(如 Anthropic 的研究)对于构建更实用的评估至关重要,但这常与隐私保护相冲突。





训练集污染


在互联网规模数据上训练模型,几乎无法保证测试集数据没有在训练中出现过。这导致了训练集与测试集的重叠问题,使得评估分数可能虚高。社区需要鼓励更规范的实践,例如模型提供者报告去污检查结果。


数据集质量

许多基准测试数据集本身存在标签错误或其他质量问题。修正这些错误后,模型的性能分数可能会发生显著变化。



评估对象不明确



我们到底在评估什么?是评估一种新的训练方法(算法创新),还是评估一个在特定时间点、用特定数据训练出来的系统(产品)?明确游戏规则对于解读评估结果至关重要。



总结


本节课中我们一起学习了大型语言模型的评估。我们首先探讨了评估的多种目的和基本框架。然后,我们回顾了从困惑度到各种下游任务基准(如MMLU、GPQA、Chatbot Arena、SWE-bench)的评估方法。最后,我们深入分析了当前评估领域面临的主要挑战,包括现实性不足、训练集污染、数据集质量问题以及评估目标不明确。




评估是一个深刻影响模型开发方向的关键环节。理解这些基准的构成、优势与局限,对于正确解读模型性能、做出合理决策以及推动该领域向前发展都至关重要。


斯坦福 CS336 课程笔记:第13讲 - 数据1 🗂️
在本节课中,我们将要学习构建大型语言模型(如ChatGPT)时至关重要的一个环节:数据。我们将探讨数据的来源、筛选方法、处理流程以及相关的法律与伦理问题。理解数据是理解现代语言模型如何工作的关键。
概述
在之前的讲座中,我们讨论了如何根据固定数据集来训练模型,涵盖了架构、优化器、标记化、缩放法则和并行性。本节课,我们将焦点转向数据本身:用什么数据来训练模型。数据是决定模型准确性和能力的最重要因素之一。



数据的重要性与保密性
我认为要让语言模型准确,数据是最重要的。虽然有人可能认为缩放定律最为重要,但我的依据是观察公司在论文中实际透露的信息。
如果你看看所有的开放权重模型,例如 Llama 3 乃至 DeepSeek,它们都完整披露了自身架构,并在论文中大量谈及了训练过程,但基本没提数据。例如,Llama 3 的论文包含诸多方面的详细信息,但关于数据,他们只是宏观地谈及了数据过滤方式,具体信息并不多。
这种保密是有原因的。一是竞争因素,二是他们不想引发法律诉讼。在基础模型出现之前,数据的重要性就已得到明确认可,因为推动监督学习需要对数据进行标注。如今即便所需标注减少,数据工作仍不可或缺,且涉及大量数据整理与清理工作。


数据是一个长尾问题。人们如此重视它的原因在于它实际上扩展性很强。如果你想构建一个能处理各类不同任务的模型,你可以轻松雇几百人组成团队,负责数据的不同方面,如多语言和代码处理。如果是多模态模型,你还能处理图像等数据。相比之下,架构通常由一个小团队来定义。
训练阶段与数据流程
语言模型的开发通常包含多个训练阶段,每个阶段使用不同类型和质量的数据。
- 预训练:这是本课程大部分内容的重点。使用通常来自网络的原始数据进行训练。
- 中期训练:在这一阶段,需要挑选出一小批高质量的数据文档,目标是培养特定能力,如数学、代码或长文本语境理解。
- 训练后阶段:此时会在遵循指令的数据或聊天数据上进行微调,或者进行强化学习,让模型能真正对话。通常,向安全对齐的内容也在这个阶段处理。
实际上,这些阶段的界限很模糊。在最近的模型中,常常有更多阶段。但基本思路是清晰的:从大量低质量数据入手,最后使用少量高质量数据进行训练。
一些术语:
- 基础模型:一般指的是预训练和中期训练后得到的检查点。
- 结构化模型:是在后期训练(微调等)完成后得到的模型。
数据来源与处理实例


让我们通过一个开源模型的例子,看看数据集的具体内容。这个例子来自 AI2 发布的一系列开源模型。
预训练数据组合(典型的开源模型配置):
- 来自名为
Dolma的网页数据(一个大型网络语料库)。 - 代码。
- 学术论文。
- 数学数据。
- 维基百科。
总计约有 3.9 万亿个词元(Token)。
中期训练阶段:
你会看到实际上有一堆相同的来源,但它们被进一步筛选了。例如,从 3.7 万亿个词元中筛选出 7000 亿个高质量词元。这个阶段还包括:
- 一些
FLAN数据集(指令微调数据集)。 - 维基百科(我们一直很喜欢维基百科)。
- 一些新的合成生成的数据集,例如 GSM8K 数学训练集。
总计大约有 1000 亿个训练词元。
训练后工作:
有一篇名为 Tulu 的独立论文负责实际的训练后工作。这里展示的是各类数据组合,基本上有来自不同来源的聊天数据,还有一堆用于涵盖不同方面的合成数据。
如何选取和处理数据?

为了不让你日后失望,需要说明的是:并没有一个完美的、形式化的原则来决定如何选取和处理所有数据。鉴于这门课的性质,这或许并不令人意外。即便在架构方面,我们也没有什么像样的原则,尤其在数据方面更是如此。

我将介绍人们长期以来使用的各种数据集、它们的来源和一些特性。希望你们能发挥归纳能力,来找出什么样的数据是好的、什么样的数据不行的一些直观感受。
我先从预训练说起,接着再讲讲训练中期和后期的情况,不过大部分内容还是围绕预训练。




预训练数据的历史演变
2018年:BERT 与早期数据
我们从2018年的 BERT 模型开始。BERT 是基于书籍和维基百科训练出来的。
- 书籍语料库:有一个叫
Smashwords的网站,允许任何人发布电子书。在2015年,一篇论文抓取了该网站的数据,创建了一个由免费的自出版书籍构成的语料库(约7000本书)。此后它因违反服务条款被撤下。这体现了书籍数据的重要性。 - 维基百科:一个人人皆知的免费百科全书。它不包含原创观点,所有内容都引用自原始资料,并且基于知名度(必须有多个来源提及)。维基百科有很多有价值的内容,但也有一些领域它不覆盖,比如个人观点、食谱等。维基百科定期生成包含所有内容的备份文件(转储文件),可供下载。
关于数据的一个关键问题:数据中毒
攻击者可以在维基百科定期转储之前注入恶意编辑,使这些内容进入训练数据,即便之后编辑被回滚。这可能导致模型学习到有害的关联(例如,将负面情绪与特定品牌关联)。这说明了来自互联网的开放数据可能被操纵,从而影响模型行为。
BERT 是根据书籍和维基百科训练的。那时人们对语言模型的数据重复问题没那么关注。



2019年:GPT-2 与网络文本筛选
GPT-2 收集了一个名为 WebText 的数据集。思路是:网络规模大但质量可能不高,如何快速获取一个多样且高质量的子集?
他们的方法是:选取那些在社交媒体(如Reddit)上获得超过一定“积分”(如3个赞)的帖子中的外链网页。这产生了800万个页面,40GB的文本。他们没有发布这个数据集,但此后出现了公开复制版,称为 OpenWebText。
通用网络爬虫 (Common Crawl)
我希望到这结束的时候,每当有人跟你说“语言模型是通过互联网训练的”,你可以指出并讲那是错的。更准确的说法是:通过通用网络爬虫等特定来源的、经过大量筛选和处理的数据。
- 是什么:
Common Crawl是一个非营利组织,自2007年起每月进行网页抓取,已持续约17年。抓取本身成本不高,可以在云服务上运行机器在两周内完成。 - 如何工作:它使用一组种子网址(数亿个),通过广度优先搜索的方式进行爬取。它必须遵守
robots.txt协议(网站声明是否允许爬虫访问的文件),并避免使服务器过载。 - 数据格式:爬虫生成两种格式:
WARC文件:原始 HTTP 响应。WET文件:将 HTML 转换为纯文本后的格式(这是一个有损过程)。
- 注意点:转换工具(HTML 转文本)对数据质量有显著影响。一篇论文发现,使用不同转换器,模型性能差异可达4个百分点。
- 覆盖率:它并非旨在全面爬取整个互联网,政策上要求“温和礼貌”。例如,并非所有维基百科文章都在其中。
- 内容:默认情况下包含大量内容,包括可能有害或冒犯性的材料,以及受版权保护的材料。
关于 robots.txt
网站可以通过 robots.txt 文件声明禁止哪些爬虫访问。例如,纽约时报禁止谷歌爬虫访问其大部分内容。但遵守 robots.txt 只是行业规范,并非法律强制。
早期数据筛选方法
由于直接从 Common Crawl 随机抽样的数据质量很低,人们开始尝试筛选。
1. CCNet (Meta)
- 目标:创建一个处理 Common Crawl 的通用程序,返回高质量、多语言数据集。
- 方法:
- 去重。
- 语言识别(保留目标语言)。
- 关键:基于质量的筛选。他们训练一个n-gram语言模型(例如五元语法模型)在维基百科文本上,然后用它给 Common Crawl 中的文档评分。思路是:维基百科是高质量的替身,寻找类似维基百科的文档。
- 效果:用此数据训练的模型比仅用维基百科训练的表现更好。
2. C4 (Google)
- 代表“Colossal Clean Crawled Corpus”。与 T5 模型一同发布。
- 方法:完全基于启发式规则进行筛选。
- 保留以标点结尾的行。
- 删除少于三句话的页面。
- 删除包含脏话的页面。
- 删除包含花括号
{}的页面(这会删除大量代码)。 - 删除样板文本(如菜单、页脚)。
- 仅保留英文。
- 与 CCNet 对比:CCNet 使用基于模型的过滤(像维基百科),C4 使用基于规则的过滤。两者是互补的:基于模型的方法可能遗漏格式正确但不像维基百科的句子;基于规则的方法可能放过结构完整但质量低下的句子。
GPT-3 时代及之后的数据集
GPT-3 数据集
包含经过处理的 Common Crawl、WebText、两个书籍语料库和维基百科,总计约4000亿个词元。
- 核心处理:他们训练了一个质量分类器,用于从海量数据中区分出“高质量”内容(以 WebText、维基百科和书籍作为正例)。目标是找出更多类似的高质量内容。
The Pile (EleutherAI)
在 GPT-3 封闭的背景下,EleutherAI 试图创建开源语言模型。The Pile 是一个由社区精心筛选的多样化数据集,包含22个来源,如:
- Common Crawl
- OpenWebText
- Stack Exchange
- 维基百科
- 学术论文(如 PubMed)
- 代码(GitHub)
- 书籍(PG-19,来自古登堡计划)
- 甚至包含安然公司邮件(一个著名的公共数据集)。
其数据量比 GPT-3 使用的还要多。
关于一些特定数据源的说明
- 古登堡计划:提供公共领域的书籍(版权过期,约7.5万册)。
PG-19数据集用于长文本建模基准测试。 - 影子图书馆(如 LibGen):提供大量有版权的书籍,常引发法律纠纷。据透露,Meta 曾用其训练模型。
- Stack Exchange:一个问答网站集合(如 Stack Overflow)。其数据天然具有问答格式,与聊天机器人指令跟随任务相似。注意其数据转储通常只允许非商业使用。
- GitHub:代码的主要来源。获取“在 GitHub 上训练”的数据需要大量预处理:克隆仓库、筛选许可证、去重等。
The Stack是一个处理好的开源代码数据集。
Gopher (DeepMind) 与 MassiveText
MassiveText数据集涵盖大量网页(C4)、书籍、新闻、GitHub、维基百科。- 筛选方法:主要使用人工规则的质量筛选器(例如,80%的单词必须包含字母),并利用谷歌安全搜索进行有害内容过滤。当时避免使用模型过滤是担心弱模型会引入偏见。
LLaMA (Meta)
- 数据集使用 Common Crawl(通过 CCNet 获取),但分类器思路不同:训练分类器判断一个页面是否像会被维基百科引用的页面(而不仅仅是像维基百科页面)。他们还纳入了 C4、GitHub、维基百科、古登堡计划、Stack Exchange 等。
- 总计获得1.2万亿个词元。他们没有发布数据集,但开源社区发起了
RedPajama项目来重现它。
RefinedWeb (TII)
- 核心论点:也许我们把网络数据筛选得足够好,那就是所需的一切。互联网理论上包含所有内容。
- 方法:对 Common Crawl 使用
trafilatura提取工具,应用 Gopher 的规则,避免基于机器学习的过滤(防偏差),并进行模糊去重。 - 获得了一个包含5万亿词元的数据集(发布了6000亿的子集)。
FineWeb是其后继改进版本,获得了15万亿词元。可视为一个轻度过滤的数据集,可作为进一步模型过滤的基础。
Dolma (AI2) 与 OLMo 模型
Dolma数据集包含 Common Crawl、The Stack(代码)、C4、RedPajama、学术论文、古登堡计划、维基百科等。- 初始的 OLMo 模型在 Dolma 上训练,未使用基于模型的过滤,仅使用语言识别、质量规则筛选、有害内容分类和去重,产出3万亿词元。
DataComp for LM
- 这是一项多组织合作的工作,旨在为数据集创建方法建立基准和竞赛。
- 他们处理 Common Crawl 生成
Dolma池(240万亿词元),然后严格筛选到Dolma极限(仅保留1.4%)。 - 筛选方法:大力使用基于模型的质量过滤。
- 正例:来自
OpenHermes(GPT-4生成的指令数据)和Dolphi(高质量问答)数据集。这很有趣,他们用指令数据来挑选预训练数据。 - 反例:从 RefinedWeb 中采样的数据。
- 正例:来自
- 训练一个快速文本分类器,将 240 万亿词元筛选到 3.8 万亿词元。结果表明,其模型比使用 RefinedWeb 的模型在基准测试上高出约3%。
Nemotron-CC (NVIDIA)
- 核心论点:Dolma 极限筛选太严格(3.8万亿词元),对于训练更大模型(如4000亿参数)不够。
- 方法:
- 选用
just-text而非trafilatura进行 HTML 转文本,以保留更多词元。 - 使用多种质量筛选器集成:
- 让一个大语言模型根据“教育价值”评分。
- 使用 DataComp 的分类器。
- 从每个分类区间抽样,而不仅仅是顶部,以确保多样性。
- 数据改写:
- 对低质量数据,用语言模型改写成更高质量。
- 对高质量数据(如维基百科),用语言模型生成任务(如问答对、总结),为指令微调做准备。
- 选用
- 最终获得 6.3 万亿词元,几乎是 Dolma 极限的两倍,且基准测试表现更优。
中期与训练后数据
这部分界限模糊,目标通常是培养特定能力或进行对齐。
长上下文扩展
- 目的:使模型能处理长文本。
- 常用数据源:书籍(长依赖)、数学文本、合成数据。
- 时机:常在中期训练加入,因为如果模型能力不足,在长上下文上训练是浪费。
指令微调与对齐数据
目标是让模型能够遵循一次性指令。
- 早期方法:整合传统 NLP 任务,形成标准格式(如
Super-NaturalInstructions、FLAN数据集)。但提示往往模板化。 - 合成数据兴起:以
Alpaca模型为代表,使用“自我指导”让语言模型生成示例。 - 数据来源:
- GPT-4生成:最简单,但可能违反服务条款。
- 开源模型生成:使用 Llama 等宽松许可的模型生成。
- 人工标注:最贵最慢,但质量高,需注意标注者可能使用 GPT-4。
- 其他技术:
Evol-Instruct(使问题复杂化)、从问答网站提取等。 - 示例数据集:
OpenHermes、Lima、Nemotron训练后数据(混合了公共数据集和合成数据,包含推理链)。
版权与法律问题
版权基础
- 目标:激励创作。
- 保护对象:以有形形式表现的原创作品(表达,而非思想)。代码可受版权保护,算法不能。
- 现状:无需注册即受保护(与专利不同),有效期约75年。互联网上大多数内容都受版权保护。
合法使用数据的途径
- 获取许可:与版权方签订合同(如谷歌与 Reddit)。
知识共享 (Creative Commons)许可是一种特殊的免费许可(维基百科使用此许可)。 - 合理使用:即使无许可,在某些条件下也可使用。考量因素包括:
- 使用目的(教育 vs. 商业)。
- 作品性质(纪实 vs. 虚构)。
- 使用部分的数量和实质性。
- 对潜在市场的影响。
- 语言模型训练常以“变革性使用”(提取语言模式而非复制表达)为由辩护,但存在记忆和提取训练数据的问题,使情况复杂。
挑战
- 即使内容属于合理使用,违反网站服务条款(如用脚本下载 YouTube 视频)也可能导致不合法。
- 开源模型发布数据和模型时,版权风险更高。
- 拥有专有数据(如 X/Twitter、YouTube)的公司可能有优势,但内部使用也受限制。
总结
本节课我们一起学习了构建大型语言模型所需数据的全貌:


- 数据不会凭空而来:需要从原始服务(如 Common Crawl、GitHub)获取,并经过大量处理(提取、过滤、去重)才能用于训练。
- 数据是区分模型的关键:在架构趋同的今天,数据质量是决定模型能力的核心因素。
- 筛选方法多样:从早期基于规则(C4)和简单模型(CCNet),发展到如今集成多个模型和质量维度(Nemotron-CC)的复杂方法。趋势是更多地使用模型进行筛选。
- 流程分阶段:从预训练(大量、相对粗糙数据)到中期训练(高质量、针对性数据)再到训练后(指令、对齐数据),数据质量和目标逐渐细化。
- 面临法律与伦理挑战:版权和合理使用是悬在数据收集之上的


课程14:数据筛选与去重算法详解 🧹📊

在本节课中,我们将深入学习数据处理流程中的两个核心环节:质量筛选与数据去重。上一讲我们回顾了语言模型训练所用的各类数据集。本讲将深入探讨如何从海量原始数据中,高效地筛选出高质量、低毒性的子集,并移除重复内容,以提升模型训练的效率与效果。





筛选算法概述
在数据处理中,一个常见的模式是:你拥有少量高质量的目标数据 T 和大量待处理的原始数据 R。我们的目标是找到 R 的一个子集,该子集在分布上与 T 相似。

对筛选算法有两个核心要求:
- 泛化能力:算法应能基于
T进行泛化,而非简单匹配T本身。 - 高效快速:算法需能在整个网络规模的数据上高效运行,计算成本应远低于直接训练模型。
我们将介绍三种实现此目标的方法。


方法一:N元语法模型

N元语法模型是一种经典的统计语言模型。其核心思想是基于前 N-1 个词来预测第 N 个词的概率。
公式:对于一个文本语料库,条件概率 P(w_N | w_1, ..., w_{N-1}) 的最大似然估计为:
count(w_1, ..., w_N) / count(w_1, ..., w_{N-1})



主要挑战是数据稀疏性,许多合理的N元语法出现次数为零。因此需要采用平滑技术(如Kneser-Ney平滑)来处理未登录词。

其应用方式是:在目标数据 T 上训练一个N元语法模型,然后用它计算原始数据 R 中每个文档的困惑度。保留困惑度低(即模型认为概率高)的文档。
# 示例:使用N元语法模型计算句子困惑度
from kenlm import Model
model = Model('wiki.arpa') # 加载在维基百科上训练的模型
sentence = "Stanford University was founded in 1885."
print(model.score(sentence)) # 计算对数概率
这种方法简单快速,常被用于早期数据筛选(如CCNet论文中的做法)。
方法二:线性分类器(FastText)
FastText是一个高效的文本分类库。其动机在于,对于许多分类任务,简单的线性分类器在速度上远超复杂神经网络,且效果相当。
其核心是降维:将词向量空间映射到一个更小的隐藏维度 H,然后进行线性分类。这可以看作是一种矩阵分解,大幅减少了参数量。
为了捕捉N元语法信息,FastText将句子拆分为N元语法集合,并通过哈希函数将这些N元语法映射到固定数量的桶中,从而处理词汇量爆炸的问题。

在筛选任务中,通常训练一个二分类器(判断文档属于“高质量目标数据”还是“低质量原始数据”)。保留被分类器判定为“高质量”的文档。

方法三:重要性重采样
重要性重采样是一种更原则性的方法,旨在使筛选后的数据分布匹配目标分布。
基本概念回顾:假设我们想从目标分布 P 中采样,但只能从一个提议分布 Q 中采样。我们可以先从 Q 中采样一批样本,然后为每个样本计算重要性权重 w_i = P(x_i) / Q(x_i),最后根据归一化后的权重进行重采样,从而近似 P 分布。
在数据筛选中的应用:
- 将目标数据集
T视为来自分布P的样本。 - 将原始数据集
R视为来自分布Q的样本。 - 由于
T很小,无法直接拟合好的P模型,因此使用哈希N元语法等简单技术来估计P和Q。 - 计算原始数据中每个文档的重要性权重(即
P与Q的似然比),并依此进行重采样。
这种方法更注重匹配整体分布,可能对数据多样性更有利。
筛选算法的统一视角
以上三种方法共享一个高级框架:
- 估计模型:基于目标数据
T和/或原始数据R,估计一个评分函数(如概率、分类得分、似然比)。 - 应用评分:用该评分函数对原始数据
R中的每个示例进行打分。 - 选择样本:保留高分示例(通过设定阈值或根据权重重采样)。

这个框架不仅适用于质量筛选,也可灵活应用于其他任务。





筛选算法的应用场景


以下是筛选机制在不同任务中的应用:
1. 语言识别
目标:从多语言数据中筛选出特定语言(如英语)的文本。
方法:使用FastText等工具训练语言分类器。例如,Dolma数据集使用FastText语言ID模型,保留被分类为英语且概率大于0.5的页面。
2. 质量筛选
目标:筛选出“高质量”文本,但“质量”定义多样。
实践案例:
- GPT-3:从高质量来源(如维基百科、书籍)采样正例,从通用爬虫数据采样负例,训练线性分类器进行筛选。
- Phi-1:使用GPT-4,根据“教育价值”提示,对代码数据进行标注,生成正例,然后训练随机森林分类器进行筛选。这展示了利用强大语言模型自动生成目标数据
T的新范式。
3. 毒性过滤
目标:移除包含仇恨、侮辱、威胁等有毒内容的文本。
实践案例:Dolma团队使用在“Jigsaw有毒评论”数据集上训练的FastText分类器,来识别和过滤有毒内容。
数据去重算法
去重旨在移除数据集中的重复或近乎重复的文档,以提升训练效率并减少模型对数据的死记硬背。
去重的设计空间
- 去重单位:句子、段落还是文档?
- 匹配标准:完全匹配还是近似匹配(如Jaccard相似度>0.99)?
- 处理动作:删除所有重复项还是仅保留一个?
核心挑战是算法复杂度。朴素的两两比较是 O(N^2),不可行。我们需要基于哈希的线性时间算法。
精确去重
方法:计算每个文档(或文本块)的哈希值(如MurmurHash)。将哈希值映射到桶,每个桶内保留一个文档即可。
优点:简单、精确、易于并行(MapReduce)。
缺点:无法检测近似重复。
实践:C4数据集使用三句子片段进行精确去重。
布隆过滤器
一种高效、节省内存的近似集合成员判定数据结构。
- 原理:使用
k个哈希函数,将元素映射到一个长度为m的位数组中。插入时,将对应k个位置置1。查询时,若k个位置均为1,则可能在集合中(可能有误报);若有任一为0,则肯定不在集合中。 - 特点:可设置超参数(
m,k)来权衡内存、计算成本和误报率。
# 布隆过滤器概念示例
from pybloom_live import BloomFilter
bf = BloomFilter(capacity=1000, error_rate=1e-15)
bf.add("document_hash_123")
print("document_hash_123" in bf) # 可能为 True
print("document_hash_456" in bf) # 肯定为 False (除非误报)
近似去重
为了检测几乎相同但略有差异的文档(如修改个别单词、标点),我们需要近似匹配。
Jaccard相似度与最小哈希
- Jaccard相似度:衡量两个集合
A和B的相似度,公式为|A ∩ B| / |A ∪ B|。 - 最小哈希:一种哈希函数,其关键性质是:
P[minHash(A) = minHash(B)] = Jaccard(A, B)。即,两个集合哈希冲突的概率等于它们的Jaccard相似度。
局部敏感哈希
单个最小哈希冲突概率仍等于相似度,不够“尖锐”。LSH通过组合多个最小哈希函数来“锐化”这一概率曲线。
- 方法:生成
n个最小哈希函数,将其分成b个波段,每个波段含r个哈希(n = b * r)。我们规定:如果两个文档在至少一个波段内的所有r个哈希值都相同,则它们被视为候选重复对。 - 调整:通过调整
b和r,可以控制相似度阈值和判定的严格程度。增大r会使判定更严格(曲线右移),增大b会增加匹配机会(曲线左移)。
实践:许多论文使用基于最小哈希的LSH进行近似去重,例如设置 b=22, r=450,以实现约0.99相似度的严格去重。
总结
本节课我们一起学习了数据准备过程中的关键算法:
-
筛选算法:我们探讨了N元语法模型、线性分类器(FastText)和重要性重采样三种方法。它们共享一个核心模式:基于目标数据学习一个评分函数,用于从原始数据中筛选样本。这套方法可广泛应用于语言识别、质量筛选和毒性过滤。
-
去重算法:我们学习了精确去重(哈希表、布隆过滤器)和近似去重(基于最小哈希的局部敏感哈希)技术。去重能有效提升训练计算效率,并减轻模型对数据的机械记忆。哈希是这些算法的基石,它将成对比较问题转化为线性时间的操作。


掌握这些算法工具,是构建高质量训练数据集的重要一步。真正的数据洞察力还需通过实践——亲自查看、筛选数据并观察模型效果——来逐步建立。


课程15:对齐 - 监督微调与人类反馈强化学习 🎯

在本节课中,我们将要学习如何将一个大型预训练语言模型(如GPT-3)转变为一个实用、安全且能遵循指令的系统(如ChatGPT)。我们将重点探讨训练后的两个核心步骤:监督微调和基于人类反馈的强化学习。


从预训练到后训练 🔄

上一讲我们讨论了预训练数据。本节中,我们来看看如何利用预训练好的模型,通过后训练使其变得有用和安全。
截至目前,我们主要关注大型预训练系统的数据组件。接下来,我们将使用这个大型预训练模型,并通过多种方式让它变得实用。这包括两个主要阶段:首先通过监督微调让模型学会遵循指令,然后通过人类反馈强化学习来进一步优化其行为,确保安全性和有用性。

现代指令跟随模型的能力令人印象深刻。例如,它们能够遵循一长串嵌套的复合指令,并生成复杂的代码。然而,要使模型达到这种实用水平,并确保其安全性以防止滥用,需要进行专门的后训练处理。

第一部分:监督微调 📝


监督微调是构建指令遵循模型的第一步。其核心思想是:我们收集人类专家提供的“指令-回答”示范数据,然后训练模型去模仿这些示范。

训练数据的形态与挑战

要让监督微调运作起来,我们需要考虑两件事:训练数据和训练方法。首先,我们来看看数据。
高质量的训练数据至关重要。如果数据有噪声,模型可能会学到离谱的行为。以下是构建指令微调数据的三种不同范式:

- FLAN数据集:通过整合大量现有自然语言处理任务数据集构建。其特点是任务类型多样,但回复通常较短(如单词或短语),交互风格不像常见聊天。
- Alpaca数据集:利用语言模型生成指令,再用另一个模型生成回复。其风格更接近ChatGPT,输入为简短指令,输出为较长的自然语言回复。
- OpenAssistant数据集:由在线爱好者人工编写。其特点是查询更复杂,回复非常详细且常包含引用,质量高但编写难度大。

这些数据集在输入输出长度、知识深度和风格上差异很大。

数据收集的实践与陷阱

为了让大家亲身体验数据收集的挑战,我们进行了一个互动练习:为指令“CS336是什么?”编写回答。


从收集到的回复中,我们可以观察到:
- 让人们写出长篇、详细的回复非常困难。
- 回复质量参差不齐,存在大量简短或敷衍的内容。
- 激励人们生成高质量数据而非简单回复,是一项艰巨的任务。

此外,数据收集还需考虑安全微调。我们需要让模型学会拒绝不安全的指令,但也要避免过度拒绝合理的请求(例如,“如何杀掉一个Python进程”)。这需要在数据中精心平衡。


监督微调的方法与扩展


一个简单的答案是:有了专家示范数据,直接通过梯度下降训练模型去模仿即可。然而,在现代大规模训练中,方法更为复杂。
一种越来越流行的做法是两阶段训练或中期训练:
- 第一阶段进行纯预训练。
- 在训练后期(如学习率衰减阶段),将高质量的指令微调数据与预训练数据混合。
- 最后可能再进行一轮小规模的纯指令微调。

这种方法的好处是:
- 能更深度地将指令遵循能力融入模型。
- 有助于缓解灾难性遗忘问题。
- 让模型在保持通用能力的同时,获得我们期望的行为。
公式表示混合训练:总损失 = 预训练损失 + λ * 指令微调损失,其中λ在训练后期增大。

需要注意的是,这也会模糊“基础模型”的界限,因为许多所谓的“基础模型”可能已经隐含地经历了指令微调阶段。
监督微调的核心要点
总结一下监督微调部分:
- 效果强大:即使少量数据也能显著改变模型行为。
- 数据质量复杂:“高质量”的定义并不简单,需要仔细思考数据如何影响模型行为,特别是要警惕模型因无法达到数据中的知识深度而学会“虚构”的捷径行为。
- 方法演进:现代训练流程趋向于将指令微调数据混合到预训练后期,以实现更好的效果和扩展性。
第二部分:基于人类反馈的强化学习 🤖
上一节我们介绍了通过模仿进行学习的监督微调。本节中,我们来看看另一种更强大的优化方法——基于人类反馈的强化学习。
监督微调要求提供专家示范,成本高昂。而RLHF的核心思想是:收集人类对模型多个输出的偏好比较(例如,输出A比输出B好),然后利用这些比较信息来训练模型。
为何需要RLHF?


- 成本更低:获取成对的偏好反馈通常比让人工撰写长篇优质回复更便宜、更快。
- 质量可能更高:研究表明,人类在评判回答质量时,有时比他们自己撰写回答表现得更好。存在“生成与验证”的差距。

如何收集成对反馈数据?
典型的流程是:向标注者展示同一个提示的两个模型输出,让他们选择更好的一个。标注者会依据一些准则(如有用性、真实性、无害性)进行判断。
然而,通过实践我们发现,即使对于“哪个回答事实更准确”这样的简单判断,在短时间内进行核实也非常困难。这揭示了大规模收集高质量成对反馈的挑战:
- 寻找高质量标注者很难。
- 在时间压力下核实事实准确性极具挑战。
- 需注意标注者可能直接使用AI工具(如GPT-4)来辅助判断,引入偏差。
此外,RLHF数据对最终模型行为有巨大影响,必须谨慎对待标注者群体带来的文化或价值观偏差。

从人类反馈到人工智能反馈

由于人类反馈收集的挑战,人工智能反馈变得越来越流行。例如,使用强大的大语言模型(如GPT-4)来生成偏好比较。研究发现,AI反馈与人类反馈的一致性程度,接近于人类之间的一致性,且成本低得多。

许多最新的开源项目(如Tulu)都采用了AI生成的偏好数据来训练奖励模型。

RLHF的算法实现

我们的目标是找到一个策略(即模型)π,使其生成的回答能最大化某个奖励函数r(x, y)。但我们无法直接获得奖励函数,只有人类的成对偏好数据。

标准流程(如InstructGPT):
- 收集偏好数据,训练一个奖励模型
r_φ(x, y),用于给任何输出打分。 - 使用强化学习算法(如近端策略优化)来优化语言模型策略π_θ,以最大化奖励,同时避免偏离初始监督微调模型太远。



优化目标公式:
目标 = E[r_φ(x, y)] - β * KL(π_θ(y|x) || π_SFT(y|x))
其中,β是控制偏离程度的超参数,π_SFT是监督微调后的初始模型。

PPO算法较为复杂,涉及优势函数估计和策略约束。近年来,一种更简单的方法直接偏好优化受到了广泛关注。
直接偏好优化
DPO的巧妙之处在于,它绕过了训练奖励模型和运行复杂RL算法的步骤。其核心思想是:将强化学习问题转化为一个简单的分类问题。
通过数学推导,我们可以将最大化偏好数据概率的目标,直接转化为一个类似于监督微调的损失函数:

DPO损失函数:
L_DPO = -log σ( β * log(π_θ(y_w|x) / π_SFT(y_w|x)) - β * log(π_θ(y_l|x) / π_SFT(y_l|x)) )
其中,y_w是偏好输出,y_l是非偏好输出,σ是逻辑函数,β是参数。

这意味着,我们可以直接使用偏好数据,通过梯度下降来更新模型,让模型更倾向于生成被偏好的输出,同时避免生成不被偏好的输出。这种方法更简单、更稳定,正在成为学术研究和实践中的热门选择。
总结 📚

本节课中,我们一起学习了将预训练语言模型对齐到人类期望的两大关键技术:

- 监督微调:通过模仿人类编写的“指令-回答”对,让模型初步学会遵循指令。我们讨论了数据收集的挑战、高质量数据的复杂性,以及现代两阶段训练方法。
- 基于人类反馈的强化学习:通过人类对模型输出的偏好比较,进一步优化模型。我们探讨了数据收集的难点、从人类反馈到AI反馈的趋势,并介绍了PPO和更简单的DPO这两种核心算法。


这些后训练步骤对于打造像ChatGPT这样既强大又相对安全实用的AI助手至关重要。它们将模型从“什么都知道一点”的预训练状态,塑造成了“能听懂话、办好事”的智能体。在下节课中,我们将继续探讨基于可验证奖励的强化学习等更深入的话题。




课程16:对齐 - 强化学习1 🧠

在本节课中,我们将学习训练后优化系列的第二部分内容,重点是基于可验证奖励的强化学习。我们将首先回顾并完成上一讲关于基于人类反馈的强化学习的讨论,然后深入探讨一种更简单、更高效的新方法——广义信赖域策略优化,并分析其在构建现代推理模型中的应用。
回顾:基于人类反馈的强化学习

上一节我们介绍了训练后优化的基本概念。本节中,我们先来快速回顾基于人类反馈的强化学习的关键点。

我们希望利用成对的偏好数据来训练一个语言模型策略,使其能最大化某个潜在的奖励。直接策略优化是一种优化该目标的算法,它将奖励函数重新参数化为策略的函数,并带入布拉德利-特里模型的目标中。


以下是DPO更新的核心形式:
梯度更新 ∝ β * (奖励估计误差) * (提升正样本概率 - 降低负样本概率)
本质上,强化学习算法通常归结为增加“好”行为的权重,减少“坏”行为的权重。DPO因其简单有效,一度成为主流的训练后优化方法。

随后出现了许多DPO的变体,例如CPO和长度归一化DPO,它们通过调整更新权重或进行长度归一化来改进原始算法。

关于RLHF,有两个重要的经验性发现需要注意:
- 过度优化:当持续优化策略以提升代理奖励时,模型可能会逐渐偏离真实的人类偏好。这类似于过拟合,在人类偏好数据存在噪声时尤为常见。
- 校准度降低:与监督微调不同,强化学习优化的策略不一定对应一个概率分布。因此,经过RLHF的模型在温度设置为1时,常常表现出过度自信的行为,校准度较低。

基于人类反馈的强化学习虽然强大,但存在扩展性差和优化困难的问题。这引出了我们的核心问题:是否存在更高效的强化学习方法?
转向:基于可验证奖励的强化学习


上一节我们指出了人类反馈的局限性。本节中,我们来看看一种更高效的替代方案:基于可验证奖励的强化学习。


其核心思路是:与其优化难以捉摸的人类偏好,不如在那些我们能够快速、准确、大规模评估真实奖励的领域应用强化学习。例如,在数学解题或代码生成任务中,答案的正确性是可以被自动验证的。这样,我们就能借鉴强化学习在其他领域(如AlphaGo)的成功经验。



接下来,我们将分两部分讲解:首先深入算法细节,然后分析三个大型推理模型的案例。



算法详解:从PPO到GRPO

为了理解GRPO为何存在及其优势,我们需要先简要了解其前身——近端策略优化算法。

PPO:强大的但复杂的算法
PPO是一种非常成功的通用强化学习算法。其核心思想是在策略更新中引入一个“信赖域”,通过裁剪概率比来防止新策略与旧策略差异过大,从而保证训练的稳定性。



PPO的目标函数(裁剪版本)如下:
L(θ) = E[ min( r(θ) * A, clip(r(θ), 1-ε, 1+ε) * A ) ]
其中 r(θ) 是新旧策略的概率比,A 是优势估计,ε 是裁剪参数。
然而,PPO的实现相当复杂,主要挑战在于:
- 需要价值函数:为了计算优势估计
A,通常需要训练一个额外的价值函数网络,这使内存消耗和实现复杂度翻倍。 - 众多实现细节:如广义优势估计、奖励塑形、KL散度计算等,细微的调整都可能显著影响性能。
GRPO:更简单的替代方案
PPO的复杂性促使人们寻找更简单的替代方案。广义信赖域策略优化应运而生,其动机和操作都非常简单。

GRPO的核心改进在于用更简单的组件替换PPO中复杂的优势估计 A。具体来说,GRPO为每个提示(组)采样多个回答,并计算组内奖励的Z分数作为优势估计:
A_i = (R_i - μ_group) / σ_group
其中 R_i 是第i个回答获得的奖励,μ_group 和 σ_group 分别是该组所有回答奖励的均值和标准差。
在纯在线(单步)设置下,GRPO的更新简化为标准的策略梯度形式,即根据Z分数调整每个回答的生成概率。
与PPO相比,GRPO的优势显而易见:
- 无需价值函数:直接使用组内统计量作为基线,省去了训练价值模型的麻烦。
- 实现简单:算法逻辑清晰,代码量大幅减少。
- 效果显著:在数学推理等任务上,GRPO表现出了与PPO相媲美甚至更优的性能。

GRPO的细节与修正

尽管GRPO很有效,但从理论角度看,其原始形式存在两个可能的问题:


- 除以标准差:策略梯度定理允许减去一个与采样无关的基线,但除以标准差在理论上并不严格成立。这可能会在奖励分布极端(全对或全错)时,过度放大更新权重。
- 长度归一化:原始GRPO对奖励进行长度归一化(
奖励/长度),这会激励模型在答错时生成长文本(以减少负奖励的绝对值),在答对时生成短文本(以增大正奖励的绝对值),可能导致模型行为异常。

研究表明,移除这两个操作(即仅减去均值,不做长度归一化)的GRPO变体,能在获得相近性能的同时,更稳定地控制输出长度。


案例研究:现代推理模型的构建


掌握了GRPO等算法后,我们现在可以剖析三个利用基于可验证奖励的强化学习构建的著名推理模型:DeepSeek-R1、Kimi-1.5和Qwen2.5-3B。
这些模型都遵循相似的流程:预训练模型 → 监督微调 → 基于可验证奖励的RL → 指令微调/RLHF。它们的成功验证了此路径的可行性。

DeepSeek-R1
R1的贡献在于以相对简单的方式复现了类似GPT-4o的性能。
- 核心方法:在数学、代码等可验证领域,使用基于结果的奖励(答案是否正确)和GRPO进行强化学习。
- 关键流程:
- 监督微调:使用链式思维数据初始化模型,使其具备长文本推理能力。
- 强化学习:在数学等领域应用GRPO,并添加“语言一致性奖励”以防止思维链中语言混用。
- 后续优化:进行常规的指令微调和基于人类反馈的强化学习。
- 重要结论:实验表明,过程奖励模型和蒙特卡洛树搜索等复杂技术,在当前阶段并非构建高性能推理模型的必要条件。

Kimi-1.5

Kimi-1.5与R1同期发布,取得了类似效果,但算法和侧重点有所不同。
- 强化学习算法:使用了一种更接近DPO风格的算法,但其梯度更新后形式与GRPO相似,也包含基线处理和策略正则化。
- 长度控制奖励:创新性地引入了长度控制奖励,在训练后期激励模型用更短的思维链获得正确答案,以优化推理成本。
- 数据与系统:详细阐述了数据筛选(如按难度分级、剔除通过率过高或过低的问题)和分布式训练架构,揭示了大规模RL训练的系统复杂性。

Qwen2.5-3B
作为较新的模型,Qwen2.5-3B在之前工作基础上进行了优化。
- 数据高效性:仅使用约4000个高质量样本进行强化学习就获得了显著提升,体现了该方法的数据效率潜力。
- 思维模式融合:其关键创新是思维模式融合。通过对同一模型进行两种微调(带“思考”标签和不带标签),使单个模型能够根据需求在“深度思考”和“快速响应”模式间切换,为用户提供了控制思考成本的旋钮。
- 权衡揭示:实验显示,在通用指令遵循任务上,RLHF能提升经过推理RL的模型性能;但在数学/编码任务上,RLHF有时会带来性能下降,这揭示了不同优化目标间存在的权衡。

总结 🎯
本节课中,我们一起学习了从基于人类反馈的强化学习向基于可验证奖励的强化学习的演进。

我们首先回顾了RLHF及其面临的过度优化和校准挑战。接着,我们探讨了PPO算法的原理与复杂性,并引出了更简单高效的替代方案——GRPO,分析了其算法核心及可能的改进。


最后,我们通过DeepSeek-R1、Kimi-1.5和Qwen2.5-3B三个案例,深入了解了如何利用基于可验证奖励的强化学习(特别是GRPO及其变体)来构建强大的现代推理模型。这些实践表明,在数学、代码等奖励明确的领域进行强化学习,是一条行之有效且相对简洁的模型能力提升路径。


课程17:对齐 - 强化学习2 🧠
在本节课中,我们将深入探讨策略梯度方法,特别是其在语言模型对齐中的应用。我们将从回顾强化学习的基本设置开始,然后详细讲解策略梯度、基线方法以及广义近端策略优化等核心概念,并通过一个简单的排序任务示例来展示其代码实现。
概述
上一节我们从可验证奖励的角度概述了强化学习,介绍了近端策略优化等算法。本节中,我们将深入探讨策略梯度及其变体的工作机制,包括如何通过引入基线来降低方差,以及如何在实际任务中应用这些方法。我们将结合数学公式和代码示例,让初学者能够理解并掌握这些核心概念。
强化学习设置回顾
首先,我们明确在语言模型中进行强化学习的设置。

- 状态:是提示加上目前已生成的响应。
- 动作:生成下一个特定的标记。
- 奖励:取决于整个响应的质量。在本课程中,我们聚焦于结果奖励,即奖励在生成完整响应后根据其正确性一次性给出。我们主要关注可验证的奖励,即通过确定性函数计算,无需人工评估。
在这种设置下,智能体(语言模型)生成一系列动作(标记)后获得一个奖励。这与过程奖励(在生成过程中获得多次奖励)不同。虽然奖励稀疏且有延迟,但概念上更清晰。
状态转移在语言模型中很简单:将动作(标记)添加到当前状态(文本)即可。这带来了巨大的优势,因为我们可以精确模拟“世界动态”,从而进行规划。

策略 是一个语言模型,它基于当前状态(提示和已生成响应)来生成下一个动作(标记)。它通常基于预训练模型进行微调。
我们的目标是最大化期望奖励。期望基于环境给出的提示分布以及策略生成的响应。
策略梯度方法
策略梯度是一类直接对策略参数求梯度以改进策略的方法。
为了简化符号,我们用 A 表示整个响应(动作序列)。在结果奖励设定下,我们可以将所有动作视为由语言模型一次性生成,然后获得奖励。
我们要最大化的目标是期望奖励 J(θ):
J(θ) = E_{s~p, a~π_θ(·|s)} [R(s, a)]
对其求梯度,应用策略梯度定理(实质上是对数函数的链式法则),我们得到:
∇_θ J(θ) = E_{s~p, a~π_θ(·|s)} [∇_θ log π_θ(a|s) * R(s, a)]
朴素策略梯度
朴素策略梯度直接使用上述公式的采样估计:
- 从分布中采样一个提示 s。
- 用当前策略 π_θ 采样一个响应 a。
- 计算奖励 R(s, a)。
- 按以下公式更新参数:θ ← θ + α * ∇_θ log π_θ(a|s) * R(s, a)
这类似于监督微调,但所有更新都由奖励 R 加权。
直观示例:如果奖励是二元的(0或1,代表错误或正确),那么朴素策略梯度只会对正确的响应进行更新,模仿那些正确的回答。主要挑战在于,当奖励稀疏(多数响应得0分)且策略较差时,梯度更新可能非常稀少,导致学习停滞。



基线方法

为了降低策略梯度估计的方差,我们引入基线。


核心思想是优化 R(s, a) - b(s) 的期望,而非单纯的 R(s, a)。其中 b(s) 是一个只依赖于状态 s 的函数。因为 E_{a~π}[b(s)] = b(s) 是一个常数,所以这种变换不改变优化目标,但可以显著影响方差。
策略梯度公式变为:
∇_θ J(θ) = E_{s~p, a~π_θ(·|s)} [∇_θ log π_θ(a|s) * (R(s, a) - b(s))]
如何选择基线 b(s)? 一个常见且有效的启发式选择是 b(s) = E_{a~π}[R(s, a)],即给定状态下期望奖励的估计。这引出了优势函数的概念:
A(s, a) = R(s, a) - E_{a‘~π}[R(s, a’)]
优势函数衡量了在状态 s 下采取动作 a 相对于平均表现有多好。此时,策略梯度就是在优化优势函数。
在实践中,我们无法精确计算期望,因此需要采样估计。

广义近端策略优化
广义近端策略优化是一种策略梯度算法,它天然适合语言模型设置,因为它利用了一个提示可以生成多个响应这一特性来自然分组并计算基线。
以下是其核心步骤的伪代码概述:
对于 每一轮训练:
对于 数据集中的每个提示 s:
使用当前策略 π_θ 生成 k 个响应 {a_1, ..., a_k}
计算每个响应的奖励 {r_1, ..., r_k}
计算奖励的均值 μ 和标准差 σ
计算每个响应的归一化优势估计:δ_i = (r_i - μ) / (σ + ε)
对于 每个响应 a_i:
计算重要性权重:ratio = π_θ(a_i|s) / π_θ_old(a_i|s)
计算裁剪后的目标:L_i = -min(ratio * δ_i, clip(ratio, 1-ε, 1+ε) * δ_i)
可选:添加KL散度惩罚项,使 π_θ 不要偏离参考模型 π_ref 太远
使用 L 的梯度更新策略参数 θ
关键点在于:
- 归一化:
(r_i - μ) / σ使更新幅度不受奖励绝对数值尺度的影响,并提供了组内的相对比较。 - 裁剪:防止重要性权重
ratio变化过大,导致训练不稳定。 - KL惩罚:作为一种正则化,防止策略在单次更新中偏离旧策略或参考策略太远。
实践示例:排序任务
为了具体理解,我们定义一个简单任务:对N个数字进行排序。
任务与环境
- 提示:一串N个未排序的数字,例如
[3, 1, 2]。 - 响应:一串N个数字,期望是排序后的结果,例如
[1, 2, 3]。 - 奖励函数设计:
- 稀疏奖励:若响应完全正确排序,奖励为1,否则为0。这会导致初期学习困难。
- 稠密奖励(本例采用):结合部分正确性给予奖励。
- 对响应中每个出现在正确排序结果中的数字,给予1分。
- 对响应中每一对已正确排序的相邻数字,再给予1分。
- 例如,提示
[3,1,2],正确响应[1,2,3]的奖励为:数字全匹配(3分) + 正确相邻对(1,2),(2,3)(2分) = 5分。
模型与训练
我们定义一个极度简化的模型:每个输出位置独立地基于输入提示预测一个数字。这虽然不符合自回归生成的实际,但极大简化了代码,便于演示核心算法。
以下是训练循环的核心结构代码:
# 伪代码,展示逻辑流程
for epoch in range(num_epochs):
# 1. 采样数据
prompts = sample_prompts(batch_size) # 例如 [[3,1,2], [4,2,5], ...]
all_responses = []
all_rewards = []
for prompt in prompts:
responses = model.generate(prompt, num_responses=k) # 生成k个响应
rewards = [compute_reward(prompt, resp) for resp in responses] # 计算奖励
all_responses.extend(responses)
all_rewards.extend(rewards)
# 2. 计算优势估计 (Deltas)
# 将奖励按提示分组,每组内进行归一化
deltas = compute_advantages(all_rewards) # 例如 (r_i - group_mean) / group_std
# 3. 多步优化(针对同一批采样数据)
for optimization_step in range(num_steps_per_batch):
total_loss = 0
for resp, delta in zip(all_responses, deltas):
# 计算当前策略下该响应的对数概率
log_prob_current = model.log_probability(resp)
# 计算旧策略下该响应的对数概率(从之前存储的或冻结的旧模型获取)
log_prob_old = old_model.log_probability(resp) # 旧模型参数需冻结
# 计算重要性比率
ratio = torch.exp(log_prob_current - log_prob_old)
# GRPO 裁剪损失
surr1 = ratio * delta
surr2 = torch.clamp(ratio, 1 - clip_epsilon, 1 + clip_epsilon) * delta
policy_loss = -torch.min(surr1, surr2).mean()
# 可选:KL散度惩罚
kl_penalty = compute_kl_penalty(model, reference_model, resp)
loss = policy_loss + beta * kl_penalty
total_loss += loss
# 4. 反向传播与更新
optimizer.zero_grad()
total_loss.backward()
optimizer.step()
通过这种训练,模型会逐渐学会生成排序更正确的序列。需要注意的是,奖励函数的设计至关重要,不合理的部分奖励可能导致模型陷入局部最优(例如,只学会输出包含正确数字但顺序不对的序列)。
总结
本节课我们一起深入学习了策略梯度方法在对齐语言模型中的应用。
- 回顾了设定:明确了在结果奖励模式下,用强化学习训练语言模型的状态、动作和奖励定义。
- 讲解了策略梯度:从最大化期望奖励的目标出发,推导出策略梯度定理,并指出了朴素策略梯度在稀疏奖励下的局限性。
- 引入了基线:通过减去一个只与状态相关的基线函数,可以显著降低梯度估计的方差,而使用期望奖励作为基线则引出了优势函数的概念。
- 介绍了GRPO:作为一种实用的策略梯度算法,它利用语言模型生成多个响应的特性,通过组内归一化来计算优势估计,并结合裁剪和KL惩罚来稳定训练。
- 通过实例演示:我们构建了一个简单的数字排序任务和模型,展示了从数据采样、奖励计算、优势估计到损失计算和参数更新的完整流程。


强化学习为超越模仿、直接优化复杂目标(如事实正确性、安全性、人类偏好)提供了强大框架。然而,构建一个可扩展且稳定的强化学习系统涉及推理、多模型管理和分布式计算等诸多工程挑战,这也是当前研究的前沿方向。


课程 2:PyTorch 高阶技巧与训练资源核算 🧮
在本节课中,我们将学习如何从零开始构建模型。我们将从PyTorch的张量基础入手,逐步构建模型、优化器和训练循环。课程的核心是效率,我们将密切关注计算和内存的资源消耗,并学习如何估算训练大型模型所需的资源。
张量基础与内存占用 💾
上一节我们概述了课程目标,本节中我们来看看深度学习的基础——张量。张量是存储一切数据(参数、梯度、优化器状态、数据、激活值)的基本单元。
数据类型与内存
每个张量都由特定数据类型(如浮点数)构成。不同的数据类型占用不同的内存空间。
以下是常见的浮点数类型及其内存占用:
- FP32 (32位浮点数 / 单精度):默认类型。占用 4字节。包含1位符号、8位指数、23位小数。
- FP16 (16位浮点数 / 半精度):占用 2字节。包含1位符号、5位指数、10位小数。动态范围较小,可能导致大数溢出或小数下溢。
- BF16 (Brain浮点数16):占用 2字节。包含1位符号、8位指数、7位小数。动态范围与FP32相同,精度较低,但更适合深度学习计算。
- FP8 (8位浮点数):占用 1字节。由英伟达开发,H100 GPU支持。有不同变体以权衡动态范围和精度。
内存计算公式:
内存占用(字节) = 张量元素数量 × 每个元素的字节数

例如,一个默认数据类型(FP32)的 4×8 矩阵:
内存 = 32个元素 × 4字节/元素 = 128字节
张量操作与视图
在PyTorch中,张量是指向已分配内存的指针。元数据(如形状、步长)定义了如何访问该内存。
许多操作(如切片、转置、view)创建的是视图而非副本,它们共享底层存储。修改原始张量会影响其视图。
import torch
x = torch.tensor([[1, 2, 3], [4, 5, 6]])
y = x[0] # y是x第一行的视图,不复制数据
z = x.transpose(0, 1) # z是x转置的视图,不复制数据
x[0, 0] = 999 # 修改x,y和z也会相应改变
contiguous() 操作可能创建副本,使张量在内存中连续存储。
张量计算与性能评估 ⚡
上一节我们介绍了张量的内存占用,本节中我们来看看张量的计算成本与性能评估。
爱因斯坦求和与维度命名
使用 einsum 或 einops 库可以更清晰、安全地指定复杂张量操作,避免依赖容易出错的维度索引。
# 传统方式:矩阵乘法,需跟踪维度顺序
# x.shape = (batch, seq1, hidden), y.shape = (batch, seq2, hidden)
attn_weights = torch.matmul(x, y.transpose(1, 2)) # 计算注意力权重
# 使用 einsum,维度意义一目了然
attn_weights = torch.einsum('b s1 h, b s2 h -> b s1 s2', x, y)
# 使用 einops 进行重组(例如,处理多头注意力)
from einops import rearrange, einsum
# 假设 x.shape = (batch, seq, num_heads * head_dim)
x_rearranged = rearrange(x, 'b s (h d) -> b s h d', h=num_heads)
# 后续操作...
计算成本:浮点运算次数
深度学习计算的核心是矩阵乘法。评估计算成本的关键指标是浮点运算次数。
矩阵乘法的FLOPs公式:
对于一个 (m, n) 矩阵与 (n, p) 矩阵的乘法:
FLOPs ≈ 2 × m × n × p
这是因为对于输出中的每个元素,需要进行 n 次乘法和 n 次加法。
在批量处理中,FLOPs相应倍增。对于简单的线性模型 Y = XW(X 形状为 (B, D),W 形状为 (D, K)):
前向传播 FLOPs ≈ 2 × B × D × K
性能评估:MFU
硬件有理论峰值性能(如H100对BF16约为 1979 TFLOPS)。实际运行时,我们关注模型浮点运算利用率。

MFU计算公式:
MFU = (实际完成的 FLOPs / 实际耗时) / 硬件理论峰值 FLOPS

MFU衡量了硬件计算能力的实际利用率。通常,MFU > 0.5 被认为是良好的。
梯度计算与反向传播成本 🔄
上一节我们讨论了前向传播的计算成本,本节中我们来看看反向传播(梯度计算)的成本。
对于具有 L 层、每层参数为 N 的模型,其前向和反向传播的总计算成本有一个经验规律。
总FLOPs经验公式:
总 FLOPs ≈ 6 × B × N_total_params
其中 B 是数据点(或令牌)数量,N_total_params 是模型总参数量。
这个“6倍”的由来:
- 前向传播:约
2 × B × N_paramsFLOPs。 - 反向传播:计算每个参数的梯度通常需要约
4 × B × N_paramsFLOPs(涉及梯度与激活值的矩阵乘法)。 - 两者相加:
2 + 4 = 6。

这就是课程开头估算训练700B参数模型所需时间时,使用 6 × 参数数量 × 令牌数量 计算总FLOPs的原因。



构建模型:初始化、训练与资源核算 🏗️
上一节我们分析了计算成本,本节中我们将把这些概念应用到实际的模型构建、训练和资源核算中。
参数初始化
不恰当的初始化会导致梯度爆炸或消失。常用方法是缩放初始化权重。
import torch.nn as nn
import math
d_input = 512
d_hidden = 1024
# 正确的初始化:缩放权重
layer = nn.Linear(d_input, d_hidden)
# Xavier/Glorot 初始化的一种简单形式
nn.init.normal_(layer.weight, mean=0.0, std=math.sqrt(1.0 / d_input))
# 或者使用PyTorch内置的xavier初始化
# nn.init.xavier_normal_(layer.weight)
优化器与状态内存

优化器(如Adam)需要存储额外的状态(如动量、梯度平方的移动平均),这增加了内存开销。

以下是训练时需要考虑的内存组成部分:
- 参数:模型权重本身。
- 梯度:反向传播为每个参数计算的梯度。
- 优化器状态:例如,对于AdamW,每个参数需要存储动量和方差两个状态,通常用FP32,所以是参数的2倍。
- 激活值:前向传播中需要为反向传播存储的中间结果。这是最大的变量,取决于批量大小和序列长度。
内存估算公式(以AdamW为例,混合精度训练):
总内存 ≈ 参数量 × (2 + 2 + 4 + 12) 字节
2:FP16参数(前向/反向传播)。2:FP16梯度。4:FP32的优化器状态(动量,2字节)和方差(2字节)。12:激活值(粗略估计,实际变化很大)。
因此,每个参数大约需要20字节的峰值内存。这就是课程开头估算8块80G H100显卡能训练多大模型时,用 80GB * 8 / 20字节 ≈ 320亿参数 的由来(未考虑激活值)。
训练循环与检查点
一个典型的训练循环包括前向传播、损失计算、反向传播和优化器更新。
model = MyModel().cuda()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
scaler = torch.cuda.amp.GradScaler() # 用于混合精度训练
for epoch in range(num_epochs):
for batch in dataloader:
inputs, targets = batch
inputs, targets = inputs.cuda(), targets.cuda()
optimizer.zero_grad()
# 混合精度前向传播
with torch.cuda.amp.autocast(dtype=torch.bfloat16):
outputs = model(inputs)
loss = loss_fn(outputs, targets)
# 混合精度反向传播与优化
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
# 定期保存检查点
if step % checkpoint_interval == 0:
torch.save({
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'step': step,
}, f'checkpoint_{step}.pt')
总结 📝
本节课中,我们一起学习了构建和训练深度学习模型的核心实践与资源核算:
- 张量与内存:理解了不同数据类型(FP32, BF16, FP8)的内存占用,以及张量视图与副本的区别。
- 计算成本:掌握了估算矩阵乘法FLOPs的方法(
2*m*n*p),以及评估实际硬件利用率的指标MFU。 - 反向传播成本:了解了训练总计算量约为
6 × 令牌数 × 参数量这一经验规律。 - 模型构建:实践了参数初始化的最佳实践,并实现了训练循环。
- 资源核算:学会了估算训练所需的内存,包括参数、梯度、优化器状态和激活值,认识到每个参数可能需约20字节峰值内存。


这些知识是高效训练大型模型(如Transformer)的基础。在接下来的作业中,你将把这些原理应用到实际的模型实现中。



斯坦福 CS336 课程笔记 P3:大模型架构设计与超参数调优 🏗️




在本节课中,我们将深入探讨构建大型语言模型(如ChatGPT)时,关于模型架构和超参数选择的核心知识。我们将从经典的Transformer架构出发,梳理其关键组件的现代变体,并学习如何根据经验法则和数据驱动的方法来选择超参数,以构建高效、稳定且强大的模型。





1. Transformer架构回顾与变体 🔄
上一节我们介绍了语言模型的基础。本节中,我们来看看Transformer架构的核心及其现代演变。

最初的Transformer架构包含位置嵌入、多头注意力、层归一化、残差连接、多层感知器(MLP)和Softmax输出层。然而,现代模型对此进行了多项改进。
以下是现代Transformer架构中几个关键的变体方向:

- 预归一化 vs 后归一化:现代模型普遍采用“预归一化”,即在每个子模块(如注意力、MLP)之前进行层归一化,这比原始Transformer的“后归一化”能带来更稳定的训练和更好的梯度传播。
- RMSNorm vs LayerNorm:许多模型使用RMSNorm(均方根归一化)替代标准的LayerNorm。RMSNorm不移除均值且不加偏置项,计算更高效,对性能影响甚微。
- 门控线性单元:现代前馈网络常使用门控线性单元,如SwiGLU或GeGLU,替代传统的ReLU或GELU激活函数。门控机制通常能带来持续的性能提升。
- 并行层设计:部分模型采用并行计算注意力层和MLP层,然后将结果相加,而非串行计算。这可以提高计算效率,但串行设计目前仍更为主流。
- 旋转位置编码:RoPE 已成为位置编码的主流选择。它通过旋转查询和键向量来编码相对位置信息,具有良好的外推性(处理更长文本的能力)。
2. 关键超参数的选择法则 🎛️


确定了架构之后,我们需要为模型选择具体的超参数。以下是基于大量成功模型经验总结出的核心法则。

以下是选择关键超参数的经验性指导:
- 前馈网络维度:对于标准ReLU MLP,通常将隐藏维度
d_ff设为模型隐藏维度d_model的 4倍。对于门控线性单元变体,为保持参数量大致不变,比例通常设为 约2.66倍(例如d_ff = (8/3) * d_model)。 - 注意力头维度:通常保持
d_model = n_heads * d_head的比例为 1:1。即增加头数时,每个头的维度相应减小,总参数量保持不变。 - 模型的宽高比:指模型深度(层数)与宽度(隐藏维度)的比例。研究表明,存在一个较优的范围(例如每层约128个隐藏维度),许多模型遵循类似的宽高比。
- 词汇表大小:早期单语模型词汇量约3-5万。现代多语言、生产级模型的词汇量趋向于 10万到25万 之间,以更好地处理多种语言和特殊符号。
- 正则化:在预训练中,权重衰减 被广泛使用。其主要作用并非防止过拟合(因为数据量极大),而是与学习率调度配合,在训练末期帮助模型更好地收敛,从而获得更低的训练损失。




3. 提升训练稳定性的技巧 ⚖️
随着模型规模增大和训练时间延长,训练稳定性变得至关重要。本节我们来看看几种有效的稳定性技巧。





以下是几种被证明有效的稳定性干预措施:

- Z-Loss:一种辅助损失函数,用于稳定输出层的Softmax。它通过惩罚归一化因子
Z的对数,使其接近1,从而改善数值稳定性。 - QK归一化:在注意力机制中,对查询和键向量在点积运算之前进行层归一化。这可以限制Softmax输入的幅度,防止梯度爆炸,最初在多模态模型训练中被采用。
- 注意力分数软限制:在计算注意力分数后,使用一个软性函数(如缩放的双曲正切函数)对其进行限制,防止极端值出现。



4. 注意力机制的高效变体 ⚡


标准的注意力机制在推理时可能效率低下。本节我们来看看为了优化推理性能而设计的注意力变体。

以下是两种重要的高效注意力变体:
- 多查询注意力与分组查询注意力:MQA 让所有注意力头共享同一套键和值,仅查询向量是多头的。GQA 是折中方案,将头分成若干组,组内共享键和值。这两种方法能显著减少推理时的KV缓存大小和内存访问,提升吞吐量。
- 混合注意力模式:为了处理极长的上下文,一些最新模型(如Llama 4)采用了混合注意力模式。例如,每隔几层使用一次全局注意力(无位置限制),而在其他层使用滑动窗口注意力(仅关注局部上下文)。结合RoPE,这种方法能有效平衡表达能力和长文本处理能力。

总结 📝

本节课我们一起学习了构建大型语言模型时架构与超参数的核心知识。

我们回顾了Transformer架构从经典到现代的演变,了解了预归一化、RMSNorm、门控线性单元和RoPE等成为主流的选择。我们掌握了选择前馈网络维度、注意力头配置、宽高比和词汇量等超参数的实用经验法则。我们还探讨了使用Z-Loss、QK归一化等技巧来提升大规模训练的稳定性。最后,我们了解了MQA/GQA和混合注意力模式等旨在优化推理效率和处理长文本的创新方法。





理解这些共识性的设计和选择,是亲手构建高效、强大语言模型的重要基础。



课程 P4:混合专家架构实战 🧠

在本节课中,我们将学习混合专家模型的核心概念、工作原理、设计选择以及如何在实际系统中高效地构建和训练它。混合专家模型是当前许多最先进高性能系统的构建与部署方式,理解它对于构建最佳模型至关重要。
概述
混合专家模型是一种特殊的神经网络架构,它通过稀疏激活多个子组件来提升模型容量和效率。我们将从基本概念出发,逐步深入到路由机制、训练技巧以及现代开源系统的具体实现。
混合专家模型简介
混合专家模型是一种奇特的架构,它有几个被称为“专家”的子组件,这些专家会被稀疏激活。其核心思想与多层感知器密切相关。
标准Transformer组件包括自注意力机制和全连接网络。在密集模型中,前馈层是一个大模块。在混合专家模型中,我们将这个大的前馈层进行拆分或复制,并用一个选择器层和多个较小的层来替换它。
以下是混合专家模型的基本原理:
- 你会有多个全连接网络副本。
- 你会有一个路由器,它在每次前向传播时,从这些副本中挑选出较少几个。
如果模型是稀疏激活的,例如每次只挑选一个专家,并且这个专家的大小与原始的密集全连接网络相同,那么密集模型和混合专家模型在计算量上是相同的。这样,你就能在不增加计算量的情况下,拥有更多参数。
混合专家模型的价值

对于相同浮点运算量的训练,与密集模型相比,混合专家模型通常能获得更佳的性能。
一篇2022年的论文指出,在浮点运算次数匹配的情况下,随着专家数量的增加,语言模型的训练损失会持续降低。专家越多,效果越好。
现代研究也证实了这一点。例如,AI2发表的ALO论文进行了精细对比,发现混合专家模型的训练损失下降速度比密集型模型快得多。
混合专家模型的一个显著优势是,它允许模型展示出非常吸引人的性能图表。例如,在DeepSeek V2论文中,模型仅激活少量参数,就在MMLU基准测试上取得了优异的性能。
混合专家模型的系统优势
混合专家模型为我们提供了另一种并行方式,称为专家并行。
当你拥有多个专家模块时,可以很自然地将它们放置在不同的计算设备上。由于专家是稀疏激活的,只需将令牌路由到合适的设备上进行计算即可。这对于大型模型的并行处理至关重要。
有趣的是,虽然谷歌开发了混合专家模型,但许多开源成果实际上常常来自中国。例如,Qwen和DeepSeek在混合专家架构方面做了大量工作。近期,西方的开源组织如Mixtral和最新的Llama也开始采用混合专家架构。
混合专家模型的核心组件
上一节我们介绍了混合专家模型的价值和系统优势,本节中我们来看看它的具体构成。混合专家模型架构与非混合专家模型架构在几乎所有组件上都相似,仅有一个关键组件不同:前馈层被替换为路由器和多个专家。
混合专家模型的基本形态是:将标准的全连接层进行拆分或复制,并在它们之间进行稀疏路由选择。虽然理论上也可以对注意力层进行类似操作,但在主流模型发布中,这种情况相当罕见。
以下是混合专家模型设计中几个会有所不同的方面:
- 如何进行路由:路由函数是一个重要选择。
- 专家规模与数量:需要多少专家,每个专家的规模多大。
- 如何训练路由器:路由决策不可微,训练起来具有挑战性。
路由机制详解
路由是混合专家模型的核心组件,它决定了如何将令牌匹配给专家。你大致有三种不同的路由选择:
- 令牌选择:每个令牌对不同专家有一定的路由偏好,为每个令牌选出前K个专家。
- 专家选择:每个专家对令牌有一定的排名偏好,为每位专家选出排名前K的令牌。这样做的一大好处是在专家间能保持负载平衡。
- 全局分配:解决复杂的优化问题,以确保专家与令牌之间的映射能保持某种平衡。
几乎所有现代的混合专家模型都采用令牌选择前K的做法。在这种机制中,每个令牌会按亲和度对专家进行排序,然后选择前K个。
令牌选择路由的表现通常优于专家选择路由。在AOL论文的消融实验中,令牌选择路由的验证损失下降更快。

路由器的参数设置通常非常基础。一个典型的令牌选择路由工作原理如下:


- 残差流输入
x进入路由器。 - 路由器计算
x与每个专家对应的学习向量e_i的内积,得到亲和度分数。 - 对亲和度分数应用 softmax 进行归一化。
- 选取归一化后权重最高的前K个专家。
- 使用这K个权重作为门控信号,对所选专家的输出进行加权求和。
- 将加权求和的结果加回原始残差流中。

代码描述路由前向过程:
# 假设输入 x 的维度为 [batch_size, hidden_dim]
# 专家参数 W_experts 的维度为 [num_experts, hidden_dim, expert_hidden_dim]
# 路由器参数 W_router 的维度为 [hidden_dim, num_experts]

scores = x @ W_router # 计算亲和度分数,[batch_size, num_experts]
weights = softmax(scores, dim=-1) # 归一化
topk_weights, topk_indices = torch.topk(weights, k=K, dim=-1) # 选取前K个
# 稀疏计算:仅计算被选中的专家输出
output = 0
for i in range(batch_size):
for j in range(K):
expert_idx = topk_indices[i, j]
gate = topk_weights[i, j]
expert_out = x[i] @ W_experts[expert_idx] # 专家前向计算
output[i] += gate * expert_out
# 残差连接
final_output = x + output
为什么必须使用 top-K 操作?因为在训练和推理时,我们必须确保只激活少量专家,以维持系统的计算效率。如果对所有专家进行门控,就会失去稀疏性带来的效率优势。
专家设计:细粒度与共享专家

基本的混合专家模型结构是复制专家。例如,如果你有前2的路由选择,激活参数会是原始密集模型的两倍。
人们很快意识到,拥有大量专家是好事。但为了不增加参数成本,DeepSeek 提出了细粒度专家的理念。

其做法是:将标准的全连接层拆分成更小的部分。例如,原本隐藏层维度会乘以4得到投影层,现在可以只乘以2,从而得到规模更小、数量更多的专家。这允许你在不显著增加计算量的情况下大幅增加专家数量。
另一个已被研究的方面是共享专家。其思路是,可能总有一些固定的处理操作是无论哪个令牌都需要的。因此,设置一个或几个共享专家来处理这些共享结构,而让其他专家专注于特定模式,可能是有益的。
自 DeepSeek MoE 以来,几乎所有开源的混合专家模型变体都采用了细粒度专家,有些也采用了共享专家。消融实验表明,细粒度专家通常能带来显著提升。
现代混合专家模型配置
现在,我们来看看近期发布的一些混合专家模型,了解常见的配置模式。
一些早期的谷歌论文(如 GShard, Switch Transformer)拥有大量的被路由专家(如256个)。很快,一个常见的阶段出现了,模型拥有8-16个专家,其中2个是活跃的(如 Mixtral, DBRX, Grok)。
后来,DeepSeek MoE 或 DeepSeek V1 出现了典型配置:64个细粒度专家,其中6个被主动路由,外加2个共享专家。每种专家的规模大约是标准专家规模的1/4。
其他中文混合专家模型,如 Qwen1.5 MoE, MiniMax,其配置与 DeepSeek V1 基本类似,都使用了细粒度专家,并常有共享专家。
最新的模型如 Llama 4 也采用了细粒度专家和共享专家。对于像 Llama 4 和 DeepSeek V3 这样的大模型,总的专家数量可以非常多。
训练挑战与技巧
训练混合专家模型的主要挑战在于路由决策的不可微性。我们不能在训练时激活所有专家,否则计算成本会过高。因此,我们需要训练时的稀疏性,但这带来了一个类似强化学习的优化问题。
实践中,人们主要采用以下方法:
- 强化学习:将路由视为策略进行优化。但这种方法梯度方差大、复杂度高,在大规模应用中并不流行。
- 随机探索:在路由分数中添加噪声,以鼓励探索。但这种方法在后期论文中一定程度上被摒弃,因为效果不如基于启发式损失的方法。
- 启发式损失(负载均衡损失):这是目前最主流的方法。其核心思想是添加一个辅助损失项,鼓励令牌在不同专家间均匀分配。
最经典的负载均衡损失来自 Switch Transformer (2022):
公式描述负载均衡损失:
L_balance = α * N * Σ_i (f_i * p_i)
其中:
N是专家数量。f_i是批次中分配给专家i的令牌比例(实际决策)。p_i是路由器分配给专家i的概率总和(原始意图)。α是一个超参数,控制平衡损失的权重。
这个损失函数会惩罚那些获得过多令牌的专家,促使路由器进行更均衡的分配。如果不进行专家平衡,模型往往会陷入局部最优,即只频繁使用一两个专家,而浪费其他专家,导致性能下降。
DeepSeek V3 对此进行了创新,采用了“辅助无损失平衡法”。他们去掉了针对每个专家的平衡损失项,改为为每个专家学习一个微小的偏置项 b_i。在每一批次中,根据专家获得的令牌数量在线调整这个偏置:如果某个专家得到的令牌不够,就增加其 b_i,使其更具吸引力;反之则减少。这个偏置仅用于路由决策,不用于门控权重。
系统考量与稳定性
混合专家模型能完美适配设备间的专家并行。你可以将不同专家安排在不同设备上。经过路由器后,令牌被发送到相关设备进行计算,然后再将结果返回合并。这为大规模训练提供了另一种并行维度。
混合专家模型在训练和推理时都可能引入随机性。例如,如果路由器给某个专家分配了过多令牌,可能会因为设备内存不足而出现“令牌丢弃”现象,即某些令牌不被任何专家处理,直接通过残差连接。这可能导致基于批次的推理结果出现非确定性。
混合专家模型的训练有时可能不稳定,微调也可能比较困难。以下是一些提升稳定性的技巧:
- 数值稳定性:在路由器的 softmax 计算中使用 float32 精度。
- Z-损失:添加一个辅助损失,惩罚 softmax 归一化前的 logits 的平方和,这有助于稳定训练。
- 过拟合问题:在少量数据上微调庞大的混合专家模型容易过拟合。解决方案包括:交替使用密集层和 MoE 层(只微调密集层),或者使用大量 SFT 数据。
- 升级回收:从一个训练好的密集模型出发,复制其前馈层作为专家初始化,然后添加一个随机初始化的路由器,再开始训练混合专家模型。这是一种获得高性能 MoE 模型的划算方法。
DeepSeek V3 架构纵览
最后,我们以 DeepSeek V3 为例,梳理一个现代高性能开源混合专家系统的组成部分。值得注意的是,其核心的混合专家架构自 DeepSeek V1 (MoE) 以来变化不大。
DeepSeek V1 (MoE) 起点:
- 参数:1600亿总参数,280亿激活参数。
- 架构:2个共享专家 + 64个细粒度专家,每次激活约6个。
- 路由:标准 Top-K 路由,在 Top-K 之前进行 softmax。
- 平衡:使用辅助损失进行专家级和设备级负载均衡。
DeepSeek V2 的演进:
- 参数:2360亿总参数,210亿激活参数。
- 架构相同。
- 新增 Top-M 设备选择:先限制令牌只能路由到前 M 个设备,再在每个设备内选 Top-K 专家,以控制通信成本。
- 新增 通信平衡损失:同时考虑输入和输出的通信成本进行平衡。
DeepSeek V3 的改进:
- 参数:6710亿总参数,370亿激活参数。
- 门控归一化:将 softmax 归一化操作移至 Top-K 之后,并对门控权重使用 sigmoid 函数。
- 平衡机制:采用前述的“辅助无损失平衡法”(基于偏置 b_i 的在线调整),并保留一个序列级别的辅助损失以确保推理时的鲁棒性。
- 保留了 Top-M 设备选择以优化系统性能。
除了混合专家部分,DeepSeek V3 还有其他创新:
- MLA (多头潜在注意力):一种优化 KV 缓存的方法。它将 Key 和 Value 投影到一个低维的潜在空间进行缓存,在需要时再投影回高维进行计算,并结合矩阵乘法的结合律避免额外计算开销。
- MTP (多令牌预测):在损失函数中并行预测未来多个令牌(实际上主要针对下一个令牌),使用一个轻量级的辅助预测头。
总结
本节课中,我们一起学习了混合专家模型。如今,它在一定程度上处于如何构建高性能大规模系统的核心。混合专家模型利用了稀疏性的概念,即并非所有参数都需要一直被使用。
其核心挑战在于离散的路由决策,而启发式方法(特别是负载均衡损失)在实践中被证明是有效的。大量实证证据表明,在计算量受限的情况下,混合专家模型是一个高效且性能优异的选择。


我们详细探讨了路由机制、专家设计(细粒度与共享)、训练技巧、系统考量,并以 DeepSeek V3 为例,剖析了一个现代开源系统的具体实现。理解混合专家模型,对于构建和优化前沿的大语言模型至关重要。


课程五:GPU原理与分布式训练基础 🚀

在本节课中,我们将要学习GPU的工作原理以及如何利用其特性进行高效的分布式训练。我们将从GPU的基本架构讲起,逐步深入到内存模型、性能瓶颈以及关键的优化技术。最后,我们会将这些知识整合起来,理解像Flash Attention这样的高性能算法是如何构建的。

概述:为什么硬件如此重要? 💡

深度学习性能的提升,很大程度上依赖于更快的硬件、更高的利用率和优化的并行化。计算资源的扩展速度远超内存带宽的增长,这使得理解硬件,特别是GPU,变得至关重要。本节课的目标是揭开GPU的神秘面纱,让你能够得心应手地思考如何加速算法。


GPU vs CPU:设计哲学的差异 ⚙️

上一节我们介绍了硬件对性能提升的驱动作用。本节中我们来看看GPU和CPU在设计目标上的根本区别。
CPU(中央处理器)是为优化延迟而设计的,目标是尽快完成单个任务。它拥有大型控制单元来处理复杂的分支和逻辑。


GPU(图形处理器)则是为优化吞吐量而设计的,不在乎单个任务的延迟,只希望所有任务能尽快完成。它拥有大量并行的计算单元(ALU),由一小部分控制逻辑协调。
核心公式:
- CPU:
高性能 = 低延迟 - GPU:
高性能 = 高吞吐量

GPU架构详解 🏗️
理解了设计目标后,我们来深入GPU的内部结构。

GPU的核心计算单元是流式多处理器。每个SM包含多个流式处理器,它们并行执行大量线程。其执行模型是单指令多线程:一个线程束(通常32个线程)对不同的数据执行相同的指令。


除了计算,内存是影响GPU性能的另一个关键因素。内存离SM越近,速度越快。GPU的内存层次结构如下(从快到慢):
- 寄存器:每个线程私有,速度最快。
- 共享内存/L1缓存:位于SM内部,块内线程共享。
- L2缓存:所有SM共享,位于芯片上。
- 全局内存:即显存,位于芯片外,速度最慢。

访问全局内存可能需要数百个时钟周期,而访问共享内存仅需几十个周期。因此,减少对全局内存的访问是性能优化的核心。

GPU执行与内存模型 🧠

现在,我们结合执行和内存模型来理解GPU如何工作。

GPU的执行涉及三个层次的粒度:
- 线程:最小的执行单位。
- 线程束:由32个连续编号的线程组成,是调度和执行的基本单位。
- 线程块:一组线程,被分配到一个SM上执行。
逻辑内存模型与硬件对应:
- 每个线程有自己的寄存器。
- 一个线程块内的线程共享共享内存。
- 所有线程都能访问全局内存,但速度很慢。
理想的编程模式是:将数据从全局内存加载到共享内存,在共享内存中进行密集计算,最后将结果写回全局内存。

TPU:另一种加速器视角 🤖
GPU并非唯一的AI加速器。TPU(张量处理器)在设计理念上与GPU有相似之处。
TPU同样包含:
- 矩阵乘法单元:专门执行矩阵运算的硬件。
- 高速片上内存:类似于共享内存。
- 高带宽内存:类似于全局内存。
主要区别在于,TPU的架构更专注于矩阵乘法,控制逻辑相对更简单。理解GPU的许多概念同样适用于TPU。
GPU性能优化技巧 🛠️
了解了基础原理,我们来看看如何让算法在GPU上跑得更快。性能瓶颈主要来自内存访问。
以下是关键的优化技巧列表:

1. 降低精度
使用FP16或BF16代替FP32,可以将内存传输量减半,从而有效提升吞吐量。通常采用混合精度训练,即用低精度存储和计算,用高精度进行累加以保持稳定性。
2. 内核融合
将多个连续的操作融合成一个内核,避免中间结果频繁写回和读取全局内存。

# 未融合:多个独立内核启动
y = torch.sin(x)
z = torch.cos(x)
out = y**2 + z**2

# 融合:单个内核完成所有计算
# (可通过 `torch.compile` 或手动编写CUDA内核实现)
3. 重计算
用额外的计算换取内存访问。在反向传播时不存储中间激活值,而是在需要时重新计算。这常用于节省显存和提升带宽利用率。

4. 内存访问合并
当线程束中的所有线程访问全局内存中连续对齐的内存块时,硬件可以合并这些访问,实现一次传输获取所有数据,极大提升带宽利用率。编程时应确保线程的访问模式是连续的。

5. 分块
将数据分割成小块,以便能装入快速的共享内存中。在块内进行大量计算,从而减少对全局内存的访问次数。
对于矩阵乘法 C = A @ B,如果分块大小为 T:
- 朴素算法:每个矩阵元素被访问约
O(N)次(从全局内存)。 - 分块算法:每个矩阵元素被访问约
O(N/T)次(从全局内存),其余O(T)次访问发生在共享内存中。



6. 对齐与填充
确保数据块的大小与内存突发传输边界(通常是32字节或128字节)对齐,可以避免低效的非对齐访问。有时需要给矩阵额外填充一些元素以达到合适的尺寸。

揭秘性能波动图表 📊
掌握了上述技巧,我们就可以解释课程开始时那个神秘的性能波动图表了。

图表中的波动主要由以下因素导致:
- 计算强度与内存墙:小矩阵时受内存带宽限制(屋顶线左侧上升区);大矩阵时受计算能力限制(屋顶线顶部平台区)。
- 分块效率:当矩阵维度不是分块大小的整数倍时,会产生利用率低的“尾块”,导致性能下降。
- SM利用率:线程块的数量略多于SM数量时,会造成负载不均衡和空闲等待,形成性能低谷。
- 对齐问题:矩阵维度不是内存对齐值的倍数时,会导致非合并访问,性能急剧下降。
因此,选择2的幂次方或与硬件对齐的维度(如64、128、256的倍数)通常能获得最佳性能。
案例研究:Flash Attention ⚡
最后,我们将所有知识融会贯通,看看高性能算法Flash Attention是如何实现的。

Flash Attention的核心目标是:在计算精确注意力时,实现对高带宽内存的访问次数低于二次方。
它主要应用了两项技术:
- 分块:将大的
Q, K, V矩阵分成小块,在共享内存中进行计算。 - 重计算:在前向传播时不存储
O(N^2)的注意力矩阵,在反向传播时重新计算。
关键突破:在线Softmax
标准的Softmax需要看到整行数据才能计算。Flash Attention使用了在线Softmax算法,可以逐块更新Softmax的归一化因子,从而允许在分块计算注意力时,无需将中间结果写回全局内存。

通过结合分块、重计算和在线Softmax,Flash Attention成功地将内存访问复杂度从O(N^2)降低到O(N^2 / T)(T为块大小),从而实现了巨大的速度提升和更长的上下文处理能力。
总结 🎯
本节课中我们一起学习了:
- GPU架构:其高吞吐量设计源于大量的SM和SIMT执行模型。
- 内存层次:理解寄存器、共享内存和全局内存的速度差异是性能优化的基础。
- 性能瓶颈:内存带宽是当前及未来GPU的主要瓶颈。
- 优化技巧:包括降低精度、内核融合、重计算、内存合并、分块和对齐。
- 实践应用:这些技巧共同构成了像Flash Attention这样的现代高性能Transformer算法的基础。


记住,要充分利用硬件,必须深入思考数据移动的效率,而不仅仅是计算操作的数量。将数据尽可能保留在高速内存中,是释放GPU全部潜力的关键。


课程6:内核优化与Triton框架应用 🚀


在本节课中,我们将学习如何为语言模型中的标准组件编写高性能的GPU代码。我们将从GPU基础回顾开始,然后学习基准测试与性能分析的方法,接着会分别使用CUDA和Triton框架编写内核,最后了解PyTorch的即时编译优化。通过本节课,你将掌握编写和优化GPU内核以加速深度学习模型的核心技能。





GPU架构与执行模型回顾 ⚙️



上一节我们介绍了课程的整体目标,本节中我们来回顾一下GPU的基本工作原理,这对于理解后续的优化技术至关重要。



当我们使用A100或H100这类GPU时,其内部包含多个流多处理器(SM)。每个流多处理器内部都有大量计算单元(如FP32单元),可以启动大量线程进行计算。

GPU拥有多层内存结构:
- 全局内存(DRAM):容量大,但速度慢。
- 缓存:速度比全局内存快得多。
- 寄存器文件:速度极快,每个线程都可以访问。在编写高性能GPU代码时,我们会大量使用寄存器。

GPU的执行模型基于线程块(Thread Block)和线程(Thread):
- 一个线程块会在单个流多处理器上调度,是执行的基本原子单元。
- 每个线程块内包含许多线程,实际的计算由这些线程完成。
- 线程块内的线程可以通过共享内存进行快速通信,但跨线程块的通信则开销很大。
线程被分组为连续的32线程块,称为一个波(Warp)。波会在一个SM中一次性全部执行。为了获得最佳性能,我们希望所有波都有等量的计算,并且线程块数量应远多于流多处理器数量。
一个核心性能概念是算术强度,其公式为:
算术强度 = 浮点运算次数 / 内存传输字节数
我们希望保持高算术强度,因为现代GPU的计算能力提升速度远快于内存带宽提升速度。许多操作是内存受限的,优化目标就是减少这种限制。
基准测试与性能分析 📊
上一节我们回顾了GPU的基础知识,本节中我们来看看如何评估代码性能。编写高性能代码的第一步是进行准确的基准测试和性能分析,以定位真正的瓶颈。
如何进行基准测试
基准测试用于测量代码的端到端运行时间。在测量时,有两点至关重要:
- 热身迭代:首次运行代码时,GPU需要编译指令、初始化等,会产生额外开销。进行几次热身迭代可以确保我们测量的是稳态性能。
- 设备同步:CPU和GPU是独立运行的。CPU发出GPU计算指令后不会等待其完成。使用
torch.cuda.synchronize()可以确保CPU等待GPU完成所有任务后再计时,从而获得准确的GPU执行时间。
以下是一个基准测试函数的示例代码:
import torch
import time
def benchmark(func, *args, warmup=5, trials=10):
# 热身阶段
for _ in range(warmup):
func(*args)
torch.cuda.synchronize()
# 正式测量阶段
start = time.time()
for _ in range(trials):
func(*args)
torch.cuda.synchronize()
end = time.time()
return (end - start) / trials
性能分析工具
性能分析能提供比基准测试更细粒度的信息,帮助我们了解时间具体花费在哪些操作或内核上。
PyTorch提供了内置的性能分析器。例如,分析一个矩阵加法操作,我们可以看到在Python层简单的 A + B 背后,实际调用了底层的CUDA内核 elementwise_add_kernel,并且还能看到内核启动和设备同步的开销。
对于更复杂的模型(如多层感知机MLP),可以使用更强大的工具如 NVIDIA Nsight Systems。它可以可视化GPU和CPU的执行时间线,清楚地展示出:
- CPU如何提前将CUDA内核命令排队发送给GPU。
- CPU和GPU之间的执行如何交错进行。
- 如果在训练循环中插入打印损失值的语句,会强制CPU等待GPU计算完成损失,从而可能破坏这种提前执行的流水线,引入CPU瓶颈。
核心要点:在优化代码前,务必使用分析器找到真正的性能瓶颈,避免在非关键部分浪费时间。
编写CUDA内核(C++风格) ⚡
上一节我们学习了如何定位性能问题,本节中我们动手编写高性能内核来解决它。我们将从一个具体的例子——高斯误差线性单元(GELU)激活函数开始。

首先,我们对比两种PyTorch实现:
- 手动实现:将GELU公式分解为多个PyTorch运算(如乘法、指数、双曲正切)。这会导致启动多个独立的CUDA内核,每个内核都有内存读写开销,性能较差(例如8.1毫秒)。
- PyTorch原生函数:
torch.nn.functional.gelu是一个融合内核,所有计算在一个CUDA内核中完成,性能极佳(例如1.1毫秒)。



我们的目标是接近原生函数的性能。首先,我们使用CUDA C++ API来编写融合内核。





CUDA内核结构

一个CUDA内核函数通常包含两部分:
- 设备端内核函数:在GPU上执行的函数,用
__global__关键字修饰。它负责具体的并行计算。 - 主机端包装函数:在CPU上运行的函数,负责准备数据、计算执行配置(网格和线程块大小)并启动内核。
以下是GELU的CUDA内核实现概览:



主机端包装函数 (gelu):
torch::Tensor gelu(torch::Tensor x) {
// 检查输入是否在GPU上且内存连续
TORCH_CHECK(x.is_cuda(), "Input must be a CUDA tensor");
TORCH_CHECK(x.is_contiguous(), "Input must be contiguous");
// 分配输出张量
auto y = torch::empty_like(x);
// 计算执行配置:线程块数量和每个块的线程数
int64_t n = x.numel();
const int threads = 1024; // 常见的块大小
const int blocks = (n + threads - 1) / threads; // 向上取整
// 启动CUDA内核
gelu_kernel<<<blocks, threads>>>(x.data_ptr<float>(), y.data_ptr<float>(), n);
return y;
}
设备端内核函数 (gelu_kernel):
__global__ void gelu_kernel(const float* x, float* y, int n) {
// 计算当前线程处理的全局索引
int i = blockIdx.x * blockDim.x + threadIdx.x;
// 边界检查:防止越界
if (i < n) {
float xi = x[i];
// 内联计算GELU近似公式
float y_i = 0.5 * xi * (1.0 + tanhf(0.79788456 * xi * (1 + 0.044715 * xi * xi)));
y[i] = y_i;
}
}

通过这种融合实现,我们将运行时间从8.1毫秒降低到了1.8毫秒,性能大幅提升。性能分析显示,现在只有一个 gelu_kernel 占用了几乎全部GPU时间。

使用Triton框架编写内核 🐬
上一节我们用C++编写了CUDA内核,虽然高效但较为繁琐。本节我们介绍一种更友好的方式——使用 Triton 领域特定语言(DSL)。
Triton允许我们用Python编写GPU内核,它自动处理许多底层细节,如内存访问合并、共享内存管理和线程同步,同时保持了高性能。
Triton编程模型
Triton的编程模型以线程块为中心,而不是单个线程。我们编写操作作用于整个块上的向量化代码。
以下是Triton版本的GELU内核:
内核函数 (gelu_kernel):
import triton
import triton.language as tl
@triton.jit
def gelu_kernel(x_ptr, y_ptr, n_elements, BLOCK_SIZE: tl.constexpr):
# 计算当前块负责的数据范围
pid = tl.program_id(axis=0) # 块ID
block_start = pid * BLOCK_SIZE
offsets = block_start + tl.arange(0, BLOCK_SIZE)
# 创建掩码,防止越界访问
mask = offsets < n_elements
# 从全局内存加载一个向量块到寄存器
x = tl.load(x_ptr + offsets, mask=mask)
# 计算GELU(向量化操作)
# 注意:Triton代码中需要使用tl.math.tanh等内置函数
y = 0.5 * x * (1.0 + tl.math.tanh(0.79788456 * x * (1 + 0.044715 * x * x)))
# 将结果存回全局内存
tl.store(y_ptr + offsets, y, mask=mask)
主机端启动代码:
def triton_gelu(x):
assert x.is_cuda and x.is_contiguous()
n_elements = x.numel()
y = torch.empty_like(x)
# 定义块大小(例如1024),并计算需要的块数
BLOCK_SIZE = 1024
grid = (triton.cdiv(n_elements, BLOCK_SIZE),) # 计算块数
# 启动内核
gelu_kernel[grid](x, y, n_elements, BLOCK_SIZE=BLOCK_SIZE)
return y
使用Triton,我们同样获得了约1.8毫秒的运行时间,与手写CUDA内核性能相当,但编写和调试的难度大大降低。我们还可以查看Triton编译生成的底层PTX代码,了解其如何将向量化操作映射到GPU线程和寄存器。
PyTorch即时编译(torch.compile)与总结 🎯
上一节我们使用Triton简化了GPU编程,本节我们看看能否更省力地获得高性能。现代深度学习框架的即时编译器已经非常强大。
PyTorch的 torch.compile 功能可以自动对模型代码进行优化,包括内核融合。对于我们的简单GELU例子,只需对原始的手动实现代码添加一个装饰器或进行编译:
@torch.compile
def manual_gelu_compiled(x):
return 0.5 * x * (1 + torch.tanh(0.79788456 * x * (1 + 0.044715 * x * x)))
编译后的版本运行时间可以达到约1.47毫秒,甚至优于我们手写的CUDA和Triton内核。分析器显示,它自动生成了一个融合的Triton内核。
何时需要手写内核?
那么,在 torch.compile 如此强大的情况下,我们何时还需要手写内核呢?
- 简单操作:对于像逐元素运算、简单规约等,编译器通常能做得很好,手动优化收益不大。
- 复杂创新优化:当你有全新的、复杂的计算模式(例如FlashAttention系列中的优化),这些模式可能难以被编译器自动发现。此时,使用Triton或CUDA手动实现可以释放硬件的全部潜力。
- 利用新硬件特性:针对最新GPU的特定硬件特性进行优化(如H100的TMA单元),手动编码可能更直接。
扩展:Triton实现Softmax
最后,我们简要看一个稍复杂的例子——Softmax。Softmax包含规约操作(求和),在Triton中实现也很直观。核心思想是让一个线程块处理输出矩阵的一行。
以下是实现思路:
- 执行配置:设置线程块大小等于或大于列数(向上取整到2的幂),网格大小等于行数。
- 内核逻辑:每个块加载一行数据到共享内存或寄存器,先找出该行最大值(规约),然后计算指数并求和(另一个规约),最后进行归一化并写回结果。
通过Triton,我们可以将Softmax实现为一个高效的单内核融合操作。


本节课总结:我们一起学习了高性能GPU编程的核心路径。我们从基准测试和分析开始,确保优化有的放矢。然后,我们掌握了两种编写融合内核的方法:使用CUDA C++ API进行底层控制和使用Triton DSL进行更高效的Python级开发。最后,我们认识到PyTorch的即时编译器(torch.compile)对于许多常见模式已经足够优秀。在实际工作中,应根据优化复杂度、开发效率和性能收益,在这些工具中做出明智选择。




课程7:分布式训练技术(上):数据与流水线并行 🚀

在本节课中,我们将要学习如何将大型语言模型的训练扩展到多台机器上。我们将从优化单个GPU的吞吐量开始,逐步理解当模型过大无法装入单个GPU时,如何利用多台服务器进行高效训练。这涉及到处理计算和内存两方面的挑战,以及不同机器间复杂的通信模式。

网络基础与硬件背景






上一节我们介绍了分布式训练的必要性,本节中我们来看看支撑这些算法的网络硬件基础。


GPU通常以集群形式部署。在一台物理机器(节点)内部,多个GPU通过高速互联(如NVLink)连接,通信速度极快。而当GPU需要与另一台机器上的GPU通信时,则需要通过网络交换机,其速度要慢得多。
这种硬件层级结构对我们如何拆分模型有重大影响:在单台机器内部,我们可以使用通信密集型的策略;而在跨机器时,则需要考虑通信开销更大的策略。

集合通信操作

在深入并行策略之前,我们需要了解一些基础的集合通信操作,它们是构建并行算法的基础。

以下是几种关键的集合通信原语:
- 全规约:所有节点都拥有数据,执行某种规约操作(如求和)后,将结果复制到所有节点。通信成本约为数据总量的两倍。
- 广播:将一个节点的数据复制到所有其他节点。通信成本约为数据总量的一倍。
- 规约散射:对所有节点的数据进行规约(如按行求和),但只将各部分结果分别留在不同的节点上。
- 全收集:每个节点将自己数据的一部分发送给所有其他节点,最终所有节点都拥有完整的数据集。


一个重要的等价关系是:一次全规约操作可以等价地拆分为一次规约散射加上一次全收集操作,且总通信成本不变。这个特性在后续优化中非常关键。


核心并行策略概述

面对内存和计算的限制,我们主要有三种并行策略来拆分模型和数据:
- 数据并行:在不同GPU上复制完整的模型参数,但将批次数据拆分到不同GPU上处理。
- 模型并行:将模型本身拆分到不同的GPU上,每个GPU只负责模型的一部分。
- 激活并行:专门管理前向传播过程中产生的、占用大量内存的激活值。


接下来,我们将详细探讨数据并行和模型并行中的流水线并行。





数据并行与ZeRO优化







数据并行是最直观的策略。在朴素的实现中,每个GPU保存完整的模型副本,处理批次数据的不同部分,然后同步梯度。其计算扩展性良好,但内存扩展性很差,因为每个GPU都需要存储完整的参数、梯度和优化器状态。



为了节省内存,微软提出了ZeRO(零冗余优化器) 系列优化。其核心思想是:并非所有状态都需要在每个GPU上保留完整副本。


以下是ZeRO优化的几个阶段:
- ZeRO-1(优化器状态分片):仅将优化器状态(如Adam中的一阶、二阶矩估计)分片到各个GPU上。参数和梯度仍被每个GPU完整保留。通信模式与朴素数据并行相同,但显著减少了内存占用。
- ZeRO-2(梯度分片):在ZeRO-1的基础上,进一步对梯度进行分片。在反向传播过程中,每计算完一层的梯度就立即进行规约散射,将其发送到负责该部分参数的GPU上,然后释放内存。总通信量不变,但需要更精细的调度。
- ZeRO-3(参数分片):在ZeRO-2的基础上,进一步对模型参数本身进行分片。每个GPU只保存一部分参数。在前向和反向传播过程中,按需通过全收集操作获取所需参数,计算后再释放。这实现了最大的内存节省(几乎所有状态都除以GPU数量),但通信成本增加(约为参数量的3倍)。FSDP(完全分片数据并行) 就是PyTorch对ZeRO-3的实现。


ZeRO-3的关键性能技巧是计算与通信重叠。通过预取技术,在GPU计算当前层时,后台已经开始通信获取下一层所需的参数,从而有效掩盖通信延迟。


模型并行:流水线并行

当模型过大,无法通过数据并行和ZeRO装入内存时,我们需要拆分模型本身。流水线并行是一种直观的模型并行方式。


其基本思想是:将模型的各层按顺序分配到不同的GPU上。一个GPU完成其负责层的计算后,将激活值(输出)发送给下一个GPU,依次类推,像工厂流水线一样。



然而,简单的按层划分会导致严重的“流水线气泡”问题,即大部分时间只有少数GPU在工作,利用率极低。


为了解决这个问题,我们引入了微批次的概念。将一个大批次拆分成多个微批次,依次注入流水线。这样,当第一个微批次流过第一层后,第二个微批次可以立即进入第一层,而第一个微批次则进入第二层,以此类推,使得多个GPU能够同时工作,减少了空闲时间。


流水线并行的效率公式大致为:效率 ≈ 1 - (流水线阶段数 - 1) / 微批次数量。因此,需要足够大的批次(微批次)大小才能有效掩盖气泡。

更先进的调度策略如“零气泡流水线”或“1F1B”(一前向一反向),通过将反向传播中权重梯度的计算与激活值的反向传播解耦并重新调度,可以进一步填充气泡,但实现起来非常复杂。


本节总结


本节课中我们一起学习了分布式训练的上半部分,重点介绍了数据并行及其ZeRO优化,以及模型并行中的流水线并行。

- 数据并行易于实现,但内存消耗大。ZeRO优化通过分片优化器状态、梯度和参数,在保持通信效率的同时,极大地扩展了可训练模型的大小。
- 流水线并行通过按层划分模型来扩展内存,但其效率严重依赖于足够大的批次大小来减少“气泡”。它是一种在跨机器慢速链路上常用的策略。



我们了解到,批次大小是一种关键资源,它可以被用于数据并行(增加并行度)或流水线并行(减少气泡),需要根据实际情况进行权衡。同时,硬件网络拓扑(机器内高速 vs. 机器间低速)直接影响我们对并行策略的选择。




在下节课中,我们将继续探讨另一种模型并行策略——张量并行,以及如何管理激活内存,最后学习如何将这些并行策略组合起来,形成高效的3D并行训练方案。


课程 8:分布式训练技术(下):模型与张量并行 🚀
在本节课中,我们将要学习如何利用多个GPU进行高效的分布式模型训练。我们将深入探讨模型并行与张量并行的核心概念,并通过代码示例理解其实现原理。课程内容将涵盖集合通信操作、数据并行、张量并行以及流水线并行等关键技术。

概述:硬件与通信基础

上一节我们介绍了单个GPU内的并行优化技术。本节中,我们来看看如何在多个GPU甚至多个计算节点之间组织计算,以克服数据传输瓶颈,保持高算术强度,让GPU持续高效运转。
现代深度学习硬件通常由多个GPU节点组成,每个节点包含多个GPU。数据需要在GPU间传输,而通信速度往往远慢于计算速度,因此优化数据传输是关键。

硬件层级结构遵循从快到慢、从小到大的原则:
- L1缓存:位于单个GPU的流式多处理器(SM)内,速度极快,容量极小。
- 高带宽内存(HBM):位于单个GPU上,容量更大。
- NVLink:连接同一节点内的不同GPU,速度高于PCIe。
- NVSwitch:在英伟达生态系统中,用于跨节点直接连接GPU,绕过以太网。

我们的目标是组织计算,尽可能让数据在高速缓存中处理,避免频繁访问低速的HBM或进行跨设备通信。
第一部分:集合通信操作
集合通信操作是分布式编程的基础原语,它提供了比手动管理点对点通信更优的抽象。以下是核心操作介绍:
以下是主要的集合通信操作类型:
- 广播(Broadcast):将某个设备(如
rank 0)上的数据分发到所有其他设备上。 - 散射(Scatter):将一个数据集合的不同部分分发到不同的设备上,每个设备获得的值不同。
- 收集(Gather):散射的逆操作,将不同设备上的数据收集到一个设备上。
- 规约(Reduce):与收集类似,但会对数据进行某种可结合、可交换的操作(如求和
sum、求最大值max),然后将结果放在一个设备上。 - 全收集(All-Gather):对所有设备执行收集操作,使得所有设备都拥有完整的数据集合。
- 全规约(All-Reduce):相当于规约后接全收集,所有设备最终都拥有规约后的结果。
- 规约散射(Reduce-Scatter):类似于规约,但规约后的结果会像散射一样分布到不同设备上。
记忆技巧:
- 规约意味着执行求和、求平均等操作。
- 广播、散射是收集的反向操作。
- 全(All-)表示操作的目标是所有设备。
在PyTorch中的实现
PyTorch的torch.distributed库为这些操作提供了高级接口,并支持多种后端(如用于GPU的NCCL,用于CPU的gloo)。
以下是一个使用all_reduce(全规约)和reduce_scatter(规约散射)的代码示例:
import torch
import torch.distributed as dist
def run_collective_ops(rank, world_size):
# 初始化进程组
dist.init_process_group(backend='nccl', rank=rank, world_size=world_size)
# 示例:All-Reduce
tensor = torch.tensor([0, 1, 2, 3]) + rank
print(f"Rank {rank} before all_reduce: {tensor}")
dist.all_reduce(tensor, op=dist.ReduceOp.SUM) # 对所有设备的tensor求和
print(f"Rank {rank} after all_reduce: {tensor}")
# 示例:Reduce-Scatter
input_tensor = torch.tensor([0, 1, 2, 3]) + rank
output_tensor = torch.zeros(1)
dist.reduce_scatter(output_tensor, [input_tensor for _ in range(world_size)], op=dist.ReduceOp.SUM)
print(f"Rank {rank} after reduce_scatter output: {output_tensor}")
dist.destroy_process_group()
性能基准测试
了解操作的带宽性能至关重要。我们可以通过测量传输时间和数据量来计算有效带宽。
对于all_reduce操作,总传输数据量约为 2 * world_size * tensor_size,因为每个设备既要发送自己的数据,也要接收规约后的结果。
def benchmark_all_reduce(rank, world_size):
dist.init_process_group(...)
tensor_size = 100_000_000
tensor = torch.randn(tensor_size, device='cuda')
# 预热
dist.all_reduce(tensor)
torch.cuda.synchronize()
dist.barrier()
start = torch.cuda.Event(enable_timing=True)
end = torch.cuda.Event(enable_timing=True)
start.record()
dist.all_reduce(tensor)
end.record()
torch.cuda.synchronize()
elapsed_time_ms = start.elapsed_time(end)
# 计算带宽 (GB/s)
bytes_per_element = tensor.element_size() # 例如 float32 为 4
total_bytes_transferred = 2 * (world_size - 1) * tensor_size * bytes_per_element / world_size # 简化模型
bandwidth_gbs = (total_bytes_transferred / 1e9) / (elapsed_time_ms / 1000)
print(f"Bandwidth: {bandwidth_gbs:.2f} GB/s")
第二部分:分布式训练策略
现在我们将集合通信应用于实际的模型训练。主要有三种并行策略:数据并行、张量并行和流水线并行。我们将在一个简单的多层感知机(MLP)上演示它们的基本实现。
1. 数据并行 (Data Parallelism) 📊
在数据并行中,模型被完整地复制到每个GPU上,但每个GPU处理批次数据的不同子集(切片)。反向传播后,需要同步所有GPU上计算出的梯度。
核心步骤:
- 将全局批次数据沿批次维度分割,每个设备获得一个本地小批次。
- 每个设备用完整的模型对其本地数据进行前向和反向传播,计算本地梯度。
- 使用
all_reduce操作对所有设备上的梯度进行求和或平均。 - 每个设备使用同步后的梯度更新其本地模型参数。
由于所有设备使用相同的梯度更新,模型参数始终保持一致。
def data_parallel_mlp(rank, world_size, batch_size=128, num_dim=1024, num_layers=4):
# 1. 数据分割
local_batch_size = batch_size // world_size
start_idx = rank * local_batch_size
end_idx = start_idx + local_batch_size
data = torch.randn(batch_size, num_dim).cuda()
local_data = data[start_idx:end_idx] # 每个设备获取数据的一部分
# 2. 定义模型 (每个设备都有完整的模型副本)
model = [torch.randn(num_dim, num_dim, requires_grad=True).cuda() for _ in range(num_layers)]
optimizer = torch.optim.SGD(model, lr=0.01)
# 3. 训练步骤
for step in range(10):
# 前向传播
x = local_data
for layer in model:
x = torch.relu(x @ layer)
loss = x.sum() # 示例损失
# 反向传播
optimizer.zero_grad()
loss.backward()
# 4. 梯度同步 (关键步骤!)
for param in model:
dist.all_reduce(param.grad, op=dist.ReduceOp.AVG) # 平均梯度
# 参数更新
optimizer.step()
print(f"Rank {rank}, Step {step}, Loss: {loss.item()}")
2. 张量并行 (Tensor Parallelism) ⚖️
在张量并行中,单个模型层(如MLP中的矩阵)被分割到多个GPU上。每个GPU只持有参数的一部分,并计算激活值的一部分。为了进行下一层的计算,需要聚合所有GPU的中间结果。
核心步骤(以单层矩阵乘法 Y = X @ W 为例):
- 将权重矩阵
W沿列维度分割,每个设备持有W_part。 - 每个设备用完整的输入
X与自己的W_part计算,得到输出的一部分Y_part。 - 使用
all_gather操作收集所有设备上的Y_part,拼接成完整的输出Y,用于下一层计算。
这种方法通信密集,需要高速的GPU间互联。
def tensor_parallel_mlp_forward(rank, world_size, batch_size=128, num_dim=1024):
# 1. 模型参数分割
local_dim = num_dim // world_size
# 假设有一层,权重被分割
local_weight = torch.randn(num_dim, local_dim, requires_grad=True).cuda()
# 2. 输入数据 (完整数据)
x = torch.randn(batch_size, num_dim).cuda()
# 3. 本地计算
local_output = x @ local_weight # 形状: [batch_size, local_dim]
# 4. 全局收集激活值
gathered_outputs = [torch.zeros_like(local_output) for _ in range(world_size)]
dist.all_gather(gathered_outputs, local_output)
# 5. 拼接得到完整输出
full_output = torch.cat(gathered_outputs, dim=-1) # 形状: [batch_size, num_dim]
print(f"Rank {rank}, local output shape {local_output.shape}, full output shape {full_output.shape}")
return full_output
3. 流水线并行 (Pipeline Parallelism) 🚰
在流水线并行中,模型的不同层被放置在不同的GPU上。一个批次的数据被拆分成多个微批次(micro-batches)。GPU像工厂流水线一样工作:当一个GPU完成对当前微批次的计算后,立即将结果发送给下一个GPU,并开始处理下一个微批次。
核心步骤:
- 将模型按层分组,每组分配到不同的设备。
- 将批次数据划分为微批次。
- 设备0处理第一个微批次,然后将结果发送给设备1,同时开始处理第二个微批次。
- 设备1收到设备0的结果后开始计算,以此类推,形成流水线。
基础实现容易产生“流水线气泡”(设备空闲等待)。优化策略包括精心安排微批次、重叠通信与计算等。
def pipeline_parallel_mlp(rank, world_size, batch_size=128, num_dim=1024, num_layers=4, num_micro_batches=4):
layers_per_device = num_layers // world_size
my_layers = [torch.randn(num_dim, num_dim).cuda() for _ in range(layers_per_device)]
micro_batch_size = batch_size // num_micro_batches
data = torch.randn(batch_size, num_dim).cuda()
micro_batches = torch.chunk(data, num_micro_batches)
for i, micro_batch in enumerate(micro_batches):
if rank == 0:
x = micro_batch
else:
# 接收来自上一个设备的激活值
x = torch.zeros_like(micro_batches[0])
dist.recv(x, src=rank-1)
# 本地层的前向传播
for layer in my_layers:
x = torch.relu(x @ layer)
if rank != world_size - 1:
# 发送给下一个设备
dist.send(x, dst=rank+1)
else:
# 最后一个设备,得到最终输出
final_output = x
总结与展望
本节课我们一起学习了分布式训练的核心技术。我们首先回顾了集合通信操作及其在PyTorch中的使用,它们是构建分布式训练的基础。然后,我们深入探讨了三种主流的模型并行化策略:
- 数据并行:复制模型,分割数据。实现简单,但要求每个GPU都能容纳整个模型。
- 张量并行:分割模型层,需要频繁聚合激活值,对通信带宽要求高。
- 流水线并行:分割模型层组,按流水线处理微批次,需要精细调度以减少空闲时间。
这些策略可以组合使用,以训练超大规模的模型。在实际应用中(如训练Transformer),框架如PyTorch的FSDP、Megatron-LM或JAX的自动分片功能,会帮助我们管理复杂的并行逻辑和通信优化。


分布式训练的本质是在计算、内存和通信之间进行权衡。硬件在不断进步,但模型规模的增长和对效率的追求,使得理解这些底层并行技术始终具有重要意义。


斯坦福 CS336 课程笔记:手把手构建 ChatGPT - 第九讲:缩放定律 1 🧮

在本节课中,我们将要学习缩放定律。缩放定律是构建大型语言模型时,用于预测模型性能随计算资源、数据量和模型规模变化的核心工具。理解它,能帮助我们在不进行昂贵的大规模训练之前,就做出关键的工程决策。

缩放定律的背景与动机 🎯
上一节我们介绍了模型架构和超参数选择。本节中我们来看看,当我们拥有大量计算资源时,如何科学地规划训练,而不是盲目地“抄袭”现有模型。
想象一个场景:你有一位富有的朋友,每月提供10万个H100 GPU,目标是构建最好的开源大语言模型。你拥有基础设施、训练框架和预训练数据集。问题在于,如何创新而非模仿?答案就在于利用缩放定律。

缩放定律的核心是:通过训练一系列小模型,观察其性能与资源(如数据量、模型大小)的关系,并拟合出预测规则。然后,我们可以将这些规则外推,以预测更大模型的行为,从而指导超参数选择、架构决策和资源分配。

缩放定律的历史与理论基础 📜
缩放定律并非新概念。其思想根源可以追溯到统计机器学习理论。
在经典机器学习理论中,我们关心泛化误差如何随样本数量 N 增加而下降。例如,对于有限假设集合,泛化误差的上界与 1/√N 成正比。这本身就是一种理论上的“缩放定律”,它预测了误差衰减的速率。
对于更复杂的非参数模型(如拟合一个平滑的密度函数),误差衰减遵循所谓的非参数速率,其形式上界为 N^{-β/(2β+1)},其中 β 与函数平滑度有关。
关键跨越在于,缩放定律从这些理论上的上界,转向了实证可观测的规律。我们通过实验发现,模型的实际损失与资源量之间,存在稳定、可预测的关系。
早在1993年,贝尔实验室的一篇论文就提出了类似思想:在不训练完整模型的情况下,预测分类器的性能。其提出的误差公式(不可约误差 + 多项式衰减项)已初具现代缩放定律的形态。
数据缩放定律 📈
数据缩放定律研究测试损失如何随训练数据量增加而变化。这是最直观的缩放类型。
核心观察

当我们在双对数坐标图(两个坐标轴都取对数)上,绘制数据集大小 N 与测试损失 L 时,会发现它们呈线性关系。
这意味着损失 L 与数据量 N 之间存在幂律关系:
L(N) ≈ C * N^{-α}
其中 C 是常数,α 是斜率(指数)。取对数后:log L = log C - α * log N,这正是线性关系。



为何幂律是自然的?

以下两个例子说明,幂律衰减是统计估计中自然出现的现象。

- 估计均值:假设数据来自高斯分布,任务是用样本估计总体均值。估计的均方误差为 σ²/N。这显然是关于 N 的幂律(指数为 -1)。取对数后得到线性关系:
log(误差) = -1 * log N + 2 log σ。

- 非参数回归:考虑一个更复杂的场景——在二维单位方格中估计一个平滑的回归函数。一种简单方法是把空间划分为小格子。如果有 N 个样本,大约有 √N 个格子,每个格子约有 √N 个样本,其估计误差约为 1/√N。推广到 d 维空间,误差约为 N^{-1/d}。这解释了为什么对于复杂任务(如语言建模),我们观察到的指数 α 远小于 1(例如 0.095),因为这反映了任务内在的高维度复杂性。

结论:数据缩放定律的斜率 α,在一定程度上揭示了学习任务的内在难度或内在维度。
数据缩放定律的应用
基于数据缩放定律的可预测性,我们可以进行多种工程优化:
以下是数据缩放定律的几个实际应用方向:
- 最优数据混合:通过在小模型上试验不同数据混合比例,观察其对损失曲线(主要是截距)的影响,可以预测该混合在大规模时的效果,从而找到最优数据配方。
- 数据重复使用分析:研究重复使用数据(多轮训练)的收益递减规律。缩放定律可以修改为包含“有效数据量”的概念,帮助决定何时需要新数据而非重复旧数据。
- 数据选择:在资源有限时,无需在大模型上测试所有数据,可以在小模型上利用缩放定律外推,筛选出高质量数据。
模型缩放定律与架构选择 🤖
上一节我们介绍了数据如何影响性能。本节中我们来看看,模型规模(参数量)和模型架构本身如何缩放,以及如何利用这种关系做选择。
核心方法:比较缩放曲线
对于任何想要测试的架构或超参数(例如:Transformer vs LSTM, Adam vs SGD, 不同的深度/宽度比),我们可以:
- 在多个不同计算规模(从小型到中型)上训练该配置的模型。
- 在双对数图上绘制计算量 FLOPs 与测试损失的关系。
- 观察其缩放曲线(斜率)以及与基线(如标准Transformer)的垂直偏移。
分析:
- 斜率:如果两条曲线斜率相同,说明它们缩放效率一致。扩大规模时,它们的相对优劣关系保持不变。
- 截距/偏移:曲线之间的恒定垂直差距,代表了一个架构相对于另一个的恒定效率因子。例如,如果LSTM的曲线始终比Transformer的曲线高一个固定量(在对数尺度上),意味着要达到相同性能,LSTM需要付出恒定倍数(如15倍)的计算成本。
应用实例
以下是利用模型缩放定律进行分析的具体案例:
- 架构比较:早期研究表明,Transformer的缩放曲线显著优于LSTM。近期研究也通过类似方法,验证了像门控线性单元(GLU) 和混合专家(MoE) 这样的架构,其缩放曲线与Transformer基线相当或更优,这为当前的研究方向提供了依据。
- 优化器比较:同样,可以通过缩放曲线比较Adam和SGD等优化器,发现它们之间存在恒定的计算效率差距。
- 超参数选择(如深度与宽度):通过在不同模型规模下绘制“损失 vs. 宽高比”曲线,可以发现一个宽高比的稳定区间。在这个区间内,不同规模模型的最优值相近,这意味着我们可以从小规模实验中确定一个适用于大规模模型的稳健超参数。
重要提示:进行参数缩放分析时,通常使用非嵌入参数。嵌入参数的行为不同,将其计入会扭曲缩放曲线的形状。

批量大小与学习率的缩放 🔧
当我们扩大模型规模时,批量大小和学习率是需要特别调整的两个超参数。缩放定律帮助我们理解如何协调它们。
批量大小的缩放



批量大小存在一个临界点(临界批量大小)。在临界点之前,增大批量大小几乎能带来线性的加速收益(因为相当于并行进行了更多梯度步骤)。超过临界点后,收益急剧递减。

关键发现是,临界批量大小与目标损失有关。当训练后期损失目标更小时,需要更精确的梯度,因此临界批量大小会增大。这解释了为什么在实际训练(如LLaMA)中,我们经常看到训练中后期动态增加批量大小的策略。
通过缩放定律分析,我们可以预测对于给定的计算预算,最优的批量大小是多少,从而在系统并行效率与优化效果之间取得平衡。

学习率的缩放
对于标准Transformer,最优学习率随模型宽度增加而减小。经验上,学习率应与宽度的某个负幂次成比例(例如 ~1/宽度)。

更先进的方法是重新参数化模型,例如使用μP(Maximal Update Parametrization)。通过对初始化和前向传播进行与宽度相关的缩放,可以使最优学习率在不同规模的模型间保持稳定。这意味着,我们只需在最小的模型上调整一次学习率,就可以直接将其应用到巨大型号上,而无需复杂的预测。
联合缩放定律:数据、模型与计算的权衡 ⚖️
到目前为止,我们分别讨论了数据和模型的缩放。但最核心的工程问题是:给定固定的计算预算(总FLOPs),我们应该用来训练一个更大的模型,还是用于收集/训练更多数据?

这就是联合缩放定律要解决的问题。
联合缩放公式
研究者(如Rosenfeld, Kaplan)提出了同时包含数据量 N 和参数量 P 的损失公式。一个典型形式是:
L(N, P) = (A / N^α) + (B / P^β) + L∞
其中 A, B, α, β 是常数,L∞ 是不可约误差。这个公式在实证中拟合得非常好。

Chinchilla 分析:寻找最优配比

DeepMind的Chinchilla论文是联合缩放定律的经典应用。它旨在为给定的计算预算 C(总FLOPs)找到最优的参数量 P 和数据量(令牌数) D。因为 C ≈ 6 * P * D(训练一个Transformer的近似计算量)。


他们采用了三种方法:
- 最小包络法:绘制不同
(P, D)组合在训练过程中的损失曲线,取所有计算预算下的最低损失点,发现这些最优点形成一条幂律线,从中推导出最优P和D。 - 异构FLOPs分析法(最常用):固定几个总计算预算
C,对于每个C,训练不同P(对应不同D)的模型,得到一条损失 vs. 参数量的曲线。取每条曲线的最低点,这些最优点也遵循缩放规律,从而得到最优的P(C)和D(C)。 - 函数拟合法:直接使用联合缩放公式拟合大量
(N, P, L)数据点,从拟合出的曲面中推导最优解。
结论:三种方法最终都指向相似的结论,即对于训练计算最优的大语言模型,每个参数大约需要20个令牌(例如,一个700亿参数的模型,应在约1.4万亿令牌上训练)。这比早期工作(如GPT-3每个参数约2个令牌)的数据效率高得多。


后续发展:随着业界更关注推理成本,趋势是进一步增加“令牌数/参数量”的比例,用更多的前期训练计算换取更小、更高效的最终模型,以降低部署成本。最新的模型已在每个参数使用远超20个令牌的数据进行训练。



注意事项与总结 ⚠️

重要注意事项

- 损失 vs. 下游任务:缩放定律在预测训练损失(困惑度) 方面非常可靠。然而,下游任务的能力(如问答、代码生成)的缩放规律可能不那么平滑和可预测,可能会出现“涌现”现象或不同的趋势。切勿将两者混淆。
- 适用范围:缩放定律通常在模型处于“幂律区”(即未达到数据或模型容量饱和的区域)时表现良好。需要避免进入“高原区”。
本节课总结
在本节课中,我们一起学习了缩放定律的核心思想与应用:

- 动机:缩放定律允许我们通过训练一系列小模型,来预测大模型的行为,从而指导昂贵的训练决策。
- 数据缩放:测试损失与训练数据量在双对数图上呈线性关系(幂律)。这源于统计估计理论,其斜率反映了任务的内在难度。
- 模型与架构缩放:通过比较不同配置(架构、优化器、超参数)的缩放曲线,我们可以基于它们在小规模下的相对效率和缩放斜率,选择那些能高效扩展到大规模的方案。
- 批量大小与学习率:扩大规模时,需要协调调整批量大小和学习率。μP等重参数化方法可以稳定学习率。
- 联合缩放(Chinchilla):对于固定计算预算,存在数据量和模型大小的最优配比,经典结论是“每个参数20个令牌”。这优化了从训练计算到模型性能的转换效率。



掌握缩放定律,意味着你掌握了在构建大型语言模型时,进行科学规划和资源分配的强大工具。


浙公网安备 33010602011771号