python - 多核机器上单精度和 double 组矩阵乘法的性能下降

标签 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_*.so 替换为更新的 MKL,我才能在 python 和 C 调用中匹配性能。

背景/图书馆信息。

矩阵乘法是通过 sgemm(单精度)和 dgemm( double )英特尔的 MKL 库调用,通过 numpy.dot 函数完成的。库函数的实际调用可以通过例如验证。 oprof。

这里使用 2x18 核心 CPU E5-2699 v3,因此总共有 36 个物理核心。 KMP_AFFINITY=分散。在 linux 上运行。

TL;DR

1) 为什么 numpy.dot,即使它调用相同的 MKL 库函数,最多也比 C 编译代码慢两倍?

2) 为什么通过 numpy.dot 会随着内核数量的增加而降低性能,而在 C 代码中没有观察到相同的效果(调用相同的库函数)。

问题

我观察到,在 numpy.dot 中对单精度/ double float 进行矩阵乘法,以及直接从编译的 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

执行与上述完全相同的操作,但使用 double 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

double :

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)

但是,与 Python 代码(即 numpy.dot 调用)相比,从 C 编译的二进制调用 MKL 的 cblas_sgemm/cblas_dgemm 以及通过 malloc 分配的数组的显式调用提供了几乎 2 倍的性能。此外,没有观察到随着内核数量的增加性能下降的影响。 单精度矩阵乘法的最佳性能为 900 毫秒,这是通过 mkl_set_num_cores 使用所有 36 个物理内核并使用 numactl --interleave=all 运行 C 代码时实现的。

也许有任何花哨的工具或建议来进一步分析/检查/理解这种情况?任何阅读 Material 都非常感谢。

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

最佳答案

我怀疑这是由于不幸的线程调度造成的。我能够重现与您类似的效果。 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是一个有用的替代方案,提供更方便的控制。

一般来说,单精度至少应该和 double 一样快。 double 可能会更慢,因为:

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

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

当您为 MKL/*gemm 增加线程数时,请考虑

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

我认为没有一种非常简单的方法可以衡量您的应用程序如何受到不良调度的影响。您可以使用 perf trace -e sched:sched_switch 公开它,并且有 some software可视化这一点,但这会带来很高的学习曲线。再说一次 - 对于并行性能分析,无论如何你都应该固定线程。

关于python - 多核机器上单精度和 double 组矩阵乘法的性能下降,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35041597/

相关文章:

c - 强制结构内部 union 对齐

python - 如何对 numpy 数组位置值进行分组?

python - 使用二变量样条与 scipy.ndimage.geometric_transform 来配准图像

python - 在 Mac OS 上设置 tkinter 图标

python - 这个全局类变量如何兼容 pep8 并且仍然有效?

c - == 运算符跳过条件 if 语句,即使条件为 true

c++ - 从 C++ 调用 WDF 驱动程序

python-3.x - 无法访问内置网络摄像头 Python

python - 在字典键中查找字符串,其中字符串不完全是字典中的键

python - PySpark jdbc谓词错误: Py4JError: An error occurred while calling o108. jdbc