ai-isa-jit で学ぶGCN

@tanakmura

自己紹介

@tanakmura

id:w_o

F社のほうから来ました

(と言えと言われた気がしたので一応)

ai-isa-jit(あいあいさーじっと)

実行時にGCNの命令を生成するライブラリ

プログラムのリアルタイム生成(数百usecオーダ)を実現する

ほぼなにもしないプログラム@i7-4700MQ で

  • clBuildProgram : 30msec
  • ai-isa-jit : 300usec

100倍の高速化を実現

今回の内容

  1. ai-isa-jitの紹介(した)
  2. 動機
  3. ai-isa-jit 使いかた
  4. ai-isa-jitの実装について
  5. 色々計測
  6. まとめ

動機

単純な処理がいくらかあって、

それを実行時に並びかえたりしたい、みたいなことを言われた

(単純な処理=データを一個拾って少し演算して戻す)

プログラムの並びかえ

普通の実装だと、 __kernel をいくつか作って、
それを呼び出す順序を変える

  • 問題点1 : 計算量に対してメモリ転送量が増える
  • 問題点2 : カーネル呼び出しオーバーヘッドが大きくなる

演算量と転送量

R290X

(http://en.wikipedia.org/wiki/Comparison_of_AMD_graphics_processing_unitsより)

bandwidth 320 GB/s = 単精度floatで80G要素/s

read/writeなので転送量半分とすると、40G要素/s

  • 演算 : 5632 GFLOP/s
  • 転送 : 40G要素/s

要素あたり100演算ぐらい無いと転送律速になる

各処理ごとにはそんなに演算は無い

カーネル呼び出しオーバーヘッド

環境にもよるけど、Haswell i7で100usec周辺

-

R290X のbandwidth = 320GB/s

1080p SFloatのread/write = 1920x1080x4x2=約16MiB

転送律速だとすると、大体各処理の演算時間は、

16MiB / 320GB = 500usec

カーネル呼び出しオーバーヘッド

処理 500usec + clEnqueueNDRangeKernel呼び出し 100usec = 600 [usec/処理]

FLOPS値とは一体…

解決方法

複数の処理をいっこのカーネルでやる

レジスタ上で演算できるのでメモリ転送いらない

カーネル呼び出しオーバーヘッドを削減できる


/* before */
__kernel void k0(__global float *out, __global float *in) {
   out[i] = f0(in[i]);
}
__kernel void k1(__global float *out, __global float *in) {
   out[i] = f1(in[i]);
}

/* after */
__kernel void k01(__global float *out, __global float *in) {
    out[i] = f1(f0(in[i]));
}
      

今回

単純な処理がいくらかあって、

それを実行時に並びかえたりしたい、みたいなことを言われた

  • → 実行時にならびかえないといけない
  • → 事前に全部をくっつけた一個のカーネルをつくるのは難しい
  • → じゃあ実行時コード生成だ!!

実行時コード生成

OpenCLは、実行時にCをコンパイルしているので

実装はそれほど難しくない

Cプログラム文字列をconcatして実現可能

…だが

実行時コード生成の問題点

clBuildProgramに時間がかかる

プログラム/環境に依存するけど、最低でも30msecぐらい

ちょっと許容できない

GCNのコンパイルの内訳

(ビルド時に-fbin-exeとか付けると対応するバイナリを出せるので
それで計測 )

全部 : 30msec

C → LLVM IR : 10msec

LLVM IR → AMD IL : 18msec

AMD IL → GCN ISA : 2msec

clBuildProgram の時間短縮案

  • OpenCL SPIR に期待して LLVM IR を事前に出しておく → 没。LLVM IR → AMD IL で18msecかかってる
  • AMD IL を出す → 没。2msecかかる。そもそもGCN決め打ちならAMD ILにするメリットは全く無い
  • → GCNの機械語を直接出す以外の選択肢は無い !!!
  • → !!! ai-isa-jit を使おう !!!

ai-isa-jit の使いかた

  1. テンプレート用バイナリをclBuildProgramで作成
    (バイナリに引数の情報を入れるために必要)
  2. code emit API (xbyakみたいなの) を使ってコード生成
  3. できたバイナリを抜き出して clCreateProgramWithBinary

コード生成 API

__kernel void f(__global int *out) {  out[0] = 1; }
/* 上のCLと同じ処理をする */
static void
gen_code(struct AIISA_Program *prog, struct AIISA_CodeBuffer *buf)
{
    aiisa_code_buffer_reset(buf);

    aiisa_s_buffer_load_dword_immoff(buf, S(0), S(8), 0x4);
    aiisa_s_waitcnt(buf, LGKMCNT(0));
    aiisa_v_mov_b32(buf, V(0), S(0));
    aiisa_v_mov_b32(buf, V(1), 129);
    aiisa_tbuffer_store_format_x(buf, NFMT_FLOAT, DFMT_32, FLAG_OFFEN, 0,
                                 ZERO, 0, S(4), V(1), V(0));
    aiisa_s_endpgm(buf);

    aiisa_replace_text(prog, buf);
}

注意事項

  • undocumented かつ実装に依存しています(バージョン変わると動かなくなる可能性が高いです)
  • 完全に解析できたわけではないです。
    • レジスタ数
    • localのサイズ
    • カーネルの呼び出し規約
    • 複数カーネルが入っている場合

用法・用量を守って正しくお使いください

ai-isa-jit の実装について

http://openwall.info/wiki/john/development/GCN-ISA を参考にしてます。

AMD CL binary format

ELF の .text の中に ELF が入っている。(何故…?)

-fbin-exe を付けてclBuildProgramした場合、
この中のELF(以下innerELF)の.text に
GCN のコードが含まれている。

謎ELF

Linux だと inner ELF はなんかセクションがダブってる。何これ?

 [ 0]                   NULL            00000000 000000 000000 00      0   0  0
 [ 1] .shstrtab         STRTAB          00000000 0000fc 000028 00      0   0  0
 [ 2] .text             PROGBITS        00000000 0004d8 0008a8 00      0   0  0
 [ 3] .data             PROGBITS        00000000 000d80 001280 1280      0   0  0
 [ 4] .symtab           SYMTAB          00000000 002000 000060 10      5   1  0
 [ 5] .strtab           STRTAB          00000000 002060 00001b 00      0   0  0
 [ 6] .text             PROGBITS        00000000 00262f 000068 00      0   0  0  (これ)
 [ 7] .data             PROGBITS        00000000 002697 001280 1280      0   0  0
 [ 8] .symtab           SYMTAB          00000000 003917 000060 10      9   1  0
 [ 9] .strtab           STRTAB          00000000 003977 00001b 00      0   0  0

まあよくわからんのでスーパーダーティーハック…

  1. clBuildProgram で AMD ELF つくる
  2. AMD ELF の 5番目(決め打ち)のセクションの中身(inner ELF)を取り出す
  3. inner ELF の 6番目(決め打ち)のセクションの中身を
    生成したコードで書きかえる
  4. サイズが変わったらinner ELFの .textの後ろにある.data/.symtab/.strtab (決め打ち)変わった分ずらす
  5. phdr のサイズを変える
  6. AMD ELF の .text を書きかえる。うしろにある.commentずら す
  7. (windowsだとshdr が最後にあるのでそれずらさないといけないけどまだ)

まあそんな感じでダーティハックにダーティハックを重ねて動くように なった

https://github.com/tanakamura/ai-isa-jit

(ポインタを一個かきかえるだけのデモが動きます)

その後

こういう実装でどうか?と聞いてみたら、
「別にコンパイルはリアルタイムでなくてもいいのだけど」
と言われた


〜完〜

とくにこれ以上の使い途思い付かないのでせっかくだから今日この場で供養してあげようと思って今日はここへ来ました

(Mantleがわかればcalling convetionとかなんかもうちょっとなんとかしようかと思ったがそんなことはなかった)

以下、ダラダラとGCNの話でもします

ai-isa-jit で学ぶ GCN

(まあ別に無理してai-isa-jit使う案件でもないけど)

GCNの特徴

  • VLIW をやめた
  • なんかよくわからん CLAUSE とかいうのをやめた
  • スカラ1個とSIMD64並列

64way SIMD とは一体…?

性能出すには少なくとも256 item必要

16way SIMD が 4個並んでて、16way SIMDのひとつが4cycleかけて1wave front = 64要素処理する、
みたいなことが書いてある

わかりづらいけど、4wave front いれないとフルで動かない

    // vector mov が 128個入ったループを16384回回す
    aiisa_s_movk_i32(buf, S(0), 0);
    aiisa_s_movk_i32(buf, S(1), 10);

    label0 = buf->cur;
    aiisa_s_addk_i32(buf, S(0), 1);
    for (i=0; i<128; i++) {
        aiisa_v_mov_b32(buf, V(0), S(0));
    }
    aiisa_s_cmpk_le_i32(buf, S(0), 16384);

こういうのを入れてwork item 数を 1, 16, 64, 256 で実行して CodeXL に入れると、

item数 VALU Utilization[%] VALU Busy[%]
1 1.56 23.52
16 25 23.39
64 100 23.51
256100 94.04
  • VALU Utilization = Warp divergence みたいなの。 同じ命令数なら100% に近いほうがいい
  • VALU Busy 16 way のがどのぐらい動いてるか。 同じ命令数なら100% に近いほうがいい

VALU Busy を 100% にするには 256item必要

float 演算はレイテンシ4なのでitem内のスケジューリングは不要

VALU と SALU

CellのSPUとか、CUDAは、全演算がSIMDなので、ループカウンタのインクリメントとかすると勿体ない気分になる。

// CUDAです
__global void f(int n) {
    // この i は、全threadで同じ計算をするのでもったいない
    for (int i=0; i<n; i++) { ... }
}

GCNならそんな心配いらない!!

VALU と SALU

ベクタとスカラで別々のレジスタ/ALUがある。

  • ベクタ演算は4cycleで64要素計算する
  • スカラ演算は4cycleで1要素計算する

VALU と SALU

8つのwave frontから、
4つのベクタ演算、4つのスカラ演算を実行可能

アイテム間で変わらない演算はベクタ演算と並行して実行可能

(8wavefrontなので最低でも512item立ち上げる必要がある)

    // コード 1
    // ループ中に128個スカラ演算とベクタ演算が交互に出てくる
    aiisa_s_addk_i32(buf, S(0), 1);
    for (i=0; i<128; i++) {
        aiisa_s_movk_i32(buf, S(1), 100);  // s_ ではじまるのがスカラ演算
        aiisa_v_mov_b32(buf, V(0), S(0));  // v_ ではじまるのがベクタ演算
    }

    aiisa_s_cmpk_le_i32(buf, S(0), 16384);

    // コード 2
    // 128個スカラ演算と128個のベクタが連続して出る
    aiisa_s_addk_i32(buf, S(0), 1);
    for (i=0; i<128; i++) {
        aiisa_s_movk_i32(buf, S(1), 100);  // s_ ではじまるのがスカラ演算
    }
    for (i=0; i<128; i++) {
        aiisa_v_mov_b32(buf, V(0), S(0));  // v_ ではじまるのがベクタ演算
    }

    aiisa_s_cmpk_le_i32(buf, S(0), 16384);
VALU Utilization VALU Busy SALU Busy
ループ1 256item 100 47.88 48.63
ループ1 512item 100 95.55 97.04
ループ2 256item 100 48.09 48.84
ループ2 512item 100 64.31 65.31

→ 8 wave front 詰める + スカラとベクタ演算を詰めることで
ワークアイテム間で変化しない値をベクタ演算とは別に実行可能

lgkmcntとかvmcnt ってなんじゃ

Stream Kernel Analyzer で出すと出てくる lgkmcnt とかについて

s_waitcnt     lgkmcnt(0)

lgkmcntとかvmcnt ってなんじゃ

GCNはメモリアクセスが非同期。

メモリアクセスの完了は各自で書かないといけない。

完了を待つのがs_waitcnt命令。3つのカウンタがあって、

  • VMCNT : ベクタロード
  • LGKM_CNT : スカラとかconstant
  • EXP_CNT : ベクタ書きこみ (用のレジスタを使ってる数)

メモリアクセスするごとに 1 増える

メモリアクセス完了ごとに 1 減るというようになっている。

このカウントがいくらか減るかまで待つ命令がs_waitcnt

vmcnt(0) とかすると、VMCNT が 0 になるまで待つ。

lgkmcntとかvmcnt ってなんじゃ

演算とメモリアクセスをオーバーラップさせやすくするには、

メモリアクセスとs_waitcntの間に演算をたくさん入れるべき

    /* ループ 1. ロードのすぐあとに waitcnt  */
    for (j=0; j<16; j++) {
        aiisa_s_buffer_load_dword_immoff(buf, S(3), S(8), 0x4);
        aiisa_s_waitcnt(buf, LGKMCNT(0));
        for (i=0; i<8; i++) {
            aiisa_v_mov_b32(buf, V(0), S(0));
        }
    }
    /* ループ 2. ロード後すこし空けて waitcnt waitcnt  */
    for (j=0; j<16; j++) {
        aiisa_s_buffer_load_dword_immoff(buf, S(3), S(8), 0x4);
        for (i=0; i<8; i++) {
            aiisa_v_mov_b32(buf, V(0), S(0));
        }
        aiisa_s_waitcnt(buf, LGKMCNT(0));
    }
VALU Utilization VALU Busy
ループ1 256item 100 51.16
ループ1 512item 100 78.46
ループ2 256item 100 75.25
ループ2 512item 100 97.89

→ 手でスケジュールすることでメモリアクセスを隠蔽できる

他よくわからない点

ロードストアむずかしい

型変換モードやらオフセットの付けかたが色々ある。

呼び出し規約

get_global_id(0)を呼んでるかどうかで引数の渡されかたが変わる?

まあ全然理解してない。

まとめ

ai-isa-jit というのを勢いで作ったけど特に使いみちがなかった

しかたないので普通にアセンブラとして使っていくらか計測した

おわり

ご静聴ありがとうございました。