web-dev-qa-db-ja.com

行列の乗算でMATLABが高速なのはなぜですか?

CUDA、C++、C#、およびJavaでベンチマークを作成し、検証と行列生成にMATLABを使用しています。しかし、MATLABで乗算すると、2048x2048およびさらに大きな行列もほぼ瞬時に乗算されます。

             1024x1024   2048x2048   4096x4096
             ---------   ---------   ---------
CUDA C (ms)      43.11      391.05     3407.99
C++ (ms)       6137.10    64369.29   551390.93
C# (ms)       10509.00   300684.00  2527250.00
Java (ms)      9149.90    92562.28   838357.94
MATLAB (ms)      75.01      423.10     3133.90

CUDAだけが競争力がありますが、少なくともC++は多少近づき、60xより遅くなるとは思いませんでした。

だから私の質問は-どのようにMATLABはそれを高速にしていますか?

C++コード:

float temp = 0;
timer.start();
for(int j = 0; j < rozmer; j++)
{
    for (int k = 0; k < rozmer; k++)
    {
        temp = 0;
        for (int m = 0; m < rozmer; m++)
        {
            temp = temp + matice1[j][m] * matice2[m][k];
        }
        matice3[j][k] = temp;
    }
}
timer.stop();

編集:C#の結果についてどう考えるべきかわかりません。アルゴリズムはC++およびJavaと同じですが、2048から1024への巨大なジャンプがありますか?

Edit2:MATLABと4096x4096の結果を更新しました

178
Wolf

以下は、Tesla C2070を搭載したマシンでMATLAB R2011a + Parallel Computing Toolbox を使用した結果です。

>> A = Rand(1024); gA = gpuArray(A);
% warm up by executing the operations a couple of times, and then:
>> tic, C = A * A; toc
Elapsed time is 0.075396 seconds.
>> tic, gC = gA * gA; toc
Elapsed time is 0.008621 seconds.

MATLABは、行列乗算に高度に最適化されたライブラリを使用しているため、単純なMATLAB行列乗算は非常に高速です。 gpuArrayバージョンは MAGMA を使用します。

Tesla K20c、および新しいtimeitおよびgputimeit関数を搭載したマシンでR2014aを使用して更新します。

>> A = Rand(1024); gA = gpuArray(A);
>> timeit(@()A*A)
ans =
    0.0324
>> gputimeit(@()gA*gA)
ans =
    0.0022

16個の物理コアとTesla V100を搭載したWIN64マシンでR2018bを使用して更新します。

>> timeit(@()A*A)
ans =
    0.0229
>> gputimeit(@()gA*gA)
ans =
   4.8019e-04
82
Edric

この種の質問は繰り返し発生し、「Matlabは高度に最適化されたライブラリを使用する」または「MatlabはMKLを使用する」よりも、Stackoverflowで一度より明確に回答する必要があります。

履歴:

マトリックス乗算(マトリックスベクトル、ベクトルベクトル乗算、および多くのマトリックス分解と組み合わせて)は、線形アルゴリズムで最も重要な問題です。エンジニアは初期の頃からコンピューターでこれらの問題を解決してきました。

私は歴史の専門家ではありませんが、どうやら当時は誰もがFortranバージョンを単純なループで書き直していたようです。その後、標準化が行われ、ほとんどの線形代数の問題を解決するために必要な「カーネル」(基本ルーチン)が特定されました。これらの基本操作は、基本線形代数サブプログラム(BLAS)と呼ばれる仕様で標準化されました。エンジニアは、コードでこれらの標準の十分にテストされたBLASルーチンを呼び出すことができ、作業がはるかに簡単になりました。

BLAS:

BLASは、レベル1(スカラーベクトルおよびベクトルベクトル演算を定義した最初のバージョン)からレベル2(ベクトルマトリックス演算)からレベル3(マトリックスマトリックス演算)に進化し、より標準化された「カーネル」を提供しました。さらに基本的な線形代数演算の多く。オリジナルのFortran 77実装は、まだ NetlibのWebサイト で入手できます。

パフォーマンスの向上に向けて:

そのため、長年にわたって(特にBLASレベル1とレベル2リリースの間:80年代前半)、ハードウェアが変更され、ベクター操作とキャッシュ階層が出現しました。これらの進化により、BLASサブルーチンのパフォーマンスを大幅に向上させることができました。その後、さまざまなベンダーが、より効率的なBLASルーチンの実装を行いました。

歴史的な実装のすべてを知っているわけではありません(私は生まれも子供も当時ではありませんでした)が、2000年代初期に最も注目すべき2つが出てきました。IntelMKLとGotoBLASです。 MatlabはIntel MKLを使用しています。これは非常に優れた最適化されたBLASであり、優れたパフォーマンスを示しています。

行列乗算の技術的詳細:

では、Matlab(MKL)がdgemm(倍精度の一般的な行列と行列の乗算)でこんなに速いのはなぜですか?簡単に言えば、ベクトル化とデータの適切なキャッシングを使用しているためです。より複雑な用語では、ジョナサンムーアが提供する 記事 を参照してください。

基本的に、提供したC++コードで乗算を実行すると、キャッシュフレンドリーではありません。行配列へのポインタの配列を作成したと思われるため、「matice2」のmatice2[m][k]への内部ループでのアクセスは非常に遅くなります。実際、matice2[0][k]にアクセスするときは、マトリックスの配列0のk番目の要素を取得する必要があります。次に、次の反復で、matice2[1][k]にアクセスする必要があります。これは、別の配列(配列1)のk番目の要素です。その後、次の反復でさらに別の配列にアクセスします...マトリックス全体matice2が最高のキャッシュに収まらないため(8*1024*1024バイトの大きさ)、プログラムはメインメモリから必要な要素、多くの時間を失います。

マトリックスを転置しただけで、アクセスが連続したメモリアドレスにある場合、コンパイラはキャッシュ内の行全体を同時にロードできるため、コードはすでにはるかに高速に実行されます。この修正版を試してください:

timer.start();
float temp = 0;
//transpose matice2
for (int p = 0; p < rozmer; p++)
{
    for (int q = 0; q < rozmer; q++)
    {
        tempmat[p][q] = matice2[q][p];
    }
}
for(int j = 0; j < rozmer; j++)
{
    for (int k = 0; k < rozmer; k++)
    {
        temp = 0;
        for (int m = 0; m < rozmer; m++)
        {
            temp = temp + matice1[j][m] * tempmat[k][m];
        }
        matice3[j][k] = temp;
    }
}
timer.stop();

そのため、キャッシュの局所性だけでコードのパフォーマンスが大幅に向上したことがわかります。現在、実際のdgemm実装はそれを非常に広範なレベルで活用します。TLBのサイズで定義された行列のブロックで乗算を実行します(翻訳ルックアサイドバッファー、長い話:効果的にキャッシュできるもの)。処理できるデータの量だけをプロセッサにストリーミングします。もう1つの側面はベクトル化です。プロセッサのベクトル化された命令を使用して、最適な命令スループットを実現します。これは、クロスプラットフォームのC++コードでは実行できません。

最後に、StrassenのアルゴリズムまたはCoppersmith–Winogradのアルゴリズムが間違っていると主張する人々は、上記のハードウェアの考慮事項のため、これらのアルゴリズムは実際には実装できません。

161

これが理由です 。 MATLABは、C++コードで行ったようにすべての単一要素をループすることにより、単純な行列乗算を実行しません。

もちろん、自分で乗算関数を記述する代わりに、C=A*Bを使用したと仮定しています。

39
Doug Stephen

MatlabはLAPACKを少し前に組み込んだので、私は彼らの行列乗算が少なくともそれ以上のものを使用していると思います。 LAPACKのソースコードとドキュメントはすぐに入手できます。

http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.140.1785&rep=rep1&typeのGotoとVan De Geijnの論文「Anatomy of High-Performance Matrix Multiplication」もご覧ください。 = pdf

19
Jonathan Moore

答えは LAPACK および BLAS ライブラリであり、MATLABの人々が所有している独自のコードではなく、MATLABを行列演算で途方もなく高速にします。

C++コードで行列演算に LAPACK および/または BLAS ライブラリを使用すると、MATLABと同様のパフォーマンスが得られます。これらのライブラリは、最新のシステムで自由に利用できる必要があり、部品は学界で数十年にわたって開発されました。 Intel MKL などのクローズドソースを含む複数の実装があることに注意してください。

BLASがどのように高性能を得るかについての議論 ここで利用可能です


ところで、cから直接LAPACKライ​​ブラリーを呼び出すのは、私の経験にとって大きな痛みです(しかし、それだけの価値はあります)。ドキュメントを非常に正確に読む必要があります。

9
Matthew Gunn

行列乗算を行う場合、O(n^3)の時間を要する単純な乗算方法を使用します。

O(n^2.4)をとる行列乗算アルゴリズムが存在します。つまり、n=2000では、アルゴリズムには最適なアルゴリズムの最大100倍の計算が必要です。
それを実装する効率的な方法の詳細については、ウィキペディアのページで行列の乗算を確認してください。

9
Jouni Osmala

Matlabのバージョンによっては、GPUをすでに使用している可能性があります。

別物; Matlabは、マトリックスの多くのプロパティを追跡します。その対角線、hermetianなど、およびそれに基づいてアルゴリズムを専門化します。たぶん、あなたがそれを渡しているゼロ行列に基づいたその特化、またはそのようなものでしょうか?繰り返しの関数呼び出しをキャッシュしているのかもしれませんが、これはタイミングを台無しにしていますか?おそらく、繰り返し使用されていないマトリックス製品を最適化するのでしょうか?

そのような事態の発生を防ぐには、乱数の行列を使用し、結果を画面またはディスクなどに出力して強制的に実行するようにしてください。

6

MATLABは、 Intel Math Kernel Library (Intel MKL)-具体的には dgemm関数 として知られるIntelのLAPACKの高度に最適化された実装を使用します。速度このライブラリは、SIMD命令やマルチコアプロセッサなどのプロセッサ機能を利用します。彼らは、使用する特定のアルゴリズムを文書化しません。 C++からIntel MKLを呼び出す場合、同様のパフォーマンスが得られるはずです。

MATLABがGPU乗算に使用するライブラリはわかりませんが、おそらく nVidia CUBLAS のようなものです。

3
gregswiss

シャープなコントラストは、Matlabの驚くべき最適化(すでに他の多くの回答で説明されているように)だけでなく、マトリックスをオブジェクトとして定式化した方法にも起因します。

マトリックスをリストのリストにしたように思えますか?リストのリストには、リストへのポインターが含まれ、リストにはマトリックス要素が含まれます。含まれるリストの場所は任意に割り当てられます。最初のインデックス(行番号)をループしているため、メモリアクセスの時間は非常に重要です。それに比べて、次の方法を使用して、マトリックスを単一のリスト/ベクトルとして実装してみませんか?

#include <vector>

struct matrix {
    matrix(int x, int y) : n_row(x), n_col(y), M(x * y) {}
    int n_row;
    int n_col;
    std::vector<double> M;
    double &operator()(int i, int j);
};

そして

double &matrix::operator()(int i, int j) {
    return M[n_col * i + j];
}

フロップの数が同じになるように、同じ乗算アルゴリズムを使用する必要があります。 (サイズnの正方行列の場合はn ^ 3)

結果が以前の(同じマシン上で)のものに匹敵するように時間を調整してください。比較により、メモリアクセス時間がどれだけ重要かを正確に示すことができます。

2
Argyll

「matlabはxxxを他のプログラムよりも高速にする理由」に対する一般的な答えは、matlabには多くの組み込みの最適化された機能があるということです。

多くの場合、使用される他のプログラムにはこれらの機能がないため、人々は独自の創造的なソリューションを適用しますが、プロが最適化したコードよりも驚くほど遅くなります。

これは、2つの方法で解釈できます。

1)一般的/理論的な方法:Matlabはそれほど高速ではなく、ベンチマークを間違えているだけです

2)現実的な方法:このような場合、Matlabはc ++などの言語が非効率的な方法であまりにも簡単に使用されるため、実際には高速です。

2

マルチスレッドを使用していないため、C++では低速です。本質的に、すべて行列であるA = BCの場合、Aの最初の行は2番目の行などから独立して計算できます。A、B、およびCがすべてn×n行列の場合、乗算をn ^ 2の係数として

a_ {i、j} = sum_ {k} b_ {i、k} c_ {k、j}

たとえば、Eigen [ http://eigen.tuxfamily.org/dox/GettingStarted.html ]を使用すると、マルチスレッドが組み込まれ、スレッドの数が調整可能になります。

2
wsw

MATLABは、最初は数値線形代数(行列操作)用に開発されたプログラミング言語であり、特に行列乗算用に開発されたライブラリがあります。また、NOWMATLABは、これにGPU(グラフィックスプロセッシングユニット)を追加で使用することもできます。

そして、計算結果を見ると:

             1024x1024   2048x2048   4096x4096
             ---------   ---------   ---------
CUDA C (ms)      43.11      391.05     3407.99
C++ (ms)       6137.10    64369.29   551390.93
C# (ms)       10509.00   300684.00  2527250.00
Java (ms)      9149.90    92562.28   838357.94
MATLAB (ms)      75.01      423.10     3133.90

次に、MATLABが行列乗算で非常に高速であるだけでなく、CUDA C(NVIDIAのプログラミング言語)がMATLABよりも優れた結果をもたらすことがわかります。 CUDA Cには、行列乗算用に特別に開発されたライブラリもあり、GPUを使用します。

MATLABの短い履歴

ニューメキシコ大学のコンピューターサイエンス部門の会長であるCleve Molerは、1970年代後半にMATLABの開発を開始しました。彼は、学生にLINPACK(数値線形代数を実行するためのソフトウェアライブラリ)およびEISPACK(線形代数の数値計算用のソフトウェアライブラリです)、Fortranを学ぶ必要はありません。それはすぐに他の大学に広がり、応用数学コミュニティ内で強い聴衆を見つけました。エンジニアのジャック・リトルは、1983年にスタンフォード大学を訪れたときにモラーに暴露されました。その商業的可能性を認識して、彼はモラーとスティーブ・バンガートに加わりました。彼らは開発を続けるために、MATLABをCで書き直し、1984年にMathWorksを設立しました。これらの書き換えられたライブラリは、JACKPACと呼ばれていました。 2000年に、MATLABは、マトリックス操作用の新しいライブラリセットLAPACK(数値線形代数用の標準ソフトウェアライブラリ)を使用するように書き直されました。

ソース

CUDA Cとは

CUDA Cは、OPENGL(Open Graphics Library)などの行列乗算用に特に開発されたライブラリも使用します。また、GPUとDirect3D(MS Windows上)も使用します。

CUDAプラットフォームは、C、C++、Fortranなどのプログラミング言語で動作するように設計されています。DIRECT3DOPENGLなどの従来のAPIとは対照的に、このアクセシビリティにより、並列プログラミングの専門家はGPUリソ​​ースを簡単に使用できます。グラフィックプログラミングの高度なスキルが必要です。また、CUDAはOPENACCOPENCLなどのプログラミングフレームワークをサポートしています。

enter image description here

CUDA処理フローの例:

  1. メインメモリからGPUメモリにデータをコピーする
  2. CPUがGPU計算カーネルを開始します
  3. GPUのCUDAコアはカーネルを並行して実行します
  4. 結果のデータをGPUメモリからメインメモリにコピーします

CPUとGPUの実行速度の比較

インテルXeonプロセッサーX5650で64、128、512、1024、2048のグリッドサイズで50タイムステップを実行し、NVIDIA Tesla C2050 GPUを使用するのにかかる時間を測定するベンチマークを実行しました。

enter image description here

グリッドサイズが2048の場合、アルゴリズムでは、CPUでの1分以上からGPUでの10秒未満への計算時間が7.5倍短縮されます。対数目盛プロットは、グリッドサイズが小さいほどCPUが実際に高速であることを示しています。しかし、テクノロジーが進化し成熟するにつれて、GPUソリューションは小さな問題を処理できるようになり、これは今後も続くと予想されます。

ソース

CUDA Cプログラミングガイドの紹介から:

リアルタイムの高解像度3Dグラフィックスに対する市場の飽くなき需要に後押しされて、プログラム可能なグラフィックプロセッサユニットまたはGPUは、Figure 1およびFigure 2で示されるように、途方もない計算処理能力と非常に高いメモリ帯域幅を備えた高度に並列でマルチスレッドのメニーコアプロセッサに進化しました。

図1.CPUおよびGPUの1秒あたりの浮動小数点演算

enter image description here

図2。 CPUおよびGPUのメモリ帯域幅

enter image description here

CPUとGPUの浮動小数点機能の不一致の背後にある理由は、GPUが計算集約型の高度な並列計算に特化されているためです-グラフィックレンダリングが正確に何であるか-したがって、より多くのトランジスタがデータ処理に専念するように設計されていますFigure 3で模式的に示すように、データキャッシュとフロー制御ではありません。

図3。 GPUがより多くのトランジスタをデータ処理に捧げる

enter image description here

より具体的には、GPUは、データ並列計算として表現できる問題に対処するのに特に適しています-同じプログラムが多くのデータ要素で並列に実行されます-高い演算強度で-メモリー演算に対する演算の比率。各データ要素に対して同じプログラムが実行されるため、高度なフロー制御の要件が低くなり、多くのデータ要素で実行され、高い演算強度を備えているため、ビッグデータキャッシュの代わりに計算でメモリアクセス遅延を隠すことができます。

データ並列処理は、データ要素を並列処理スレッドにマップします。大きなデータセットを処理する多くのアプリケーションは、データ並列プログラミングモデルを使用して計算を高速化できます。 3Dレンダリングでは、ピクセルと頂点の大きなセットが並列スレッドにマッピングされます。同様に、レンダリング画像の後処理、ビデオのエンコードとデコード、画像スケーリング、ステレオビジョン、パターン認識などの画像およびメディア処理アプリケーションは、画像ブロックとピクセルを並列処理スレッドにマッピングできます。実際、画像のレンダリングと処理の分野以外の多くのアルゴリズムは、一般的な信号処理や物理シミュレーションから計算ファイナンスや計算生物学まで、データ並列処理によって加速されます。

ソース

高度な読書


いくつかの興味深いfacs

Matlabのものと同じくらい速いC++行列乗算を書いたが、それはいくらか注意を払った。 (MatlabがこれにGPUを使用する前)

この回答からの引用。

2
Bharata