在今天的 PyTorch 2022 开发者大会上,PyTorch 团队发布了一个新特性torch.compile
,这个新特性将 PyTorch 的性能推向了新高度,并开始将 PyTorch 的部分实现从 C++ 中迁移到 Python 中。他们相信这是 PyTorch 一个实质性的新方向–因此称之为 PyTorch 2.0。torch.compile
是一个完全附加的(和可选的)功能,因此 PyTorch 2.0 100% 向后兼容, 因此基于 PyTorch 1.x 开发的项目可以不用做任何修改就能迁移到 PyTorch2.0 。
PyTorch 2.0 正式版本预计在 2023 年 3 月份发布,但可以使用以下命令安装预发布版本提前体验:
pip install numpy --pre torch[dynamo] --force-reinstall --extra-index-url https://download.pytorch.org/whl/nightly/cu117
只需使用torch.compile
对模型进行编译,就可以享受 PyTorch 2.0 给模型速度带来的极致提升,简单示例代码如下。
import torch
import torchvision.models as models
model = models.resnet18().cuda()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
compiled_model = torch. compile (model) # 关键一行
x = torch.randn(16, 3, 224, 224).cuda()
optimizer.zero_grad()out = compiled_model(x)
out.sum().backward()
optimizer.step()
PyTorch 团队在 163 个开源模型(包括图像分类、目标检测、图像生成等领域)进行验证,结论是模型在 NVIDIA A100 GPU 上的运行速度快了 43%。在 Float32 精度下,它的运行速度快了 21%,在 AMP 精度下,它的运行速度快了为 51%。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1arAr5ht-1670047163094)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c9d8f0797ba4495189ae25e5691e546b~tplv-k3u1fbpfcp-zoom-1.image)]
以下内容翻译自 https://pytorch.org/get-started/pytorch-2.0
介绍 PyTorch 2.0 的变化是迈向下一代 PyTorch 2.x 系列版本的第一步。在过去的几年里,从 PyTorch 1.0 到 1.13,我们持续迭代和创新,并将 PyTorch 转移到新成立的 PyTorch 基金会,它是 Linux 基金会的一部分。 PyTorch 的最大优势除了我们令人惊叹的社区之外,还在于我们继续作为一流的 Python 集成、命令式风格、用户友好的 API 和选项。与 PyTorch 1.0 一样,PyTorch 2.0 依然提供了相同的 eager 开发模式和友好的用户体验,同时从根本上改变和提高了 PyTorch 在编译器级别下的操作方式。我们能够提供更快的性能、对动态输入和分布式有更好的支持。
今天,我们发布了torch.compile
,这项特性将 PyTorch 的性能推向了新的高度,并开始将PyTorch 的部分内容从 C++ 中移回到 Python 中。我们相信这是 PyTorch 一个重要的新方向–因此我们称之为 2.0。torch.compile
是一个完全附加的(和可选的)功能,因此 2.0 的定义是 100% 向后兼容。
支撑 torch.compile
的一些新技术:TorchDynamo、AOTAutograd、PrimTorch 和TorchInductor。
TorchDynamo、AOTAutograd、PrimTorch 和 TorchInductor 是用 Python 编写的,并支持动态形状(即能够输入不同大小的 Tensor 而无需重新编译),这使得它们很灵活,很容易拓展,并降低了开发人员和供应商的准入门槛。
为了验证这些技术,我们使用了来自不同机器学习领域的 163 个开源模型。我们精心打造了这个基准(benchmark),包括图像分类、物体检测、图像生成等任务,以及各种 NLP 任务,如语言建模、问答、序列分类、推荐系统和强化学习。我们将这些基准分为三类:
我们没有修改这些开源模型,只是增加了一个torch.compile
函数来包装它们。
然后我们测量这些模型的速度并验证精度。由于加速比可能取决于数据类型,我们在Float32 和自动混合精度(AMP)上都进行了测速。我们报告了 0.75 * AMP + 0.25 * Float32 的加权平均速度,因为我们发现 AMP 在实践中更常见。
在这 163 个开源模型中,有 93% 的模型可以被 torch.compile
正常编译,并且编译后模型在 NVIDIA A100 GPU 上的训练运行速度提高了 43%。在 Float32 精度下,它的运行速度平均为 21%,在AMP精度下,它的运行速度平均为 51%。
注意事项:在桌面级 GPU(如 NVIDIA 3090)上,我们测得的速度比在服务器级 GPU(如 A100)上要低。截至目前,我们的默认后端 TorchInductor 支持 CPU 和 NVIDIA Volta 和 Ampere GPU。它(还)不支持其他 GPU、xPU 或旧的 NVIDIA GPU。
Speedups for torch.compile against eager mode on an NVIDIA A100 GPU
立刻尝鲜 : torch.compile
正处于开发的早期阶段。从今天开始,你可以使用 PyTorch 的 nightly 版本试用torch.compile
。我们预计在 2023 年 3 月初发布第一个稳定的 2.0 版本。
在 PyTorch 2.x 的规划中,我们希望在性能和可扩展性方面将编译模式(compiled mode)推得越来越远。正如我们在今天的大会上所谈到的,其中一些工作正在进行中,有些工作还没有开始,有些工作是我们希望看到的,但我们自己没有足够的精力来做。如果你有兴趣做出贡献,请在本月开始的 "Ask the Engineers: 2.0 Live Q&A Series"现场问答系列中与我们交谈(详情见本帖末尾),或者通过 Github/论坛。
下面是 PyTorch 的一些用户对我们新方向的看法。
“只需添加一行代码,PyTorch 2.0 就能在训练 Transformers 模型时实现 1.5 倍到 2.0 倍的速度提升,这是自混合精度训练推出以来最令人兴奋的事情!”
“大多数的 TIMM 模型可以开箱即用,将其用于推理和训练,无需修改代码”
“PyTorch 2.0 体现了深度学习框架的未来。不需要用户干预即可捕获 PyTorch 程序,开箱即用的程序生成,以及巨大的设备加速,这种可能性为人工智能开发人员打开了一个全新的维度。”
PyTorch 的理念一直是将灵活性和可编程性(hackability)放在首位,而性能紧随其后。我们致力于:
自 2017 年推出 PyTorch 以来,硬件加速器(例如 GPU)的计算速度提高了约 15 倍,内存访问速度提高了约 2 倍。因此,为了保持高性能的 eager 执行方式,我们不得不将 PyTorch 的核心部分转移到 C++ 中,但将这些部分转移到 C++ 则难以避免地降低它们的可编程性(hackability),并增加代码贡献的上手难度。 从第一天起,我们就知道 eager 执行方式的性能限制。2017 年 7 月,我们开始了第一个为 PyTorch 开发编译器的研究项目。这个编译器需要让 PyTorch 程序运行得更快,但不能以牺牲 PyTorch 体验为代价。我们的关键标准是保持灵活性——支持研究人员在不同探索阶段使用动态输入和动态的程序。
多年来,我们在 PyTorch 中构建了多个编译器项目。让我们将编译器分解为三个部分:
在构建 PyTorch 编译器时,graph acquisition 是更艰巨的挑战。 在过去的 5 年里,我们构建了 torch.jit.trace
、TorchScript
、FX tracing
、Lazy Tensors
,但它们都没有给我们带来想要的一切。有些灵活但不快,有些快但不灵活,有些既不快也不灵活,还有些用户体验不好(例如静悄悄地出错)。虽然 TorchScript 很有前途,但它需要对你的代码和你的代码所依赖的代码进行大量修改。对很多 PyTorch用户来说,这种要求使其成为一个不可行的方案
The PyTorch compilation process
今年早些时候,我们开始研究 TorchDynamo,它使用了 PEP-0523 中引入的CPython 新功能 Frame Evaluation API,我们采用数据驱动的方法来验证其在 Graph Capture 上的有效性。我们使用了 7,000 多个用 PyTorch 编写的 Github 项目作为我们的验证集。TorchDynamo 正确、安全地捕获了它们中 99% 的计算图,并且开销可以忽略不计——不需要对原有代码做任何修改。作为对比,TorchScript 和其他方法甚至难以捕获其中的 50%,而且通常开销很大。这时我们才知道,我们终于突破了多年来在灵活性和速度上苦苦挣扎的障碍。
对于 PyTorch 2.0 的新编译器后端,我们从用户编写高性能自定义内核的方式中汲取灵感:越来越多地使用 Triton 语言。我们还希望有一个编译器后端能够使用类似于PyTorch eager 的抽象,并具有足够的通用性,以支持 PyTorch 中广泛的功能。TorchInductor 使用 pythonic define-by-run loop level IR 自动将 PyTorch 模型映射到 GPU 上生成的 Triton 代码和 CPU 上的 C++/OpenMP。TorchInductor 的核心 loop level IR 仅包含约 50 个运算符,并且它是用 Python 实现的,使其易于编程和扩展。
对于 PyTorch 2.0,我们想要加速训练。因此,至关重要的是,我们不仅要捕获用户级代码,还要捕获反向传播。此外,我们也想要重用已有的、久经检验的 PyTorch Autograd 系统。AOTAutograd 利用 PyTorch 的 torch_dispatch ****扩展机制来追踪我们的 Autograd 引擎,使我们能够“提前”捕获向后传递,这使我们能够使用 TorchInductor 同时加速前向和反向传播。
为 PyTorch 编写后端具有挑战性。PyTorch 有 1200 多个运算符,如果考虑每个运算符的各种重载,则有 2000 多个。
A breakdown of the 2000+ PyTorch operators
因此,编写后端或跨领域的功能成为一项耗费精力的工作。在 PrimTorch 项目中,我们致力于定义更小且更稳定的运算符集。PyTorch 程序可以统一降解到这些运算符集。我们的目标是定义两个运算符集:
我们将在下面的开发者/供应商体验部分讨论更多关于此主题的内容
我们引入了一个简单的函数 torch.compile 来包装您的模型并返回一个编译后的模型。
compiled_model = torch.compile(model)
这个 compiled_model 保持着对您的模型的引用,并将 forward 函数编译为更优化的版本。在编译模型时,我们给出了几个参数来配置:
def torch.compile(model: Callable,
*,
mode: Optional[str] = "default",
dynamic: bool = False,
fullgraph:bool = False,
backend: Union[str, Callable] = "inductor",
# advanced backend options go here as kwargs
**kwargs
) -> torch._dynamo.NNOptimizedModule
mode ****指定编译器在编译时应该优化什么。
编译体验旨在在默认模式下提供最多的好处和最大的灵活性,上图是您在每种模式下获得模型的特点。
现在,让我们看一下编译真实模型并运行它(使用随机数据)的完整示例
import torch
import torchvision.models as models
model = models.resnet18().cuda()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
compiled_model = torch.compile(model)
x = torch.randn(16, 3, 224, 224).cuda()
optimizer.zero_grad()
out = compiled_model(x)
out.sum().backward()
optimizer.step()
第一次运行时optimized_model(x)
,它会编译模型。因此,运行时间会更长。但随后的运行速度很快。
编译器有一些预设,以不同的方式调整编译后的模型。你可能正在运行一个小模型,由于框架的开销而变得很慢。或者,你可能正在运行一个大的模型,它几乎无法容纳在内存中。根据你的需要,你可能想使用不同的模式。
# API NOT FINAL
# default: optimizes for large models, low compile-time
# and no extra memory usage
torch.compile(model)
# reduce-overhead: optimizes to reduce the framework overhead
# and uses some extra memory. Helps speed up small models
torch.compile(model, mode="reduce-overhead")
# max-autotune: optimizes to produce the fastest model,
# but takes a very long time to compile
torch.compile(model, mode="max-autotune")
访问模型属性的工作方式与在 eager 执行方式下一样。您可以像往常一样访问或修改模型的属性(例如model.conv1.weight
)。这在代码修正方面是完全安全可靠的。TorchDynamo 将守卫(guards)插入代码以检查其假设是否成立。如果属性以某种方式发生变化,那么 TorchDynamo 就会知道根据需要自动重新编译。
# optimized_model works similar to model, feel free to access its attributes and modify them
optimized_model.conv1.weight.fill_(0.01)
# this change is reflected in model
Module 和 Tensor hooks 目前还不能完全工作,在完整开发完成后将能使用完整功能。
你可以序列化优化前后模型的 statedict。它们指向相同的参数和状态,因此是等价的。
torch.save(optimized_model.state_dict(), "foo.pt")
# both these lines of code do the same thing
torch.save(model.state_dict(), "foo.pt")
您当前还无法序列化 optimized_model。如果您希望直接保存对象,请改为保存 model。
torch.save(optimized_model, "foo.pt") # Error
torch.save(model, "foo.pt") # Works
对于模型推理,在使用 torch.compile
生成编译模型后,在实际模型服务之前最好运行一些预热步骤。这有助于缓解服务初期的峰值延迟。 此外,我们将引入一种称为 torch.export
的模式,该模式会为需要高保证、可预测延迟的环境谨慎地导出整个模型和守卫(guards)。torch.export
将需要更改您的程序,特别是如果您有数据相关的控制流。
# API Not Final
exported_model = torch._dynamo.export(model, input)
torch.save(exported_model, "foo.pt")
此特性还在在开发的早期阶段。
编译模式是不透明的并且难以调试。你会有这样的问题:
如果编译模式产生错误或崩溃或与 eager 模式不同的结果(超出机器精度限制),这不太可能是您的代码的错误。但是,了解错误的原因是哪一段代码是有用的。 为了帮助调试和可复现性,我们创建了几个工具和日志记录功能,其中一个尤为重要:Minifier
。 Minifier
****会自动将您看到的问题缩小为一小段代码。这个小代码片段重现了原始问题,您可以使用缩小的代码提交 github issue。这将帮助 PyTorch 团队轻松快速地解决问题。 如果您没有看到预期的加速,那么我们有 torch._dynamo.explain
****工具可以解释您的代码的哪些部分导致了我们所说的“图形中断”。图中断通常会阻碍编译器加速代码,减少图中断的数量可能会加速您的代码(达到收益递减的某个限制)。 您可以在 PyTorch 的故障排除指南中了解这些以及更多内容。
在查看支持 PyTorch 代码通用性的必要条件时,一个关键要求是支持动态形状,并允许模型采用不同大小的张量,而无需在每次形状更改时重新编译。 截至今天,对动态形状的支持是有限的,并且正在快速进行中。它将在稳定版本中具有完整的功能。你可以设置 dynamic=True 以启用它。在特征分支(symbolic-shapes)上我们取得了更多进展,我们已经在使用 TorchInductor 的完整符号形状训练中成功运行 BERT_pytorch。对于动态形状的推理,我们有更多的覆盖面。例如,让我们看一下常见的动态形状发挥作用的场景——使用语言模型生成文本。 我们可以看到,即使形状从 4 一直动态变化到 256,编译模式也能够始终优于 eager
高达 40%。在不支持动态形状的情况下,常见的解决方法是填充到最接近的 2 的幂。但是,正如我们从下表中看到的那样,它会产生大量的性能开销,并且还会导致编译时间显著延长。此外,填充有时并不容易正确执行。 通过在 PyTorch 2.0 的编译模式中支持动态形状,我们可以获得最佳的性能和易用性。
目前的工作发展非常迅速,随着我们对基础设施进行根本性改进,我们可能会暂时让一些模型倒退。可以在此处找到我们在动态形状方面的最新进展。
总之,torch.distributed
的两个主要分布式包装器在编译模式下运行良好。 DistributedDataParallel
(DDP) 和 FullyShardedDataParallel
(FSDP) 都可以在编译模式下正常工作,并且相对于 eager 模式提供了更好的性能和内存利用率。但也有一些注意事项和限制。 FP32 精度加速
左图:编译模式下 FSDP 比 eager 模式的加速(FP32 精度)。 右图:编译模式下的 FSDP 比 eager 模式下占用的内存要少得多
外部启动脚本以及那些内在使用 DDP 的包装器通常可以开箱即用。Hugging Face Accelerate、Lightning、torchrun 和 Ray Train 都已经过测试和验证。DeepSpeed 和 Horovod 尚未经过测试,我们希望尽快启用它们。 手动梯度检查点(即torch.utils.checkpoint*
)正在开发中,预计将在不久的将来启用。启用它的工作正在进行中,AOTAutograd 的 min-cut 分区程序部分缓解了这种情况,它重新计算 backward 调用中的某些值以减少峰值内存使用。从图中显示的 FSDP 在编译模式下的内存压缩结果可以看出这一点。 其他实验性分布式子系统,例如 DistributedTensor 和 PiPPy,尚未使用 TorchDynamo 进行测试。
DDP 的效率提升依赖于反向传播计算与 AllReduce 通信的重叠,并将较小的逐层 AllReduce 操作分组到“桶”中。TorchDynamo 编译的 AOTAutograd 函数在与 DDP 简单结合时阻碍了通信重叠;但通过为每个“桶”编译单独的子图,并允许通信操作发生在子图外部和子图之间,性能得以恢复。编译模式下的 DDP 支持目前还需要static_graph=True
和find_unused_parameters=True
,但这些不会是长期要求。有关 DDP + TorchDynamo 的方法和结果的更多详细信息,请参阅这篇文章。
FSDP 本身是一个“测试版”PyTorch 功能,由于能够调整哪些子模块被包装,并且通常有更多的配置选项,因此它的系统复杂性比 DDP 更高。配置 use_original_params=True 后,FSDP 可与 TorchDynamo 和 TorchInductor 一起用于各种流行模型。兼容性问题预计在特定模型或配置中产生,但都会被积极改进。提交 github issue 的模型将会被优先改进。 用户指定一个 auto_wrap_policy 参数来指示将其模型的哪些子模块一起包装在用于状态分片的 FSDP 实例中,或者手动将子模块包装在 FSDP 实例中。例如,当每个“transformer block”都包装在一个单独的 FSDP 实例中时,许多 transformer 模型运行良好,因此一次只需要具体化一个 transformer 块的完整状态。Dynamo 将在每个 FSDP 实例的边界插入图中断,以允许前向(和后向)通信操作发生在图外并与计算并行。 如果在不将子模块包装在单独实例中的情况下使用 FSDP,它会回退到与 DDP 类似的操作,但没有分桶。因此,所有梯度都在一次操作中减少,即使在 Eager 中也不会有计算/通信重叠。此配置仅使用 TorchDynamo 进行了功能测试,未进行性能测试。
我们对PyTorch 2.0及以后的发展方向感到非常兴奋。通往最终2.0版本的道路将是崎岖不平的,但请尽早加入我们的旅程。如果你有兴趣进一步深入研究或为编译器做出贡献,请关注这个月开始的 Ask the Engineers: 2.0 Live Q&A Series。其他资源包括:
欢迎点击链接加入技术交流群讨论 PyTorch 2.0 !!!