[英]Performance degradation of matrix multiplication of single vs double precision arrays on multi-core machine
更新
不幸的是,由于我的疏忽,我将较旧版本的MKL(11.1)与numpy链接在一起。 较新版本的MKL(11.3.1)在C中以及从python调用时均具有相同的性能。
令人困惑的是,即使将编译后的共享库与较新的MKL显式链接,并通过LD_ *变量指向它们,然后在python中执行import numpy,还是使python调用了旧的MKL库。 只有在python lib文件夹中将所有libmkl _ *。so替换为较新的MKL,我才能匹配python和C调用中的性能。
背景/图书馆信息。
矩阵乘法是通过numpy.dot函数通过sgemm(单精度)和dgemm(双精度)Intel的MKL库调用完成的。 可以使用例如oprof来验证库函数的实际调用。
在这里使用2x18核心CPU E5-2699 v3,因此共有36个物理核心。 KMP_AFFINITY =散点图。 在linux上运行。
TL; DR
1)为什么numpy.dot即使调用了相同的MKL库函数,也比C编译代码慢了两倍?
2)为什么通过numpy.dot会导致性能随着内核数量的增加而降低,而在C代码中却没有看到相同的效果(调用相同的库函数)。
问题
我观察到,与从纯C内部调用相同的MKL cblas_sgemm / dgemm函数相比,在numpy.dot中对单精度/双精度浮点数进行矩阵乘法以及直接从已编译的C 共享库中调用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.8 s,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代码时,可获得最佳性能。
也许有任何精美的工具或建议可用于进一步分析/检查/了解这种情况? 任何阅读材料也非常感谢。
更新按照@Hristo Iliev的建议,运行numactl --interleave = all ./ipython不会更改计时(在噪声范围内),但是可以改善纯C二进制运行时。
我怀疑这是由于不幸的线程调度造成的。 我能够重现与您相似的效果。 Python的运行时间约为2.2 s,而C版本的显示时间为1.4-2.2 s。
应用: KMP_AFFINITY=scatter,granularity=thread
这可以确保28个线程始终在同一处理器线程上运行。
将C和python的两个运行时都降低到更稳定的〜1.24 s和〜1.26 s。
这是在28核双插槽Xeon E5-2680 v3系统上。
有趣的是,在非常相似的24核双插槽Haswell系统上,即使没有线程亲缘关系/固定,python和C的性能也几乎相同。
python为什么会影响调度? 我认为周围还有更多的运行时环境。 最重要的是,没有固定的性能结果将是不确定的。
还需要考虑的是,英特尔OpenMP运行时产生了一个额外的管理线程,该线程可能会使调度程序混乱。 固定还有更多选择,例如KMP_AFFINITY=compact
但由于某些原因,这完全弄乱了我的系统。 您可以在变量中添加,verbose
以查看运行时如何固定线程。
likwid-pin是有用的替代方法,可提供更方便的控制。
通常,单精度至少应与双精度一样快。 双精度会变慢,原因是:
我认为,一旦您摆脱了性能异常,这就会反映在您的数字中。
在扩展MKL / * gemm的线程数时,请考虑
我认为没有一种非常简单的方法可以衡量不良的计划如何影响您的应用程序。 您可以使用perf trace -e sched:sched_switch
公开它,并且可以使用一些软件来对其进行可视化,但这将带来很高的学习难度。 然后再次-对于并行性能分析,无论如何,您都应该固定线程。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.