PGI Accelerator Compiler 製品の OpenACC の準拠レベルは以下のようになっている。
PGI 14.1 以降の機能追加については 10 章を参考のこと。なお、本プログラミング・ガイドは、OpenACC 2.0/2.5 仕様を基にドキュメント化している。
PGI Accelerator コンパイラを使って、OpenACC プログラムをコンパイルするためのオプションを説明する。OpenACC に構文を解釈し、アクセラレータ用のコンパイルを行うためには、 -acc オプションを必要とする。また、コンパイルとリンクを別々に行う場合は、リンク時にも -acc オプションを必要とする。なお、OpenACC 用コンパイル・オプションの詳細については、こちらのページをご参照のこと。以下は、PGI 14.1 以降のオプションを使用した例です。
● Fortranコンパイルの一例 pgfortran -O2 -Minfo=accel -acc test.f90 -o {executable名} -acc の他に、-ta=tesla or -ta=radeon のサブオプションを付加することも可能 pgfortran -O2 -Minfo=accel -acc -ta=tesla,cuda5.5,cc20,kepler test.f90 -o {executable名} ● C11 コンパイルの一例 pgcc -O2 -Minfo=accel -acc test.c -o {executable名} -acc の他に、-ta=tesla or -ta=radeon のサブオプションを付加することも可能 pgcc -O2 -Minfo=accel -acc -ta=tesla,cc35,keep test.c -o {executable名} ● C++ コンパイルの一例 pgc++ -O2 -Minfo=accel -acc test.cpp (Linux only GNU g++ ABI互換コンパイラ)
OpenACC の実行モジュール(executable)を実行する際に利用可能な環境変数を以下に示す。
export ACC_DEVICE_TYPE=NVIDIA
export ACC_DEVICE_TYPE=RADEON
setenv ACC_DEVICE_TYPE NVIDIA
setenv ACC_DEVICE_TYPE RADEON
export ACC_DEVICE_NUM=1
setenv ACC_DEVICE_NUM 1
export ACC_NOTIFY=1
setenv ACC_NOTIFY 1
(メッセージ一例)
launch kernel file=/home/kato/GPGPU/OpenMP/jacobi4.F function=jacobi line=229 device=1 grid=2500 block=128x4
export PGI_ACC_NOTIFY=2
setenv PGI_ACC_NOTIFY 2
(メッセージ一例)
upload CUDA data file=acc_f2a.f90 function=main line=37 device=0 variable=a bytes=4000000
download CUDA data file=acc_f2a.f90 function=main line=41 device=0 variable=r bytes=4000000
export PGI_ACC_TIME=1
setenv PGI_ACC_TIME 1
export PGI_ACC_BUFFERSIZE=32M (32MBを指定した場合)
setenv PGI_ACC_BUFFERSIZE 32M
C、C++ では、OpenACC ディレクティブは、#pragma を使って指定される。その指定方法は以下の形をとる。directive-name(構文名)は必ず一つ指定すること。その後の clause(節)は任意で指定することが可能で、複数の clause を指定する場合はカンマあるいは空白で区切る。clause(節)とは、構文の細かな機能を指定するためのオプションと考えれば良い。なお、以下に記載している「構造化ブロック」の意味については、こちらを参照のこと。
#pragma acc directive-name [clause(節) [[,] clause]…] { 構造化ブロック } (一例) #pragma acc kernels copy(a) for( int j = 1; j < n-1; j++) { ..... }
Fortran では、OpenACC ディレクティブは、!$acc を使って指定される。その指定方法は以下の形をとる。! は、Fortran 言語のコメント用のプリフィックスである。これを先頭に付けて "!$acc" は一つの単語として指定する。この文字の間に空白を入れてはならない。なお、Fortran の自由形式で記述されたプログラムの場合、"!$acc" の前が空白かタブである限り、どのカラムから記述を開始しても良い。
72 カラム固定形式で記述されたプログラムの場合は、"!$acc"、"c$acc"、"*$acc" のいずれかを 1~5 カラムまでに記述することが必要である。6 カラム目は継続記述子用のカラムとなる。directive-name(構文名)は必ず一つ指定すること。その後の clause(節)は、任意で指定することが可能で、複数の clause を指定する場合は、カンマあるいは空白で区切る。
!$acc directive-name [clause(節) [[,] clause]…] 構造化ブロック !$acc end directive-name (一例) !$acc kernels copy(a) do i = 1, n do j = 1, m ... end do end do !$acc end kernels 72カラム固定形式で記述されている場合は、以下の形態となる。 !$acc directive-name [clause [[,] clause]…] c$acc directive-name [clause [[,] clause]…] *$acc directive-name [clause [[,] clause]…]
Fortran において OpenACC ディレクティブを継続する場合は、以下のような形態をとる。Fortran では、FORTRAN77 以前の 固定形式(実行文は7カラム目から 72カラム目までと言う書式)と Fortran90 以降の自由形式の二つの記述書式がある。古いレガシーなプログラムでは、固定形式で記述したプログラムが多いと思うが、この二つの書式の違いによって、アクセラレータ・ディレクティブを「継続」するための書式の書き方が異なる。
一般に、FORTRAN77 スタイルの固定形式の場合は、その継続行の示し方は 6カラム目に任意の文字を入れる方法となる。!$acc のディレクティブでも同様で、その clauses を複数行に渡って定義する場合は、!$acc の継続を行わなければならない。その方法は以下の通り。6カラム目に「継続文字」を入れて、継続することを指示する。ここでは、一例として "+" 文字を継続文字として使っている。
!$acc kernels !$acc+copyin (...) !$acc+copyout(...) 以下のように、7カラム目に空白欄を入れた方が見えやすいかもしれない。 !$acc kernels !$acc+ copyin (...) !$acc+ copyout(...)
一方、Fortran90 以降の自由形式の記述形式の場合は、一般に、継続する際の文字として "&" を使用する。継続する行の前の行の末尾に "&" を入れることで、継続することを指示する。!$acc のディレクティブでも同様で、末尾に "&" を入れると、次の行は継続することを意味する。
!$acc kernels & !$acc copyin (...) & !$acc copyout(...)
PGIコンパイラでは、 固定形式のソースファイルであるか、自由形式のソースファイルかは、.f(.F) の suffix か、.f90 or .f95 等の suffix かによって区別する。.f(.F) の場合だけは、固定形式となる。必ず、72 カラム形式で記述したプログラムでなければならない。.f90 (.F90, F95) の場合は、固定形式あるいは自由形式のどちらの書式であってもソースファイルとして認識する。
これからの OpenACC ディレクティブの説明の中に、「カーネル」あるいは「カーネルコード」という言葉が頻繁に現れる。この言葉の意味を最初に説明しておく。以下の図で説明しているとおり、「カーネル」とはアクセラレータ側で動作するコードのことを意味する。OpenACC の場合、並列化の対象とする部分は「ループ」であり、この部分をコンパイラは procedure(ルーチン)として切り出し、アクセラレータの並列構造に応じたデバイス・コードとして生成する。
CPUホストからアクセラレータ側に処理をオフロードするために策定された OpenACC ディレクティブの全体像を掴むために、この章では主なディレクティブの機能について概説する。プログラミングの前に、OpenACC ディレクティブとして三つの構文を覚えることから始めることをお勧めする。以下の図を見て欲しい。
①は、Accelerator Compute 構文で、アクセラレータ上にオフロードするループ対象部分を指定するためのディレクティブである。当該対象とするものは、ベクトル化・並列化可能なループであり、ループ文の直前にディレクティブを置くことにより、コンパイラは、自動的にアクセラレータ用の並列化コードを生成する。
②は、Data 構文で、ホストとアクセラレータ間のメモリデータの転送(場所)を明示的に指示するためのディレクティブである。このディレクティブを使って適切な場所でデータ転送を行うようにすることは、OpenACC を使ったプログラミングにおいて最も重要な作業タスクとなる。
③は、Loop 構文で、①の Accelerator Compute 構文で指示したループに対して、当該ループのベクトル長や並列分割の方法をユーザが明示的に指示するために使用される。これを使って並列実行単位をハードウェアの並列演算コアにマッピングする際の分割等の調整を行うことができる。一般的には、性能をチューニングする際に使用する。
ユーザが行うプログラミング・タスクとして、上記の①~③を具体的に述べると以下のようなことになる。
アクセラレータ上に処理をオフロードする対象部分に関する概要は、1章において説明した。基本的に「Do / For」ループを対象とするが、そのループ構造はネスティングの状態により以下の図に表すような形態となる。ループ内の処理においてデータ依存性がなければ、OpenACC 並列化の対象とすることが出来る。当該ループ文の直前に OpenACC Accelerator Compute 構文を挿入することにより、コンパイラはアクセラレータ上の並列化コードを作成する。
実際のプログラムの流れを例にとり、OpenACC Accelerator Compute 構文を挿入する部分を具体的に示してみよう。以下の図に示したプログラムは、CG 反復法のプログラムを一部切り出したものである。一番外側のループ内に、個々に線形計算を行う並列化可能なループが順番に記述されている。こうした場合は、個々のループの直前に OpenACC Accelerator Compute (kernels や parallel) 構文を挿入する形となる。
OpenACC 1.0 の仕様においては、ループ内に procedure call が存在する場合、そのループは OpenACC による並列化はできない。例えば、以下の図に示すような形態である。k ループ内には、複数の i と j のネスト・ループが存在しているが、ループ内で cfftz と言うサブルーチンを call している。こうした場合は、OpenACC 1.0 においては、cfftz ルーチンのソース内容をインライン展開して並列化するしかない。しかし、OpenACC 2.0 の仕様では、この制約がなくなる。ループ内部に procedure call があっても OpenACC による並列化が出来るようになる。なお、OpenACC 2.0 に準拠したコンパイラは、PGI の場合、PGI 2014 以降でリリースされる予定である。
並列化対象となるループの前に、Accelerator Compute 構文を挿入することにより、コンパイラはアクセラレータ用のコードを作成する。下図に、単純なシングルループを有する Fortran プログラムの一部を示した。こうした単純なループは、そもそも CPU 側で処理するよりも遅くなるのが一般的であるが、ここでは単に説明の目的のために使用することにする。以下の例では、Accelerator Compute 構文として kernels 構文を使用している。Fortran の場合は、対象となるループを !$acc kernels ~ !$acc end kernels のディレクティブで囲むだけである。コンパイラはこのヒントをもとに、当該ループのアクセラレータ用の並列コードのみならず、ホスト~アクセラレータ間のデータ転送のためのコードも生成する。最低限、ユーザは Accelerator Compute 構文を指示するだけで、アクセラレータ用のコードを生成することが出来る。以下で説明する ② の Data 構文や ③ の Loop 構文は、必要に応じて指定するものであり、特に Data 構文は、本来「個々のループ本体」に対して指定するために使うものではない。
Accelerator Compute 構文は、以下に示すように「parallel 構文」と「kernels 構文」の二つが用意されている。この機能の違いに関しては、改めて 6 章で説明する。ここでは簡単に説明しておこう。
Data 構文は、前述の通り「個々のループ本体」に対して指定するものではないが、ユーザがデータ転送の指示を「明示的に行う」ためのものであることを示すために、以下の図のシングルループに対しても、あえて data 構文を挿入した様子を示した。この場合、kernels 構文の前に指定する。!$acc data に続いて、データ転送の属性を指示する「clause (節)」を指定する。copyin(a(1:n)) と言う節は、ホスト側からアクセラレータ側へ a 配列の 1 から n までの要素をコピーすることを指示するものである。この copyin 節を指定した場合、!$acc end kernels の時点では、アクセラレータ側の a 配列の内容をホスト側に戻すことは行わない。プログラムを見ての通り、a 配列は、ループ内で参照のみ行われる配列であるため、ホスト側にその値を戻す必要がない。もう一つ、copyout(r) と言う節は、アクセラレータ上に r 配列をアロケートし、計算した結果を格納しているアクセラレータ側の r 配列の全要素の内容をホスト側にコピーせよと言う意味となる。 in と out と言う言葉は、アクセラレータから見ての方向性を意味している。また、単に copy 節で指定した場合は、in/out の両方向でコピーを行うと言う意味となる。
アクセラレータへデータ転送を行うタイミングは、data 構文が指定された時点で行われ、アクセラレータからホスト側へのデータ転送を行うタイミングは、Fortran の場合は、!$acc end data を指定している時点、C/C++ の場合は #pragma acc data のブロック範囲の終了時点で行われる。
data 構文の節 (clause) には、上述したとおり copy / copyin / copyout 等がある。これらは、二つのメモリ間のデータコピーの転送方向を表すものである。さらに、present と言う節と create と言う節がある。前者の意味は、「すでにデバイスメモリ上にデータは存在しているので、データのコピーは必要ない」と言うことをコンパイラに指示するものである。後者の create 節は、「デバイス側で一時的に使用する配列データであるため、デバイスの上だけで配列をアロケートして使用するものである」と言うことを指示するものである。data 構文はこうした節を指定して、明示的にデータの転送のコントロールや明示的なデータ割付、デバイス上に当該データが存置されているかどうかの確認等の処理を指示するために使用される。
loop 構文は、Accelerator Compute 構文の後に指定する。kernels 構文を使用した場合は任意であるが、parallel 構文を使用した場合は、この loop 構文を指定したループが work-sharing を開始する場所となるため必須となる。この構文は、ループのベクトル長や並列分割の方法等をユーザが明示的に指示するために使用される。work-sharing と言う言葉を初めて聞く人もいるかと思う。この言葉は、OpenMP のプログラミングモデルでも良く使う概念である。これは、「分割した処理を複数の実行主体(スレッド等)が各々分担して、真に並列処理を行う」ことを意味する。OpenMP でも OpenACC でも Parallel リージョンが開始されてから、全ての実行主体が同じプログラムを冗長に実行し始める。その後、loop ディレクティブ等で指定された work-sharing ループに到達した時点で、初めて並列実行のモードとなる。
kernels 構文配下で loop 構文を使用した場合、コンパイラは自動的に並列実行単位をハードウェアの並列演算コアにマッピングするが、プログラムの特性によっては、コンパイラが決めた並列分割の方法やベクトル長の指定が最善ではない場合がある。こうした場合を想定して、ユーザは明示的に並列性能に関わるパラメータを各種「節」を指定して指示できる。下図の中の ③ の !$acc loop gang(32),vector(64) は、gang 分割 32 でベクトルの長さを 64 と言う単位で実行する並列分割を行えと言う意味となる。このディレクティブについては、性能チューニングする際に試行錯誤で gang/vector 値を変えながら、性能変化があるかどうかを確認することをお勧めする。loop 構文の clause の詳細に関しては、後章で説明する。
loop 構文の clause(節)を以下に示す。なお、OpenACC 2.0 において、auto、tile、device_type が追加された。loop 構文の詳細説明は、8 章を参考されたし。
今まで Fortran の例で説明したが、C プログラムの場合は、同じように #pragma を用いたディレクティブの設定を行う。
loop 構文の中に、gang、worker, vector と言う clause(節)がある。この三つは、OpenACC の実行モデルの中で使用される「並列性の概念」である。この言葉の意味を説明する前に、一般的なアクセラレータ(NVIDIA に限らない)のハードウェアが備えている parallelism(並列性)の話をしよう。アクセラレータは、2 もしくは 3 つのレベルの並列性に対する実行モードを有している。一つは、ほとんどのアクセラレータがサポートする並列性であるが、「粗粒度の並列性(coarse-grain)」がある。これは、デバイス内に実装されている個々の「実行ユニット」を使って、完全に独立して並列実行を行うモードを言う。但し、粗粒度の並列処理を行っている実行ユニット間では、その同期処理の機能は限定されている。もう一つの並列性として「細粒度の並列性(fine-grain)」があるが、これも多くのアクセラレータがサポートしている。このメカニズムは、一つの実行ユニット内でマルチスレッド実行による方法で実現されている。典型的には長いメモリアクセスの遅延を隠すために、実行するスレッドを頻繁にスイッチすることにより、実行可能なスレッドを常にアクティブにして、実行多重度を増やし、かつ実行ストールを隠す形で実現される。そして三つ目の並列性として、ほとんどのアクセラレータが実装している機能であるが、一つの実行ユニット内で 「SIMD あるいはベクトル処理」をサポートしている。
以下の図は、gang、worker, vector の概念を説明したものである。特に、OpenACC 2.0 の仕様において、これらの概念が明確に定義されたので、ここでは OpenACC 2.0 に準拠した形で説明する。gang は大きなタスク(粗粒度の並列性)のレベルで完全な並列実行を実現する並列単位である。これは、ハードウェア上の同期機構のない「実行ユニット」毎に独立に実行される単位となる。worker は、同期機構のある「実行ユニット」内で細粒度並列性の実行を実現する並列単位とも言える。各 gang は一つ以上の worker を有する。vector は worker 内部で実行される SIMD あるいはベクトル処理のための並列単位となる。
上記のように OpenACC 2.0 において gang, worker, vector の定義を行った場合、実際のプログラム上では、以下に示すように gang はネストループの一番外側で指定する clause となる。また、vector は、一番内側のループで指定できる clause と言うことになる。また、OpenACC 2.0 では、gang ループの内側には、gang clause を含んだループがあってはならない。同様に、worker ループの内側に gang、worker clause のループがあってはならない。さらに、Vector ループの内側には、gang、worker、vector clause のループがあってはならないと言う約束事ができた。OpenACC 1.0 仕様においては、この辺りの定義が曖昧であったため、gang の内側に gang ループがあっても良かった。もし、現在こうしたディレクティブ実装を明示的に行っている場合は、OpenACC 2.0 準拠になってから変更する必要がある。
さらに、OpenACC 2.0 では、gang、worker, vector の実行モードを明確に定義した。これは、OpenACC 1.0 において、parallel 構文における実行モードの定義が曖昧であったために行ったものである。parallel 構文では並列実行時に、gang による「冗長実行(gang redundant = GR mode)」で開始されて、work-sharing を行う loop 構文の実行時点で、gang 並列モード(gang partitioned mode = GP mode) が実行される。gang の場合と同様に worker、vector 実行モードにおいても、同じように worker-single(WS mode) / vector-single(VS mode) と言う言葉で、work-sharing されていない場合の実行モードを表現する。この場合は、一つの worker / vector lane だけがアクティブに実行される。並列に実行するモード、すなわち、work-sharing のモードに移行した場合、worker partitioned mode(WP mode) / vector partitioned mode(VP mode) と言う並列の実行形態となる。
もう少し、具体的に gang~worker~vectorへの partitioned mode に遷移する際の動きを具体的に説明しよう。先に述べたように、gang が冗長実行モード(GR) から work-sharing を行う時点に到達して、並列実行のための GP モードに移行する。但し、その際は、一つの active gang 当たり一つの worker single(WS) でかつ、worker 当たり一つの vector lane だけの動作モードとなっている。これが GP モードの状態となる。
GR モードあるいは GP モードの時に、gang が worker の work-sharing を行うループに到達した時点で、今度は worker レベルの並列実行である WP モードに移行する。この時点で、gang 内の全ての worker がアクティベートされる。各 gang 内の worker によってループを並列分割し実行される。もし、同じループに対して GP と WP の両方が指定されている場合は、全ての gang と worker を使ってループのイテレーションを並列分割して実行される。
もし、worker が vector レベルの work-sharing のループに到達した時、worker は、VP モードに移行する。WP モードと同じように、 VP モードに移行する際に、 worker の全ての vector lane がアクティベートされる。そのループのイテレーションは、ベクトルあるいは SIMD オペレーションを使った vector lane によって並列に分割され実行される。もし、一つのループに対して、gang、worker、vector が指定されている場合は、これらgang、worker、vector の全てを利用した並列分割を実施して実行する。
そして、OpenACC における「アクセラレータのスレッド(thread)」という言葉の定義であるが、これは、「a single vector lane of a single worker of a single gang 」とされている。すなわち、一つの gang 内の一つの worker 内にある一つの vector レーンの実行体を thread として定義されている。
上記で使用したプログラムを以下に示した。同じプログラムを kernels 構文で実装した場合と parallel 構文で実装した場合の例を示す。
内容 | C | Fortran |
---|---|---|
kernels 構文で実装 | c2.c | f2.f90 |
parallel 構文で実装 | c2-parallel.c | f2-parallel.f90 |
C プログラム用ヘッダーファイル | timer.h | -- |
なお、Winodws 環境の C コンパイラでコンパイルする場合は、時間計測関数を変更するためコンパイルオプションに "-DWIN32" を付けてコンパイルする必要がある。Fortran の場合はその必要はない。
(Windows の場合) $ pgcc -O2 -DWIN32 -acc -Minfo=accel c2.c
以下は、kernels 構文を用いた Fortran のプログラム例である。太字で示した kernels 構文だけしか挿入していない。data 構文や loop 構文は、コンパイラが自動的に設定するため、ここではあえて明示していない。
program main use accel_lib integer :: n ! size of the vector real,dimension(:),allocatable :: a ! the vector real,dimension(:),allocatable :: r ! the results real,dimension(:),allocatable :: e ! expected results integer :: i integer :: c0, c1, c2, c3, cgpu, chost character(10) :: arg1 if( iargc() .gt. 0 )then call getarg( 1, arg1 ) read(arg1,'(i10)') n else n = 100000 endif if( n .le. 0 ) n = 100000 allocate(a(n)) allocate(r(n)) allocate(e(n)) do i = 1,n a(i) = i*2.0 enddo call acc_init( acc_device_nvidia ) call system_clock( count=c1 ) !$acc kernels do i = 1,n r(i) = sin(a(i)) ** 2 + cos(a(i)) ** 2 enddo !$acc end kernels call system_clock( count=c2 ) cgpu = c2 - c1 do i = 1,n e(i) = sin(a(i)) ** 2 + cos(a(i)) ** 2 enddo call system_clock( count=c3 ) chost = c3 - c2 ! check the results do i = 1,n if( abs(r(i) - e(i)) .gt. 0.000001 )then print *, i, r(i), e(i) endif enddo print *, n, ' iterations completed' print *, cgpu, ' microseconds on GPU' print *, chost, ' microseconds on host' end program
コンパイルをしてみる。-acc オプションとコンパイル情報を出力するためのオプション -Minfo=accel を指定する。コンパイル・コマンドを実行するとアクセラレータ関係のみのメッセージが出力される。以下の中で、赤字のメッセージに着目して欲しい。データの転送に関するメッセージとループが並列化されて「Accelerator kernel generated」というメッセージを見ることが出来る。並列化出来ない場合は、その理由と「Accelerator scaler kernel generated」と言うメッセージが出力される。これは、スカラ実行のカーネルコードは生成したが、「遅い」コードであることを意味しているので、アクセラレータ用のコードではないと認識して欲しい。なお、並列化が実施された場合は、「26, !$acc loop gang, vector(128)」と言った並列マッピングに関するメッセージも必ず確認して欲しい。この例では、gang 並列を実施して、その中で vector(ベクトル化)が実施されるコードを生成したことを表している。すなわち、2階層の並列性を利用している。一般に、アクセラレータの性能を最大限活かすためには、こうした 2 階層以上の並列性を利用したコードの生成が必要である。
[kato@photon29 ACC]$ pgfortran -o f2.exe f2.f90 -acc -Minfo=accel -fast main: 25, Generating present_or_copyin(a(1:n)) Generating present_or_copyout(r(1:n)) Generating NVIDIA code Generating compute capability 1.0 binary Generating compute capability 2.0 binary Generating compute capability 3.0 binary 26, Loop is parallelizable Accelerator kernel generated 26, !$acc loop gang, vector(128) ! blockidx%x threadidx%x
実行をしてみよう。このプログラムは引数に「ループの繰り返し数」を指定できるようにしている。これを変更して GPU 上と CPU 上での実行時間を比べて見る。繰り返し数を増やしていくと GPU 時間と CPU 時間が逆転することが分かる。計算量が多くなればなるほど、GPU 向きのコードとなっていくことが分かる。
[kato@photon29 ACC]$ f2.exe 100000 iterations completed 25169 microseconds on GPU 1352 microseconds on host [kato@photon29 ACC]$ f2.exe 1000000 1000000 iterations completed 25976 microseconds on GPU 34451 microseconds on host [kato@photon29 ACC]$ f2.exe 10000000 10000000 iterations completed 50063 microseconds on GPU 557515 microseconds on host
以下の例は、C プログラムで kernels 構文を利用した場合のプログラムである。Fortranの場合と同じように、一連の出力例を以下に示す。
#include <stdio.h> #include <stdlib.h> #include <assert.h> #include <math.h> #include <accelmath.h> #include <openacc.h> #include "timer.h" int main( int argc, char* argv[] ) { int n; /* size of the vector */ float *restrict a; /* the vector */ float *restrict r; /* the results */ float *restrict e; /* expected results */ float s, c; struct timeval t1, t2, t3; double cgpu, chost; int i; if( argc > 1 ) n = atoi( argv[1] ); else n = 100000; if( n <= 0 ) n = 100000; a = (float*)malloc(n*sizeof(float)); r = (float*)malloc(n*sizeof(float)); e = (float*)malloc(n*sizeof(float)); for( i = 0; i < n; ++i ) a[i] = (float)(i+1) * 2.0f; /*acc_init( acc_device_nvidia );*/ StartTimer(); #pragma data copyin(a[0:n]), copyout(r) { #pragma acc kernels #pragma acc loop gang, vector(128) for( i = 0; i < n; ++i ){ s = sinf(a[i]); c = cosf(a[i]); r[i] = s*s + c*c; } } cgpu = GetTimer(); StartTimer(); cgpu = (t2.tv_sec - t1.tv_sec)*1000000 + (t2.tv_usec - t1.tv_usec); for( i = 0; i < n; ++i ){ s = sinf(a[i]); c = cosf(a[i]); e[i] = s*s + c*c; } chost = GetTimer(); chost = (t3.tv_sec - t2.tv_sec)*1000000 + (t3.tv_usec - t2.tv_usec); /* check the results */ for( i = 0; i < n; ++i ) assert( fabsf(r[i] - e[i]) < 0.000001f ); printf( "%13d iterations completed\n", n ); printf( "%13g microseconds on GPU\n", cgpu*1000 ); printf( "%13g microseconds on host\n", chost*1000 ); return 0; } [kato@photon29 ACC]$ pgcc -o c2.exe c2.c -acc -Minfo=accel -fast main: 34, Generating present_or_copyout(r[0:n]) Generating present_or_copyin(a[0:n]) Generating NVIDIA code Generating compute capability 1.0 binary Generating compute capability 2.0 binary Generating compute capability 3.0 binary 36, Loop is parallelizable Accelerator kernel generated 36, #pragma acc loop gang, vector(128) /* blockIdx.x threadIdx.x */ [kato@photon29 ACC]$ c2.exe 100000 iterations completed 183329 microseconds on GPU 1437 microseconds on host [kato@photon29 ACC]$ c2.exe 1000000 1000000 iterations completed 179438 microseconds on GPU 33796 microseconds on host [kato@photon29 ACC]$ c2.exe 10000000 10000000 iterations completed 200065 microseconds on GPU 560842 microseconds on host
OpenACC を適用する際の作業心得を以下に示すが、段階的に OpenACC の適用範囲を広げていくことをお勧めする。
Loop is parallelizable Accelerator kernel generated !$acc loop gang, vector(128) ! blockidx%x threadidx%x以下のメッセージは、並列化されていないメッセージであるため、上記と混同しないようにして欲しい。
Accelerator scaler kernel generated
Generating present_or_copyin(a(1:n)) Generating present_or_copyout(r(1:n)
$ export PGI_ACC_TIME=1 (Linux / Windows cygwin/ OS X シェル) $ set PGI_ACC_TIME=1 (Windows のコマンドプロンプト 環境セット) コンパイラが生成した個々のカーネルの実行プロファイルの結果が表示される。 $ ./c2.exe (実行) Accelerator Kernel Timing data .../c2.c main NVIDIA devicenum=0 time(us): 428 34: compute region reached 1 time (34行目で compute 領域の開始) 34: data copyin reached 1 time device time(us): total=83 max=83 min=83 avg=83 (デバイスへのデータコピーの時間) 36: kernel launched 1 time grid: [782] block: [128] (36行目のループを並列化、gridサイズ 782 で、 thread-block が128の並列) device time(us): total=273 max=273 min=273 avg=273 elapsed time(us): total=282 max=282 min=282 avg=282 (実行時間が total 292マイクロ秒) 41: data copyout reached 1 time (ホスト側へデータコピーの時間) device time(us): total=72 max=72 min=72 avg=72
NVIDIA の GPU の場合は、NVIDIA 社の nvprof と言うコマンドでプロファイル情報を得ることもできる。
$ nvprof c2.exe 10000000(実行) ==24944== Profiling application: c2.exe ==24944== Profiling result: Time(%) Time Calls Avg Min Max Name 44.51% 670.50us 1 670.50us 670.50us 670.50us [CUDA memcpy HtoD] 39.79% 599.42us 1 599.42us 599.42us 599.42us [CUDA memcpy DtoH] 15.71% 236.64us 1 236.64us 236.64us 236.64us main_36_gpu
[Reference]