下面的 C 程序 (dgesv_ex.c)
#include <stdlib.h>
#include <stdio.h>
/* DGESV prototype */
extern void dgesv( int* n, int* nrhs, double* a, int* lda, int* ipiv,
double* b, int* ldb, int* info );
/* Main program */
int main() {
/* Locals */
int n = 10000, info;
/* Local arrays */
/* Initialization */
double *a = malloc(n*n*sizeof(double));
double *b = malloc(n*n*sizeof(double));
int *ipiv = malloc(n*sizeof(int));
for (int i = 0; i < n*n; i++ )
{
a[i] = ((double) rand()) / ((double) RAND_MAX) - 0.5;
}
for(int i=0;i<n*n;i++)
{
b[i] = ((double) rand()) / ((double) RAND_MAX) - 0.5;
}
/* Solve the equations A*X = B */
dgesv( &n, &n, a, &n, ipiv, b, &n, &info );
free(a);
free(b);
free(ipiv);
exit( 0 );
} /* End of DGESV Example */
使用命令在 Mac mini M1 上编译
clang -o dgesv_ex dgesv_ex.c -framework accelerate
仅使用处理器的一个核心(活动监视器也显示)
me@macmini-M1 ~ % time ./dgesv_ex
./dgesv_ex 35,54s user 0,27s system 100% cpu 35,758 total
我检查了二进制文件的类型是否正确:
me@macmini-M1 ~ % lipo -info dgesv
Non-fat file: dgesv is architecture: arm64
作为比较,在我的英特尔 MacBook Pro 上,我得到以下输出:
me@macbook-intel ˜ % time ./dgesv_ex
./dgesv_ex 142.69s user 0,51s system 718% cpu 19.925 total
这是一个已知问题吗?也许是编译标志或其他?
答案 0 :(得分:6)
Accelerate 使用 M1 的 AMX 协处理器来执行其矩阵运算,它不使用处理器中的典型路径。因此,计算 CPU 使用率没有多大意义;在我看来,当 CPU 内核向 AMX 协处理器提交指令时,它被视为在等待协处理器完成其工作时保持 100% 的利用率。
通过并行运行 dgesv
基准测试的多个实例,并观察运行时间增加两倍,我们可以看到这一点的证据,但 CPU 监视器仅显示使用 100% 一个内核的两个进程:
clang -o dgesv_accelerate dgesv_ex.c -framework Accelerate
$ time ./dgesv_accelerate
real 0m36.563s
user 0m36.357s
sys 0m0.251s
$ ./dgesv_accelerate & ./dgesv_accelerate & time wait
[1] 6333
[2] 6334
[1]- Done ./dgesv_accelerate
[2]+ Done ./dgesv_accelerate
real 0m59.435s
user 1m57.821s
sys 0m0.638s
这意味着每个dgesv_accelerate
进程都在消耗一个共享资源;一个我们不太了解的。我很好奇这些 dgesv_accelerate
进程在等待 AMX 协处理器完成其任务时是否真的在消耗计算资源,因此我将您示例的另一个版本与 OpenBLAS 联系起来,这就是我们用作 Julia language 中的默认 BLAS 后端。我正在使用托管在 this gist 中的代码,它有一个方便的 Makefile 用于下载 OpenBLAS(及其附带的编译器支持库,例如 libgfortran
和 libgcc
)并编译所有内容并运行计时测试。< /p>
请注意,由于 M1 是 big.LITTLE 架构,因此我们通常希望避免创建过多线程,以免在“效率”内核上安排大型 BLAS 操作;我们主要想坚持只使用“性能”核心。您可以通过打开活动监视器的“CPU 历史记录”图大致了解正在使用的内容。这是一个展示正常系统负载的示例,然后是运行 OPENBLAS_NUM_THREADS=4 ./dgesv_openblas
,然后是 OPENBLAS_NUM_THREADS=8 ./dgesv_openblas
。请注意,在四线程示例中,工作是如何正确安排到性能核心上的,而效率核心可以自由地继续做一些事情,例如在我输入此段落时呈现此 StackOverflow 网页,以及在后台播放音乐。然而,一旦我使用 8 个线程运行,音乐就开始跳动,网页开始滞后,效率核心被它们并非设计用于执行的工作负载所淹没。所有这一切,时间甚至根本没有改善:
$ OPENBLAS_NUM_THREADS=4 time ./dgesv_openblas
18.76 real 69.67 user 0.73 sys
$ OPENBLAS_NUM_THREADS=8 time ./dgesv_openblas
17.49 real 100.89 user 5.63 sys
既然我们在M1上有两种不同的消耗计算资源的方式,我们可以比较一下,看看它们是否相互干扰;例如如果我启动您示例的“加速”驱动的实例,它会减慢 OpenBLAS 驱动的实例的速度吗?
$ OPENBLAS_NUM_THREADS=4 time ./dgesv_openblas
18.86 real 70.87 user 0.58 sys
$ ./dgesv_accelerate & OPENBLAS_NUM_THREADS=4 time ./dgesv_openblas
24.28 real 89.84 user 0.71 sys
因此,遗憾的是,CPU 使用率似乎是真实的,并且它消耗了 OpenBLAS 版本想要使用的资源。 Accelerate 版本也变慢了一点,但幅度不大。
总而言之,Accelerate-heavy 进程的 CPU 使用率数字具有误导性,但并非完全如此。 Accelerate 似乎确实在使用 CPU 资源,但存在多个 Accelerate 进程必须争夺的隐藏共享资源。使用诸如 OpenBLAS 之类的非 AMX 库会产生更熟悉的性能(并且在这种情况下运行时更好,尽管情况并非总是如此)。处理器的真正“最佳”使用可能是在 3 个 Firestorm 内核和一个 Accelerate 进程上运行 OpenBLAS 之类的东西:
$ OPENBLAS_NUM_THREADS=3 time ./dgesv_openblas
23.77 real 68.25 user 0.32 sys
$ ./dgesv_accelerate & OPENBLAS_NUM_THREADS=3 time ./dgesv_openblas
28.53 real 81.63 user 0.40 sys
这一次解决了两个问题,一个需要 28.5 秒,一个需要 42.5 秒(我只是移动了 time
来测量 dgesv_accelerate
)。这将 3 核 OpenBLAS 的速度降低了约 20%,将 Accelerate 的速度降低了约 13%,因此假设您有一个应用程序需要解决很长的这些问题队列,您可以将它们提供给这两个引擎并在与适度的开销并行。
我并不是说这些配置实际上是最佳的,只是探索了这个特定工作负载的相对开销是多少,因为我很好奇。 :) 可能有办法改善这一点,而这一切都可能随着新的 Apple Silicon 处理器而发生巨大变化。