NVIDIA GPUの実効メモリ帯域(STREAMベンチマーク)

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 化したものです。

Nehalem x64 システムのメモリバンド幅(倍精度演算)

 まず、ホスト側 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 のメモリ帯域を使用できていることが分かります。

表-1 Nehalem システムのメモリバンド幅(MB/sec)
Copy Scale Add Triad
15,797 16,217 16,848 16,439

GeForce GTX 285 の内部のデバイスメモリのメモリバンド幅(倍精度演算)

 上記の 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の世界に携わってきた筆者にとっても驚きのメモリバンド幅といえます。

表-2 GPU GeForce GTX 285 のメモリバンド幅(MB/sec)
Copy Scale Add Triad
132,231 133,908 120,457 119,601

GPUとx64系システムのメモリバンド幅の比較(倍精度変数・演算時)

表-3 GPUとx64系システムのメモリバンド幅の比較 (MB/sec)
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年6月 CUDA 3.0 環境での性能再測定

 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
表-4 CUDA 3.0 環境でのGPU GeForce GTX 285 のメモリバンド幅(MB/sec)
Copy Scale Add Triad
151,977 151,587 145,015 145,157

プログラムのプロファイルを取得する(時間の掛かる部分を特定する)へ続く