LAPACK的dsyevr函数(用于特征值和特征向量)不应该是线程安全的吗?

10

在尝试并行计算多个矩阵的特征值和特征向量时,我发现LAPACK的dsyevr函数似乎不是线程安全的。

  • 有人知道这个问题吗?
  • 我的代码有什么问题吗?(请参见下面的最小示例)
  • 欢迎提供适用于密集矩阵的特征求解实现,该实现既不太慢,又绝对是线程安全的。

以下是C语言的最小代码示例,演示了该问题:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <assert.h>
#include <omp.h>
#include "lapacke.h"

#define M 8 /* number of matrices to be diagonalized */
#define N 1000 /* size of each matrix (real, symmetric) */

typedef double vec_t[N]; /* type for length N vector */
typedef double mtx_t[N][N]; /* type for N x N matrices */

void 
init(int m, int n, mtx_t *A){
    /* init m symmetric n x x matrices */
    srand(0);
    for (int i = 0; i < m; ++i){
        for (int j = 0; j < n; ++j){
            for (int k = 0; k <= j; ++k){
                A[i][j][k] = A[i][k][j] = (rand()%100-50) / (double)100.;
            }
        }
    }
}

void 
solve(int n, double *A, double *E, double *Q){
    /* diagonalize one matrix */
    double tol = 0.;
    int *isuppz = malloc(2*n*sizeof(int)); assert(isuppz);
    int k;
    int info = LAPACKE_dsyevr(LAPACK_COL_MAJOR, 'V', 'A', 'L', 
                              n, A, n, 0., 0., 0, 0, tol, &k, E, Q, n, isuppz);
    assert(!info);
    free(isuppz);
}

void 
s_solve(int m, int n, mtx_t *A, vec_t *E, mtx_t *Q){
    /* serial solve */
    for (int i = 0; i < m; ++i){
        solve(n, (double *)A[i], (double *)E[i], (double *)Q[i]);
    }
}

void 
p_solve(int m, int n, mtx_t *A, vec_t *E, mtx_t *Q, int nt){
    /* parallel solve */
    int i;
    #pragma omp parallel for schedule(static) num_threads(nt) \
        private(i) \
        shared(m, n, A, E, Q)
    for (i = 0; i < m; ++i){
        solve(n, (double *)A[i], (double *)E[i], (double *)Q[i]);
    }
}

void 
analyze_results(int m, int n, vec_t *E0, vec_t *E1, mtx_t *Q0, mtx_t *Q1){
    /* compare eigenvalues */
    printf("\nmax. abs. diff. of eigenvalues:\n");
    for (int i = 0; i < m; ++i){
        double t, dE = 0.;
        for (int j = 0; j < n; ++j){
            t = fabs(E0[i][j] - E1[i][j]);
            if (t > dE) dE = t;
        }
        printf("%i: %5.1e\n", i, dE);
    }

    /* compare eigenvectors (ignoring sign) */
    printf("\nmax. abs. diff. of eigenvectors (ignoring sign):\n");
    for (int i = 0; i < m; ++i){
        double t, dQ = 0.;
        for (int j = 0; j < n; ++j){
            for (int k = 0; k < n; ++k){
                t = fabs(fabs(Q0[i][j][k]) - fabs(Q1[i][j][k]));
                if (t > dQ) dQ = t;
            }
        }
        printf("%i: %5.1e\n", i, dQ);
    }
}


int main(void){
    mtx_t *A = malloc(M*N*N*sizeof(double)); assert(A);
    init(M, N, A);

    /* allocate space for matrices, eigenvalues and eigenvectors */
    mtx_t *s_A = malloc(M*N*N*sizeof(double)); assert(s_A);
    vec_t *s_E = malloc(M*N*sizeof(double));   assert(s_E);
    mtx_t *s_Q = malloc(M*N*N*sizeof(double)); assert(s_Q);

    /* copy initial matrix */
    memcpy(s_A, A, M*N*N*sizeof(double));

    /* solve serial */
    s_solve(M, N, s_A, s_E, s_Q);

    /* allocate space for matrices, eigenvalues and eigenvectors */
    mtx_t *p_A = malloc(M*N*N*sizeof(double)); assert(p_A);
    vec_t *p_E = malloc(M*N*sizeof(double));   assert(p_E);
    mtx_t *p_Q = malloc(M*N*N*sizeof(double)); assert(p_Q);

    /* copy initial matrix */
    memcpy(p_A, A, M*N*N*sizeof(double));

    /* use one thread, to check that the algorithm is deterministic */
    p_solve(M, N, p_A, p_E, p_Q, 1); 

    analyze_results(M, N, s_E, p_E, s_Q, p_Q);

    /* copy initial matrix */
    memcpy(p_A, A, M*N*N*sizeof(double));

    /* use several threads, and see what happens */
    p_solve(M, N, p_A, p_E, p_Q, 4); 

    analyze_results(M, N, s_E, p_E, s_Q, p_Q);

    free(A);
    free(s_A);
    free(s_E);
    free(s_Q);
    free(p_A);
    free(p_E);
    free(p_Q);
    return 0;
}

这就是您得到的结果(请查看最后一个输出块中的差异,其中告诉您特征向量是错误的,虽然特征值是正确的):

max. abs. diff. of eigenvalues:
0: 0.0e+00
1: 0.0e+00
2: 0.0e+00
3: 0.0e+00
4: 0.0e+00
5: 0.0e+00
6: 0.0e+00
7: 0.0e+00

max. abs. diff. of eigenvectors (ignoring sign):
0: 0.0e+00
1: 0.0e+00
2: 0.0e+00
3: 0.0e+00
4: 0.0e+00
5: 0.0e+00
6: 0.0e+00
7: 0.0e+00

max. abs. diff. of eigenvalues:
0: 0.0e+00
1: 0.0e+00
2: 0.0e+00
3: 0.0e+00
4: 0.0e+00
5: 0.0e+00
6: 0.0e+00
7: 0.0e+00

max. abs. diff. of eigenvectors (ignoring sign):
0: 0.0e+00
1: 1.2e-01
2: 1.6e-01
3: 1.4e-01
4: 2.3e-01
5: 1.8e-01
6: 2.6e-01
7: 2.6e-01

The code was compiled with gcc 4.4.5 and linked against openblas (containing LAPACK) (libopenblas_sandybridge-r0.2.8.so). Another version of LAPACK was also tested. The program was tested by calling LAPACK directly from C (without LAPACKE), and the results were the same. Substituting dsyevr with the dsyevd function (and adjusting arguments) had no effect. Here are the compilation instructions used:
gcc -std=c99 -fopenmp -L/path/to/openblas/lib -Wl,-R/path/to/openblas/lib/ \
-lopenblas -lgomp -I/path/to/openblas/include main.c -o main

很遗憾,谷歌没有回答我的问题,所以欢迎任何提示!

编辑:为了确保BLAS和LAPACK版本的一切正常,我从http://www.netlib.org/lapack/(版本3.4.2)获取了参考LAPACK(包括BLAS和LAPACKE)。 编译示例代码有点棘手,但最终可以通过分别编译和链接来实现。

gcc -c -std=c99 -fopenmp -I../lapack-3.4.2/lapacke/include \
    netlib_dsyevr.c -o netlib_main.o
gfortran netlib_main.o ../lapack-3.4.2/liblapacke.a \
    ../lapack-3.4.2/liblapack.a ../lapack-3.4.2/librefblas.a \
    -lgomp -o netlib_main

该示例程序和netlib LAPACK/BLAS的构建是在 Darwin 12.4.0 x86_64 Linux 3.2.0-0.bpo.4-amd64 x86_64 平台上完成的。该程序存在一致性问题。为了使内容更加通俗易懂,本文不进行过多解释,但会保留 HTML 标签。

2
http://www.netlib.org/lapack/lapack-3.3.0.html 表明 LAPACK 3.3 中的所有例程都是线程安全的。 - M. S. B.
1
我使用Intel MKL和icpc尝试了您的代码,并在第二次运行时仅出现数值误差(<1e-12)。但是,我必须将类型添加到“malloc”语句中并更改头文件为“mkl_lapacke.h”... - Stefan
感谢提示。OpenBLAS在多线程应用程序中表现出有时奇怪的行为,昨天我自己在其他“实验”中也遇到了这种情况。这就是为什么我重新编译并使用来自netlib的参考BLAS/LAPACK运行所有内容的原因(请参见问题的编辑)。 - dastrobu
1
“had to add the types” 表示 icpc 抱怨将 void 指针分配给另一种类型的指针,因此我不得不在 solve 函数中的 malloc 中添加 (int*) 等。 数值误差仅在并行部分中发生。 顺序部分对所有结果都恰好为零。 - Stefan
@dastrobu 这很有趣。icc(英特尔的C编译器)抱怨你不能在循环中定义变量(for (int i = 0...)。这就是我认为你在用C++的原因。 - Stefan
显示剩余10条评论
4个回答

7
我最终收到了LAPACK团队的解释,并获得了授权引用。我觉得你所看到的问题可能是由于您使用的FORTRAN版本的LAPACK库编译方式造成的。当我使用gfortran 4.8.0(在Mac OS 10.8上)使用-O3选项编译LAPACK时,我可以重现您看到的问题。如果我重新编译LAPACK和参考BLAS库以使用-fopenmp -O3,则问题就消失了。在gfortran手册中有一条注释,指出“-fopenmp意味着-frecursive,即所有本地数组都将在堆栈上分配”,因此可能有一些辅助例程中使用本地数组,dsyevr调用这些例程,编译器的默认设置会导致它们以非线程安全的方式进行分配。无论如何,为这些分配在堆栈上,似乎-fopenmp可以解决这个问题。
我可以确认这可以解决netlib-BLAS/LAPACK的问题。请记住,堆栈大小是有限的,并且可能需要调整,如果矩阵变大或数量变多。
OpenBLAS必须使用USE_OPENMP = 1和USE_THREAD = 1编译,以获得单线程和线程安全的库。
使用这些编译器和make标志,示例程序在所有库中都可以正确运行。仍然存在一个问题,如何确保将代码交给最终用户的人链接到正确编译的BLAS/LAPACK库?如果用户只是得到一个分段错误,那么可以在README文件中添加一条注释,但由于错误更加微妙,甚至不能保证用户会认识到错误(用户默认不阅读README文件;-))。将BLAS/LAPACK与代码一起发货并不是一个好主意,因为BLAS/LAPACK的基本思想是每个人都有一个专门针对他的计算机进行优化的版本。欢迎提出意见...

1
这个问题在LAPACK 3.5.0中已得到修复:http://www.netlib.org/lapack/errata_from_342_to_350.html#_strong_span_class_green_bug112_span_strong_dsyevr_does_not_seem_to_be_thread_safe - pattivacek

1

关于另一个库:GSL。它是线程安全的。但这意味着您必须为每个线程创建工作区,并确保每个线程使用它的工作区,例如通过线程编号索引指针。


这也在我的脑海中浮现过,我刚刚通过使用调用GSL的等效函数替换solve函数进行了测试。这为串行和并行版本提供了一致的结果,并且也等同于串行LAPACK调用的解决方案。不幸的是,GSL要慢一个到两个数量级。至少这是一个解决方法,但并不是真正令人满意的。 - dastrobu

0

看起来你促使LAPACK开发人员引入了一个“修复”方案。实际上,他们在make.inc.example中的编译器标志中添加了-frecursive。通过测试你的示例代码,这个修复似乎是无关紧要的(数值误差并没有消失),而且也不是很理想的(可能会影响性能)。

即使这个修复是正确的,-frecursive也被-fopenmp隐含,所以使用一致的标志的人是安全的(那些使用预打包软件的人则不是)。

总之,请修复你的代码,而不是让人们感到困惑。


0

[在正确解决方案未知之前添加的以下答案]

免责声明:以下是一个解决问题的临时方法,它既不能解决原始问题,也不能解释LAPACK出现了什么问题。但是,它可能帮助面临相同问题的人。


被转换成C语言的LAPACK旧版f2c(称为CLAPACK)似乎没有线程安全问题。请注意,这不是将Fortran库转换为C接口的版本,而是LAPACK自动转换为C版本。

使用最新版本的CLAPACK(3.2.1)进行编译和链接可以正常工作。因此,在这方面CLAPACK似乎是线程安全的。当然,CLAPACK的性能不如netlib-BLAS / LAPACK或OpenBLAS / LAPACK,但至少它不像GSL那样糟糕。

下面是对所有测试过的LAPACK变体(以及GSL)进行特征值分解一个1000 x 1000矩阵的时间记录(当然只使用了一个线程),矩阵使用init函数初始化(有关定义,请参见问题)。

time -p ./gsl
real 17.45
user 17.42
sys 0.01

time -p ./netlib_dsyevr
real 0.67
user 0.84
sys 0.02

time -p ./openblas_dsyevr
real 0.66
user 0.46
sys 0.01

time -p ./clapack_dsyevr
real 1.47
user 1.46
sys 0.00

这表明GSL绝对不是处理数千维大矩阵的好方法,特别是如果你有很多这样的矩阵。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接