南の島のISA

先日発売されたAMD Radeon 7970の命令セット(ISA)について、実際のコードをみながらコメントしてみよう。例としてILでpixel shaderとして記述されたDGEMMカーネルから生成されたコードをgistに貼りつけた。利用したSDKのバージョンは"OpenCL 1.1 AMD-APP (831.4)"。

https://gist.github.com/1632529

前説

まず、前提として、このカーネルでは4x4のレジスタブロッキングをしている。そのため、ループの中ではそれぞれ4ワードを行列A, Bから読み込み、外積形式出部分行列の積を計算し、和を保持する4x4 = 16ワードの部分行列Cをアップデートする構造となっている。詳しくは、

http://galaxy.u-aizu.ac.jp/note/wiki/Fast_GEMM_Implementation_On_Cypress

にあるスライド参照。このカーネルはスライドの説明にあるところのTNカーネルに対応する。

また我々の最新のDGEMMカーネルのパフォーマンスについては、

Multi-level Optimization of Matrix Multiplication for GPU-equipped Systems, K.Matsumoto etal. http://dx.doi.org/10.1016/j.procs.2011.04.036

を参照のこと。この論文では巨大行列のためのDGEMMの性能最適化について報告している。

概観

コードをみてすぐに分かるのは"v_"と"s_"のprefixがついた命令があることである。これはprefixから予想されるように、ベクトル命令とスカラー命令なのだろう。これらの命令は、パッと見たところ完全に混在している。レジスタは"vn"の形式で32bitごとにアクセスできることがわかる。また"v[n:n+1]"の形式は二つの連続する32bitレジスタを表すのだろうから、これは64bitの1ワードに相当する。このカーネルの場合これは倍精度変数である。

コード右側の""というコメント(と思われる)以降にある数字の意味は不明。普通に考えるとbinary形式だろうけど。

個々の命令については現時点ではdocumentが公開されていないので、名前から動作を推測するしかない。とはいえ、一部を除いて、どの命令も明示的な名前が付けられており、特記するまでもない。"image_sample"命令は、texture unit経由のデータ読み込みを意味すると思われる。

レビュー

コードの流れは単純で、最初に部分行列Cを初期化して(20 - 51行:32bitで32ワードを0で初期化)、52行のラベルからループに突入し、123行の"s_branch"命令がループの終了部分。それ以降は"alpha C + beta AB"を計算している。この最後の部分(125 - 252行)が妙に長くなっているが、カーネルあたり一度しか実行されないので、計算時間上のインパクトはない。実際、上記K.Matsumoto論文によると、GEMMのカーネルとしては、このカーネルのように"C <- alpha C + beta AB"の全てを計算するのは高速ではなく、ABを計算するカーネルを利用し、Cとの和はホストで計算したほうが高速である。ここではあくまで一番シンプルな実装を紹介しているにすぎない。いずれにしろ、この部分では部分行列Cをロードして、レジスタに保持されているカーネルで計算した結果との和をとる処理をするため、"tbuffer_load"や"tbuffer_store"という命令が使われている。

ということでDGEMMカーネルとして計算時間がかかるループの本体部分は52 - 123行である。このループの大部分は"v_fma_f64"という命令が占めている。これはその名の通りの倍精度FMA命令だろう。FMA命令はは素直に4 operandsの命令になっている(古い世代のCypressやCaymanでもFMA命令は4 operandsだった)。なお、AMD社は最新のx86_64のCPUでも4 operandsのFMA命令を採用している。一方Intel社は、将来的には3 operandsのFMA命令を採用することになっている。両社の方針の違いは、将来的にはx86_64 CPUに統合されるであろうGPUのアーキテクチャにも反映されている/深く関係しているのかもしれない。

53 - 60行はループの終了条件を判定している部分になる。"s_waitcnt"はよくわからない。こここでthreadが切り替わっているのかもしれない。

その意味では61行目のラベルからがループ本体であり、62 - 67行はデータをロードするアドレスの準備をしているようにみえる。この部分は冗長にみえる。

69 - 72行で行列Aと行列Bをロードしている。"image_sample"では、一番左のレジスタがdestinationだろうから、ひとつの命令あたり32bitで4ワードをロードしている。よって、この4命令で合計倍精度変数を8ワード、ロードすることになる。

74 - 90行で、ロードした変数どうしの外積形式による行列乗算がおこなわれる。"v_fma_f64"の個数は16個であり、これは4x4のレジスタブロッキングをしているので当然。ところどころwait用?の命令が挟み込まれている。

91 - 100行でまたなんらかのアドレスの準備をして、101 - 104行で再び行列AとBをロードする。実はこのカーネルでは2段のループアンローリングをしているのでこうなっている。実際74 - 90行と処理自体は同等だ。

不可解なのはループを抜けたあとの126 - 147行で、なぜかレジスタ同士で入れ替えをしている。入れ替えをする理由は、その後で結果をメモリに書き込むためで、その時にレジスタ上で連続するようにという理由付けはできるが、実際にはv15 - v36に連続しているものをv12 - v33にコピーしているので、よくわからない。

追加コメント

全体を見ると、これくらいであれば手で書こうと思えば書けなくもないかもしれない。ただ、アドレス計算はやっぱり面倒。

ループ本体の中にデータロードとFMA以外の命令が43%も含まれている。このコードの性能は最大600 GFLOPSであるが、これはピーク性能の63%に相当する。これは偶然なのかどうか、というところが、今後の最適化の指針になるだろう。

126 - 147行は明らかに不必要。

随時アップデートするかもー。

Comments

No comments.