跳转至

性能评测与优化

本节介绍如何评测 NPU 的算力与利用率,以及可以从哪些方向优化。考察的核心不只是“跑得越快越好”,更是能说清楚瓶颈在哪里、优化带来了多少收益。

性能评测方法

为了评估优化效果,需要对 NPU 的软硬件进行性能评测,方法如下:

  1. 选取几个不同规模的矩阵乘分别进行性能评测,可以先从 4x4x4 开始,逐步扩大,建议选取脉动阵列大小的整倍数,更大的矩阵可以带来更高的计算访存比,进而更容易达到更高的算力利用率,但也对片上存储带来了挑战;
  2. 初始情况下,输入的 A 和 B 矩阵都在 SRAM(或 CPU 能访问的缓存)当中,且必须是行优先存储,没有额外的 padding,每次评测前需要由 CPU 进行随机的初始化,使得每次计算的输入矩阵都不相等;
  3. 统计矩阵乘的总时间,从调用算子开始,到算子执行结束为止,此时结果 C 矩阵须在 SRAM(或 CPU 能访问的缓存)当中,必须是行优先存储,没有额外的 padding,在 CPU 侧测量端到端的周期数;
  4. 至少评测五次,去掉开头的两次,取其余三次性能的平均值,且这五次评测是连续进行的,即在 CPU 的 C 代码里通过循环实现;
  5. 计算实际算力:运算次数等于 2*M*N*K(一次乘加计 2 次运算),时间等于周期数除以主频,二者做除法即得实际算力;
  6. 计算峰值算力:理论峰值算力等于 2*PE_N*PE_N 乘以主频;
  7. 计算利用率:用实际算力除以峰值算力即得利用率。

伪代码:

total_time = 0;
for (int i = 0;i < 5;i++) {
  matrix_randomize(a, M, K);
  matrix_randomize(b, K, N);
  cycle_begin = rdcycle();
  matrix_multiply(a, b, c, M, N, K);
  cycle_end = rdcycle();
  elapsed = cycle_end - cycle_begin;
  if (i >= 2) {
    total_time += elapsed;
  }
}
// 注意可能溢出,可根据实际情况调整计算过程
// 也可改成 64 位整数或单双精度浮点运算
printf("%d OPS\n", M * N * K * 2 * freq * 3 / total_time);
printf("%d%% Utilization\n", 100 * (M * N * K * 2 * freq * 3 / total_time) / (PE_N * PE_N * 2 * freq));

注意事项:

  1. 如果实现了缓存,那么在测试的过程中,A、B、C 矩阵的地址可以保持不变,只是每次评测时随机化 A、B 矩阵的数据,以避免缓存的来回冲刷。
  2. 如果做了一些矩阵数值无关的预处理操作(比如类似 CUDA Graph 的录制),没有进行矩阵数据的搬运,这些操作不计时间。
  3. 计算过程中,如果需要对矩阵的布局进行修改,那么重新布局的时间需要计入,并且用户输入的矩阵 A、B 的原始数据不能更改,意味着需要另外开辟空间来完成这件事情。
  4. 如果对脉动阵列的实现做了修改,使其每周期可以进行多于 2 次的运算,或者采用了脉动阵列以外的设计,则按照具体情况计算峰值算力。
  5. 为在实现加速的同时保证一定的灵活性,不得直接由硬件实现硬编码的完整的矩阵乘法,而是由 CPU 调度 NPU 内各单元的执行,为此需要用多个(至少五个)不同规模的矩阵来进行性能测试,以展示灵活性和在不同规模下的性能。
  6. CPU 除了控制以外,也可以参与到计算当中,提供算力。这部分算力如果用上了,也需要计入峰值算力当中。
  7. 考虑到在高算力下,FPGA 的动态功耗和发热量可能会比较大,而目前在线实验平台缺少相关的压力测试,所以不建议在实验平台上持续运行性能测试太久,尽量控制在秒的量级以内,上述性能测试方法实际上只需要毫秒量级。

举例:在 PE_N 等于 4,主频 50MHz 的情况下,理论峰值算力就是 2*4*4*50000000=1600 MOPS。如果计算 M=N=K=32 的矩阵,要花费 197738 个周期,那么实际算力就是 2*32*32*32/(197738/50000000)=16.57 MOPS,利用率就是 16.57/1600=1.04%,说明优化空间非常巨大。如果利用率优化到接近 100%,在不提升峰值算力的前提下,就实现了近两个数量级的性能提升。

优化方向

NPU 的优化可以按“阵列计算、访存与总线、FPGA、软件”四类来考虑。下面列出一些常见方向,供选题时参考;不需要全部做完,选一个方向做深、做透即可。

  1. 针对脉动阵列的优化:
    1. 扩大脉动阵列,修改脉动阵列的配置,修改片上存储以适配扩大后的脉动阵列所需的带宽,修改软件以支持扩容后的 NPU;
    2. 实现多条 TMUL 指令的连续执行,减少因斜入斜出导致的流水线气泡;
    3. 修改 PE,使其 PE 内部有两套权重寄存器,允许 LDTILE 指令在加载权重的同时,TMUL 用另一套权重寄存器进行计算;
    4. 修改 PE,使其每周期可以计算更多的乘加运算;
  2. 针对访存的优化:
    1. 给 NPU 增加内存搬运单元(DMA)和内存搬运指令,让数据的搬移工作从 CPU 挪到 NPU,让 NPU 自行完成数据搬运;
    2. 改进 NPU 的控制逻辑,允许 NPU 同时进行 LDTILE 指令、TMUL 指令和新增的内存搬运指令;
    3. 在片上存储实现双缓冲(Double Buffer),让脉动阵列在访问片上存储时,还可以进行数据搬运,从而隐藏加载延迟;
    4. 修改内存控制器,允许 CPU 或 NPU 同时访问两片 SRAM,从而提供更高的内存带宽;
    5. 给 NPU 增加缓存以提供更高的带宽,或者给 CPU 和 NPU 增加共享的缓存;
    6. 更换总线协议,把 Wishbone 替换为支持更高带宽和更低延迟的总线协议;
    7. 给 NPU 增加指令队列,减少 CPU 和 NPU 之间的 MMIO 交互开销;
    8. 把 NPU 要执行的指令,放到内存里,由 NPU 通过 DMA 读取,而不是由 CPU 通过 MMIO 写入 NPU,进一步减少 CPU 与 NPU 之间的 MMIO 交互开销;
  3. 针对 FPGA 的优化:
    1. 尽量用满 FPGA 的各项硬件资源;
    2. 提升时钟频率,注意时序是否满足约束,检查 UART 和 SRAM 控制器的时序;
    3. 优化逻辑,在相同的硬件资源前提下,实现更高的算力或利用率;
  4. 针对软件的优化:
    1. 增大单次 TMUL 的 M,让单次 TMUL 实现更多次运算,减少脉动阵列计算过程中的气泡;
    2. 调整分块方式,进一步提升数据复用、减少数据搬运;
    3. 开启编译优化,减少不必要的指令执行开销;
    4. 提升 CPU 性能,减少 CPU 侧开销。

报告建议

性能评测报告至少应包含:

  • 硬件规格:PE_N、各片上存储的容量、主频;
  • 测试参数:矩阵规模、tiling 方式;
  • 性能数据:周期数、实际算力(OPS)、峰值算力(OPS)、利用率,至少 5 组不同规模;
  • 瓶颈分析:计算、片上存储访问、MMIO、总线、CPU 开销各占多少;可以画 Gantt 图或数据流图;
  • 优化前后对比:优化做了什么、峰值是否变化、实际算力和利用率提升了多少、代价是什么;
  • 反思:哪些优化没有达到预期,为什么;如果继续做,下一步会做什么。

关于“峰值算力”

如果你扩大了脉动阵列、改变了每周期乘加次数,峰值算力要按新的结构重新计算,不能继续用 2*PE_N*PE_N*freq。同样,如果让 CPU 参与计算,CPU 贡献的算力也可以计入峰值,但要在报告中说明。

作者:Jiajie Chen