趋近智
现代深度学习 (deep learning)加速器提供大量的算术吞吐量 (throughput)。然而,实现峰值利用率很少受限于浮点运算单元的速度。相反,瓶颈几乎总是数据从内存移动到处理器的速率。从DRAM获取操作数的成本可能比执行乘法本身的成本高出几个数量级。这种差异通常被称为内存墙。
为减少这种延迟,硬件架构师采用多级缓存(L1、L2、L3)。循环分块,也叫循环阻塞,是使软件执行与此硬件层级对齐 (alignment)的主要软件转换方法。它将循环嵌套的迭代空间划分为更小的块,这些块可以放入特定级别的快速内存中。这能保证数据块一旦载入,就会在被淘汰前尽可能多地重复使用。
考虑一个标准的稠密矩阵乘法 ,其中所有矩阵的尺寸均为 。一个简单的实现通常包含三个嵌套循环:
在行主序存储格式中,访问模式显示出性能风险:
当 足够大时,工作集大小会超出缓存容量。当程序完成 的一行并需要重用 的元素时,包含 的缓存行很可能已被淘汰。
分块通过引入新循环来按块遍历数据,以解决此问题。我们将原始迭代空间 分为遍历块的外部循环 和在块内迭代的内部循环 。
如果我们选择块大小为 ,转换后的逻辑如下:
// 遍历块
for (int i_o = 0; i_o < N; i_o += T) {
for (int j_o = 0; j_o < N; j_o += T) {
for (int k_o = 0; k_o < N; k_o += T) {
// 执行当前块(适应缓存)
for (int i_i = i_o; i_i < min(i_o + T, N); i_i++) {
for (int j_i = j_o; j_i < min(j_o + T, N); j_i++) {
for (int k_i = k_o; k_i < min(k_o + T, N); k_i++) {
C[i_i][j_i] += A[i_i][k_i] * B[k_i][j_i];
}
}
}
}
}
}
这种重构改变了操作顺序,但保持了数学上的等价性。处理器现在将 和 的一个 块载入缓存,并利用这些数据计算所有必需的部分积,然后再移至下一个块。
下图说明了迭代空间如何被划分为块。灰色区域表示正在处理的活跃块,它必须完全适应目标缓存级别。
将大型矩阵迭代空间分解为更小的块。活跃块(蓝色)代表当前驻留在 L1 缓存中的工作集。
我们可以通过计算数据重用因子来量化 (quantization)分块的好处。假设一个简化的缓存模型,其中缓存可以容纳内部循环所需的三个 块。
不进行分块时,矩阵 的内存传输总次数(假设严格执行的LRU淘汰策略和较大的 )接近 ,因为 的行会为 的每一列重新获取。
通过分块,要计算 的一个 块,我们需要遍历 和 的 个块。
算术密度(每字节浮点运算数)随块大小 线性提高:
较大的块意味着更好的带宽效率,前提是它们能适应缓存。如果选择 使 ,就会发生缓存颠簸,性能会急剧下降。
下方图表显示了块大小与有效内存带宽利用率之间的理论关系。请注意,当块大小超出缓存容量时会出现“性能断崖”。
块大小对内存带宽的影响。性能随块大小的增加而提高,因为重用效果更好,直到工作集超出 L1 缓存容量(红色虚线),导致因颠簸而下降。
现代CPU和GPU有多层内存(寄存器、L1、L2、VRAM/DRAM)。高级编译器不止步于单一级别的分块。它们执行多级分块以在层级结构的每一级实现最大化重用。
在 Apache TVM 或 Halide 等深度学习 (deep learning)编译器中,分块作为一种调度原语表达,与算法定义分离。这种分离使得寻找最佳块大小 () 的过程可以自动化。
例如,在 TVM 的 te(张量表达式)语言中,分块调度应用于计算的轴:
# 定义计算
A = te.placeholder((N, N), name='A')
B = te.placeholder((N, N), name='B')
k = te.reduce_axis((0, N), name='k')
C = te.compute((N, N), lambda i, j: te.sum(A[i, k] * B[k, j], axis=k))
# 创建调度
s = te.create_schedule(C.op)
# 应用分块
# 将 i 和 j 轴按因子 bn 和 bm 分割
bn, bm = 32, 32
xo, yo, xi, yi = s[C].tile(C.op.axis[0], C.op.axis[1], bn, bm)
# 重新排序,先外部循环,再规约,最后内部循环
k_outer, k_inner = s[C].split(k, factor=4)
s[C].reorder(xo, yo, k_outer, xi, yi, k_inner)
tile 原语有效地分割轴并重新排序它们。随后的 reorder 命令作用很大;它确保遍历缓存驻留块 (xi, yi) 的循环是最内层的空间循环。
当矩阵维度 不能被块大小 完全整除时,分块会带来一些复杂情况。这会导致“余数”循环,即矩阵边缘的小块计算。
编译器通过两种方式处理这种情况:
掌握循环分块,您就定义了硬件消耗数据的粒度。这是将数学定义转换为高性能二进制文件的主要步骤。随后的部分将说明如何对这些块进行矢量化 (quantization)以使用 SIMD 指令。
这部分内容有帮助吗?
© 2026 ApX Machine LearningAI伦理与透明度•