单核与双精度阵列矩阵乘法在多核机器

时间:2016-01-27 15:35:58

标签: python c numpy openmp intel-mkl

更新

不幸的是,由于我的疏忽,我有一个旧版本的MKL(11.1)链接到numpy。较新版本的MKL(11.3.1)在C中和从python调用时具有相同的性能。

什么是模糊的东西,即使将已编译的共享库与新的MKL明确地链接,并将LD_ *变量指向它们,然后在python中执行import numpy,也是以某种方式使python调用旧的MKL库。只有在python lib文件夹中替换所有libmkl _ *。所以使用更新的MKL我能够匹配python和C调用中的性能。

背景/图书馆信息。

通过numpy.dot函数,通过sgemm(单精度)和dgemm(双精度)Intel的MKL库调用完成矩阵乘法。可以用例如图1来验证库函数的实际调用。 oprof。

在这里使用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,与调用相比,性能明显更差相同的MKL cblas_sgemm / dgemm函数来自纯C代码。

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核:20核,6核:10核,12核:5核,18核:4.3s,24核:3核,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代码时实现。

也许有任何花哨的工具或建议可以进一步分析/检查/了解这种情况?任何阅读材料也非常受欢迎。

更新 按照@Hristo Iliev的建议,运行numactl --interleave = all ./ipython并没有改变时间(在噪声中),但改进了纯C二进制运行时。

1 个答案:

答案 0 :(得分:7)

我怀疑这是由于不幸的线程调度。我能够重现与你类似的效果。 Python运行时间约为2.2秒,而C版本则显示1.4-2.2秒的巨大差异。

应用: KMP_AFFINITY=scatter,granularity=thread 这可确保28个线程始终在同一处理器线程上运行。

对于C,将两个运行时间减少到更稳定的~1.24秒,对于python,运行时间减少到1.26秒。

这是在28核双插槽Xeon E5-2680 v3系统上。

有趣的是,在一个非常相似的24核双插槽Haswell系统上,即使没有线程亲和/固定,python和C也几乎完全相同。

为什么python会影响调度?我假设它周围有更多的运行时环境。底线是,如果没有固定你的表现,结果将是不确定的。

此外,您还需要考虑,英特尔OpenMP运行时会产生一个额外的管理线程,可能会使调度程序混乱。固定有更多选择,例如KMP_AFFINITY=compact - 但由于某种原因,我的系统完全搞砸了。您可以将,verbose添加到变量中,以查看运行时如何固定线程。

likwid-pin是提供更方便控制的有用替代方案。

通常,单精度应至少与双精度一样快。双精度可能会更慢,因为:

  • 双倍精度需要更多内存/缓存带宽。
  • 您可以为单精度构建具有更高吞吐量的ALU,但这通常不适用于CPU,而是适用于GPU。

我认为一旦你摆脱了性能异常,这将反映在你的数字中。

当你扩大MKL / * gemm的线程数时,请考虑

  • 内存/共享缓存带宽可能成为瓶颈,限制了可扩展性
  • Turbo模式在提高利用率时会有效降低核心频率。这甚至在您以标称频率运行时也适用:在Haswell-EP处理器上,AVX指令会施加较低的“AVX基频” - 但允许处理器超过使用较少核心/热余量可用时的处理器,一般情况下甚至更多的时间。如果您想获得完全中性的结果,则必须使用AVX基频,即1.9 GHz。它记录在here,并在one picture中进行了解释。

我认为没有一种非常简单的方法来衡量应用程序如何受到错误调度的影响。您可以使用perf trace -e sched:sched_switch公开此内容,并some software可以看到这一点,但这会带来很高的学习曲线。然后再次 - 对于并行性能分析,您应该将线程固定。