собственный решатель С++ с низким потреблением оперативной памяти

Я новичок в программировании на C++, но у меня есть задача вычислить собственные значения и собственные векторы (стандартная задача на собственные значения Ax=lx) для симметричных матриц (и эрмитовых)) для очень больших матрица размера: биномиальная(L,L/2), где L составляет примерно 18-22. Теперь я тестирую его на машине с 7,7 ГБ оперативной памяти, но в конце концов у меня будет доступ к ПК с 64 ГБ оперативной памяти.

Я начал с Lapack++. В начале мой проект предполагал решить эту проблему только для симметричных вещественных матриц.

Эта библиотека была отличной. Очень быстро и мало потребляет оперативной памяти. Он имеет возможность вычислять собственные векторы и помещать их во входную матрицу A для экономии памяти. Оно работает! Я думал, что Lapack++ eigensolver может работать с эрмитовой матрицей, но по неизвестной причине не может (может быть, я делаю что-то не так). Мой проект развился, и я должен быть в состоянии вычислить и эту задачу для эрмитовых матриц.

Поэтому я попытался изменить библиотеку на библиотеку Armadillo. Он отлично работает, но не так хорош, как Lapack++, который заменяет mat A всеми eigenvec, но, конечно, поддерживает эрмитовы матрицы.

Немного статистики для L=14

  • Lapack++ ОЗУ 126 МБ, время 7,9 с, собственное значение + собственные векторы

  • Armadillo ОЗУ 216 МБ, время 12 с, собственное значение

  • Armadillo ОЗУ 396 МБ время 15 с собственное значение + собственные векторы

Давайте произведем расчет: переменная double составляет около 8B. Моя матрица имеет размер binomial(14,7) = 3432, поэтому в идеальном случае она должна иметь размер 3432^2*8/1024^2 = 89 МБ.

У меня такой вопрос: можно ли изменить или заставить Armadillo делать хороший трюк, как Lapack++? Armadillo использует подпрограммы LAPACK и BLAS. Или, может быть, кто-то может порекомендовать другой подход к этой проблеме, используя другую библиотеку?

P.S.: Моя матрица действительно разреженная. Он содержит около 2 * binomial(L,L/2) ненулевых элементов. Я пытался вычислить это с помощью SuperLU в формате CSC, но это было не очень эффективно, для L = 14 -> RAM 185 МБ, но время 135 с.


person andywiecko    schedule 28.08.2015    source источник
comment
Рассматривали ли вы возможность использования SLEPcEigenSolver с PETSc?   -  person symphonic    schedule 28.08.2015
comment
@symphonic Я слышал об этом, но думаю, что эта библиотека будет для меня слишком сложной. Если будет время попробую протестировать.   -  person andywiecko    schedule 28.08.2015
comment
В LAPACK есть подпрограмма zheev (apfel.mathematik.uni-ulm.de/~lehn/FLENS/cxxlapack/netlib/lapack/) для вычисления собственных значений и -векторов эрмитовой матрицы. Однако вы, кажется, правы, в Lapack ++ кажется, что они не поддерживают матрицы с комплексными значениями (или, по крайней мере, не эрмитовы). Так что, может быть, это хорошая идея — вызывать подпрограмму LAPACK непосредственно из C++.   -  person Michael Lehn    schedule 28.08.2015
comment
@MichaelLehn Спасибо за ответ. Я знаю, что в LAPACK есть такая процедура, но я еще не научился вызывать ее напрямую на C++, поэтому сначала попробую научиться вызывать ее! :)   -  person andywiecko    schedule 28.08.2015
comment
В FLENS есть обертки C++ для LAPACK. В том числе и для жеев. Так что в основном вы можете вырвать FLENS. Вы также можете сначала проверить, выполняет ли FLENS эту работу за вас. Здесь (apfel.mathematik.uni-ulm .de/~lehn/FLENS/flens/examples/) является примером вычисления собственных значений/-векторов для симметричной матрицы. Для эрмитовых матриц есть только пример (github.com/michael-lehn/FLENS/blob/public/flens/examples/) в github, но еще не в онлайн-руководстве. Эти примеры из учебника настраивают только небольшие демо-кейсы.   -  person Michael Lehn    schedule 28.08.2015
comment
Но по-прежнему рекомендуется начинать с них, чтобы вы были уверены, как компоновать внешний LAPACK (по сути, вы просто компилируете с -DUSE_CXXLAPACK и добавляете обычные параметры компоновщика). Для больших примеров посмотрите учебник о том, как настроить большие матрицы.   -  person Michael Lehn    schedule 28.08.2015


Ответы (2)


И Lapackpp, и Armadillo полагаются на Lapack для вычисления собственных значений и собственных векторов комплексных матриц. Библиотека Лапака предоставляет другой способ выполнения этих операций для сложных эрмитовых матриц.

  • Функция zgeev() не заботится о том, чтобы матрица эрмитов. Эта функция вызывается библиотекой Lapackpp для матриц типа LaGenMatComplex в функции LaEigSolve. Эта функция вызывается функцией eig_gen() библиотеки Armadillo.

  • Функция zheev() предназначена для сложных эрмитовых матриц. Сначала он вызывает ЖЕТРД, чтобы привести эрмитову матрицу к трехдиагональной форме. В зависимости от того, нужны ли собственные векторы, он использует QR-алгоритм для вычисления собственных значений и собственных векторов. (если нужно). Функция eig_sym() библиотеки Armadillo вызывает эту функцию, если выбран метод std.

  • Функция zheevd() делает то же самое, что и zheev(), если собственные векторы не требуются. В противном случае используется алгоритм «разделяй и властвуй» (см. zstedc()). Функция eig_sym() библиотеки Armadillo вызывает эту функцию, если выбран метод dc. Поскольку метод «разделяй и властвуй» оказался быстрее для больших матриц, теперь он является методом по умолчанию.

Функции с большим количеством опций доступны в библиотеке Lapack. (см. zheevr() или zheevx). Если вы хотите сохранить формат плотной матрицы, вы также можете попробовать ComplexEigenSolver библиотеки Eigen.

Вот небольшой тест C++ с использованием C-обертки LAPACKE библиотеки Lapack. Составил 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 который можно использовать в качестве отправной точки.

person francis    schedule 28.08.2015

Если у кого-то в будущем возникнет та же проблема, что и у меня, есть несколько советов после того, как я нашел решение (спасибо всем, кто опубликовал ответы или подсказки!).

На сайте Intel вы можете найти несколько хороших примеров, написанных на 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, я не знаю, что она делает, но, вероятно, есть проблемы с примером Intel, когда они помещают строку, а не char в функцию вызова zheev( "Vectors", "Lower", ... )

person andywiecko    schedule 30.08.2015