l'essentiel est invisible pour les yeux

Thursday, November 30, 2006

[Cell] Function-Offloadプログラミングモデル 2

[Cell] Function-OffloadモデルでHello, Worldでは、SPE上でhelloプロシージャを実行した。

今回は入力引数として2つの配列を受け取り、出力として配列の各々の値を合計した値を返すリモートプロシージャーを定義する。この加算処理を行うプロシージャを次の3パターン定義する。

  • 同期(SPEでの処理が終了するまで待つ)して実行する。
  • 同期+SIMDで実行する。
  • 非同期で実行する。
前回同様に、次の順番で3つのファイルを作成する。
  1. インタフェース定義ファイル(IDL)を定義 (add.idl)
  2. プロシージャの実装を定義 (spu/add.c)
  3. PPEメインプログラム (ppu/add.c)
インタフェースの定義フォーマット

プロシージャーの定義は次の4つのコンポーネントから構成され、フォーマットは次の通り。
  • 同期タイプ
  • 返り値(常にidl_id_t)
  • プロシージャの名前
  • パラメータの詳細
[<op_attribute>] idl_id_t <identifier> <parameter_declarators>

  • op_attribute - プロシージャーの同期に関して指定する。
  • sync - 同期実行する。PPEアプリケーションはSPEでの実行が終了するまで待たされる。
  • async_b - 非同期で実行する。プログラムは関数が値を返すとすぐに入力用のバッファを再利用できる。プロシージャーの返り値idl_id_tは、以降のPPEプログラム中のjoin_func(idl_id_t id)関数中で、プロシージャーの実行完了を待つ際に使用する。
  • async_i - async_bと同じだが、プロージャーを呼び出した後の入力引数のバッファをjoin_funcが成功する以前に再利用することが出来ない。
  • async - async_iと同じ。
引数の定義(parameter_attributes)は次のようなフォーマットに従う。
[<parameter_attributes>] <type_specifier> <parameter_declarator>

parameter_attributes以下のどれかを指定する。
  • in - 入力パラメータであることを指定する。データはPPEプログラムからSPEプロシージャーへ渡される。
  • out - 出力パラメータであることを指定する。データはPPEプログラムからSPEプログラムへ渡される。
  • size_is(val) - valのサイズを指定する。整数か定数かを指定する。
  • タは整数か定数で指定されるvalのサイズを持つ。この値が配列のサイズに一致しなければダブルバッファリングクエリは無視される。

引数に配列を指定する際は、size_is(val)属性を使用してサイズを指定しなければならない。



add.idl: リモートプロシージャのインタフェースを定義する。

interface add {
import "../stub.h";

const int ARRAY_SIZE = 1024;
[sync] idl_id_t do_add([in] int array_size, [in, size_is(array_size)] int array_a[],
[in, size_is(array_size)] int array_b[],
[out, size_is(array_size)] int array_c[]);

[sync] idl_id_t do_vec_add([in] int array_size, [in, size_is(array_size)] int array_a[],
[in, size_is(array_size)] int array_b[],
[out, size_is(array_size)] int array_c[]);

[async] idl_id_t do_async_add([in] int array_size, [in, size_is(array_size)] int array_a[],
[in, size_is(array_size)] int array_b[],
[out, size_is(array_size)] int array_c[]);
}


spu/do_add.c: プロシージャの実装をする。

#include <stdio.h>
#include <spu_intrinsics.h>
#include <idl_util.h>
#include "../stub.h"

idl_id_t do_add(int array_size, int array_a[], int array_b[], int array_c[]) {
int i;
printf("SPU do_add: start executing with array_size=%d, array_a=0x%x, array_b=0x%x\n", array_size, (int)array_a, (int)array_b);
for(i=0;i<array_size;i++) {
array_c[i] = array_a[i] + array_b[i];
}

return 0;
}

idl_id_t do_vec_add(int array_size, int array_a[], int array_b[], int array_c[]) {
int i;
printf("SPU do_vec_add: start executing with array_size=%d, array_a=0x%x, array_b=0x%x\n", array_size, (int)array_a, (int)array_b);
vec_int4 *vi_a = (vec_int4 *) array_a;
vec_int4 *vi_b = (vec_int4 *) array_b;
vec_int4 *vi_c = (vec_int4 *) array_c;
for(i=0;i<array_size/4;i++) {
vi_c[i] = spu_add(vi_a[i], vi_b[i]);
}

return 0;
}

idl_id_t do_async_add(int array_size, int array_a[], int array_b[], int array_c[]) {
int i;
printf("SPU do_add: start executing with array_size=%d, array_a=0x%x, array_b=0x%x\n", array_size, (int)array_a, (int)array_b);
for(i=0;i<array_size;i++) {
array_c[i] = array_a[i] + array_b[i];
}
return 0;
}

プロシージャの出力は、インタフェース定義ファイルで[out]属性を指定したパラメータに設定することでPPE上から値を参照できる。


ppu/add.c: PPE上で実行するメインプログラムを定義する。

#include <stdio.h>
#include <libspe.h>
#include <libidl.h>
#include "../stub.h"
#include "../ppu_time.h"

int array_a[ARRAY_SIZE] __attribute__((aligned(128)));
int array_b[ARRAY_SIZE] __attribute__((aligned(128)));
int array_add[ARRAY_SIZE] __attribute__((aligned(128)));

#define PrintDebug(array, size) { \
unsigned int i;\
printf("array: {");\
for(i=0;i<size;i++) printf("%d ", array[i]); \
printf("}\n"); \
}

int main() {
int i, async_add_id;
uint64_t ts, time_do_add, time_do_vec_add, time_do_async_add, time_ppu_add;

puts("PPU starting.");
for(i=0;i<ARRAY_SIZE;i++) {
array_a[i] = i << 1;
array_b[i] = i;
array_add[i] = 0;
}

// リモートプロシージャを同期呼び出し—
printf("PPU: do_add calling...\n");
StartTimer(ts);
do_add(ARRAY_SIZE, (int*)(&array_a), (int*)(&array_b), (int*)(&array_add));
StopTimer(time_do_add, ts);
puts("done.");

// SIMDを用いて加算処理するプロシージャーの呼び出し†
printf("PPU: do_vec_add calling...\n");
StartTimer(ts);
do_vec_add(ARRAY_SIZE, (int*)(&array_a), (int*)(&array_b), (int*)(&array_add));
StopTimer(time_do_vec_add, ts);
puts("done.");

// リモートプロシージャを非同期呼び出し—
#ifdef IDL_ASYNC
printf("PPU: do_async_add calling...\n");
StartTimer(ts);
async_add_id = do_async_add(ARRAY_SIZE, (int*)(&array_a), (int*)(&array_b), (int*)(&array_add));
StopTimer(time_do_async_add, ts);

printf("PPU join_do_add \n");
idl_join_do_async_add(async_add_id);
#endif

// PPU 上で同じ処理を実行Œ
StartTimer(ts);
for(i=0;i<ARRAY_SIZE;i++) array_add[i] = array_a[i] + array_b[i];
StopTimer(time_ppu_add, ts);

// 実行時間を出力Š›
printf("PPU do_add: ");
PrintTimer(time_do_add);
printf("PPU do_vec_add: ");
PrintTimer(time_do_vec_add);
printf("PPU do_async_add: ");
PrintTimer(time_do_async_add);
printf("PPU add: ");
PrintTimer(time_ppu_add);

return 0;
}
Tips:
プロシージャで利用する、array_a, array_b, array_cとDMA転送されるデータを128byte境界でアラインする。実際PPEプログラム中でリモートプロシージャに渡す引数の値は128byteでアラインしなくても動作した。これは生成されたスタブ中で128byte境界でアラインするようにメモリの再割り当てを行っているためである。だが、ここでは128byteアラインを明示的に書くことにした。

IDLコンパイラにより生成されるソースコードの一部(ppu/stub_add.c)
/* make sure array_a is properly aligned */

/* allocate 128 bytes aligned chunk of mem for idl_wi->array_a */
idl_alloc_array_a = (int*)alloca(array_size*sizeof(int) + 127);
idl_wi->array_a = (int*)(((unsigned int)idl_alloc_array_a + 127) & 0xFFFFFF80);

memcpy (idl_wi->array_a, array_a, array_size*sizeof(int));
全ての引数に対してこの処理を適用している。

プロシージャの返り値であるidl_id_t型は、IDLコンパイラによってプロシージャーごとにユニークな値(1001から順にインクリメントされた値)が割り当てられる。この値を使用してプロシージャーを識別しidl_join_{プロシージャ名}で実行完了を待つことが出来る。

ppu_time.hの内容は、[Cell] PPE上での実行時間計測のプロファイリング用の関数とマクロをヘッダファイルにしたもの。

コンパイルと実行
ディレクトリ構造は、[Cell] Function-OffloadモデルでHello, Worldの記事と同じspu, ppuというサブディレクトが存在する。Makefileを次のように作成する。

./Makefile
DIRS      := ppu spu
IDL_SRC := add.idl
IDL_FLAGS := -i -p ppu/stub_add.c -s spu/stub_add.c -n 4 -b spu_add
INCLUDE := -I $(SDKINC) -I spu
include /opt/IBM/cell-sdk-1.1/make.footer

ppu/Makefile
PROGRAM_ppu     = ./ppu_add
CFLAGS := -DIDL_ASYNC -DDEBUG
CC_OPT_LEVEL := -O0
IMPORTS = $(SDKLIB)/libidl.a $(SDKLIB)/libmisc.a -lspe

CC_OPT_LEVELで最適化レベルを最適化無しにしないと、ppu_time.h中のmftb関数がインライン展開されて、loopラベルが衝突する。(これ、どうすればいいのだろう?)

spu/Makefile

PROGRAM_spu = $(SDKBIN_ppu)/samples/spu/spu_sync_add
IMPORTS = $(SDKLIB)/libc.a
include ../../../../../../make.footer
コンパイルと実行
% make
% cp spu/spu_add ppu/ppu_add /tmp

Cellシミュレータ上に転送

# callthru source /tmp/ppu_add > ppu_add
# callthru source /tmp/spu_add > spu_add
# chmod +x spu_add ppu_add
# ./ppu_add
省略
PPU do_add: time: 17.489200(msec)
PPU do_vec_add: time: 7.99760(msec)
PPU do_sync_add: time: 2.018320(msec)
PPU add: time: 0.008000(msec)
#

シミュレータ上での実行なので、上記のプロファイリング値はあくまで参考だが、実機でもスタブを介したDMA転送のコストが気になる。DMA転送を減らしながらタスクをSPEに並列化してスケジューリングするあたりのTipsが必要な気がする。PPU上のL1, L2キャッシュ機構やDMA転送のコスト、MFCといったCellのアーキテクチャをあとでやる。

実行結果



実行できた。