OpenCL版

今の状況を書いておくと、

https://github.com/tanakamura/waifu2x-converter-cpp/blob/gpu/src/modelHandler_OpenCL.cl

いちおう動いている、が、FMA版よりまだ遅い。i7 4700MQ vs GTX 765M でFLOPS値 200 : 80 ぐらい 765M は 1300GFLOPS なので効率6%ぐらい。

FMA 版と同じように、outputplane を最内ループにすると、係数全部ローカルメモリに乗せないといけないのだが、16KBでは全然入らない。(最大576KBになる)

案としては、

  • occupancy に頼らない。参考:http://www.cs.berkeley.edu/~volkov/volkov10-GTC.pdf
  • SMあたり65536,32768 レジスタあることを考えると、128x9 は余裕で全部レジスタにのる。ただ、これを分けるとxxxxxが必要になる。まあ、スレッドあたり63レジスタまで使えると考えると、4x9ぐらいは1スレッドに乗るので、それなら32thread reductionならまあ許容範囲?
  • for (x) { for (inputPl) { for (outputPl){} } }
  • 128x128x9 = 147456 = 3SM あればレジスタに乗る。outputPlane を 65535(強いNVIDIA)レジスタあるマシンでは3つか4つ、32768(弱いNVIDIA)マシンでは8ぐらい、16384(AMD)では16ぐらいに分ける
    • は?さすがに16だと割に合わんでしょ。没没。あとinputのlocality を維持するのがむずい
    • AMD はlocal memory が64KBある。
      • AMD : register 64KB lmem 64KB
      • NVIDIA(low) : register 128KB lmem 16KB
      • NVIDIA(high) : register 256KB lmem 16KB
      • weight : 128x128x9 = 576KB
  • for (x // lmemに入るだけブロッキング) { for (outputPlBlock) { for(inputPl) { for (outputPl) { } } } } // 没
// 没

for (x=0; x<width; x+=X_BLOCK_SIZE) {
  for (ip=0; ip<inputPlane; ip++) {
     coef0 = coef@global[0*32 + lid];
     coef.. = coef@global[.. + lid];
     coef8 = coef@global[8*32 + lid];

     x_block@local[] = input@global(ip,x,y)

     if (ip != 0) {
        sum = sum@global(op,
     }

     for (xb=0; xb<X_BLOCK_SIZE; xb++) {
         
     }
  }
}
  • for (x) { for (inputPlane) { for (output) { for (x_block // 64@NV, ) { for (outputblock // 4) {} } sum@global[64*output] = v } } } こうかな
    • 1. inputPlane は局所性いちばんきくので外側
    • 2. 没
  • outputPlane を最内にすると中間データが増える。入力の局所性が上がる
  • xi を最内にすると気合いだけで演算メモリ比は9:2あたりまで持っていける、が、そこに限界がある
  • inputPlane を最内にすると中間データは要らないが、入力の局所性が無くなる


つまりまとめると

  • outputPlane 方向にN回ブロッキングすると、入力メモリ量1/N になる。中間メモリ使用量がN倍になる。出力メモリ回数は変わらない
  • inputPlane 方向にM回ブロッキングすると、出力メモリ量が1/Mになる。inputを保持するローカルメモリがM倍になる。入力メモリ回数は変わらない
  • 使用レジスタ数は N x M に比例(weight を全部レジスタに乗せるなら)

というトレードオフか。


これを踏まえると、手順としては、

  1. xi を最内にして、演算メモリ比 9:2 版を作る (多分効率5%ぐらい)
  2. outputPlane 方向に4回ぐらいブロッキング。メモリネックなら2倍に近づくはず(効率10%、レジスタ36個)
  3. occupancy が下がらない程度のローカルメモリ量になるまで inputPlaneを増やしていく (余計な命令が増えて演算ネックになってくるはず。効率15%)

という手順かな。

いやそれでも15%か。30%ぐらいはいかないといけないのだけど。

いや

  • 128x128x9 = 147456 = 3SM あればレジスタに乗る。outputPlane を 65535(強いNVIDIA)レジスタあるマシンでは3つか4つ、32768(弱いNVIDIA)マシンでは8ぐらい、16384(AMD)では16ぐらいに分ける

これが正解な気がしてきた。レジスタが無限にあるなら、演算:メモリ比は 1000:1 ぐらいあって、メモリアクセスの無駄は10x程度までは許容できる、AMDではワースト30xか60xぐらいになりそうだけど、L2がいくらか効くだろうから、実際にはもうちょっと抑えられるのではないか?

並列プログラミングに目覚めた!スレッド起動レイテンシ14.5msec のカスペル先生から受けたひどい仕打ち

#include <thread>
#include <windows.h>
#include <vector>


struct Obj {
    HANDLE ev;
    std::thread t;
    Obj(HANDLE ev, std::thread &&t)
        :ev(ev), t(std::move(t))
    {
    }
};

std::vector<Obj> threads;

static double sec()
{
    LARGE_INTEGER v,f;
    QueryPerformanceFrequency(&f);
    QueryPerformanceCounter(&v);

    return v.QuadPart / (double)f.QuadPart;
}

int main(int argc, char**argv)
{
    int n = 10;
    if (argc >= 2) {
        n = atoi(argv[1]);
    }
    bool ev = false;
    double t0;

    if (!ev) {
        Sleep(1000);
        t0 = sec();
    }

    for (int i=0; i<n; i++) {
        HANDLE h;
        h = CreateEvent(nullptr, FALSE, FALSE, nullptr);
        threads.push_back(Obj(h,std::thread(
                                  [](HANDLE h, bool ev) {
                                      if (ev) {
                                          WaitForSingleObject(h, INFINITE);
                                      }
                                  }, h, ev
                                  ))
            );
    }

    if (ev) {
        Sleep(1000);

        t0 = sec();
        for (int i=0; i<n; i++) {
            SetEvent(threads[i].ev);
        }

    }

    for (int i=0; i<n; i++) {
        threads[i].t.join();
    }

    double t1 = sec();

    printf("%f\n", (t1-t0)/n);
}
  • カスペルあり : 15msec
  • カスペルなし : 200usec

スレッド起動が遅くない最近のOSに慣れてスレッドプールのつくりかたを忘れた我々に警鐘を鳴らしてくれるカスペル先生大好き!愛してる!消滅して!!


レイテンシ15msecとかふざけてんの?60fpsほぼ終わってるんですけど。

  • AMD : register 64KB lmem 64KB

ごめんこれ間違い。レジスタ256KBあったわ。256register x 256item x 4byteだった。

これなら、256+64=320KBで、574KBのweightの半分が載せられるので、演算メモリ比は1000:7までいけそう。まあ効率30%はいけるかな...