GPUを手っ取り早く活用するためのライブラリ、ArrayFireのご紹介

この記事は GPGPU Advent Calender の1日目の記事です
参加者募集中です。マジで。

GPGPU Advent Calendar人いなさすぎじゃね?大丈夫?
とものすごいいろんな方からご心配いただきましたが大丈夫じゃないです
まぁ、それがある意味でGPGPUの現状なのではないかという気がしていて、盛り上げていくためにいろいろやりたいですね、というところです。

さて、1日目は、「GPUを手っ取り早く活用するためのライブラリ、ArrayFireのご紹介」をしたいと思います

はじめに

GPGPUもだいぶ普及してきたとはいえ、耳にするのはやはり敷居の高さです
「CUDAとかOpenCLとか難しいでしょ?」という話はよく聞きますし、そのたびに「まぁそうですねぇ……」と返すのが様式美というかなんというか
難しい要因として、

  • ホスト、デバイスプログラムという二つのプログラムを書く必要がある
  • バイス側は独自拡張言語
  • メモリ転送が絡んだりするのが面倒
  • そもそもWarpとかWorkItemとか意味不明

などなどが挙げられます
統括すると、速くはしたいけど、自分の手間は押さえたい。

そんなときにおススメなのが、このArrayFireです

ArrayFireとは?

様々な行列演算をサポートしたライブラリです
公式サイトは
http://www.accelereyes.com/products/arrayfire

CUDA / OpenCLに対応しており、現在はCUDA版のみ公開されています(おかげで当初の予定が狂った)
無料版の場合、

  • 行列の四則演算
  • 線形代数学演算
  • 画像処理
  • 信号処理

のメソッドが用意されています
有料版の場合は、上記のものに加え

  • 疎行列演算

が追加されます
また、無料版の場合は対応しているのは単精度のみですが、有料になると倍精度もサポートします

ダウンロードには登録が必要です。
現行最新verは1.9です
対応している環境は、Windows, Mac, Linux.
インストールの方法などは各環境依存なので、ここでは割愛します
また、この記事では行列(の、ほんのさわりについて)のみに限定してお話をしたいと思います。もっと細かいのがいい!という人はリクエストください

はじめてのArrayFire

本記事では、以下の環境で実行しています

また、ArrayFireのインストールディレクトリ,CUDAのインストールディレクトリを、それぞれ以下のように設定しています

/usr/local/ArrayFire
/usr/local/cuda-5.0

ほかのOSの場合などは、以下のコマンド、パスを適宜読み替えてください
でははじめてのArrayFire

#include <arrayfire.h>

int main()
{
    af::info();

    return 0;
}

簡単ですね
さて、ではコンパイルします

$ g++ getInfo.cpp -I /usr/local/cuda-5.0/include/ -I /usr/local/ArrayFire/include/ -L /usr/local/ArrayFire/lib64/ -laf

肝心なのは、libaf.soをリンクすることによって、ArrayFireが使えるようになるということでスね
出来上がったものを実行すると、次のような結果が表示されるかと思います

$ ./a.out
ArrayFire v1.9 (build bbeef42) by AccelerEyes (64-bit Linux)
License: Server (27000@server.accelereyes.com)
CUDA toolkit 5.0, driver 310.19
GPU0 GeForce GTX 580, 3072 MB, Compute 2.0 (single,double)
Memory Usage: 3005 MB free (3072 MB total)

おめでとうございます!初めてのArrayFireですよ!
……といってもArrayFireらしいこと何一つしていませんので、もっとらしいことをやりましょう

できる!ArrayFire 入門編

大変うれしいことに、ArrayFireではデータをたった一つのクラスで表現しています
それがarrayクラスです。
さっそくarrayクラスを使うようなコードを書いてみましょう

#include <arrayfire.h>

int main()
{
    af::info();

    af::array a = af::randu(4, 4);
    af::array b = af::randu(4, 4);

    af::array c = a + b;

    af::print(a);
    af::print(b);
    af::print(c);

    return 0;
}

これを先ほどのようにコンパイルして、実行すると次のような結果が出るかと思います

$ ./a.out
ArrayFire v1.9 (build bbeef42) by AccelerEyes (64-bit Linux)
License: Server (27000@server.accelereyes.com)
CUDA toolkit 5.0, driver 310.19
GPU0 GeForce GTX 580, 3072 MB, Compute 2.0 (single,double)
Memory Usage: 3005 MB free (3072 MB total)
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

c =
           1.0596        1.1331        0.7045        1.2926
           1.7319        1.0573        1.3925        0.9124
           0.1931        0.9746        1.4224        1.0371
           1.4141        0.5255        1.2212        1.0149

あっさり行列の足し算ができました。簡単ですね。

arrayクラスの生成を行っているのは

    af::array a = af::randu(4, 4);
    af::array b = af::randu(4, 4);

ですが、右辺のrandu()関数は、ご存じのとおり[0,1]の乱数を生成する関数です
ほかにも以下のような関数が用意されています

array 	constant (float, unsigned nx, unsigned ny, dtype ty=f32)
array 	identity (unsigned nx, unsigned ny, dtype ty=f32)
array 	randu (unsigned nx, unsigned ny, dtype ty=f32)
array 	randn (unsigned nx, unsigned ny, dtype ty=f32)
array 	rand (unsigned nx, unsigned ny, dtype ty=u32)

上から、

  • nx,nyの行列を生成し、第一引数を全要素にセットする
  • nx,nyの行列の単位行列を生成する
  • nx,nyの行列を生成し、U[0,1]の乱数を各要素に対しセットする
  • nx,nyの行列を生成し、N[0,1]の乱数を各要素に対しセットする
  • nx,nyの行列を生成し、各ビット0または1の乱数を生成しセットする

という風になっています
最後の引数のdtypeはデータタイプで、所謂型と呼ばれるものです。ArrayFireでは、データタイプとして

をサポートしています。
ただし、先述のとおりf64,c64は有料版のみのサポートです

基本的に行列演算はオペレータで書くことができますが、行列の乗算だけは気を付けなければなりません
乗算をオペレータで書くと……

#include <arrayfire.h>

int main()
{
    af::info();

    float aa[] = {0.0f, 1.0f, 2.0f, 3.0f};
    float ab[] = {4.0f, 5.0f, 6.0f, 7.0f};

    af::array a(2, 2, aa);
    af::array b(2, 2, ab);

    af::array c = a * b;

    af::print(a);
    af::print(b);
    af::print(c);

    return 0;
}
a =
           0.0000        2.0000
           1.0000        3.0000

b =
           4.0000        6.0000
           5.0000        7.0000

c =
           0.0000       12.0000
           5.0000       21.0000

何かがおかしい。
ちなみに、arrayクラスのコンストラクタで配列のポインタを渡すことで初期化もできます。それが上の使い方ですね
閑話休題
cの結果だと、a,bの各要素を乗算した値になっています。つまりは
c[i] = a[i] * b[i]
こういうケースが必要な場合はオペレータで表記できますが、行列の乗算をしてほしい場合はこうじゃねーんだよ!とPCに殴り掛かること請け合い
行列の乗算をしたいときは、次の関数を使用します

    c = af::matmul(a, b);
a =
           0.0000        2.0000
           1.0000        3.0000

b =
           4.0000        6.0000
           5.0000        7.0000

c =
          10.0000       14.0000
          19.0000       27.0000

よし。正しく乗算の結果が得られました。
……ところで。
この乗算のパフォーマンス、はたしていかほどのものなのでしょうか?
いくら手軽に書けるとはいえ、CPUよりも遅いとかになっていたらわざわざライブラリを使う必要はありません。
それでは測ってみようじゃありませんか。そのパフォーマンスとやらを!

ArrayFireの実力 入門編

行列の乗算について、FLOPSを算出します
今回使ったGPUはGTX 580です。580のCUDA coreは積和演算を1clockでできるので2opで、
512core * 2 op * 1544Mhz = 1581GFLops
……さすが速いな

さて、実効性能はどんなもんでしょうね

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

af::array A;
static void calc(void)
{
    af::array dst = af::matmul(A, A);
    dst.eval();
}

int main()
{
    for(int i = 128; i <= 4196; i += 128) {
        A = af::randu(i, i);

        double time = af::timeit(calc);
        double flops = 2.0 * powf(i, 3) / (time * 1.0e9);

        std::cout << i << " * " << i << " time: " << time << ", GFlops: " << flops << std::endl;
    }

    return 0;

}

こういうコードをコンパイルして実効します
結果は

行列サイズ FLOPS
128 * 128 278.781
256 * 256 409.526
384 * 384 454.729
512 * 512 729.838
640 * 640 588.356
768 * 768 675.322
896 * 896 667.429
1024 * 1024 758.828
1152 * 1152 707.298
1280 * 1280 737.136
1408 * 1408 730.231
1536 * 1536 758.927
1664 * 1664 744.276
1792 * 1792 756.01
1920 * 1920 753.608
2048 * 2048 771.99
2176 * 2176 758.935
2304 * 2304 766.328
2432 * 2432 763.542
2560 * 2560 771.544
2688 * 2688 765.283
2816 * 2816 769.794
2944 * 2944 767.804
3072 * 3072 775.545
3200 * 3200 769.943
3328 * 3328 772.364
3456 * 3456 771.217
3584 * 3584 774.082
3712 * 3712 773.062
3840 * 3840 772.706
3968 * 3968 773.953
4096 * 4096 780.347

50%…?
とはいえ、ArrayFireって実はcuBLASのラッパーなんですが
それでも、CPUと比較してこれだけの演算性能がかなりお手軽に得られるというのは、結構メリットのあることなのではないでしょうか?

こんくるーじょん

ArrayFireのほんの表層をご紹介しました。
パフォーマンス的にcuBLASとほぼ同等と考えていますが、cuBLASのベンチも後で載せておきます

なんで表層だけかというと、あと二回ぐらい書かなきゃいけなさそうだったのでぶっちゃけ時間稼ぎです

次はLinearなところとかもお話します

ということで、むしろGPGPU Advent Calendarは明日からが本番!
w_o先生がもっとこゆい話をしてくれる…!
では自壊次回もよろしくお願いします

ついしん.
ほかのひともご参加くだされば本当に泣いて喜びます