Gears OS の並列処理
|
伊波 立樹
琉球大学理工学研究科 河野研
|
並列処理の重要性
- 並列処理は現在主流のマルチコアCPU の性能を発揮するには重要なものになっている
- しかし、並列処理のチューニングや信頼性を保証するのは難しい
- 共通資源の競合などの非決定的な実行が発生するため、従来のテストやデバッグではテストしきれない部分が残ってしまう
- GPU などのアーキテクチャに合わせた並列プログラミングの記述
Gears OS
- 本研究室では処理の単位を Code Gear、 データの単位を Data Gear を用いて 信頼性が高い並列処理を行う Gears OS を開発している
- 並列処理の Task を Code Gear と実行するときに必要な Input Data Gear と出力するための Output Data Gear の組で表現される
- 計算をノーマルレベルとメタレベルに階層化、 信頼性と拡張性をメタレベルで保証する
- 並列処理の信頼性を通常の計算(ノーマルレベル) に保証
- CPU、GPU などの実行環境の切り替え、データ拡張等を提供
Gears OS
- 本研究ではGears OS の並列処理機構、並列処理構文(par goto)の実装、Gears OS を実装するにつれて必要なったモジュール化の導入を行う
- また、並列処理を行う例題を用いて評価、 OpenMP、 Go 言語との比較を行う
Code Gear/Data Gear
- Gears OS は Code Gear、 Data Gear という単位で構成される
- Code Gear はプログラムの処理そのものを表す
- Data Gear はデータそのものを表す
- Code Gear は必要な Input Data Gear が揃ったら実行し、Output Data Gear を生成する
- Code Gear と Input / Output Data Gear の対応から依存関係を解決し、Input Data Gear が揃った Code Gear の並列実行を行う
メタ計算
- メタ計算 は通常の計算を実行するための計算
- 信頼性の確保やメモリ管理、スレッド管理、CPU、GPU の資源管理等
- Gears OS のメタ計算は通常の計算とは別の階層のメタレベルで行われる
- メタレベルは Code/Data Gear に対応して Meta Code/Data Gear で行われる
- メタ計算 は Code Gear の接続間に行われる
- メタ計算 の処理も Code/Data Gear で実現する
- この Gear を Meta Code/Data Gearと呼ぶ
- Meta Code Gear は メタ計算 のプログラム部分
- Meta Data Gear は Meta Code Gear で管理されるデータ部分
- Gears OS は通常の Code/Data Gear から Meta Code/Data Gear 部分は見えないように実装を行う
Continuation based C
- Gears OS の実装は本研究室で開発している Continuation based C(CbC) を用いる
- CbC は Code Gear を用いて記述する事を基本とする
Continuation based C
- CbC の Code Gear の定義は __code CG名 で行う
- Code Gear 間は goto CG名 で移動する。この移動を継続と呼ぶ
- Code Gear の継続は C の関数呼び出しとは異なり、戻り値を持たないためスタックの変更を行わない
- 下記のコードでは Code Gear を2つ定義し、 cg0 から cg1 への継続を示している
__code cg0(int a, int b) {
goto cg1(a+b);
}
__code cg1(int c) {
goto cg2(c);
}
Context
- Context は接続可能な Code/Data Gear の集合を表現する Meta Data Gear
- 従来のOS のスレッドやプロセスに対応し、以下の要素を定義している
- 独立したメモリ空間
- Code/Data Gear へのポインタ
- 並列実行用の Task 情報
- Data Gear の型情報
- Gears OS ではメタ計算でこの Context を経由して Data Gear にアクセスする
Data Gear の表現
- Data Gear は構造体を用いて定義する
- メタ計算では任意の Data Gear を一律に扱うため、全ての Data Gear は共用体の中で定義される
- Data Gear のメモリに確保する際のサイズ情報はこの型から決定する
/* data Gear define */
union Data {
struct Timer {
union Data* timer;
enum Code start;
enum Code end;
enum Code next;
} Timer;
struct TimerImpl {
double time;
} TimerImpl;
....
};
stub Code Gear
- Data Gear にアクセスするにはContext を経由する
- だが、通常の Code Gear では Meta Data Gear である Context の参照は避ける必要がある
- Gears OS ではメタレベルで通常の Code Gear で必要な Data Gear を Context から取り出す処理を行う stub Code Gear を用意している
// normal level Code Gear
__code cg0(struct Context* context, struct Integer integer, struct Queue queue) {
...
}
// meta level stub Code Gear
__code cg0_stub(struct Context* context) {
// get data index number
Integer integer = &context->data[context->dataNum]->Integer
// get enum data
Queue* queue = &context->data[Queue]->Queue;
// continuation Code Gear
goto cg0(context, integer, queue);
}
Context での stub Code Gear の記述の問題点
- stub Code Gear は Context から Code Gear と Data Gear の全ての組合せを展開して記述する必要がある
- しかし、 Gears OS を実装するに連れて、 stub Code Gear の記述が煩雑になる場所がでてきた
- Data Gear は番号で指定するため、 Code Gear が どの Data Gear の番号に対応しているかを記述する必要がある
- stub Code Gear を自動生成するために、同じ番号の Data Gear を使いまわす問題
- そのため Gears OS のモジュール化する仕組みとして Interface を導入した
Interface
- Interface はある Data Gear と それに対する操作(API) を行う Code Gear の集合を表現する Meta Data Gear
- stub Code Gear はInteface を実装した Code Gear で決まった形になるため、自動生成が可能である
- Interface を導入することで、 Stack や Queue などのデータ構造を仕様と実装に分けて記述することが出来る
- Interface は Java のインターフェース、 Haskell の型クラスに対応する
Interface の定義
- Interface の定義には以下の内容を定義する
- 引数のData Gear 群
- 操作(API) 実行後に継続される Code Gear
- 操作(API) である Code Gear と Code Gear に渡す引数情報
typedef struct Queue<Impl>{
// Data Gear parameter
union Data* queue;
union Data* data;
__code next(...);
__code whenEmpty(...);
// Code Gear
__code clear(Impl* queue, __code next(...));
__code put(Impl* queue, union Data* data, __code next(...));
__code take(Impl* queue, __code next(union Data*, ...));
__code isEmpty(Impl* queue, __code next(...), __code whenEmpty(...));
} Queue;
Interface の実装
- Interface には複数の実装を行うことが出来る
- 実装した Code Gear を Interface で定義した Code Gear に代入することで実装を行う
- 代入する Code Gear を入れ替えることで別の実装を表現する
- 実装した Data Gear の生成は関数呼び出しで行われ、外から見るとInterface の型で扱われる
Queue* createSingleLinkedQueue(struct Context* context) {
struct Queue* queue = new Queue(); // Allocate Queue interface
struct SingleLinkedQueue* singleLinkedQueue = new SingleLinkedQueue(); // Allocate Queue implement
queue->queue = (union Data*)singleLinkedQueue;
singleLinkedQueue->top = new Element();
singleLinkedQueue->last = singleLinkedQueue->top;
queue->clear = C_clearSingleLinkedQueue;
queue->put = C_putSingleLinkedQueue;
queue->take = C_takeSingleLinkedQueue;
queue->isEmpty = C_isEmptySingleLinkedQueue;
return queue;
}
Interface の実装例
- SingleLinkedQueue の put 実装
- 引数は Queue Interface の put 定義にあわせる
- 第1引数は 実装対象の Data Gear の型になる
- 第3引数の(…) は Output Data Gear を記述する
- … は可変長引数のような扱いで、 継続先の Code Gear が複数の値をInput Data Gear とする可能性がある
__code putSingleLinkedQueue(struct SingleLinkedQueue* queue, union Data* data, __code next(...)) {
Element* element = new Element();
element->data = data;
element->next = NULL;
queue->last->next = element;
queue->last = element;
goto next(...);
}
Interface を利用した Code Gear の呼び出し
- Interface を利用した Code Gear への継続は
goto interface->method
で行われる
- ここでの interface は Interfaceの型で包んだData Gear、 method は実装した Code Gear に対応する
__code code1() {
Queue* queue = createSingleLinkedQueue(context);
Node* node = new Node();
node->color = Red;
goto queue->put(node, queueTest2);
}
Interface を利用した Code Gear の呼び出し(スクリプト変換後)
- Interface を利用した Code Gear の継続はスクリプトによって変換される
- 変換後は Context を参照するため、メタレベルの記述になる
- Gearef マクロは Context から Interface の引数格納用の Data Gear を取り出す
- この Data Gear は Context を初期化した際に特別に生成され、型は Interface と同じになる
- 呼び出すCode Gear の引数情報に合わせて引数に格納し、 実装された Code Gear へ継続する
__code code1(struct Context *context) {
Queue* queue = createSingleLinkedQueue(context);
Node* node = &ALLOCATE(context, Node)->Node;
node->color = Red;
Gearef(context, Queue)->queue = (union Data*) queue;
Gearef(context, Queue)->data = (union Data*) node;
Gearef(context, Queue)->next = C_queueTest2;
goto meta(context, queue->put);
}
Interface での stub Code Gear
- メタ計算で格納された引数は stub Code Gear で Code Gear に渡される
- Interface を実装した Code Gear は Interface の定義から stub Code Gear の自動生成が可能である
__code putSingleLinkedQueue(struct Context *context,struct SingleLinkedQueue* queue, union Data* data, enum Code next) {
Element* element = &ALLOCATE(context, Element)->Element;
element->data = data;
element->next = NULL;
queue->last->next = element;
queue->last = element;
goto meta(context, next);
}
// generated by script
__code putSingleLinkedQueue_stub(struct Context* context) {
SingleLinkedQueue* queue = (SingleLinkedQueue*)GearImpl(context, Queue, queue);
Data* data = Gearef(context, Queue)->data;
enum Code next = Gearef(context, Queue)->next;
goto putSingleLinkedQueue(context, queue, data, next);
}
並列処理の構成
- 今回はInterface を利用した並列処理機構の実装を行う
- 構成要素として以下が挙げられる
- Task(Context)
- TaskManager
- Worker の生成、依存関係を解決したTask を Worker に送信する
- Worker
- SynchronizedQueue から Task を取得し、実行する
- SynchronizedQueue
- マルチスレッド 環境でもデータの一貫性を保証する Queue
Task(Context)
- Gears OS では並列実行する Task を Context で表現する
- Context は Task用の情報として以下の情報をもっている
- 実行する Code Gear
- Input/Output Data Gear の格納場所
- 待っている Input Data Gear の数
- 実際に実行される Code Gear の引数情報は Interface の Code Gear 実装と同等に記述できる
- Code Gear の stub Code Gear は自動生成される
__code add(struct Integer* input1, struct Integer* input2, __code next(struct Integer* output, ...)) {
output->value = input1->value + input2->value;
goto next(output, ...);
}
TaskManger
- 依存関係を解決した Task を各 Worker の Queue に送信する
- Worker を作成、終了処理も行う
- Task を Input Data Gear として
TaskManager の spawn を呼び出す
- Input Data Gear が揃っているかを確認する
- 揃っている場合、 Worker の Queue に
Task を送信する
Worker
- 初期化の際にスレッドと Worker 用の Context を生成する
- 生成されたスレッドではTaskManager から送信された Task を取得して実行する
- Worker は Queue から Task を取得する
- Worker の Context から
Task の Context へ入れ替える
- Task の Code Gear を実行
- Task の Output Data Gear の書き出し
- Task Context から
Worker の Context へ入れ替える
- Worker は再び Queue から Task を取得する
Synchronized Queue
- Worker で使用される Queue
- Task を送信するスレッドと Task を取得するスレッドで操作される
- そのためマルチスレッドでのデータの一貫性を保証する必要がある
- Gears OS では CAS(Check and Set、 Compare and Swap) を使用した Synchronized Queue として実装する
- CAS は値を更新する際に更新前の値と実際に保存されているメモリ番地の値を比較する
- この Queue は Queue Interface を実装し、 List を利用した実装を行った
struct SynchronizedQueue {
struct Element* top;
struct Element* last;
struct Atomic* atomic;
};
// Singly Linked List element
struct Element {
union Data* top;
struct Element* next;
};
依存関係の解決
- 依存関係の解決は Data Gear がメタレベルで持っている Queue を使用する
- この Queue には Data Gear に依存関係がある Code Gear が格納されている
- Task の Code Gear を実行する
- Output Data Gear の書き出し処理を行う
この際にメタレベルの Queue を参照する
- 依存関係にある Task を取り出し、 待っている
Data Gearのカウンタをデクリメントする
- カウンタの値が0になった実行可能な Task は TaskManager を通して Worker に送信される
並列構文
- 並列実行の Task の生成は新しく Context を生成し、実行する Code Gear、待ち合わせる Input Data Gear の数、Input/Output Data Gear への参照を設定する
- この記述を直接書くと Meta Data Gear である Context を直接参照しているため、ノーマルレベルでの記述では好ましくない
- Task の設定は煩雑な記述であるが、並列実行されることを除けば通常の CbC の goto 文と同等である
- そこで Context を直接参照しない並列構文、 par goto 構文を新たに考案した
- par goto 構文には引数として Input/Output Data Gear等を渡す
- スクリプトによって Code Gear の Input/Output の数を解析する
__code code1(Integer *integer1, Integer * integer2, Integer *output) {
par goto add(integer1, integer2, output, __exit);
goto code2();
}
CUDA への対応
- Gears OS は GPU での実行もサポートする
- GPU で性能を出すためには GPU に合わせた並列プログラミングが必要になる
- 今回は CUDA への実行のサポートをおこなった
- CUDA は GPU を Device、 CPU を Host として定義する
- CUDA は処理の最小の単位を thread とし、それをまとめた block を展開し Device 上で実行されるプログラム(Kernel)を実行する
- 今回 CUDAWorker、CUDAExecutor、 CUDABuffer を使用して CUDA に合わせた並列処理機構を提供する
CUDAWorker
- CUDA で実行する Task を受け取る Worker
- 初期化の際に CUDA ライブラリの初期化等を行う
CUDAExecutor
- CUDAExecutor は Executor Interface を実装した以下の Code Gear を持つ
- HostからDevice へのデータの送信(read)
- kernel の実行(exec)
- Device から Host へのデータの書き出し(write)
typedef struct Executor<Impl>{
union Data* Executor;
struct Context* task;
__code next(...);
// method
__code read(Impl* executor, struct Context* task, __code next(...));
__code exec(Impl* executor, struct Context* task, __code next(...));
__code write(Impl* executor, struct Context* task, __code next(...));
}
CUDABuffer
- Host、Device 間でデータのやり取りをする際、 Gears OS での Data Gear をDevice 用にマッピングする必要がある
- Device にデータ領域を確保するにはサイズの指定が必要
- Data Gear には Meta Data Gear でデータのサイズを持っている
- しかし、 Data Gear の要素の中に Data Gear へのポインタがあるとポインタ分でサイズ計算してしまうため、 GPU では参照できなくなってしまう
- CUDA Buffer ではそのマッピングを行う
- このマッピングは Task の stub Code Gear で行われる
CUDA での呼び出し
- Gears OS では Task で実行される Code Gear の stub Code Gear で CUDA による実行を切り替える
- stub Code Gear で CUDABuffer でのマッピング、 実行する kernel の読み込みを行う
- stub Code Gear は CUDA で実行する際は CUDAExecutor の Code Gear に継続する
Gears OS の評価
- 並列処理のタスクの例題として Twice と BitonicSort を実装し、 以下の環境で測定を行った
- CPU 環境
- Model : Dell PowerEdgeR630
- Memory : 768GB
- CPU : 2 x 18-Core Intel Xeon 2.30GHz
- GPU 環境
- GPU : GeForce GTX 1070
- Cores : 1920
- ClockSpeed : 1683MHz
- Memory Size : 8GB GDDR5
- Memory Bandwidth : 256GB/s
Twice
- Twice は与えられた整数配列を2倍にする例題である
- 並列実行の依存関係がなく、並列度が高い課題である
Twice の結果
- 要素数 2^27
- CPU での実行時は 2^27 を 2^6 個に分割して Task を生成する
- GPU での実行時は1次元の block 数を 2^15、 block 内の thread 数を 2^10 で展開
- 1 CPU と 32 CPU では 約27.1倍の速度向上が見られた
- GPU 実行は kernel のみの実行時間は32 CPU に比べて約7.2倍の速度向上、通信時間を含めると 16 CPU より遅い
- 通信時間がオーバーヘッドになっている
Processor |
Time(ms) |
1 CPU |
1181.215 |
2 CPUs |
627.914 |
4 CPUs |
324.059 |
8 CPUs |
159.932 |
16 CPUs |
85.518 |
32 CPUs |
43.496 |
GPU |
127.018 |
GPU(kernel only) |
6.018 |
BitonicSort
- 並列処理向けのソートアルゴリズム
- 決まった2点間の要素の入れ替えをステージ毎に並列に実行し、 Output Data Gear として書き出し、次のステージの Code Gear の Input Data Gear とする
BitonicSort の結果
- 要素数 2^24
- CPU での実行時は 2^24 を 2^6 個に分割して Task を生成する
- GPU での実行時は1次元の block 数を 2^14、 block 内の thread 数を 2^10 で展開
- 1 CPU と 32 CPU で約22.12倍の速度向上
- GPU は通信時間を含めると 8 CPU の約1.16倍、 kernel のみの実行では 32 CPU の約11.48倍になった
- 現在の Gears OS の CUDA 実装では Output Data Gear を書き出す際に一度 GPU から CPU へ kernel の結果の書き出しを行っているため、差がでてしまった
Processor |
Time(s) |
1 CPU |
41.416 |
2 CPUs |
23.340 |
4 CPUs |
11.952 |
8 CPUs |
6.320 |
16 CPUs |
3.336 |
32 CPUs |
1.872 |
GPU |
5.420 |
GPU(kernel only) |
0.163 |
OpenMP との比較
- OpenMP は C、 C++ のプログラムにアノテーションを付けることで並列化を行う
- データの待ち合わせ処理はバリア等のアノテーションで記述する
- Gears OS は並列処理を par goto 構文、 データの待ち合わせを Code Gear と Input/Ouput Data Gear の関係で行う
#pragma omp parallel for
for(int i = 0; i < length; i++) {
a[i] = a[i] * 2;
}
OpenMP との比較
- OpenMP で Twice を実装し、速度比較を行った
- OpenMP は 1CPU と 32CPU で約10.8倍の速度向上が見られた
- 一方 Gears OS では約27.1倍と台数効果は高くなっている
- しかし、 Gears OS は 1CPU の実行速度が OpenMP に比べて大幅に遅くなっている
Go 言語との比較
- Go 言語は並列実行を go funciton(argv) の構文で行う。 この実行を goroutine と呼ぶ
- データの待ち合わせはチャネルというデータ構造で行う
- チャネルでのデータの送受信は <- を使用して行うため、簡潔に書くことが出来る
- しかし、 チャネルは複数の goroutine で共有されるため、データの送信元が推測しづらい
- Gears OS では goroutine は par goto 文とほぼ同等に扱える
- par goto 文では書き出す Data Gear を指定するため、書き出し元が推測しやすい
c := make(chan []int)
for i :=0; i < *split; i++ {
// call goroutine
go twice(list, prefix, i, c);
}
func twice(list []int, prefix int, index int, c chan []int) {
for i := 0; i < prefix; i++ {
list[prefix*index+i] = list[prefix*index+i] * 2;
}
c <- list
}
Go 言語との比較
- Go 言語でも OpenMP と同様に Twice を実装し、速度比較を行った
- Go 言語は 1CPU と 32CPU で約4.33倍の速度向上が見られた
- OpenMP と同様に台数効果自体は Gears OS が高いが、 1CPU での実行時間は Go 言語が大幅に速い
まとめ
- Gears OS の並列処理機構の実装を行った
- Interface を導入することで、見通しの良し Gears OS のプログラミングが可能となった
- par goto 構文を導入することで、ノーマルレベルで並列処理の記述が可能になった
- 2つの例題である程度の台数効果が出ることを確認した
今後の課題
- Gears OS の並列処理の信頼性の保証、チューニングを行う
- Gears OS では検証とモデル検査をメタレベルで実現することで信頼性を保証する
- 証明は CbC のプログラムを証明支援系の Agda に対応して行う。 並列処理の信頼性を保証するには SynchronizedQueue の証明を行う必要がある
- モデル検査は CbC で記述された モデル検査器である akasha を使用して行う。 モデル検査の方針としては Code Gear の並列実行を擬似並列で実行し、全ての組合せを列挙する方法で行う
- 現在の CUDA 実装では CPU、GPU 間のデータの通信コストがかかってしまうことが例題からわかった
- Meta Data Gear に Data Gear が CPU、 GPU のどこで所持されているのかを持たせ、 GPU の Data Gear が CPU で必要になったときに始めてデータの通信を行う
- OpenMP、 Goとの比較から、 Gears OS が 1CPU での動作が遅いということがわかった。
- par goto 文を使用する度に Context を生成するため、 ある程度の時間がかかってしまう
- モデル検査で par goto の Code Gear のフローを解析し、処理がかる場合は Context を生成せずに関数呼出しを行う等の最適化が必要