1カ月集中講座

骨まで理解するPCアーキテクチャ(GPU編) 第4回

~GPGPU性能引き上げのカギとなるCPUとGPUの連携

 最終回となる今回は、CPUとGPUの連携について説明したいと思う。

 そもそもGPUが単に2Dグラフィック出力を行なうデバイスだった時代には、CPUとGPUの間の連携を考える必要はあまりなかった。

 というのはGPUは出力専用デバイスだったので、CPU側は単に描画コマンドをGPU側に投げるだけで良く、CPUが受け取るのは最終的に描画コマンドの完了通知だけであった。これは極端な話、割り込みを1つ受けるだけでも済むから、帯域云々の議論は皆無だ。CPUは自分の都合で処理を進め、GPU向けの描画命令を作り終わったらまとめて渡すという流れである。よって、GPUに求められることは、まずは「いかに受け取った処理を速やかに終わらせるか」で、次が「いかにCPUの処理負荷を減らすか」である。この2番目が、2Dの描画アクセラレータとして実装された要因である。

 しかし3Dになると少しだけ話が変わってきた。というのは描画命令に加えて、3Dのポリゴン表面に貼り付けるテクスチャデータが必要になったからだ。このテクスチャデータはものによっては非常にデータ量が多い。ビデオカードのオンボードメモリには乗り切らない可能性もあり、かつ処理に応じて任意のテクスチャを貼り付けるので、これをCPU側で管理するのは難しい(よって処理が間に合わない)恐れがあった。

 そこでIntelが音頭を取って開発したのが「AGP」である。テクスチャそのものは本体のメモリ上に格納しておくが、必要に応じてGPU側にDMA(Direct Memory Access)で高速に転送できるようにする、という発想で作られたものである。

 最初のAGP 1Xの転送速度は266MB/secで、以後2Xで533MB/sec、4Xで1.06GB/sec、8Xでは2.13GB/secまで転送速度は引き上げられたが、面白いのはこの速度はあくまでCPU(より正確にはメモリ)→GPUの速度であって、逆にGPU→CPUの転送速度は1X~8Xの全てで266MB/secで固定されていた。要するに必要なのはCPU→GPUの速度であって、逆はどうでもいい(これは言いすぎかも知れないが)というわけだ。

 余談になるが、これで効果があったかといえば、あまりなかった。というのは、AGPが登場したのは1996年のことだが、この時期の代表的なビデオカードの1つということで、AGP 2Xに対応したNVIDIAの「RIVA TNT」のオンボードメモリの帯域は880MB/sec、AGP 4Xに対応した「RIVA TNT2」では2.4GB/secに達しており、AGPの転送速度の2倍以上になっている。搭載メモリ量もRIVA TNTで8~16MB、RIVA TNT2で16~32MBになっており、当時の3Dゲームのテクスチャを格納するのは十分なサイズだった。

 これは続く世代でも同じで、結局3Dゲームの必要とするメモリ量に合わせる形でオンボードメモリを増やしてきた(もしくはオンボードメモリ量の増加傾向を見越してゲームが構成を変えてきたのかも知れない)結果、AGPは3Dゲームのロード時にまとめてテクスチャの読み込みを行なうのに使われる程度で、ゲーム中にAGP経由で新規のテクスチャをロードするといった使われ方はほとんどなかった。これはその後、AGP 8X→PCI Expess Gen1に切り替わった時も、帯域は理論上倍(AGP 8X:2.13GB/sec、PCIe Gen1 x16:4GB/sec)になったにも関わらず、ゲーム性能に全く影響しなかった事からも窺い知れる。

 実のところ、こうした状況は現在もあまり変わっていない。例えばSLIやCrossFireを構成するとき、PCIe x16ではなくPCIe x8+x8、あるいはPCIe x8+x4+x4構成で接続したり、あるいはスロットこそx16相当ながら内部には信号がx1しかきてないといったマザーボードを使ったりしても、性能の劣化はほとんどないあたり、要するに1度GPUで描画を始めてしまうと、いちいち通信なんかしない(したら遅くなる)。結果的にインターフェイス速度は描画を始める前しか影響がないわけで、その意味で言えば、こと3D描画に関する限り、CPUとの連携とかを考えてもあまり意味はない(AMDのMantleはまた別の話である)し、もちろんデータ量やピクセル数、トライアングル数やテクスチャなどの絶対量が1996年ごろから桁違いに増えている昨今では、それなりに高速なインターフェイスは必要なものの、相変わらずCPUとGPUはある意味独立して動いていると言ってそう間違いではない。

 こうした話が変わってくるのは、GPGPU的な要素が出てきたからだ。GPUとして使う場合、演算結果(つまり最終的に生成した映像)をCPU側に戻す、という処理は基本的になかったが、GPGPUとして使う場合は演算結果を再びCPUに戻さなければならない。従って、まずはGPU→CPUの転送性能を上げなければならない。幸いにもAGPからPCI Expressに切り替わったことで、CPU⇔GPU間の転送性能は上りも下りも理論上同じになったから、特に戻しが遅い、というわけではなかった。

 次に、「そもそもPCI Expressが遅い」という話が出てきた。これには2つの側面がある。

 1つは絶対的な帯域である。最初のGPGPU向け製品となったNVIDIAのC870の場合、ピーク性能で430GFLOPSということになっている。すごくラフに、1FLOPS=32bit(4バイト)の結果を生成すると考えると、ピークで1.72TB/secの結果生成が行なわれる計算になる。もちろんオンボードメモリですらこんな帯域は持ち合わせていない(ピークで76.8GB/sec)から、演算効率をやや落とさざるを得ないのだが、オンボードのメモリは1.5GBしかないから、これを使い切ると当然ながらPCI Express経由でメインメモリに書き出さないといけない。こちらの帯域は(C870がPCI Express Gen1対応ということもあり)わずか4GB/secである。

 つまり、ある程度大きなデータ量の計算を行なおうとすると、430GFLOPSどころか1GFLOPS程度の性能に律速されてしまうことになる。この帯域のボトルネックというのは現在も引き続き存在するネックである。何しろPCI Express Gen2でも8GB/secだから2GFLOPS相当、Gen3でも16GB/secだから4GFLOPS相当にしかならない訳である。

 だからといってこれはどうしようもないので、プログラムの書き方を工夫して「なるべく転送を減らす」ことを心がけるしかない。例えば図1の様に「Y=AX^2+BX+C」という計算をGPUでやらせるのに、ベタに書いて、

1.AとXを渡してAXの2乗を計算
2.BとXを渡してBXを計算
3.これをCPUでまとめてYを算出

とやっていると、CPUの計算量も多いし、煩雑に転送が発生する。

 そこで最初にA/B/C/Xを全部渡し、全ての計算をGPUにやらせて、最後に結果だけ受け取るようにすれば、転送のオーバーヘッドが大幅に減る。ちなみに図1では、転送よりも計算の方が時間がかかるように描かれているが、実際には計算よりも転送の方が時間がかかる。どれだけ転送を減らし、まとめてGPUに計算させるかが、GPGPUのプログラミングでは大きなテーマになっており、これは今も変わらない。

【図1】データ転送量を減らす工夫

 もう1つの側面は、同期の遅さである。例えば図2のように、CPUが複数のスレッド(これはNVIDIAやAMDが言うスレッドとは異なり、プログラム言語としてのスレッドの方である)をGPU上で動かすことを考える。こうしたやり方では、全てのGPUのスレッドには同程度の負荷を与えることで、特定のスレッドが長時間動くような事態を避けるように工夫するが、例えばオンボードメモリにアクセスが集中するから、スレッドによっては多少メモリアクセスの待ち時間が長くなって処理時間が増えるといったことは存在するので、各スレッドの終了時間は多少ばらつく。

【図2】スレッドの待ち時間

 さて、終了するとスレッドからCPUに対して「タスク終了」の通知をするのだが、PCI Express(というか、I/Oデバイス一般)に、この通知の負荷が大きく、かつ遅いという問題があった。

 実はこれに対して最初に明確な解を持ち出してきたのはIntelである。2007年のIDF Beijingのプレス向け説明会において、「Geneseo」と呼ばれるPCI Expressのコプロセッサ向け拡張を明らかにした。要するにこれは、PCI Expressに色々と手を加えて、通知や同期のオーバーヘッドを大幅に減らそうという取り組みである。これは最終的に、PCI Express 2.1という形でPCI Express Gen2の拡張仕様として標準化された。「では昨今のGPGPUは全てこれを使っているか」と言えばさにあらず。提案したIntel自身も「Xeon Phi」ではこの拡張仕様に未対応だし、AMD/NVIDIAともに積極的に使おうという動きを見せていない。

GPGPUで重要になるCPU-GPU間の通信

 ということで、ここからが本題である。CPUとGPUの連携に関して、最初に動いたのはAMDである。AMDによるATIの買収は、単にGPUコアを入手するだけでなく、将来、アクセラレータとしてGPUを使うことを想定してのものであったのは、後藤氏のこちらの記事にもある通り。実際に、買収直前に行なわれたインタビュー記事を読み直すと、趣深いものがある。

 ただ、このインタビューの中で故Phil Hester氏(2013年9月に逝去された)が述べていた「将来、もっとタイトな統合もできる」という話は、例えるなら図3のような構成であった。

【図3】AMDが構想したCPUとGPUの統合

 要するにCPUパイプラインの中にGPU(=シェーダ)を組み込むといったものだ。もっともこの図はあくまでもCPUから見たGPUのポジションを示しただけで、GPUはGPUでデコーダ/スケジューラ/ディスパッチャといったユニットを別途持つ構想だったのかも知れないが、このあたりまでは不明だし、それはそれほど重要ではない。重要なのは、CPUとGPUが簡単にデータ交換や同期、メモリ共有を行なえる仕組みを持ち込むということである。

 AMDがやはり2007年に発表したSSE5はこの良い例で、16bitの浮動小数点演算やFMA(Fused Multiply Accumulate)命令などは、GPUとのデータ交換を前提にしたフォーマットと言っても過言ではない。最終的にAMDはSSE5を放棄してAVX互換としたが、拡張命令としてXOP/CVT16/FMA4を新たに定義した(こちらの記事を参照されたい)あたりは、このGPUとのデータ交換を引き続き重視していることの現れと考えて良いと思う。とはいえ、流石に図3のような構成を取るには、CPU側もGPU側も技術的な挑戦が多すぎるのは明らかで、もう少し現実的な路線にする必要があった。

 この「現実的な路線」の中には、外付けGPU(Discrete GPU)を統合することを放棄するという項目も含まれていた。先ほども書いたが、仮に同期の遅さを何とかして解消できたとしても、帯域の不足は補いようがないからだ。後述するようにメモリ空間を共有できるようにした場合、キャッシュコヒーレンシを取るためには、PCI Expressは絶望的に遅すぎる。そこでAMDは、オンチップのグラフィックに限った形でCPUとGPUの統合を進めることにした。

 ここからAMDはしばらく苦闘することになる。2008年末に発表されたロードマップで、APUという名前になった最初の製品が登場するのは2011年と予告された。最初のシリコンのお披露目は、2010年のCOMPUTEXのタイミングで行なわれている。ただ、このときの「Llano」はCPUとGPUが一体でダイ上に搭載され、かつメモリコントローラが共通化されたのが主要な特徴であるが、逆に言えばそれだけという言い方もできる。内部構造については下記の記事に詳細な解説があり、この世代のCPUとGPUの連携に関する特徴の最大のものは下記記事中の冒頭の図「Llanoのアーキテクチャ」に集約されているとも言える。具体的に言えば、「Onion」と「Garlic」という2種類の内部バスが混在していることだ。この理由については、下記の2番目の記事に解説があるが、もうちょっと基本的なところから説明し直したい。

 「Fusion」という名前で統合のプランを練ったCPUとGPUは、最終的にOpenCLをベースとした「HSA」(Heterogeneous System Architecture)という形で統合されることになった。OpenCLは、図3に出てくるような命令セットレベルより、もう少し大きな枠での処理分散の仕組みになる。ちょっと記事が前後するが、HSAの核となるのはHSAILと呼ばれる中間言語で、このHSAILがハードウェアの差異(例えばCPUとGPU)を吸収してくれる。具体的な実装はこちらの記事に詳しいので、ここではその詳細は置いておく。要点は、HSAILを利用することで、アプリケーション的にCPUとGPUの差異がなくなっていくことである。

 こうなってくると、これまでGPGPUをPCI Express経由でアクセラレータ的に使ってきたスキームでは効率が悪い。まずはメモリの問題。PCI Express経由とする場合、GPUはあくまでもI/Oデバイスの扱いになる。その場合、GPUがアクセスできるメモリ空間に限りがある。図4は簡単な模式図であるが、物理メモリはプログラム(=プロセス)に割り当てられるメモリ空間と、I/Oデバイスに割り当てられるI/O空間という2つに分割される。

【図4】CPU、GPUがアクセスできる領域

 CPUはどちらにもアクセスできるが、GPUの方はI/O空間のみにしかアクセスできないという問題がある。「ではプロセスもI/O空間だけを使ってプログラムを走らせれば?」というのは無理な相談で、元々この領域はI/Oデバイスとの通信用に設けられた特殊な領域なので、プロセスからアクセスはできるといっても、本来のメモリ空間と同じように扱えるわけではない。よって、旧来の方式では、

1.CPUが元データをメモリ空間からI/O空間にコピー
2.I/O空間にGPUがアクセスして処理を行ない、I/O空間に結果を書き戻す
3.CPUが結果をI/O空間からメモリ空間にコピー

という面倒な手順になっていた。ところがHSAの環境は、そもそもこうしたCPUの介在なしにGPUが直接メモリ空間からデータを取得し、かつ結果を格納できることを前提としたものである。そうでないとオーバーヘッドが大きすぎて困るからだ。

 そこで、従来型のアクセスに加えて直接メモリ空間にアクセスできるための機能をGPUに追加しよう、という話になるのだが、ここで問題になるのは

・ページフォールト機能の追加
・キャッシュコヒーレンシ機能の追加

となる。

 まずページフォールト。CPUコア上で、あるプロセスがある仮想アドレスにアクセスしようとして、その仮想アドレスが物理メモリにマッピングされていない場合、CPUはページフォールトという割り込みを発生させる。これを受けて、OSがその仮想アドレスに物理メモリを割り当てるわけだが、GPUにもこれと同じ機能が必要になる。ところがI/Oデバイスがページフォールトを起こすことは、従来の構造ではそもそも考慮していなかった。そこで、既存のI/Oデバイス用のMMU(Memory Management Unit:メモリ管理ユニット)を拡張し、I/Oデバイスからのページフォールトを受け付けるような拡張が必要になった。これが「IOMMU v2」と呼ばれるものである。

 次はキャッシュコヒーレンシについて。既存のマルチコアCPUでは、CPUコアに付属するキャッシュ(L1/L2キャッシュ)についてはキャッシュコヒーレンシが保たれている。日本語では「キャッシュの一貫性」と訳す。例えば図5のような構造を考えてみよう。4コアのCPUだが、1コア毎に共有L2キャッシュを持っているケースだ。

【図5】キャッシュコヒーレンシの概念

 ここでCPU #1があるメモリ領域に対して書き込みを行なったとする。その場合、まずはL1キャッシュに書き込まれ、これはL2キャッシュ経由でメモリに書き出される(図5-1)。この時に、もしCPU #2~#4が同じメモリ領域をキャッシュしていたとすると、実際のメモリの値とそれぞれがキャッシュしていた値が食い違うことになる。そこで、こうしたケースではそれぞれのキャッシュに対して、同じ値になるように書き換えを行なう(図5-2)。これにより、全てのキャッシュの内容に食い違いがないようにする(一貫性を保つ)というメカニズムがキャッシュコヒーレンシである。さて、ここまでは話が単純であるが、ここにGPUが加わると、今度はCPU⇔GPU間のキャッシュコヒーレンシも取るような仕組みが必要になる。

 「取るような仕組みが必要になる」とさらっと書いたが、これを実現するためにはAMDとしても3世代に渡る実装が必要になった。「Llano」→「Trinity」→「Kaveri」とどう進化したかは下記の記事に詳しい。

 簡単にまとめると

Llano:Onion/Garlicの2つの内部バスを設けることで、GPUコアのメモリへの広帯域アクセスと、限定的なGPU→CPU方向のキャッシュコヒーレンシを取る仕組みのみが実装された。

Trinity:基本構成はLlanoと変わらないが、IOMMU v2が実装されてGPU側のページフォールトがハードウェアレベルでサポートされた。またOnionのバス幅が倍増された。

Kaveri:新しく「Onion+」というバスが追加され、CPU→GPU方向のキャッシュコヒーレンシを取るための仕組みが実装された。

といった形だ。

 ちなみにKaveriもまだ、ある意味で中途半端である。先の図5に戻るが、図5-1であるメモリ領域が変更された場合、図5-2で「そのデータをキャッシュしている場合に書き戻しをかける」(この操作をスヌーピングと呼ぶ)のだが、このスヌーピングの際には一般に「そのアドレスをキャッシュしているかどうか」を確認する方法が必要になる。なぜならある領域を書き換えたときに、無条件にそのほかのコアのキャッシュに書き換えリクエストを出していたら、トラフィックが膨大な量になるし、そもそもそのアドレスを、もしキャッシュしていなかったとすれば、スヌーピングそのものが無駄になるからだ。

 ところがKaveriの世代においても、CPUからGPUのキャッシュ領域を確認することができないので、そのままだと猛烈なトラフィックが発生することになってしまう。これを避けるため、KaveriではOnion+を経由する場合は、GPUのL2キャッシュをバイパスするという形で実装を行なっている。このあたりがまだKaveriと言えど完全なヘテロジニアス構成にはなりきっていない部分ではあるのだが、このあたりを解決するにはもう少し時間が必要になるであろう。

 ただ、不十分であっても一応HSAの基礎が整ったことで、CPUとGPUで負荷分散を行なう仕組みが整ったことになる。これに併せて「hUMA」や「hQ」といった仕組みもKaveriに入れ込んだ事で、OpenCLベースのTaskに関する限り、CPUとGPUを並列に扱えるようになった。

 現在のAMDの問題は、ハードウェアよりもむしろソフトウェア面である。先にGPU側のページフォールトが「ハードウェアレベルで」サポートされたと書いたのは、ソフトウェア側の対応がまだ整っていないためだ。これはOS側の問題であり、実のところ現在のWindowsではHSAをフルに活用できない。これに関してはAMD単独ではどうしようもなく、色々OSベンダーと協議しているとは言っているが、具体的な実装スケジュールなどはまだ明確ではない。

 こうしたAMDの動きとは対照的に、後手に回ったのがNVIDIAである。CUDAがアクセラレータとしてGPUを利用するAPIとして広く普及してしまい、逆にこのモデルからの脱却が難しくなった、という側面もあるのだが、最大の理由はGPUしか持っていない(CPUを持ち合わせていない)点にある。もちろん2010年にPC向けチップセットから撤退を表明する前に、ARMベースのSoCに製品展開を切り替えて行なったのは以前こちらの集中講座で紹介した通りだ。

 ただ現在の「Tegra」シリーズは、製品ターゲットがモバイル向けとなっていることもあり、絶対的な性能は低い。少なくとも同社の「Tesla」のホストとして動かせるようなCPU性能もI/O性能も持ち合わせていないから、モバイル向けはともかくHPCなどCUDAの主戦場では引き続きIntelのプラットフォームを利用する必要がある。そうなるとPCI Expressベースのアクセラレータカードという以上のものに進化するのは物理的に難しい状況である。

 ただそのままの状況を放置する訳にも行かないので、アクセラレータとしての性能を高めつつ、かつCUDAの構成を少しずつほかの環境に対応出来るように切り替えてゆく必要がある。その兆候は例えば下記の記事からも読み取れる。

 PCのメモリとコヒーレンシを取るなど高速な転送を行なうことが短期的に不可能であれば、とにかくカードの上に大量に高速なメモリを積むことで性能を改善するしかなく、そのための策がTSV(Through Silicon Via:シリコン貫通ビア)を使った3DスタックドDRAMの利用や「OpenACC」の推進である。

 このうちTSVの3Dメモリは、2013年のGTCにおいて、「Volta」で実装されることが明らかにされた。また、Voltaと同じ時期に投入される、次々世代のTegraであるPascalが、やはりこの3DスタックドDRAMを利用することが明らかにされている。

 またCPUとの協調に関しては、Maxwell世代からCPUコアを統合していく方向になっており、おそらく2015年に投入されると予想されるMaxwellのハイエンドGPUには同社の「ARM v8」コアである「Denver」ベースのものが搭載されるだろう。もちろんこれに関しても、AMDと同様にキャッシュコヒーレンシに関する仕組みを仕込んでいく必要はあるが、CUDAそのものがまだHSAほどCPU/GPUの協調動作を前提としていないから、それほど複雑な仕組みは「今のところ」必要ではない。これに先立って、2013年5月に発表されたCUDA 5.5ではARMへの対応が実装されている。現実問題、CUDAを利用するためにはG80系以降のコアが必要であり、G70ベースのTegra 4までは意味がない。Tegra系では「Tegra K1」が初のCUDAベースの製品となるが、MaxwellへのDenverの実装もこれに準ずる形になるのではないかと思う。

 さらに、よりハイエンド向けにNVIDIAが発表したのが「NVLink」である。こちらの記事に詳細があるが、GPU-CPUおよびGPU-GPU用のインターコネクトであり、IBMのPowerプロセッサがこのNVLinkに対応することも報じられている。

 NVLinkの詳細は不明(物理層はともかくプロトコル層が非公開)なので断言はしにくいが、少なくともPCI Expressの延長にはないようだ。ということは、NVLinkで接続されたデバイスは、既存のカードと異なりI/Oデバイスの扱いにならない可能性がある。PCI Expressでは不可能だったキャッシュのスヌーピングなども実装出来る可能性がある。またNVLink自体は信号速度も高速であり、80~200GB/secという帯域が可能となっている。これは3DスタックドDRAMに比べれば遅い(こちらは1TB/secを狙わんばかりの勢いだ)が、旧来のPCI Expressに比べればずっと高速であり、PCI Expressと同じような使い方をしても大幅に実効性能を引き上げられることを期待できる。

 AMD/NVIDIAともに、GPGPUの性能をどう引き上げるか、という根本部分は同じなのだが、解決法が大幅に違うのが面白いところである。最大のポイントは、すでにCPUを持っているか否かで、これがそのままソリューションの違いとして見えているわけだ。最近AMDはHPCマーケットを捨てた、という言われ方がしばしば(HPC業界の人から)聞くが、AMDのソリューションはある意味で外付けGPUカードを捨てたソリューションであり、そう言われても仕方がないところだ。逆にNVIDAのソリューションはHPCに特化する方向に進んでいるとも言えるわけで、あとはそのマーケットだけで本当にビジネスができるのかが問われることになるだろう。

(大原 雄介)