CPUメモリとGPUメモリの分散メモリの問題

AMDのAPUのようにCPUとGPUを1チップに集積した製品では、どちらからも共通のメインメモリをアクセスできるという構造が使われるが、ハイエンドのGPUを使う場合は、CPUはCPUのメモリを持ち、GPUはGDDR5で出来たGPUメモリをアクセスするという分散メモリになる。そして、CPUとGPUはPCI Expressでつながっているという構造となるのが一般的である。

分散メモリであるので、両者の間のデータのやり取りは明示的に行う必要があり、プログラムの中でデータ転送を記述する必要がある。データが単純な配列の場合は、前述のGPUdirect v2を使えばコピーはそれほど面倒ではないが、構造体の配列で、構造体の要素にはポインタが含まれているという場合は、構造体の配列だけをコピーしても、その中に含まれるポインタで指される実体のデータにはGPUからはアクセスできない。

したがって、ポインタで指されるデータのGPUメモリへのコピーと、コピーされた構造体の中のポインタもそれに合わせてコピーされたデータのアドレスを指すように変更する必要があり、かなり複雑な処理になる。

論理的に共通メモリに見せるUnified Memory

このようにCPUとGPUのメモリが分離されていることが、GPUプログラミングを難しく、取っき難いものとしている。このため、GPUメーカは両者のメモリが統一された共通のメモリであるように見せようとしている。

NVIDIAはCUDA 6でUnified Memoryと呼ぶ統一メモリ機能をサポートした。Unified Memoryではメモリ領域を割り付ける時に、これはCPUとGPUが共用する統一メモリ領域であると指定する。そうすると、図3-60に示すように、CPUメモリとGPUメモリの両方に、要求された量のメモリ領域Pg Aが確保される。Pg Aに対するCPU側のページテーブルのエントリがValidになっていると、CPUはメインメモリ上のページPg Aを読み書きすることができる。その時、GPU側のページテーブルのPg Aのエントリは逆のInvalidにしておく。

そして、GPUがページPg Aをアクセスしようとすると、そのエントリはInvalidであるのでメモリのアクセス違反となりOSに通知される。OSはアクセス違反の原因を解析し、Unified Memoryのアクセスが原因であることが分かると、CPU側のメインメモリ上のPg AのデータをGPU側のデバイスメモリ上のPg Aの領域にコピーする。そして、CPUのページテーブルのPg Aに対するエントリをInvalidに変え、GPUのページテーブルのPg Aに対するエントリをValidに変える。これで、GPUはCPUがPg Aに書き込んだデータをアクセスすることができ、あたかもPg Aが共通にアクセスできるメモリであるかのように動作する。

また、GPU側のPg AがValidの状態でCPU側がそのページをアクセスすると、CPU側のページテーブルはInvalidであるので、アクセス違反の割り込みがあがり、OSはGPUからCPUのメモリにデータをコピーして、CPU側のページテーブルの当該エントリをValidにし、GPU側のページテーブルの当該エントリをInvalidにする。

図3-60 CUDAのUnified Memoryの仕組み

つまり、CPUがページPg Aに書き込んだデータが、GPUで読め、逆にGPUが書き込んだデータがCPUで読めることになり、両者が共通のメモリを使っているのと同じ効果が得られる。

このメカニズムから想像されるように、CPUとGPUが交互に同じページへのアクセスを繰り返すような場合には、ページのコピーが頻繁に繰り返されて効率が悪い。しかし、一般的にはGPUでの処理の実行の前に、CPUが共通領域に入力データを書き込み、GPUの処理が終わったら、CPUが共通領域にアクセスして結果を受け取るというような処理が多い。このような処理であれば頻繁な転送は必要とはならず、大きなオーバヘッドとはならない。

また、どちらのページテーブルがValidであるかはページごとに指定できるので、ページAとBを使い、GPUがページAに書き込んでいるときには、CPUは次のデータをページBに書き込み、ページAへの処理結果の書き込みが終わったら、ページAはCPUがアクセスして結果を読み、GPUはページBのデータを処理するというダブルバッファの処理も問題なく行える。