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 |