 | レベル: 中級 土居 意弘 (munepi@jp.ibm.com), IBM Systems & Technology Group, AKD48
2009年 05月 15日 Accelerated Library Framework (ALF) の新しいタスクモデルである、軽量タスクサポート (Lightweight task support, LTS) が SDK 3.1 から加わりました。本記事では、ALF を利用したライブラリの作成を容易にする、共有 ALF インスタンスについて解説します。また、複数のタスクを ALF がどのようにスケジューリングするのか、実験で確認してみます。
はじめに
本記事では、ALF を利用したライブラリが複数ある場合等に有効な共有 ALF インスタンスについて解説します。
また、作成した複数のタスクが要求している SPE の合計が、システムが備える SPE の総数を超えるような場合、各タスクがどのようにスケジューリングされるのか、ALF LTS の場合に関して実験によって確かめます。
共有 ALF インスタンスとは
ALF を使用する場合、まずはじめに必要なことは、alf_init() を実行して ALF のライブラリインスタンスを初期化することです。これによって ALF ハンドルを取得し、その後のタスク生成で使用します。
ここで、内部的に ALF を使用する2つの異なる関数 start_task_A() と start_task_B() を使いたいとしましょう。そして、それぞれ4 個と 2 個の SPE を使いたいとします。また、これらの関数はタスク完了を待たずに制御を返し、それぞれ join_task_A() と join_task_B() によって完了を待機するものとします。
alf_init() による方法では、各 ALF インスタンスごとにシステムが備える SPE をすべて予約します。インスタンスごとに使用する SPE の制御は独立しているので、それぞれの関数が全ての SPE を使わないとしても、それらを融通することはできません。
例えば、start_task_A() と start_task_B() を順に実行するとした場合、それぞれのインスタンスで 全 SPE を予約してしまうので、 Cell/B.E. には 8 個の SPE があるにもかかわらず、start_task_A() で生成されたタスクが終了した後でなければ、 start_task_B() で生成されたタスクを実行開始できないのです (図 1、注)。
図 1. 独立した2つのインスタンスによるタスクの実行
このような問題を解決するものとして、SDK3.1 からは、ALF 共有インスタンスという機能が導入されました。
ALF 共有インスタンスとは、プロセスに1つだけ存在する ALF インスタンスで、それを全ての ALF タスク生成で利用します。
ハンドルを共有する仕組みを作成すれば同様の機能は実現可能ではありますが、例えば異なるライブラリに含まれる関数の間にそのような共有機構を導入するのは困難であり、 ALF 自身によってサポートされたことは非常に重要です。
先の例を再び考えてみましょう。共有インスタンスを用いれば、 8 個ある SPE のうち、4 個を使って start_task_A() を、2 個を使って start_task_B() を並列実行できるようになります (図 2、注)。
図 2. 共有インスタンスによるタスクの実行
使い方は簡単で、alf_init() の代わりに alf_init_shared() を使用するだけです。
既に作成された共有インスタンスがあればその参照を返し、なければ新たに ALF のリソースを確保します。
alf_exit()の使い方も変わりません。他に共有インスタンスを参照しているハンドルが存在していなければリソースを削除します。
注:なお、図 1 と図 2 では、わかりやすさのためインスタンスの作成と SPE の予約を同じタイミングであるかのように描きましたが、実際はこれらは同じではありません。たとえばインスタンス A が SPE を予約している間にもインスタンス B を作り、タスクを投入しておくことはできます。しかし、この場合でもインスタンス A が SPE を解放しなれば、インスタンス B のタスクの実行は開始されません。
使ってみよう
早速使ってみましょう。
ある並列処理関数を、ALF LTS と共有インスタンスを使って作成してみます。ALF LTS 関連の処理はすべてこの関数の中に収めることで、ユーザーからは ALF を使用していることがわからないようにしてみます。そして、この ALF LTS ライブラリ関数を複数回呼び出して連続実行してみましょう。
リスト 1 がユーザー側のプログラムです。start_task()とjoin_task()が何らかの処理をする関数で、内部的に ALF LTS によって並列処理を行います。
リスト 1. ALF LTS を使用した並列処理ライブラリのユーザー側プログラム
001: #include <stdio.h> // for printf()
002:
003: extern void* start_task(int num_spe_desired,
004: const char* name);
005: extern void join_task(void*);
006:
007: void test1()
008: {
009: void* task1 = start_task(1, "Task 1");
010: void* task2 = start_task(2, "Task 2");
011:
012: join_task(task1);
013: join_task(task2);
014: }
|
- 9, 10行目: タスクを開始しています。引数は、使用したい SPE 個数とタスク名です。ここでは並列を処理したらすぐに関数から戻り、完了待機は行いません(ノンブロッキング)。Task 1 と Task 2 は並列に実行されます。
- 12, 13行目: タスクの終了を待機しています。
リスト 2 は、ライブラリ関数のホスト側プログラムです。
リスト 2. ALF LTS を使用した並列処理ライブラリのプログラム (ホスト側)
001: #include <cstdio> // for printf
002: #include <cstdlib> // for NULL
003: #include <string> // for string
004: #include <alf.h> // for alf_xxxx()
005:
006: struct INFO
007: {
008: alf_handle_t handle;
009: alf_task_handle_t task_handle;
010: std::string name;
011: };
012:
013: void* start_task(int num_spe_desired,
014: const char* name)
015: {
016: alf_handle_t handle;
017: alf_task_desc_handle_t task_desc_handle;
018: alf_task_handle_t task_handle;
019: INFO* info;
020:
021: // Initialize ALF.
022: alf_init_shared(NULL, &handle);
023: alf_num_instances_set(handle, 8);
024:
025: // Create task descriptor.
026: alf_task_desc_create(handle,
027: (ALF_ACCEL_TYPE_T)NULL,
028: &task_desc_handle);
029:
030: // Set parameters for task main function.
031: alf_task_desc_set_int32(task_desc_handle,
032: ALF_TASK_DESC_TASK_TYPE,
033: ALF_TASK_TYPE_LIGHTWEIGHT);
034: alf_task_desc_set_int64(task_desc_handle,
035: ALF_TASK_DESC_ACCEL_LIBRARY_REF_L,
036: (unsigned long long)"libspu_prog.so");
037: alf_task_desc_set_int64(task_desc_handle,
038: ALF_TASK_DESC_ACCEL_IMAGE_REF_L,
039: (unsigned long long)"spu_prog");
040: alf_task_desc_set_int64(task_desc_handle,
041: ALF_TASK_DESC_ACCEL_LTS_MAIN_REF_L,
042: (unsigned long long)"task_main");
043:
044: // Create task.
045: alf_task_create(task_desc_handle,
046: NULL,
047: num_spe_desired,
048: 0,
049: 0,
050: &task_handle);
051:
052: // Return ALF task handle
053: info = new INFO;
054: info->handle = handle;
055: info->task_handle = task_handle;
056: info->name = name;
057:
058: // Run the task.
059: alf_task_finalize(task_handle);
060:
061: return (void*)info;
062: }
063:
064: void join_task(void* info_)
065: {
066: INFO* info = (INFO*)info_;
067:
068: // Wait task completion.
069: alf_task_wait(info->task_handle, -1);
070:
071: // Exit ALF.
072: alf_exit(info->handle, ALF_EXIT_POLICY_WAIT, -1);
073:
074: // Delete info object.
075: delete info;
076: }
|
このシリーズのこれまでの記事を読んでいれば特に難しい部分はないでしょう。
ユーザーが何度も呼び出すかもしれないライブラリ関数の中に ALF プログラムのすべてが入っている点に注目です。
- 6~11行目: タスクの生成と完了待機を
start_task()とjoin_task()に分離したいため、それらの間でデータの橋渡しをする構造体を定義しています。今回は複数のタスクを並列実行したいのでこのようにしています。
- 22行目: 今回のポイントである ALF 共有インスタンスの生成を行っています。
- 23行目: この共有インスタンスで使用する SPE 総数をセットしています。
リスト 3 は、ALF LTS を使用した並列処理ライブラリのアクセラレータ側プログラムです。並列処理と言ってもここでは Hello World !!! と表示するだけですが、ぜひ読者の皆さんで面白い処理を実装してください。
リスト 3. ALF LTS を使用した並列処理ライブラリのプログラム (アクセラレータ側)
001: #include <spu_mfcio.h>
002: #include <stdio.h>
003:
004: #include <alf_accel.h>
005:
006: int task_main(void* p_task_ctx ,
007: int instance_id,
008: int number_of_instance)
009: {
010: printf("[%d/%d] Hello World !!!\n",
011: instance_id,
012: number_of_instance);
013:
014: return 0;
015: }
016:
017: ALF_ACCEL_EXPORT_API_LIST_BEGIN;
018: ALF_ACCEL_EXPORT_API("", task_main);
019: ALF_ACCEL_EXPORT_API_LIST_END;
|
実行すると図 3 のような結果になります。
各行の先頭に表示されている数字の分母を見ると、SPE を 2 個使うタスクと、SPE を 1 つだけ使うタスクが、同時に実行されている様子がわかります。
図 3. 実行結果
ALF LTS のスケジューリング方法を知る
共有インスタンスを使ってライブラリを構築すると、SPE を使いたい関数が同時にいくつも並列実行される可能性があります。このような場合に、ALF がどのようにアクセラレータインスタンスを作成するのか調べてみましょう。
なお、本記事では ALF LTS のみに注目しているので、ALF ワークブロックタスクの場合についてはドキュメントを参照ください。
デフォルトのスケジューリング
ユーザー側のプログラムをリスト 4 のように変えて、2 つのタスクで使用する SPE の合計が、総数である 8 個を超えるようにしてみます。
リスト 4. SPE 総数以上の SPE を使おうとするユーザープログラム
void test2()
{
void* task1 = start_task(7, "Task 1");
void* task2 = start_task(8, "Task 2");
join_task(task1);
join_task(task2);
}
|
図 4 が実行結果です。
なんと、2 番目に呼び出した、SPE 8 個を要求する Task 2 は、SPE 1 個のみで実行されてしまいました。
このように、デフォルトでは実行開始を早くするというスケジューリングがなされます。しかし、マルチコアによる並列化を期待していた場合は望む結果が得られません。
図 4. SPE 総数を超える場合のデフォルトのスケジューリングによる実行結果
イベントハンドラーでさらに確かめる
どちらのタスクがどのように処理されているかをより正確に確認するために、イベントハンドラーを使用しましょう。
リスト 5 をリスト 2 に追加してください。 ALF の内部イベントが発生する度に指定したコールバック関数 (task_event_handler()) が呼び出されるようになります。
リスト 5. イベントハンドラー
012:
static
int task_event_handler(alf_task_handle_t task_handle,
ALF_TASK_EVENT_TYPE_T event,
void* p_data)
{
INFO* info = (INFO*)p_data;
switch ( event )
{
case ALF_TASK_EVENT_FINALIZED :
fprintf(stderr, "[%s:0x%08u] ALF_TASK_EVENT_FINALIZED\n",
info->name.c_str(),
(unsigned int)task_handle);
break;
case ALF_TASK_EVENT_READY :
fprintf(stderr, "[%s:0x%08u] ALF_TASK_EVENT_READY\n",
info->name.c_str(),
(unsigned int)task_handle);
break;
case ALF_TASK_EVENT_FINISHED :
fprintf(stderr, "[%s:0x%08u] ALF_TASK_EVENT_FINISHED\n",
info->name.c_str(),
(unsigned int)task_handle);
break;
case ALF_TASK_EVENT_INSTANCE_START :
fprintf(stderr, "[%s:0x%08u] ALF_TASK_EVENT_INSTANCE_START\n",
info->name.c_str(),
(unsigned int)task_handle);
break;
case ALF_TASK_EVENT_INSTANCE_END :
fprintf(stderr, "[%s:0x%08u] ALF_TASK_EVENT_INSTANCE_END\n",
info->name.c_str(),
(unsigned int)task_handle);
break;
case ALF_TASK_EVENT_DESTROY :
fprintf(stderr, "[%s:0x%08u] ALF_TASK_EVENT_DESTROY\n",
info->name.c_str(),
(unsigned int)task_handle);
break;
}
return 0;
}
013:
...
057:
// Set event handler for the task.
alf_task_event_handler_register(task_handle, task_event_handler,
info, sizeof(INFO),
0xffffffff);
058:
|
イベントハンドラーによって得られたタスク実行の様子を見てみましょう (図 5)。
ALF_TASK_EVENT_INSTANCE_START の個数を数えてみると、やはり Task 2 は 1 つの SPE のみで実行されていることが確かめられました。
図 5. SPE 総数を超える場合のデフォルトのスケジューリングによる実行結果(イベント)
SPE 個数固定スケジューリング
使用する SPE の数を指定した値に固定したい場合は、ALF_TASK_ATTR_SCHED_FIXEDを使用することで実現できます。
リスト 2 を次のように変更します。
リスト 6. SPE 個数固定スケジューリング (リスト 2 との差分のみ)
044: // Create task.
045: alf_task_create(task_desc_handle,
046: NULL,
047: num_spe_desired,
048: ALF_TASK_ATTR_SCHED_FIXED,
049: 0,
050: &task_handle);
|
固定スケジューリングを使用した結果が 図 6 です。
Task 2 も、8 個の SPE で実行されるようになりました。
図 6. SPE 個数固定スケジューリングによる実行結果
アウト・オブ・オーダースケジューリング
Task 2 は 8 個の SPE が利用できるようになるまで待機していますが、Task 1 は 7 つの SPE しか使用しませんので 1 つ空いています。
そこでリスト 7 のような実験をしてみましょう。
リスト 7 では、3 つのタスクを実行しています。2 つはこれまでと同じですが、3 つ目として SPE を 1 つだけ利用するタスクを追加しています。あわよくば Task 2 を追い越して空いている SPE を使おうというわけです。
リスト 7. 3 つのタスクを実行するユーザープログラム
void test3()
{
void* task1 = start_task(7, "Task 1");
void* task2 = start_task(8, "Task 2");
void* task3 = start_task(1, "Task 3");
join_task(task1);
join_task(task2);
join_task(task3);
}
|
実行結果が 図 7 です。
見事 Task 3 の追い越し実行 (アウト・オブ・オーダー実行) に成功しました。
このように ALF LTS では空いている SPE を最大限利用するように効率的にスケジューリングしてくれます。
図 7. アウト・オブ・オーダー実行の結果
まとめ
本記事では、 ALF 共有インスタンスの使い方を解説しました。また、ALF LTS の SPE スケジューリングについて実験を行い、効率的なスケジューリングを行ってくれることを確認しました。
ALF を使って、どんどん面白いプログラムを書いていきましょう!
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | 
|  | 土居意弘はIBMで6年間、Cell/B.E. や Digital Signal Processor 上で画像処理システムなどを開発してきました。現在は日本IBMのテクニカルエキスパートとして、 Cell/B.E. 向けソフトウェアの実装や Hybrid & Multicore システム設計のコンサルティング業務などを行っています。 |
記事の評価
|  |