性能上重要なローカルデータシェア

GPUは多数の演算器を搭載しているが、それらの演算器にデータを供給し、演算結果を格納するメモリが無ければ演算を続けることはできない。この点でコンピュートユニットに内蔵されているローカルデータシェアというメモリをうまく活用することが重要である。

GCNアーキテクチャのGPUは各コンピュートユニットに64KBのローカルデータシェアと呼ぶ高速メモリを内蔵している。これはNVIDIAのGPUのシェアードメモリと同じ位置づけのメモリである。ローカルデータシェアは、そのコンピュートユニットで同時並列的に実行されるすべてのWavefrontのすべてのスレッドで共用される。

一方、異なるコンピュートユニットで実行されるスレッド間では異なるローカルデータシェアが使われるので、ローカルデータシェア経由でデータの交換はできない。

ローカルデータシェアは、図8.5に示すように、32バンクに分割して作られており、同一バンクへのアクセスが重ならなければ、32個のアクセスを並列に処理することができる。そして、1サイクルで処理できるのは最大32アクセスであるので、64スレッドのWavefrontのアクセスを処理するには、少なくとも2サイクルを必要とする。

図8.5 ローカルデータシェアは、32個の2KBバンクで構成されている。各バンクは独立のアドレスのアクセスを行うことが出来るので、バンクが重複しなければ、32の異なるアドレスへのアクセスを並列実行することができる

ローカルデータシェアは64KBといっても、同時並列的に10 Wavefrontが実行され、それぞれがアクセスするアドレスの異なる64スレッドが実行されれば、1スレッドあたりは100バイト(4バイト変数25個)のメモリしか使えない。ローカルデータシェアは、GPUコアに一番近く、高速にアクセスできるメモリであるが、容量が限られるので、大事に使わなければならない資源である。

また、高性能のプログラムを作るためには、ローカルデータシェアを有効に使用して、GPUコアにデータを供給し、結果を格納するプログラムを作ることが重要である。

AMDのGCNアーキテクチャのWavefrontは64スレッドを含み、これらの64スレッドは64個の異なるアドレスをアクセスすることができる。従って、64スレッドがすべて同じメモリバンクの異なるアドレスをアクセスすることになり得る。この場合。1つの命令を実行するのに、64回のローカルデータシェアへのアクセスが必要となってしまう。

例えば、A[N][64]という配列を作ると、図8.6のようにローカルデータシェアに格納される。この配列を各スレッドがN=0、1、2、3、4…とアクセスすると、図8.6で黄色で示した部分をアクセスすることになり、すべてのアクセスが同じバンクに集中してしまう。そうなると、1サイクルには1個のデータしかアクセスできないので、Wavefront全体では、ローカルデータシェアのアクセスに64サイクル掛かることになり、これでは性能が出ない。

図8.6 A[N][64]のローカルデータシェア上での各要素の格納位置。黄色で示したA[N][0]はすべて同じバンクに置かれるので、Wavefront全体では64回のアクセスが必要になる

必要な配列はA[N][64]であるが、もう1つ要素を追加してA[N][65]という配列を作ると、ローカルデータシェアでの格納位置は図8.7のようになる。1つ余計な要素を追加すると、N=0のデータは、最初の256バイトに収まらず、最後の要素は、Bank0に入ってしまう。そして、N=1のアクセスはBank1へのアクセスとなる。同様に、N=2、3、4…のアクセスは異なるバンクへのアクセスとなり、A[N][0]へのアクセスは黄色で示すように、すべて異なるバンクへのアクセスに変る。

図8.7 配列をA[N][65]と、各行に1つ要素を追加すると、A[N][0]へのアクセスは異なるバンクへのアクセスに変る

このようにすれば、各行に、本来不要な65番目の要素の領域を確保する必要があり、1.6%ほどメモリがムダになるが、各スレッドのアクセスが異なるバンクに分散して、32スレッドのローカルデータシェアのアクセスが並列に実行できるようになって、大幅にメモリアクセス時間を改善することができる。

このテクニックは、CPUだけの時代にも、キャッシュアクセスのバンド幅の改善のために用いられてきた手法であるが、GPUの世界でも、重要な最適化テクニックとなっている。

なお、ここでは、説明を簡単にするため、1つのWavefrontでローカルデータシェアをアクセスするように書いたが、実際は、1サイクルに2つのSIMDからの16レーン分のアクセスを処理する構造になっている。