Mark Saroufim
llm.c:性能与开发的权衡torch.compile:应对内存带宽瓶颈本次演讲的目标是展示 PyTorch 用户如何从机器学习研究者转变为系统研究者,并说服听众也进行同样的转变。
llm.c:性能与开发的权衡llm.c 是一个用原生 CUDA 实现的完整 GPT-2 训练循环。
- GitHub 仓库: https://github.com/karpathy/llm.c
- 其性能优于 torch.compile。
- 然而,该项目由 4 位顶尖工程师耗时约 6 个月完成。
- 这引出了一个核心问题:什么是更好的权衡(tradeoff)?
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 的计算能力增长速度远超内存带宽的增长速度。这导致许多操作受限于内存带宽,而非计算能力。
torch.compile:应对内存带宽瓶颈torch.compile 是一个融合编译器(fusion compiler),它是应对内存带宽瓶颈的主要方法之一。通过将多个操作融合成一个单一的 CUDA 核函数,可以显著减少内存的读写次数。
示例:
torch.compile(torch.square): 编译成 1 个核函数。torch.compile(torch.square(torch.square)): 同样只编译成 1 个核函数。融合的优势(以 Double Square 为例):
警告: 融合并非总是有益的,因为它可能导致寄存器溢出(spilling registers),因此需要启发式方法来决定何时进行融合。
尽管 torch.compile 等编译器功能强大,但在某些情况下,手动编写 CUDA 仍然是必要的。
编译器在处理需要对表达式进行数学重写(同时保持数值稳定性)的优化时,通常会遇到困难。一个典型的例子是 Flash Attention 2。
在关于 Flash Attention 2 论文的 OpenReview 讨论中,一位审稿人提出了编译器是否能自动生成类似 FA-2 的优化的问题。作者回答:
<blockquote>“编译器通常可以执行融合。然而,对于那些需要对同一表达式进行数学重写(同时保持数值稳定性)的优化,编译器通常更难处理。”
来源: https://openreview.net/forum?id=mZn2Xyh9Ec
</blockquote>这表明,对于需要深度领域知识和复杂算法重构的顶尖性能优化,手动编写 CUDA 仍然具有不可替代的价值。
torch.compile() 完成的,但我们最快的 int4 推理内核是使用 CUTLASS 编写的。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 扩展,您需要在两个地方进行设置:
在 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
这意味着您现在需要为所有可能的组合构建您的软件包:
对于 torchao 项目,我们在持续集成(CI)中启动了许多机器来完成这项工作。
许多项目会要求您从源码安装,或者将支持限制在带有最新稳定版 CUDA 和 Torch 的 Linux 系统上。
如果您不支持某个版本,pip 会在用户机器上静默地降级软件包。🤯
长期来看,我们需要转向一个类似 JIT 的工作流,但其代价是较长的冷启动时间。
通常情况下,torch.compile 将内核视为黑盒,并且没有好的工具来解决黑盒融合问题。
下面的方法对于简单的逐点操作(pointwise ops)是可行的,但通常不具有普适性。
如上图所示,对于两个由 triton.jit 定义的函数 func1 和 func2,一种简单的融合思路是直接通过字符串拼接来创建一个新函数 func3,该函数按顺序调用前两者。但这并非一个通用的解决方案。
如果您需要同行交流和发布工作的平台,可以考虑加入 Discord 社区:https://discord.gg/gpumode
(此页为空白,作为附录部分的开始)
来自 Fred 的反馈:
来自 Vikram 的反馈:
来自 Tami 的反馈: