1カ月集中講座

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

~固定機能からシェーダへの移り変わり

 最近はGPUのアーキテクチャ刷新が非常に早くなっている。特にGPGPU世代になってから急激に加速されているのだが、その結果として以前のGPUなどとは大きく違う演算装置(Computation Unit)に進化しつつある。そこでちょっと基本に立ち帰って、GPUの中身を少し分かりやすく説明してゆきたい。

 そもそも初期、GPUという名前が出てくる以前のグラフィックスコントローラのレベルまで遡ると、グラフィックスコントローラは「メモリをデータで埋める」のが主な機能であり、その意味では演算器というよりもメモリコントローラに必要な機能が追加されたもの、というレベルに近い。

 しかし、高機能化するに従って、演算器的な機能も搭載されるようになってきた。例えば「画面全体の色調を少し変える」という描画を行ないたいとする。画面が一様(図1)であれば、これは簡単だ。左の(R,G,B)=(00,00,FF)から右の(R,G,B)=(00,C8,C8)へ変更するには、フレームバッファに対して(R,G,B)=(00,C8,C8)というデータをベタ塗りすればいい。この場合は別に演算器的な機能は必要ない。

【図1】一様な画面の書き換え
【図2】一様ではない画面の書き換え

 問題は一様でない場合だ。図2のようなケースではA~Cの各ピクセルの値は

A:(R,G,B)=(00,00,FF)
B:(R,G,B)=(BA,BA,FF)
C:(R,G,B)=(FF,FF,FF)

となっており、これの色調を変えたA'~C'のピクセルの値は

A':(R,G,B)=(00,C8,C8)
B':(R,G,B)=(BA,E5,E5)
C':(R,G,B)=(FF,FF,FF)

へ変換する作業になる。このケースだと、図1のようにベタ塗りは不可能で、「フレームバッファからピクセルのデータを読み出し、その値に応じて変更後の値を計算し、それをフレームバッファに再び書き戻す」という作業が必要になる。初期のPCでは、こうした計算は全てCPU側の仕事で、色調変更後の値を1画面分計算してから、おもむろにビデオカードのフレームバッファに書き込んでいたわけだが、言うまでもなくこれでは画面更新に時間がかかってしまう。そこでグラフィックスコントローラ側にも簡単な演算器を搭載することで、こうしたピクセル値の変更を高速に処理しよう、というのがそもそもの経緯である。

 ただし、こうした初期のものは、演算器そのものは持っているものの、まだプロセッサとは遠い存在だった。というのは、

  • 演算処理そのものは行なうが、プログラムを動的に解釈する機能は持っていないか、持っていても限定的
  • 実行制御機能はほぼ無し
  • 特殊な関数などもほぼ無し

といった構成だからだ。もっと正確に言えば、演算はY=A×X+Bという積和演算に事実上限られていた。AとBは係数で、これは外部(CPU側)から設定する形である。その意味ではDSP(Digital Signal Processor)に近い(といってもDSPよりもさらに機能が乏しい)のが初期のグラフィックスチップ内部の演算器であったと言ってよいだろう。

 ちなみにこの当時、と書くと時期が明確にならないのでDirectX 6世代まで、としてしまって良いだろうが、ほとんどが並列度1の演算器構成である。いわゆるSISD(Single Instruction, Single Data)構成である。

 例えばNVIDIAの「RIVA 128」の場合、コアとメモリの周波数はどちらも100MHz、バス幅は128bitである。仮にこのバス幅をフルに活かすと、当時は16bpp(bit per pixel)のHigh Color構成だから、1サイクルあたり128÷16=8ピクセル分の描画ができることになる。実際にはメモリからの読み込みとメモリへの書き出しの両方が必要なので、帯域をざっくり半分と見積もっても1サイクルあたり4ピクセル分の描画出力が可能なはずであるが、実際にはRIVA 128のピクセルレート(フレームバッファにピクセルを書き込む速度)は最大100MPixel/secに過ぎない。つまり1サイクルあたり1ピクセルということで、その意味では16bitの積和演算器が1つだけ入っている構造である。

 これがDirectX 7の世代になると、複数の変化が起きた。まず1つはハードウェアT&Lのサポートである。T&LはTransformation & Lightingの略であるが、モデルの移動と回転(変形)の計算と、照明効果の計算を行なうことである。DirectX 6までの世代では、極端な事を言えば2Dグラフィックの延長にある。もともと3Dゲーム類は、まず仮想的な3D空間内に描画したい「物」(オブジェクト)を配置し、次いでその物を「どこからどう見るか」に相当するビューポートというものを配置、最後にその「物」にどう照明があたるかを設定する。これはDirect 3Dに限らず、CGでも何でも3次元のモデリングをする際には一般的な手順であるが、DirectX 6まではこの作業は全てCPU側(=ソフトウェア)で行なっていた。DirectX 7になって、やっとこれがハードウェア側で処理するべきものに切り替わったのである。

 これに対応するために何が必要か? というと、まずは物体の回転や移動の計算をするために、3次元のベクトル演算を行なう必要がある。この時点で、少なくとも浮動小数点の乗算と加算が必要である。また水平移動だけならばともかく回転もカバーするためには、簡単な三角関数(といってもサインとコサインだけでいいのだが)も無いと不便である。通常3次元のベクトルを扱う場合は3次の正方行列として扱うわけで、簡単に言えば、ハードウェアT&Lとは正方行列の演算をする専用の演算器が追加されたということだ。

 ちなみに、これは頂点演算と呼ばれることもある。3次元の「物」を複数の三角形(トライアングルまたはポリゴンと呼ばれる)を立体的に組み合わせて近似した上で、各々の三角形の位置を変化させることで移動や回転、変形を表現することに起因する。三角形の位置を計算するためには、三角形の3つの頂点の座標を変化させるということであり、これが故に頂点演算と呼ばれるわけだ。この処理を行なうための機能が、従来までの演算器とは別に入る形になった。

 もう1つはその演算器の並列化である。例えばDirectX 7に最初に対応した製品の1つである「GeForce 256」の場合、コアは120MHzで動作し、ピクセルレートは480MPixel/secということで、1サイクルあたり4ピクセルの処理が可能になっている。これを実現するために、演算器は16bit幅のものが4つ並ぶ、SIMD風のものに拡張された。ただ、あくまでSIMD“風”であって、自由にプログラミングを実行できるわけではない。

GPUに変革をもたらしたプログラマブルシェーダの登場

 さて、ここまでは過去の延長という形で性能を上げる方向に展開してきたわけだが、DirectX 8でプログラマブルシェーダという概念が出てきたことで、内部構成が大きく変わることになった。

 図3は、DirextX 7までの内部の描画の流れ(これをグラフィックスパイプラインと呼ぶ)を簡略化して書いたものだ。まず頂点計算と、カメラから見えない場所のデータを切り捨てるクリッピングという処理(陰面処理もここに含まれる)はハードウェアT&Lで行なわれる。これを2次元データ化するラスタライズを行なう。そこに模様/外観に相当するテクスチャを貼り付けるわけだが、そのために、あらかじめテクスチャを取り込み、実際の形に合わせて変形した上で貼り合わせる形になる。最後にこれをフレームバッファに出力する形で終了するが、このうち、ALUに相当する汎用演算器が使われていたのは最後だけで、あとはハードウェアT&Lと専用回路で構成されていた。

【図3】旧来のグラフィックス処理の流れ(グラフィックスパイプライン)

 ところがDirectX 8以降はこうした固定処理を廃し、プログラムで自由に設定を変えられるようにしよう、という発想になった(図4)。この結果として、従来は64bit幅のSIMDが1個だけある、という状況だったのが、一転して多数のSIMD演算器が内部に入り、これを適宜組み合わせることが可能になった(図5)。

【図4】シェーダ登場後のグラフィックス処理の流れ
【図5】プログラマブルシェーダの登場でSIMD演算器を組み合わせて処理を行なう

 もう1つの違いは、これまでは単純に演算器のみが入っていたのが、これがシェーダというものになり、単に演算器だけがあるのではなく、自由にプログラムを解釈・実行できるものに変わったことだ。シェーダはその後、DirectX 10で統合型シェーダ(Unified Shader)と呼ばれるものに切り替わる。図5では頂点(バーテックス)シェーダとピクセルシェーダが完全に分離していたが、この区別が無くなった形だ。頂点シェーダはハードウェアT&Lの流れを汲んで浮動小数点演算が可能なものとなっており、一方ピクセルシェーダは整数演算のみでも実装可能だったのが、統合型シェーダとなったことで全てのシェーダが整数及び浮動小数点演算が可能になった(それが必要な形になったとも言える)。

 こうしたシェーダの進化と併せてプロセスの微細化が進んだことで、「性能向上にはシェーダの数を増やすのが得策」という方向性が次第に出来始めてゆく。DirectX 8以降のNVIDIAの主要な製品についてシェーダ数をまとめてみると、

  • GeForce 3 Ti : 頂点×1、ピクセル×4
  • GeForce 4 Ti : 頂点×2、ピクセル×4
  • GeForce FX : 頂点×3、ピクセル×4
  • GeForce 6000 : 頂点×6、ピクセル×16
  • GeForce 7000 : 頂点×8、ピクセル×24
  • GeForce 8000 : 統合型×128
  • GeForce 9000 : 統合型×128
  • GeForce 100 : 統合型×128
  • GeForce 200 : 統合型×240
  • GeForce 300 : 統合型×112
  • GeForce 400 : 統合型×480
  • GeForce 500 : 統合型×512
  • GeForce 600 : 統合型×1,536
  • GeForce 700 : 統合型×2,880

といった具合になっている(いずれも各シリーズのハイエンド品のもの)。製品のリナンバリングがあったりすることで数値が同じだったり、GeForce 300世代のようにローエンド品向けのみの製品化だったりした関係で数字が下がることもあるが、大きく見るとひたすらシェーダの数を増やす方向で性能を強化していることが分かる。

 ただこうなってくると、そもそもシェーダの中身が単純なSIMDでいいのか、シェーダ同士の連携をどうするのか、そもそも全体の管理をどうするのか、といった問題が発生する。加えて、「これだけ演算器が一杯あるなら、ほかの用途に当然使えるよね」という色気も出てくる。古いところでは例えば10年前にもこんな話題が出ているわけで、これがGPGPU的な流れに繋がっているのはご存知の通り。

 しかし、こうした用途に使うためには、大量のシェーダを効率よく仕事させる必要がある。この「効率よく」の方法論が、AMD(ATI)とNVIDIAで大きく違っていた(私見では、そうは言ってもだんだん似てきた気がする)のが、このGPU周りの構造を非常に分かりにくくしているように思う。そこで、来週はまずNVIDIAの方法論について紹介したいと思う。

(大原 雄介)