第10回:OpenACCでできる最適化とは?

前回は、OpenACCプログラムにおけるGPU向けライブラリプログラムの呼び出し方の解説に加え、行列積のような計算量オーダーの大きな計算パターンにおいては、GPUの性能を発揮するためのプログラムの最適化を十分に行えず、満足な性能が得られない可能性があることを解説しました。

今回はここをもう少し掘り下げて、GPUプログラミングにおける最適化とOpenACCの関係性について解説します。

OpenACCでできる最適化とは?

アプリケーションプログラムを極限まで高速化しようと思ったら、CPUでもGPUでも最適化は欠かせません。

では、OpenACCで可能な最適化、不可能な最適化とはどんなものがあるでしょうか。
代表的な最適化を表にしてみましょう。

表1:OpenACCでできる最適化・できない最適化

OpenACCCUDA
Pinned memory の利用
(使うとCPU-GPU間のデータ転送速度が上がる)
△  ※1
GPUのスレッド数の調整△  ※2
L1キャッシュやレジスタ利用の効率化△  ※3
Shared memory の利用△  ※4
Shuffle 命令の利用×

※1 ・・・OpenACCとしてはサポートされてないが、PGI compiler の機能として一応使える。(その場合は全てPinned memory として扱われる)

※2 ・・・多重ループに対するスレッド割り当てをCUDAほど細かく調整することはできない。意図通りのスレッド割り当てをするためには工夫(多重ループの一重化など)が必要なケースが多い。

※3 ・・・NVIDIAのGPUはL1キャッシュのサイズを調整できる作りになっているが、OpenACCでサイズ調整はできない。

※4 ・・・OpenACCでも、限定的な状況でのみ使用できるcache指示文というものを使うと、一応shared memory が利用されるので、全く使えないというと嘘になるが、基本的には使えない。

…と、よく使われる最適化でも、ほとんどに制限がかかっています。

このうち最も影響が大きいのが、Shared memoryやShuffle命令の制限です。
これらは、スレッドブロックやWarpと呼ばれる、スレッドの小グループ内における、データのやりとりを行うための機能です。

どのくらい性能に影響を与えるか、行列積プログラムを例に見てみましょう。

まず、図1がなんの最適化も施していないコード(Baseline)です。
カーネル部分だけみると、OpenACCの実装とCUDAの実装で大差ないですね(CUDAは関数呼び出し部分で色々処理が入る)。

図1:行列積のOpenACCーCUDA実装
図1:行列積のOpenACC/CUDA実装。

図2が性能を表すグラフですが、Baselineのコードに関してはほとんど変わらないです。

図2:P100GPUにおける行列積のOpenACC-CUDA実装性能
図2:P100 GPUにおける、行列積のOpenACC/CUDA実装の性能。
(なお、コンパイラの条件を合わせるために、本結果はOpenACC/CUDA両者ともFortranにより実装し、
PGI Compilerを用いた際のものです。実装内容はC版と同等。)

一方で、図3の最適化を行ったコード(Optimized)に関してはどうでしょうか。

ここではキャッシュメモリまたはshared memoryを用いたブロッキング(ここでは詳しく解説しません。「行列積 キャッシュブロッキング」などで検索してください。)と、ループアンローリング(同じく検索してください。)と呼ばれる最適化を行っています。

図3:行列積のOpenACC-CUDA実装の最適化版
図3:行列積のOpenACC/CUDA実装の最適化版。
​(この実装は簡単のために、行列サイズNが2のべき乗であることを前提とした実装となっています。)

OpenACC版とCUDA版で、4倍程の差がついてしまっていますね。
限界まで最適化された行列積と言える、CUBLASの性能と比べると、その差はさらに大きいです。

この行列積のプログラムのように、計算量オーダーの大きな計算パターンは、各種最適化によりGPUの限界に近い演算性能を達成し得ます。
特にshared memoryなどの役割は大きく、OpenACCで限界性能を目指すのは難しいと言えます。

一方で、shared memory の利用があまり重要ではない拡散方程式のプログラムなどにおいては、CUDAとほとんど遜色のない性能が得られます(図5)

​(大変面倒くさい最適化である、テンポラルブロッキングを実装する場合には、やはりshared memoryが必要となるため、性能差は大きくなると思われる。)。

OpenACCの最適化版コードは図4です。
CUDA版もほとんど同様の実装をしています。
​最適化版の実装においても、行列積プログラム程の極端な性能差はありませんね。

図5のグラフ右軸は、メモリバンド幅性能を表しています。

図4:拡散方程式プログラムのOpenACC
図4:拡散方程式プログラムの
​OpenACC最適化版の実装。
図5:P100 GPUにおける拡散方程式プログラム
図5:P100 GPUにおける拡散方程式プログラムのOpenACC/CUDA実装の性能。

P100 GPUの場合、メモリの中でただデータをコピーする際の性能(おおよそメモリ読み書きの限界性能と思ってよい)が550GB/sec程度ですので、このプログラムはメモリ転送性能をほとんど使いきっています。

計算量オーダーの小さい計算パターンにおいては、この拡散方程式のプログラムのようにメモリの性能が重要になり、またshared memoryを利用したデータの再利用の最適化が効きづらく、従ってOpenACCでも性能が得やすい傾向にあります。

とは言え、図3や図4を見ていただければわかるように、プログラムの最適化は非常に大変です!

アプリケーション中の全てのループにこのような最適化を施していては、いつまでたってもアプリケーションの開発が終わりません。

故に私は、

  1. まずはアプリケーション全体をOpenACCで並列化
  2. 実行時間が大きい場所を探し、そこだけCUDAに切り替えて最適化

という、OpenACC+CUDAの利用を推奨しているのです!

前回解説したhost_data指示文を使えば、CUDAの関数も簡単に呼び出せます。

限界の性能を求める場合、ぜひCUDAも覚えてくださいね。

1ヵ月間有効のスパコンお試しアカウント

東京大学情報基盤センターでは、教育の一環として、制限はあるものの一ヵ月の間有効なスパコンアカウントを提供しています。

現在3つのスパコンが運用されていますが、そのうちReedbushと呼ばれるスパコンには、一世代前のものではありますがGPUが搭載されていて、OpenACCを使える環境も整っています。

自分でどんどん自習したい場合は、ご利用を考えてみてください。

トライアルアカウント申し込みページ
https://www.cc.u-tokyo.ac.jp/guide/trial/free_trial.php

< 過去の講習会の資料やプログラム公開中 >

講習会ページ
https://www.cc.u-tokyo.ac.jp/events/lectures/

講習会で用いているプログラム
https://www.dropbox.com/s/z4fmc4ibdggdi0y/openacc_samples.tar.gz?dl=0​