注意
点击此处下载完整的示例代码
性能调优指南¶
创建于: 2020 年 9 月 21 日 | 最后更新: 2024 年 6 月 12 日 | 最后验证: 2024 年 11 月 05 日
作者: Szymon Migacz
性能调优指南是一系列优化和最佳实践,可以加速 PyTorch 中深度学习模型的训练和推理。所提出的技术通常只需更改几行代码即可实现,并且可以应用于所有领域范围广泛的深度学习模型。
通用优化¶
启用异步数据加载和增强¶
torch.utils.data.DataLoader 支持在单独的工作进程子进程中进行异步数据加载和数据增强。DataLoader
的默认设置为 num_workers=0
,这意味着数据加载是同步的并在主进程中完成。因此,主训练进程必须等待数据可用才能继续执行。
设置 num_workers > 0
启用异步数据加载以及训练和数据加载之间的重叠。num_workers
应根据工作负载、CPU、GPU 和训练数据的位置进行调整。
DataLoader
接受 pin_memory
参数,该参数默认为 False
。当使用 GPU 时,最好设置 pin_memory=True
,这指示 DataLoader
使用锁页内存,并实现从主机到 GPU 更快和异步的内存复制。
禁用验证或推理的梯度计算¶
PyTorch 保存来自所有涉及需要梯度的张量的操作的中间缓冲区。通常,验证或推理不需要梯度。torch.no_grad() 上下文管理器可以应用于禁用指定代码块内的梯度计算,这可以加速执行并减少所需的内存量。torch.no_grad() 也可以用作函数装饰器。
为直接跟随批归一化的卷积禁用偏置¶
torch.nn.Conv2d() 具有 bias
参数,该参数默认为 True
(Conv1d 和 Conv3d 也是如此)。
如果 nn.Conv2d
层直接后跟 nn.BatchNorm2d
层,则不需要卷积中的偏置,而是使用 nn.Conv2d(..., bias=False, ....)
。不需要偏置,因为在第一步中,BatchNorm
减去均值,这有效地抵消了偏置的影响。
这也适用于 1d 和 3d 卷积,只要 BatchNorm
(或其他归一化层)在与卷积偏置相同的维度上进行归一化即可。
从 torchvision 获得的模型已经实现了此优化。
使用 parameter.grad = None 而不是 model.zero_grad() 或 optimizer.zero_grad()¶
而不是调用
model.zero_grad()
# or
optimizer.zero_grad()
来清零梯度,而是使用以下方法
for param in model.parameters():
param.grad = None
第二个代码片段不会清零每个单独参数的内存,而且后续的反向传播使用赋值而不是加法来存储梯度,这减少了内存操作的数量。
将梯度设置为 None
与将其设置为零相比,具有略微不同的数值行为,有关更多详细信息,请参阅文档。
或者,从 PyTorch 1.7 开始,调用 model
或 optimizer.zero_grad(set_to_none=True)
。
融合操作¶
逐点操作(如逐元素加法、乘法)和数学函数(如 sin()、cos()、sigmoid() 等)可以组合成单个内核。这种融合有助于减少内存访问和内核启动时间。通常,逐点操作受内存限制;PyTorch eager 模式为每个操作启动一个单独的内核,这涉及从内存加载数据、执行操作(通常不是最耗时的步骤)以及将结果写回内存。
通过使用融合运算符,仅为多个逐点操作启动一个内核,并且数据仅加载和存储一次。这种效率对于激活函数、优化器和自定义 RNN 单元等特别有利。
PyTorch 2 引入了由 TorchInductor 促成的编译模式,TorchInductor 是一种底层编译器,可以自动融合内核。TorchInductor 将其功能扩展到简单的逐元素操作之外,从而能够高级融合符合条件的逐点和归约操作,以实现优化性能。
在最简单的情况下,可以通过将 torch.compile 装饰器应用于函数定义来启用融合,例如
@torch.compile
def gelu(x):
return x * 0.5 * (1.0 + torch.erf(x / 1.41421))
有关更高级的用例,请参阅 torch.compile 简介。
为计算机视觉模型启用 channels_last 内存格式¶
PyTorch 1.5 引入了对卷积网络的 channels_last
内存格式的支持。此格式旨在与 AMP 结合使用,以进一步加速具有 Tensor Cores 的卷积神经网络。
对 channels_last
的支持是实验性的,但预计它可以用于标准的计算机视觉模型(例如 ResNet-50、SSD)。要将模型转换为 channels_last
格式,请按照 Channels Last 内存格式教程 进行操作。本教程包括关于转换现有模型 的部分。
检查点中间缓冲区¶
缓冲区检查点是一种缓解模型训练内存容量负担的技术。它不是存储所有层的输入以在反向传播中计算上游梯度,而是存储少数层的输入,并在反向传播过程中重新计算其他层。减少的内存需求可以增加可以提高利用率的批量大小。
应仔细选择检查点目标。最好的方法是不存储重新计算成本较低的大层输出。示例目标层是激活函数(例如 ReLU
、Sigmoid
、Tanh
)、上/下采样以及累积深度较小的矩阵向量运算。
PyTorch 支持原生的 torch.utils.checkpoint API,以自动执行检查点和重新计算。
禁用调试 API¶
许多 PyTorch API 旨在用于调试,应在常规训练运行中禁用
CPU 特定优化¶
利用非统一内存访问 (NUMA) 控制¶
NUMA 或非统一内存访问是一种用于数据中心机器的内存布局设计,旨在利用多插槽机器中具有多个内存控制器和块的内存局部性。一般来说,所有深度学习工作负载(训练或推理)在不跨 NUMA 节点访问硬件资源的情况下都能获得更好的性能。因此,推理可以与多个实例一起运行,每个实例在一个插槽上运行,以提高吞吐量。对于单节点上的训练任务,建议使用分布式训练,以使每个训练过程在一个插槽上运行。
在一般情况下,以下命令仅在第 N 个节点上的核心上执行 PyTorch 脚本,并避免跨插槽内存访问以减少内存访问开销。
numactl --cpunodebind=N --membind=N python <pytorch_script>
更详细的描述可以在此处找到。
利用 OpenMP¶
OpenMP 用于为并行计算任务带来更好的性能。OMP_NUM_THREADS
是最简单的开关,可用于加速计算。它确定用于 OpenMP 计算的线程数。CPU 亲和性设置控制工作负载如何在多个核心上分布。它会影响通信开销、缓存行失效开销或页面抖动,因此正确设置 CPU 亲和性会带来性能优势。GOMP_CPU_AFFINITY
或 KMP_AFFINITY
确定如何将 OpenMP* 线程绑定到物理处理单元。详细信息可以在此处找到。
使用以下命令,PyTorch 在 N 个 OpenMP 线程上运行任务。
export OMP_NUM_THREADS=N
通常,以下环境变量用于为 GNU OpenMP 实现设置 CPU 亲和性。OMP_PROC_BIND
指定线程是否可以在处理器之间移动。将其设置为 CLOSE 可使 OpenMP 线程在连续位置分区中靠近主线程。OMP_SCHEDULE
确定 OpenMP 线程的调度方式。GOMP_CPU_AFFINITY
将线程绑定到特定的 CPU。一个重要的调整参数是核心绑定,它可以防止线程在多个 CPU 之间迁移,从而增强数据位置并最大限度地减少核心间通信。
export OMP_SCHEDULE=STATIC
export OMP_PROC_BIND=CLOSE
export GOMP_CPU_AFFINITY="N-M"
Intel OpenMP 运行时库 (libiomp
)¶
默认情况下,PyTorch 使用 GNU OpenMP (GNU libgomp
) 进行并行计算。在 Intel 平台上,Intel OpenMP 运行时库 (libiomp
) 提供 OpenMP API 规范支持。与 libgomp
相比,有时它会带来更多的性能优势。利用环境变量 LD_PRELOAD
可以将 OpenMP 库切换到 libiomp
export LD_PRELOAD=<path>/libiomp5.so:$LD_PRELOAD
与 GNU OpenMP 中的 CPU 亲和性设置类似,libiomp
中提供了环境变量来控制 CPU 亲和性设置。KMP_AFFINITY
将 OpenMP 线程绑定到物理处理单元。KMP_BLOCKTIME
设置线程在完成并行区域执行后,在进入睡眠状态之前应等待的时间(以毫秒为单位)。在大多数情况下,将 KMP_BLOCKTIME
设置为 1 或 0 会产生良好的性能。以下命令显示了使用 Intel OpenMP 运行时库的常见设置。
export KMP_AFFINITY=granularity=fine,compact,1,0
export KMP_BLOCKTIME=1
切换内存分配器¶
对于深度学习工作负载,与默认的 malloc
函数相比,Jemalloc
或 TCMalloc
可以通过尽可能多地重用内存来获得更好的性能。Jemalloc 是一种通用的 malloc
实现,它强调避免碎片和可扩展的并发支持。TCMalloc 还具有多项优化功能,可加速程序执行。其中一项是在缓存中保存内存,以加速常用对象的访问。即使在解除分配后仍保留此类缓存也有助于避免以后重新分配此类内存时产生昂贵的系统调用。使用环境变量 LD_PRELOAD
来利用其中之一。
export LD_PRELOAD=<jemalloc.so/tcmalloc.so>:$LD_PRELOAD
将 oneDNN Graph 与 TorchScript 用于推理¶
oneDNN Graph 可以显着提高推理性能。它融合了一些计算密集型操作,例如卷积、matmul 及其邻居操作。在 PyTorch 2.0 中,它作为 Float32
和 BFloat16
数据类型的 beta 功能受支持。oneDNN Graph 接收模型的图,并根据示例输入的形状识别运算符融合的候选对象。应使用示例输入对模型进行 JIT 跟踪。在对与示例输入形状相同的输入进行几次预热迭代后,将观察到加速。以下代码片段是针对 resnet50 的,但它们也可以很好地扩展为将 oneDNN Graph 与自定义模型一起使用。
# Only this extra line of code is required to use oneDNN Graph
torch.jit.enable_onednn_fusion(True)
使用 oneDNN Graph API 仅需额外一行代码即可使用 Float32 进行推理。如果您正在使用 oneDNN Graph,请避免调用 torch.jit.optimize_for_inference
。
# sample input should be of the same shape as expected inputs
sample_input = [torch.rand(32, 3, 224, 224)]
# Using resnet50 from torchvision in this example for illustrative purposes,
# but the line below can indeed be modified to use custom models as well.
model = getattr(torchvision.models, "resnet50")().eval()
# Tracing the model with example input
traced_model = torch.jit.trace(model, sample_input)
# Invoking torch.jit.freeze
traced_model = torch.jit.freeze(traced_model)
使用示例输入对模型进行 JIT 跟踪后,即可在几次预热运行后将其用于推理。
with torch.no_grad():
# a couple of warm-up runs
traced_model(*sample_input)
traced_model(*sample_input)
# speedup would be observed after warm-up runs
traced_model(*sample_input)
虽然 oneDNN Graph 的 JIT 融合器也支持使用 BFloat16
数据类型进行推理,但只有具有 AVX512_BF16 指令集架构 (ISA) 的机器才能体现 oneDNN Graph 的性能优势。以下代码片段用作使用 BFloat16
数据类型进行 oneDNN Graph 推理的示例
# AMP for JIT mode is enabled by default, and is divergent with its eager mode counterpart
torch._C._jit_set_autocast_mode(False)
with torch.no_grad(), torch.cpu.amp.autocast(cache_enabled=False, dtype=torch.bfloat16):
# Conv-BatchNorm folding for CNN-based Vision Models should be done with ``torch.fx.experimental.optimization.fuse`` when AMP is used
import torch.fx.experimental.optimization as optimization
# Please note that optimization.fuse need not be called when AMP is not used
model = optimization.fuse(model)
model = torch.jit.trace(model, (example_input))
model = torch.jit.freeze(model)
# a couple of warm-up runs
model(example_input)
model(example_input)
# speedup would be observed in subsequent runs.
model(example_input)
使用 PyTorch ``DistributedDataParallel``(DDP) 功能在 CPU 上训练模型¶
对于小型模型或内存受限模型(例如 DLRM),在 CPU 上训练也是一个不错的选择。在具有多个插槽的机器上,分布式训练带来了高效的硬件资源利用率,以加速训练过程。Torch-ccl 使用 Intel(R) oneCCL
(集体通信库)进行优化,以实现高效的分布式深度学习训练,实现诸如 allreduce
、allgather
、alltoall
等集体通信,实现 PyTorch C10D ProcessGroup
API,并且可以作为外部 ProcessGroup
动态加载。在 PyTorch DDP 模块中实现的优化的基础上,torch-ccl
加速了通信操作。除了对通信内核进行的优化外,torch-ccl
还具有同步计算-通信功能。
GPU 特定优化¶
启用 Tensor Cores¶
Tensor Cores 是专门设计的硬件,用于计算矩阵-矩阵乘法运算,主要用于深度学习和 AI 工作负载。Tensor Cores 具有特定的精度要求,可以通过手动或通过自动混合精度 API 进行调整。
特别是,张量运算利用较低精度的工作负载。这可以通过 torch.set_float32_matmul_precision
进行控制。默认格式设置为“highest”,它使用张量数据类型。但是,PyTorch 提供替代的精度设置:“high”和“medium”。这些选项优先考虑计算速度而不是数值精度。”
使用 CUDA Graphs¶
在使用 GPU 时,工作必须首先从 CPU 启动,在某些情况下,CPU 和 GPU 之间的上下文切换可能会导致不良的资源利用率。CUDA Graphs 是一种将计算保持在 GPU 内的方法,而无需支付额外的内核启动和主机同步成本。
# It can be enabled using
torch.compile(m, "reduce-overhead")
# or
torch.compile(m, "max-autotune")
对 CUDA Graph 的支持正在开发中,其使用可能会导致设备内存消耗增加,并且某些模型可能无法编译。
启用 cuDNN 自动调优器¶
NVIDIA cuDNN 支持多种算法来计算卷积。自动调优器运行一个简短的基准测试,并为给定的硬件和给定的输入大小选择性能最佳的内核。
对于卷积网络(目前不支持其他类型),在启动训练循环之前,通过设置以下内容来启用 cuDNN 自动调优器
torch.backends.cudnn.benchmark = True
自动调优器的决策可能是不确定的;对于不同的运行,可能会选择不同的算法。有关更多详细信息,请参阅 PyTorch:可重复性
在某些罕见的情况下,例如输入大小变化很大的情况下,最好在禁用自动调优器的情况下运行卷积网络,以避免与为每个输入大小选择算法相关的开销。
避免不必要的 CPU-GPU 同步¶
避免不必要的同步,以便让 CPU 尽可能提前于加速器运行,以确保加速器工作队列包含许多操作。
如果可能,请避免需要同步的操作,例如
print(cuda_tensor)
cuda_tensor.item()
内存复制:
tensor.cuda()
、cuda_tensor.cpu()
和等效的tensor.to(device)
调用cuda_tensor.nonzero()
依赖于 CUDA 张量上执行的操作结果的 Python 控制流,例如
if (cuda_tensor != 0).all()
直接在目标设备上创建张量¶
而不是调用 torch.rand(size).cuda()
来生成随机张量,而是在目标设备上直接生成输出:torch.rand(size, device='cuda')
。
这适用于所有创建新张量并接受 device
参数的函数:torch.rand()、torch.zeros()、torch.full() 等。
使用混合精度和 AMP¶
混合精度利用 Tensor Cores,并在 Volta 和更新的 GPU 架构上提供高达 3 倍的总体加速。要使用 Tensor Cores,应启用 AMP,并且矩阵/张量维度应满足调用使用 Tensor Cores 的内核的要求。
要使用 Tensor Cores
在可变输入长度的情况下预分配内存¶
用于语音识别或 NLP 的模型通常在具有可变序列长度的输入张量上进行训练。可变长度可能会给 PyTorch 缓存分配器带来问题,并可能导致性能下降或意外的内存不足错误。如果短序列长度的批次之后是另一个较长序列长度的批次,则 PyTorch 将被迫释放先前迭代的中间缓冲区并重新分配新缓冲区。此过程非常耗时,并会导致缓存分配器中的碎片,这可能会导致内存不足错误。
一个典型的解决方案是实现预分配。它包括以下步骤
生成一个(通常是随机的)具有最大序列长度的输入批次(对应于训练数据集中的最大长度或某些预定义的阈值)
使用生成的批次执行前向和后向传递,不执行优化器或学习率调度器,此步骤预分配最大大小的缓冲区,这些缓冲区可以在后续训练迭代中重复使用
梯度归零
继续正常训练
分布式优化¶
使用高效的数据并行后端¶
PyTorch 有两种实现数据并行训练的方法
DistributedDataParallel
提供更好的性能和多 GPU 扩展性。有关更多信息,请参阅 PyTorch 文档中 CUDA 最佳实践的相关部分。
如果使用 DistributedDataParallel
和梯度累积进行训练,则跳过不必要的 all-reduce¶
默认情况下,torch.nn.parallel.DistributedDataParallel 在每次后向传递后执行梯度 all-reduce,以计算参与训练的所有工作进程的平均梯度。如果训练使用 N 步的梯度累积,则不需要在每个训练步骤后都进行 all-reduce,只需要在最后一次调用后向传递之后,优化器执行之前执行 all-reduce。
DistributedDataParallel
提供了 no_sync() 上下文管理器,它禁用特定迭代的梯度 all-reduce。no_sync()
应应用于梯度累积的前 N-1
次迭代,最后一次迭代应遵循默认执行并执行所需的梯度 all-reduce。
如果使用 DistributedDataParallel(find_unused_parameters=True)
,则匹配构造函数和执行期间的层顺序¶
torch.nn.parallel.DistributedDataParallel 与 find_unused_parameters=True
使用模型构造函数中的层和参数顺序来构建 DistributedDataParallel
梯度 all-reduce 的桶。DistributedDataParallel
将 all-reduce 与后向传递重叠。只有当给定桶中参数的所有梯度都可用时,才会异步触发特定桶的 all-reduce。
为了最大化重叠量,模型构造函数中的顺序应大致匹配执行期间的顺序。如果顺序不匹配,则整个桶的 all-reduce 将等待最后到达的梯度,这可能会减少后向传递和 all-reduce 之间的重叠,all-reduce 最终可能会暴露出来,从而减慢训练速度。
带有 find_unused_parameters=False
(这是默认设置)的 DistributedDataParallel
依赖于基于后向传递期间遇到的操作顺序的自动桶形成。使用 find_unused_parameters=False
,无需重新排序层或参数即可实现最佳性能。
在分布式设置中负载均衡工作负载¶
负载不均衡通常可能发生在处理序列数据的模型中(语音识别、翻译、语言模型等)。如果一个设备接收到的数据批次的序列长度比其余设备的序列长度长,则所有设备都将等待最后一个完成的工作进程。后向传递充当具有 DistributedDataParallel 后端的分布式设置中的隐式同步点。
有多种方法可以解决负载均衡问题。核心思想是在每个全局批次中尽可能均匀地在所有工作进程上分配工作负载。例如,Transformer 通过形成具有大致恒定数量的 tokens(和批次中可变数量的序列)的批次来解决不均衡问题,其他模型通过对具有相似序列长度的样本进行分桶甚至通过按序列长度对数据集进行排序来解决不均衡问题。
脚本总运行时间: ( 0 分钟 0.000 秒)