十分な並列性の確保

GPUはCPUと比べて高い演算性能をもっているが、その理由は、個々の演算器の性能が高いわけではなく、単純に演算器がたくさんあるからである。従って、高い実効性能を引き出すためには、たくさんの演算器と高バンド幅のメモリを遊ばせることなく、有効に使うプログラムが必要となる。

Keplerの演算パイプラインをフルに動かすには、32bit浮動小数点演算の場合、SMあたり約1700命令が必要。また、メモリバンド幅をフルに使用するにはSMあたり100以上のメモリアクセスが必要 (2)

KeplerのSMには192個の32bit(単精度)浮動小数点演算器があり、演算レーテンシは10~20サイクルかかる。この間は別の演算を実行する必要があり、ワープのすべての命令が直前の命令の演算結果を使用する場合は、1920~3840命令を必要とする計算になる。しかし、実際には直前の命令の演算結果を使わない命令もあり、この図では、1700命令程度あれば良いということになっている。しかし、1つのSMが実行できるのは最大64ワープ、2048命令であり、この内、1700命令を埋めるというのはかなり高いハードルである。

また、常時、100以上のメモリアクセスが並行に実行されなければならず、そのためには多くのワープをSMに詰め込む必要がある。ただし、SMのレジスタファイルは64Kエントリであり、SMEMは最大48KB、命令は64ワープ、16並列スレッドスレッドブロックという資源制約があり、この範囲でより多くのワープをSMに詰め込むということが必要になる。

なお、この1700スレッドと100+のメモリアクセスという数字は目安であり、最大性能を実現するにはこれらの数字を100%満足する必要があるとは限らず、必要な並列性が達成されれば、それ以上に並列性をあげるプログラム改造をおこなっても性能は向上しない。

並列性を上げるには、1つのSMの並列性を上げることと、チップ上の全SMにバランスして仕事を割り当てることが必要。これを実現するには、ブロックのスレッド数、ブロック数、ブロックの仕事量を調整することが必要となる (2)

Kepler GPUではワープは32スレッドと決まっているので、スレッドブロックのスレッド数が32の倍数でなく、半端が出るとダミーのスレッドを入れて32スレッドに切り上げられてしまう。スレッドブロックのスレッド数が小さい場合は、このダミーのスレッド分による性能低下が無視できない。従って、可能な限り、スレッドブロックのスレッド数は32の倍数にすることが望ましい。

また、次の図のように、スレッドブロックに含まれるスレッド数が少なすぎると、スレッドブロックを最大限詰め込んでもガイドラインの1700スレッドに届かず、十分な並列性が得られない。一方、スレッドブロックのスレッド数が多すぎると、ケース2のところで述べたように、2つのスレッドブロックではかなり空きが残るが、3スレッドブロックは入らないということが起こり、このケースも十分な並列性が得られない。

スレッドブロックのサイズ(含まれるスレッド数)が少なすぎても、多すぎても十分な並列性が得られない (2)

次に、Kepler GPU全体で考えると、最大性能を引き出すためには、すべてのSMを有効に利用することが重要である。

次の図のように、仮に8個のSMのGPUがあり、各SMは1個のスレッドブロックしか実行できない規模であったとする。このGPUに12スレッドブロックを含むグリッドを処理させると、最初のwave 0では8個のSM全部に仕事が行きわたるが、Tail(尻尾)となるwave1では4個のSMしか仕事が無く、GPU全体で見ると75%の利用率にしかならない。

図を簡単にするため、チップに8SMで、1個のSMでは1つのスレッドブロックしか実行できないとする。これに12スレッドブロックのグリッドを実行させる場合、最初のwave 0ではすべてのSMを使うが、Tailとなるwave 1では4個のSMしか仕事が無い

Tailの影響は実行するスレッドブロック数が少なくWave数が少ない場合に顕著になる。また、スレッドブロックの数がSMの数の倍数になっていても、スレッドブロックの実行は完全には同期しないので、Tailの影響が出る場合がある (2)

実行するスレッドブロック数がSM数の整数倍であっても、スレッドブロックの実行は完全には同期しないので、Tailの影響が出てくる場合がある。従って、Tailの影響を小さくするためには、グリッドに含まれるスレッドブロックの数を増やし、Wave数を多くすることが望ましい。

そのやり方であるが、1つのスレッドで実行していた処理を複数のスレッドに分割してスレッドブロック数を増やす、あるいは、1つのスレッドブロックに含まれるスレッド数を減らすという手が使える。また、別のストリームのカーネルを同時に実行させて、SMが分担するスレッドブロックの数を増やすという手もある。

上のように大きなスレッドブロックで実行するとWave数が少なくTailの影響が大きい。下の図のように、スレッドの仕事を減らして小さなスレッドブロックとして数を増やすと、Tailの影響を減らすことができる (2)

以上のように、Kepler GPUの性能を引き出すためには、スレッドブロックのサイズを適当に選ぶことが必要である。目安としては、スレッドブロックに含まれるスレッドの数は、ワープサイズの32の整数倍で、128~256スレッドとしておき、その後、チューニングでこの値を振ってみるのが良いという。

そして、Tailの影響を減らすにはスレッドブロック数を多くする必要があり、グリッドに含まれるスレッドブロック数は1000、あるいはそれ以上とするのが良い。将来のGPUは、現在のKepler GPUよりも集積度が上がってSM数が増加すると考えられ、グリッドのサイズを大きくしておくことは、将来のGPUでもWave数を確保しTailの影響を抑えるという点でコードの寿命を延ばす効果が期待できる。