PGIコンパイラによる GPU Computing シリーズ (2)
前のコラム「マルチコアCPU上の並列化手法、その並列性能と問題点」で、従来の x64 系マルチコア・プロセッサ技術における問題点を整理しました。その際、NVIDIA の GPU 上での計算処理との違いも簡単に紹介しました。その違いの一つとして、メモリ帯域(バンド幅)の違いが大きいことを述べました。また、マルチコアのCPU、あるいはGPUにおける現存のプロセッサ技術の上では、「メモリアクセス(方法等)を何らかの方法で高速化(最適化)すること」が、最大の「全体性能高速化手法」であると言うことも説明しました。このコラムでは、NVIDIA GPU (GeForce GTX 285) の実際のメモリ帯域の測定を行い、実感として x64系でも最大と言われる Nehalem メモリ帯域とは大きなの違いがあることを理解してもらいます。測定に使用したメモリバンド幅測定のベンチマークは、HPC業界では一般的な STEREAM Benchmark を使用します。
2010年3月1日 Copyright © 株式会社ソフテック 加藤
以下に示すシステムは、前のコラム「マルチコアCPU上の並列化手法、その並列性能と問題点」で使用したものと同じものです。メモリに関しては、本システムは 2チャネルを使用する構成となっているため、本来3チャネル使用できる Nehalem システムのものより利用帯域は低いです。
| ホスト側プロセッサ | Intel(R) Core(TM) i7 CPU 920 @ 2.67GHz x 1 プロセッサ |
| ホスト側メモリ | PC3-10600(DDR3-1333)2GB x 2 枚 (2チャネル) |
| ホスト側 OS | Cent OS 5.4、カーネル 2.6.18-164.el5 |
| GPU ボード | GeForce GTX 285 |
| GPU Compute capability | 1.3 |
| NVIDIA CUDA 環境 | ドライバ : 2.3、Toolkit : 2.3 |
| GPU デバイスメモリ | 1 GB |
| PGI コンパイラ | PGI 10.2 (PGI 2010) |
ここで用いるメモリバンド幅の測定のためのプログラムは、 STREAM Benchmark で提供している Fortran プログラム(倍精度演算)を使用します。時間計測用のC関数ルーチンをリンクします。
一方、PGI CUDA Fortran で記述されたGPU用の STREAM Benchmarkのソースは、以下のものを使用します。これは、STREAMベンチマークの stream_tune.f を CUDA Fortran 化したものです。
CUDA Fortran STREAM benchmark source code (F77 固定記述形式ソースです)
まず、ホスト側 Nehalem の x64 システムのメモリバンド幅を測定します。以下のコンパイラのオプションの中で、-mp=align の意味は、 -mp 単独では、OpenMP の指示行を解釈して、並列スレッドコードを作成すると言う意味となります。また -mp=align というオプション・フラグは、並列化と SSE によるベクトル化の両方が適用されるループにおいて、ベクトル化のためのアライメント(整列)を最大化するようなアルゴリズムを使用して、OpenMP スレッドにループ回数を割り当てるようにするものです。この機能は、こうした特性を帯びた多くのループがプログラムに存在する時に性能が向上します。STREAM ベンチマークはその最たる例です。
単一ループ(最内側ループ)を SSEベクトル化とスレッド並列の二つを共存させるように -mp=align を指定する。
!$OMP PARALLEL DO
DO 60 j = 1,n
a(j) = b(j) + scalar*c(j)
60 CONTINUE
【SSEベクトル化コード生成用のオプションとコンパイルメッセージ】
$ pgf95 -fastsse -Minfo -mp=align stream.f -DUNDERSCORE mysecond.c
stream.f:
stream:
149, Parallel region activated
150, Begin master region
153, End master region
Parallel region terminated
157, Parallel region activated
158, Parallel region terminated
161, Parallel region activated
162, Parallel loop activated with static block schedule 並列コード化
Generated an alternate loop for the loop
Generated vector sse code for the loop SSEベクトル化
166, Parallel region terminated
168, Parallel region activated
169, Parallel loop activated with static block schedule
Generated vector sse code for the loop
Generated a prefetch instruction for the loop
171, Parallel region terminated
182, Invariant assignments hoisted out of loop
Loop not vectorized/parallelized: contains a parallel region
186, Parallel region activated
187, Parallel loop activated with static block schedule
Memory copy idiom, loop replaced by call to __c_mcopy8
189, Parallel region terminated
196, Parallel region activated
197, Parallel loop activated with static block schedule
Generated an alternate loop for the loop
Generated vector sse code for the loop
Generated a prefetch instruction for the loop
199, Parallel region terminated
206, Parallel region activated
207, Parallel loop activated with static block schedule
Generated an alternate loop for the loop
Generated vector sse code for the loop
Generated 2 prefetch instructions for the loop
209, Parallel region terminated
216, Parallel region activated
217, Parallel loop activated with static block schedule
Generated an alternate loop for the loop
Generated vector sse code for the loop
Generated 2 prefetch instructions for the loop
219, Parallel region terminated
226, Generated vector sse code for the loop
227, Loop unrolled 4 times (completely unrolled)
234, Loop not vectorized/parallelized: contains call
realsize:
279, Loop not vectorized: mixed data types
283, Loop not vectorized/parallelized: multiple exits
checktick:
363, Loop not vectorized/parallelized: contains call
372, Loop not vectorized: mixed data types
checksums:
419, Loop unrolled 2 times
436, Parallel region activated
437, Parallel loop activated with static block schedule
Generated an alternate loop for the loop
Generated vector sse code for the loop
Generated 3 prefetch instructions for the loop
441, Begin critical section
End critical section
Parallel region terminated
mysecond.c:
実行
1 スレッド実行 [kato@photon29 STREAM]$ export OMP_NUM_THREADS=1 [kato@photon29 STREAM]$ ./a.out ---------------------------------------------- Double precision appears to have 16 digits of accuracy Assuming 8 bytes per DOUBLE PRECISION word ---------------------------------------------- ---------------------------------------------- STREAM Version $Revision: 5.6 $ ---------------------------------------------- Array size = 20000000 Offset = 0 The total memory requirement is 457 MB You are running each test 10 times -- The *best* time for each test is used *EXCLUDING* the first and last iterations ---------------------------------------------- Number of Threads = 1 ---------------------------------------------- Printing one line per active thread.... ---------------------------------------------------- Your clock granularity/precision appears to be 1 microseconds ---------------------------------------------------- Function Rate (MB/s) Avg time Min time Max time Copy: 13908.8611 0.0230 0.0230 0.0230 Scale: 14260.8832 0.0224 0.0224 0.0225 Add: 14928.6731 0.0322 0.0322 0.0322 Triad: 14773.7697 0.0325 0.0325 0.0325 ---------------------------------------------------- Solution Validates! ---------------------------------------------------- 2 スレッド実行 [kato@photon29 STREAM]$ export OMP_NUM_THREADS=2 [kato@photon29 STREAM]$ a.out (省略) ---------------------------------------------- Number of Threads = 2 ---------------------------------------------------- Your clock granularity/precision appears to be 1 microseconds ---------------------------------------------------- Function Rate (MB/s) Avg time Min time Max time Copy: 15797.0114 0.0203 0.0203 0.0204 Scale: 16217.3133 0.0198 0.0197 0.0198 Add: 16843.8897 0.0286 0.0285 0.0286 Triad: 16439.5208 0.0292 0.0292 0.0293 ---------------------------------------------------- Solution Validates! ---------------------------------------------------- 4 スレッド実行 [kato@photon29 STREAM]$ export OMP_NUM_THREADS=4 [kato@photon29 STREAM]$ a.out -(省略) ---------------------------------------------- Number of Threads = 4 ---------------------------------------------------- Your clock granularity/precision appears to be 1 microseconds ---------------------------------------------------- Function Rate (MB/s) Avg time Min time Max time Copy: 14236.8314 0.0227 0.0225 0.0231 Scale: 15564.9045 0.0208 0.0206 0.0210 Add: 16135.4293 0.0300 0.0297 0.0303 Triad: 15970.1891 0.0304 0.0301 0.0306 ---------------------------------------------------- Solution Validates! ----------------------------------------------------
x64 システム(ホスト側)のメモリバンド幅
ベンチマークの中で、単純なメモリコピー(Copy)は、15.7GB/sec で、倍精度演算を含む triad 系では、16GB/sec のメモリ帯域を使用できていることが分かります。
| Copy | Scale | Add | Triad |
|---|---|---|---|
| 15,797 | 16,217 | 16,848 | 16,439 |
上記の CUDA Fortran STREAM の Fortran プログラムを含む tar ファイルを展開し、stream_cudafor.cufをシステム上に置きます。 CUDA Fortran のファイル名のコンベンション(慣習)については、こちらを参照ください。なお、コンパイル時には、ソースが FORTRAN77 形式の固定記述形式となっていますので、明示的に -Mfixed オプションを付けて、コンパイラにその旨を指示する必要があります。
【CUDA Fortran STREAM のソースコード例 】
module stream_cudafor
use cudafor
contains
attributes(global) subroutine stream_triad(a, b, c, scalar, n)
real*8, device :: a(*), b(*), c(*)
real*8, value :: scalar
integer, value :: n
j = threadIdx%x + (blockIdx%x-1) * blockDim%x
if (j .le. n) a(j) = b(j) + scalar*c(j)
return
end subroutine
end module
【CUDA Fortranのコンパイル 最適化モード -O2 】
[kato@photon29 STREAM]$ pgf95 -Mcuda -Mfixed -O2 stream_cudafor.cuf -Minfo
checksums:
479, sum reduction inlined
481, sum reduction inlined
483, sum reduction inlined
GPU を使用して実行
[kato@photon29 STREAM]$ ./a.out ---------------------------------------------- Double precision appears to have 16 digits of accuracy Assuming 8 bytes per DOUBLE PRECISION word ---------------------------------------------- Array size = 20000000 Offset = 0 The total memory requirement is 457 MB You are running each test 10 times -- The *best* time for each test is used *EXCLUDING* the first and last iterations ---------------------------------------------------- Your clock granularity/precision appears to be 1 microseconds ---------------------------------------------------- Function Rate (MB/s) Avg time Min time Max time Copy: 132231.4008 0.0024 0.0024 0.0024 Scale: 133908.2526 0.0024 0.0024 0.0024 Add: 120457.7364 0.0040 0.0040 0.0040 Triad: 119601.9687 0.0040 0.0040 0.0040 ---------------------------------------------------- Solution Validates! ----------------------------------------------------
GPU のメモリバンド幅
ベンチマークの中で、単純なメモリコピー(Copy)では、132GB/sec を記録している。倍精度演算を含む triad 系では、それでも119GB/sec のメモリ帯域を使用できていることが分かります。この数字は、長い間HPCの世界に携わってきた筆者にとっても驚きのメモリバンド幅といえます。
| Copy | Scale | Add | Triad |
|---|---|---|---|
| 132,231 | 133,908 | 120,457 | 119,601 |
| Copy | Scale | Add | Triad |
|---|---|---|---|
| 132,231 | 133,908 | 120,457 | 119,601 |
| 15,797 | 16,217 | 16,848 | 16,439 |
| 8.4倍 | 8.3倍 | 7.1倍 | 7.3倍 |
上記の表で、1行目が GPU の実測メモリバンド幅で2行目が Nehalem のメモリバンド幅です。そして、3行目にその比率を示しました。HPCアプリケーションのような「ストリーム型」で常にプロセッサ・コアへデータの供給が必要なアプリケーション特性の場合は、特別なメモリ階層に関する最適化を施さない限り、この「ストリーム」型メモリバンド幅が全体性能(Performance)を決める要素となります。別の見方をすると、ほとんどのCPUコアの演算能力は、「遊びの状態」で有効に使われていない現実を知ることが必要です。さて、上記のバンド幅の比率をよく覚えておいてください。これは何を意味するのか?ということですが、これは一般的なHPCアプリケーションを何らかの形で GPU 用のコードにポーティングできて、かつ、GPU上で理想的なメモリへのコアレッシング・アクセスが可能な最適化が成功した時に達成しうる「性能向上率」の平均的な値と言うことになります。GPU処理計算で、数十倍~100倍といった性能向上がみられる事例は多々ありますが、これらは、さらに、CUDAプログラミング環境のチューニング技術(ソフトウェア・キャッシュ構造を明示的に使用すると言った方法)を駆使することによって達成できる性能です。
2010年4月に CUDA 環境が 3.0 にバージョンアップされ、それに伴い、CUDA用のドライと CUDA Toolkit も 3.0 に上がりました。上記と同じシステム上で、CUDA の最新バージョンに変えて、再度、性能を測定しました。CUDA 2.3のドライバの時に較べ、性能の大幅な向上がありました。これは、CUDA Toolkit の向上ではなく、CUDA ドライバの改善によるものと思われます。以下の表は、上記で述べたシステム/ソフトウェア環境に較べて変更のあったソフトウェアを記しています。
| NVIDIA CUDA 環境 | ドライバ : 3.0、Toolkit : 3.0 を PGI で使用 |
| PGI コンパイラ | PGI 10.5 (PGI 2010) |
PGI 10.5 + CUDA driver 3.0 + CUDA Toolkit 3.0 での測定
[kato@photon29 STREAM]$ ./a.out ---------------------------------------------- Double precision appears to have 16 digits of accuracy Assuming 8 bytes per DOUBLE PRECISION word ---------------------------------------------- Array size = 20000000 Offset = 0 The total memory requirement is 457 MB You are running each test 10 times -- The *best* time for each test is used *EXCLUDING* the first and last iterations ---------------------------------------------------- Your clock granularity/precision appears to be 1 microseconds ---------------------------------------------------- Function Rate (MB/s) Avg time Min time Max time Copy: 151977.9985 0.0021 0.0021 0.0021 Scale: 151892.5874 0.0021 0.0021 0.0021 Add: 145015.8081 0.0033 0.0033 0.0033 Triad: 145157.5458 0.0033 0.0033 0.0033
| Copy | Scale | Add | Triad |
|---|---|---|---|
| 151,977 | 151,587 | 145,015 | 145,157 |