작은 정사각 행렬 (10x10)을위한 가장 빠른 선형 시스템 해법


9

작은 매트릭스 (10x10), 때로는 작은 매트릭스 라고도하는 선형 시스템 해결에서 지옥을 최적화하는 데 매우 관심이 있습니다. 이에 대한 준비된 해결책이 있습니까? 행렬은 단수로 가정 할 수 있습니다.

이 솔버는 Intel CPU에서 10 만 회 이상 마이크로 초 단위로 실행됩니다. 컴퓨터 게임에 사용되는 최적화 수준에 대해 이야기하고 있습니다. 어셈블리 및 아키텍처 별 코드로 코딩하거나 정밀도 또는 안정성 트레이드 오프 감소를 연구하고 부동 소수점 핵을 사용하더라도 (-ffast-math 컴파일 플래그를 사용하지만 문제 없음). 해결은 시간의 약 20 %까지 실패 할 수 있습니다!

Eigen의 partialPivLu는 현재 벤치 마크에서 가장 빠르며 -O3 및 우수한 컴파일러로 최적화했을 때 LAPACK을 능가합니다. 그러나 지금은 사용자 정의 선형 솔버를 손수 만드는 시점에 있습니다. 모든 조언을 주시면 감사하겠습니다. 솔루션을 오픈 소스로 만들고 출판물 등의 주요 통찰력을 알 수 있습니다.

관련 : 블록 대각 행렬을 사용한 선형 시스템 해석 속도 수백만 개의 행렬을 반전시키는 가장 빠른 방법은 무엇입니까? https://stackoverflow.com/q/50909385/1489510


7
이것은 스트레치 목표처럼 보입니다. 이론상 최대 처리량 4 개의 단정도 TFLOP와 함께 가장 빠른 Skylake-X Xeon Platinum 8180을 사용하고 10x10 시스템 하나에 700 (대략 2n ** 3 / 3)의 부동 소수점 연산이 필요하다고 가정 해 봅시다. 그런 다음 1M의 이러한 시스템 배치는 이론적으로 175 마이크로 초 내에 해결 될 수 있습니다. 그것은 초과 할 수없는 빛의 속도입니다. 가장 빠른 기존 코드로 현재 달성하고있는 성능을 공유 할 수 있습니까? BTW, 데이터는 단 정도인가 배정도인가?
njuffa

@ njuffa 예, 1ms에 가까운 것을 목표로했지만 마이크로는 또 다른 이야기입니다. 마이크로의 경우 종종 발생하는 유사한 매트릭스를 감지하여 배치에서 증분 역 구조를 이용하는 것을 고려했습니다. 성능은 프로세서에 따라 10 ~ 500ms 범위에 있습니다. 정밀도는 두 배 또는 복잡한 배입니다. 단 정밀도가 느려집니다.
rfabbri

@njuffa 속도의 정확성을 높이거나 낮출 수 있습니다
rfabbri

2
정확성 / 정확성이 우선 순위가 아닌 것 같습니다. 목표를 위해 비교적 적은 수의 평가에서 잘린 반복 방법이 유용 할 수 있습니까? 특히 합리적인 초기 추측이 있다면.
스펜서 브린 겔슨

1
피벗합니까? 가우스 제거 대신 QR 분해를 수행 할 수 있습니까? SIMD 명령어를 사용하고 한 번에 여러 시스템을 수행 할 수 있도록 시스템을 인터리브합니까? 루프와 간접 주소 지정이없는 직선 프로그램을 작성합니까? 어떤 정확도를 원하며 시스템을 어떻게 조정합니까? 그것들은 악용 될 수있는 구조를 가지고 있습니까?
Carl Christian

답변:


7

컴파일 타임 에 행과 열 수가 유형으로 인코딩되는 고유 행렬 유형을 사용하면 LAPACK보다 우위를 점할 수 있습니다. 여기서 행렬 크기는 런타임에만 알려집니다. 이 추가 정보를 통해 컴파일러는 전체 또는 부분 루프 언 롤링을 수행하여 많은 분기 명령을 제거 할 수 있습니다. 자체 커널을 작성하지 않고 기존 라이브러리를 사용하려는 경우 행렬 크기를 C ++ 템플릿 매개 변수로 포함 할 수있는 데이터 형식이 필요할 수 있습니다. 내가 아는 유일한 다른 라이브러리는 blaze 이므로 Eigen에 대한 벤치마킹 가치가 있습니다.

자체 구현을 시작하기로 결정한 경우 PETSc 자체가 아마도 마음에 드는 도구가 아닐 수도 있지만 PETSc가 블록 CSR 형식에 대해 수행하는 작업이 유용한 예가 될 수 있습니다. 루프를 작성하는 대신 작은 행렬-벡터 곱셈에 대한 모든 단일 작업을 명시 적으로 작성합니다 ( 저장소 의이 파일 참조 ). 이렇게하면 루프에서 얻을 수있는 분기 명령어가 없습니다. AVX 명령어가 포함 된 코드 버전은 실제로 벡터 확장을 사용하는 방법에 대한 좋은 예입니다. 예를 들어이 함수__m256d동시에 4 개의 더블에서 동시에 작동하는 데이터 유형. 행렬 벡터 곱셈 대신 LU 인수 분해에 대해서만 벡터 확장을 사용하여 모든 연산을 명시 적으로 작성하면 상당한 성능 향상을 얻을 수 있습니다. 실제로 C 코드를 직접 작성하는 대신 스크립트를 사용하여 생성하는 것이 좋습니다. 명령 파이프 라이닝을 더 잘 활용하기 위해 일부 작업을 재정렬 할 때 상당한 성능 차이가 있는지 확인하는 것도 재미있을 수 있습니다.

STOKE 도구에서 약간의 마일리지를 얻을 수도 있습니다.이 프로그램은 가능한 더 빠른 버전을 찾기 위해 가능한 프로그램 변환 공간을 무작위로 탐색합니다.


tx. 이미 Map <const Matrix <complex, 10, 10>> AA (A)와 같은 Eigen을 성공적으로 사용하고 있습니다. 다른 것들을 확인합니다.
rfabbri

Eigen에는 AVX와 complex.h 헤더도 있습니다. 왜 PETSc입니까? 이 경우 Eigen과 경쟁하기가 어렵습니다. 나는 내 문제에 대해 Eigen을 더욱 전문화했으며 열보다 최대를 차지하는 대신 대략적인 피벗 전략을 사용하여 3 배 더 큰 다른 것을 찾으면 즉시 피벗을 교환합니다.
rfabbri

1
@rfabbri 나는 당신이 이것을 위해 PETSc를 사용하라고 제안하지 않았으며, 특정 인스턴스에서 그들이하는 일이 유익 할 수 있다고 제안했습니다. 더 명확하게하기 위해 답변을 편집했습니다.
Daniel Shapero

4

또 다른 아이디어는 생성 방식 (프로그램 작성 프로그램)을 사용하는 것입니다. 10x10 시스템에서 피벗되지 않은 ** LU를 수행하기 위해 C / C ++ 명령어 시퀀스를 뱉어내는 (메타) 프로그램을 작성하십시오. 기본적으로 k / i / j 루프 네스트를 취하여이를 O (1000) 정도로 줄이십시오. 스칼라 산술. 그런 다음 생성 된 프로그램을 최적화 컴파일러에 공급하십시오. 내가 여기서 흥미로운 점은 루프를 제거하면 모든 데이터 종속성과 중복 하위 표현이 노출되고 컴파일러가 실제 하드웨어 (예 : 실행 단위 수, 위험 / 정지 수)에 잘 매핑되도록 명령을 다시 정렬 할 수있는 최대 기회를 제공한다는 것입니다. 의 위에).

모든 행렬 (또는 그 중 몇 개) 만 알고있는 경우 스칼라 코드 대신 SIMD 내장 함수 / 함수 (SSE / AVX)를 호출하여 처리량을 향상시킬 수 있습니다. 여기서는 단일 인스턴스 내에서 병렬 처리를 추적하는 대신 인스턴스 전체에서 난처한 병렬 처리를 활용합니다. 예를 들어, 레지스터에 "4"매트릭스를 패킹하고 이들 모두에 대해 동일한 작업 **을 수행함으로써 AVX256 내장 함수를 사용하여 동시에 4 배 정밀도 LU를 수행 할 수 있습니다.

** 따라서 피벗되지 않은 LU에 중점을 둡니다. 피벗은이 방법을 두 가지 방식으로 망칩니다. 첫째, 피벗 선택으로 인해 분기가 발생하여 데이터 종속성이 완벽하게 알려지지 않았습니다. 둘째, 이는 인스턴스 A가 인스턴스 B와 다르게 피봇 될 수 있기 때문에 다른 SIMD "슬롯"이 다른 작업을 수행해야한다는 것을 의미합니다. 각 열의 대각선).


행렬이 너무 작기 때문에 미리 축척 된 경우 피벗을 제거 할 수 있습니다. 심지어 행렬을 선회하지 않습니다. 우리가 필요로하는 것은 항목이 서로 2 ~ 3 배 안에 있다는 것입니다.
rfabbri

2

귀하의 질문은 두 가지 다른 고려 사항으로 이어집니다.

먼저 올바른 알고리즘을 선택해야합니다. 따라서 행렬이 어떤 구조를 가지고 있는지에 대한 질문을 고려해야합니다. 예를 들어, 행렬이 대칭 인 경우 Cholesky 분해가 LU보다 효율적입니다. 제한된 양의 정확도 만 필요한 경우 반복 방법이 더 빠를 수 있습니다.

둘째, 알고리즘을 효율적으로 구현해야합니다. 그렇게하려면 알고리즘의 병목 현상을 알아야합니다. 구현이 메모리 전송 속도 또는 계산 속도에 의해 제한됩니까? 당신은 단지 고려하기 때문에10×10행렬, 행렬은 CPU 캐시에 완전히 맞아야합니다. 따라서 SIMD 장치 (SSE, AVX 등)와 프로세서 코어를 사용하여 사이클 당 가능한 많은 계산을 수행해야합니다.

귀하의 질문에 대한 답변은 귀하가 고려하는 하드웨어 및 매트릭스에 따라 크게 다릅니다. 확실한 대답은 없을 것이므로 최적의 방법을 찾으려면 몇 가지를 시도해야합니다.


지금까지 Eigen은 이미 크게 최적화하고 SEE, AVX 등을 사용하며 예비 테스트에서 반복 방법을 시도했지만 도움이되지 않았습니다. 나는 Intel MKL을 시도했지만 최적화 된 GCC 플래그로 Eigen보다 낫지 않습니다. 저는 현재 Eigen보다 더 좋고 간단한 것을 손수 만들고 반복적 인 방법으로 더 자세한 테스트를 수행하려고합니다.
rfabbri

1

나는 블록 단위의 역전을 시도 할 것입니다.

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

Eigen은 최적화 된 루틴을 사용하여 4x4 행렬의 역수를 계산합니다. 가능한 많이 사용하십시오.

http://www.eigen.tuxfamily.org/dox/Inverse__SSE_8h_source.html

왼쪽 상단 : 8x8 오른쪽 상단 : 8x2. 왼쪽 하단 : 2x8 오른쪽 하단 : 2x2. 최적화 된 4x4 반전 코드를 사용하여 8x8을 반전시킵니다. 나머지는 매트릭스 제품입니다.

편집 : 6x6, 6x4, 4x6 및 4x4 블록을 사용하면 위에서 설명한 것보다 약간 빠릅니다.

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;
}

다음은 백만 개의 Eigen::Matrix<double,10,10>::Random()행렬과 Eigen::Matrix<double,10,1>::Random()벡터를 사용한 벤치 마크 실행 결과입니다 . 모든 테스트에서 항상 역수가 빠릅니다. 내 해결 루틴에는 역을 계산 한 다음 벡터를 곱하는 것이 포함됩니다. 때로는 아이겐보다 빠르며 때로는 그렇지 않습니다. 벤치 마킹 방법에 결함이있을 수 있습니다 (터보 부스트를 비활성화하지 않은 경우 등). 또한, 아이겐의 랜덤 함수는 실제 데이터를 나타내지 않을 수 있습니다.

  • 고유 부분 피벗 역수 : 3036 밀리 초
  • 8x8 상단 블록과의 역수 : 1638 밀리 초
  • 6x6 상위 블록과의 역수 : 1234 밀리 초
  • 고유 부분 피벗 해결 : 1791 밀리 초
  • 8x8 상단 블록으로 해결 : 1739 밀리 초
  • 6x6 상단 블록으로 해결 : 1286 밀리 초

나는 gazillion 10x10 행렬을 뒤집는 유한 요소 응용 프로그램을 가지고 있기 때문에 누군가가 이것을 더 최적화 할 수 있는지에 매우 관심이 있습니다. .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.