GPUを手っ取り早く活用するためのライブラリ、ArrayFireのご紹介 gfor編(みかん)

この記事はGPGPU Advent Calendarの12月10日、10日目の記事です。日付を豪快にぶっちしそうですが10日目の記事です

俺「今日俺の番なんですよぉお」
先輩「どうすんの? ってか俺絶対3日とか書かないからね!」

という会話が先ほど帰り際にありました(実話
我が名はインフィニティ―…無限のメモリーなり…

さて、ArrayFireですが、前回は行列の演算までやりました。
続いて、ArrayFire(Jacketも、ですが) の特徴的なgfor構文についてみていきたいと思います。

gfor?

gfor構文とは、forループを展開するものです。
はい、何のことを言ってるかわからないですね。アンロールでもするの? いえいえ、違います。
百聞は一見にしかずなので、次のコードをご覧ください。

const size_t num = 1024;
af::array A(num);

for(int i = 0; i < num; ++i) {
    A(i) = i;
}

gfor(af::array i, num) {
    A(i) = i;
}

forがgforに置き換わっていて、中が見慣れないものになっています。
これは、今Aぶっ壊していますが、どちらも同じものが入ります。

本当にそうなるか?
gforに変えると何かいいことがあるのか?
それを検証するのが次のコードです

#include <arrayfire.h>
#include <iostream>

int main()
{
    const size_t num = 1024;
    af::array A  = af::seq(static_cast<double>(num));

    af::timer::start();

    for(size_t i = 0; i < num; ++i) {
        A(i) = A(i) + 1;
    }

    double time_for = af::timer::stop();
    std::cout << "for time: " << time_for << "sec. " << std::endl;

    af::array B  = af::seq(static_cast<double>(num));

    af::timer::start();
    gfor(af::array i, num) {
        B(i) = B(i) + 1;
    }

    double time_gfor = af::timer::stop();
    std::cout << "gfor time: " << time_gfor << "sec. " << std::endl;

    af::print(A);
    af::print(B);

    return 0;
}

最初の段階では、A,Bにはそれぞれ、0から1023までの値が代入されています。

    af::array A  = af::seq(static_cast<double>(num));
(中略)
    af::array B  = af::seq(static_cast<double>(num));

この部分で初期化を行っているわけです。
for,gforの中で、それぞれイテレータを足しているので、1から1024までの値が入っていることになります

中身の出力は割合しますが、時間だけを抜き出して表示すると

for time: 2.05661sec.
gfor time: 0.000577sec.

と、このように、実に3000倍!? おいこれ計算間違ってんだろ!?
……?いや、間違って……ない……?
……とまぁ、超高速。あまりに高速すぎて訝しむぐらいには。現在絶賛訝しみ中。

gforには、いくつか書き方があります。

gfor(var, n);
gfor(var, first, last);
gfor(var, first, increment, last);

varというのが、いわゆるsize_t i = 0とか、お約束のように書くイテレータに相当します
af::seqが使用され、一番上の場合は0,1,...,n-1までが生成されます。
二つ目は、最初と最後が指定できる形式です。first,first+1,...,last-2,last-1までが生成されます
三つ目は、二つ目に加えて増加量を指定したものです。first+increment, first+increment*2,...,last-increment*(n-1)までが生成されます

gforはえー!といいたいところですが、これだけじゃあまり実用性が…
こっちが作った関数を呼びたいとか、そういうのが人のサガというものです。これが ひとの サガ か

#include <arrayfire.h>
#include <iostream>

af::array mul(af::array A, af::array B)
{
    af::array ret;
    ret = A * B;

    return ret;
}

int main()
{
    const size_t num = 4;
    af::array A = af::randu(num, num);
    af::array B = af::randu(num, num);

    af::array res = af::constant(0, num, num);
    try {
        gfor(af::array k, num) {
            res(af::span, k) = mul(A(af::span, k), B(af::span, k));
        }
    } catch (af::exception& e) {
        std::cout << e.what() << std::endl;
    }

    af::print(A);
    af::print(B);
    af::print(res);

    return 0;
}

ただ乗算するだけの関数ですが、gforの内部で呼び出しています。
ここで見慣れないaf::spanなるものが出てきていますが、これは、「その次元すべて」を意味します。

A =
           0.7402        0.9251        0.4702        0.7140
           0.9210        0.4464        0.5132        0.3585
           0.0390        0.6673        0.7762        0.6814
           0.9690        0.1099        0.2948        0.2920

B =
           0.3194        0.2080        0.2343        0.5786
           0.8109        0.6110        0.8793        0.5538
           0.1541        0.3073        0.6462        0.3557
           0.4452        0.4156        0.9264        0.7229

res =
           0.2364        0.1924        0.1102        0.4131
           0.7468        0.2727        0.4513        0.1986
           0.0060        0.2051        0.5016        0.2424
           0.4313        0.0457        0.2731        0.2111

結果はこんな感じで、乗算がなされていることが確認できます。
ユーザー関数も呼べるんだ!!

ただし注意点があります。
gforで実行される関数の内部でifで分岐するようなコードを書くことはできません。

af::array mul(af::array A, af::array B, float t)
{
    af::array ret;
    if(t > 0.0f) {
        ret = A * B;
    }

    return ret;
}

こんなコードを書くとエラー出ます。
というか、gfor内部での条件分岐はエラーが出ます。
回避するためには投機的実行っぽい感じにしなければなりません。どういうこと?

どういうことかは待て次回?