アクセラレータ (SPE) にタスクを実行させる場合には、パラメータを渡して作業内容を指示してやらなければなりません。
ALF LTS ではタスクコンテキストを使ってアクセラレータへ起動時パラメータを渡すことができます。本記事では、このタスクコンテキストの使い方を解説します。
タスクコンテキストは ALF LTS 専用の機能ではなく、従来の ALF (ワークブロックタスク)でもリダクションやアクセラレータデータ分割など、分割統治型のアルゴリズムを実装する際に活用されている機能です。
また、分割統治型のアルゴリズムでは、前後のタスクの依存関係を正しく順序付けてやらなくてはなりません。
タスクコンテキストと依存関係の考え方を学んでおくことは、ALF 以外のマルチコアフレームワークを使って並列処理を行う場合にも役立つことでしょう。
タスクコンテキストとは、タスクに対して割り当てられるデータセットのことです。
タスクコンテキストを用意しておくと、その内容が、タスクの実行直前にアクセラレータのローカルメモリにコピーされます(図 1 )。
例えば、タスクコンテキストにシステムメモリ上のアドレスを格納しておき、アクセラレータはそのアドレスに対して DMA 転送を行ってデータを取得し、それを処理する、といった用途に使用できます。
図 1. タスクコンテキスト
あるデータ列の最大値・最小値を探すプログラムを、タスクコンテキストを使用して作成してみましょう。
仕様は以下のとおりです(図 2 )。
- バラバラに並んだシステムメモリ上のデータ列から、アクセラレータで最大値・最小値を探索する。
- タスクコンテキストを使用してデータ列のアドレスをアクセラレータへ渡す。
- 発見した最大値・最小値は、データ列の先頭に DMA 転送で書き込む。
図 2. 最大値・最小値の探索
リスト 1 がホスト側 (PPE) のプログラムです。
リスト 1. 最大値・最小値探索プログラム (ホスト側)
001: #include <stdio.h> // for NULL, printf()
002: #include <memory.h> // for memcpy
003: #include <alf.h> // for alf_xxxx()
004:
005: #include "task_context.h"
006:
007: #define NUM_SPE (1)
008:
009: uint32_t _data[32] __attribute__((aligned(128))) = {
010: 1, 15, 28, 14,
011: 25, 20, 19, 11,
012: 21, 31, 2, 13,
013: 6, 5, 17, 27,
014: 18, 10, 30, 23,
015: 8, 7, 9, 26,
016: 29, 22, 12, 0,
017: 4, 16, 24, 3
018: };
019:
020: int main(int argc, const char* argv[])
021: {
022: task_context_t ctx;
023: alf_handle_t handle;
024: alf_task_desc_handle_t task_desc_handle;
025: alf_task_handle_t task_handle;
026:
027: //-------------------------------------------
028: // Initialization
029: //-------------------------------------------
030: // Set parameter to be passed to SPEs.
031: ctx.addr = (uint64_t)((uint32_t)_data);
032: ctx.length = sizeof(_data) / sizeof(_data[0]);
033: ctx.numspe = NUM_SPE;
034:
035: // Initialize ALF.
036: alf_init(NULL, &handle);
037: alf_num_instances_set(handle, NUM_SPE);
038:
039: //-------------------------------------------
040: // Finding phase
041: //-------------------------------------------
042: // Create task descriptor.
043: alf_task_desc_create(handle,
044: ALF_ACCEL_TYPE_EDP,
045: &task_desc_handle);
046:
047: // Set parameters for task main function.
048: alf_task_desc_set_int32(task_desc_handle,
049: ALF_TASK_DESC_TASK_TYPE,
050: ALF_TASK_TYPE_LIGHTWEIGHT);
051: alf_task_desc_set_int64(task_desc_handle,
052: ALF_TASK_DESC_ACCEL_LIBRARY_REF_L,
053: (unsigned long long)"libspu_library.so");
054: alf_task_desc_set_int64(task_desc_handle,
055: ALF_TASK_DESC_ACCEL_IMAGE_REF_L,
056: (unsigned long long)"spu_prog");
057: alf_task_desc_set_int64(task_desc_handle,
058: ALF_TASK_DESC_ACCEL_LTS_MAIN_REF_L,
059: (unsigned long long)"task_main");
060:
061: // Set parameter for task context.
062: alf_task_desc_set_int32(task_desc_handle,
063: ALF_TASK_DESC_MAX_STACK_SIZE,
064: 4096);
065: alf_task_desc_set_int32(task_desc_handle,
066: ALF_TASK_DESC_TSK_CTX_SIZE,
067: sizeof(task_context_t));
068: alf_task_desc_ctx_entry_add(task_desc_handle,
069: ALF_DATA_BYTE,
070: sizeof(task_context_t));
071:
072: // Run the task and wait.
073: alf_task_create(task_desc_handle, &ctx, NUM_SPE, 0, 0, &task_handle);
074: alf_task_finalize(task_handle);
075: alf_task_wait(task_handle, -1);
076:
077: //-------------------------------------------
078: // Cleaning up
079: //-------------------------------------------
080: // Exit ALF.
081: alf_exit(handle, ALF_EXIT_POLICY_FORCE, 0);
082:
083: // Output result.
084: printf("Maximum=%u, Minimum=%u\n", _data[0], _data[1]);
085:
086: return 0;
087: }
|
- 9~18行目: 探索対象のデータ列です。0~31 までの値がバラバラに並んでいます。
- 30~33行目: タスクコンテキストとして渡すデータを初期化しています。このサンプルプログラムでは、
task_context_tという構造体でタスクコンテキストを渡します。内容はデータ列のアドレス、データ総数、探索に使ったアクセラレータの個数です。 - 61~70行目: タスクコンテキストを使うための設定です。
ALF_TASK_DESC_MAX_STACK_SIZEはランタイムが使用する作業用メモリのサイズです。何も指定しなければ 1024 が使われます。タスクコンテキストのサイズ以上であれば構いません。ALF_TASK_DESC_TSK_CTX_SIZEはタスクコンテキストに使われるメモリのサイズです。デフォルトでは 0 ですので、タスクコンテキストを使用する場合は必ずセットする必要があります。alf_task_desc_ctx_entry_add()は、タスクコンテキストの内容を宣言します。この例ではバイト配列として宣言しています。
- 73行目: タスクコンテキストとして渡すデータを引数として与えます。
リスト 2 はアクセラレータ側 (SPE) のプログラムです。
リスト 2. 最大値・最小値探索プログラム (アクセラレータ側)
001: #include <spu_mfcio.h>
002: #include <stdio.h>
003:
004: #include <alf_accel.h>
005:
006: #include "../task_context.h"
007:
008: uint32_t _data[32] __attribute__((aligned(128)));
009:
010: int task_main(void* p_task_ctx ,
011: int instance_id,
012: int number_of_instance)
013: {
014: unsigned int i, length;
015: uint64_t addr;
016: uint32_t maximum, minimum;
017: task_context_t* ctx = (task_context_t*)p_task_ctx;
018:
019: // Get data
020: length = ctx->length;
021: addr = ctx->addr;
022: mfc_get(_data,
023: addr,
024: sizeof(uint32_t) * length,
025: 0, 0, 0);
026: mfc_write_tag_mask(1 << 0);
027: mfc_read_tag_status_all();
028:
029: // Find maximum and minimum.
030: maximum = 0;
031: minimum = 0x7fffffff;
032: for (i = 0; i < length; i++)
033: {
034: if ( _data[i] > maximum ) maximum = _data[i];
035: if ( _data[i] < minimum ) minimum = _data[i];
036: }
037: printf("[%d/%d] Maximum=%u, Minimum=%u\n",
038: instance_id,
039: number_of_instance,
040: maximum, minimum);
041:
042: // Put result
043: _data[0] = maximum;
044: _data[1] = minimum;
045: mfc_put(_data,
046: addr,
047: sizeof(uint32_t) * 2,
048: 0, 0, 0);
049: mfc_write_tag_mask(1 << 0);
050: mfc_read_tag_status_all();
051:
052: return 0;
053: }
054:
055: ALF_ACCEL_EXPORT_API_LIST_BEGIN;
056: ALF_ACCEL_EXPORT_API("", task_main);
057: ALF_ACCEL_EXPORT_API_LIST_END;
|
- 10, 17行目: タスクメイン関数の第一引数としてタスクコンテキストが渡されます。このサンプルプログラムでは
task_context_tという構造体でデータを渡しているので、キャストして使います。 - 19~27行目: 探索対象のデータ列をローカルメモリに転送します。
- 29~36行目: 最大値、最小値の探索をしています。
- 42~50行目: 発見した最大値・最小値をシステムメモリに転送します。
リスト 3 では、ホストとアクセラレータでタスクコンテキストとして共有されるデータ構造を宣言しています。
リスト 3. タスクコンテキストのデータ構造
001: #ifndef TASK_CONTEXT__H__
002: #define TASK_CONTEXT__H__
003:
004: #include <stdint.h>
005:
006: typedef struct task_context_tag
007: {
008: uint64_t addr;
009: uint32_t length;
010: uint32_t numspe;
011:
012: } task_context_t;
013:
014: #endif//TASK_CONTEXT__H__
|
実行すると図 3 のような結果になります。
図 3. 実行結果
マルチコアを活用するためには、アルゴリズムを並列化する必要があります。各アクセラレータで部分ごとに処理させて、その結果を統合するアルゴリズム(分割統治)に変更してみましょう(図 4 )。
部分ごとの処理はこれまでのプログラムを活用し、統合するプログラムを新たに追加します。
図 4. 分割統治型の最大値・最小値探索
複数のアクセラレータを使用する場合でも、タスクコンテキストは全てのアクセラレータで共有されます(図 5 )。アクセラレータごとに処理する部分を変えるためには、タスクメイン関数に渡される instance_id と number_of_instance を活用します。
図 5. 複数のアクセラレータがある場合のタスクコンテキスト
分割統治型のホスト側プログラムをリスト 4 に示します。変更のない部分は省略してあります。
リスト 4. 複数のアクセラレータがある場合の最大値・最小値探索プログラム (ホスト側、差分のみ)
・・・ 007: #define NUM_SPE (4) ・・・ 077: //------------------------------------------- 078: // Reduction phase 079: //------------------------------------------- 080: // Set parameters for task main function. 081: alf_task_desc_set_int64(task_desc_handle, 082: ALF_TASK_DESC_ACCEL_LTS_MAIN_REF_L, 083: (unsigned long long)"reduction_main"); 084: 085: // Run the task and wait. 086: alf_task_create(task_desc_handle, &ctx, NUM_SPE, 0, 0, &task_handle); 087: alf_task_finalize(task_handle); 088: alf_task_wait(task_handle, -1); ・・・ |
- 7行目: アクセラレータの個数を増やします。この例題では 2 の累乗のみサポートしています。
- 77目~88行目: 統合処理をするタスクを追加します。
reduction_mainという名前の統合用タスクメイン関数を使用します。
アクセラレータ側では、統合処理のためのタスクメイン関数 reduction_main() を追加しています。また、最大値・最小値探索のためのタスクメイン関数 task_main() も少しだけ変更があります(リスト 5 )。
リスト 5. 複数のアクセラレータがある場合の最大値・最小値探索プログラム (アクセラレータ側、差分のみ)
・・・
020: length = ctx->length / number_of_instance;
021: addr = ctx->addr + sizeof(uint32_t) * length * instance_id;
・・・
055: int reduction_main(void* p_task_ctx ,
056: int instance_id,
057: int number_of_instance)
058: {
059: unsigned int i, length;
060: uint32_t maximum, minimum;
061: task_context_t* ctx = (task_context_t*)p_task_ctx;
062:
063: maximum = 0;
064: minimum = 0x7fffffff;
065: length = ctx->length / ctx->numspe;
066: for (i = 0; i < ctx->numspe; i++)
067: {
068: // Get data
069: mfc_get(_data,
070: ctx->addr + i * sizeof(uint32_t) * length,
071: sizeof(uint32_t) * 2,
072: 0, 0, 0);
073: mfc_write_tag_mask(1 << 0);
074: mfc_read_tag_status_all();
075:
076: // Find maximum/minimum of partial results.
077: if ( _data[0] > maximum ) maximum = _data[0];
078: if ( _data[1] < minimum ) minimum = _data[1];
079: }
080:
081: // Put result
082: _data[0] = maximum;
083: _data[1] = minimum;
084: mfc_put(_data,
085: ctx->addr,
086: sizeof(uint32_t) * 2,
087: 0, 0, 0);
088: mfc_write_tag_mask(1 << 0);
089: mfc_read_tag_status_all();
090:
091: return 0;
092: }
093:
094: ALF_ACCEL_EXPORT_API_LIST_BEGIN;
095: ALF_ACCEL_EXPORT_API("", task_main);
096: ALF_ACCEL_EXPORT_API("", reduction_main);
097: ALF_ACCEL_EXPORT_API_LIST_END;
|
- 20行目: 探索対象データの総数をアクセラレータの個数で割り、自分が担当すべき個数を求めています。
- 21行目: 同様に、自分が担当すべき範囲の、先頭アドレスを求めています。
- 55~92行目: 統合のためのタスクメイン関数です。図 4 の黄色で示した部分のみを順に読み取り、最大値・最小値を求めます。
- 96行目: エクスポートも忘れずに。
これを実行すると図 4 のような結果になります。順番は環境によって異なるでしょう。
図 6. 複数アクセラレータの場合の実行結果
各アクセラレータで異なる部分を並列処理することができました。
リスト 4 では、最大値・最小値の部分探索と統合を2つのタスクに分けて、探索の完了を待機してから統合を実行していました。しかし、ALF にはタスク依存関係を定義することでこの待機&実行を自動的に行う仕組みがあります。
この仕組みを使ってタスクをパイプライン実行させるようにしたのが、リスト 6 です。
リスト 6. タスクパイプライン (ホスト側)
・・・
020: int main(int argc, const char* argv[])
021: {
・・・
025: alf_task_handle_t task_handle_1;
026: alf_task_handle_t task_handle_2;
027:
・・・(Initializationは同じ)
040: //-------------------------------------------
041: // Finding phase
042: //-------------------------------------------
・・・
073: // Create task
074: alf_task_create(task_desc_handle, &ctx, NUM_SPE, 0, 0, &task_handle_1);
075:
076: //-------------------------------------------
077: // Reduction phase
078: //-------------------------------------------
・・・
084: // Create task
085: alf_task_create(task_desc_handle, &ctx, NUM_SPE, 0, 0, &task_handle_2);
086:
087: //-------------------------------------------
088: // Execution with pipelining
089: //-------------------------------------------
090: alf_task_depends_on(task_handle_2, task_handle_1);
091:
092: alf_task_finalize(task_handle_1);
093: alf_task_finalize(task_handle_2);
094: alf_task_wait(task_handle_2, -1);
095:
096: //-------------------------------------------
097: // Cleaning up
098: //-------------------------------------------
・・・
|
- 25~26行目: タスクハンドルは 2 つ用意しなければなりません。これまでは前のタスクが終ってから次のタスクを作成していたので再利用できましたが、パイプラインの場合にはできません。
- 74、85行目: 各タスクは作成だけを行い、ファイナライズと完了待機は後で行います。ファイナライズされるまで実行は始まりません。
- 90行目: 作成済みの 2 つのタスクの間に依存関係を設定しています。1 番目の引数で与えたタスクは、2 番目の引数で与えたタスクが完了するまで待機してから、実行を開始します。
- 92~93行目: 依存関係を定義したらファイナライズを行います。ファイナライズされると実行が始まります。
- 94行目: 2 番目のタスクを待機します。1 番目のタスクの完了を待機する必要はありません。
実際には、alf_task_depends_on() の呼び出しは、1 番目のタスクのファイナライズ後でも構いません(そしてもちろん 2 番目のタスクのファイナライズ前でなければなりません)。ですので、forループの中で依存関係のチェーンを作りながら次々とタスクを実行するようなことも可能です。
例ではシステムメモリにデータを置いて DMA 転送で読み書きを行いましたが、タスクコンテキストに直接データ列を与えて書き換えることはできないのでしょうか?
答えは No です。
LTS ではタスクコンテキストに対する書き込みはサポートされていません。書き込みをしてもホスト側に反映されません。結果を戻す場合は、本記事の例題のようにタスクコンテキストの中に書き込み先のアドレスを格納しておいて明示的に DMA 転送を実行する必要があります。
ところで、皆さんの中には普段は C++ を使っているという方も多いと思います。
もちろん、ALF LTS も C++ とともに使うことができますが、少しだけ工夫が必要ですのでここで紹介しておきます。
タスクメイン関数のエクスポートは、 3 つのマクロ ALF_ACCEL_EXPORT_API_LIST_BEGIN 、 ALF_ACCEL_EXPORT_API 、 ALF_ACCEL_EXPORT_API_LIST_END を利用します。
しかし、マクロ内部でライブラリの外部参照を利用しており、ここが C++ に対応していません。
対策をしたものがリスト 7 です。
リスト 7. C++ で ALF を使う場合 (アクセラレータ側)
001: #include <alf_accel.h>
002:
003: extern "C"
004: int task_main(void* p_task_ctx ,
005: int instance_id,
006: int number_of_instance)
007: {
008: return 0;
009: }
010:
011: extern "C"
012: {
013: ALF_ACCEL_EXPORT_API_LIST_BEGIN;
014: ALF_ACCEL_EXPORT_API("", task_main);
015: ALF_ACCEL_EXPORT_API_LIST_END;
016: }
|
まず ALF_ACCEL_EXPORT_API_LIST_BEGIN ~ ALF_ACCEL_EXPORT_API_LIST_END の前後を extern "C" { ... } で囲みます (11 行目~ 16 行目) 。これにより、マクロの問題を回避できます。
しかし、この副作用として 14 行目でエクスポートする関数名に、 C++ の装飾名が使えなくなります (注) 。そこで、タスクメイン関数のプロトタイプ宣言および定義にも extern "C" を付けます。
以上のことに注意すれば C++ でも ALF LTS を使うことができます。
ホスト側は C++ に対応しているので、特に注意事項はありません。
(注) C++ では、関数オーバーロード機能への対応のために関数名を内部的に変更(装飾)しています。しかし、この関数名装飾の方法はコンパイラ依存であり、異なるコンパイラ間や異なる言語間で共通に使用されるライブラリを作成する場合には、 extern "C" を付加して装飾名の使用を禁止するのが一般的な方法となっています。装飾を禁止した関数については、オーバーロード機能は使用できなくなります。
本記事では、 ALF 軽量タスクサポートを使って SPE タスクに起動時パラメータ(コンテキスト)を渡す方法と、依存関係を利用したタスクパイプラインについて解説しました。
学ぶために
- ALF については、Fun with ALF シリーズをチェックしましょう。
- 第 1 回 大きな行列の加算
- 第 2 回 I/Oデータの変換
- 第 3 回 最小値と最大値の探索
- 第 4 回 大きなベクトルの内積の計算
- 第 5 回 オーバーラップI/Oバッファを利用した行列の加算
- 第 6 回 タスク依存関係の利用
- The IBM Semiconductor Solutions Technical Library Power Architecture offerings セクションには、ダウンロードマニュアルや、仕様書などたくさんの有益な情報があります。
製品や技術を入手するために
- 全ての Cell/B.E. 情報 --関連記事、ディスカッションフォーラム、CellSDK、その他のダウンロード-- は IBM developerWorks の Cell Broadband Engine resource center にあります。
- Cell/B.E. を手に入れるにはこちら: IBM ディープ・コンピューティング | Cell/B.E ブレード
- Cell/B.E.のカスタム製品のお問い合わせはこちら: Contact us about Cell Broadband Engine technology.
議論するために
- ディスカッションフォーラムに参加してください.
- 質問はIBM developerWorks Power Architecture Cell Broadband Engine discussion forum へ投稿してください。
