キューを使った処理分担

複数のGPUが処理を分担する場合、新たな仕事が出てくるとキューに入れ、空きができたGPUはキューの先頭から次の処理の指令を取り出して処理をしていくというやり方が使われる。

  • DGX-2

    GPUに処理分担を行わせるにはキューを使うことが多い。仕事が発生するとキューに詰め込み、手すきのGPUはFIFOのキューから仕事を取り出して処理を行う

新たな仕事がでてくると、キューのHeadポインタを進めて、新しい仕事を書き込むエリアを作る。そして、新しい仕事のデータを書き込む。これが新しい仕事をキューに入れるPush動作である。

  • DGX-2

    新しい仕事が出てくると、Headポインタを1つ進めて書き込みエリアを作り、そこに新しい仕事を書き込む

キューから仕事を取り出すには、Tailポインタを1つ進めて、その場所から仕事の指令を取り出す。これがPop操作である。

  • DGX-2

    仕事を取り出す場合は、Tailポインタを1つ進めて、元のTailの位置から仕事を取り出す

通常はこれで良いのであるが、空のキューへのPushとPopが同時に起こった場合はうまく行かない。

  • DGX-2

    このやり方は、PopとPushが同時に発生した場合にはうまくいかない

アトミックアクセスを使って、排他処理行えば正しく動作するが、排他処理のオーバヘッドが大きい。

この問題を解決するには、2つのHeadポインタ(あるいは2つのTailポインタ)を使う。Pushの方は、Outer Headポインタを進めて、それが指す場所に新しい仕事のデータを書き込む。そして、Inner Headポインタを進める。

一方、Pop側では"tail"=="Inner Head"であれば、何もしない。そうでなければtailポインタを進める。そして、新しい仕事のデータを読み出す。つまり、キューが空であれば、Popは無効になり、空のキューからロ見出しをおこなうことはない。そしてPushが行なわれてinner Headが進められると、Popができるようになる。

  • DGX-2

    OuterとinnerのHeadポインタ、outerとinnerのTailポインタを使えば、オーバヘッドの大きい排他処理を使わなくても、PushとPopが同時に起こったケースをうまく処理できる

複数のプロデューサーと複数のコンスーマーがある場合は、innerとouterのheadを使いアンダーフローを避ける。また、innerとouterのtailを使いオーバフローを避ける。ポインタの操作はアトミックに行い複数のプロデューサー、コンスーマーが同時にアクセスしようとしても正しく動作するようにする。

NVLinkはアトミックなアクセスができるので、このようなポインタ操作は簡単であるが、アトミックなメモリアクセスをサポートしていないPCIeでこれを実現するのは容易ではない。

  • DGX-2

    OuterとinnerのHeadポインタを使いアンダーフローを避け、outerとinnerのTailポインタを使いオーバフローを避ける。ポインタのアクセスはAtomicに行う。NVLinkはAtomicなメモリ操作をサポートしているので問題は無い

NVLinkの場合、コンスーマーの数を増やすのは容易である。性能的に制約になるのはキューとコンスーマーの間のメモリバンド幅である。

  • DGX-2

    コンスーマーの数の制約は、キューとコンスーマーの間のバンド幅の制約によって決まる

次のグラフは横軸がGPUの数で、縦軸が使われたメモリバンド幅である。

  • DGX-2

    横軸はGPU数で、縦軸はスループットである。GPUの数が増えると処理バンド幅もリニアに伸びている

次のグラフはキューアクセスの競合による制約を示すもので、横軸はコンスーマー数である。最初はコンスーマーが増えるとスループットは上がっていくが、60~70コンスーマーがベストで、それ以上になるとキューアクセスの競合のためにスループットが下がってしまう。多数のGPUがキューのheadをアクセスするのでメモリが飽和してしまうからである。

  • DGX-2

    横軸のコンスーマーの数が増えると、最初はスループットが増えるが、60~70コンスーマーを超えるとキューアクセスの競合でスループットが低下してしまう

この問題は、キューアクセスができなかったときに、ただちにキューアクセスを繰り返すのではなく、バックオフ時間を入れて、しばらく待ってから次回のアクセスを試みるという方法を取ることで改善できる。次のグラフはパケットの処理時間はゼロで、キューが取れなかった場合は66μsのバックオフを入れた場合のスループットを示すものである。前のグラフと比較すると、100コンスーマー以上のところではスループットの低下が抑えられている。しかし、不必要にバックオフ時間を大きくすると処理のレーテンシが大きくなってしまうという問題がある。

  • DGX-2

    キューが取れなかった時には66μs待って次のキューアクセスを行うようにすると、100コンスーマー以上の領域でのスループットの低下が抑えられている

他のGPUのメモリのアクセスがローカルのアクセスのように見なせるのはNVSwitchのお蔭である。しかし、NVSwitchの総バンド幅は2TB/sであるし、もし、全部のGPUが1つのGPUと通信しようとする場合は137GB/sのバンド幅を全部のGPUで分け合うことになってしまう。

また、NVLinkのバンド幅の問題ではなく、1つのメモリアドレスへのアクセスの競合が問題を引き起こす場合もある。何しろ、260万スレッドあるのだから、メモリアクセスの競合がひどくなることは十分にあり得る。

  • DGX-2

    NVSwitchのバンド幅は2TB/sであり、全GPUが1つのGPUと通信しようとすれば137GB/sを全員で分け合うことになる。また、260万スレッドもあるので、あるアドレスにアクセスが集中すると大きく性能が低下することが起こる

高い性能を実現するためにはスレッドやデータをすべてのGPUに分散することが重要である。データを1つのストレージGPUだけに格納するのではなく、全部のGPUに分散して格納すればメモリアクセスの競合が減少する。また、スレッドを全GPUに分散すれば一部の計算GPUにトラフィックが集中してしまうことも無い。

  • DGX-2

    データは全部のGPUのメモリに分散して格納し、仕事を分担するスレッドはすべてのGPUにバラまくように配置すれば、メモリアクセス、計算能力へのアクセスの競合が起こりにくく、高い性能を維持しやすい

DGX-2のユニファイド仮想メモリは、複数GPUに跨る単純なメモリモデルを実現してくれる。しかし、リモートメモリのトラフィックはL1キャッシュを使用する。VoltaはSMごとに128KBのL1キャッシュを持っているが、このキャッシュはコヒーレントではなく、書き込みの後、一貫性を保つためにはfence命令を使う必要がある。

ユニファイドメモリは便利であるが、ローカルやリモートのメモリを明示的に管理することにより、NVSwitchに頼るよりも性能を改善できる場合もある。

  • DGX-2

    ユニファイドメモリは複数のGPUに跨るメモリを単純な1つのメモリに見せてくれる。しかし、L1キャッシュはコヒーレントではなく、書き込みの後にはfence命令が必要である。また、場合によっては明示的にローカルとリモートのメモリを管理したほうが高い性能が得られる場合がある

結論

NVLinkのファブリックはDGX-2を単なる16GPUのマシン以上のものにしている。これが単に16個のGPUを並べただけのマイニング用のマシンとDGX-2の違いである。DGX-2は優れたストロングスケーリングができるマシンである。

また、マルチGPUのプログラミングのオーバヘッドが小さく、ナイーブなコードでもうまく動く。その点で少ない努力でも報酬が大きいマシンであると言える。

ユニファイド仮想メモリは全GPUのメモリを1つのリニアなメモリとして見せてくれて使いやすい。

  • DGX-2

    NVLinkのファブリックがある点が、DGX-2とGPUを並べただけのマイニングマシンとの大きな違いである。DGX-2はユニファイドメモリで使いやすく、ナイーブなコードでも性能が出る。そして、ストロングスケーリングにも優れている

NVIDIAのDGX-2は16個のVolta GPUを搭載し、強力な演算能力を持っている点に目が行きやすいが、この発表で強調したように、NVLinkとNVSwitchによる16GPU共通のリニアなメモリモデルの提供は、プログラムの作り易さの点で画期的な改善である。また、ストロングスケールがやり易く、ナイーブなコードでも性能が出る点もプログラミングのやり易さに貢献している。