banner
Light

Light Log

做充满希望的动物
x
github
bilibili
steam

実験進捗が締切に追いつかず、GPUを使って異種計算を行わざるを得なかった件について(1)

前情提要#

私が会議の原稿を提出することを決めたとき、締切まで残り 1 週間しかありませんでした。しかし、以前に取得したデータは直接使用できず、最初からプログラムを設計してデータを取得する必要がありました。幸いなことに、コア計算モジュールは再利用できました。最初の 1 日目は楽観的で、実験データを取得するのに 2 日、論文を書くのに 3 日、残りの時間で修正し、締切前に提出できると見込んでいました。

しかし、実際の状況は思わしくありませんでした。過去の経験から、私は実験の実行時間を誤って見積もっていました。元々の実験では、各ラウンドで 4 つの大規模な複素行列を計算していましたが、今回はより詳細な結果を得るために、各ラウンドで計算する行列の数が 42 に急増しました。さらに悪いことに、私は 28 ラウンドの計算を予定していましたが、各ラウンドの各行列の規模は前のラウンドの 2 倍になります。28 ラウンド目には、各行列のデータ量が約 1GiB に達し、この行列を処理するためには、同じ規模の行列を 3 つ追加で保存する必要があります(説明を簡単にするために、以前の行列をコア行列と呼び、これらの 3 つの行列を補助行列と呼びます)!つまり、コア行列を処理するだけで 4GiB のスペースが必要であり、中間生成物のデータも考慮する必要があります。このような計算を 42 回行う必要があるのです(これはまだ 28 ラウンドの単一ラウンド計算プロセスに過ぎません!)......

このような計算量を 1 つの CPU コアに押し込むことはできません。そうしないと、永遠に計算し続けることになります...... 私の最初の計画はマルチコア並列計算を使用することでした。各コア行列の計算は互いに独立しており、並列計算に非常に適しているため、最初の実験では正しかったのですが、各ラウンドで関与するのは 4 つのコア行列だけであり、中間生成物を加えても私の持っている 64GB のメモリを超えることはありませんでした。他の選択肢を考慮する必要はなく、4 つの CPU コアを同時に起動すれば、約 2 時間で結果を得ることができました。

しかし、各ラウンドのコア行列の数が 42 に達すると、このような計画は通用しなくなります!まず、計算時間を短縮するために、より大きな並列数が必要です。そして、並列数を 16 に引き上げたとき、28 ラウンドの総メモリ使用量は 100GB を超えました!私はやむを得ず追加のメモリを追加しましたが、実際には、十分なメモリを追加しても、このような計算量とデータ交換量は一般的な家庭用 CPU には大きすぎました。20 ラウンド以降の各ラウンドの計算時間は時間単位であり、指数関数的に増加します...... さらに、途中で予期しない終了や出力結果が不満足な場合、プログラム設計を再検討せざるを得ず、最悪の場合は最初から数ラウンド再実行する必要があります......

私は本当に時間が数ヶ月戻ることを願っています!そうすれば、実験計画をしっかりと設計でき、計算プロセス全体を待つための十分な時間もあります。しかし、今はたとえ 2 日で論文を書き終えても、私には 4〜5 日しかありません!私は非常に複雑な最適化設計を行う余裕がありません。なぜなら、プログラムを何度もデバッグする時間がないからです。今、私の前にあるのは 1 つの道だけです:大部分のプログラムを再利用できるシンプルで粗暴な方法を見つけ、全体のフレームワークを大幅に変更することなく、指数関数的に増加する計算時間を抑える方法を考え出すことです。

この問題の核心的な考え方を見てみましょう:各ラウンドのコア行列の規模は倍増します。計算時間を線形に保つためには、各行列の計算時間も線形に保つ必要があります。したがって、最も簡単な方法は、行列の規模が倍増する場合、計算モジュールも倍増させることです。

残念ながら、私の CPU はこれを実現できません。コアは 8 つしかありません!しかし、他の手段がないわけではありません。有名なビデオを思い出してください。「流言終結者」のエピソードの中で、彼らはカラーボール銃を使って CPU と GPU の主な違いを視覚的に再現しました。もし私たちの CPU のコアが指数的な成長の要求を満たすことができないのであれば、行列に関連する計算を並列計算能力が高い GPU に任せるのは非常に賢明な選択です。GPU はまさにそのために生まれたのです!

異構計算方案#

異なる計算ユニットを組み合わせて計算する方法を異構計算と呼びます。最も典型的な例は CPU と GPU の組み合わせです。はい、この言葉は非常に高度に聞こえますが、さまざまな分野で広く使用されています。その中で最も親しみやすいのはおそらくゲームです。CPU と GPU の組み合わせは人気のある分野であり、このアプローチの普遍性のおかげで、単一の CPU アーキテクチャのプログラムを GPU と組み合わせたプログラムに変換するための多くの使いやすいツールがあります。これらのツールの中で最も代表的なのは、NVIDIA 社が自社の GPU 製品向けに提供している CUDA と、複数のメーカーの GPU をサポートする OpenCL です。近年、GPU 計算への注目が高まる中、AMD も自社の ROCm ソリューションを発表しましたが、このツールのコミュニティとエコシステムはまだ改善の余地があります。私は新しい技術を試すことに非常に前向きですが、現時点ではまず実験を完了させる必要があります!

私の自宅のコンピュータと研究室のコンピュータはどちらも NVIDIA の GPU を使用しているため、最初に考えたのは CUDA のアプローチを使用することでした。CUDA は非常に成熟したツールであり、それに対して多くの使いやすいライブラリが開発されています。特に近年、GPU が AI 分野で使用されるようになり、CUDA を簡単に呼び出すライブラリが多数登場しました。さらには、元のコードを変更せずに計算モジュールを GPU に移行できる手段もありますが、残念ながら私の現在のプログラムには適用できませんでした。最終的に私は CuPy という Python ライブラリを使用しました。これは NumPy と非常に高い類似性があり、NumPy の GPU バージョンの簡単な実装と考えることができます。これは私にとって非常に重要です。なぜなら、行列計算に関わるため、計算速度を向上させるためにプログラム内で NumPy の計算方法とデータ型を大量に使用しているからです。計算モジュールを GPU にオフロードする際に、データを過剰に整形する必要がないことが重要です。なぜなら、コードを大幅に変更する必要があるだけでなく、追加の計算オーバーヘッドも発生するからです。

CuPy の使い方は非常に簡単で、約 1 時間ほどでプログラムを修正して実行することに成功しました。ここでコード実装の詳細を過度に説明するつもりはありませんが、時間があればこれらについて別の記事を書こうと思っています(もちろん、CUDA についてより深く理解した後が望ましいです)。しかし、この時点で私は望んでいた結果を得ることができませんでした。現実は常に人々が想像するよりも複雑です。

最初の問題は依然としてデータ転送の問題です。現代の GPU は高速ストレージを使用していますが、CPU と GPU の間でデータを繰り返し交換すると、大量の時間がこれに消費されます。計算部分は常にデータが整うのを待たなければならず、データが集まった後はすぐに結果を出力しなければなりません。これは、パイプライン構造を使用したり、データをブロック転送したりすることで解決できる問題ではありません。最初の設計では、GPU は各行列要素に対していくつかの単純な計算を行うだけで済むため、各要素を単独で 2 倍にするだけだと簡単に理解できます。したがって、パイプラインのような計算構造はあまり適用できません。また、最初から計算は全体の行列の転送を待つ必要がなく、計算プロセスも大きな遅延を引き起こすことはありません。すべての問題の根源はここにあります。計算部分は十分に速いため、性能のボトルネックはデータ転送にあります。

元の設計を変更せずにこの点を変えるのは非常に難しいため、私はやむを得ず計算構造を調整し、CPU 上のデータ処理部分を GPU に移行しました。この修正は非常に効果的で、GPU の計算負荷を増加させ、CPU と GPU 間の頻繁なデータ転送をできるだけ減らしました。これにより、私の GPU の最大計算負荷は 80% に達しました。

しかし、ここで別の問題が発生しました。現在、私の GPU は各行列要素を処理するだけでなく、処理された行列を保存し、その全体を後続の処理に使用する必要があります。最初に述べたように、非常に大きな行列が必要になります。このような場合、私の GPU のメモリが不足し始めます。残念ながら、私の GPU のメモリは 4GB しかありません。SWAP を通じてシステムメモリの一部を共有できますが、この部分は頻繁なデータ転送の問題に直面します... 私が考えた手段は、行列を分割し、GPU に部分的な行列を与えて部分的な結果を取得し、それを CPU に転送して統合することです。これにより、元の方法よりは少し改善されましたが、24 ラウンド以降でもデータ転送によるボトルネックに直面せざるを得ませんでした。ハードウェアの制約は想像以上に厳しいものでした。

時間の制約から、私は元々の 28 ラウンドの計画を放棄せざるを得ず、実験結果の検証可能性と完全性をできるだけ保証しながら、計算回数を減らし、25 ラウンドの計画を選択しました。この段階で、私は 8 時間以内に必要なすべての結果を得ることに成功し、締切前に論文を完成させました(私の先生に感謝します!)。

私は引き続きプログラムの最適化を行っています。今後の方向性は 2 つあります。1 つは MPI などの手法を使用して計算負荷を複数のマシンに分散させることですが、これもデータ転送のボトルネックが待ち受けています。もう 1 つは FPGA を使用してデータ計算を行い、ホストに渡すことです。リソースが十分であれば、このアプローチはデータ転送と遅延の面で GPU よりも優れていますが、開発コストも大幅に増加します。このアプローチについてはまだ迷っています。伝統的なアプローチでは解決できない問題を解決するためにカスタマイズされたハードウェアが本当に必要だと気づく日が来るまで。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。