[英]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. 編譯不當
如果忘記在 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.