Manish Gupta, April 13, 2021
CUTLASS 是一个用于深度学习和线性代数的 CUDA C++ 模板库。下图展示了其从 CUTLASS 1.3 到 2.6 的发展时间线,以及与 CUDA 版本的对应关系。GTC 2021 的重点是 2.4 至 2.6 版本,主要涵盖了隐式 GEMM 卷积、张量规约和结尾融合等功能。
CUTLASS 2.4 - 2020年11月
CUTLASS 2.5 - 2021年2月
CUTLASS 2.6 - 即将发布
下图展示了在 NVIDIA A100 GPU 和 CUDA 11.3 环境下,使用混合精度训练 (F16<->F16+F32) 时,CUTLASS 2.5 在 ResNet50 各层上的性能与 cuDNN 的对比。几何平均 (Geomean) 结果显示,CUTLASS 的性能达到了 cuDNN 的 90%。
卷积操作涉及三个张量:激活张量 (x)、滤波器张量 (w) 和输出张量 (y)。
NPQK = {1, 3, 3, 4}NHWC = {1, 4, 4, 3}KRSC = {4, 2, 2, 3}前向传播的数学定义如下:
其中:
* y[n, p, q, k] 是输出张量在特定位置的值。
* x[...] 是输入激活张量的值。
* w[...] 是滤波器张量的值。
* h_bar 和 w_bar 是根据步长 (stride)、填充 (pad) 和扩张 (dilation) 计算出的输入坐标。
卷积操作可以映射为通用的矩阵乘法 (GEMM) 操作,这使得可以利用高度优化的 GEMM 核函数来加速卷积。
y = CONV(x, w)C = GEMM(A, B)映射关系如下:
* 4D 激活张量 x[N,H,W,C] -> 2D 卷积矩阵 A[NPQ, RSC]
* 4D 滤波器张量 w[K,R,S,C] -> 2D 滤波器矩阵 B[RSC, K]
* 4D 输出张量 y[N,P,Q,K] -> 2D 输出矩阵 C[NPQ, K]
从 GEMM 的滤波器矩阵坐标 (gemm_k, gemm_n) 到 4D 滤波器张量坐标 (k, r, s, c) 的映射关系。
从 GEMM 的输出矩阵坐标 (gemm_m, gemm_n) 到 4D 输出张量坐标 (n, p, q, k) 的映射关系。
从 GEMM 的卷积矩阵坐标 (gemm_m, gemm_k) 到 4D 激活张量坐标 (n, h_bar, w_bar, c) 的映射关系。这是隐式 GEMM 的核心,即在计算过程中动态计算激活张量的坐标,而无需事先构造一个巨大的矩阵(im2col)。
以下伪代码展示了如何通过一个三重循环来实现映射到 GEMM 的卷积操作。循环遍历 GEMM 输出矩阵的维度 (M, N) 和内积维度 (K),并在循环体内将 GEMM 坐标实时转换回卷积张量的坐标,以访问激活张量和滤波器张量中的相应元素。
前向传播(Fprop)的卷积运算 y = CONV(x,w) 可以通过通用矩阵乘法(GEMM)实现。其中:
- x[N,H,W,C]:4D激活张量
- w[K,R,S,C]:4D滤波器张量
- y[N,P,Q,K]:4D输出张量
卷积运算映射到GEMM的维度如下:
- GEMM-M = NPQ
- GEMM-N = K
- GEMM-K = RSC
朴素显式GEMM卷积实现会在全局内存(Global Memory)中创建卷积矩阵(即im2col操作)。这种方法存在一个主要缺点:显式GEMM卷积会使全局内存占用和流量增加RS倍,其中R和S是滤波器的高度和宽度。
隐式GEMM卷积是一种更高效的实现方式。它在共享内存(Shared Memory)中动态(on the fly)构建卷积矩阵,而不是在全局内存中物化整个矩阵。
与显式方法相比,隐式GEMM卷积不会增加全局内存的占用和流量,从而显著提高了效率。
分块GEMM是一种分层、切片的计算模型,旨在通过在共享内存和寄存器中重用数据来优化计算。其数据流如下:
1. 全局内存:存储原始的GEMM A和GEMM B矩阵。
2. 共享内存:加载一个线程块(Thread Block)大小的数据块(Tile)。
3. 寄存器文件:从共享内存加载一个线程束(Warp)大小的数据块。
4. CUDA/Tensor Cores:在这些计算单元上执行计算。
5. SMEM/CUDA Cores:通过Epilogue Tile和Epilogue Functor处理计算结果。
6. 全局内存:将最终结果写回。
更多关于此模型的详细信息,可以参考CUTLASS GTC 2018和2020的演讲。
实现隐式GEMM卷积需要新的CUTLASS组件。下图展示了隐式GEMM卷积如何映射到CUTLASS的分层结构中:
cutlass::conv::threadblock: 负责处理隐式GEMM卷积的数据加载部分,将数据从全局内存加载到共享内存。这是本次演讲(GTC talk 2021)的重点。cutlass::gemm::warp: 负责线程束级别的计算,这部分重用了现有的GEMM组件(在GTC talks 2018-2020中介绍)。cutlass::epilogue: 负责处理计算的收尾工作,如Epilogue Fusion(将在CUTLASS 2.6版本中发布)。隐式GEMM卷积算法主要包括以下三个步骤:
1. 将卷积矩阵和滤波器矩阵的一个数据块(tile)加载到共享内存中。
2. 对共享内存中的操作数执行矩阵乘累加(mma)计算。
3. 沿RSC维度进行迭代。
以一个具体的例子说明数据块的加载过程:
- 示例数据块大小:
- Tile_M = 128
- Tile_N = 128
- Tile_K = 32
- 输入类型: F16
(n, p, q)坐标。c坐标。c坐标。k坐标。c=0..7的数据到共享内存。此步骤直接使用cutlass::gemm::warp中的线程束级(warp-level)mma操作符,以充分利用NVIDIA Tensor Cores的计算能力。数据从共享内存经由寄存器文件流向Tensor Cores进行计算。
迭代过程包括:
a) 前进以加载下一个数据块到共享内存。
b) 确保对所有滤波器位置(r, s)和通道c进行累加。
为了覆盖整个GEMM-K(RSC)维度,需要在滤波器位置s、r以及通道c上进行迭代。需要启动足够多的平铺迭代来覆盖所有通道元素(C)和滤波器位置(R-by-S)。
平铺迭代次数的计算公式为:
num_tiled_iterations = R * S * ((C + Tile_K - 1) / Tile_K)
cutlass::conv::threadblock::Iterators是为实现隐式GEMM卷积而设计的关键抽象。这些迭代器封装了复杂的地址计算和数据加载逻辑。
迭代器实现的抽象接口:
- advance(): 移动到GEMM-K维度上的下一个平铺迭代。
- operator++(): 移动到线程的下一个加载位置。
- at(): 应用函数将输出坐标(p, q)映射到激活张量中的坐标(h, w)。
- valid(): 检查对全局内存中张量的访问是否越界。
- get(): 根据张量坐标获取全局内存中的指针。
以下是迭代器中部分核心功能的伪代码实现,展示了如何从逻辑上的卷积矩阵坐标分析计算出物理内存地址。
at(): 根据输出坐标(p, q)、滤波器偏移(r, s)、步长和扩张率计算出在激活张量中的实际坐标(h, w)。valid(): 检查at()函数计算出的坐标是否在激活张量的有效范围内。get(): 根据多维坐标和张量的步长(Strides)计算一维内存偏移量,并返回指针。然而,这种朴素的分析式实现方式存在性能问题。
一个朴素的分析式实现会产生过多的非Tensor Core数学指令,这会影响整体性能。
为了优化卷积抽象的实现,采用了以下策略:
增量表 (Delta tables)
掩码谓词 (Mask predicates)
下图展示了卷积运算中涉及的矩阵:滤波器矩阵 (B, Filter matrix),卷积矩阵 (A, Convolution matrix),以及输出矩阵 (C, Output matrix)。
在标准的实现中,访问激活张量的坐标需要复杂的计算,如 valid() 和 get() 函数所示。这些计算涉及多次乘法和加法,以确定内存偏移量,并在主循环中重复执行,效率低下。
为了覆盖卷积矩阵中单个 npq(批次、输出高、输出宽)对应的 RSC(滤波器高、滤波器宽、输入通道)维度,需要在 r, s, c 维度上移动并索引到每个 rsc 位置。这导致了过多的非张量核心(non-tensor core)运算。
通过预计算增量,可以简化地址计算。这些增量值对于固定的npq和问题规模是恒定的。
s=0 移动到 s=1 时,在激活张量(NHW-by-C格式)中垂直移动的元素数量。Δs = C 个元素。r=0 移动到 r=1 时,在激活张量中垂直移动的元素数量。Δr = W * C 个元素。Tile_K 通道元素时,在激活张量中水平移动的元素数量。Δc = Tile_K 个元素。增量表的不变性 (Invariance)
关键在于,Δs, Δr, 和 Δc 这三个增量值在整个内核执行过程中都是不变的。因此,可以预先计算它们,并在循环中通过简单的加法来更新激活张量的指针,从而极大地减少了计算开销。下图展示了如何使用这些预计算的增量来遍历激活张量。
掩码谓词策略旨在优化越界(Out-of-Bounds, OOB)检查。原始的 valid() 函数需要在循环内进行多次逻辑比较。
该策略利用了访问模式的规律性。对于一个给定的线程块,其访问的激活张量区域是固定的。因此,哪些访问是有效的(在边界内),哪些是无效的(越界),是可以预先知道的。
下图展示了n=0时,线程T0在不同Tile_K迭代中的首次访存情况。
Tile_K 迭代 (r=0, s=0, c=0-7): 计算出的 h_bar 为 -1,这是一个越界访问(OOB)。Tile_K 迭代 (r=0, s=1, c=0-7): 计算出的 h_bar 仍为 -1,越界。Tile_K 迭代 (r=0, s=2, c=0-7): 计算出的 h_bar 仍为 -1,越界。掩码谓词的不变性 (Invariance)
随着迭代的进行,最终会出现有效的访问。
通过这种方式,循环内的复杂逻辑判断被替换为简单的位运算和查表,从而提高了效率。
反向数据梯度(Dgrad)的计算可以表示为 dx = CONV(dy, w),其中 dy 是输出梯度张量,w 是滤波器张量,dx 是激活梯度张量。这个卷积操作同样可以映射为一个通用的矩阵乘法(GEMM)操作。
GEMM-M = NHWGEMM-N = CGEMM-K = KRS下图展示了如何将 dy(输出梯度)和 w(滤波器)重排成矩阵,通过 GEMM 运算得到 dx(激活梯度)。
后向权重梯度 (Wgrad) 的计算 dw = CONV(dy, x) 也可以映射为 GEMM。
- dy[N,P,Q,K]: 4D 输出梯度张量
- x[N,H,W,C]: 4D 激活张量
- dw[K,R,S,C]: 4D 滤波器梯度张量
GEMM 的维度映射如下:
- GEMM M = K
- GEMM N = RSC
- GEMM K = NPQ
在此映射中:
- GEMM-A 是输出梯度矩阵 dy,维度为 K x NPQ。
- GEMM-B 是卷积矩阵 x(激活张量),维度为 NPQ x RSC。
- GEMM-C 是滤波器梯度矩阵 dw,维度为 K x RSC。
基于 Resnet50 层的 Tensor Cores 性能 (F16 <= F16*F16 + F32)
Tensor Core (F16): 16x
在 NVIDIA A100 上,使用 F16 输入的 Tensor Cores 是训练最快的数据类型。
图表显示了在 NVIDIA A100 上,使用 Tensor Cores 进行混合精度训练 (F16 <= F16*F16 + F32) 时,相对于 CUDA Cores (F32) 的加速比。cuDNN 的几何平均加速比为 11.6x,而 CUTLASS 为 10.8x。
基于 Resnet50 层的 Tensor Cores 性能 (F32 <= TF32*TF32 + F32)
Tensor Core (TF32): 8x
TensorFloat32 (TF32) 使 Tensor Cores 能够直接处理 F32 输入。
图表显示了 CUTLASS 在 Tensor Cores 上使用 TensorOp(TF32) 进行训练时的性能。前向传播、后向数据梯度和后向权重梯度的几何平均加速比为 5.3x。
基于 Resnet50 层的 Tensor Cores 性能
以下图表展示了 CUTLASS 2.5 在 NVIDIA A100 和 2080Ti 上,使用 F16、S8 和 S4 数据类型进行推理时,相对于 cuDNN F16[NHWC] 的性能。
F16 数据类型 (F16 <= F16*F16 + F16)
S8 数据类型 (S8 <= S8*S8 + S32)
S4 数据类型 (S4 <= S4*S4 + S32)
Epilogue Fusion 是指在 GEMM 计算(位于寄存器中)完成之后、将结果写回全局内存之前,将一个或多个附加操作融合进来的技术。
CUTLASS 支持的 Epilogue Fusion 模式及其应用:
| Epilogue Fusion 模式 | 应用 |
|---|---|
| Element-wise operators (按元素操作) | Scale (缩放), bias (偏置), activations (激活函数) |
| Data type conversion (数据类型转换) | F32->F16, Int32->Int8 |
| Broadcast vector over columns (列向量广播) | Bias add (偏置加法) |
| Partial reduction over columns (列部分规约) | Sum or sum-of-squares for batch norm (批量归一化的求和或平方和) |
下图展示了两种 Epilogue Fusion 模式:列向量广播(用于偏置加法)和列部分规约(用于批量归一化)。
CUTLASS 2.4 和 2.5: Nov 2020 和 Feb 2021
- 开源的 CUDA C++ 模板库,用于 CUDA 开发。
- 提供可重用的构建模块,用于利用 Tensor Cores 进行 GEMM 和卷积运算。
- CUTLASS 卷积性能与 cuDNN 相当(> 90%)。
CUTLASS 2.6: 即将发布的版本展望
- 支持新的 epilogue fusion 模式:
- 列向量广播(偏置加法)
- 列部分规约(批量归一化)
立即开始! https://github.com/NVIDIA/cutlass