详细摘要 摘要
生成:2025-05-13 16:46摘要详情
- 音频文件
- Stanford CS336 Language Modeling from Scratch | Spring 2025 | 02 Pytorch, Resource Accounting
- 摘要类型
- 详细摘要
- LLM 提供商
- openai
- LLM 模型
- gemini-2.5-pro-exp-03-25
- 已创建
- 2025-05-13 16:46:22
摘要内容
概览/核心摘要 (Executive Summary)
本讲座(Stanford CS336, Spring 2025)深入探讨了从零开始构建语言模型所需的PyTorch核心组件和资源核算方法。核心目标是让学习者理解模型构建的底层机制,并掌握内存与计算效率的量化分析。讲座首先通过估算大规模模型(如700亿参数模型)训练时间和单机(8卡H100)可训练最大模型规模(约400亿参数,不含激活)来强调资源核算的重要性,指出效率直接关系到成本。
讲座详细介绍了PyTorch中的张量(Tensor)及其不同浮点数表示(float32, float16, bfloat16, fp8)对内存占用的影响,强调bfloat16在动态范围和内存效率上的优势。接着,讨论了计算资源(CPU/GPU)、张量操作(视图、复制、einops库)及其计算成本(FLOPs)。核心结论是矩阵乘法(matmul)主导计算量,其FLOPs约为2 * M * K * N。讲座引入了模型FLOPs利用率(MFU)作为衡量硬件效率的指标。关于梯度计算,指出反向传播的计算量约是前向传播的两倍,因此训练一个参数的总FLOPs约为6 * num_tokens * num_params。最后,概述了模型参数初始化、自定义模型构建、数据加载、优化器(以Adagrad为例说明其状态内存需求,Adam优化器通常需要额外存储两倍参数量的状态,即参数、梯度、优化器状态共需约16字节/参数,不含激活)、训练循环和混合精度训练等实践环节。
PyTorch基础与资源核算动机
Speaker 1首先回顾了上节课关于语言模型概览及从零构建原因的内容,并提及了Tokenization。本讲座聚焦于实际模型构建,涵盖PyTorch原语、模型、优化器和训练循环,并特别关注效率,即内存和计算资源的使用。
资源评估的启发性问题
为了引出资源核算的重要性,讲座提出了两个可通过“餐巾纸计算”(napkin math)解答的问题:
-
训练时长估算:在1024张H100上训练一个700亿参数的稠密Transformer模型,使用15万亿Token,需要多长时间?
- 计算方法:
- 总FLOPs ≈
6 * num_parameters * num_tokens - H100单卡FLOPs/秒(假设MFU为0.5)
- 计算集群总FLOPs/天
- 总FLOPs / 集群每日FLOPs ≈ 144天
- 总FLOPs ≈
- “这个6倍参数量乘以Token数的公式来源,将在本次讲座中讨论。”
- 计算方法:
-
最大可训练模型规模估算:在8张H100(每张80GB HBM显存)上,使用AdamW优化器(不采用特别技巧),能训练的最大模型参数量是多少?
- 计算方法:
- 每个参数所需字节数(参数、梯度、优化器状态):16字节 (讲座后续会解释来源)
- 总可用显存 / 每参数字节数 ≈ 400亿参数
- “这是一个非常粗略的估算,因为它没有考虑激活值(activations)的显存占用,激活值大小取决于批量大小(batch size)和序列长度(sequence length)。”
- 计算方法:
Speaker 1强调:“效率是关键(efficiency is the name of the game)。要做到高效,你必须确切知道你实际消耗了多少FLOPs,因为当这些数字变得巨大时,它们直接转化为美元,而你希望这个成本尽可能小。”
学习目标
- 机制 (Mechanics):PyTorch及其底层工作原理。
- 思维模式 (Mindset):资源核算。
- 直觉 (Intuitions):目前主要关注机制和思维模式,关于模型性能的直觉暂为宏观层面。
内存核算 (Memory Accounting)
张量 (Tensors)
- 深度学习中存储一切(参数、梯度、优化器状态、数据、激活值)的基础构建块。
- 创建方式多样,包括未初始化创建。
浮点数表示与内存占用
-
float32(fp32 / 单精度):- 32位:1位符号,8位指数,23位尾数。
- 每个元素占 4字节。
- 被认为是计算领域的“黄金标准”,在机器学习中常被称为“全精度”(尽管科学计算领域可能使用
float64或更高精度)。 - 内存计算:
num_elements * element_size_in_bytes。 - 示例:一个
4x8的float32张量占用32 * 4 = 128字节。 - GPT-3中一个前馈层权重矩阵(
2048 x 8192)约占用 2.3GB。
-
float16(fp16 / 半精度):- 16位:1位符号,5位指数,10位尾数。
- 内存减半,计算通常也更快。
- 缺点:动态范围(dynamic range)有限,容易出现上溢(overflow)或下溢(underflow),例如
1e-8在float16中会变成0。不适用于大型模型训练或需要高精度表示的场景。
-
bfloat16(bf16 / Brain Float):- 由Google于2018年开发,专为深度学习设计。
- 16位:1位符号,8位指数,7位尾数。
- 特点:与
float16占用相同内存,但拥有与float32相同的动态范围,牺牲了尾数精度。 - 深度学习对动态范围的需求高于精度,因此
bf16表现良好。1e-8在bf16中不会是0。 - “bf16基本上是进行计算时通常会使用的数据类型,因为它对于前馈和反向传播计算来说足够好。”
- 注意:存储优化器状态和参数本身通常仍推荐使用
float32以免训练不稳定。
-
fp8(8位浮点数):- 由Nvidia于2022年开发。
- 位数极少,表示非常粗略。存在两种变体,侧重于不同方面(更高分辨率或更大动态范围)。
- H100 GPU支持
fp8。
-
混合精度训练 (Mixed Precision Training):
- 根据模型不同部分的需求选择最低可用精度。
- 例如:参数和优化器状态使用
float32,前向传播和反向传播中的矩阵乘法使用bf16,某些如Attention的敏感部分可能仍用float32。 - Speaker 1观点:“目前来看,在深度学习中你可能不想使用float16。”
计算核算与硬件 (Compute Accounting & Hardware)
CPU 与 GPU 数据传输
- PyTorch张量默认在CPU上创建和存储。
- 使用GPU进行计算必须显式将张量转移到GPU:
tensor.to('cuda')或创建时指定device='cuda'。 - 数据从CPU RAM传输到GPU HBM(High Bandwidth Memory)需要时间。
- “在PyTorch中处理张量时,应始终清楚它驻留在哪里。”
硬件概览
- 示例中使用的GPU:NVIDIA H100,拥有80GB HBM。
张量操作与视图 (Tensor Operations & Views)
- 张量的本质:PyTorch中的张量是指向已分配内存区域的指针,并包含元数据(如
stride)来解释如何索引该内存。 - 视图 (Views):许多操作(如索引、
tensor.view()、tensor.transpose())不会创建数据的副本,而是创建指向相同底层存储的新“视图”。- 修改视图会影响原始张量,反之亦然。
tensor.storage().data_ptr()可检查两个张量是否共享底层存储。
- 连续性 (Contiguity):
- 如果张量在内存中是按其逻辑顺序连续存储的,则为连续张量。
- 转置等操作可能导致非连续张量。
- 对非连续张量执行某些视图操作可能会失败。此时需先调用
tensor.contiguous(),这会创建一个数据的副本使其连续。 tensor.reshape()行为类似tensor.contiguous().view()。
- 建议:视图操作本身开销很小(不分配新内存),可多加使用以提高代码可读性。但需注意
contiguous()或reshape()可能触发数据复制。
创建新张量的操作
- 元素级操作(element-wise operations)通常会创建新张量。
torch.triu()(上三角矩阵) 是一个元素级操作,常用于创建因果注意力掩码 (causal attention mask)。
矩阵乘法 (Matrix Multiplications / Matmuls)
- 深度学习的核心运算。
- 支持批处理:例如,一个
(batch_size, seq_len, input_dim)的张量与一个(input_dim, output_dim)的权重矩阵相乘,PyTorch会自动处理为在每个batch和sequence position上进行独立的矩阵乘法,结果为(batch_size, seq_len, output_dim)。
einops 库
- 受爱因斯坦求和约定启发,用于以更清晰、不易出错的方式处理张量维度操作。
- 动机:避免使用难以理解的负数索引(如
transpose(-2, -1))。 - 核心思想:为维度命名。
jax typing库可用于在类型注解中以字符串形式声明维度名称(如Float[Tensor, "batch seq_len hidden_dim"]),但PyTorch本身不强制执行。
einsum(fromeinops):- 用于执行带维度名称的矩阵乘法或更广义的张量缩并。
- 示例:
einsum(x, y, 'b s1 h, b s2 h -> b s1 s2')表示将两个张量x(batch, seq1, hidden) 和y(batch, seq2, hidden) 沿hidden维度相乘并求和,保留batch,seq1,seq2维度。 ...可用于表示任意数量的前导广播维度。- Speaker 1观点:“一旦你习惯了它,它会比使用-2、-1索引好得多。” 并且
torch.compile可以有效地编译einsum操作。
reduce(fromeinops): 用于按名称聚合维度。rearrange(fromeinops): 用于重排、拆分或合并维度,类似更强大的view。- 示例:将
batch seq (heads hidden_dim)拆分为batch seq heads hidden_dim。
- 示例:将
张量操作的计算成本 (FLOPs)
- FLOP (Floating Point Operation):指单个浮点运算(如加法、乘法)。衡量计算量。
- FLOPs/sec (Floating Point Operations per second):衡量硬件速度。本课程避免使用大写S的FLOPs,而明确写为
FLOPs/sec。 - 数量级参考:
- GPT-3训练约消耗
3.23e23FLOPs。 - GPT-4训练(推测)约消耗
2e25FLOPs。 - 美国曾有行政命令要求超过
1e26FLOPs 的基础模型需向政府报告(后被撤销)。欧盟AI法案中有1e25FLOPs 的阈值。
- GPT-3训练约消耗
- 硬件性能:
- A100峰值性能(fp16/bf16):312 TFLOPs/sec。
- H100峰值性能(fp16/bf16):1979 TFLOPs/sec (带稀疏性),约一半即 ~990 TFLOPs/sec (不带稀疏性)。
fp32在H100上的性能远低于此。- 稀疏性说明:NVIDIA宣传的稀疏性加速特指“结构化稀疏性(structured sparsity),即每4个元素中有2个为0”,Speaker 1评论:“没人用它,这是市场部门的说法。”
- 矩阵乘法的FLOPs:对于
(M, K) @ (K, N) -> (M, N),FLOPs ≈2 * M * K * N(每个输出元素需要K次乘法和K-1次加法,近似为2K)。- “你应该记住,如果你在做矩阵乘法,FLOPs数量是三个维度乘积的两倍。”
- 其他操作的FLOPs通常与张量大小成线性关系,远小于大型矩阵乘法。因此,性能分析常聚焦于矩阵乘法。
壁钟时间与模型FLOPs利用率 (MFU)
- MFU (Model FLOPs Utilization) = (实际有效FLOPs / 实际耗时) / 硬件理论峰值FLOPs/sec。
- 衡量模型在特定硬件上实际利用硬件计算能力的程度。
- MFU > 0.5 通常被认为是好的。
- MFU通常在矩阵乘法占主导时较高。
- 实验数据 (H100, 32x32x32 matmul):
float32: 耗时0.16s, 实际FLOPs/sec 5.4e13, H100float32理论峰值 67 TFLOPs/sec, MFU ≈ 0.8。bfloat16: 耗时0.03s, 实际FLOPs/sec更高。但相对于bfloat16的理论峰值(约990 TFLOPs/sec),MFU可能反而较低(示例中为0.26),表明理论峰值有时过于乐观。
- 建议:始终对代码进行基准测试,不要假设能达到理论性能。
梯度与反向传播的计算成本
- 两层线性网络示例:
Y = W2 @ (W1 @ X)- 前向传播FLOPs:
2 * num_tokens * (params_W1 + params_W2)。对于简单线性模型,可简化为2 * num_tokens * total_params。 - 反向传播FLOPs:
- 计算
dL/dW2涉及类矩阵乘法,FLOPs ≈2 * B * D * K(假设W2是D*K)。 - 计算
dL/dH1(H1是W1的输出) 涉及类矩阵乘法,FLOPs ≈2 * B * D * K。 - 对W1重复此过程。
- 核心结论:反向传播计算量大约是前向传播的两倍。
- “前向传播是2倍参数量,反向传播是4倍参数量。” (这里的参数量指的应是与数据点数相乘后的总计算量基数)
- 计算
- 总FLOPs (训练一步):前向 + 反向 ≈
(2 + 4) * num_tokens * num_params=6 * num_tokens * num_params。- 这解释了讲座开头估算训练时长时使用的
6x因子。 - 此规则对许多模型(包括Transformer,当序列长度不过大时)大致成立。
- 这解释了讲座开头估算训练时长时使用的
- 前向传播FLOPs:
构建模型 (Building Models)
参数 (Parameters)
- 在PyTorch中,模型参数通常存储为
torch.nn.Parameter对象,它们是会自动记录梯度的张量。
参数初始化 (Parameter Initialization)
- 问题:朴素的高斯初始化(如
torch.randn)可能导致激活值随网络深度增加而爆炸或消失,因为方差会累积(输出标准差约与sqrt(hidden_dimension)成正比)。 - 解决方案 (Kaiming/Xavier 初始化思想):按
1 / sqrt(input_dimension)缩放初始权重。- 例如:
W = nn.Parameter(torch.randn(in_dim, out_dim) / math.sqrt(in_dim)) - 这有助于使输出激活值的方差保持在1附近。
- 例如:
- 额外技巧:截断正态分布(truncated normal),例如将值限制在
[-3, 3]标准差范围内,以避免极端值。
自定义模型示例:Cruncher (深度线性网络)
- 一个包含
num_layers个线性层(矩阵乘法)的简单模型。 - 使用
nn.Module和nn.ModuleList构建。 - 参数量计算:若每层为
D x D矩阵,共L层,加一个D维输出头,则总参数为L * D*D + D。 - 模型和数据都需移至GPU (
model.to('cuda'),data.to('cuda'))。
随机性管理 (Randomness)
- 随机性来源:参数初始化、Dropout、数据打乱顺序等。
- 最佳实践:始终为每个随机源传递固定的随机种子 (
random_seed) 以保证可复现性。torch.manual_seed(),numpy.random.seed(),random.seed()。
- “确定性是调试时的朋友。”
训练循环组件 (Training Loop Components)
数据加载 (Data Loading)
- 语言模型数据通常是Token ID序列(整数)。
- 内存映射 (Memory Mapping):对于非常大的数据集(如Llama的2.8TB数据),一次性加载到内存不可行。可使用
numpy.memmap,它允许像操作内存数组一样操作磁盘上的文件,数据按需加载。 torch.utils.data.Dataset和torch.utils.data.DataLoader用于高效批处理数据。
优化器 (Optimizers)
- 常见优化器回顾:
- SGD (Stochastic Gradient Descent):沿负梯度方向更新。
- Momentum:引入梯度的一阶动量(指数移动平均),加速收敛并抑制震荡。
- Adagrad (Adaptive Gradient Algorithm):为每个参数维护一个梯度的平方和,并用此来调整学习率(梯度大的参数学习率减小)。
- RMSProp (Root Mean Square Propagation):Adagrad的改进,使用梯度的平方的指数移动平均,避免学习率过早衰减。
- Adam (Adaptive Moment Estimation):结合了Momentum(一阶矩估计)和RMSProp(二阶矩估计)。是目前广泛使用的优化器。
- 实现自定义优化器 (以Adagrad为例):
- 继承
torch.optim.Optimizer。 - 在
step()方法中:- 遍历
self.param_groups中的参数。 - 访问参数的梯度 (
param.grad)。 - 更新优化器状态(
self.state[param]),例如Adagrad中累积梯度平方和 (g_squared_sum)。 - 根据优化算法更新参数值 (
param.data.add_())。 - (可选)在
step()结束时zero_grad()(或在训练循环中显式调用)。
- 遍历
- 继承
- 优化器状态的内存需求:
- Adagrad:为每个参数额外存储一个浮点数(梯度平方和)。
- Adam:为每个参数额外存储两个浮点数(一阶矩和二阶矩)。
- 因此,使用Adam时,除了参数本身和梯度,优化器状态大约需要两倍于参数量的内存。
- 总计(不含激活):参数(P) + 梯度(P) + Adam状态(2P) = 4P。若为
float32(4字节/值),则每个参数约需 16字节。
完整模型资源需求总结 (以简单线性网络为例)
- 参数 (Parameters):
D*D * num_layers + D(假设fp32,每个参数4字节) - 激活 (Activations):
batch_size * D * num_layers(假设fp32,每个激活值4字节)。- “为什么需要存储激活值?朴素地看,因为在反向传播时,计算前一层的梯度依赖于后一层的激活值。如果更聪明些,可以不必存储所有激活值,可以通过重新计算来获得,这是一种称为激活检查点(activation checkpointing)的技术。”
- 梯度 (Gradients):与参数量相同。
- 优化器状态 (Optimizer States):Adagrad为1倍参数量,Adam为2倍参数量。
- 总内存 =
4 bytes * (num_params + num_activations + num_gradients + num_optimizer_states) - 总FLOPs ≈
6 * num_tokens * num_params
模型检查点 (Checkpointing)
- 语言模型训练耗时长,易中断。
- 应定期保存模型状态(
model.state_dict())和优化器状态(optimizer.state_dict())以及当前迭代次数到磁盘,以便恢复训练。
混合精度训练进阶 (Mixed Precision Training)
- 权衡:高精度(准确、稳定,但昂贵) vs. 低精度(便宜,但可能不稳定)。
- 建议:
- 默认使用
float32。 - 尽可能尝试
bf16甚至fp8。 - 一种常见策略:前向/反向传播使用低精度(如
bf16),参数更新和主权重保持float32。
- 默认使用
- PyTorch提供
torch.cuda.amp(Automatic Mixed Precision) 工具简化此过程。 - 前沿研究:探索全程使用
fp8进行训练,挑战在于数值稳定性控制。模型设计与硬件特性协同发展,例如NVIDIA芯片对低精度(如int4)的支持可能驱动新的模型架构。 - 训练 vs. 推理:训练对精度要求更高。模型训练完成后,在推理时可以采用更激进的量化策略(如
int8,int4) 以获取性能提升。
总结与展望
讲座系统梳理了从张量到完整训练循环的PyTorch原语,并重点讲解了内存和FLOPs的核算方法。通过作业一(Assignment 1)对Transformer模型进行类似的分析,将有助于巩固这些概念。