OpenMP — CAE用語解説
理論と物理
OpenMPの基本概念
OpenMPって、並列計算のためのものだというのは聞いたんですが、具体的に何をしているんですか?MPIとは何が違うんでしょうか?
良い質問だ。OpenMPは、共有メモリ型の並列化を実現するAPIの規格だ。1台の計算機(サーバーやワークステーション)の中で、複数のCPUコアが同じメモリ空間を共有し、協調して処理を行う。対してMPIは、分散メモリ型で、異なる計算機(ノード)間でメッセージを送受信して連携する。つまり、OpenMPは「1台の中の複数コア」、MPIは「複数台のマシン」を主な対象としているんだ。
共有メモリということは、すべてのスレッドが同じ変数にアクセスできるってことですか?それだと、同じ場所を同時に書き換えて計算がおかしくなったりしないんですか?
その通り、それが最大の課題だ。これをデータ競合(Data Race)と呼ぶ。例えば、全スレッドで共有するカウンタ変数`sum`に対して、各スレッドが`sum = sum + local_value`と同時に実行すると、更新が上書きされて正しい合計値が得られない。OpenMPでは、`#pragma omp critical`ディレクティブや`#pragma omp atomic`ディレクティブを使って、その部分の実行を排他的に制御する。あるいは、アルゴリズムを最初から競合が起きないように設計する必要がある。
規格って言われましたが、誰が決めているんですか?バージョンによって何が変わるんですか?
OpenMP Architecture Review Board (ARB)という団体が規格を策定・公開している。2023年現在の最新はOpenMP 5.2だ。バージョンが上がるごとに機能が拡張される。例えば、OpenMP 4.0でSIMD(ベクトル化)ディレクティブが正式導入され、1コア内の演算ユニットを効率的に使えるようになった。OpenMP 5.0では、タスク依存性の表現が強化され、複雑な非均一なワークロードの並列化がしやすくなった。CAEソルバーは、使用するコンパイラがサポートするOpenMPバージョンに依存する部分が多い。
数値解法と実装
CAEソルバーでの実装例
CAEの計算で、具体的にどの部分にOpenMPが使われるんですか?全体の計算が一気に速くなるわけではないですよね?
その通り、並列化できる部分とできない部分がある。並列化の効果が大きいのは、要素剛性マトリクスの生成や内部力ベクトルの計算だ。例えば、10万要素のモデルがあれば、各要素の計算は互いに独立している。ここをOpenMPの`#pragma omp parallel for`で並列化する。反復解法の前処理や行列-ベクトル積も並列化対象だ。しかし、直接法ソルバーの前進消去・後退代入や、収束判定などは逐次処理部分となり、これがボトルネックになる。この影響をアムダールの法則で見積もることができる。並列化率が95%のコードを16スレッドで実行すると、理論上の最大加速比は
`parallel for`の使い方で気をつけることは?単にforループの前に書けばいいんですか?
いや、安易に使うと逆に遅くなったり結果が壊れたりする。まず、ループの各反復が独立(independent)であることが大前提だ。i回目の計算結果がi-1回目に依存するようなループは並列化できない。次に、先ほど話したデータ競合がないか確認する。また、ループの反復数が少なすぎると、スレッドの生成・管理のオーバーヘッドが効いて逆効果だ。経験則では、スレッドあたり少なくとも1000〜10000回程度の反復が欲しい。さらに、スケジューリングの指定が重要で、`schedule(static)`(均等割り当て)か`schedule(dynamic)`(動的割り当て)かを計算負荷の均一性に応じて選ぶ。
コンパイルはどうするんですか?特別なオプションが必要?
必要だ。主要なコンパイラでは以下のオプションを付ける。
- Intel Compiler (icc/icpc): `-qopenmp` (旧`-openmp`)
- Microsoft Visual C++: `/openmp`
- LLVM Clang: `-fopenmp`
これを付けないと、プラグマは単なるコメントとして無視され、並列化されないシリアルコードとしてコンパイルされる。リンク時にもOpenMPランタイムライブラリが必要になる。
実践ガイド
性能チューニングの実際
実際にCAEソルバーを動かすとき、OpenMPのスレッド数はどう決めればいいですか?CPUのコア数と同じにすれば最大性能が出ますか?
必ずしもそうとは限らない。まず、物理コア数と論理コア数(Hyper-Threadingなどによる)を区別せよ。計算負荷が重いCAEでは、物理コア数に合わせるのが基本だ。例えば、20コア40スレッドのCPUなら、OpenMPスレッド数は20に設定する。さらに重要なのは、メモリ帯域とキャッシュだ。全てのコアがフル稼働するとメモリ帯域が飽和し、性能が頭打ちになることがある。その場合は、コア数を少し減らした方が全体のスループットが上がる場合もある。環境変数`OMP_NUM_THREADS`で設定するが、ソルバー内部で上書きされることもあるので、マニュアルを確認すること。
スレッド数を変えて計算時間を計測したら、8スレッドまでは速くなるけど、16スレッドにするとむしろ遅くなりました。なぜですか?
典型的な問題だ。原因はいくつか考えられる。第一に、メモリ帯域の飽和だ。先ほど述べた通り、全コアがメモリにアクセスし合うと帯域が足りなくなり、コアが待ち状態になる。第二に、False Sharingだ。異なるスレッドが、同じキャッシュライン上にある別々の変数を頻繁に更新すると、キャッシュの一貫性を保つためのオーバーヘッドが膨大になる。第三に、並列化の粒度が細かすぎて、スレッド管理のオーバーヘッドが支配的になっている可能性もある。性能プロファイリングツール(Intel VTune, GNU gprofなど)を使って、どこで時間がかかっているかを特定する必要がある。
OpenMPとMPIを組み合わせる(Hybrid並列)という話も聞きますが、CAEでは一般的なんですか?
大規模計算では非常に一般的だ。例えば、512コアのクラスタを32ノード×16コア/ノードで構成したとする。この時、MPIで32プロセスを立ち上げ、各MPIプロセス内でOpenMPを使って16スレッドの並列化を行う。これにより、MPIプロセス間通信の回数とデータ量を削減できる(1ノード内は共有メモリで高速にデータ交換できる)。ANSYS MechanicalやLS-DYNAの多くのソルバーは、このHybrid並列をサポートしており、ジョブ投入時に`-np`(MPIプロセス数)と`-nt`(OpenMPスレッド数)の両方を指定する。最適な組み合わせはハードウェアと問題規模に依存するので、ベンチマークが必要だ。
ソフトウェア比較
主要CAEソルバーにおけるOpenMPサポート
具体的なソフトでは、ANSYSやAbaqusはOpenMPをどう使っているんですか?ユーザーが意識して設定する部分はありますか?
ソルバー内部の実装はブラックボックスだが、ユーザーが設定できるパラメータはある。
Abaqus/Standardでは、環境ファイル(abaqus_v6.env)に`mp_routine=...`や`cpus=N`を設定する。Abaqus/Explicitはデフォルトでスレッド並列を使用する。どちらも内部でOpenMPを活用している部分が多い。
オープンソースのCAEソルバー、例えばCalculiXやCode_Asterではどうでしょうか?
オープンソースソルバーは、ユーザーがソースコードを直接コンパイルするため、OpenMPの有無や性能がコンパイル設定に直結する点が特徴だ。
Code_Asterは非常に大規模で、線形ソルバー(MUMPS, PETScなど)の並列機能に大きく依存するが、要素計算などの部分でOpenMPタスク並列も利用している。実行設定ファイル(`.export`)で`ncpus=N`を指定することで、MPIプロセス数とOpenMPスレッド数の両方を制御できる。
商用ソルバーとオープンソースで、OpenMPの使い方や性能に大きな違いはありますか?
大きな違いは、チューニングの深度と安定性だ。ANSYSやAbaqusのような商用ソルバーは、Intel CompilerやPGI Compilerなど特定のコンパイラと緊密に連携し、ハードウェアベンダー(Intel, AMD)と共同で、キャッシュ最適化や命令レベルのチューニングを何年もかけて行っている。また、様々な問題規模やアーキテクチャでテストされているので、スレッド数を増やした時の性能低下(スケーリング)が比較的予測しやすい。
トラブルシューティング
よくあるエラーとデバッグ
OpenMPを使った自作コードで、実行するたびに最終結果が微妙に違う値になります。再現性がありません。これはデータ競合が原因ですか?
ほぼ間違いなくデータ競合が原因だ。並列実行のタイミングによって、どのスレッドが共有変数を先に読み書きするかが変わるため、結果が非決定的になる。デバッグの第一歩は、スレッド数を1に設定(`OMP_NUM_THREADS=1`)して実行してみること。これで結果が常に同じで正しければ、並列化部分に問題があると断定できる。次に、Intel InspectorやValgrindのHelgrindツールのような、データ競合を検出する専用ツールを使う。あるいは、全ての共有変数へのアクセスを疑い、`critical`セクションで保護してみて、問題が解消するかどうかを試す。
商用ソルバー(例えばANSYS)で、メモリ不足エラーが出ました。スレッド数を増やすと必要なメモリも増えるんですか?
増える可能性が高い。特に直接法ソルバー(スパース直接解法)を使用している場合だ。各スレッドが作業用のバッファや、スレッドローカルなデータを持つことがある。また、行列の並列因数分解では、Fill-in(埋まり)のパターンが逐次処理と異なり、より多くの非零要素が生じることが知られている。これにより、全体として必要なメモリ量が増加する。ANSYSのドキュメントには、SMP(共有メモリ並列)使用時はメモリを最大で(スレッド数)x 0.2 〜 0.5倍多く見積もれ、と書いてあるものもある。反復法ソルバーの場合は、この影響は小さいが、前処理に依存する。
計算中に「スタックサイズが不足しています」というエラーが出て落ちました。これもOpenMPと関係ありますか?
大いに関係ある。OpenMPの並列領域(`parallel`)内で、スレッドごとに大きな自動変数(ローカル配列など)を宣言すると、それは各スレッドのプライベートスタックに確保される。デフォルトのスレッドスタックサイズ(Linuxでは数MB程度)ではすぐに不足する。これを解決するには、環境変数でスタックサイズを大きく設定する。Linuxなら`export OMP_STACKSIZE=256M`、Windowsならスレッドスタックサイズをリンカオプションで設定する。あるいは、大きなデータは`malloc`や`new`でヒープに確保するようにコードを変更する方が安全な場合もある。
並列化しているはずなのに、CPU使用率が100%になりません。一部のコアしか使われていないように見えます。なぜですか?
考えられる原因はいくつかある。第一に、コードの大部分が並列化されていない逐次領域にある。OpenMPは並列領域と逐次領域を繰り返すので、その様子をプロファイラで確認せよ。第二に、負荷の偏り。`schedule(static)`で、あるスレッドにだけ重い計算が集中し、他のスレッドは早く終わって待機している。第三に、同期(Barrier)での待ち。`#pragma omp barrier`や、暗黙のバリア(`parallel for`の終了時など)で、最も遅いスレッドを全員が待っている。第四に、I/O(ファイル読み書き)やロック(`critical`セクション)による競合で、スレッドが順番待ちをしている。性能解析ツールでホットスポットを特定することが必須だ。
関連トピック
なった
詳しく
報告