低内存消耗c++特征解算器

low RAM consuming c++ eigen solver

本文关键字:特征 c++ 内存      更新时间:2023-10-16

我是新手在 c++编程,但我有一个任务来计算特征值和特征向量(标准特征问题Ax=lx对称矩阵(和厄米特))对于非常大的矩阵的大小:二项式(L,L/2)其中L约为18-22。现在我正在一台有7.7 GB可用内存的机器上测试它,但最终我将能够访问具有64GB内存的PC。

我从 lapack++ 开始。在一开始,我的项目假设只解决对称实矩阵的这个问题。

这个库很棒。非常快和小的RAM消耗。它可以选择计算特征向量并将其放入输入矩阵A中以节省内存。它的工作原理!我认为 lapack++ 特征求解器可以处理厄米矩阵,但由于未知的原因(也许我做错了什么)。我的项目已经发展,我应该也能够计算厄米矩阵的这个问题。

所以我尝试将library更改为Armadillo library。它工作得很好,但它不像 lapack++ 那样好,它将mat A替换为所有eigenvec,但当然支持厄米矩阵。

L=14的一些统计量

  • lapack++ RAM 126MB时间7.9s特征值+特征向量

  • Armadillo RAM 216MB时间12s特征值

  • Armadillo RAM 396MB时间15s特征值+特征向量

我们计算一下:double变量约为8B。我的矩阵有大小binomial(14,7) = 3432,所以在理想情况下,它应该有3432^2*8/1024^2 = 89 MB

我的问题是:是否有可能修改或增强Armadillo来做一个很好的技巧 lapack++ ?Armadillo使用LAPACKBLAS例程。或者有人可以推荐使用其他库来解决这个问题的另一种方法?

注。我的矩阵非常稀疏。它有大约2 *二项(L,L/2)非零元素。我试着用CSC格式的SuperLU来计算这个,但它不是很有效,对于L=14 -> RAM 185MB,但时间为135秒。

Lapackpp和Armadillo都依赖于Lapack来计算复矩阵的特征值和特征向量。Lapack库提供了对复杂厄米矩阵执行这些操作的不同方法。

  • 函数zgeev()不关心矩阵是否是厄米矩阵。这个函数由Lapackpp库在函数LaEigSolve中为类型为LaGenMatComplex的矩阵调用。Armadillo库的eig_gen()函数调用这个函数

  • 函数zheev()专用于复厄米矩阵。首先调用ZHETRD将厄米矩阵化简为三对角线形式。根据是否需要特征向量,然后使用QR算法计算特征值和特征向量(如果需要)。如果选择了方法std, Armadillo库的eig_sym()函数将调用此函数

  • 如果不需要特征向量,函数zheevd()zheev()做同样的事情。否则,它会使用分治算法(参见zstedc())。如果选择了方法dc, Armadillo库的eig_sym()函数将调用该函数。由于分治法被证明对于大矩阵更快,所以它现在是默认方法。

有更多选项的函数在Lapack库中可用。(参见zheevr()zheevx)。如果你想保持密集的矩阵格式,你也可以尝试Eigen库的ComplexEigenSolver

下面是一个使用Lapack库的C包装器LAPACKE的c++小测试。由g++ main.cpp -o main2 -L /home/...../lapack-3.5.0 -llapacke -llapack -lblas

编译
#include <iostream>
#include <complex>
#include <ctime>
#include <cstring>
#include "lapacke.h"
#undef complex
using namespace std;
int main()
{
    //int n = 3432;
    int n = 600;
    std::complex<double> *matrix=new std::complex<double>[n*n];
    memset(matrix, 0, n*n*sizeof(std::complex<double>));
    std::complex<double> *matrix2=new std::complex<double>[n*n];
    memset(matrix2, 0, n*n*sizeof(std::complex<double>));
    std::complex<double> *matrix3=new std::complex<double>[n*n];
    memset(matrix3, 0, n*n*sizeof(std::complex<double>));
    std::complex<double> *matrix4=new std::complex<double>[n*n];
    memset(matrix4, 0, n*n*sizeof(std::complex<double>));
    for(int i=0;i<n;i++){
        matrix[i*n+i]=42;
        matrix2[i*n+i]=42;
        matrix3[i*n+i]=42;
        matrix4[i*n+i]=42;
    }
    for(int i=0;i<n-1;i++){
        matrix[i*n+(i+1)]=20;
        matrix2[i*n+(i+1)]=20;
        matrix3[i*n+(i+1)]=20;
        matrix4[i*n+(i+1)]=20;
        matrix[(i+1)*n+i]=20;
        matrix2[(i+1)*n+i]=20;
        matrix3[(i+1)*n+i]=20;
        matrix4[(i+1)*n+i]=20;
    }
    double* w=new double[n];//eigenvalues
    //the lapack function zheev
    clock_t t;
    t = clock();
    LAPACKE_zheev(LAPACK_COL_MAJOR,'V','U', n,reinterpret_cast< __complex__ double*>(matrix), n, w);
    t = clock() - t;
    cout<<"zheev : "<<((float)t)/CLOCKS_PER_SEC<<" seconds"<<endl;
    cout<<"largest eigenvalue="<<w[n-1]<<endl;
    std::complex<double> *wc=new std::complex<double>[n];
    std::complex<double> *vl=new std::complex<double>[n*n];
    std::complex<double> *vr=new std::complex<double>[n*n];
    t = clock();
    LAPACKE_zgeev(LAPACK_COL_MAJOR,'V','V', n,reinterpret_cast< __complex__ double*>(matrix2), n, reinterpret_cast< __complex__ double*>(wc),reinterpret_cast< __complex__ double*>(vl),n,reinterpret_cast< __complex__ double*>(vr),n);
    t = clock() - t;
    cout<<"zgeev : "<<((float)t)/CLOCKS_PER_SEC<<" seconds"<<endl;
    cout<<"largest eigenvalue="<<wc[0]<<endl;
    t = clock();
    LAPACKE_zheevd(LAPACK_COL_MAJOR,'V','U', n,reinterpret_cast< __complex__ double*>(matrix3), n, w);
    t = clock() - t;
    cout<<"zheevd : "<<((float)t)/CLOCKS_PER_SEC<<" seconds"<<endl;
    cout<<"largest eigenvalue="<<w[n-1]<<endl;
    t = clock();
    LAPACKE_zheevd(LAPACK_COL_MAJOR,'N','U', n,reinterpret_cast< __complex__ double*>(matrix4), n, w);
    t = clock() - t;
    cout<<"zheevd (no vector) : "<<((float)t)/CLOCKS_PER_SEC<<" seconds"<<endl;
    cout<<"largest eigenvalue="<<w[n-1]<<endl;
    delete[] w;
    delete[] wc;
    delete[] vl;
    delete[] vr;
    delete[] matrix;
    delete[] matrix2;
    return 0;
}

我的计算机的输出是:

zheev : 2.79 seconds
largest eigenvalue=81.9995
zgeev : 10.74 seconds
largest eigenvalue=(77.8421,0)
zheevd : 0.44 seconds
largest eigenvalue=81.9995
zheevd (no vector) : 0.02 seconds
largest eigenvalue=81.9995

这些测试可以通过使用Armadillo库来执行。直接调用Lapack库可能会允许您获得一些内存,但是Lapack的包装器在这方面也是有效的。

真正的问题是你是需要所有的特征向量,所有的特征值还是只需要最大的特征值。因为在最后一种情况下有非常有效的方法。看看Arnoldi/Lanczos迭代算法。如果矩阵是空间的,则可能获得巨大的内存增益,因为只执行矩阵-向量乘积:没有必要保持密集格式。这是在SlepC库中完成的,它利用了Petsc的空间矩阵格式。下面是一个可以作为起点的Slepc示例:

如果有人有同样的问题,我在未来有一些提示后,我找到了解决方案(感谢所有张贴的一些答案或线索!)。

在英特尔网站上,你可以找到一些用Fortran和c写的很好的例子。例如厄米特征值问题例程zheev():https://software.intel.com/sites/products/documentation/doclib/mkl_sa/11/mkl_lapack_examples/zheev_ex.c.htm

要使它在c++中工作,你应该在代码中编辑一些行:

在原型函数声明中,对所有函数做类似的声明:extern void zheev( ... )更改为extern "C" {void zheev( ... )}

更改调用lapack的函数,添加_符号,例如:zheev( ... )zheev_( ... )(通过替换使其在代码中全部可用,但我不知道为什么它可以工作。我通过做一些实验弄明白了。

您可以选择将printf函数转换为标准流std::cout,并将包含的标头stdio.h替换为iostream

编译运行命令:g++ test.cpp -o test -llapack -lgfortran -lm -Wno-write-strings

关于最后一个选项-Wno-write-strings,我现在不知道它在做什么,但可能有问题英特尔的例子,当他们把字符串而不是字符调用函数zheev( "Vectors", "Lower", ... )