1 面向 GEMM 引入新封装的 API
矩阵-矩阵乘法 (GEMM) 是众多科学、工程和机器学习应用中的重要操作。优化这一操作的需求一直存在,英特尔® 数学核心函数库(英特尔® MKL)提供了并行的高性能 GEMM 实施。为了提供最佳性能,英特尔 MKL 提供的 GEMM 实施通常将原始输入矩阵转换为最适合目标平台的内部数据格式。数据转换(也被称为封装)会产生高昂的费用,尤其在处理具有 1 个或多个小尺寸的输入矩阵时。
英特尔 MKL 2017 引入了 [S,D]GEMM 封装应用编程接口 (API),该接口支持用户将矩阵明确转换为内部封装格式,以及将封装矩阵传输至多个 GEMM 调用。如果在 GEMM 调用之间重复使用输入矩阵(A 或 B),借助这种方法能在多个 GEMM 调用内分摊封装成本。
2 示例
以下 3 个 GEMM 调用使用了相同的 A 矩阵,但是每个调用使用了不同的 B/C 矩阵:
float *A, *B1, *B2, *B3, *C1, *C2, *C3, alpha, beta;
MKL_INT m, n, k, lda, ldb, ldc;
// initialize the pointers and matrix dimensions (skipped for brevity)
sgemm(“T”, “N”, &m, &n, &k, &alpha, A, &lda, B1, &ldb, &beta, C1, &ldc);
sgemm(“T”, “N”, &m, &n, &k, &alpha, A, &lda, B2, &ldb, &beta, C2, &ldc);
sgemm(“T”, “N”, &m, &n, &k, &alpha, A, &lda, B3, &ldb, &beta, C3, &ldc);
在每个 sgemm 调用中,A 矩阵被转换为内部封装数据格式。如果 n 较小(B/C 的列数),会导致 3 次封装矩阵的相对成本较高。通过单次封装 A 矩阵,并在 3 个连续的 GEMM 调用内使用经过封装的矩阵,可以最大限度地降低成本,其代码示例如下所示:
// allocate memory for packed data format
float *Ap;
Ap = sgemm_alloc(“A”, &m, &n, &k);
// transform A into packed format
sgemm_pack(“A”, “T”, &m, &n, &k, &alpha, A, &lda, Ap);
// SGEMM computations are performed using the packed A matrix:Ap
sgemm_compute(“P”, “N”, &m, &n, &k, Ap, &lda, B1, &ldb, &beta, C1, &ldc);
sgemm_compute(“P”, “N”, &m, &n, &k, Ap, &lda, B2, &ldb, &beta, C2, &ldc);
sgemm_compute(“P”, “N”, &m, &n, &k, Ap, &lda, B3, &ldb, &beta, C3, &ldc);
// release the memory for Ap
sgemm_free(Ap);
为了支持面向 GEMM 的封装 API,上述代码示例使用了 4 个新函数:sgemm_alloc、sgemm_pack、sgemm_compute 和 sgemm_free。首先,利用 sgemm_alloc 分配封装格式所需的内存,该函数接收一个确定封装矩阵(在本示例中为 A)的字符参数和三个表示矩阵尺寸的整数参数。然后,sgemm_pack 将原始 A 矩阵转换为封装格式 Ap 并执行 alpha 缩放。未更改原始 A 矩阵。三个 sgemm 调用被替换为三个 sgemm_compute 调用,后者处理封装矩阵并假设 alpha=1.0。输入 sgemm_compute 的前两个字符参数表示 A 矩阵为封装格式 (“P”),B 矩阵为非转置 column major 格式 (“N”)。最终,通过调用 sgemm_free 释放了分配给 Ap 的内存。
从本示例可以看出,GEMM 封装 API 消除了在 3 个矩阵-矩阵乘法操作中两次封装矩阵 A 的成本。如果在多个 GEMM 调用之间重复使用 A 和/或 B 输入矩阵,封装 API 可以消除该输入矩阵的数据转换成本。
3 性能
下表显示了利用封装 API 在英特尔® 至强融核™ 处理器 7250 上取得的性能提升。假设如果使用相同的 A 矩阵的 SGEMM 调用数量很多,可以完全分摊封装成本。本文还提供了普通 SGEMM 调用的性能,以进行对比。
4 实施要点
建议利用相同数量的线程调用 gemm_pack 和 gemm_compute,以达到最佳性能。需要指出的是,如果使用相同的 A 或 B 矩阵的 GEMM 调用数量较少,封装 API 可能提供微乎其微的性能优势。
gemm_alloc 例程分配的内存与原始输入矩阵的规模相当。这意味着对于大型输入矩阵,应用对内存的要求将显著增大。
在英特尔 MKL 2017 中,只面向 SGEMM 和 DGEMM 实施 GEMM 封装 API。它们适用于所有的英特尔架构,但是只面向 64 位英特尔® AVX2 及更高版本进行了优化。
5 结论
[S,D]对于使用相同输入矩阵的多个 GEMM 调用,利用 GEMM 封装 API 能够最大限度地降低数据封装成本。如性能图表所示,如果在多个 GEMM 调用中矩阵得到充分地重复使用,调用它们能显著提升性能。可以在英特尔 MKL 2017 中获取这些封装 API,支持 FORTRAN 77 和 CBLAS 两种接口。如欲获取更多文档,请查看英特尔 MKL 开发人员参考。