これは何? †
aparapi の取得 †
- OS 依存のネイティブコード部分があるので Maven Repository では公開されていない
- 配布されているバイナリ形式は古い
- GitHub? からソースコードをダウンロードしてきてビルドする必要あり
- 今回のビルド環境
- Mac OS X 10.10.5 Yosemite
- Java8, ant, gcc はインストール済み
- OpenCL は OS 組み込み (Windows や Linux の場合には別途インストールの必要があるはず)
- git からソースコードを取得してきてビルドする
[/opt]$ git clone https://github.com/aparapi/aparapi.git
[/opt]$ cd aparapi/
[/opt/aparapi]$ ant dist
すると /opt/aparapi/dist_mac_x86_64 に
LICENSE.TXT
aparapi.jar
api
libaparapi_x86_64.dylib
samples
ができる
- ローカルの Maven レポジトリに Java 部分を登録する
$ mvn install:install-file -Dfile=aparapi.jar -DgroupId=aparapi -DartifactId=aparapi -Dversion=1.0 -Dpackaging=jar -DgeneratePom=true
Hello GPU World! †
- 普通の Maven Java プロジェクトを作る
- pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany</groupId>
<artifactId>AparapiExam</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>aparapi</groupId>
<artifactId>aparapi</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</project>
- Main.java
package com.mycompany.aparapiexam;
import com.amd.aparapi.Kernel;
import com.amd.aparapi.Range;
public class Main {
public static void main(String[] args) {
final float inA[] = new float[]{1.0f, 2.0f, 3.0f, 4.0f};
final float inB[] = new float[]{0.1f, 0.2f, 0.3f, 0.4f};
final float result[] = new float[inA.length];
// for (int i = 0; i < inA.length; i++) {
// result[i] = inA[i] + inB[i];
// }
Kernel kernel = new Kernel() {
@Override
public void run() {
int i = getGlobalId();
result[i] = inA[i] + inB[i];
}
};
Range range = Range.create(result.length);
kernel.execute(range);
for (int i = 0; i < result.length; i++) {
System.out.println(String.format("%d %f", i, result[i]));
}
System.out.println(kernel.getExecutionMode().name());
System.out.println(kernel.getExecutionMode().isOpenCL() ? "OpenCL" : "Not OpenCL");
}
}
- 実行時引数に -Djava.library.path で libaparapi_x86_64.dylib のあるパスを指定する
- 実行結果
9 10, 2015 10:50:19 午後 com.amd.aparapi.internal.model.ClassModel$AttributePool <init>
警告: Found unexpected Attribute (name = org.netbeans.SourceLevelAnnotations)
0 1.100000
1 2.200000
2 3.300000
3 4.400000
GPU
OpenCL
Hello GPU World!
Quick Reference †
GPGPU を使った計算の概要 †
- 計算の手順
- CPU側で inA[]、inB[] にデータを格納する
- メインメモリ上の inA[]、inB[] を GDRAM にコピー (PUSH) する
- GPGPUが多コアで一気に処理 result[] = f(inA[], inB[])
- GDRAM 上の result[] をメインメモリにコピー (PULL) する
- CPU側で result[] を使って後続処理を行う
- aparapi を使うと、なんにも考えないで GPGPU を呼び出せてしまうけど、パフォーマンスのために最低限の動作イメージの把握は必要
パフォーマンス上の留意点 †
- 特に必要がなければ float[ ] を使う。GPGPU は float[ ] の計算に特化して設計されており、double[ ] に比べて倍ぐらい速い
型 | 値域 | 有効桁 |
int | -2147483648〜2147483647 | |
float | ±10-38〜10-38 | 7桁 |
double | ±10-308〜10-308 | 15桁 |
- メインメモリと GDRAM の間でのデータのコピーが性能上のボトルネックになる。なるべく通信をせずにすむようにアルゴリズムを工夫する
配列の長さは、2の階乗 (2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, ...) にする。
GPU の Core にタスクをうまく割り振れるようにするため。配列の長さが素数だと 1 Core しか使われない → 特にパフォーマンスには関係ないようだ
GPGPU の呼び出し †
- com.amd.aparapi.Kernel を継承したクラスの run() に GPU で実行したいプログラムを記述する
- OpenCL の "Kernel" になる
- 無名クラスを使う
public class Main {
public static void main(String[] args) {
final float inA[] = new float[]{1.0f, 2.0f, 3.0f, 4.0f};
final float inB[] = new float[]{0.1f, 0.2f, 0.3f, 0.4f};
final float result[] = new float[inA.length];
Kernel kernel = new Kernel() {
@Override
public void run() {
int i = getGlobalId();
result[i] = inA[i] + inB[i];
}
};
Range range = Range.create(result.length);
kernel.execute(range);
}
}
- 使えるデータは、プリミティブ型と、プリミティブ型の一次配列
- クラス外の変数は final にする必要がある
- プログラムは Kernel#run() に記述する
- 起動は Kernel#execute(range)
- Kernel#run() は、range 並列で実行される。何番目の実行スレッドかは Kernel#getGlobalId?() で取得できる
- Kernel#execute() は、全ての Kernel#run() が終わると復帰する。処理結果は GPGPU 側からメインメモリに書き戻される
- Kernel を継承したクラスを作る
public class FFTKernel extends Kernel {
// データサイズ
private int n = 0;
// 入力データの実数部
private float[] real;
// 入力データの虚数部
private float[] imag;
// ビット反転表
private int[] bitrev;
... (略) ...
@Override
public void run() {
int p = getPassId() * buttCal + getGlobalId();
int i = buttI[p];
int ik = buttIK[p];
float s = sign * buttSin[p];
float c = buttCos[p];
float dx = s * imag[ik] + c * real[ik];
float dy = c * imag[ik] - s * real[ik];
real[ik] = real[i] - dx;
real[i] += dx;
imag[ik] = imag[i] -dy;
imag[i] += dy;
}
public void fft(float[] x, float[] y) {
fftsub(x,y,1.0f);
}
public void ifft(float[] x, float[] y) {
fftsub(x,y,-1.0f);
for (int cnt = 0; cnt < n; cnt++) {
real[cnt] /= n;
imag[cnt] /= n;
}
}
private void fftsub(float[] x, float[] y, float sign) {
... (略) ...
// ビット反転
... (略) ...
// バタフライ演算
// buttCal 組の演算を buttStep 段繰り返す
// このようにループにしないと、GPGPU はステップごとに順に計算せずに
// 一気に全段計算してしまい、まともな結果が得られない
this.execute(buttCal, buttStep);
}
}
- クラス内の変数を使うことができる
- プログラムは Kernel#run() に記述する
- 起動は Kernel#execute(buttCal, buttStep) とも書ける。buttCal 並列の演算を buttStep 回繰り返す。
- Kernel#run() は、buttCal 並列で実行される。
- 何番目の実行スレッドかは Kernel#getGlobalId?() で取得できる
- 何回目のループかは Kernel#getPassId?() で取得できる
- Kernel#execute() は、全ての Kernel#run() が終わると復帰する。処理結果は GPGPU 側からメインメモリに書き戻される
実行IDの整理 †
- Kernel#execute(16, 2); で、16個の並列演算を2回実行する
- それぞれの Kernel#run() で、ループ番号、実行スレッド番号、グループ番号、ローカル番号を Kernel から取得できる
- Kernel#getPassId?()
- Kernel#getGlobalSize?()
- Kernel#getGlobalId?()
- Kernel#getGroupSize?()
- Kernel#getGroupId?()
- Kernel#getLocalId?()
- Group は、実行環境 (GPGPUのコア数) にしたがって自動的に設定される
実行モード †
Kernel.EXECUTION_MODE.ACC | Xeon Phi |
Kernel.EXECUTION_MODE.JTP | Java Thread Pool |
Kernel.EXECUTION_MODE.SEQ | Java Single Thread |
Kernel.EXECUTION_MODE.GPU | GPU |
Kernel.EXECUTION_MODE.CPU | CPU |
Kernel.EXECUTION_MODE.NONE | Auto |
デフォルトは NONE (自動設定)
明示的なバッファ管理 †
GPGPU組み込み関数 †
- Kernel#run() 内で、数値演算関数を使う場合には Kernel クラスの組み込みの関数を使う
⇒ aparapi が OpenCL のコードにコンパイルする際に OpenCL の関数にマッピングされる
Kernel | OpenCL | |
abs | fabs, abs | |
acos | acos | |
asin | asin | |
atan | atan | |
atan2 | atan2 | |
ceil | ceil | |
cos | cos | |
exp | exp | |
floor | floor | |
max | fmax, max | |
min | fmin, min | |
log | log | |
native_sqrt | native_sqrt | |
pow | pow | |
IEEEremaininder | remainder | 剰余 ANSI/IEEE 754-1985; §5.1 |
rint | rint | |
round | round | |
rsqrt | rsqrt | |
sin | sin | |
sqrt | sqrt | |
tan | tan | |
toRadians | radians | |
toDegree | degree | |
FFT(高速フーリエ変換) †
おさらい †
- フーリエ変換について
- どんな関数でも理論的に f(x) = Σ(Pi×sin(λi) + Qi×cos(λi)) の形に変換できる (フーリエ変換)
- ってことは、時系列の観測データも Σ(Pi×sin(λi) + Qi×cos(λi)) で表せるんだよね (離散フーリエ変換(DFT))
- これって時系列データを周波数成分に分離できるってことだよね。信号解析につかえるじゃん
- FFT(高速フーリエ変換)について
- DFTは便利なんだけど、そのままやったら計算量が膨大 O(N2) (Nはデータ数)
- Cooley-Tukey法を使うと O(N log(N)) で計算可能
- Cooley-Tukey法
GPGPU で FFT を計算するための工夫 †
- GPGPU
- 条件分岐が苦手
- float[ ] の各要素をまとめてドバっと処理するのが得意
- ということで、GPGPU 側で計算するときには、条件分岐はさせずに、とにかく float[ ] の要素同士の計算をドバっとやれるように前準備をしてあげる必要がある
- GPGPU 側で、バタフライ演算のペアを決定するのはしんどい
⇒ 事前に CPU 側でバタフライ演算の計算指示書 (どの相手とバタフライ演算をやるか) をつくってやって、GPGPU 側は何も考えずに計算指示書に従ってひたすら float[ ] の要素を操作するようにしてやりゃよかんべ
- 例:データ数8の場合の計算指示書
Step | バタフライ演算ペア | 係数1 | 係数2 |
1 | 0 1 | 0.000000 | 1.000000 |
1 | 2 3 | 0.000000 | 1.000000 |
1 | 4 5 | 0.000000 | 1.000000 |
1 | 6 7 | 0.000000 | 1.000000 |
2 | 0 2 | 0.000000 | 1.000000 |
2 | 4 6 | 0.000000 | 1.000000 |
2 | 1 3 | 1.000000 | 0.000000 |
2 | 5 7 | 1.000000 | 0.000000 |
3 | 0 4 | 0.000000 | 1.000000 |
3 | 1 5 | 0.707107 | 0.707107 |
3 | 2 6 | 1.000000 | 0.000000 |
3 | 3 7 | 0.707107 | -0.707107 |
- ※ 計算指示書は、データ数が同じであれば同じものをつかえる。⇒ 一度作れば使い回せる
ということで作ってみた †
package com.mycompany.aparapiexam;
import com.amd.aparapi.Kernel;
import java.util.LinkedList;
import java.util.List;
import org.apache.commons.lang.ArrayUtils;
public class FFTKernel extends Kernel {
// データサイズ
private int n = 0;
// 入力データの実数部
private float[] real;
// 入力データの虚数部
private float[] imag;
// ビット反転表
private int[] bitrev;
// バタフライ演算の指示書(i)
private int[] buttI;
// バタフライ演算の指示書(ik)
private int[] buttIK;
// バタフライ演算の指示書(sin)
private float[] buttSin;
// バタフライ演算の指示書(cos)
private float[] buttCos;
// バタフライ演算の段数
private int buttStep;
// バタフライ演算の一段あたりの計算の組数
private int buttCal;
// FFT(1) / 逆FFT(-1) フラグ
private float sign = 1.0f;
public FFTKernel (int size) {
n = size;
// 入力データ格納領域
real = new float[n];
imag = new float[n];
// FFT Step0 ビット反転表を作る
createBitrev();
// FFT Step1〜n バタフライ演算の指示書を作る
createButterfly();
}
private void createBitrev(){
bitrev = new int[n];
int i = 0;
int j = 0;
bitrev[i] = 0;
while (++i < n) {
int k = n / 2;
while (k <= j) {
j -= k;
k /= 2;
}
j += k;
bitrev[i] = j;
}
}
private void createButterfly() {
double step = 2.0 * Math.PI / n;
List<Integer> buttIArray = new LinkedList<>();
List<Integer> buttIKArray = new LinkedList<>();
List<Float> buttSinArray = new LinkedList<>();
List<Float> buttCosArray = new LinkedList<>();
int cnt = 0;
buttStep = 0;
for (int k = 1; k < n; k *= 2) {
int h = 0;
int d = n / (k * 2);
buttCal = 0; // バタフライ計算の段数を数える
for (int j = 0; j < k; j++) {
float s = (float) Math.sin(step * h);
float c = (float) Math.cos(step * h);
for (int i = j; i < n; i+= k * 2) {
int ik = i + k;
buttIArray.add(i);
buttIKArray.add(ik);
buttSinArray.add(s);
buttCosArray.add(c);
}
h += d;
buttCal += 1; // 一段につき何組の計算があるかを数える
}
buttStep += 1;
}
buttI = ArrayUtils.toPrimitive(buttIArray.toArray(new Integer[0]));
buttIK = ArrayUtils.toPrimitive(buttIKArray.toArray(new Integer[0]));
buttSin = ArrayUtils.toPrimitive(buttSinArray.toArray(new Float[0]));
buttCos = ArrayUtils.toPrimitive(buttCosArray.toArray(new Float[0]));
// for(int i = 0; i < buttI.length; i++) {
// System.out.println(String.format("%d %d %d %f %f", i, buttI[i], buttIK[i], buttSin[i], buttCos[i]));
// }
}
@Override
public void run() {
int p = getPassId() * buttCal + getGlobalId();
int i = buttI[p];
int ik = buttIK[p];
float s = sign * buttSin[p];
float c = buttCos[p];
float dx = s * imag[ik] + c * real[ik];
float dy = c * imag[ik] - s * real[ik];
real[ik] = real[i] - dx;
real[i] += dx;
imag[ik] = imag[i] -dy;
imag[i] += dy;
}
public void fft(float[] x, float[] y) {
fftsub(x,y,1.0f);
}
public void ifft(float[] x, float[] y) {
fftsub(x,y,-1.0f);
for (int cnt = 0; cnt < n; cnt++) {
real[cnt] /= n;
imag[cnt] /= n;
}
}
private void fftsub(float[] x, float[] y, float sign) {
if (n != x.length || n != y.length) {
throw new IllegalArgumentException("Illegal data size");
}
for (int cnt = 0; cnt < n; cnt++) {
this.real[cnt] = x[cnt];
this.imag[cnt] = y[cnt];
}
this.sign = sign;
// ビット反転
for (int i = 0; i < n; i++) {
int j = bitrev[i];
if (i < j) {
float t;
t = real[i];
real[i] = real[j];
real[j] = t;
t = imag[i];
imag[i] = imag[j];
imag[j] = t;
}
}
// バタフライ演算
// buttCal 組の演算を buttStep 段繰り返す
// このようにループにしないと、GPGPU はステップごとに順に計算せずに
// 一気に全段計算してしまい、まともな結果が得られない
this.execute(buttCal, buttStep);
}
public float[] getReal() {
return real;
}
public float[] getImag() {
return imag;
}
public float[] getSin() {
float[] sin = new float[n/2];
sin[0] = imag[0] / n; // this must be zero.
for (int cnt = 1; cnt < sin.length; cnt++) {
sin[cnt] = -2.0f * imag[cnt] / n;
}
return sin;
}
public float[] getCos() {
float[] cos = new float[n/2];
cos[0] = real[0] / n;
for (int cnt = 1; cnt < cos.length; cnt++) {
cos[cnt] = 2.0f * real[cnt] / n;
}
return cos;
}
}
テスト実行 †
package com.mycompany.aparapiexam.test;
import com.mycompany.aparapiexam.FFTKernel;
import org.junit.Test;
public class FFTKernelTest {
@Test
public void testFFT2() {
int size = 8;
FFTKernel fftKernel = new FFTKernel(size);
float[] real = new float[size];
float[] imag = new float[size];
for (int cnt = 0; cnt < size; cnt++) {
real[cnt] = (float) (
1.0 * Math.sin(0 * Math.PI * cnt / size)
+ 2.0 * Math.cos(0 * Math.PI * cnt / size)
+ 3.0 * Math.sin(2 * Math.PI * cnt / size)
+ 4.0 * Math.cos(2 * Math.PI * cnt / size)
+ 5.0 * Math.sin(4 * Math.PI * cnt / size)
+ 6.0 * Math.cos(4 * Math.PI * cnt / size)
+ 7.0 * Math.sin(6 * Math.PI * cnt / size)
+ 8.0 * Math.cos(6 * Math.PI * cnt / size)
);
imag[cnt] = 0.0f;
}
fftKernel.fft(real, imag);
real = fftKernel.getReal();
imag = fftKernel.getImag();
for (int cnt = 0; cnt < real.length; cnt++) {
System.out.println(String.format("%d %f \t+ %fi", cnt, real[cnt], imag[cnt]));
}
float[] cos = fftKernel.getCos();
float[] sin = fftKernel.getSin();
System.out.println("* cos\t\tsin");
for (int cnt = 0; cnt < cos.length; cnt++) {
System.out.println(String.format("%d %f\t%f", cnt, cos[cnt], sin[cnt]));
}
System.out.println(fftKernel.getExecutionMode());
}
@Test
public void testReverse() {
int size = 8;
FFTKernel fftKernel = new FFTKernel(size);
float[] real = new float[size];
float[] imag = new float[size];
for (int cnt = 0; cnt < size; cnt++) {
real[cnt] = (float) (
1.0 * Math.sin(2 * Math.PI * cnt / size)
+ 2.0 * Math.cos(2 * Math.PI * cnt / size)
+ 3.0 * Math.sin(4 * Math.PI * cnt / size)
+ 4.0 * Math.cos(4 * Math.PI * cnt / size)
+ 5.0 * Math.sin(6 * Math.PI * cnt / size)
+ 6.0 * Math.cos(6 * Math.PI * cnt / size)
);
imag[cnt] = 0.0f;
}
fftKernel.fft(real, imag);
fftKernel.ifft(fftKernel.getReal(), fftKernel.getImag());
float[] resReal = fftKernel.getReal();
float[] resImag = fftKernel.getImag();
System.out.println("* fft->ifft\t\t\toriginal");
for (int cnt = 0; cnt < resReal.length; cnt++) {
System.out.println(String.format("%d %f\t+%fi\t%f\t+%fi", cnt, resReal[cnt], resImag[cnt], real[cnt], imag[cnt]));
}
System.out.println(fftKernel.getExecutionMode());
}
}
- サンプルデータ
1.0 * Math.sin(0πi / size) // zero
2.0 * Math.cos(0πi / size) // constant value
3.0 * Math.sin(2πi / size)
+ 4.0 * Math.cos(2πi / size)
+ 5.0 * Math.sin(4πi / size)
+ 6.0 * Math.cos(4πi / size)
+ 7.0 * Math.sin(6πi / size)
+ 8.0 * Math.cos(6πi / size)
- サンプルデータ内に、1波長分, 2波長分, 3波長分 入る sin, cos をそれぞれ強度を変えて合成したもの
- データ数は 8
- FFT変換結果
0 16.000000 + 0.000000i
1 15.999999 -12.000000i
2 24.000000 -20.000000i
3 32.000000 -28.000000i
4 0.000000 + 0.000000i
5 32.000000 + 28.000000i
6 24.000000 + 20.000000i
7 15.999999 + 12.000000i
* cos sin
0 2.000000 0.000000
1 4.000000 3.000000
2 6.000000 5.000000
3 8.000000 7.000000
GPU
- 複素数で表した場合、真ん中で折り返されている (共役複素数になっている)
- cos = 実数部 * (2/size)
- sin = -1 * 虚数部 * (2/size)
- 今回の例では、データ点数が 8 なので 4 波長以上の周波数の解析はできない
- あと、ちゃんと GPU で計算しているっぽい
- FFT変換FFT逆変換で元に戻るか
* fft->ifft original
0 12.000000 +0.000000i 12.000000 +0.000000i
1 4.414214 -0.000000i 4.414214 +0.000000i
2 -8.000000 +0.000000i -8.000000 +0.000000i
3 4.071068 -0.000000i 4.071068 +0.000000i
4 -4.000000 +0.000000i -4.000000 +0.000000i
5 1.585786 -0.000000i 1.585786 +0.000000i
6 0.000000 -0.000000i 0.000000 +0.000000i
7 -10.071068 +0.000000i -10.071068 +0.000000i
よかですたい
ハマったところ †
// バタフライ演算
// buttCal 組の演算を buttStep 段繰り返す
// このようにループにしないと、GPGPU はステップごとに順に計算せずに
// 一気に全段計算してしまい、まともな結果が得られない
this.execute(buttCal, buttStep);
最初、
this.execute(計算指示書の配列数);
とやっていて、CPU での逐次処理ではうまくいくのに GPGPU で並列処理をしたら変な値になって悩んだ。
考えて見れば当たり前で、this.execute(計算指示書の配列数); で GPGPU で実行すると、Step1〜StepN のバタフライ演算が同時に実行されてしまう。Step 内のバタフライ演算は同時実行可能だけど、Step1,2,3...N は同時実行可能でなく、きちんと順番を守ってやる必要がある。
ということで Kernel.execute(range, pass) を使った。
めでたしめでたし
性能評価 †
Java#Others