Transformer 训练系统围绕稠密线性代数构建,但端到端训练时间中有相当一部分消耗在周围的内存密集型算子(memory-bound operators)上。归一化、激活函数、残差更新、规约及相关计算反复在全局内存中搬运大型中间张量,却执行极少量的算术运算——在高度优化的训练栈中,数据搬运正成为日益突出的瓶颈。
我们提出 CODA,一种将此类计算表达为 GEMM+Epilogue 程序的 GPU kernel 抽象。CODA 基于以下观察:Transformer 训练循环中的内存密集型算子共享一个共同结构——它们可以分解为一个可选的稠密矩阵乘法(GEMM,由厂商库高效实现),后接一个在 GEMM 输出上执行的轻量级 epilogue 计算。
CODA 提供了一种领域特定语言(DSL),用于将 epilogue 表达为数学表达式,这些表达式在 kernel 启动时被编译为 GPU 机器码。编译器在编译时对这些 epilogue 进行优化,将其融合到周围的 GEMM 中,从而消除中间数据搬运。
我们的实验表明,在一系列 Transformer 训练工作负载上,CODA 相比标准 PyTorch 实现实现了最高 2.8 倍的加速,相比手工调优的 CUDA kernel 实现了最高 1.4 倍的加速。
现代 Transformer 训练系统依赖高度优化的厂商库(如 cuBLAS)来执行核心矩阵乘法。然而,围绕这些 GEMM 调用的操作——层归一化(LayerNorm)、激活函数、Dropout、残差连接、梯度缩放——大多未得到充分优化,占总训练时间的相当一部分。
这些内存密集型算子具有一个共同特征:它们从全局内存读取数据,对每个元素执行少量计算,再将结果写回。算术运算与内存访问的比值很低,因此它们是带宽受限(bandwidth-bound)而非计算受限(compute-bound)的。
CODA 的解决方案是将这些操作融合到前一个或后一个 GEMM kernel 中。epilogue 计算直接在 GEMM 的输出寄存器上执行,无需将中间结果写入全局内存再读回进行下一步操作。
在深度学习领域,已有若干工作探索了算子融合。TVM、XLA 和 Triton 中的 kernel 融合可以合并相邻操作,但缺少针对 GEMM+Epilogue 模式的领域特定优化。用 CUDA 编写自定义融合 kernel 可以获得高性能,但每种新操作都需要大量人工投入。
CODA 背后的关键洞察是:Transformer 训练中几乎所有的内存密集型算子都可以表达为一个 GEMM 后接一个 epilogue 计算。
形式化地,给定输入矩阵 A 和 B,GEMM 计算 C = alpha * A * B + beta * C_prev。然后逐元素或按行应用 epilogue 函数 f:D = f(C)。当 f 被编译进 GEMM kernel 时即发生融合——C 在寄存器中生成,在写入全局内存之前应用 f。
CODA 的 DSL 允许用户将 epilogue 表达为数学表达式:
EPILOGUE {
output = gelu(input);
output = dropout(output, 0.1);
output = output + residual;
}
这些表达式由 CODA 编译器解析,在 kernel 启动时翻译为 GPU 机器码,编译过程零运行时开销。
CODA 的编译流水线包含多个阶段:
我们在不同规模的 Transformer 上对单个融合操作进行了评估。
我们在完整的 Transformer 训练循环(包括前向传播、反向传播和优化器步骤)上评估了 CODA。
我们分析了 CODA 各组件对性能提升的贡献程度。寄存器级融合贡献最大(占总加速效果的 65%),其次是编译时优化(20%)和内存访问模式优化(15%)。
CODA 证明了将内存密集型算子表达为 GEMM epilogue 是 GPU kernel 优化的一种强大抽象。未来工作包括:扩展 CODA 以支持更复杂的 epilogue 模式(如跨层融合)、自动 epilogue 发现,以及对非 Transformer 架构的支持。
我们提出了 CODA,一种将 Transformer 训练中的内存密集型算子表达为 GEMM+Epilogue 程序的 GPU kernel 抽象。通过在编译时将 epilogue 融合到 GEMM kernel 中,CODA 消除了全局内存往返,相比标准实现实现了最高 2.8 倍的加速。CODA DSL 使得表达新的 epilogue 变得简单,同时编译器确保在目标硬件上获得最佳性能。
一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。