近年のIT業界のトレンドを見ていくと『ビッグ・データ』に始まり、集まったデータに意味を見出すため『データ・マイニング(統計によるデータ解析)』や『人工知能(ディープ・ラーニング)』に注目が集まるという流れがあるように感じる。いずれも多量のマシンパワーを必要とするため処理時間を短縮する技術がよく併用され、Hadoopに代表される並列分散処理やGPGPU(GPUによる汎用計算)についてはまだまだ需要が高そうである。
という訳で、今回はJavaでGPGPUを簡単に行えるライブラリ『Aparapi』を見ていく。
■ GPGPUとは?Aparapiとは?
GPGPU(General-purpose computing on graphics processing units)自体は2006年頃に登場した技術である(
*1)。もともと3D処理を専門に行うデバイスであったGPUに、CPUと同じような計算をさせようとしたのがGPGPUである。行列計算など特定の並列処理であればCPUよりも高速に処理を行える。ただし、CPUからGPUにデータ転送を行う分オーバーヘッドがあったり、GPUゆえの制限があるため、コーディングを工夫しないと高速化はできない場合もある。
Aparapiは2011年にAMD社(Advanced Micro Devices, Inc)が公開したGPGPU用ライブラリである。正確に言うとOpenCLのJavaラッパー・ライブラリであり、AparapiはJavaで記述されたコードをOpenCLコードに変換するだけであり、GPGPUの処理自体はOpenCLが行っている。このため直接OpenCLを利用するよりも遅くはなると思われるが、OpenCL利用時の煩雑な前処理・後処理を記述しないでよい点がAparapiの長所である。また、GPUが利用できない環境ではJavaスレッド上で動作するため、GPUがないので動かないという事態にならない点も長所である。
環境設定
Aprapiを利用するため、まずはEclipseプロジェクトにAparapiライブラリを関連付ける方法から確認する。手順は以下の通り。
1.Aparapiライブラリをダウンロード
最新版は『
Github』にて公開されているがソースのコンパイルが必要となる。しかし、面倒なので今回は『
Google Code』からコンパイル済みライブラリをダウンロードして利用する。
※「Aparapi_2013_01_23_○○.zip」というファイルの中で、自分の環境に合ったファイルを利用する。筆者環境では「Aparapi_2013_01_23_windows_x86_64.zip」を選択。解凍したフォルダ内のjar及びdllファイルが必要なライブラリである。
2.eclipseにて、新規プロジェクトを作成
Aparapiは通常のJavaプロジェクトで利用可能だが、今回はサンプルプログラム2で画像処理を行うためにJavaFXプロジェクトを作成する。
3.プロジェクトにjarファイルを追加
ダウンロードしたaparapi.jarをプロジェクトに追加する。方法は、プロジェクト名を右クリック→「外部アーカイブの追加」で、aparapi.jarを選択する。
4.プロジェクトへdllファイルへのパスを設定
Aparapi.jarは内部でaparapi_x86_64.dllを呼び出しているため、こちらをOSの環境変数PATHに登録する必要がある。これまた面倒だったので、今回は実行の構成で環境変数を指定する。
メニュー「実行 - 実行構成」を開くと以下のダイアログが表示される。画面左から該当プロジェクト名を選択し、右画面の「環境」タブで「新規」ボタンから新しい変数を追加する。追加する変数は「変数=PATH」「値=(aparapi_x86_64.dllへのフルパス)」。
※パスがうまく設定できていない場合、プロジェクト実行時に以下のようなメッセージが表示される。
Check your environment. Failed to load aparapi native library aparapi_x86_64 or possibly failed to locate opencl native library (opencl.dll/opencl.so). Ensure that both are in your PATH (windows) or in LD_LIBRARY_PATH (linux).
■ サンプル1(単純な加算)
Aparapiを利用するサンプルプログラムを以下に示す。サンプルでは2つの配列a,bについて、各要素を加算し出力した配列resultを計算し出力する。
◇サンプルプログラム
package application;
import com.amd.aparapi.Kernel;
import com.amd.aparapi.Range;
/**
* aparapiの動作確認プログラム1
* 『単純な加算』
*
* @author karura
*
*/
public class TestAparapi1
{
public static void main( String[] args )
{
// GPUと授受する変数を定義(final変数のみ授受可能)
final float a[] = new float[]{ 1.0f , 2.0f , 3.0f , 4.0f };
final float b[] = new float[]{ 1.0f , 2.0f , 3.0f , 4.0f };
final float result[] = new float[ a.length ];
// GPGPU用の無名クラスを生成
Kernel kernel = new Kernel()
{
@Override
public void run()
{
// 配列の加算を実行
int i = getGlobalId();
result[i] = a[i] + b[i];
}
};
// 配列の要素数を指定して、GPGPU実行
Range range = Range.create( result.length );
kernel.execute( range );
// 実行結果を出力
for( float el : result ){ System.out.println( el ); }
// 実行モードを出力
System.out.println( kernel.getExecutionMode().name() );
// GPGPU用の無名クラスを解放
kernel.dispose();
}
}
◇実行結果
2.0
4.0
6.0
8.0
GPU
◇解説
上記コードではKernel::run関数部分がGPGPUにより計算される。23行目~32行目でGPGPUでの動作を記述する無名クラスを宣言し、35行目で1次元配列の計算をすることを宣言、36行目で実際に処理を実行している。42行目では、Kernel::getExecutionMode::name関数により動作モード(EXECUTION_MODE)を出力し、GPUでの実行に成功したかを確認できるようにしている。EXECUTION_MODEの値が示す意味は以下の通り。最後、45行目では使用したリソースを開放している。
EXECUTION_MODEの説明
値 |
説明 |
GPU |
OpenCLを利用し、利用可能な1番目のGPU上で計算 |
CPU |
OpenCLを利用し、利用可能な1番目のCPU上で計算 |
JTP |
1コアにつき1スレッドを生成するJavaスレッド・プール上で計算 (Thread Pool) |
SEQ |
シングル・ループ上で計算(デバッグ時に利用するモードで、他のモードよりも遅い) |
プログラミング上の注意点としては、Kernel::run関数内で使う変数はKernelクラスの内部変数かfinal宣言された変数でなければならない点である。
■ サンプル2(単純なパターンマッチ)
GPGPUによる時間短縮効果を確認するため、以下に単純なパターンマッチを行うサンプルプログラムを示す。パターンマッチングの手法としてテンプレートマッチングを採用し、探索対象の画像内からパターン画像と一致する領域を検出する。テンプレートマッチングは探索画像上にパターン画像を重ね、重なった部分のピクセルの誤差合計が最も小さい場所を探す探索手法である。
◇リソース
【実行環境】
機能 |
内容 |
OS |
Windows7(64bit) |
CPU |
Intel Core2 (1.86GHz)
…プロセッサ数=2 |
GPU |
GeForce GTX650
…プロセッサ数=384 |
Java |
1.8.0_60 |
|
【フォルダ構成】
TestJavaFX(プロジェクト)
┣ src
┃ ┗ TestAparapi2_1.java
┣ 参照ライブラリ
┃ ┗ aparapi.jar
┗ img
┣ 01.png
┗ 02.png |
【使用画像】
画像 |
ファイル説明 |
|
探索対象画像(01.png)
提供元:写真素材ぱくたそ |
|
パターン画像(02.png)
…探索対象画像の一部を切り取った画像 |
◇サンプルコード
package application;
import java.io.File;
import com.amd.aparapi.Kernel;
import com.amd.aparapi.Range;
import javafx.application.Application;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.stage.Stage;
/**
* aparapiの動作確認プログラム2
* 『単純なパターンマッチング(aparapi)』
* 画像 : https://www.pakutaso.com/20120450102post-1371.html
*
* @author karura
*
*/
public class TestAparapi2_1 extends Application
{
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception
{
System.out.println( "start!" );
// GPGPU用のクラスを生成して初期化
PatternMatchKernel pmKernel = new PatternMatchKernel();
String file1 = new File("img/01.png").toURI().toString();
String file2 = new File("img/02.png").toURI().toString();
pmKernel.init( file1 , file2 );
// GPGPU実行(同時に処理時間取得)
long startTime = System.nanoTime();
pmKernel.execute();
long endTime = System.nanoTime();
float interval = ( endTime - startTime ) / 1000000000.0f ;
System.out.println( String.format( "time :%f [sec]", interval ) );
// 結果を出力
float[] result = pmKernel.getResult();
for( int i=0 ; i<result.length - 1 ; i++ )
{
// 2乗誤差が0になるインデックスを特定
if( result[i] == 0 )
{
// インデックスから画像のxy座標を計算し、出力
int x = pmKernel.getResultX( i );
int y = pmKernel.getResultY( i );
System.out.println( String.format( "result :(%d,%d) gap = %f" , x , y , result[i] ) );
}
}
// 実行モードを出力
System.out.println( "mode :" + pmKernel.getExecutionMode().name() );
// GPGPU用の無名クラスを解放
pmKernel.dispose();
}
}
/**
* GPGPU用のクラス
*
* @author tomo
*
*/
class PatternMatchKernel extends Kernel
{
// 探索対象画像情報
private float target[]; // 探索画像のピクセル配列
private int targetHeight; // 探索画像の高さ
private int targetWidth; // 探索画像の幅
// パターン画像情報
private float pattern[]; // パターン画像のピクセル配列
private int patternHeight; // パターン画像の高さ
private int patternWidth; // パターン画像の幅
// 結果情報
private float result[]; // 探索画像とパターン画像の誤差配列
/**
* クラスの初期化(画像のピクセル情報等を取得)
*
* @param targetFile 探索対象画像ファイル
* @param patternFile パターン画像ファイル
*/
public void init( String targetFile , String patternFile )
{
// 探索対象の画像ファイルを読込
try
{
// 画像ファイルの取込
Image img = new Image( targetFile );
// 変数の初期化
targetHeight = (int) img.getHeight();
targetWidth = (int) img.getWidth();
target = new float[ targetHeight * targetWidth ];
// ピクセル配列を取得
PixelReader reader = img.getPixelReader();
for( int y = 0 ; y < targetHeight ; y++ )
for( int x = 0 ; x < targetWidth ; x++ )
{
int i = ( y * targetWidth ) + x;
target[ i ] = reader.getArgb( x, y );
}
}catch( Exception e ){
e.printStackTrace();
}
// パターン画像ファイルを読込
try
{
// 画像ファイルの取込
Image img = new Image( patternFile );
// ピクセル格納用の配列を初期化
pattern = new float[ (int) (img.getHeight() * img.getWidth()) ];
patternHeight = (int) img.getHeight();
patternWidth = (int) img.getWidth();
// ピクセル配列を取得
PixelReader reader = img.getPixelReader();
for( int y = 0 ; y < patternHeight ; y++ )
for( int x = 0 ; x < patternWidth ; x++ )
{
int i = y * patternWidth + x ;
pattern[ i ] = reader.getArgb( x, y );
}
}catch( Exception e ){
e.printStackTrace();
}
// 結果配列を初期化
result = new float[ target.length ];
}
@Override
public void run()
{
// 値を初期化
float value = 0;
// GlobalIDが示す
// 探索範囲の左上ピクセル位置を取得
int i = getGlobalId();
int x = i % targetHeight;
int y = i / targetHeight;
// 探索範囲とパターン画像の2乗誤差を計算
for( int dy=0 ; dy<patternHeight ; dy++ )
{
for( int dx=0 ; dx<patternWidth ; dx++ )
{
// 範囲外の場合は適当な誤差を加算し、処理を飛ばす
if( y + dy >= targetHeight ){ value += 100; continue; }
if( x + dx >= targetWidth ){ value += 100; continue; }
// インデックス計算
int targetIndex = ( y + dy ) * targetWidth + ( x + dx ) ;
int patternIndex = dy * patternWidth + dx;
// 2乗誤差を計算
// 実際はR・G・B・Aの各色成分ごとに誤差をとるほうが良いが、
// 今回はパターン画像と完全一致する部分を得るため、計算を簡略化している
float gap = target[ targetIndex ] - pattern[ patternIndex ];
gap *= gap;
// 誤差を加算
value += gap;
}
}
// 誤差の合計を結果配列に格納
result[i] = value;
}
/**
* GPGPU実行
*/
public void execute()
{
// 配列の要素数を指定して、GPGPU実行
Range range = Range.create( result.length );
execute( range );
}
/**
* GPGPU計算結果を返す
* @return
*/
public float[] getResult()
{
return result;
}
/**
* GPGPU計算結果のインデックスから
* 探索画像上のx座標を計算する
* @param index
* @return
*/
public int getResultX( int index )
{
return index % targetHeight;
}
/**
* GPGPU計算結果のインデックスから
* 探索画像上のy座標を計算する
* @param index
* @return
*/
public int getResultY( int index )
{
return index / targetHeight;
}
};
◇実行結果
start!
time :1.435098 [sec]
result :(623,774) gap = 0.000000
mode :GPU
◇解説
探索画像のサイズが1500 x 1000[pixel]なので、合計で150万回のパターンマッチングを1.4秒程で行っている。これが早いかどうかについてだが、
同じ処理をシングル・スレッドで実行するコード(TestAparapi2_2.java)の処理時間を計測したところ36秒前後であったことを踏まえると、かなり早くなっていると言えるのではないだろうか。ちなみに、パターン検出した範囲を赤枠で囲むと下左図の通り。パターン画像(下右図)と比べると正しい位置が検出されていることがわかる。
検出結果 |
パターン画像 |
次にプログラムを確認する。
まず、JavaFXプログラムとなっているため、JavaFXを知らない方は別記事『
JavaFX入門』あたりを見ていただきたい。start関数(28行目)からプログラムが始まることと、PixelReaderにより画像をピクセル配列として取得することを理解していただければよい。start関数内では、探索画像とパターン画像をピクセル配列として取り込み(32行目~36行目)、GPGPU内でパターンマッチングと処理時間の計測を行い(38行目~43行目)、マッチングの結果誤差が0となる場所の座標を出力(46行目~57行目)している。GPGPUを実行するクラスについては、Kernelを継承したPatternMatchKernelクラスを宣言している(74行目)。
テンプレートマッチングの実装部はrun関数(152行目~190行目)である。run関数が1回処理されるとマッチングが1回完了する形になっており、GPU上ではこのrun関数が並列で動作するイメージとなる。
■ 参照
- 古石貴裕「GPGPU 登場の背景とプログラム環境構築について」