在Ascend C和GPU等并行计算编程中,Bank冲突是一个极其重要且常见的性能杀手。
简单来说,Bank冲突是指多个内存访问请求同时指向了同一个内存Bank,导致本可以并行执行的访问被迫串行化,从而大幅降低内存带宽和计算性能。
为了让你彻底弄懂,我们结合Ascend C的编程模型(特别是Local Memory/UB)来拆解这个概念。
1. 什么是Bank?为什么要有Bank?
为了实现极高的内存吞吐量,AI Core内部的存储(如Unified Buffer, UB)和外部存储(如L1 Cache, Global Memory)通常不会做成一整块大内存,而是被切割成多个独立的小模块,这些小模块就称为Bank。
- 交叉编址:数据在物理上是交替分布在这些Bank中的。
- 例如,假设有4个Bank(Bank 0, 1, 2, 3)。那么:
- 地址 0, 4, 8, 12… 存在 Bank 0
- 地址 1, 5, 9, 13… 存在 Bank 1
- 地址 2, 6, 10, 14… 存在 Bank 2
- 地址 3, 7, 11, 15… 存在 Bank 3
- 例如,假设有4个Bank(Bank 0, 1, 2, 3)。那么:
- 并行访问:每个Bank都有自己的数据总线。如果在一个时钟周期内,你需要读取地址0、1、2、3的数据,它们刚好分布在4个不同的Bank中,硬件就可以同时把这4个数据读出来,实现4倍的带宽。
2. 什么是Bank冲突?
如果在一个时钟周期内,多个并行执行的线程或向量计算单元,试图同时访问同一个Bank中的不同地址,冲突就发生了。
因为一个Bank同一时间只能响应一个读取/写入请求,硬件只能把这些请求排队(串行化)执行。
3. 在Ascend C中,Bank冲突通常发生在哪里?
在Ascend C编程中,最容易出现Bank冲突的地方是Unified Buffer (UB)。
当你在CopyIn阶段使用DataCopy把数据搬入UB,或者在Compute阶段使用向量指令(如Add,Mul)操作UB中的数据时,如果访问模式(步长)设计不当,就会引发冲突。
经典场景:矩阵转置或按列读取
假设你在UB中存了一个矩阵,按行优先排布。现在你想按列读取数据(比如读取第0列的所有元素)。
- 如果矩阵的宽度刚好是Bank数量的整数倍(比如16个元素,且硬件Bank数也是16的倍数)。
- 那么第0列的元素:第0行第0列、第1行第0列、第2行第0列……它们的地址分别是
0, 16, 32, 48... - 根据前面的交叉编址规则,这些地址全都映射到了同一个Bank(Bank 0)!
- 此时,如果你用一条向量指令去读取这一列,硬件就会发现所有请求都挤在Bank 0上,性能直接跌入谷底。
4. Bank冲突对性能的影响
- 流水线停顿:计算单元必须等待数据,Vector流水线被打断。
- 有效带宽骤降:如果有4个请求冲突在同一个Bank,原本1个周期读完的数据需要4个周期,实际带宽变成了理论值的1/4。
5. 如何解决和避免Bank冲突?
解决Bank冲突的核心思想是:打破数据在Bank上的规律性对齐,让访问请求均匀分散到不同Bank中。
常用手段:Padding(补齐)
这是最常用、最简单粗暴的方法。在数据搬入UB时,人为地在每一行末尾添加几个无用的“占位”数据,改变行的实际宽度,从而让下一列的数据错开同一个Bank。
- 原矩阵:宽度16,按列读取全在Bank 0。
- Padding后:在每行末尾加1个无用数据,实际宽度变成17。
- 此时第0列的地址变成:
0, 17, 34, 51... - 它们会分别映射到不同的Bank(因为17不是Bank数的整数倍),冲突解除!
在Ascend C的DataCopy接口中,通常可以通过设置DataCopyParams中的blockLen、stride等参数,或者在定义Tensor时预留Padding空间来实现。
其他手段: - 数据重排:在数据搬入UB前,在GM阶段就通过双缓冲或特定指令将其重新排布成更适合后续计算的格式。
- 使用专门的指令:Ascend C提供了一些专门用于数据搬运和格式转换的高效指令(如
CopyData配合特定stride),硬件在设计时已经考虑了避免冲突,尽量使用这些原语而不是自己手动按奇奇怪怪的步长去读UB。
总结
- Bank= 内存物理上的并行通道。
- Bank冲突= 多个请求挤在同一通道,导致并行变串行。
- Ascend C中常见诱因= 在UB中按不合理的步长(尤其是Bank数整数倍的步长)跨行访问数据。
- 终极解法=Padding(补齐),破坏对齐规律,让数据均匀分布。