CALプログラミング(4) – 計算加速
    

wiki:CALプログラミング(4)

# Index

CALプログラミング(0)
CALプログラミング(1)
CALプログラミング(2)
CALプログラミング(3)
CALプログラミング(4)

# N体計算

ここまでの説明を理解することで、最もシンプルなN体計算プログラムを作成できると思う。

具体的にはGRAPEなどと同じように、ホストから粒子の位置と質量をGPUに送り、GPU上で全粒子間で互いに及ぼし合う力を足しあわせて、結果として加速度を得るようなプログラムである。粒子間に働く力がニュートン重力の場合は、CAL200808.pdf (60ページ以降)で説明しているIL kernelプログラムとなる。ループの部分のみを再掲すると、以下のようなものである。

1 whileloop
2 ige r88.x___, r100.x, r77.x
3 break_logicalnz r88.x
4 sample_resource(0)_sampler(0) r0, r2
5 sub r5.xyz, r0.xyz, r4.xyz
6 dp4 r6, r5, r5
7 rsq r7, r6
9 mul r8, r7, r7.xyz1
10 mul r8, r8, r7.xyz1
11 mul r9, r8, r5.xyz1
12 mad r3, r9, r0.w, r3
13 add r2.x___, r2.x, l1.x
14 iadd r100.x___, r100.x, l0.z
15 umod r101.x, r100.x, r77.y
16 if_logicalz r101.x
17 add r2.0y, r2.0y, l1.x
18 endif
19 endloop

2粒子間の重力の計算をおこなっている部分は5-12行である。データ構造の定義の詳細についてはファイルを参照のこと。

このILプログラムは、基本構造は(3)と同じであるが、ポインタ変数(ここではr2.xy)の処理が異なる。なぜかというと、粒子の座標が格納されているid = 0のリソースは2次元のメモリとして指定、確保されているからである。具体的には、ホスト側のプログラムで以下のようにメモリの確保をおこなった：

calResAllocLocal2D(&inputRes, device, nx, ny, CAL_FORMAT_FLOAT_4, 0);

nxとnyはそれぞれ、配列の添字の最大値を指定している。nxが最低次元である。よって、このAPIによりinputResで指定されるメモリ領域は"inputRes[ny][nx]"の大きさをもつ2次元配列データとなる。フォーマットとして、CAL_FORMAT_FLOAT_4を指定しているため、このデータは単精度浮動小数点であり、4つの要素を持つ。具体的には"float inputRes[ny][nx][4]"のような3次元の配列と同等である。ただし、実際のメモリ配列については連続しているとは限らない、下記参照。

こうする理由は、"calResAllocLocal1D"により1次元のメモリとして確保した場合、その次元の最大値は8192までという制限があるためである。よって、上のILプログラムの場合には、1粒子のデータとして4要素のfloat変数を使っているので、1次元メモリで単純にN体計算を実装すると8192粒子までの粒子しか扱うことができない。これでは実用的な価値がなく、実際にベンチマークテストをしてみると、この粒子数ではRV770の性能を引き出すことができない。

# 2次元配列

2次元のメモリからデータを読み出すには、(3)では無視してきた読み込みポインタのy成分を適切にアップデートすればよい。上のILプログラムでは、13-18行の部分でその処理をおこなっている。

繰り返しになるが、読み込みポインタとデータの対応関係は：

v0.xy = {0.0, 0.0} ---> res0[0][0]
v0.xy = {1.0, 0.0} ---> res0[0][1]
v0.xy = {2.0, 0.0} ---> res0[0][2]
...
v0.xy = {0.0, 1.0} ---> res0[1][0]
v0.xy = {1.0, 1.0} ---> res0[1][1]
v0.xy = {2.0, 1.0} ---> res0[2][1]
...

のようになっている。

それを踏まえて13-18行をC言語に翻訳してみると、処理内容が理解できると思う：

r2.x = r2.x + l1.x;
r100.x = r100.x + l0.z;
if (r100.x % r77.y == 0) {
r2.x = 0.0;
r2.y = r2.y + l1.x;
}

変数の意味と定数の値はそれぞれ以下のとおり：

r2.xy 配列読み込みのポインタ
!l1.x 浮動小数点定数 1.0
r100.x ループカウンター 整数
!l0.z 整数定数 1
r77.y 整数定数 配列の最下位次元の大きさ 256

r77.yの意味について補足すると、256になるのは、id = 0のメモリが"res0[256][256]"として宣言されているからである。

よってILプログラムのこの部分は、2次元配列のポインタ演算をおこなっているのと同等である。細かく説明すると、r2.x(1次元目の添え字に対応)が0から1ずつ増えていき、256に等しくなったたら、それを0にクリアし、r2.y(2次元目の添え字に対応)をインクリメントしている。
この13-18行を基にすることで、任意のサイズの2次元配列のランダムアクセスが可能であることがわかると思う。このILプログラムの場合、アクセスパターンが一定でありバースト的なため、GPUの内部構造に(特にキャッシュ機構)に適しており、非常に高速に動作する。一方、もしランダムアクセスを実装すると、メモリ読み込みのレイテンシーがパフォーマンスの問題となる可能性がある。

"calResAllocLocal2D"または"calResAllocRemote2D"で確保した場合でも、それぞれの次元でのサイズの最大値は8192と思われる。よって、ひとつのメモリ領域に割り当て可能な最大メモリ量は、256M個のfloat変数(またはLocalの場合、ボードの搭載メモリ量の制限値まで)になる。(この項詳しく調べる必要あり)

# ホストとGPUの通信

Last modified 15 years ago
Last modified on Mar 22, 2009 1:31:03 PM

### Download in other formats:

Plain Text