计算性能(Computational Performance) #
编译器(Compilers)与解释器(Interpreters) #
编译器(Compilers)和解释器(Interpreters)是两种不同的程序执行方式。编译器会将整个源代码一次性翻译为目标机器可以直接执行的二进制代码(Machine Code),然后运行该编译后的程序,例如C/C++使用的GCC(GNU Compiler Collection)。相比之下,解释器逐行读取并执行代码,而不是提前转换为二进制文件,例如Python的CPython解释器。
在 Python 中,由于其是一种 解释型语言(Interpreted Language),代码的执行主要依赖解释器。Python源代码(.py
文件)首先被解析为中间字节码(Bytecode),然后由Python虚拟机(Python Virtual Machine, PVM)逐行解释执行。这种方式使Python具有高度的可移植性和灵活性,但也带来了较高的运行时开销(Runtime Overhead),因为每次执行代码时都需要经过解析和解释的过程。Python 代码执行流程一般为:
- 源代码(Source Code):Python 程序员编写的 .py 文件
- 解析(Parsing):Python 解释器会对代码进行语法分析(Syntax Analysis),构建抽象语法树(Abstract Syntax Tree, AST),并检查代码是否存在语法错误。
- 编译为字节码(Bytecode Compilation):Python 并不会直接执行源代码,而是将其转换为 字节码(Bytecode),这是一种低级中间表示,独立于具体的计算机架构。Python 代码的字节码通常存储在 .pyc 文件中(位于
__pycache__
目录下)。 - Python 虚拟机(PVM)执行:Python 的解释器(如 CPython)包含Python 虚拟机(Python Virtual Machine, PVM),它逐条读取字节码并执行相应的操作。例如,当 PVM 读取
add(2, 3)
时,它会调用add
函数并计算2 + 3
的结果,然后继续执行print(result)
语句。
Note:Python 是一种动态语言,源代码首先被编译成字节码(
.pyc
文件),但字节码并不是机器代码,它仍然需要 Python 解释器来解释和执行。这意味着,Python 字节码只能在 Python 运行时环境中执行,不能像 C++ 编译的程序那样直接由操作系统和硬件执行。Python 的解释器会读取字节码,并逐条指令解释执行。这个过程并不直接交给机器,而是通过解释器与操作系统交互来实现。
C++ 代码在运行前由编译器(Compiler)一次性编译成机器码(Machine Code),然后直接运行:
- 编译(Compilation):C++ 源代码(
.cpp
)被编译成目标文件(.o
)。 - 链接(Linking):多个目标文件被链接成最终的可执行文件(
.exe
/a.out
)。 - 执行(Execution):
- CPU 直接执行机器码,不需要解释器。
- 没有字节码解析或解释的开销,因此执行速度更快。
Note:C++ 是一种静态编译语言,在编译时,源代码会被直接编译成平台特定的机器代码(即可执行文件),这意味着 编译后的程序可以直接由操作系统加载,并由计算机的 CPU 执行,不需要额外的解释器。C++ 程序在编译阶段就转化为操作系统特定的二进制机器代码,然后由操作系统调度执行,直接运行在 CPU 上。
虽然 C++ 代码在逻辑上是按照顺序执行的,但 CPU 可能进行自动优化,而 Python 解释器必须逐条解析并执行,因此 C++ 不是“逐条执行”的,而是“指令流优化执行”的。
- 指令并行(Instruction-Level Parallelism, ILP):如果没有数据依赖,可能会同时执行不同的指令。
- 指令重排序(Out-of-Order Execution, OOOE):如果某条指令需要等待数据,CPU 可能会提前执行后面的指令。
命令式编程(Imperative Programming) #
我们主要此前关注命令式编程(Imperative Programming),这类编程使用 print
、+
和 if
等语句来改变程序的状态。例如,Python 是一种解释型语言(Interpreted Language),当执行 fancy_func
这个函数时,它会按照顺序执行其中的语句,例如 e = add(a, b)
,然后将结果存储在变量 e
中。接下来的 f = add(c, d)
和 g = add(e, f)
也是类似的执行方式。
def add(a, b):
return a + b
def fancy_func(a, b, c, d):
e = add(a, b)
f = add(c, d)
g = add(e, f)
return g
print(fancy_func(1, 2, 3, 4))
虽然命令式编程较为直观,但它可能并不高效。例如,即使 add
函数在 fancy_func
中被多次调用,Python 仍然会逐条执行这三个函数调用。当这些操作在 GPU(甚至多个 GPU)上执行时,由 Python 解释器带来的开销可能会非常大。此外,在 fancy_func 运行结束之前,Python 需要存储 e 和 f 的值,因为无法提前确定这些变量是否会在后续程序中被使用。
符号式编程(Symbolic Programming) #
相比之下,符号式编程(Symbolic Programming) 通常会等到整个计算过程完全定义后再执行。这种策略被多个深度学习框架采用,例如 Theano 和 TensorFlow(尽管 TensorFlow 现在也支持命令式扩展)。它通常包含以下几个步骤:
- 定义要执行的操作。
- 将操作编译(Compile)为可执行程序。
- 提供必要的输入,并调用已编译的程序进行执行。
Note:可以理解为先定义并优化整个计算过程,然后一次性执行所有操作,而不是逐步执行每个步骤。操作被 预定义并优化为更高效的执行计划,在执行时无需再逐条执行每一条 Python 语句,而是将计算过程整体处理,减轻了 Python 解释器逐条解析和执行的负担。
在符号式编程中,计算图已经提前优化,并且计算图的执行往往是通过专门的引擎(如 TensorFlow、Theano、PyTorch)来处理,而这些框架通常采用低层次的 C/C++ 实现,并且能够通过多个 GPU 加速计算。因此,虽然在 Python 层面可能仍然会有一些动态开销,但符号式编程通过将 核心计算任务转移到更高效的底层计算引擎,绕过了 Python 解释器的瓶颈,极大地提高了性能。
这种方式带来了显著的优化效果:
- 减少 Python 解释器的开销。在多个 GPU 配合单个 CPU 线程运行时,Python 解释器可能成为性能瓶颈。
- 优化代码执行。编译器可以优化代码,例如将
print((1 + 2) + (3 + 4))
直接转换成print(10)
,因为它在编译阶段可以看到完整的代码。 - 高效的内存管理。编译器可以在变量不再需要时释放内存,甚至不分配内存。
- 代码转换与优化。编译器可以将代码转换为等效但更高效的版本。
混合编程(Hybrid Programming) #
混合编程(Hybrid Programming) 是将不同编程范式、语言或框架结合起来,以利用各自的优点,解决不同层次或任务的计算问题。这种方法能够在性能和灵活性之间找到一个平衡点,通常包括:使用低级语言(如 C、C++)处理性能密集型任务,结合高级语言(如 Python)处理灵活性较强的任务,或者将不同计算模型结合起来。
Python 是解释型语言,执行时逐行解释,这导致了在 多 GPU 环境下,Python 本身成为了性能瓶颈。即使是快速的单个 GPU 也可以顺利运行,但一旦使用多个 GPU(比如 8 个 GPU),Python 的 GIL(全局解释器锁)和单线程执行限制就会使得 Python 解释器成为性能瓶颈。
在这种情况下,混合编程的解决方案是将 Python 层的计算和低级优化代码结合起来,特别是使用 HybridSequential
这样的结构,替代传统的 Sequential
层。
HybridSequential
是在 PyTorch 中使用的一种优化方式,它结合了高层 Python 接口和底层 C++ 代码,使得模型的计算更高效,尤其是在多 GPU 环境下。通过使用混合编程,框架可以将大部分计算推向底层的高效实现,从而减少 Python 解释器的开销,提升并行性。通过使用 HybridSequential,计算图会被优化,能够利用底层的高效并行计算,这样 Python 的解释器不会成为瓶颈,计算可以被充分分配到多个 GPU 上,从而提升性能。
Note:在大型语言模型(LLM)中,尤其是像 GPT、BERT 等模型,混合编程也是提高计算效率的关键。LLM 通常涉及大量的矩阵运算和深度神经网络的训练,通常需要 在多个 GPU 上并行处理。
例如,在训练 GPT 等大型模型时,框架(如 HuggingFace 的 Transformers)将模型的计算图分解,并通过低级实现(如 CUDA 核心代码)来高效地在多 GPU 上运行,而这些操作会结合 Python 的高级接口与 C/C++ 的底层优化代码。在模型的计算中,尽量避免 Python 逐条解释执行的过程,而是通过优化的计算图或批量操作一次性完成。
示例:
- 在训练过程中,通过框架的混合编程,将数据传递到 GPU 并行计算的同时,Python 只负责协调工作流程,而 具体的矩阵运算、权重更新等则交给底层的优化实现(如 CUDA 或 C++ 编写的加速库)来处理,从而避免 Python 解释器的瓶颈。
异步计算 (Asynchronous Computation) #
现代计算机是高度并行的系统,通常包含多个CPU核心(每个核心可能有多个线程)、每个GPU有多个处理单元,并且每个设备通常还配备多个GPU。简而言之,我们可以同时处理很多不同的任务,且常常在不同的设备上执行。然而,Python并不是编写并行和异步代码的最佳选择,至少在没有额外帮助的情况下是这样的。毕竟,Python是单线程的,这一点未来不太可能发生改变。像MXNet和TensorFlow这样的深度学习框架采用异步编程模型来提高性能,而PyTorch则使用Python自带的调度器,从而带来了不同的性能权衡。对于PyTorch而言,默认情况下,GPU操作是异步的。当调用一个使用GPU的函数时,操作会被加入到指定设备的任务队列中,但不一定立刻执行。这允许我们并行执行更多计算任务,包括CPU或其他GPU上的操作。
通过后端实现异步 (Asynchrony via Backend) #
在PyTorch中,前端与用户进行交互,例如通过Python进行编程,后端则用于执行计算任务。无论使用何种前端编程语言(如Python、C++),PyTorch程序的执行主要发生在C++实现的后端中。前端语言发出的操作会传递给后端执行,后端管理自己的线程,持续收集并执行排队的任务。后端需要能够跟踪计算图中各步骤之间的依赖关系,因此,依赖关系密切的操作无法并行执行。
例如,在PyTorch中,通过前端语言(Python)执行的计算任务会先加入到后端队列中,而不立即执行。当需要打印最后一行结果时,前端线程会 等待C++后端线程完成计算并返回结果。这种设计的好处是,Python前端线程不需要执行实际计算,因此Python的性能对程序整体性能影响较小。
提高计算效率 (Improving Computation) #
在高度多线程的系统中(即使是普通的笔记本电脑也有4个或更多线程,在多插槽的服务器上这个数字可能超过256),操作调度的开销可能变得非常显著。因此,实现计算和调度的异步和并行化非常重要。
举个例子,假设我们要将变量递增1 多次,我们可以通过同步和异步两种方式进行对比。通过异步执行,前端线程不必等待每个操作的结果,计算任务可以并行执行,从而显著提高效率。
简化后的前端(Python)和后端(C++)的交互过程如下:
- 前端将计算任务(如 y = x + 1)加入任务队列。
- 后端从队列中获取任务并执行实际的计算。
- 计算结果返回给前端。
如果不使用异步编程,执行10000次计算的总时间大约是 t1 + t2 + t3,而如果使用异步编程,前端可以并行执行任务,因此执行10000次计算的总时间可以减少为 t1 + t3(假设 t2 可以并行执行)。
自动并行化(Automatic Parallelism) #
深度学习框架(例如 MXNet 和 PyTorch)在后台会自动构建计算图(computational graph)。通过计算图,系统可以了解所有操作之间的依赖关系,并选择性地并行执行多个相互独立的任务,从而提高计算速度。
通常,一个操作会使用所有CPU的计算资源或单个GPU的计算资源。例如,点积(dot)操作会使用所有CPU上的核心(core)和线程(thread),即使在同一台机器上有多个CPU处理器。这同样适用于单个GPU。因此,对于单设备计算机来说,并行化的效果并不显著。多个设备的情况则有所不同。在多个GPU的场景中,并行化尤为重要,同时添加本地CPU也能稍微提高性能。
Note:核心(Core):核心是 物理处理单元,也就是CPU内部可以独立执行计算任务的部分。一个CPU可能有多个核心,比如双核(2 cores)、四核(4 cores)、十六核(16 cores)等。每个核心可以独立执行指令,所以多个核心可以 并行执行多个任务,提升计算性能。
线程(Thread):线程是 操作系统调度的最小单位,它是运行在 核心上的执行流。一个核心可以支持多个线程,例如 超线程(Hyper-Threading, HT)技术 允许每个物理核心模拟出 两个逻辑线程,从而在一定程度上提高 CPU 利用率。
Automatic Parallelism(自动并行化)指的是深度学习框架(如 PyTorch、MXNet)在后端自动构建计算图(Computational Graph),并根据计算任务之间的依赖关系,智能地调度和执行多个独立的任务,使其在多个计算设备(如 CPU、GPU)上并行运行,以提高计算效率。用户无需手动编写复杂的并行代码,框架会自动管理任务分配和计算资源调度。自动并行化主要发生在以下几种情况:
- 独立任务(Independent Tasks)
- 当多个计算任务之间没有数据依赖关系(即它们的计算结果互不影响),框架可以将它们同时调度执行。例如,在 PyTorch 中,如果两个张量(Tensor)分别初始化且不相互依赖,那么它们可以被并行计算。
- 单个运算符(Single Operator)
- 一个算子(Operator)本身可能已经进行了多线程或多核心优化。例如,在 CPU 上执行
torch.matmul()
(矩阵乘法)时,它会自动使用所有可用的 CPU 核心(cores)和线程(threads)进行计算,而无需用户手动并行化。 - 在 GPU 上,许多计算任务会自动分配到多个 CUDA 核心(CUDA cores),如
torch.mm()
(矩阵乘法)或torch.conv2d()
(卷积运算)。
- 一个算子(Operator)本身可能已经进行了多线程或多核心优化。例如,在 CPU 上执行
- 多设备计算(Multi-device Computation)
- 如果有多个 GPU,深度学习框架可以自动调度计算任务到多个设备。例如,在数据并行(Data Parallelism)中,模型的不同 mini-batch 可能被分配到不同的 GPU 进行计算。
- 同时,部分任务也可以在 CPU 上执行,以进一步优化计算效率(例如 GPU 计算梯度,CPU 负责数据预处理)。
- 计算与通信并行(Computation and Communication Overlap)
- 在分布式训练或多 GPU 计算时,梯度需要在多个设备之间传输。PyTorch 提供
non_blocking=True
选项,使得数据传输(如to()
、copy_()
)可以与计算同时进行,而不会相互阻塞,从而提升效率。
- 在分布式训练或多 GPU 计算时,梯度需要在多个设备之间传输。PyTorch 提供
Note:CUDA的内核(kernel)和流(stream)具体指什么?
Kernel(CUDA 内核):Kernel(内核) 指的是在 GPU 上执行的并行计算任务,它是一个在 GPU 上运行的函数。GPU 由多个 CUDA 核心(CUDA Cores)组成,每个 Kernel 运行时,会在多个 CUDA 核心上执行多个线程,实现大规模并行计算。
Stream(CUDA 流):Stream(流) 是 CUDA 任务执行的流水线,表示一系列按顺序执行的计算或数据传输操作。默认情况下,CUDA 计算是在一个流(default stream)中串行执行的,但如果使用多个流(streams),计算可以并行进行,从而提高计算效率
硬件 #
在学习计算性能(Computational Performance)时,硬件是不可忽视的关键因素,理解计算机的硬件架构和性能特点对于设计高效的算法至关重要。好的系统设计可以带来数量级的性能提升,可能会影响训练一个深度学习模型所需的时间,从几个月缩短到几周甚至几天。
计算机硬件 #
大多数深度学习研究者和实践者使用的计算机都配备了大量内存和计算能力,并且常常有某种形式的加速器(如GPU)来提升性能。计算机的关键组件包括:
- 处理器(CPU):负责执行程序,通常包含8个或更多的核心(Cores)。
- 内存(RAM):用于存储计算结果,例如权重向量、激活值以及训练数据。
- 网络连接:如以太网(Ethernet),其速度范围从1GB/s到100GB/s。高端服务器可能配备更先进的互联技术。
- 高速扩展总线(high speed expansion bus, PCIe):将计算机与一个或多个GPU连接。在服务器中,通常有8个加速器,而在桌面计算机中通常有1或2个,具体取决于用户的预算和电源供应。
- 持久存储:如硬盘驱动器(HDD)或固态硬盘(SSD),用于高效传输训练数据和存储中间检查点。
这些组件通过 PCIe 总线连接到CPU。以AMD的Threadripper 3为例,它有64个PCIe 4.0通道,每个通道可以实现16 Gbit/s的双向数据传输。内存直接连接到CPU,带宽可高达100GB/s。
为了实现良好的性能,计算任务需要流畅地将数据从存储传输到处理器(CPU或GPU),进行计算,然后再将结果返回到内存和持久存储。为了避免性能瓶颈,需要确保系统中的每个部分都能高效地工作。
内存(RAM) #
内存的基本作用是存储需要快速访问的数据。当前CPU内存通常采用DDR4内存,每个内存模块的带宽为20–25GB/s。每个模块有64位宽的数据总线,通常使用内存模块对来提供多个内存通道。CPU通常有2到4个内存通道,总带宽可达100GB/s。
内存访问的成本并不只是带宽问题。访问内存时,需要首先将内存地址发送到内存模块,随后进行读取。第一次读取的成本通常较高,大约为100纳秒,而随后的读取则更为高效,仅需0.2纳秒。为了提高性能,最好避免随机内存访问,而应尽量使用“突发读取”(Burst Read)。这类读写操作一次性传输大量数据,效率远高于单个数据的随机读取。
对于GPU而言,由于其有更多的计算单元,因此内存的带宽要求更高。常见的解决方案是使用宽总线和高性能内存。例如,NVIDIA的RTX 2080 Ti具有352位宽的总线,能够同时传输更多信息。GPU常用的高性能内存如GDDR6,其带宽可超过500GB/s。高带宽内存(HBM)则通过专用硅片与GPU连接,成本较高,通常只用于高端服务器。
Note:总线宽度(Bus Width)和带宽(Bandwidth)具体指什么?
总线宽度(Bus Width):总线宽度指的是显卡或计算机内存总线中并行数据传输的“通道”数,也就是一次能够传输多少位的数据。在显卡中,通常用位(bit)来表示总线宽度。例如,352位总线意味着显卡的内存控制器可以同时传输352个比特(bit)的数据。总线宽度越大,意味着每个时钟周期内可以传输的数据量越大。
带宽(Bandwidth):带宽是指在单位时间内能够传输的数据量,通常以每秒多少字节(GB/s或GB/s)来表示。带宽越大,意味着显卡可以在单位时间内处理更多的数据,这对于图形处理和并行计算任务尤为重要。
假设一款显卡的内存时钟为21GHz(每秒21亿次时钟),总线宽度为352位,那么它的带宽计算如下:
\[ \text{带宽} = 21 \, \text{GHz} \times 352 \, \text{bit} \times 2 = 14,784 \, \text{GB/s} \]如果将其转化为GB/s,可以得到大约 500GB/s 的带宽,表明显卡可以在每秒钟内传输500GB的数据。这对于大规模图形渲染、视频处理、深度学习等高带宽需求的任务至关重要。
显卡的内存带宽对图形处理和计算的性能至关重要。较大的带宽可以让GPU快速访问大量图形数据(如纹理、帧缓冲等),并支持更高效的计算任务,如实时渲染、深度学习训练等。这对于游戏性能、视频编辑和AI处理等场景都有显著影响。
存储(Storage) #
存储设备与内存类似,关键特性也包括带宽和延迟。不同的是,存储设备之间的差异可能更加显著。
硬盘驱动器(HDD):硬盘驱动器已经存在了超过半个世纪。它由多个旋转的盘片组成,通过磁头来读取和写入数据。虽然HDD相对便宜,但其读取延迟较高,特别是当磁头需要移动到正确的扇区时。硬盘通常每秒可进行100次输入输出操作(IOPs),并且数据传输速度大约为100–200MB/s,因此HDD逐渐被用于归档存储和大数据集的低质量存储。
固态硬盘(SSD):固态硬盘(SSD)使用闪存(Flash Memory)来存储数据,相比于HDD,它的访问速度更快,能够达到每秒10万到50万次I/O操作(IOPs)。现代SSD的带宽通常可以达到1GB/s到3GB/s,比HDD快一个数量级。然而,SSD的设计也有其局限性,尤其是随机写入的性能较差。为了提高性能,通常需要批量写入数据,而不是进行单独的位级写入。此外,SSD的内存单元会随着写入次数的增加而磨损,因此不建议将SSD用于频繁交换文件或日志文件的大规模写入操作。
云存储:云存储提供可调节的性能范围,用户可以动态地配置存储资源,以满足不同的需求。在进行大规模训练时,如果数据访问的延迟较高,可以考虑增加IOP的数量。
CPU(中央处理器) #
CPU 是计算机的核心部件,由多个关键组成部分构成:
- 处理器核心(Processor Cores):能够执行机器指令(machine code)。
- 总线(Bus):连接各个核心,不同处理器型号、代际和厂商的拓扑结构差异较大。
- 缓存(Cache):用于提供比主存(main memory)更高的带宽和更低的延迟,提升内存访问速度。
- 向量处理单元(Vector Processing Units, VPU):现代 CPU 主要用于执行线性代数(linear algebra)和卷积(convolution)运算,加速媒体处理(media processing)和机器学习(machine learning)任务。
每个处理器核心由多个复杂组件构成,尽管不同厂商和代际的实现方式有所不同,但基本功能大致相同:
指令处理流程
- 前端(Front-end):负责加载指令并预测程序执行路径(branch prediction)。
- 指令解码(Instruction Decoding):
- 将汇编代码(assembly code)解码成微指令(microinstructions)。
- 复杂指令可能被拆解为更低级的基本操作指令集。
- 执行核心(Execution Core):
- 负责执行指令。
- 现代 CPU 通常支持 多发射(multiple issue),即同时执行多个操作。例如,ARM Cortex A77 核心可在同一时钟周期内执行 8 条指令。
- 整数单元(Integer Units) 专门处理整数运算,而 浮点单元(Floating Point Units, FPU) 负责浮点计算。
分支预测(Branch Prediction):在执行过程中,CPU 可能同时跟踪多个代码路径:
- CPU 可能会同时执行多个分支指令,并丢弃未被采纳的分支结果(称为 投机执行(Speculative Execution))。
- 分支预测单元(Branch Prediction Unit)位于 前端(Front-end),用于选择最有可能的执行路径,以提高指令吞吐量(throughput)。
深度学习(Deep Learning)对计算能力需求极高,因此 CPU 需要在 一个时钟周期内执行多个操作。这通过 向量处理单元(Vector Units) 实现。尽管 CPU 可进行向量化加速,但远不及 GPU(图形处理器, Graphics Processing Unit)。
缓存(Cache) #
假设我们有一个 4 核心(4-core)CPU,运行在 2 GHz 频率,指令吞吐率(IPC, Instructions per Clock)为 1,并且支持 256 位 AVX2 指令。如果每个 AVX2 操作需要从内存读取 一个寄存器,则 CPU 每个时钟周期可能需要 大量数据。然而: * 内存带宽 仅 20–40 GB/s,远远低于 CPU 需求。 * 因此,减少内存访问 并 利用缓存(Cache) 是提升 CPU 性能的关键。
现代 CPU 采用 分层缓存(cache hierarchy) 来减少访问主存的需求:
- 寄存器(Registers)
- 不是严格意义上的缓存,但可以直接在 CPU 内部存取。
- 访问速度最快,无需额外延迟(clock penalty)。
- C 语言中的 register 关键字 允许编译器优化寄存器使用。
- L1 缓存(一级缓存, Level 1 Cache)
- 大小:32–64 KB,通常分为 数据缓存(Data Cache) 和 指令缓存(Instruction Cache)。
- 访问速度最快,但容量极小。
- L2 缓存(二级缓存, Level 2 Cache)
- 大小:256–512 KB/核心。
- 可能是 专属缓存(exclusive, 每个核心独立) 或 共享缓存(shared, 多核心共用)。
- 访问 L2 需要先检查 L1 是否命中,带来额外延迟。
- L3 缓存(三级缓存, Level 3 Cache)
- 共享缓存(Shared Cache),多个核心共享。
- 大小:4–8 MB(典型值),AMD EPYC 服务器 CPU 可达 256 MB。
GPU与其他加速器(GPUs and other Accelerators) #
深度学习的成功离不开GPU(图形处理单元,Graphics Processing Unit)。同时,GPU制造商也因深度学习的发展获得了巨大的商业利益。这种硬件与算法的协同进化(co-evolution)使得深度学习成为目前主流的统计建模范式(statistical modeling paradigm)。因此,理解GPU及其他加速器(accelerators)如TPU(张量处理单元,Tensor Processing Unit)的优势是非常重要的。
GPU加速器通常针对训练或推理进行优化:
- 推理(Inference):仅需执行神经网络的前向传播(Forward Propagation),不需要存储反向传播(Backpropagation)的中间数据。此外,计算精度要求较低,通常使用 FP16(半精度浮点数)或INT8(8位整数) 即可。
- 训练(Training):需要存储所有中间结果以计算梯度,并在梯度累积时保持较高精度以防止数值下溢(Underflow)或溢出(Overflow)。最低要求是FP16(或FP32混合精度,Mixed Precision)。此外,训练需要更快、更大的显存(例如HBM2 vs. GDDR6)以及更强的计算能力。例如:
- NVIDIA Turing T4 GPU 专为推理优化
- NVIDIA V100 GPU 更适用于训练任务
网络与总线(Networks and Buses) #
当单个计算设备的能力不足时,需要通过数据传输(Data Transfer)在多个设备间同步计算。这时,就需要高效的网络(Networks)和总线(Buses)。数据传输方案需要在带宽(Bandwidth)、成本(Cost)、传输距离(Distance)和灵活性(Flexibility) 之间做权衡。
WiFi
- 优点:无需布线,使用方便,成本低。
- 缺点:带宽和延迟表现不佳,不适用于深度学习计算集群。
PCIe(Peripheral Component Interconnect Express)
- 专门用于高带宽点对点连接(High Bandwidth Point-to-Point Connection)
- PCIe 4.0(16通道) 最大带宽 32GB/s,延迟约 5μs。
- CPU的PCIe通道数受限:
- AMD EPYC 3:最多128条PCIe通道
- Intel Xeon:最多48条PCIe通道
- 桌面级CPU:
- Ryzen 9(20通道)
- Core i9(16通道)
- 限制:GPU通常占用16条通道,而PCIe通道还需分配给存储设备(SSD)、网络设备(Ethernet)等。因此,多个GPU共享通道可能导致带宽瓶颈(Bandwidth Bottleneck)。
NVLink
- NVIDIA专有的高带宽互连技术(High Bandwidth Interconnect)
- 带宽:
- 每条NVLink最高可达 300 Gbit/s
- V100 GPU 配备6条NVLink
- RTX 2080 Ti 仅有 1条NVLink,带宽降低至 100 Gbit/s
多GPU训练(Training on Multiple GPUs) #
按网络结构划分(Model Parallelism)-(每个 GPU 负责不同的网络层)
- 策略:每个 GPU 处理神经网络的 部分层(Subsequent Layers),并将中间计算结果传递给下一个 GPU。适用于 超大模型(Very Large Models),因为单个 GPU 无法容纳完整的网络参数。
- 优点:
- 减少单个 GPU 的显存占用(Memory Footprint per GPU 可控)。
- 可训练更深的神经网络(适用于大规模模型)。
- 缺点:
- 需要 跨 GPU 层同步(Synchronization between Layers),计算负载必须均衡,否则可能导致计算瓶颈。梯度(Gradients) 和 激活值(Activations) 需要频繁跨 GPU 传输,可能会导致 总线带宽(GPU Bus Bandwidth) 过载。
- 难以扩展到大量 GPU,特别是对包含 顺序计算(Sequential Computation) 的任务而言(如 RNN、Transformer)。目前,除非有 优秀的框架或操作系统支持(如 Pipeline Parallelism in PyTorch & DeepSpeed),否则不推荐此方法。
按通道划分(Layer-wise Parallelism)-(每个 GPU 负责不同的通道)
- 策略:在 CNN 中,将特征图(Feature Maps)通道均分到不同的 GPU 计算。例如,若 CNN 需要计算 64 个通道(Channels),则可将其拆分到 4 块 GPU,每块 GPU 计算 16 个通道。在 全连接层(Fully Connected Layers),可以按照输出神经元个数划分 GPU 计算。
- 优点:良好的计算扩展性(Computation Scaling),特别是在 GPU 数量较少 时效果较好。适用于较小显存的 GPU。
- 缺点:
- 需要大量的 同步(Synchronization Operations),因为每层计算依赖其他层的计算结果。
- 需要传输的数据量可能比 网络结构划分(Model Parallelism) 方式更大,导致 通信开销(Communication Overhead) 增加。适用范围有限,现代 GPU 显存较大,一般不推荐此方法。
按数据划分(Data Parallelism)-(每个 GPU 处理不同的数据子集)
- 策略:每个 GPU 复制完整的神经网络,但使用 不同的训练样本(Data Shards) 进行计算。每个 GPU 计算 损失(Loss) 和 梯度(Gradients),然后进行 梯度聚合(Gradient Aggregation),最后同步所有 GPU 的参数。适用于 所有深度学习任务,且扩展性极好。
- 优点:最简单、最通用的方法,只需在 每个 minibatch 之后进行同步。计算效率高,因为所有 GPU 进行 相同的计算任务(Same Computation on Different Data)。可扩展性极佳(Scalability),适用于大规模 GPU 服务器。
- 缺点:不能训练 更大的模型,仅能提升计算效率。梯度同步(Gradient Synchronization) 可能成为 瓶颈(Bottleneck),尤其是在 大量 GPU 参与训练时。
数据并行(Data Parallelism)的训练过程 #
- 划分数据(Data Splitting)
- 在每次训练迭代中,将 minibatch 数据划分为 k 份,并分配到不同 GPU 计算。
- 计算梯度(Gradient Computation)
- 每个 GPU 独立计算其数据子集的 损失(Loss) 和 梯度(Gradient)。
- 梯度聚合(Gradient Aggregation)
- 所有 GPU 的局部梯度被聚合为 全局梯度(Global Gradient)。
- 参数更新(Parameter Update)
- 同步更新(Synchronized Update):所有 GPU 用 相同的全局梯度 更新各自的模型参数。
- 增大 minibatch 大小(Scaling Up Minibatch Size)
- 训练多个 GPU 时,增大 minibatch 大小 k 倍,保持每个 GPU 的计算量不变。
- Batch Normalization(BN) 需要适配,如在每个 GPU 维护独立的 BN 统计量。