小さな正方行列(10x10)の最速線形システム解決


9

小さな行列(10x10)(小さな行列と呼ばれることもある)の線形システム解法の地獄を最適化することに非常に興味があります。これのための準備ができた解決策はありますか?行列は非特異と見なすことができます。

このソルバーは、Intel CPUでマイクロ秒単位で1 000 000回を超えて実行されます。コンピューターゲームで使用される最適化のレベルについて話しています。アセンブリやアーキテクチャ固有でコーディングしたり、精度や信頼性のトレードオフの削減を検討したり、浮動小数点ハックを使用したりしても(-ffast-mathコンパイルフラグを使用します。問題ありません)。ソルブは約20%の時間失敗することさえあります!

EigenのpartialPivLuは私の現在のベンチマークで最速で、-O3と優れたコンパイラーで最適化するとLAPACKよりも優れています。しかし今、私はカスタム線形ソルバーを手作りする段階にあります。何かアドバイスをいただければ幸いです。私は自分のソリューションをオープンソースにし、出版物などで重要な洞察を認めます。

関連:ブロック対角行列を使用して線形システムを解く速度 何百万もの行列を反転する最も速い方法は何ですか? https://stackoverflow.com/q/50909385/1489510


7
これはストレッチゴールのようです。4つの単精度TFLOPの理論上のピークスループットで最速のSkylake-X Xeon Platinum 8180を使用し、1つの10x10システムが約700(約2n ** 3/3)の浮動小数点演算を解決する必要があると仮定します。次に、1Mのこのようなシステムのバッチは、理論的には175マイクロ秒で解決できます。これは、光速を超えることのできない数値です。現在達成しているパフォーマンスを、最速の既存のコードと共有できますか?ところで、データは単精度ですか、それとも倍精度ですか?
njuffa

@njuffaはい1ms近くを達成することを目指しましたが、マイクロは別の話です。マイクロでは、頻繁に発生する類似のマトリックスを検出することにより、バッチで増分逆構造を利用することを検討しました。パフォーマンスは、プロセッサによって異なりますが、10〜500ミリ秒の範囲です。精度は倍精度または倍精度です。単精度は遅くなります。
rfabbri

@njuffa速度を上げるために精度を下げたり上げたりできます
rfabbri

2
精度/正確さはあなたの優先事項ではないようです。あなたの目標のために、おそらく比較的少数の評価で切り捨てられた反復法が役立つでしょうか?特に、妥当な初期推測がある場合。
スペンサーブリンゲルソン

1
ピボットしますか?ガウスの除去の代わりにQR分解を行うことができますか。SIMD命令を使用して複数のシステムを同時に実行できるように、システムをインターリーブしますか?ループや間接アドレス指定のない直線的なプログラムを書いていますか?どの精度が必要ですか、またシステムはどのように調整されますか?それらは悪用される可能性のある構造を持っていますか?
カールクリスチャン

回答:


7

コンパイル時に行と列の数が型にエンコードされる固有行列タイプを使用すると、LAPACKよりも有利になります。LAPACKの場合、行列のサイズは実行時にしかわかりません。この追加情報により、コンパイラーは完全または部分的なループのアンロールを行うことができ、多くの分岐命令が排除されます。独自のカーネルを作成するのではなく、既存のライブラリを使用する場合は、C ++テンプレートパラメータとして行列サイズを含めることができるデータ型が必要になるでしょう。私が知っている他の唯一のライブラリはblazeであるため、Eigenに対してベンチマークする価値があるかもしれません。

独自の実装をロールすることにした場合、PETSc自体はおそらくあなたが考えていることに適したツールではないかもしれませんが、ブロックCSRフォーマットに対してPETScが何をするかがわかるかもしれません。ループを記述するのではなく、小さな行列とベクトルの乗算のすべての演算を明示的に書き出します(リポジトリのこのファイルを参照)。これにより、ループで得られるような分岐命令がないことが保証されます。AVX命令を含むコードのバージョンは、ベクトル拡張を実際に使用する方法の良い例です。たとえば、この関数__m256d4つのdoubleを同時に操作するデータ型。行列とベクトルの乗算ではなくLU因数分解の場合のみ、ベクトル拡張を使用してすべての演算を明示的に書き出すことにより、パフォーマンスを大幅に向上させることができます。実際にCコードを手動で作成するのではなく、スクリプトを使用して生成する方がよいでしょう。命令のパイプライン処理をより効果的に利用するために一部の操作を並べ替えるときに、かなりのパフォーマンスの違いがあるかどうかを確認するのも楽しいかもしれません。

また、可能なプログラム変換の領域をランダムに探索して、より高速なバージョンを見つけるツールSTOKEから、いくらかのマイレージを得ることができます。


tx。Map <const Matrix <complex、10、10>> AA(A)のようなEigenを既に使用しています。他のものにチェックインします。
rfabbri

EigenにはAVXもあり、そのためのcomplex.hヘッダーさえあります。なぜPETScなのか?この場合、アイゲンと競争するのは難しい。私は自分の問題のためにさらにEigenを特化し、列を最大にする代わりに、3桁大きい別のピボットを見つけるとすぐにピボットを交換する近似ピボット戦略を使用しました。
rfabbri

1
@rfabbri私はこれにPETScを使用することを示唆していませんでした。その特定のインスタンスで彼らが行うことが有益である可能性があるということだけです。答えをわかりやすくするために編集しました。
Daniel Shapero

4

別のアイデアは、生成アプローチ(プログラムを書くプログラム)を使用することです。C / C ++命令のシーケンスを吐き出して、10x10システムでアンピボット** LUを実行する(メタ)プログラムを作成します。基本的に、k / i / jループのネストを取得し、O(1000)程度にフラット化します。スカラー算術。次に、その生成されたプログラムを最適化コンパイラにフィードします。ここで私が興味深いと思うのは、ループを削除すると、すべてのデータ依存関係と冗長な副次式が公開され、コンパイラーが命令を並べ替えて、実際のハードウェア(実行ユニットの数、ハザード/ストールの数など)に適切にマップできるようになることです。オン)。

すべての行列(またはそれらのいくつか)を知っている場合は、スカラーコードの代わりにSIMD組み込み関数/関数(SSE / AVX)を呼び出すことにより、スループットを向上させることができます。ここでは、単一のインスタンス内の並列処理を追跡するのではなく、インスタンス全体の厄介な並列処理を利用します。たとえば、AVX256コンパイラ組み込み関数を使用して4つの倍精度LUを同時に実行するには、レジスタ全体に4つの行列をパッキングし、それらすべてに対して同じ演算**を実行します。

**したがって、ピボットされていないLUに焦点が当てられます。ピボットは、このアプローチを2つの点で台無しにします。まず、ピボットの選択によりブランチが導入されます。つまり、データの依存関係は完全にはわかっていません。第2に、インスタンスAはインスタンスBとは異なる方法でピボットする可能性があるため、SIMDの異なる「スロット」は異なる処理を実行する必要があることを意味します。各列の対角線)。


行列は非常に小さいので、事前にスケーリングされていれば、ピボットは実行できます。行列を事前にピボットすることすらありません。必要なのは、エントリが互いの2〜3桁以内であることです。
rfabbri

2

あなたの質問は、2つの異なる考慮事項につながります。

まず、適切なアルゴリズムを選択する必要があります。したがって、行列が何らかの構造を持っているかどうかという問題を考慮する必要があります。たとえば、行列が対称である場合、コレスキー分解はLUよりも効率的です。限られた量の精度のみが必要な場合、反復法の方が高速です。

10×10

全体として、質問への答えは、検討するハードウェアとマトリックスに大きく依存します。おそらく明確な答えはなく、いくつかのことを試して最適な方法を見つける必要があります。


これまでのところ、Eigenはすでに大幅に最適化されており、SEE、AVXなどを使用しており、予備テストで反復法を試してみましたが、効果がありませんでした。私はインテル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;
}

以下は、100万のEigen::Matrix<double,10,10>::Random()行列とEigen::Matrix<double,10,1>::Random()ベクトルを使用して実行された1つのベンチマークの結果です。すべての私のテストでは、私の逆は常に高速です。私の解決ルーチンは、逆を計算し、それをベクトルで乗算することを含みます。Eigenよりも速い場合もあれば、そうでない場合もあります。私のベンチマーキング方法に問題がある可能性があります(ターボブーストを無効にしていないなど)。また、アイゲンのランダム関数は実際のデータを表していない場合があります。

  • 固有部分ピボット逆:3036ミリ秒
  • 上部ブロックが8x8の逆数:1638ミリ秒
  • 6x6の上部ブロックを持つ私の逆:1234ミリ秒
  • 固有部分ピボット解決:1791ミリ秒
  • 私の8x8上部ブロックでの解決:1739ミリ秒
  • 6x6上部ブロックでの解決:1286ミリ秒

ガジリオンの10x10行列を反転する有限要素アプリケーションがあるので、これをさらに最適化できるかどうかに非常に興味があります(そうです、逆の個々の係数が必要なので、線形システムを直接解くことは必ずしもオプションではありません)。 。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.