一直对昇腾矩阵乘法的A,B和C矩阵的各类数据格式很好奇,到底这样设置为什么会对性能有较大的提升?

参加资料:

官方参照链接

1 矩阵乘法基础

矩阵乘法是深度学习与高性能计算中的核心运算之一, 矩阵乘(MatMul)的基本计算公式为:

C = A × B + Bias

其中:

  • A(左矩阵):形状为 [M, K]

  • B(右矩阵):形状为 [K, N]

  • C(结果矩阵):形状为 [M, N]

  • Bias(偏置向量):形状为 [N],在推理或全连接层中常用

在 Ascend C 中,该公式通过 Matmul 对象实现,支持半精度(half)和单精度(float)数据类型。

矩阵乘法

2 矩阵乘相关分形格式

为什么需要分形格式?

AI Core 中的 Cube 单元是专为矩阵运算优化的硬件模块,其计算模式并非逐元素操作,而是每次以小数据块(16×16×16)为单位进行并行计算(以half数据类型为例)。为了在一个时钟周期内高效地为计算单元提供数据,内存中的数据必须满足以下条件:

  • 连续访问:计算所需的数据块应尽量连续存储,以最大化内存带宽利用率。

  • 数据复用:合理安排数据布局,使已加载的数据能在多次计算中被重复使用,减少数据搬运开销。

传统的 ND(行优先/列优先)格式虽然适合 CPU 的缓存访问模式,但在面对 Cube 单元的块计算时,数据往往呈现跳跃式分布,导致访存延迟增加、效率降低。为此,分形格式通过数据重组,实现了计算数据在物理内存中的“对齐”。

使用Mmad基础API进行矩阵乘计算时,对矩阵输入输出的数据排布格式有一定的要求,如下图所示,要求A矩阵(位于L0A Buffer)为FRACTAL_ZZ,B矩阵(位于L0B Buffer)为FRACTAL_ZN,C矩阵(位于L0C Buffer)为FRACTAL_NZ。这些格式将矩阵划分成了一些分形(Fractal Matrix),适配Cube计算单元每次读取(16, 16)× (16, 16) 的数据进行计算的硬件特点(以half数据类型为例),从而提高矩阵计算的效率。分形的大小和数据类型有关,也和所在的存储位置有关。

NZ格式

ok,到这里都是官方对概念介绍的内容,以下开始真正的分析,主要这么排列的为什么。

A矩阵(位于L0A Buffer)为FRACTAL_ZZ?

B矩阵(位于L0B Buffer)为FRACTAL_ZN?

C矩阵(位于L0C Buffer)为FRACTAL_NZ?

3 分析

直接举例,A和B都是个4*4的矩阵,如下:

1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16

数据格式当前按照ND格式排列。现在要计算A*B,并且按照2*2的方块设计,每次计算2*2的数据大小。

3.1 ND格式计算

这里有个概念,在计算机中,N维的数据也都是按照1维的数据进行排布存放的,计算机取数据可没有立体的概念。根据【首地址+偏移】方式就能取到任意的数据。

那么在ND下,A和B矩阵在计算机的存储方式就都是

1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,16

假设A1和B1每次都2*2 =4个数据进行计算。对A和B矩阵的小块矩阵,分别命名为A1,B1。

A1:

1 2
5 6

B1:

1 2
5 6

那么A作为左矩阵,应该是先算1, 2,然后接着算5, 6。B作为右矩阵,应该是先算1, 5,然后接着算2, 6。

接下来开始取数据。

上面说过,A矩阵的排布方式:1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,16。会发现,无论是1, 2,还是5, 6都是存储连续的。

B矩阵的排布方式同样也是:1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,16。会发现,无论是1, 5,还是2, 6都是存储不连续的。

不连续会有什么问题?

性能问题。因为计算发生时都是有cache的,连续数据存储和读取都不需要加偏移量,能直接从cache line中读,命中率高,自然性能好。

所以,在当前这种情况下,矩阵B数据的读取性能是差的。

3.2 CUBE矩阵计算格式

那能不能让B矩阵的读取连续呢?当然可以,B矩阵按照列优先存储就可以。

要求A矩阵(位于L0A Buffer)为FRACTAL_ZZ,B矩阵(位于L0B Buffer)为FRACTAL_ZN,C矩阵(位于L0C Buffer)为FRACTAL_NZ。

首先看A矩阵,FRACTAL_ZZ/ZZ

FRACTAL_ZZ格式,简称ZZ(大Z小Z)格式,是对一个Tensor最低两维(一个Tensor的所有维度,右侧为低维,左侧为高维)进行填充(pad)、拆分(reshape)和转置(transpose)操作后得到的格式。具体转换过程如下:

(M, K)大小的矩阵被分为M1 * K1个分形,按照row major排布,形状如Z字形;每个分形内部有M0 * K0个元素,按照row major排布,形状如Z字形,所以这种数据格式称为ZZ格式。其中,(M0, K0)表示一个分形的大小,分形Shape为16 x (32B / sizeof(Datatype)),大小为512字节。

FRACTAL_ZZ

取块按照行(大Z),块内部也按照行(小Z)。

还是拿上面的矩阵作为示例,那么读取方式就是:内部按照行,外部1->2->3->4

所以,矩阵A存储顺序就会变为

1,2,5,6,3,4,7,8,9,10,13,14,11,12,15,16

接着看B矩阵,FRACTAL_ZN/ZN

FRACTAL_ZN格式,简称ZN(大Z小N)格式,是对一个Tensor最低两维(一个Tensor的所有维度,右侧为低维,左侧为高维)进行填充(pad)、拆分(reshape)和转置(transpose)操作后得到的格式。具体转换过程如下:

(K, N)大小的矩阵被分为K1 * N1个分形,按照row major排布,形状如Z字形;每个分形内部有K0 * N0个元素,按照column major排布,形状如N字形,所以这种数据格式称为ZN格式。其中,(K0, N0)表示一个分形的大小,分形shape为 (32B / sizeof(Datatype)) x 16,大小为512字节。

FRACTAL_ZN

取块按照行(大Z),块内部按照列(小N)。

还是拿上面的矩阵作为示例,那么读取方式就是:内部按照列,外部1->2->3->4

1,5,2,6,3,7,4,8,9,13,10,14,11,15,12,16

同样,假设A1和B1每次都2*2 =4个数据进行计算。对A和B矩阵的小块矩阵,分别命名为A1,B1。

A1:

1 2
5 6

B1:

1 2
5 6

那么A作为左矩阵,应该是先算1, 2,然后接着算5, 6。B作为右矩阵,应该是先算1, 5,然后接着算2, 6。

接下来开始取数据。

A矩阵的排布方式是:1,2,5,6,3,4,7,8,9,10,13,14,11,12,15,16。会发现,无论是1, 2,还是5, 6都是存储连续的。

B矩阵的排布方式是:1,5,2,6,3,7,4,8,9,13,10,14,11,15,12,16。会发现,无论是1, 5,还是2, 6都是存储连续的。

连续的问题解决了,但是那只是小块,那整体的大块是怎么计算的呢?

整体逻辑按照行优先计算。

第1步:A按照大Z取块1,2,5,6。先取A:1,2,然后取B(小N):1,5;保持A不变,取B:2,6

然后A动 (小Z)行,取A:5,6,然后取B(小N):1,5;保持A不变,取B:2,6

2*2的小块完成计算,得到了输出的4个数(11,14,15,46)。

第2步:再取2*2的小块计算,为提高缓存命中率,保持A矩阵小块不变,依旧是:1,2,5,6。变化B矩阵,B矩阵按行取块(大Z),也就是3,7,4,8。

先看小块,先取A:1,2,然后取B(小N):3,7;保持A不变,取B:4,8

然后A动(小Z) ,取A:5,6,然后取B(小N):3,7;保持A不变,取B:4,8

2*2的小块完成计算,得到了输出的7个数(17,20,57,68)

到这里,B矩阵的一行8个元素(小Z块组成)完全取完了。

第3步:B矩阵取完了,现在要动A矩阵小块,A矩阵按照行取块(大Z)

3,4,7,8,B跳到下一行开始读取,要取的9,13,10,14(大Z)。

先取A:3,4,然后取B(小N):9,13;保持A不变,取B:10,14

然后A动(小Z) ,取A:7,8,然后取B(小N):9,13;保持A不变,取B:10,14

2*2的小块完成计算,得到了输出的4个数(79,86,167,182),与第1步中的4个数(11,14,15,46)相加,得到最终结果左上角的2*2的最终块大小(90,100,202,228)。

第4步:再取2*2的小块计算,为提高缓存命中率,保持A矩阵小块不变,依旧是:3,4,7,8。变化B矩阵,B矩阵按行取块(大Z),也就是11,15,12,16。

先取A:3,4,然后取B(小N):11,15;保持A不变,取B:12,16

然后A动(小Z) ,取A:7,8,然后取B(小N):11,15;保持A不变,取B:12,16

2*2的小块完成计算,得到了输出的4个数(93,100,197,212),与第2步中的4个数17,20,57,68)相加,得到最终结果又上角的2*2的最终块大小(110,120,254,280).

到这里,A矩阵的一行8个元素(小Z块组成)完全取完了。

A矩阵一行取完了,现在要动A矩阵小块,A矩阵按照行取块(大Z)9,10,13,14。此时B矩阵回到初始值状态,从1,5,2,6开始,一直到11,15,12,16。

整个过程重复上述第1步->第4步过程。

依此类推,完成所有2*2小方块的计算。

这里不知道大家有没发现一个问题,上述步骤中,最终的结果是:
步骤1+步骤3(左上角),步骤2+步骤4(右上角)

按照当前的这种遍历方式,明显中间需要多存储一步。比如说,计算左上角的时候,就要多缓存一份步骤2的结果,以便在计算右上角的时候需要使用。明显需要变?那就得改变遍历方式,固定B,然后动A,这样效果就变过来了,也就成就了C矩阵的存储方式。

FRACTAL_NZ/NZ

FRACTAL_NZ格式,简称NZ格式,是对一个Tensor最低两维(一个Tensor的所有维度,右侧为低维,左侧为高维)进行填充(pad)、拆分(reshape)和转置(transpose)操作后得到的格式。具体的转换过程如下:

(M,N)大小的矩阵被分为M1 * N1个分形,按照column major(列优先)排布,形状如N字形;每个分形内部有M0 * N0个元素,按照row major(行优先)排布,形状如Z字形,所以这种数据格式称为NZ格式。其中,(M0, N0)表示一个分形的大小。

FRACTAL_NZ

是按照列的方式优先存储。

有兴趣的,可以按照上述方式推理下。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐