UPDATE
不幸的是,由于我的疏忽,我有一个旧版本的 MKL (11.1) 与 numpy 链接。新版本的 MKL (11.3.1) 在 C 中和从 python 调用时提供相同的性能。
令人困惑的是,即使将编译后的共享库与较新的 MKL 显式链接,并通过 LD_* 变量指向它们,然后在 python 中执行 import numpy,也会以某种方式使 python 调用旧的 MKL 库。只有通过用较新的 MKL 替换 python lib 文件夹中的所有 libmkl_*.so,我才能匹配 python 和 C 调用的性能。
背景/图书馆信息。
矩阵乘法是通过 sgemm(单精度)和 dgemm(双精度)Intel 的 MKL 库调用(通过 numpy.dot 函数)完成的。库函数的实际调用可以通过以下方式进行验证:奥教授。
这里使用 2x18 核心 CPU E5-2699 v3,因此总共有 36 个物理核心。
KMP_AFFINITY=分散。在Linux上运行。
TL;DR
1) 为什么 numpy.dot 尽管调用相同的 MKL 库函数,但与 C 编译代码相比最多慢两倍?
2) 为什么通过 numpy.dot 性能会随着内核数量的增加而降低,而在 C 代码中却没有观察到相同的效果(调用相同的库函数)。
问题
我观察到在 numpy.dot 中进行单/双精度浮点数的矩阵乘法,以及直接从编译的 C 调用 cblas_sgemm/dgemm共享库与从纯 C 代码内部调用相同的 MKL cblas_sgemm/dgemm 函数相比,性能明显较差。
import numpy as np
import mkl
n = 10000
A = np.random.randn(n,n).astype('float32')
B = np.random.randn(n,n).astype('float32')
C = np.zeros((n,n)).astype('float32')
mkl.set_num_threads(3); %time np.dot(A, B, out=C)
11.5 seconds
mkl.set_num_threads(6); %time np.dot(A, B, out=C)
6 seconds
mkl.set_num_threads(12); %time np.dot(A, B, out=C)
3 seconds
mkl.set_num_threads(18); %time np.dot(A, B, out=C)
2.4 seconds
mkl.set_num_threads(24); %time np.dot(A, B, out=C)
3.6 seconds
mkl.set_num_threads(30); %time np.dot(A, B, out=C)
5 seconds
mkl.set_num_threads(36); %time np.dot(A, B, out=C)
5.5 seconds
与上面完全相同,但使用双精度 A、B 和 C,您将得到:
3核:20s,6核:10s,12核:5s,18核:4.3s,24核:3s,30核:2.8s,36核:2.8s。
单精度浮点速度的提高似乎与缓存未命中有关。
对于 28 核运行,以下是 perf 的输出。
对于单精度:
perf stat -e task-clock,cycles,instructions,cache-references,cache-misses ./ptestf.py
631,301,854 cache-misses # 31.478 % of all cache refs
和双精度:
93,087,703 cache-misses # 5.164 % of all cache refs
C 共享库,编译为
/opt/intel/bin/icc -o comp_sgemm_mkl.so -openmp -mkl sgem_lib.c -lm -lirc -O3 -fPIC -shared -std=c99 -vec-report1 -xhost -I/opt/intel/composer/mkl/include
#include <stdio.h>
#include <stdlib.h>
#include "mkl.h"
void comp_sgemm_mkl(int m, int n, int k, float *A, float *B, float *C);
void comp_sgemm_mkl(int m, int n, int k, float *A, float *B, float *C)
{
int i, j;
float alpha, beta;
alpha = 1.0; beta = 0.0;
cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
m, n, k, alpha, A, k, B, n, beta, C, n);
}
Python包装函数,调用上面编译的库:
def comp_sgemm_mkl(A, B, out=None):
lib = CDLL(omplib)
lib.cblas_sgemm_mkl.argtypes = [c_int, c_int, c_int,
np.ctypeslib.ndpointer(dtype=np.float32, ndim=2),
np.ctypeslib.ndpointer(dtype=np.float32, ndim=2),
np.ctypeslib.ndpointer(dtype=np.float32, ndim=2)]
lib.comp_sgemm_mkl.restype = c_void_p
m = A.shape[0]
n = B.shape[0]
k = B.shape[1]
if np.isfortran(A):
raise ValueError('Fortran array')
if m != n:
raise ValueError('Wrong matrix dimensions')
if out is None:
out = np.empty((m,k), np.float32)
lib.comp_sgemm_mkl(m, n, k, A, B, out)
然而,来自 C 编译二进制文件的显式调用(调用 MKL 的 cblas_sgemm / cblas_dgemm)以及通过 C 中的 malloc 分配的数组,与 python 代码(即 numpy.dot 调用)相比,性能几乎提高了 2 倍。此外,没有观察到随着内核数量的增加而导致性能下降的影响。单精度矩阵乘法的最佳性能为 900 ms通过 mkl_set_num_cores 使用全部 36 个物理核心并使用 numactl --interleave=all 运行 C 代码时实现。
也许有任何奇特的工具或建议可以进一步分析/检查/理解这种情况?任何阅读材料也非常受欢迎。
UPDATE按照 @Hristo Iliev 的建议,运行 numactl --interleave=all ./ipython 并没有改变计时(在噪音范围内),但改善了纯 C 二进制运行时。