giải hệ phương trình tuyến tính nhanh nhất cho ma trận vuông nhỏ (10 x 10)


9

Tôi rất quan tâm đến việc tối ưu hóa địa ngục khỏi việc giải hệ thống tuyến tính cho các ma trận nhỏ (10 x 10), đôi khi được gọi là ma trận nhỏ . Có một giải pháp sẵn sàng cho việc này? Ma trận có thể được giả định là không có nghĩa.

Bộ giải này sẽ được thực hiện vượt quá 1 000 000 lần tính bằng micro giây trên CPU Intel. Tôi đang nói đến mức độ tối ưu hóa được sử dụng trong các trò chơi máy tính. Không có vấn đề gì nếu tôi viết mã theo cách lắp ráp và kiến ​​trúc cụ thể, hoặc nghiên cứu việc giảm sự đánh đổi độ chính xác hoặc độ tin cậy và sử dụng các phép hack dấu phẩy động (tôi sử dụng cờ biên dịch -ffast-math, không có vấn đề gì). Việc giải quyết thậm chí có thể thất bại trong khoảng 20% ​​thời gian!

PartPivLu của Eigen là nhanh nhất trong điểm chuẩn hiện tại của tôi, vượt trội so với LAPACK khi được tối ưu hóa với -O3 và trình biên dịch tốt. Nhưng bây giờ tôi đang ở điểm làm thủ công một bộ giải tuyến tính tùy chỉnh. Bất kỳ lời khuyên sẽ được đánh giá rất cao. Tôi sẽ làm cho giải pháp của mình trở thành nguồn mở và tôi sẽ không biết những hiểu biết chính trong các ấn phẩm, v.v.

Liên quan: Tốc độ giải hệ phương trình tuyến tính với ma trận đường chéo khối Phương pháp nhanh nhất để đảo ngược hàng triệu ma trận là gì? https://stackoverflow.com/q/50909385/1489510


7
Điều này trông giống như một mục tiêu kéo dài. Giả sử chúng ta sử dụng Skylake-X Xeon Platinum 8180 nhanh nhất với thông lượng cực đại lý thuyết của 4 TFLOP chính xác đơn, và một hệ thống 10x10 cần khoảng 700 (khoảng 2n ** 3/3) hoạt động điểm nổi để được giải quyết. Sau đó, một loạt các hệ thống 1M như vậy về mặt lý thuyết có thể được giải quyết trong 175 micro giây. Đó là một con số không thể vượt quá tốc độ ánh sáng. Bạn có thể chia sẻ hiệu suất nào bạn đang đạt được với mã hiện tại nhanh nhất của bạn không? BTW, là dữ liệu chính xác đơn hay chính xác kép?
njuffa

@njuffa vâng tôi nhắm đến đạt gần 1ms nhưng micro là chuyện khác. Đối với micro tôi đã xem xét khai thác cấu trúc nghịch đảo gia tăng trong lô bằng cách phát hiện các ma trận tương tự, thường xảy ra. Perf hiện tại ở phạm vi 10-500ms tùy thuộc vào bộ xử lý. Độ chính xác là gấp đôi hoặc thậm chí gấp đôi phức tạp. Độ chính xác duy nhất làm chậm hơn.
rfabbri

@njuffa Tôi có thể giảm hoặc tăng độ chính xác cho tốc độ
rfabbri

2
Có vẻ như độ chính xác / độ chính xác không phải là ưu tiên của bạn. Đối với mục tiêu của bạn, có lẽ một phương pháp lặp đi lặp lại ở một số lượng đánh giá tương đối nhỏ là hữu ích? Đặc biệt là nếu bạn có một dự đoán ban đầu hợp lý.
Spencer Bryngelson

1
Bạn có xoay vòng không? Bạn có thể thực hiện một nhân tố QR thay vì loại bỏ Gaussian. Bạn có xen kẽ các hệ thống của bạn để bạn có thể sử dụng các hướng dẫn SIMD và thực hiện một số hệ thống cùng một lúc không? Bạn có viết các chương trình đường thẳng không có vòng lặp và không có địa chỉ gián tiếp không? Độ chính xác nào bạn muốn và hệ thống của bạn sẽ được điều chỉnh như thế nào? Họ có bất kỳ cấu trúc nào có thể được khai thác.
Carl Christian

Câu trả lời:


7

Sử dụng loại ma trận Eigen trong đó số lượng hàng và cột được mã hóa thành loại tại thời gian biên dịch cho bạn một lợi thế so với LAPACK, trong đó kích thước ma trận chỉ được biết đến khi chạy. Thông tin bổ sung này cho phép trình biên dịch thực hiện hủy đăng ký vòng lặp đầy đủ hoặc một phần, loại bỏ rất nhiều hướng dẫn chi nhánh. Nếu bạn đang xem xét việc sử dụng một thư viện hiện có thay vì viết các hạt nhân của riêng bạn, việc có một kiểu dữ liệu trong đó kích thước ma trận có thể được đưa vào làm tham số mẫu C ++ có thể sẽ rất cần thiết. Thư viện duy nhất khác mà tôi biết về điều này là rực sáng , vì vậy đó có thể là điểm chuẩn so với Eigen.

Nếu bạn quyết định triển khai triển khai của riêng mình, bạn có thể thấy PETSc làm gì cho định dạng CSR chặn của nó là một ví dụ hữu ích, mặc dù chính PETSc có thể sẽ không phải là công cụ phù hợp với những gì bạn nghĩ. Thay vì viết một vòng lặp, họ viết ra từng thao tác cho các vectơ ma trận nhỏ nhân một cách rõ ràng (xem tệp này trong kho lưu trữ của họ). Điều này đảm bảo rằng không có hướng dẫn chi nhánh như bạn có thể nhận được với một vòng lặp. Các phiên bản của mã với hướng dẫn AVX là một ví dụ tốt về cách thực sự sử dụng các phần mở rộng vector. Ví dụ, chức năng này sử dụng__m256dkiểu dữ liệu để hoạt động đồng thời trên bốn nhân đôi cùng một lúc. Bạn có thể nhận được một sự gia tăng hiệu suất đáng kể bằng cách viết rõ ràng tất cả các hoạt động bằng cách sử dụng các phần mở rộng vectơ, chỉ cho nhân tử LU thay vì nhân vectơ ma trận. Thay vì thực sự viết mã C bằng tay, tốt hơn hết bạn nên sử dụng tập lệnh để tạo mã. Cũng có thể rất vui nếu thấy có sự khác biệt hiệu suất đáng kể khi bạn sắp xếp lại một số thao tác để tận dụng tốt hơn đường ống dẫn lệnh.

Bạn cũng có thể nhận được một số dặm từ công cụ STOKE , công cụ này sẽ khám phá ngẫu nhiên không gian của các biến đổi chương trình có thể để tìm phiên bản nhanh hơn.


tx. Tôi đã sử dụng Eigen như Map <const Matrix <Complex, 10, 10 >> AA (A) thành công. sẽ kiểm tra những thứ khác
rfabbri

Eigen cũng có AVX và thậm chí là một tiêu đề phức tạp. Tại sao PETSc cho điều này? Thật khó để cạnh tranh với Eigen trong trường hợp này. Tôi chuyên về Eigen thậm chí nhiều hơn cho vấn đề của mình và với chiến lược xoay vòng gần đúng thay vì lấy tối đa trên một cột, hoán đổi một trục ngay lập tức khi nó tìm thấy một cột lớn hơn 3 bậc.
rfabbri

1
@rfabbri Tôi không gợi ý rằng bạn sử dụng PETSc cho việc này, chỉ có điều họ làm trong trường hợp cụ thể đó có thể mang tính hướng dẫn. Tôi đã chỉnh sửa câu trả lời để làm cho rõ ràng hơn.
Daniel Shapero

4

Một ý tưởng khác có thể là sử dụng một cách tiếp cận khái quát (một chương trình viết một chương trình). Tác giả một chương trình (meta) tạo ra chuỗi các lệnh C / C ++ để thực hiện không được xoay vòng ** LU trên hệ thống 10x10 .. về cơ bản lấy tổ vòng lặp k / i / j và làm phẳng nó thành các dòng O (1000) hoặc hơn số học vô hướng. Sau đó, cung cấp chương trình đã tạo vào trình biên dịch tối ưu hóa nào. Điều tôi nghĩ là thú vị ở đây, là loại bỏ các vòng lặp phơi bày mọi phụ thuộc dữ liệu và biểu hiện phụ dự phòng, và cung cấp cho trình biên dịch cơ hội tối đa để sắp xếp lại các hướng dẫn để chúng ánh xạ tốt đến phần cứng thực tế (ví dụ: số đơn vị thực thi, mối nguy / gian hàng, vì vậy trên).

Nếu bạn tình cờ biết tất cả các ma trận (hoặc thậm chí chỉ một vài trong số chúng), bạn có thể cải thiện thông lượng bằng cách gọi nội hàm / hàm SIMD (SSE / AVX) thay vì mã vô hướng. Ở đây bạn sẽ khai thác sự song song lúng túng trong các trường hợp, thay vì theo đuổi bất kỳ sự song song nào trong một trường hợp duy nhất. Chẳng hạn, bạn có thể thực hiện đồng thời 4 độ chính xác của LU bằng cách sử dụng nội tại AVX256, bằng cách đóng gói 4 ma trận "ngang qua" thanh ghi và thực hiện cùng một thao tác ** trên tất cả chúng.

** Do đó tập trung vào LU không được xoay vòng. Xoay vòng làm hỏng cách tiếp cận này theo hai cách. Đầu tiên, nó giới thiệu các nhánh do lựa chọn trục, nghĩa là phụ thuộc dữ liệu của bạn không được biết đến một cách hoàn hảo. Thứ hai, điều đó có nghĩa là các "khe" SIMD khác nhau sẽ phải làm những việc khác nhau, vì trường hợp A có thể xoay vòng khác với trường hợp B. Vì vậy, nếu bạn theo đuổi bất kỳ điều nào trong số này, tôi khuyên bạn nên xoay vòng ma trận của mình trước khi tính toán (cho phép nhập lớn nhất của từng cột theo đường chéo).


vì các ma trận rất nhỏ, có lẽ việc xoay vòng có thể được thực hiện nếu chúng được chia tỷ lệ trước. Thậm chí không xoay vòng trước các ma trận. Tất cả những gì chúng ta cần là các mục trong vòng 2-3 bậc độ lớn của nhau.
rfabbri

2

Câu hỏi của bạn dẫn đến hai cân nhắc khác nhau.

Đầu tiên, bạn cần chọn đúng thuật toán. Do đó, câu hỏi nếu ma trận có bất kỳ cấu trúc, nên được xem xét. Ví dụ, khi các ma trận đối xứng, một phép phân tách Cholesky hiệu quả hơn LU. Khi bạn chỉ cần một lượng chính xác giới hạn, phương pháp lặp có thể nhanh hơn.

10×10

Trong tất cả, câu trả lời cho câu hỏi của bạn phụ thuộc rất nhiều vào phần cứng và ma trận mà bạn xem xét. Có lẽ không có câu trả lời chắc chắn và bạn phải thử một vài điều để tìm ra một phương pháp tối ưu.


Cho đến nay Eigen đã tối ưu hóa rất nhiều, sử dụng XEM, AVX, v.v. và tôi đã thử các phương pháp lặp trong một thử nghiệm sơ bộ và chúng không giúp ích gì. Tôi đã thử Intel MKL nhưng không tốt hơn Eigen với các cờ GCC được tối ưu hóa. Tôi hiện đang cố gắng làm thủ công một cái gì đó tốt hơn và đơn giản hơn Eigen và thực hiện các thử nghiệm chi tiết hơn với các phương pháp lặp.
rfabbri

1

Tôi sẽ thử đảo ngược khối.

https://en.wikipedia.org/wiki/Invertible_matrix#Blockwise_inversion

Eigen sử dụng một thói quen được tối ưu hóa để tính toán nghịch đảo của ma trận 4 x 4, đây có lẽ là điều tốt nhất bạn sẽ có được. Hãy thử sử dụng nó càng nhiều càng tốt.

http: //www.eigen.tuxf Family.org/dox/Inverse__SSE_8h_source.html

Trên cùng bên trái: 8x8. Trên cùng bên phải: 8x2. Dưới cùng bên trái: 2x8. Dưới cùng bên phải: 2x2. Đảo ngược 8x8 bằng cách sử dụng mã đảo ngược tối ưu hóa 4 x 4. Phần còn lại là sản phẩm ma trận.

EDIT: Sử dụng các khối 6x6, 6x4, 4x6 và 4x4 đã cho thấy nhanh hơn một chút so với những gì tôi mô tả ở trên.

using namespace Eigen;

template<typename Scalar, int tl_size, int br_size>
Matrix<Scalar, tl_size + br_size, tl_size + br_size> blockwise_inversion(const Matrix<Scalar, tl_size, tl_size>& A, const Matrix<Scalar, tl_size, br_size>& B, const Matrix<Scalar, br_size, tl_size>& C, const Matrix<Scalar, br_size, br_size>& D)
{
    Matrix<Scalar, tl_size + br_size, tl_size + br_size> result;

    Matrix<Scalar, tl_size, tl_size> A_inv = A.inverse().eval();
    Matrix<Scalar, br_size, br_size> DCAB_inv = (D - C * A_inv * B).inverse();

    result.topLeftCorner<tl_size, tl_size>() = A_inv + A_inv * B * DCAB_inv * C * A_inv;
    result.topRightCorner<tl_size, br_size>() = -A_inv * B * DCAB_inv;
    result.bottomLeftCorner<br_size, tl_size>() = -DCAB_inv * C * A_inv;
    result.bottomRightCorner<br_size, br_size>() = DCAB_inv;

    return result;
}

template<typename Scalar, int tl_size, int br_size>
Matrix<Scalar, tl_size + br_size, tl_size + br_size> my_inverse(const Matrix<Scalar, tl_size + br_size, tl_size + br_size>& mat)
{
    const Matrix<Scalar, tl_size, tl_size>& A = mat.topLeftCorner<tl_size, tl_size>();
    const Matrix<Scalar, tl_size, br_size>& B = mat.topRightCorner<tl_size, br_size>();
    const Matrix<Scalar, br_size, tl_size>& C = mat.bottomLeftCorner<br_size, tl_size>();
    const Matrix<Scalar, br_size, br_size>& D = mat.bottomRightCorner<br_size, br_size>();

    return blockwise_inversion<Scalar,tl_size,br_size>(A, B, C, D);
}

template<typename Scalar>
Matrix<Scalar, 10, 10> invert_10_blockwise_8_2(const Matrix<Scalar, 10, 10>& input)
{
    Matrix<Scalar, 10, 10> result;

    const Matrix<Scalar, 8, 8>& A = input.topLeftCorner<8, 8>();
    const Matrix<Scalar, 8, 2>& B = input.topRightCorner<8, 2>();
    const Matrix<Scalar, 2, 8>& C = input.bottomLeftCorner<2, 8>();
    const Matrix<Scalar, 2, 2>& D = input.bottomRightCorner<2, 2>();

    Matrix<Scalar, 8, 8> A_inv = my_inverse<Scalar, 4, 4>(A);
    Matrix<Scalar, 2, 2> DCAB_inv = (D - C * A_inv * B).inverse();

    result.topLeftCorner<8, 8>() = A_inv + A_inv * B * DCAB_inv * C * A_inv;
    result.topRightCorner<8, 2>() = -A_inv * B * DCAB_inv;
    result.bottomLeftCorner<2, 8>() = -DCAB_inv * C * A_inv;
    result.bottomRightCorner<2, 2>() = DCAB_inv;

    return result;
}

template<typename Scalar>
Matrix<Scalar, 10, 10> invert_10_blockwise_6_4(const Matrix<Scalar, 10, 10>& input)
{
    Matrix<Scalar, 10, 10> result;

    const Matrix<Scalar, 6, 6>& A = input.topLeftCorner<6, 6>();
    const Matrix<Scalar, 6, 4>& B = input.topRightCorner<6, 4>();
    const Matrix<Scalar, 4, 6>& C = input.bottomLeftCorner<4, 6>();
    const Matrix<Scalar, 4, 4>& D = input.bottomRightCorner<4, 4>();

    Matrix<Scalar, 6, 6> A_inv = my_inverse<Scalar, 4, 2>(A);
    Matrix<Scalar, 4, 4> DCAB_inv = (D - C * A_inv * B).inverse().eval();

    result.topLeftCorner<6, 6>() = A_inv + A_inv * B * DCAB_inv * C * A_inv;
    result.topRightCorner<6, 4>() = -A_inv * B * DCAB_inv;
    result.bottomLeftCorner<4, 6>() = -DCAB_inv * C * A_inv;
    result.bottomRightCorner<4, 4>() = DCAB_inv;

    return result;
}

Dưới đây là kết quả của một lần chạy điểm chuẩn bằng cách sử dụng một triệu Eigen::Matrix<double,10,10>::Random()ma trận và Eigen::Matrix<double,10,1>::Random()vectơ. Trong tất cả các thử nghiệm của tôi, nghịch đảo của tôi luôn luôn nhanh hơn. Thói quen giải quyết của tôi liên quan đến việc tính toán nghịch đảo và sau đó nhân nó với một vectơ. Đôi khi nó nhanh hơn Eigen, đôi khi không. Phương pháp đánh dấu băng ghế của tôi có thể là thiếu sót (không vô hiệu hóa turbo boost, v.v.). Ngoài ra, các hàm ngẫu nhiên của Eigen có thể không đại diện cho dữ liệu thực.

  • Đảo ngược một phần trục Eigen: 3036 mili giây
  • Nghịch đảo của tôi với khối trên 8x8: 1638 mili giây
  • Nghịch đảo của tôi với khối trên 6x6: 1234 mili giây
  • Pigen giải quyết một phần trục: 1791 mili giây
  • Giải quyết của tôi với khối trên 8x8: 1739 mili giây
  • Giải quyết của tôi với khối trên 6x6: 1286 mili giây

Tôi rất quan tâm xem liệu có ai có thể tối ưu hóa điều này hơn nữa không, vì tôi có một ứng dụng phần tử hữu hạn giúp đảo ngược ma trận 10x10 (và vâng, tôi cần các hệ số riêng của nghịch đảo nên việc giải quyết trực tiếp một hệ tuyến tính không phải luôn luôn là một lựa chọn) .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.