Read Onlyキャッシュ(リードオンリーキャッシュ)

グラフィックス機能の1つにTexture(テクスチャ)の貼り付けという操作がある。これは表示する多角形のパネルに壁紙を貼って、簡便により現実的な絵をつくるというもので、この壁紙の模様を記憶して高速でアクセスするためのキャッシュがテクスチャキャッシュである。壁紙の場合は、パネルの位置や傾きに合わせて模様を変形して画面のピクセルを計算する必要があり、テクスチャキャッシュは単純なアドレスベースのアクセスだけでなく、各種の変形機能を実現するアクセスができるようになっている。

しかし、このような変形機能が上手く使える科学技術計算は多くは無いので、テクスチャキャッシュは科学技術計算では単純なアドレスベースのアクセスを行って定数を保持するキャッシュとして用いられることが多い。科学技術計算ではテクスチャキャッシュに壁紙だけを格納するわけではないので、NVIDIAはリードオンリーキャッシュと呼んでいる。

GK110 GPUのSMには4つのワープスケジューラがあり、それぞれのワープスケジューラに対応する12KBのリードオンリーキャッシュがある。リードオンリーキャッシュのアクセス単位は32Bである。アクセスするアドレスがばらけている場合も、前述のキャッシュなしと同じような32Bアクセスであり、L1キャッシュを使って128B単位のアクセスをするよりムダが少ない。ただし、変形ロジックを経由するため、アクセス時間はL1キャッシュよりも遅い。

また、リードオンリーキャッシュのアクセスは4スレッド単位で行われ、1回の読み出しでは4スレッド分だけが処理される。L2キャッシュのように1つの32Bセグメントにアクセスするすべてのスレッドに1回のアクセスでデータを供給するということはできず、32スレッドのリードには、必ず8回のリードオンリーメモリのアクセスが必要となる。

一方、リードオンリーキャッシュは同じワープスケジューラで実行される他のワープと共用されるので、1つのワープがDRAMから読み込んだデータを他のワープが使い、他のワープのアクセスはリードオンリーキャッシュにヒットし、再度DRAMを読む必要はないという効果は期待できる。しかし、同じSM内の別のワープスケジューラのワープからのアクセスにはヒットせずDRAMアクセスが必要となり、同じデータが一つのSM内の複数のリードオンリーキャッシュに格納されているということも発生する。

リードオンリーキャッシュのアクセスは4スレッドずつ処理して行く (2)。128Bからの32Bセグメントは2回目の読み出しでもアクセスされるが、1回目の読み出しでRead only Cacheに入っているので、2回目はL2キャッシュから読み出す必要はない

Constantキャッシュ

また、NVIDIA GPUはConstantキャッシュという定数を格納する専用のキャッシュを持っている。このキャッシュの容量は64KBで、"_const_"と定義されたデータだけを格納する。

このキャッシュの読み出しスループットはSMあたり4Bと狭く、ワープ内のすべてのスレッドが同じデータを使うというケースが主な適用範囲となる。しかし、円周率πとか自然対数の底eなどは全てのスレッドで使えるので、このような定数はConstant Cacheに入れるのが良い。

以上、Kepler GPUハードウェアの基本的な動きを説明してきたが、具体的に幾つかのケースで、メモリアクセスがどのように性能に影響するか、そして、性能の改善方法を見ていこう。

最初のケースは単精度の浮動小数点数を3つ含む構造体を扱う例である。

x,y,zの3要素のPositionという構造体を定義し、kernelでtemp=data[idx]と構造体の要素をtempに代入するケース1 (2)

Keplerハードウェアはfloat3個分の12Bのロード、ストアは出来ないので、CUDAコンパイラは4Bのロードを3回行うコードを生成する。

1回目のxのアクセスは、この図のように0、12、24と12Bおきにアクセス。2回目のyは4、16、28、3回目のzは8、20、32バイトと読む (2)

この図のように、SMは3回のアクセスを行い、各アクセスでは、メモリから読んできた32Bの1/3のデータしか使われていないので効率が悪い。

これを構造体のアレイ(Array of Structures)では無く、x、y、zの3つの配列とすれば、連続して読めるのでメモリバンド幅を有効に使える (2)。その他の改善法としては,Read Only Cache経由で読む、あるいは一旦、SMEMに読み込んでから、SMEMから構造体を読み出すという手もある

データの持ち方をこの例のような構造体の配列(Array of Structures)でなく、x、y、zと3つの配列(あるいはそれらを要素とする構造体:Structure of Arrays)とすれば、連続アクセスとなりメモリバンド幅を無駄なく使える。

また、飛び飛びのアクセスの効率が良いリードオンリーキャッシュを使う方法や、アクセスの早いシェアードメモリに一旦読み込み、そこから構造体を読み出すという方法でも性能を改善できる。

これらの改善方法による性能改善効果。データをStructure of Arrays構造でもつ方法が一番性能が高い (2)。読み出しの場合は、元のAoS構造を1.00とすると、SoA構造の場合は1.51倍の性能。書き込みの場合は1.88倍の性能となった。Read Only CacheやSMEMの使用もSoA構造より若干性能が低いが、かなり大きな改善が見られる

このスライドのように、カーネルがリードする場合、SoA構造を使うようにプログラムを書き変えた場合が最も性能が高く、元のAoS構造の場合と比べて1.51倍の性能が得られる。リードオンリーメモリを使った場合は、1.43倍、シェアードメモリをバッファとして使った場合は1.40倍の性能であり、SoA構造に書き変えた場合と比べると若干劣るものの、かなり大きな性能改善が得られる。

カーネルがライトする場合は、AoS構造と比べるとSoA構造に書き変えると性能は1.88倍に向上している。ライトの場合はリードオンリーキャッシュは使えないが、SMEMを使った場合もSoA構造の場合と同じ性能という結果になっている。

DRAMのアクセスには400~800サイクルかかるので、Kepler GPUはSMあたり100程度のメモリアクセスが同時に行われていないとメモリバンド幅をフルに使うことができない。それぞれのメモリアクセスは128B単位なので、32Bのセグメントのアクセスで考えると常に400程度のアクセスが実行されている必要がある。

そのためには、より多くのワープを同時に動かすことが必要である。レジスタやシェアードメモリの制約で同時に実行できるワープ数が決まるので、スレッドブロックのスレッド数を調整し、レジスタ数が制約になる場合は使用レジスタ数を制限するコンパイラオプションを使うなどで、できるだけ多くのワープがSMに入るようにすることが性能改善に有効である。

メモリバンド幅を有効に使うには、32Bのセグメントのメモリアクセスが常時400アクセス程度実行されている必要がある。そのためには、SMの実行するワープ数を増やすことが重要 (2)

3DのFinite Difference Time Domain法は電磁波の解析などに用いられるコードであるが、ケース2のVTI RTMカーネルでは、レジスタやSMEMの制約でSMあたり42ワープしか入らない。

最初の設定は、スレッドブロックは32×16スレッドという構成であり、16ワープである。このスレッドブロック2個はSMに入るが3個だと48ワープとなりSMに入らない。従って、2スレッドブロック32ワープを実行することになる。しかし、32ワープではメモリスループットの37%しか有効利用できないという。

これを32×8スレッドという半分のサイズのスレッドブロックにすれば1スレッドブロックが8ワープとなり、5スレッドブロック40ワープを1つのSMに入れられる。32ワープから40ワープと並列実行するワープが増えることによりメモリアクセスが25%増え、1.28倍のスピードアップが得られたという。

ブロックサイズが大きい場合は、隙間ができて32ワープしかSMに入らないが、ブロックサイズを半分にすれば、40ワープ詰め込め、性能が1.28倍にあがるという例 (2)