SIMT実行モデルの改善

PascalまでのWarpの実行モデルは次の図のようになっており、条件分岐がある場合、まず、if側のパス1の命令が実行され、その後にthen側のパス2の命令が実行される。パス1とパス2の処理が無関係であれば、この実行で問題ない。

しかし、パス1の中にパス2のスレッドの処理結果を待つようなコードがある場合は、永久に待ってしまうデッドロックになってしまう。つまり、PascalまでのGPUでは、ワープ内のスレッドの間で通信を行う場合は、デッドロックにならないよう注意してプログラムを作る必要があった。

  • PascalのWARPの実行は、if側のパス1の命令を実行し終わってから、then側のパス2の命令を実行するというモデルであった

    PascalのWARPの実行は、if側のパス1の命令を実行し終わってから、then側のパス2の命令を実行するというモデルであった

PascalまでのGPUではPCはWarpごとに設けられており、すべてのスレッドが同じPCを使うという構造であった。これをVoltaでは、スレッドごとにPCを設け、各スレッドを個別にスケジューリングすることができる構造に変わった。

ただし、スレッドごとにPCがあるといっても、MIMDのようにスレッドごとに別の命令を同時並列的に実行できるという訳ではなく、分岐した各スレッドがどこまで命令を実行し、その時のアーキテクチャ状態をレジスタSに記憶し、次に実行すべき命令がどのアドレスであるかを示すPCを設けたのである。

  • Pascalではワープ全体に1個のPCであったが、VoltaではスレッドごとにPCと状態レジスタSを持ち、スレッドごとにどこまで実行が終わり、どのような状態にあるかを記憶し、次にどの命令から実行するかが分かるようになった

    Pascalではワープ全体に1個のPCであったが、VoltaではスレッドごとにPCと状態レジスタSを持ち、スレッドごとにどこまで実行が終わり、どのような状態にあるかを記憶し、次にどの命令から実行するかが分かるようになった

この構造変更により、if側のパス1の処理Aと処理Bの間、then側のパス2の処理Xと処理Yの間に__syncwarp();を入れた場合、パス1のAの処理が終わった時点で、パス2側の処理Xの実行に切り替わる。そして、処理Xが終わると、パス1のBの処理を行う。さらに処理Bが終わると、パス2の処理Yを行う。結果として、分岐したパス1のスレッドとパス2のスレッドが__syncwarp();を入れたところで同期することができるようになる。

  • Voltaではパス1のAの処理が終わった時の状態と次に実行を開始するパス1のBの先頭アドレスを記憶して、パス2のXの実行を開始する

    Voltaではパス1のAの処理が終わった時の状態と次に実行を開始するパス1のBの先頭アドレスを記憶して、パス2のXの実行を開始する。そして、パス2のXの処理が終わると、記憶した情報を使ってパス1のBの処理を開始する。パス1のBの処理が終わると、パス2のXの処理が__syncwarp()で終わった時に記憶した情報を使ってパス2のYの処理を行う

Pascalの場合は、ワープ内のスレッド間で通信する場合は、デッドロックに陥らないLock-Freeのアルゴリズムを使ったものしか扱えなかったが、VoltaではStarvation-Freeのアルゴリズムであれば、他スレッドを待つようなコードも使えるようになった。

  • ワープ内の他のスレッドと通信を行うプログラムは、Pascalの場合はLock-Freeのアルゴリズムでなければ使えなかった

    ワープ内の他のスレッドと通信を行うプログラムは、Pascalの場合はLock-Freeのアルゴリズムでなければ使えなかった。しかし、VoltaではStarvation-Freeであれば、他のスレッドを待つ構造のコードが使えるようになった

次の図は、よく例に使われる双方向リンクリストのAとCのノードの間に、ノードBを挿入する処理である。この例ではlock(a)とlock(a->next)で2つの資源をロックして挿入処理を行っている。そして、ノードCの挿入処理が終わるとロックした資源を解放している。

この処理を並列に実行される複数のスレッドで実行すると、ロックの競合が起こってしまうので、Pascalではうまく働かない。このため、Pascalでこの処理を行う場合は、リンクの付け替えをアトミックなCompare-Swap命令で行うようなLock-Freeなアルゴリズムに変更する必要がある。

しかし、Voltaでは、使いたい資源が他のスレッドからロックされていても、待っていればロックが解除されるので、この例のコードを正しく実行することができる。 科学技術計算では資源のロックを必要とする処理はあまり多くないと思われるが、構造が複雑なニューラルネットの処理などでは、ワープを分割して異なる処理を実行させ、それらのスレッドの間で通信を行うというケースが出てくると思われる。

  • AとCが直接つながれた双方向リンクリストのAとCの間にBを挿入する例

    AとCが直接つながれた双方向リンクリストのAとCの間にBを挿入する例。この処理はaとa->nextをlockしており、Starvation-Freeであるが、Lock-Freeではない

(次回は2月22日に掲載します)