JavaでGPU演算(Aparapi)してみた

今更感は有るが、Aparapi触ってみた。特に何か使う予定は無いけど、寒さに負けて生賴範義展に行けなかったのでなんとなく。



(1/31追記) 社内LTで再利用.

4/17追記

と指摘を受けたのでgithubのリンク先を https://github.com/Syncleus/aparapi に修正。



Aparapiとは

github.com

aparapi.com

JavaVMからOpenCL API経由でGPU演算を行うライブラリ。GPU側へ持っていけるのはJava プリミティブ型の配列のみだけれど、簡単に書ける。

とりあえず、任意の画像をグレースケール化する処理をループで書いた場合とAparapiで書いた場合を比較。

JVM内でループを回した場合

  private void convertToGrayScaleInJvm(final int[] pixels) {
    for (int i = 0; i < pixels.length; i++) {
      int pixel = pixels[i];
      int alpha = pixel >> 24 & 0xFF;
      int red   = pixel >> 16 & 0xFF;
      int green = pixel >>  8 & 0xFF;
      int blue  = pixel & 0xFF;
      int y = (int)(0.298912 * red + 0.586611 * green + 0.114478 * blue);
      pixels[i] = alpha << 24  | y << 16 | y << 8 | y;
    }
  }

Aparapiを使ってGPU演算した場合

  private void convertToGrayScaleInGpu(final int[] pixels) {
    
    Kernel kernel = new Kernel() {
      @Override
      public void run() {
        //GPU world
        int i = getGlobalId();
        int pixel = pixels[i];
        int alpha = pixel >> 24 & 0xFF;
        int red   = pixel >> 16 & 0xFF;
        int green = pixel >>  8 & 0xFF;
        int blue  = pixel & 0xFF;
        int y = (int)(0.298912 * red + 0.586611 * green + 0.114478 * blue);
        pixels[i] = alpha << 24  | y << 16 | y << 8 | y;
      }};
      int size = pixels.length;
      kernel.execute(Range.create(size));
  }

グレースケール化の方法については

https://ofo.jp/osakana/cgtips/grayscale.phtmlofo.jp

を参考とした。
ソース全体はこちら

https://gist.github.com/ka-ka-xyz/232bc0368ebf3c44bb46a7e1cb03810b

Aparapi注意点

JNI経由でOpenCLへアクセスするためにはネイティブライブラリが必要らしい。

aparapi/QuickReference.pdf at master · aparapi/aparapi · GitHub

無くても動くけれど、GPUは使わないよ的な事が書かれてた。 *1
あと、このPDFではAparapi関連のシステムプロパティのキー名が`com.amd.aparapi` で始まっているけれど、現在のv1.4.1では`com.aparapi` になっているので注意。

今回はここからdllを取得したけれど、これで良いんだろうか...
github.com

dllなりsoなりが存在するフォルダをJVMシステムプロパティ `java.library.path` で指定すれば読み込まれる。

実際に動かしてみる

www.pexels.com

から取得した画像をあれこれリサイズしてグレースケールへ変換してみた。

カラー版
f:id:ka-ka_xyz:20150913194329j:plain

グレースケール化した画像
f:id:ka-ka_xyz:20180128235338j:plain

ほんとにGPU呼んでるの?

NVIDIS GeForce GTX 1050の負荷をWin標準のパフォーマンスモニタで見てみた画像。
f:id:ka-ka_xyz:20180128235601p:plain

実行時にGPUさんが忙しそうにしてたので、呼ばれてるんだろう。多分。

パフォーマンスはどうよ?

f:id:ka-ka_xyz:20180128235656p:plain

X軸が画像のピクセル数、Y軸が処理にかかった時間(ms)。この時間はあくまでグレースケール化の処理時間で、ファイルの読み書きについては勘定していない。青がGPU演算のパフォーマンス。オレンジがJVM内での演算パフォーマンス。画像ピクセル数をあれこれ振り、CPU・JVM各5回の処理時間を取得して平均値をグラフ化。

グラフを見ると、GPU処理のためのオーバーヘッドがかなり高く、画像のピクセル数が少ないとJVMの中でループ回してたほうが圧倒的に速い。
13500x8598pxぐらいのサイズだとまだJVM内の処理が僅かに速く、18000x11464pxではGPU側が速い。

GPU演算だと並列処理のはずなのに計算量がO(N)に見えるのが気になる。仮説として

  • 処理自体は瞬時に終わるもののGPU側のメモリとJavaヒープとの間でのデータ転送に大半の時間が消費されている

と憶測してる(未検証だけど、上で挙げたパフォーマンスモニタはそれっぽく見える)。

*1: 一方で 「Aparapi:任意の計算タスクを実行するための新たな “Pure JavaAPIhttps://www.infoq.com/jp/news/2010/10/aparapi-java-and-gpus みたいな記事もあり、正直良くわからない。けど理屈としてはJVMの外のデバイスへアクセスするにはネイティブライブラリは必要だと思う