多核机器上单精度数组与双精度数组的矩阵乘法的性能下降

2023-12-28

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 二进制运行时。


我怀疑这是由于不幸的线程调度造成的。我能够重现与您类似的效果。 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会影响调度?好吧,我假设它周围有更多的运行时环境。最重要的是,如果不固定,您的性能结果将是不确定的。

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

利克维德平 https://github.com/RRZE-HPC/likwid/wiki/Likwid-Pin是一种有用的替代方案,提供更方便的控制。

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

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

我认为一旦你消除了性能异常,这就会反映在你的数字中。

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

  • 内存/共享缓存带宽可能成为瓶颈,限制可扩展性
  • Turbo模式在提高利用率的同时会有效降低核心频率。即使您以标称频率运行,这也适用:在 Haswell-EP 处理器上,AVX 指令将施加较低的“AVX 基本频率” - 但当使用较少核心/可用热空间时,处理器允许超过该频率,并且通常甚至是这样更多的时间较短。如果您想要完全中性的结果,则必须使用 AVX 基本频率,即 1.9 GHz。有记录here http://www.intel.com/content/dam/www/public/us/en/documents/specification-updates/xeon-e5-v3-spec-update.pdf,并在中解释一张照片 http://images.anandtech.com/doci/8423/Hep_AVX_turbo.png.

我认为没有一种真正简单的方法来衡量您的应用程序如何受到不良调度的影响。你可以暴露这个perf trace -e sched:sched_switch并且有一些软件 http://tu-dresden.de/zih/perf/可视化这一点,但这将伴随着很高的学习曲线。再说一遍 - 对于并行性能分析,无论如何您都应该固定线程。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

多核机器上单精度数组与双精度数组的矩阵乘法的性能下降 的相关文章

  • 用 C# 制作 Vista 风格的应用程序

    我正在运行 Windows Vista 并且希望外观看起来像常规 Vista 程序 有没有关于如何构建 Vista 风格应用程序的真正好的教程 文章 我还想学习如何使用本机代码并将其转换为 C 如this http bartdesmet n
  • Microsoft.Graph - 如何从具有不同用户名的共享邮箱发送?

    我目前正在将使用 SMTP 的服务代码移植到 Office 365 通过 SMTP 我可以使用 发件人 字段在来自共享收件箱的邮件上设置不同的用户名 同时保留共享电子邮箱地址 这似乎无法通过 Office 365 运行 其工艺流程为 客户填
  • 线程安全的 C++ 堆栈

    我是 C 新手 正在编写一个多线程应用程序 不同的编写者将对象推入堆栈 读者将它们从堆栈中拉出 或至少将指针推入对象 C 中是否有任何内置结构可以在不添加锁定代码等的情况下处理此问题 如果没有 那么 Boost 库呢 EDIT 你好 感谢您
  • Python脚本从字母和两个字母组合生成单词

    我正在编写一个简短的脚本 它允许我使用我设置的参数生成所有可能的字母组合 例如 b a 参数 单词 5 个字母 第三 第五个字母 b a 第一个字母 ph sd nn mm 或 gh 第二 第四个字母 任意元音 aeiouy 和 rc 换句
  • c# 如何生成锦标赛括号 HTML 表

    所以我已经被这个问题困扰了三个星期 但我一生都无法弄清楚 我想做的是使用表格获得这种输出 演示 http www esl world net masters season6 hanover sc2 playoffs rankings htt
  • 数据损坏 C++ 和 Python 之间的管道

    我正在编写一些代码 从 Python 获取二进制数据 将其通过管道传输到 C 对数据进行一些处理 在本例中计算互信息度量 然后将结果通过管道传输回 Python 在测试时 我发现如果我发送的数据是一组尺寸小于 1500 X 1500 的 2
  • 将 Django 中的所有视图限制为经过身份验证的用户

    我是 Django 新手 我正在开发一个项目 该项目有一个登录页面作为其索引和一个注册页面 其余页面都必须仅限于登录用户 如果未经身份验证的用户尝试访问这些页面 则必须将他 她重定向到登录页面 我看到 login required装饰器会将
  • 更改其他页面的主窗口内容

    在 WPF 应用程序的主窗口中 我有一个 Badged 元素 来自材料设计 这是我的代码
  • 为什么WCF中不允许方法重载?

    假设这是一个ServiceContract ServiceContract public interface MyService OperationContract int Sum int x int y OperationContract
  • 从给定的项目列表创建子列表

    我首先要说的是以下问题不是为了家庭作业目的即使因为我几个月前就完成了软件工程师的工作 无论如何 今天我正在工作 一位朋友向我询问了这个奇怪的排序问题 我有一个包含 1000 行的列表 每行代表一个数字 我想创建 10 个子列表 每个子列表都
  • 是什么原因导致 Linq 错误:此方法无法转换为存储表达式?

    我有一堆具有相同 select 语句的 Linq to Entity 方法 所以我想我会很聪明 并将其分离到它自己的方法中以减少冗余 但是当我尝试运行代码时 我得到了以下内容错误 该方法不能转化为 商店表达式 这是我创建的方法 public
  • 如何在 VS Code 中为 CMake 项目设置 C/C++ IntelliSense?

    我正在尝试使用 libTooling 编写一个工具 我对其进行了设置 以便它可以使用 LLVM 文档中的示例进行编译 然而 C C IntelliSense 似乎不适用于 CMake 项目 我的工具位于
  • asp.net c# 防止在从服务器端代码更改索引时触发 selectedindexchanged 事件

    我在同一个 aspx 页面上有两个下拉列表控件
  • 理解 C++11 中的 std::atomic::compare_exchange_weak()

    bool compare exchange weak T expected T val compare exchange weak 是 C 11 中提供的比较交换原语之一 它是weak即使对象的值等于 它也会返回 falseexpected
  • ProcessPoolExecutor 传递多个参数

    ESPN播放器免费 class ESPNPlayerFree def init self player id match id match id team 团队名单1 277906 cA2i150s81HI3qbq1fzi za1Oq5CG
  • Chrome 驱动程序和 Chromium 二进制文件无法在 aws lambda 上运行

    我陷入了一个问题 我需要在 AWS lambda 上做一些抓取工作 所以我按照下面提到的博客及其代码库作为起点 这非常有帮助 并且在运行时环境 Python 3 6 的 AWS lambda 上对我来说工作得很好 https manivan
  • 微软语音识别速度

    我正在使用微软的语音识别器开发一个小型练习应用程序 对于我正在做的事情来说 我似乎无法让它足够快地识别单个单词 我希望能够正常说话 系统将从我所说的内容中抓取 关键字 并生成一个字符串 目前我正在使用 5 个单词的自定义语法 红 蓝 黄 绿
  • 如何强制执行特定的 UserControl 设计

    我正在编写一个基本用户控件 它将由一堆其他用户控件继承 我需要对所有这些后代控件强制执行某种设计 例如 顶部必须有几个按钮以及一个或两个标签 后代用户控件区域的其余部分可以自由放置任何内容 最初 我认为我可以将一个面板放到 Base Use
  • 正在获取“未终止 [] 设置”。 C# 中的错误

    我正在 C 中使用以下正则表达式 Regex find new Regex url
  • Tkinter 将鼠标点击绑定到框架

    我一定错过了一些明显的东西 我的 Tkinter 程序中有两个框架 每个框架在网格布局中都有一堆标签 我想将鼠标点击绑定到其中一个而不是另一个 我目前使用 root bind

随机推荐