3476

チュートリアル

科学技術計算の分野で始まった GPU（Graphic Processing Unit）による汎用計算は、製造業、医療、金融など様々な分野へ広まり、モバイル、組み込みの分野でも使われ始めています。
当初は専用言語を使う必要があり GPU による汎用計算は気軽に始められるものではありませんでしたが、用途が広がると同時に、従来の言語で GPU 向けにプログラミングする環境が整ってきました。OpenACC はその中の一つで、既存のプログラムにディレクティブと呼ばれるヒント情報を追加するだけで、コンパイラが自動で GPU コードを生成する開発手法です。今回のチュートリアルでは、全4回のシリーズとして、この OpenACC を使ってアプリケーションを効率良く GPU で加速する方法を解説します。

OpenACC で始める GPU コンピューティング :データ転送の最適化

成瀬 彰

1

はじめに

しかし、前半の2ステップを適用した状態では、GPU実行が CPU 実行よりも遅いことも珍しくない。多くのGPU 搭載システムは、CPU と GPU でメモリが分離している（図2）。GPU でプログラムを実行するには、実行に必要なデータが GPU メモリ上に載っている必要がある。そのため、GPU 実行の前後で、CPU メモリと GPUメモリの間でデータ転送が必要となる。CPU と GPUは、多くの場合、PCIe[1] で接続されているが、そのバンド幅は、GPU メモリのバンド幅と比べて一桁以上遅い。CPU と GPU 間のデータ転送量が多いと、その転送に か か る 時 間 が 性 能 の ボ ト ル ネ ッ ク と な る。CPU・GPU 間のデータ転送は可能な限り減らさねばならない。

前回は、我々が推奨する OpenACC によるアプリケーション並列化サイクル（図1）の前半、「1）ホットスポットの特定」と「2）ループの並列化」、この2ステップに関して説明した。例題としたヤコビ反復法のサンプルコードは、この2ステップに従って、計算負荷の重い部分を GPU 実行できるようになった。CUDA 等のアクセラレータ言語に頼ることなく、既存の C / Fortran ソースコードにディレクティブを挿入するだけで GPU を使用できる。これが OpenACC のパワーである。

図1

OpenACC によるアプリケーション並列化のサイクル
図2

筆者紹介
なるせ あきら
1996年、名古屋大学大学院工学系研究科修士課程修了（情報工学専攻）。同年、富士通研究所に入社、大規模サーバ開発、HPC システム開発など、様々

GPU システムの構成

続きは Web で
日本計算工学会誌「計算工学（Vol.21, No.4）」HP:
http://www.jsces.org/Issue/Journal/

なプロジェクトに参加、関連するハードウェアやソフトウェアの研究開発に従事。2009年、山下記念研究賞受賞。2010年、SACSIS2010最優秀論文賞
受 賞。2013年、NVIDIA に 参 加、 シ ニ ア デ ベ ロ ッ
パーテクノロジーエンジニアとして様々なアルゴリズムとアプリケーションの GPU 向け並列化に従事、GPU コンピューティングの普及に努めている。

（26）

計算工学

チュートリアル

OpenACC で始める GPU コンピューティング : データ転送の最適化

今回は、OpenACC によるアプリケーション並列化のサイクルの3番目、「3）データ転送の最適化」を説明してゆく。
2

データ転送の最適化
ループの並列化（GPU 化）がある程度進んだら、次に取り組むのはデータ転送の最適化である。ループの並列化のステップでは、Kernels / Parallel コンストラクトを使ってプログラム内の並列化可能領域をコンパイラに指示した。このとき、コンパイラは指示領域の計算を 並 列 化 す る だ け で な く、 計 算 に 必 要 な デ ー タ をCPU・GPU 間で転送する処理を追加する。並列領域に入る前に、計算に必要な入力データを CPU から GPU に送り、並列領域から出た後に、CPU で使われる計算結果を GPU から CPU に戻す。コンパイラはソースコードを解析して、これらの処理を自動で追加する。
しかし、コンパイラが選択・追加したデータ転送処理が、常に最適（実行時間が短い）とは限らない。計算の 処 理 内 容 が 複 雑 な 場 合 は、 不 要 な デ ー タ を CPU・GPU 間で転送しているかもしれない。場合によっては、必要なデータ転送が行われていないかもしれな
い。また、データ転送に関するコンパイラの解析範囲は、各並列領域に閉じているので、異なる並列領域で同じデータを使用している場合でも、それを冗長に重複して転送しているかもしれない。
OpenACC では、CPU・GPU 間のデータ転送を制御するディレクティブとして、Data コンストラクト、Dataクローズ、Update ディレクティブ等が用意されている。以下、それぞれの特徴や使い方を説明する。
2.1 データ領域（Data コンストラクト）
Data コンストラクトは、複数の並列領域にまたがるデータ共有を促進するディレクティブである。Data コンストラクトで括られた部分はデータ領域と呼ばれる。
以下、Fortran と C / C++ それぞれの、Data コンストラク
トの使用例をコードサンプル（以下 CS）1と2に示す。

CS 1：data コンストラクト使用例（Fortran）

計算工学

CS 2：data コンストラクト使用例（C / C++）
この例では、data コンストラクトを追加することで、2つの並列領域間で、配列 x と y を共有することが可能となる。Data コンストラクトが無い場合、2つの並列領域の間で、CPU・GPU 間で配列 x と y のデータ転送が行われるが、CS 1、CS 2のように Data コンストラクトを追加してデータ領域を指示することで、この冗長なデータ転送を取り除くことができる。ただし、この状態では、どのデータをどのように転送するのかは、依然としてコンパイラのコード解析能力に依存しており、まだ、最適なデータ転送方法が選択されることが保証された訳ではない。
なお、データ領域の開始点と終了点は、同一スコープ、つまり、同一関数内、同一サブルーチン内になければいけない。データ領域内から関数・サブルーチンを呼び出すことは可能であり、その場合は、呼び出し先の関数・サブルーチンもデータ領域内にいる関数・サブルーチンとみなされる。また、複数のデータ領域を Data コンストラクトで纏めて、上位層のデータ領域を作ること、つまり、階層的なデータ領域の作成も可能である。
2.2 Data クローズ
Data クローズは、どの配列を、どのタイミングで、どのように転送するのかを、プログラマーがコンパイラに指示するときに使用するディレクティブである。
Data クローズは単独ディレクティブではなく、Data /Kernels / Parallel コンストラクトに追加する形で使用する（Kernel / Parallel コンストラクトが形成する並列領域は、暗黙的なデータ領域なので、data クローズを追加で き る）。 以 下 に、OpenACC で サ ポ ー ト さ れ て い る
Data クローズの名前と意味を示す。
-------------------・copy：Read / Write 配列に使用する。
データ領域に入るときに、GPU メモリ上に配列スペースを確保し（ALLOC）、配列データを GPU に送る（DATA-IN）。データ領域から出るときに、配列データを Host に戻し（DATA-OUT）、GPU メモリから配列スペースを削除する（FREE）。

（26-2）

Vol.21, No.4 2016

チュートリアル

OpenACC で始める GPU コンピューティング : データ転送の最適化

・copyin：Read-Only 配列に使用する。
データ領域に入るときに、GPU メモリ上に配列スペースを確保し（ALLOC）、配列データを GPU に送る（DATA-IN）。データ領域から出るときに、GPU メ
モリから配列スペースを削除する（FREE）。
・copyout：Write-Only 配列に使用する。
データ領域に入るときに、GPU メモリ上に配列スペースを確保する（ALLOC）。データ領域から出ると き に、 配 列 デ ー タ を Host に 戻 し（DATA-OUT）、GPU メモリから配列スペースを削除する（FREE）。
・create：Temporal 配列に使用する。
データ領域に入るときに、GPU メモリ上に配列スペースを確保する（ALLOC）。データ領域から出る
ときに、GPU メモリから配列スペースを削除する（FREE）。
・present：既に GPU メモリに載っている配列、主に、上位のデータ領域で既にデータ転送方法が指定済みの配列に使用する。動作は発生しないが、その配列が GPU メモリ上に存在しないとエラーになる。
・deviceptr：OpenACC のメモリ管理の枠外で、GPU メ
モ リ 上 に 確 保 さ れ た 配 列 に 対 し て 使 用 す る。
OpenACC を、CUDA 等の他プログラミングモデルと併用するときに使用することがある。
-------------------Data クローズの一つ、copy の使用例を CS 3と4に示す。この例では、2つの並列領域にまとめたデータ領域に対して、配列 x と y の「copy」制御を指示している。
従って、このデータ領域に入るときに配列 x と y のデータが GPU に送られ、データ領域から出るときに配列 xと y のデータが CPU に戻される。

CS 3：data クローズの使用例（Fortran）

計算工学

CS 4：data クローズの使用例（C / C++）
上説の data クローズに加え、OpenACC 1.0と2.0には、
present_or_*（* の部分には copy、copyin、copyout、createが 入 る）と い う data ク ロ ー ズ が 存 在 す る。 例 え ばpresent_or_copy の場合、まず、その配列が GPU メモリ上に存在するかどうかのチェックが行われる。もし配列が存在すれば何も行われないが、もし配列がなければcopy 動 作 が 行 わ れ る。 た だ し、OpenACC 2.5か ら、present チェックが全ての data クローズの基本動作となった た め、OpenACC 2.5対 応 の コ ン パ イ ラ な ら present_or_* クローズを使用する必要はない（互換性維持のため、OpenACC 2.5以降でも present_or_* クローズは使用できる）。
配列形状の指定：
Data クローズに配列名のみを記述すると、その配列の全データが制御の対象となる。Fortran では、配列形状がプログラムに明記されるのが一般的なので、多くの場合、コンパイラは普通に配列形状が分かる。しかし、C / C++ では、配列はポインターとして渡されることが多く、コンパイラは配列形状が分からないことが多い。配列形状・サイズが分からないと、データ転送量を決定できない。また、配列の全データではなく、その一部分しか使わないプログラムの場合、配列の全データを CPU・GPU 間で転送するのは、リソースの浪費である。
OpenACC では、配列のサイズや、Data クローズの制
御 対 象 と す る 部 分 配 列 を 記 述 す る こ と が で き る。
Fortran と C / C++、それぞれの配列形状の記述方法を以下に示すが、Fortran と C / C++ では記述方法が異なるので、注意してほしい。
-------------------・Fortran：array(start:end)
start は配列の最初の要素番号で、end は最後の要素番号を示す。従って、要素数は end – start + 1となる。
・C / C++：array[start:count]start は配列の最初の要素番号で、count は要素数を示す。従って、配列の最後の要素番号は start + count – 1となる。
--------------------（26-3）

Vol.21, No.4 2016

チュートリアル

OpenACC で始める GPU コンピューティング : データ転送の最適化

CS 5：最適な data クローズの使用例（Fortran）
ここまでの情報を踏まえて、CS 3と4のケースを再考する。前提として、配列 x と y のサイズは N より大きく、配列 x は temporal 配列（GPU 上でしか使われない配列）とする。Temporal 配列は、CPU・GPU 間でデータ転送する必要がないので、Data クローズは create を選択するのが良い。配列 y は、元の値を参照していない、つまり、CPU から GPU にデータを送る必要がないので、copyout を使えば良い。配列 x と y は、どちらも N 個の要素しか使用されないので、配列形状を指定すると、転送量を削減できる。この方針に基づいて data クローズを最適化した結果を CS 5と6に示す。

CS 7：enter / exit data ディレクティブの使用例（Fortran）
Enter data ディレクティブで指定された配列は、そのタイミングで GPU 上に必要なメモリスペースが確保される。使える data クローズは、create と copyin である。
Copyin が使われた場合は、同時にデータが CPU からGPU に送られる。
Exit data ディレクティブで指定された配列は、そのタイミングで、GPU 上からその配列のメモリスペースが 削 除 さ れ る。 使 え る data ク ロ ー ズ は、copyout とdelete（exit data ディレクティブ専用）である。Copyoutが使われた場合は、同時にデータが GPU から CPU に戻される。

CS 6：最適な data クローズの使用例（C / C++）2.3

非 構 造 デ ー タ 区 間（enter / exit data デ ィ レ クティブ）
Data コンストラクトで作成できるデータ領域は、その開始点と終了点が同一スコープ内になければならない。
しかし、プログラム・配列によっては、データを GPUに転送したいタイミング（開始点）と、データを GPU から回収したいタイミング（終了点）が、同一スコープにないこともあるだろう。そのような場合に便利なのが、OpenACC 2.0か ら 採 用 さ れ た enter / exit data デ ィ レ クティブである。CS 7と8に、その使用例を示す。

計算工学

CS 8：enter / exit data ディレクティブの使用例（C / C++）
2.4 Update ディレクティブ
GPU からのみ使われる配列（CPU で使われない配列）を、プログラムの上位層の関数・サブルーチンから、GPU メモリに常駐させるのは、性能的には良いアプローチである。しかし、基本的には GPU からのみ使われる配列でも、例えば、ファイル IO やノード間通信のために、時々CPU から参照・更新せざるを得ないこともあるだろう。このような場合に有用なのが Update（26-4）

Vol.21, No.4 2016

チュートリアル

OpenACC で始める GPU コンピューティング : データ転送の最適化ディレクティブである。
Update ディレクティブは、GPU メモリ上にある配列の値を、データ領域内のどこからでも、CPU 上の該当部分へ転送することができる。反対に、CPU 上で更新した配列の値を、GPU 上の該当部分に転送することもできる。
Update ディレクティブは、Self クローズ、もしくは、Device クローズと併用する。Update self ディレクティ
ブでは、指定した配列の値が GPU から CPU へ転送され、Update device ディレクティブでは、反対に、CPUから GPU へ転送される。文字通り、Update self ではセルフ（Host、CPU）が更新され、Update device ではデバイス（GPU）が更新される、と覚えれば良いだろう。
Update デ ィ レ ク テ ィ ブ の 使 用 例 を、CS 9と10に 示す。この例では、データ領域内に3つのループがあるが、最初と最後のループは GPU で実行、2番目のループは CPU で実行するようになっている。最初のループの実行が終わると、GPU 上の配列 x は更新されるが、その更新は Host 上の配列 x には反映されない。このまま2番目のループを CPU 上で実行すると、計算結果がおかしくなる。この例では、ここに Update self ディレクティブが入っており、配列 x の値が GPU から CPU へと転送されるので、2番目のループを CPU 上で実行して も 問 題 な い。 同 様 に、2番 目 の ル ー プ の 後 に は、Update device ディレクティブが入っており、配列 x の値が CPU から GPU へと転送されるので、最後のループも問題なく GPU で実行できる。

CS 10：update ディレクティブの使用例（C / C++）
なお、Update ディレクティブは、Enter data ディレク
ティブを使って GPU メモリ上に生成した配列に対しても適用可能である。もちろん、配列形状を指定して、配列の一部を Update ディレクティブで転送させることもできる。
2.5 ヤコビ反復法のデータ転送の最適化
ここまで説明したデータ転送に関連するディレクティブを使って、ヤコビ反復法のデータ転送を最適化してみよう。
現状確認：
まずは現状の把握である。前回のチュートリアル
で、2）ループの並列化まで適用したヤコビ反復法のコードを、pgprof[2] でプロファイリングした結果を以下に示す（関連部分のみ抽出）。

CS 9：update ディレクティブの使用例（Fortran）

計算工学

jacobi_* が GPU 上の計算処理（GPU カーネル）を示しているが、この3つを合わせても、全実行時間に占める割合は5% 未満である。実行時間の大半、95% 以上を占めているのは、[CUDA memcpy HtoD] と [CUDA memcpy
DtoH] である（HtoD は CPU から GPU へのデータ転送、
DtoH は GPU か ら CPU へ の デ ー タ 転 送）。 明 ら か に、GPU 上の計算ではなく、CPU・GPU 間のデータ転送が性能ボトルネックになっている。データ転送の最適化が必要な状況である。

（26-5）

Vol.21, No.4 2016

チュートリアル

OpenACC で始める GPU コンピューティング : データ転送の最適化

参考までに、NVIDIA Visual Proﬁler(nvvp) [3] による解析結果（時系列情報）を図3に示す（nvvp を使うと、統計情報だけでなく、このようにイベント間の時系列関係を確認できる。nvvp には他にも様々な機能が存在するが、ここではその説明は割愛する）。これ見ると、各GPU カーネル実行の前後で、データ転送が行われていることが分かる。

図3

データ転送を最適化する前のヤコビ反復法の時系列プロCS 12：データ転送を最適化したヤコビ反復法（C / C++）

ファイリング結果

ディレクティブ追加：
ヤコビ反復法のサンプルコードのメインループには、2つの並列領域が含まれているが、その間でデータが共有されていないことが、ここでは一番の問題である。その解決には、Data コンストラクトを使って、この2つの並列領域を含むデータ領域を設定すればよい。
このケースでは、while ループの外側にデータ領域を設定するのが適切である。While ループの内側にデータ領域を設定することもできるが、その場合は、while ループの iteration 間でのデータ共有のチャンスを失うことになる。

次に、各配列をどう制御するのが適切かを考える。
ここでは配列 Anew と配列 A の2つの配列が存在する。
ま ず、 配 列 Anew に つ い て 考 え る。while ル ー プ の 各
iteration で 配 列 Anew に 出 力 さ れ た 値 は、 そ の 次 のiteration で使われることがない。また、while ループの最終 iteration で出力された値が、while ループを出た後で使われることもない。つまり、配列 Anew は、この
while ル ー プ 内 に 閉 じ た Temporal 配 列 で あ り、data ク
ローズには create を適用するのが適切である。次に、配列 A だが、こちらは while ループに入る前の値が参照されており、while ループ内で配列 A に出力された値は、while ループから出た後も必要である。従って、配列 Aには copy を適用するのが適切である。この考察に基づいてディレクティブを追加したコードサンプルを CS 11と12に示す。
性能確認：
ソ ー ス コ ー ド を 変 更 し た ら、 性 能 確 認 で あ る。
Pgprof のプロファイリング結果は以下の通りで、狙い通り、データ転送時間を大幅に削減できている。

CS 11：データ転送を最適化したヤコビ反復法（Fortran）

計算工学

Nvvp による解析結果を図4に示す。この結果からも、GPU 上での計算処理間のデータ転送時間が大幅に削減できていることが分かる。転送時間は短くなっているが、依然としてデータ転送は行われている。これは、変数 error に関わるデータ転送である。1つめのループ（26-6）

Vol.21, No.4 2016

チュートリアル

OpenACC で始める GPU コンピューティング : データ転送の最適化

では、変数 error の縮約計算（reduction）が GPU 上で行われているが、その値は while ループの各 iteration で CPUに回収する必要がある。Nvvp を使うと、このようなプログラムの挙動も比較的簡単に推測・確認できるので、挙動確認には nvvp 使用を推奨する。

図4

データ転送を最適化した後のヤコビ反復法の時系列プロファイリング結果

計算工学

3

おわりに

今回は、我々が推奨する OpenACC によるアプリケーション並列化サイクルの3番目のステップ、3）データ転送の最適化を説明した。サンプルとして用いたヤコビ反復法は、このステップの適用後に、GPU 使用に相応しい性能が得られるようになった。このように、2）ループの並列化を適用して GPU 実行したら、むしろ性能が低下、しかし、3）データ転送の最適化を適用することで、性能が大幅に向上という事例を、我々はこれまでに多数見てきた。同様の状況になっても、あきらめずに、3）データ転送の最適化に取り組んで欲しい。
次回は、OpenACC によるアプリケーション並列化サイクルの最後のステップ、4）ループの最適化について説明する。
■参考文献
[1]
[2]
[3]

（26-7）

PCIe: https://ja.wikipedia.org/wiki/PCI_Express
pgprof: http://www.pgroup.com/doc/pgprofug.pdf
NVIDIA Visual Proﬁler (nvvp):
https://developer.nvidia.com/nvidia-visual-proﬁler

Vol.21, No.4 2016