L1データキャッシュ

V100 GPUのL1データキャッシュは、SMあたり128KBのメモリを分割してシェアードメモリと共用している。FermiやKeplerでは、この構造であったが、P100ではシェアードメモリは独立の構造で、L1データキャッシュはテクスチャキャッシュとメモリアレイを分割して共用するという構造になっていた。P100が出た時は、この構造はL1データキャッシュとシェアードメモリが並列に動作できるので、バンド幅が高いというメリットが強調されていたのであるが、あれはどうなってしまったのだろうか?

P100のL1データキャッシュはテクスチャキャッシュと同じリードオンリーのキャッシュであったので、コヒーレンシの問題は無かったが、V100でリードライトのキャッシュになると、どうやって各SMに存在するL1データキャッシュのコヒーレンシを取るかが問題になる。

80SMのコヒーレンシをハードウェアで維持するのは大変なので、多分、ソフトウェアでコヒーレンシを維持するのであろうが、Fermiの時のようにドライバが介入するのでは速度が遅くなってしまうが、それをどのように回避しているかは興味深い。

P100でデバイスメモリをアクセスするコードは、シェアードメモリを使って最適化したコードの70%程度の性能しか得られないが、V100ではL1データキャッシュを使うことにより93%の性能が得られたという。一例ではあるが、L1データキャッシュを使うことでシェアードメモリを使うというチューニングをしなくても3割性能が上がるのは手間が省けてうれしいことである。

SIMTモデルの変更

NVIDIAが採用したSIMTアーキテクチャは、SIMDのように多数の演算器が同じ命令で動き、かつ、ひとまとまりスレッドの中で分岐方向が異なる分岐をプレディケートを使って実現できるというものである。

通常は、これで問題ないのであるが、条件分岐がある部分で、ロックなどの排他制御が行われていると、if~then~else~のthen句やelse句を一まとめに実行してしまうと永久にロックが取れず、デッドロックに陥ってしまうことが起こるという。

これに対処するため、V100 GPUでは細切れにプレディケート実行を切り替えるというアーキテクチャに変更した。

この実行を可能にするため、V100では、ワープ内の32スレッドそれぞれにPCとスタックを持つという構造になった。これはSIMTから半分くらいMIMDに寄ったアーキテクチャで、SMがかなり大きくなっているのではないかと推測され、12nmプロセス化でもトランジスタ密度がほとんど改善していないというミステリの主要な容疑者であると思われる。

しかし、この変更で、どの命令まで実行し、どのようなアーキテクチャ状態になっているかをスレッドごとに管理できるようになった。

アーキテクチャ状態はデータ量が多いのであるが、レジスタファイルなどは、元々、スレッドごとに存在するので、追加で保持しなければならないのはPCとスタックだけなのかもしれない。しかし、スレッドごとにPCが違うので、そのPCの命令を供給する必要があり、SMの面積が増加すると思われる。

いずれにしても、アーキテクチャ的にはSIMTからMIMDに近づくビッグな変更である。

P100ではPCはワープで1つであったが、V100ではスレッドごとにPCとスタックを持つようになった

このようなアーキテクチャ変更を行うと、次の図のように、条件成立と不成立の場合の命令を細切れにして実行を切り替えることができるようになり、次の図のように、TeslaV100では、then句やelse句の中身を細切れに実行している。このような実行で、完全にデッドロックを防ぐことは出来ないが、デッドロックに陥る確率を大幅に減らすことができるという。

V100では、then句やelse句を一まとめに実行するのではなく、細切れに実行できるようになる

このような切り替えは、プレディケートを変えながら実行するプログラムを書けば実現できるが、各スレッドにPCやスタックを持たせるという大掛かりな変更を行ったのであるから、プログラムに変更を加えなくても、ハードウェアが自動的に切り替えを行ってくれと思われる。 そして、CUDA9では、新設の__syncwarp( )関数を呼び出すことにより、ワープの再統合を行わせる。

CUDA9で新設された__syncwarp( )の呼び出しで、分離した実行パスを再統合する

次のコードは、2重リンクのリストのノードaとノードcの間にノードbを挿入するという処理を行うものである。ノードaとa->nextをロックして、ノード間の接続を繋ぎ変えてbを挿入している。実は、このコードは、最後でcをアンロックしており、対応がとれていない。最初のロックはaとcが正しいと思われる。

2重リンクのリストのノードaとノードcの間にノードbを挿入するコード

各スレッドが異なるノードに対して作業を行う場合は問題ないが、同一ワープの中の複数のスレッドが同じノードのロックの獲得を試みた場合は、ロックを獲得できるのは1つのスレッドだけで、残りのスレッドはロックは獲得できない。結果として、ロックが獲得できたかという条件分岐で、ロックを獲得したノードは上記のノードbの繋ぎ込みを行い、ロックを獲得できなかったスレッドは、再度、ロックの獲得を試みることになる。

しかし、これまでのプレディケート実行で、ロックを獲得できなかった方のコードを先に実行してしまうと、ノードbを繋ぎこんでロックを解放するコードは何時まで経っても実行されず、デッドロックに陥ってしまう。

リスト全体をロックすれば、このようなデッドロックは起こらないが、1時には1つのスレッドしか処理ができず、超多スレッドのGPUのメリットはまったく活かせない。

これに対して細粒度で、条件分岐の両方向のコードのどちらも少しずつ実行して行けば、どちらを先に実行してもしばらくすれば、ロックが解除され、他のスレッドがロックを獲得することができる。つまり、forward progressが保証されていることが、細粒度のロックを使う上で欠かせない要件である。

MapDなどのGPUベースのデータベースが使われ始めており、細粒度ロックの重要性が高くなってきている。それに対応するために、スレッドごとのPCとスタックの記憶と、スレッドごとに命令を供給すると言う大きなハードウェア負担にも拘わらず、Voltaはワープの細粒度実行をサポートしたと思われる。

Googleは、データセンタに押し寄せる大量の推論処理の性能とエネルギー効率の改善を目的としてTensor Processing Unit(TPU)を開発したので、INT8のTensor計算を使うことにして多数の演算器を搭載したLSIを作った。しかし、GPUメーカーであるNVIDIAはHPCからディープラーニング、GPUデータベースといった広い用途に目を配って、すべてのマーケットにアピールする機能を盛り込んでV100 GPUを作っている。