计算性能:训练大规模模型的基础设施
本章来源:本文整理自《动手学深度学习》(zh.d2l.ai)第 12 章《计算性能》,原书作者 Aston Zhang、李沐、Zachary C. Lipton、Alexander J. Smola。
本章讲什么
深度学习对算力的需求没有上限。好的设计可以在性能上造成数量级的差距——一周 vs 三个月训完一个模型。本章讲清楚性能的物理基础:硬件、编程范式、并行计算,以及怎么用多 GPU 甚至多台机器训练。
硬件基础
计算机由 CPU、内存(RAM)、PCIe 总线、网络、持久存储组成。理解性能,关键是理解两个词:带宽(传输速率)和延迟(等待时间)。
内存的特性:首次读取(发送地址)约 100 纳秒,后续连续传输每项仅 0.2 纳秒——随机访问比顺序读取慢约 500 倍。所以应尽量顺序读取,避免随机访问。
存储分层:
- HDD(机械硬盘):约 100 IOPS,毫秒级延迟,便宜但慢且易坏。
- SSD(固态硬盘):IOPS 高 3 个数量级、带宽高一个数量级,但按块写、有写入磨损。
- 云存储:可按需配置 IOPS。
CPU:核心执行指令,含分支预测(同时执行多条路径、丢弃错的分支)、向量处理单元(一个时钟周期执行多个运算)和缓存层次(寄存器 → L1 → L2 → L3,缓存越大延迟越高)。缓存未命中代价很高。
GPU:深度学习成功的关键。大量计算核心 + 高带宽内存。注意训练与推断需求不同——训练要存中间结果、要高精度,推断只需前向、可低精度。GPU 还针对小矩阵运算有专门的张量核优化。
网络与总线:PCIe(点对点高带宽,微秒延迟)、以太网(低成本长距离但慢)、NVLink(GPU 间超高速互连)。
贯穿全章的原则:传输数据要"量大次少",矢量化是性能关键。就像搬砖——一次搬一车比一次搬一块高效得多。
从延迟数字表可以直观感受层级差距:从 L1 缓存(1.5 纳秒)到主存(约 100 纳秒)到硬盘随机读取(10 毫秒),每跳一个层级慢几个数量级,就像从"桌上随手拿"到"下楼去仓库找"到"跨城邮寄"。
编译与编程范式
两种编程范式对比:
- 命令式编程(Python 默认):逐语句执行、随时改变状态,方便易调试,但效率低——Python 解释器单线程是瓶颈,逐个操作发送给后端会让 GPU 空闲。
- 符号式编程:先完整定义计算流程,编译成可执行程序再执行。编译器能看到全部代码,可以合并重复计算、释放无用内存、把程序移植到非 Python 环境。代价是难调试。
"逐句口述 vs 写好剧本再演":命令式像口述者说一句演一句(灵活但慢),符号式像先写好完整剧本、排练优化后再公演(快但改起来麻烦)。
混合式编程(PyTorch 的 torch.jit、TensorFlow 的 tf.function 等)结合两者:先用命令式开发和调试,再编译成符号式程序获得性能和可移植性。效果是执行速度可提升数倍,还便于模型序列化部署。
异步计算
深度学习框架普遍采用"前端-后端"解耦的异步计算模型:Python 前端只把操作指令排队送入后端(C++ 实现)的任务队列,后端用自己的线程收集并执行,前端不必等每次计算完成就继续发指令。
关键机制是依赖图:后端跟踪计算图中各步骤的依赖关系,相互依赖的操作不能并行,独立操作可并行。
但异步也有陷阱——阻塞器:打印变量、转 NumPy、取标量等操作会强制前端等待对应变量算完,频繁做这类小数据转换会破坏性能。实践建议每个小批量做一次同步,保持前后端大致同步。
"餐厅点菜":前端像服务员只负责下单(不用亲自下厨),后厨(后端)按菜单依赖关系并行做菜。阻塞器像顾客非要盯着某道菜出锅才点下一道,频繁催促反而拖慢整体上菜速度。
自动并行
深度学习框架后端构建计算图,系统能自动识别无依赖的任务并并行执行,无需用户手写并行代码。
两类自动并行:
- 多设备并行:单个操作会用满单个设备的所有资源,所以真正的并行发生在多个设备(多 GPU)之间——框架自动把两个设备上的计算同时调度。
- 计算与通信并行:在多设备间移动数据时,不必等所有结果算完再统一复制——某些参数的梯度比其他的更早算好,可以在 GPU 还在算后面的结果时,提前复制前面的,让通信与计算重叠进行。
"两条流水线同时开工":两个 GPU 像两条独立生产线互不干扰;"边做边发货":像工厂一边继续生产,一边把做好的先发出去,不用等全部做完再统一发货。
多 GPU 训练
多 GPU 并行训练有三种拆分方式:
- 模型并行(跨层拆分):把网络层分到不同 GPU,前一个算完传给下一个。能处理超大网络,但层间同步密集、接口数据传输量大、负载难平衡,不推荐。
- 层内拆分:把单层的通道分到多个 GPU 并行算。显存线性扩展,但每层依赖所有层的结果、需要大量屏障同步,也不推荐。
- 数据并行:所有 GPU 持有相同参数副本、执行相同工作,每个 GPU 处理小批量的一部分,各自算出梯度,聚合梯度后广播给所有 GPU 再更新参数。最简单、可扩展,只需在每个小批量后同步一次。
"三种分工方式":模型并行像流水线分工(每人负责一道工序,串起来才完整,一处慢全线等);数据并行像每人拿同一份教材教不同学生,最后汇总学生成绩调整教材(互不干扰、只需期末汇总)。
数据并行的具体流程:把小批量均分成 k 份 → 每 GPU 独立算损失和梯度 → allreduce 聚合梯度(把各 GPU 梯度求和并广播回所有 GPU)→ 每个 GPU 用自己的参数副本更新。
注意两个细节:批量大小应随 GPU 数扩大 k 倍,学习率也需相应调大。
参数服务器:多机分布式训练
把多 GPU 扩展到多台机器,核心概念是参数服务器。
复习数据并行:小批量分到各 GPU 算梯度,聚合梯度后更新参数再广播。聚合位置可以灵活选择,关键约束是物理带宽——不同互连差异巨大(NVLink、PCIe、以太网一个比一个慢),同步策略的选择能让同样操作耗时在 15 毫秒到 80 毫秒之间波动。
环同步:把 n 个 GPU 排成环,把梯度分成 n 块,每个节点从自己的块开始向左邻发送、边传边加,n-1 步后完成聚合,总时间基本不随环大小线性增长。像"击鼓传花"式汇总——每人把自己的部分加到传来的汇总上再传给下一位,一圈下来就齐了。
多机训练流程:每机算梯度 → 机内聚合 → 发到中央参数服务器聚合 → 更新广播回各机。瓶颈是中央服务器带宽有限,解决办法是增加参数服务器数量,每台只存一部分参数。
工程抽象:把同步封装成键值存储的两个操作——push(把本地梯度推送到公共存储聚合)和 pull(取回聚合后的梯度),从而把统计建模者与系统工程师的关注点解耦。像"邮箱系统"——push 是投递,pull 是取件,投递员不用管邮箱内部怎么分拣。
小结
- 硬件性能的关键是带宽与延迟:随机访问比顺序访问慢约 500 倍,传输要"量大次少"。
- GPU 靠大量核心和高带宽内存成为深度学习的主力,训练与推断需求不同。
- 混合式编程先用命令式开发、再编译成符号式,兼顾灵活与性能。
- 异步计算让前端不阻塞,但要小心打印、转数组等"阻塞器"。
- 自动并行靠计算图自动调度无依赖任务,并让计算与通信重叠。
- 多 GPU 训练的主流是数据并行:各 GPU 持相同参数、聚合梯度后更新。
- 多机训练用参数服务器聚合梯度,环同步、push/pull 抽象提高效率。
关键术语
| 术语 | 一句话解释 |
|---|---|
| 带宽 / 延迟 | 传输速率 / 等待时间 |
| 缓存层次 | 寄存器→L1→L2→L3→主存,逐级变大的存储结构 |
| 突发读取 | 连续顺序读取,效率远高于随机访问 |
| SIMD / 矢量化 | 单指令多数据并行 |
| 命令式 / 符号式编程 | 逐语句执行 / 先编译再执行 |
| 混合式编程 | 命令式开发 + 符号式执行 |
| 异步计算 | 前端发指令不等待、后端排队执行 |
| 依赖图 | 记录操作间依赖关系用于调度 |
| 自动并行 | 框架自动调度无依赖任务的并行执行 |
| 数据并行 | 各 GPU 持相同参数、处理不同数据分片 |
| allreduce | 跨设备梯度求和并广播 |
| 参数服务器 | 集中/分布存储与聚合参数的节点 |
| 环同步 | 按环拓扑分段流水线式聚合梯度 |
| push / pull | 推送梯度聚合 / 取回聚合后的梯度 |