PGI アクセラレータにおけるマルチ GPU の使用

対象 PGI Accelerator Compiler, CUDA Fortran, Multiple GPUs

サマリー

このページは、1台のホスト・システムに複数の GPU ボードが実装されている場合、そのGPU全てを計算に使用したいと言う場合の話です。個々の GPU はそれぞれが独立のデバイスであり、共有メモリ等の共有リソースを有しているわけではありません。従って、並列プログラミング的に見れば、MPI処理のような分散メモリ構成に対する並列プログラミングを行うか、あるいは、マルチのスレッドで、各スレッドが独立にGPUを使用すると言った処理が必要となります。Copyright © 2010 株式会社ソフテック 加藤

複数の GPU ボード搭載のマシンは、最初から必要か?

 まず、最初に話さなければいけないことは、「複数の GPU ボード搭載」マシンを始めから買うべきかどうかという判断基準です。すなわち、複数の GPU を使用してさらに性能を加速したいと言う欲求に対して、それが自分のプログラムで本当に実現できるか?と言う何らかの考えを持っておく必要があります。私の見解を言うと、計算機を「道具」として使っている一般の科学者、研究者にとっては、こうした並列への移行は本業でないため、まず、1枚の GPU ボードで試してみることをお勧めします。無駄な投資になりかねない場合があります。IT分野の研究者の方には、これが本業ですので「是非チャレンジしてください」と言うスタンスです。マルチの GPU をホスト側のソフトウェア並列で使い切る技術は、GPU を使う上でのソフトウェアの制約事項が露出してきて、少々難しいと言うのが理由なのですが、これに関しては機会があれば、別のコラムで説明します。以下の例は、単純に MPI の各プロセスが、どの GPU ID のボードを使うかと言うロジックを説明しただけのものです。プログラミングの本当の難しさは、これ以降に潜んでいます。簡単に言うと、ホスト側のプロセスが管理する「メモリ・データ」とGPUデバイス側の「メモリ・データ」の交換を、MPIプロセス並列あるいはスレッド並列の中で合理的に設計し直すことができるかと言うことに尽きます。

MPIプロセスに、GPU デバイスをタイアップする

 NVIDIA社 CUDA の現在の仕様は、ホスト側の一つの「スレッド」あるいは「プロセス」が1台の GPU にのみアクセスできると言う仕様となっています。これは、PGIの制約ではありません。
 マルチ GPU を1台のプラットフォーム上で、一つのプログラムから使用するには、OpenMP、MPI, Pthread 等による「ハイレベル並列化」でプログラムを構成する必要があります。すなわち、ハイレベル並列性を有した、その下位のレイヤーで独立のGPUを使用するようにする必要があります。 GPU並列対象の上位をOpenMPスレッド並列する場合は、ループ・イテレーションを分割し、独立のスレッドが特定の GPU IDを使用するようにコーディングすることが必要です。また、上位を MPI 並列する場合は、MPIのプロセス(Rank)に特定の GPU IDを割り付けて、そのプロセス内で計算するループ構造に対して、GPU を使用すると言う構成にしなければなりません。(使用する GPU を特定の GPU ID に紐付けして使う方が、データの競合等を考慮する必要がなくなるため、余計な処理を省くことができます)

 このように、マルチ GPU ボードを1台のホストで使用するプログラミングには、CUDA Fortran であれ、 Directive で行う PGI Acceleratorモデルによる GPU 計算であれ、いずれも同じ発想が必要となります。OpenMP スレッドの分割よりも MPI を使用した方が簡単ですので、その例を以下にお見せします。MPI の中のrank(プロセス番号)毎に、独立の使用 GPU を割り当てて、それ以降、各 MPI プロセスの中の「ループ計算で」GPUパラレルを行うと言う形です。

 一般論に戻りますが、実際の OpenMP の並列プログラミングモデルを考えた場合、OpenMP並列リージョン内で各スレッドが個別の GPU を使うには相当の制約があることを理解しなければなりません。仮に、各並列スレッドがGPUデバイスを割り当てることができたとしても、そのスレッド内処理でのGPUデバイスとの「データの転送」が合理的に行うことができるか?と言う問題に直面します。ほとんどは、各スレッドが指示するGPUの演算処理よりもデータ転送のオーバーヘッドの方が大きくなります。従って、OpenMPを使用したマルチGPUの使用においては、かなり上位のレベルで並列化(タスク並列的なもの)できるようなものが理想です。少なくとも、今まで適用してきた OpenMP による細粒度並列化されたプログラムに対しては、OpenMP + PGI Accelearator の「ハイブリッド使用」は好ましくなく、むしろ、1台のGPUアクセラレータの適用だけの方が望ましいでしょう。逆に、現在 MPI 並列のコードを有している場合は、MPI + PGI Accelerator プログラミングモデルのハイブリッド型の方が親和性が高いと言えます。しかし、この directive で指示するタイプの PGI Accelerator プログラミングモデルを MPIプロセスの中で適用するときには、 !$acc region の中に "call MPI関数" が使えない等の制約が出てくるために、それを避けるための組み替えを行うか、あるいは最終的な解は、MPI + CUDA Fortran で明示的な CUDA プログラミングを行うことが理想となります。

 プログラム例を説明する前に、搭載されている GPU の数を知るための API とユニークな GPU ID をタイアップする(セットする) API の使用方法を説明します。本論から外れますが、以下の例で、#ifdef _ACCEL と言うコンパイル時のプリプロセス処理を指定しておくと便利です。アクセラレータのコンパイルモード(-ta=nvidia)が enable の時に、"_ACCEL"には値が入ります。従って、プリプロセスにおいて、以降の文は、コンパイラによって認識されます。一方、-ta=nvidiaオプションを付けないでコンパイルした場合は、"_ACCEL" に値が入りませんので、以降の文は無視されます。さて、アクセラレータの API (runtime libraryとも言う)を使用する場合は、必ず、USE accel_lib を指定(Fortran90構文)するか、あるいは、include "accel_lib.h" を宣言します。主に使用する API は、以下に示したacc_get_num_devices(デバイス数を返す)とacc_set_device_num(特定のデバイスを使うためにセット)となります。この関数の引数で、”acc_device_nvidia”は、NVIDIA GPUに対応しており、この変数名をいつも使用して下さい。これがホスト側を意味する変数名は、"acc_device_host"です。

#ifdef _ACCEL 
      use accel_lib
      
! set the device for each process
      numdevices = acc_get_num_devices(acc_device_nvidia)
      mydevice = mod(myrank,numdevices)
      call acc_set_device_num(mydevice,acc_device_nvidia)
#endif

 以下のプログラム例は、各MPIプロセスが各自にタイアップしたGPUを使用できるようにするための方法を例示したものです。MPI の rank(プロセスIDに相当する)に、使用するGPU ID 番号を割り当て定義して、それ以降の処理は、各 MPI プロセスが割り当てられた GPU を使用すると言う形にしています。
 また、以下の例は、PGI アクセラレータ機能提供 API と CUDA Fortran 提供の API を使用したサブルーチンの両方からなる実行モジュールの例です。

▶ 簡単なプログラム例
【プログラムファイル】multi.F90  
 (プリプロセス処理を行う場合のファイル名のサフィックスは大文字 *.F、*.F90) 
      program multiGPU
#ifdef _ACCEL
      use accel_lib    ! PGIアクセラレータ用関数の定義モジュールを使う
      use cudafor      ! CUDA Fortran用の定義モジュールを使う
#endif
      include 'mpif.h' ! MPI用のヘッダーファイル

      type(cudadeviceprop) :: prop
      integer :: istat

      integer :: numproc, myrank, ierr, status(MPI_STATUS_SIZE)
      integer :: i, j, n, mydevice, numdevices

! MPI initialize      
! MPIの初期化とrank取得(myrank)と総並列プロセス数(numproc)
      call mpi_init(ierr)
      call mpi_comm_rank( MPI_COMM_WORLD, myrank, ierr )
      call mpi_comm_size( MPI_COMM_WORLD, numproc, ierr )
      
! ------ MPI parallel region start -----------------------

#ifdef _ACCEL         ! MPIプロセスとGPU deviceをタイアップ
! set the device for each process
      numdevices = acc_get_num_devices(acc_device_nvidia)
      mydevice = mod(myrank,numdevices)
      call acc_set_device_num(mydevice,acc_device_nvidia)
#endif

      if (myrank == 0) then
        print *, "This program uses ",numproc, " process."
        print *, "Number of GPU devices = ",numdevices
      end if

      ! Each process prints GPU H/W properteis.
      ! (各プロセスが使用するGPUの特性を印字)
      
       print *,"=====myrank, mydevice======",myrank,mydevice
       istat = cudaGetDeviceProperties(prop, mydevice)
       call printDeviceProperties(prop, mydevice)

      call mpi_finalize(ierr)

      end
    !
      subroutine printDeviceProperties(prop, num)
      use cudafor
      type(cudadeviceprop) :: prop
      integer num
      ilen = verify(prop%name, ' ', .true.)
        write (*,900) "Device Number: "      ,num
        write (*,901) "Device Name: "        ,prop%name(1:ilen)
        write (*,903) "Total Global Memory: ",real(prop%totalGlobalMem)/1e9," Gbytes"
        write (*,902) "sharedMemPerBlock: "  ,prop%sharedMemPerBlock," bytes"
        write (*,900) "regsPerBlock: "       ,prop%regsPerBlock
        write (*,900) "warpSize: "           ,prop%warpSize
        write (*,900) "maxThreadsPerBlock: " ,prop%maxThreadsPerBlock
        write (*,904) "maxThreadsDim: "      ,prop%maxThreadsDim
        write (*,904) "maxGridSize: "        ,prop%maxGridSize
        write (*,903) "ClockRate: "          ,real(prop%clockRate)/1e6," GHz"
        write (*,902) "Total Const Memory: " ,prop%totalConstMem," bytes"
        write (*,905) "Compute Capability Revision: ",prop%major,prop%minor
        write (*,902) "TextureAlignment: "   ,prop%textureAlignment," bytes"
        write (*,906) "deviceOverlap: "      ,prop%deviceOverlap
        write (*,900) "multiProcessorCount: ",prop%multiProcessorCount
        write (*,906) "integrated: "         ,prop%integrated
        write (*,906) "canMapHostMemory: "   ,prop%canMapHostMemory
900   format (a30,i0)
901   format (a30,a)
902   format (a30,i0,a)
903   format (a30,f5.3,a)
904   format (a30,2(i0,1x,'x',1x),i0)
905   format (a30,i0,'.',i0)
906   format (a30,l0)
      return
      end

[kato@photon29 PGItest]$ pgf90 -Mmpi=mpich1 -ta=nvidia multi.F90  -Mcuda

ここで、MPI実行します。
[kato@photon29 PGItest]$ mpirun -np 2 a.out

 This program uses             2  process.
 Number of GPU devices =             2
 =====myrank, mydevice======            0            0
               Device Number: 0
                 Device Name: GeForce GTX 285
         Total Global Memory: 1.073 Gbytes
           sharedMemPerBlock: 16384 bytes
                regsPerBlock: 16384
                    warpSize: 32
          maxThreadsPerBlock: 512
               maxThreadsDim: 512 x 512 x 64
                 maxGridSize: 65535 x 65535 x 1
                   ClockRate: 1.476 GHz
          Total Const Memory: 65536 bytes
 Compute Capability Revision: 1.3
            TextureAlignment: 256 bytes
               deviceOverlap: T
         multiProcessorCount: 30
                  integrated: F
            canMapHostMemory: T
 =====myrank, mydevice======            1            1
               Device Number: 1
                 Device Name: GeForce GTX 280
         Total Global Memory: 1.073 Gbytes
           sharedMemPerBlock: 16384 bytes
                regsPerBlock: 16384
                    warpSize: 32
          maxThreadsPerBlock: 512
               maxThreadsDim: 512 x 512 x 64
                 maxGridSize: 65535 x 65535 x 1
                   ClockRate: 1.296 GHz
          Total Const Memory: 65536 bytes
 Compute Capability Revision: 1.3
            TextureAlignment: 256 bytes
               deviceOverlap: T
         multiProcessorCount: 30
                  integrated: F
            canMapHostMemory: T