9. バンクコンフリクトを解消する
バンクコンフリクトを避けるにはどうすれば良いか? それは意外と簡単で、次の図のように各行にtile[*][4]という余計な要素を1つ付け加えてやれば良い。
そうすると、次の図で黄色の字で示したようにtile[0][1]はバンク1、tile[1][1]はバンク2、tile[2][1]はバンク3、tile[3][1]はバンク0というように、列方向の要素が異なるバンクに分かれてくれる。従って、これらの列方向の要素をアクセスする命令を実行してもバンクコンフリクトは発生しない。
シェアードメモリのバンクコンフリクトを避けるためには、内側の次元に1つ要素(この例ではtile[*][4])を追加する。このtile[*][4]は格納位置をずらすために追加されており、データは格納しないのでメモリとしては無駄になっているが、行方向も列方向もバンクコンフリクトなしにアクセスできるようになり、シェアードメモリのバンド幅を最大限有効に使えるようにしている。
なお、この手法はGPUに特有のものではなく、GPUコンピューティング以前のCPUでも使われている手法である。
シェアードメモリの配列tileの各行に1要素を追加してバンクコンフリクトを解消したtranspose4の性能は、単精度では171.82GB/s、倍精度では225.68GB/sとなった。これはtranspose3と比べて単精度では1.7倍、倍精度では1.76倍の性能である。
transpose3ではFloatに比べてDoubleの方が性能が低かったが、バンクコンフリクトを無くしたtranspose4ではDoubleの方が性能が高くなっている。この理由は、どちらの場合も1つのワープの32のアクセスは完全に合体しており1回のメモリアクセスで処理されている。この点はどちらも同じであるが、Floatの場合は1つのキャッシュラインしか使われず、DRAMからの読み込みは4つのDRAMトランザクションであるが、Doubleの場合は2つのキャッシュラインへの読み込みが並行して行われ、8つのDRAMトランザクションが使われている点が異なる。
10.DRAMバンド幅をフルに使う
DRAMからの読み込みトランザクションを2倍に増やせば、単精度の場合も倍精度と同じ程度の性能が得られると考えられるので、1スレッドが処理する要素数を倍増して性能を調べたのが次の図の表である。
スレッドあたりのエレメント数を可変にしたtranspose5で、エレメント数を2にすると、確かにtranspose4の倍精度のバンド幅とほぼ同等の性能が得られた。また、単精度で4エレメントとすると、2エレメントの倍精度と同程度の性能が得られ、DRAMアクセスのバンド幅が性能を決めていることが分かる。
なお、16エレメント/スレッドの場合は、スレッド数が少なくなりすぎて実行するスレッドが足りなくなってしまったことで性能が下がっている。
複数エレメントを処理するtranspose5プログラムの性能は、Floatでは239.93GB/s、Doubleでは246.65GB/sに達した。これは最初のtranspose1プログラムと比較すると、それぞれ14.15倍と7.27倍の性能向上である。
また、K40のピークメモリバンド幅は288GB/sである。しかし、制御オーバヘッドなどを考慮しないデータ転送のスピードであり、実プログラムでは絶対に達成できない値である。行列の転置という処理で85.6%の性能を達成しているのは驚異的である。
まとめ
このチュートリアルで、覚えて帰るべきことは、
- データの再利用(ops/byte)を大きくしなければ、オペランドの供給がネックになる
メモリのバンド幅とレーテンシ
- バンド幅をフルに利用するには多くのin-flightのロードが必要
- GPUのメモリを使用する場合は
アドレスの合体
・Load/Storeをまとめるのにシェアードメモリを使う
レーテンシを隠すには
・占有率をあげる
・命令レベルの並列性
である。