The Performance of CUDA with the Flexibility of PyTorch
The Performance of CUDA with the Flexibility of PyTorch
Mark Saroufim
目录
- 摘要
llm.c:性能与开发的权衡- PyTorch 的灵活性:Eager 模式
- 硬件瓶颈的演变
torch.compile:应对内存带宽瓶颈- 为何仍需编写 CUDA
- 案例研究:量化
- 自定义操作 (Custom ops
- 如何支持自定义 CUDA 扩展
- 问题:软件包非纯 Python
- 融合到用户定义代码中
- 你应该编写什么样的内核
- TL;DR (内容摘要
- 附录 (Appendix
- 反馈 (Feedback
摘要
本次演讲的目标是展示 PyTorch 用户如何从机器学习研究者转变为系统研究者,并说服听众也进行同样的转变。
llm.c:性能与开发的权衡
llm.c 是一个用原生 CUDA 实现的完整 GPT-2 训练循环。
- GitHub 仓库: https://github.com/karpathy/llm.c
- 其性能优于 torch.compile。
- 然而,该项目由 4 位顶尖工程师耗时约 6 个月完成。
- 这引出了一个核心问题:什么是更好的权衡(tradeoff)?
PyTorch 的灵活性:Eager 模式
PyTorch 的 Eager 编程模式因其直观易用而广受欢迎。用户执行的每一行代码都会立即在后端执行相应的操作。
x = torch.tensor([1, 2, 3], device="cuda"): 分配 GPU 内存。x = x + 1: 使用 CUDA 广播数字 1 并执行向量加法。print(x): 同步 GPU 并读取结果。这使得调试和交互式编程变得简单。x = torch.cos(x): 启动一个用于计算余弦的 CUDA 核函数。x = torch.relu(x): 启动一个用于 ReLU 的 CUDA 核函数。
硬件瓶颈的演变
现代 GPU 的计算能力增长速度远超内存带宽的增长速度。这导致许多操作受限于内存带宽,而非计算能力。
- CPU 和 GPU 之间的数据传输,在实践中受限于铜线中电子的速度,成为了一个显著的瓶颈。
- 更多信息可参考 Stephen Jones 在 GTC 2022 上的演讲《How CUDA Programming Works》。
torch.compile:应对内存带宽瓶颈
torch.compile 是一个融合编译器(fusion compiler),它是应对内存带宽瓶颈的主要方法之一。通过将多个操作融合成一个单一的 CUDA 核函数,可以显著减少内存的读写次数。
-
示例:
torch.compile(torch.square): 编译成 1 个核函数。torch.compile(torch.square(torch.square)): 同样只编译成 1 个核函数。
-
融合的优势(以 Double Square 为例):
- 不使用融合: 需要 2 次内存读取和 2 次内存写入。
- 使用融合: 仅需 1 次内存读取和 1 次内存写入。
-
警告: 融合并非总是有益的,因为它可能导致寄存器溢出(spilling registers),因此需要启发式方法来决定何时进行融合。
为何仍需编写 CUDA?
尽管 torch.compile 等编译器功能强大,但在某些情况下,手动编写 CUDA 仍然是必要的。
编译器在处理需要对表达式进行数学重写(同时保持数值稳定性)的优化时,通常会遇到困难。一个典型的例子是 Flash Attention 2。
在关于 Flash Attention 2 论文的 OpenReview 讨论中,一位审稿人提出了编译器是否能自动生成类似 FA-2 的优化的问题。作者回答:
<blockquote>“编译器通常可以执行融合。然而,对于那些需要对同一表达式进行数学重写(同时保持数值稳定性)的优化,编译器通常更难处理。”
来源: https://openreview.net/forum?id=mZn2Xyh9Ec
</blockquote>这表明,对于需要深度领域知识和复杂算法重构的顶尖性能优化,手动编写 CUDA 仍然具有不可替代的价值。
案例研究:量化
- 相关博文:https://pytorch.org/blog/pytorch-native-architecture-optimization/
- 对训练和推理过程中的激活值、权重、梯度、优化器进行量化。
- 我们的大部分功能实现是通过
torch.compile()完成的,但我们最快的 int4 推理内核是使用 CUTLASS 编写的。 - 这对于减少内存带宽瓶颈非常重要。
自定义操作 (Custom ops)
PyTorch 拥有许多如 torch.add、torch.sum 这样的操作符。
如果没有自定义操作的注册,torch.compile() 和 torch.autograd 将无法理解如何处理它们。
以下代码展示了如何注册一个“伪”(fake)实现:
@torch.library.register_fake("extension_cpp::mymuladd")
def _(a, b, c):
torch.check(a.shape == b.shape)
torch.check(a.dtype == torch.float)
torch.check(b.dtype == torch.float)
torch.check(a.device == b.device)
return torch.empty_like(a)
这种方法不允许输入发生变异(mutation)。
更多信息请参考:https://pytorch.org/tutorials/advanced/custom_ops_landing_page.html
如何支持自定义 CUDA 扩展
为了支持自定义 CUDA 扩展,您需要在两个地方进行设置:
-
在
setup.py文件中:- 如左侧代码所示,通过
get_extensions()函数配置编译选项。 - 使用
torch.utils.cpp_extension.CUDAExtension来定义扩展,指定源文件(.cpp和.cu),并添加额外的编译和链接参数。
- 如左侧代码所示,通过
-
在 C++ 代码中注册操作:
- 如右侧代码所示,使用
TORCH_LIBRARY_FRAGMENT宏在 C++ 中将自定义操作注册到 PyTorch 的调度器中。 m.impl_abstract_pystub用于链接到 Python 端定义的抽象实现(pystub)。
- 如右侧代码所示,使用
更多详情可参考:https://github.com/pytorch/ao/pull/135
问题:软件包非纯 Python!
这意味着您现在需要为所有可能的组合构建您的软件包:
- Python 版本(最近由 Jane Xu 修复)
- CUDA 版本
- Torch 版本
- 操作系统
对于 torchao 项目,我们在持续集成(CI)中启动了许多机器来完成这项工作。
许多项目会要求您从源码安装,或者将支持限制在带有最新稳定版 CUDA 和 Torch 的 Linux 系统上。
如果您不支持某个版本,pip 会在用户机器上静默地降级软件包。🤯
长期来看,我们需要转向一个类似 JIT 的工作流,但其代价是较长的冷启动时间。
融合到用户定义代码中
通常情况下,torch.compile 将内核视为黑盒,并且没有好的工具来解决黑盒融合问题。
下面的方法对于简单的逐点操作(pointwise ops)是可行的,但通常不具有普适性。
如上图所示,对于两个由 triton.jit 定义的函数 func1 和 func2,一种简单的融合思路是直接通过字符串拼接来创建一个新函数 func3,该函数按顺序调用前两者。但这并非一个通用的解决方案。
你应该编写什么样的内核?
- 低比特矩阵乘法 (Low-bit matmuls)
- 稀疏注意力 (Sparse attention)
- KV 缓存管理器 (KV cache managers)
- 关注技术栈更高层级的性能问题
- 如果您编写的许多内核看起来很相似,尝试更多地使用元编程 (metaprogramming)
如果您需要同行交流和发布工作的平台,可以考虑加入 Discord 社区:https://discord.gg/gpumode
TL;DR (内容摘要)
- 世界追求性能。
- 像 PyTorch 这样的灵活框架可以解决融合问题,但无法解决算法系统层面的问题。
- 自定义内核将长期存在并持续发展,PyTorch 正在使用它们,您也应该这样做!
- 得益于 CUTLASS、Triton 和各种元编程技术,自定义内核的入门门槛已经降低。字符串(代码)易于内省(introspect),但难以调试。
- 借助我们的自定义操作支持,集成和发布自定义内核变得很容易。JIT(即时编译)与 AOT(提前编译)代表了不同的权衡。
附录 (Appendix)
(此页为空白,作为附录部分的开始)
反馈 (Feedback)
-
来自 Fred 的反馈:
- 我们是否应该提及使用云服务提供商?
- 深度学习容器(不像 CPU 那样可共享)。
- 像性能计数器这样的东西在云供应商那里很少见,只有少数提供(例如 Lightning, AWS Bare Metal)。
- 提供一些更清晰的性能数据。
-
来自 Vikram 的反馈:
- 请准确提及 cutlass 后端是如何工作的,它会生成一段 cutlass 代码。并解释其发生过程。
- 更详细地介绍模板元编程及其挑战。
- 将动机部分的时间从7分钟减少到3分钟,并扩展关于代码生成等内容。
- 主要结论应该是我们可以代码生成“难以编写”的语言,使其更易于使用。
- 针对特定形状提供更多性能数据:例如选择 llama 70B 的形状。
-
来自 Tami 的反馈:
- CUDA 技术简报会与此会议时间冲突。
- 选择上午的会议时段。