簡體   English   中英

為什么我的矩陣求逆在 C++ 中使用 LAPACKE 如此緩慢:MAGMA Alternative and setup

[英]Why my inversions of matrices are such slow with LAPACKE in C++ : MAGMA Alternative and set up

我正在使用LAPACK來反轉矩陣:我做了一個引用傳遞,即通過處理地址。 下面的函數帶有一個輸入矩陣和一個由它們的地址引用的輸出矩陣。

問題是我不得不將F_matrix轉換為1D array ,我認為這是在運行時級別的性能浪費:我可以找到哪種方法來擺脫這個耗時的補充任務,我想如果我調用很多次函數matrix_inverse_lapack

在相關功能下方:

// Passing Matrixes by Reference
void matrix_inverse_lapack(vector<vector<double>> const &F_matrix, vector<vector<double>> &F_output) {

  // Index for loop and arrays
  int i, j, ip, idx;

  // Size of F_matrix
  int N = F_matrix.size();

  int *IPIV = new int[N];

  // Statement of main array to inverse
  double *arr = new double[N*N];

  // Output Diagonal block
  double *diag = new double[N];

for (i = 0; i<N; i++){
    for (j = 0; j<N; j++){
      idx = i*N + j;
      arr[idx] = F_matrix[i][j];
    }
  }

  // LAPACKE routines
  int info1 = LAPACKE_dgetrf(LAPACK_ROW_MAJOR, N, N, arr, N, IPIV);
  int info2 = LAPACKE_dgetri(LAPACK_ROW_MAJOR, N, arr, N, IPIV);

 for (i = 0; i<N; i++){
    for (j = 0; j<N; j++){
      idx = i*N + j;
      F_output[i][j] = arr[idx];
    }
  }

  delete[] IPIV;
  delete[] arr;
}

例如,我這樣稱呼它:

vector<vector<double>> CO_CL(lsize*(2*Dim_x+Dim_y), vector<double>(lsize*(2*Dim_x+Dim_y), 0));

... some code

matrix_inverse_lapack(CO_CL, CO_CL);

反演的表現不是預期的,我認為這是由於我在函數matrix_inverse_lapack描述的這種轉換2D -> 1D

更新

有人建議我在我的 MacOS Big Sur 11.3 上安裝 MAGMA,但我在設置時遇到了很多困難。

我有一個AMD Radeon Pro 5600M顯卡。 我已經默認安裝了 Big Sur 版本的所有 Framework OpenCL(也許我這么說是錯的)。 任何人都可以告訴安裝 MAGMA 的程序。 我在http://magma.maths.usyd.edu.au/magma/上看到了 MAGMA 軟件,但它真的很貴而且不符合我想要的:我只需要所有的 SDK(頭文件和庫) ) ,如果可能的話,用我的 GPU 卡構建。 我已經在我的 MacOS 上安裝了所有的 Intel OpenAPI SDK。 也許,我可以將它鏈接到 MAGMA 安裝。

我看到了另一個鏈接https://icl.utk.edu/magma/software/index.html ,其中 MAGMA 似乎是公開的:上面的非免費版本沒有鏈接,不是嗎?

首先讓我抱怨OP沒有提供所有必要的數據。 該程序幾乎是完整的,但它不是一個最小的、可重現的示例 這很重要,因為(a)它浪費時間並且(b)它隱藏了潛在的相關信息,例如。 關於矩陣初始化。 其次,OP 沒有提供有關編譯的任何詳細信息,這也可能是相關的。 最后,但並非最不重要的是,OP 沒有檢查狀態代碼中是否存在 Lapack 函數可能的錯誤,這對於正確解釋結果也很重要。

讓我們從一個最小的可重現示例開始:

#include <lapacke.h>
#include <vector>
#include <chrono>
#include <iostream>

using Matrix = std::vector<std::vector<double>>;

std::ostream &operator<<(std::ostream &out, Matrix const &v)
{
    const auto size = std::min<int>(10, v.size());
    for (int i = 0; i < size; i++)
    {
        for (int j = 0; j < size; j++)
        {
            out << v[i][j] << "\t";
        }
        if (size < std::ssize(v)) out << "...";
        out << "\n";
    }
    return out;
}

void matrix_inverse_lapack(Matrix const &F_matrix, Matrix &F_output, std::vector<int> &IPIV_buffer,
                           std::vector<double> &matrix_buffer)
{
    //  std::cout << F_matrix << "\n";
    auto t0 = std::chrono::steady_clock::now();

    const int N = F_matrix.size();

    for (int i = 0; i < N; i++)
    {
        for (int j = 0; j < N; j++)
        {
            auto idx = i * N + j;
            matrix_buffer[idx] = F_matrix[i][j];
        }
    }

    auto t1 = std::chrono::steady_clock::now();
    // LAPACKE routines
    int info1 = LAPACKE_dgetrf(LAPACK_ROW_MAJOR, N, N, matrix_buffer.data(), N, IPIV_buffer.data());
    int info2 = LAPACKE_dgetri(LAPACK_ROW_MAJOR, N, matrix_buffer.data(), N, IPIV_buffer.data());
    auto t2 = std::chrono::steady_clock::now();

    for (int i = 0; i < N; i++)
    {
        for (int j = 0; j < N; j++)
        {
            auto idx = i * N + j;
            F_output[i][j] = matrix_buffer[idx];
        }
    }
    auto t3 = std::chrono::steady_clock::now();

    auto whole_fun_time = std::chrono::duration<double>(t3 - t0).count();
    auto lapack_time = std::chrono::duration<double>(t2 - t1).count();
    //   std::cout << F_output << "\n";
    std::cout << "status: " << info1 << "\t" << info2 << "\t" << (info1 == 0 && info2 == 0 ? "Success" : "Failure")
              << "\n";
    std::cout << "whole function:            " << whole_fun_time << "\n";
    std::cout << "LAPACKE matrix operations: " << lapack_time << "\n";
    std::cout << "conversion:                " << (whole_fun_time - lapack_time) / whole_fun_time * 100.0 << "%\n";
}

int main(int argc, const char *argv[])
{
    const int M = 5;  // numer of test repetitions

    const int N = (argc > 1) ? std::stoi(argv[1]) : 10;
    std::cout << "Matrix size = " << N << "\n";

    std::vector<int> IPIV_buffer(N);
    std::vector<double> matrix_buffer(N * N);

    // Test matrix_inverse_lapack M times
    for (int i = 0; i < M; i++)
    {
        Matrix CO_CL(N);
        for (auto &v : CO_CL) v.resize(N);

        int idx = 1;
        for (auto &v : CO_CL)
        {
            for (auto &x : v)
            {
                x = idx + 1.0 / idx;
                idx++;
            }
        }
        matrix_inverse_lapack(CO_CL, CO_CL, IPIV_buffer, matrix_buffer);
    }
}

在這里, operator<<是一種矯枉過正,但對於想要半手動驗證代碼是否有效(通過取消注釋第 26 行和第 58 行)的任何人來說可能很有用,並且確保代碼正確比衡量其性能更重要。

代碼可以編譯為

g++ -std=c++20 -O3  main.cpp -llapacke

該程序依賴於需要安裝的外部庫lapacke ,頭文件 + 二進制文件,用於編譯和運行代碼。

我的代碼與 OP 的有點不同:它更接近於“現代 C++”,因為它避免使用裸指針; 我還向matrix_inverse_lapack添加了外部緩沖區,以抑制內存分配器和釋放器的持續啟動,這是一個小改進,以可衡量的方式減少了 2D-1D-2D 轉換開銷。 我還必須初始化矩陣並找到一種方法來在 OP 的腦海中讀取N的值。 我還添加了一些用於基准測試的計時器讀數。 除此之外,代碼的邏輯不變。

現在在一個像樣的工作站上進行了一個基准測試。 它列出了轉換所花費的時間相對於matrix_inverse_lapack花費的總時間的matrix_inverse_lapack 換句話說,我衡量轉換開銷:

 N =   10, 3.5%   
 N =   30, 1.5%   
 N =  100, 1%   
 N =  300, 0.5%   
 N = 1000, 0.35%  
 N = 3000, 0.1%  

正如預期的那樣(數據未顯示),Lapack 花費的時間很好地縮放為 N 3 對於 N = 3000,反轉矩陣的時間約為 16 秒,對於 N = 10,約為 5 -6 s(5 微秒)。

我認為即使是 3% 的開銷也是完全可以接受的。 我相信 OP 使用大小大於 100 的矩陣,在這種情況下,1% 或以下的開銷當然是可以接受的。

那么什么 OP(或任何有類似問題的人)可能做錯了以獲得“不可接受的開銷轉換值”? 這是我的短名單

  1. 編譯不當
  2. 矩陣初始化不正確(用於測試)
  3. 不正確的基准測試

1. 編譯不當

如果忘記在 Release 模式下進行編譯,最終會導致優化的 Lapacke 與未優化的轉換競爭。 在我的機器上,當 N = 20 時,這會以 33% 的開銷達到峰值。

2. 矩陣初始化不當(用於測試)

如果像這樣初始化矩陣:

        for (auto &v : CO_CL)
        {
            for (auto &x : v)
            {
                x = idx; // rather than, eg., idx + 1.0/idx
                idx++;
            }
        }

那么矩陣是奇異的,lapack 很快返回,狀態不為 0。這增加了轉換部分的相對重要性。 但是奇異矩陣不是人們想要反轉的(這是不可能的)。

3. 不正確的基准測試

下面是 N = 10 的程序輸出示例:

 ./a.out 10 
 Matrix size = 10
 status: 0  0   Success
 whole function:            0.000127658
 LAPACKE matrix operations: 0.000126783
 conversion:                0.685425%
 status: 0  0   Success
 whole function:            1.2497e-05
 LAPACKE matrix operations: 1.2095e-05
 conversion:                3.21677%
 status: 0  0   Success
 whole function:            1.0535e-05
 LAPACKE matrix operations: 1.0197e-05
 conversion:                3.20835%
 status: 0  0   Success
 whole function:            9.741e-06
 LAPACKE matrix operations: 9.422e-06
 conversion:                3.27482%
 status: 0  0   Success
 whole function:            9.939e-06
 LAPACKE matrix operations: 9.618e-06
 conversion:                3.2297%

可以看到,對 lapack 函數的第一次調用可能比后續調用多花費 10 倍的時間。 這是一個相當穩定的模式,就好像 Lapack 需要一些時間來進行自初始化。 它會嚴重影響小 N 的測量。

4. 還能做什么?

OP 似乎相信他對 2D 數組的處理方法是好的,而 Lapack 在將 2D 數組打包為 1D 數組方面是奇怪且過時的。 不,Lapack 是對的。

如果將二維數組定義為vector<vector<double>> ,則會獲得一個優勢:代碼簡單。 這是有代價的。 這種矩陣的每一行都與其他行分開分配。 因此,一個 100 × 100 的矩陣可以存儲在 100 個完全不同的存儲塊中。 這對緩存(和預取器)利用率有不良影響。 Lapck(和其他線性代數包)在單個連續數組中強制壓縮數據。 這是為了最大限度地減少緩存和預取器未命中。 如果 OP 從一開始就使用這種方法,他可能會獲得超過他們現在為轉換支付的 1-3% 的收益。

這種緊湊化可以通過至少三種方式實現。

  • 為二維矩陣編寫一個自定義類,將內部數據存儲在一個一維數組中並方便地訪問成員函數(例如: operator () ),或者找到一個可以做到這一點的庫
  • std::vector編寫自定義分配器(或查找庫)。 此分配器應從與 Lapack 使用的數據存儲模式完全匹配的預分配一維向量中分配內存
  • 使用std::vector<double*>並初始化指針,地址指向預分配的一維數組的適當元素。

上述每個解決方案都會強制對周圍的代碼進行一些更改,而 OP 可能不想這樣做。 一切都取決於代碼復雜性和預期的性能提升。

編輯:替代庫

另一種方法是使用以高度優化而聞名的庫。 Lapack 本身可以被視為具有許多實現的標准接口,並且 OP 可能會使用未優化的接口。 選擇哪個庫可能取決於 OP 感興趣的硬件/軟件平台,並且可能會隨時間變化。

至於現在(2021 年中期),一個不錯的建議是:

如果 OP 使用大小至少為 100 的 martices,那么面向 GPU 的 MAGMA 可能值得嘗試。

使用並行 CPU 庫(例如 Plasma)可能更簡單(安裝、運行)。 Plsama 是 Lapack 兼容的,它已經由包括 Jack Dongarra 在內的一大群人開發,它也應該很容易在本地編譯,因為它提供了一個 CMake 腳本。

可以在此處找到基於並行 CPU 的多核實現在多大程度上優於 LU 分解的單線程實現的示例: https : //cse.buffalo.edu/faculty/miller/Courses/CSE633/Tummala -Spring-2014-CSE633.pdf (簡短回答:大小為 1000 的矩陣為 5 到 15 次)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM