レベル: 中級 Peter Seebach, Freelance author, Plethora.net
2008年 3月 04日 SDK3.0では、IDLの代わりにData Communication and Synchronization library (DaCS) が提供されることになりました。DaCS はヘテロジーニアスかつ多階層を持つシステムにおいて、アプリケーション開発を助けてくれるツール群とアプリケーションフレームワークからなります。この記事では皆さんをDaCSプロセスモデルのツアーにご招待します。通信やメモリアクセスを含む、DaCSの基本原理を探検していきましょう。
イントロダクション
前回までの本シリーズの記事では、 Cell Broadband Engine SDK version 2.1までに収録されているIDL toolについて見てきました。しかし、version 3.0 では、IDL ツールはもはや提供されていません。その代わりに、同じ役割をもつパッケージとしてData Communication and Synchronization library (DaCS) が加わっています。これは、Cell/B.E. (あるいはCell/B.E.に似た)プロセッサによる分散アプリケーション開発を容易にするために開発されたサービス群から成ります。SDKに収録されているバージョンのライブラリ群はCell/B.E.特有の部分がいくらかありますが、この環境で実行させるには最適といってよいでしょう。
DaCS は、PPEがタスクをSPEにアサインし、SPEが与えられたタスクを非同期に処理するような開発モデルを抽象化してくれます。DaCSライブラリはDaCS elements (DEs)の種類を定義していて、それらは次の2つにわけられます:
- Host elements (HEs)
- Accelerator elements (AEs)
Cell/B.E. 開発を行う際、これらをPPEの抽象化されたもの(host element) と SPEs が抽象化されたもの(accelerator elements)とみなすことができます。プロセッサを2つ持つ通常のCell/B.E.ブレードでは、16個のSPEチルドレンを持つ一つのCell/B.E.ブレードとみなすこともできれば、8個のSPEチルドレンを持つ2つのCBEデバイスを割り当てることもできます。後者の見方では、やや複雑にはなりますが、プロセッサ親和性(Processor Affinity)をよりよく制御できます。PlayStation3 システムの上で動いているCell/B.E. SDKの場合、6個のSPEチルドレンしか割り当てることはできず、より高いレベルのフレームワークを使用することはできません。
プロセスモデルの紹介
DaCSは一つのDE上での多数のプロセスの抽象化に対応していますが、SDKに収録されているものは一つのエレメントの上ではサポートされるプロセスは一つだけです。一つ一つのプロセスは通常のSPEプログラムと同じ手法で開始されて、DaCSライブラリのSPU側を初期化し、PPE側で動いているライブラリと通信できます。
DaCSを使った最も簡単なSPEプロセスのセットアップ方法は、libspeを使ったことがある方には不思議と馴染み深いものでしょう。 その理由は、DaCSライブラリが同様のEmbedded バイナリを使用するからです。SPEプログラムはspu-gcc でコンパイルされて、ppu-embedspuプログラムによりEmbedded オプジェクトに変換され、DaCSをつかってプログラムにリンクされるのです。セットアップはlibspeよりちょっと複雑ですが、その代わり自分自身でスレッドの管理をする必要がなくなります。DaCSがスレッドを透過的に管理してくれるんです。
Listing 1. SPE側のプログラム
extern spe_program_handle_t spu_prog;
de_id_t spes[16];
dacs_process_id_t pids[16];
uint32_t children;
int32_t status;
dacs_runtime_init(NULL, NULL);
dacs_get_num_avail_children(DACS_DE_SPE, &children);
dacs_reserve_children(DACS_DE_SPE, &children, spes);
dacs_de_start(spes[0], &spu_prog, NULL, NULL,
DACS_PROC_EMBEDDED, &pids[0]);
dacs_de_wait(spes[0], pids[0], &status);
dacs_runtime_exit();
|
このプログラムでは次のようなことを行っています:
- 利用可能なチルドレンの数を確認します(例では少なくとも一つのチルドレンが利用可能であることを想定)
- チルドレンの確保を行います
- 一番目のチルドレンのプログラムを開始します
- プログラムの終了を待ちます
- DaCSランタイムを終了します
難しいプログラムでは無いでしょう。エラーチェックをしていないのは、解説を目的としているからです。実際のコードでは、全てのオペレーションを注意深くチェックしたいと思うでしょうね。spu_prog ハンドルは、 このシリーズの前の記事で使ったembedded SPUコードと同じ類のものです。しかしながら、DaCS ライブラリでは、embedded ライブラリを作成する際に、-m32 オプションではなく、-m64 オプションを使う必要があります。
status変数にはSPEプログラムのmain 関数の返り値が入ります。&spu_prog の後の2つのNULL値はそれぞれ、argv と envpに渡されます。
さて、以上はプログラムで使うデータが全て引数などとして実行前に与えられるという最もシンプルなケースでした。しかしながら、DaCSの真価はチルドレンプログラムを起動した後でのデータの操作にあります。
一般的な原則を理解しよう
全てのDaCS APIにはいくつかの共通パターンが存在します。最も重要なパターンは以下の通りです:
- APIで呼ばれる処理には返り値があります
- APIで一貫したリソース開放をする必要があります
- 全APIコールに渡って、引数はアラインメントされている必要があります
DaCS API と返り値
Listing 1 の例ではエラーチェックを行っていないため、どのDaCS関数の返り値も使っていません。DaCSライブラリでは、全ての関数は単に成功か失敗のフラグだけを返します。関数によって生成されたデータは関数の引数によってポイントされるオブジェクトに格納されます。例えば、利用可能なチルドレンの数を得るためには、dacs_get_num_avail_children(DACS_DE_SPE, &children);関数を呼びます。childrenのアドレスが関数に渡され、関数はこのアドレスを通してオブジェクトを編集します。通常、返り値は、正常なオペレーションを意味する定数DACS_SUCCESSですが、エラーコードを返すこともあります。
この仕様により、エラーを返すための特別な管理値が必要になるような事態を防いでいますし、APIに一貫性を与えています。その一方、この仕様には時々混乱させられることもあり、あるオブジェクトのアドレスをある関数に通知しているのに、オブジェクト自身は他の関数に渡されているということがあります。この件については見なかったことにしましょう。とりあえず、コンパイラが言ってくる型の不一致についてのワーニングだけは無視しないようにすること!
リソースの割り当てと開放
全てのリソース割り当てAPIコールには対応するリソース解放APIコールがあります。たとえプログラムが終了寸前であってもこれらのコールはオプションではありません。実際、dacs_runtime_exit() 関数は、もしリソースがきちんと開放されていないとハングアップしてしまいます! ですから、もしあなたがdacs_remote_mem_create関数を使ってリモートメモリ領域を作ったとしたら、終了する際にはdacs_remote_mem_destroy関数を使ってリソースを開放する必要があります。もしあなたがdacs_remote_mem_accept関数を使ってメモリ領域へのアクセスを許可した場合は、終了する際にdacs_remote_mem_release関数を使って、リソースの開放しなければなりません。さもないと、dacs_remote_mem_destroy関数を呼んだとしても、 全てのクライアントが開放されるまでこの関数の終了はブロックされてしまうのです。
この仕様はなんだかとてもおせっかいな感じがしますが、実はそう悪くもないのです(ブロッキングしてしまうよりも、エラー情報を返す方だけの方が良かったとは思いますが)。 実際のコードは、リソースがすでに使われていないという場合には明確な区別がされていることが多いですから、このように状態を正しく把握することは簡単でしょう。
アラインメントの要求
ほとんど全ての変数がプロセッサーにとって最適なアライン、一般的には16バイト境界にアラインされる必要があります。ミスアラインされた変数はほとんどの場合、期待どおり動いてくれない上に、ミスアラインは見つけにくいものです。もし、奇妙なクラッシュをするようでしたら、アラインされていないアクセスを探してみてください。クラッシュしているDE上にあるとは限りませんよ! 一例をあげれば、PPEにあるミスアラインされたオブジェクトがSPEクラッシュを引き起こすこともあり得ます。(さらに悪いことに、PPE上のほかのオブジェクトのアラインの変更が、いま問題となっているオブジェクトのアラインに影響を与えることもあります)
gccの__attribute__ ((aligned (16))) はすっかりお馴染みですが、これはスタテック変数には必要なく、何の効果もありません(少なくとも、望ましい効果は何もありません)。これはつまり、あるケースではグローバルにオブジェクトを宣言するか、あるいはオブジェクトのためにメモリ確保を行う必要があるということです。残念ながら、アラインメントの要求はオブジェクトサイズと相容れません。例えば、 dacs_send 関数の引数として使うためにN次元のInteger配列を宣言することすらできません。なぜなら、もし配列の最初の要素が正しくアラインメントされたとしても、後に続く要素は正しくアラインメントされないからです!
ドキュメント類では、アラインメントの要求についてあまり詳しく説明されていません。実際上は、全てを16バイトアラインにしておけばよいでしょう。
通信とメモリアクセスに関して
SPEsはローカルストレージを持っていますが、PPEが使用しているメインメモリへの直接アクセスはできません。ほとんどの読者にはよくわかっていることでしょうが、ここで強調したいのは、ある程度複雑なCell/B.E.プログラミングでは、データ入れ替えのためにかなりの量のコードが必要となっているということです。IDLを使った場合、データは、特に明示的なオペレーション無しにライブラリによって自動的に送付されるだけでなく、Remote procedure call (RPC) インターフェースを使ってリモートシステムに送付するようにもできました。DaCSでは、データ管理はもう少し明示的に行う必要があります。
DaCS にはメッセージ送受信の機能と、要素間でメモリ領域を共有するための関数ファミリーがあります。一般的には、DaCSは完全なプロトコルとは違って、基本的なツールを提供します。オペレーションは非同期で、全体的な構造として、ある程度のやりとりを前提にしています。一般的には、メッセージを送信し、受信確認を待ちます。つまり、両方の側に協調する必要があります。もし、メッセージを送信して相手方の受信を待っている状態で、相手方が受信を行わなかったとしたら、送信側は永遠に待ち続けることになります。
多くの通信関数はバイトスワップ基本命令を備えています。これらはIntelベースやAMDベースとCell/B.E.ブレードサーバーが混ざった環境では間違いなく重要な機能ですが、純粋なCell/B.E.システムを効率よく使うためには、DACS_BYTE_SWAP_DISABLEを使ったほうがよいでしょう。
Wait識別子と転送の完了
Wait識別子は、それ自身が多くのことをするわけではありませんが、Wait識別子を理解せずにはどの通信システムも使うことはできません。DaCSで定義されているメッセージ送信関数やメモリー転送関数は非同期で発行されます。それぞれの関数で異なるテストやWaitプロトコルを要求するようなことが無いように、DaCS関数はWait識別子と呼ばれる共通の機能を使っています。一度アロケートされた(あるいは確保された)Wait識別子は、通信関数に引数として渡され、通信が完了したかどうかの問い合わせに使用されます。Wait識別子は、ローカルDEで初期化されたトランザクションに対してのみ使用することができる、ということに注意してください。つまり、どのWait識別子に対しても問い合わせをできるわけではありません。
Wait識別子のおかげで様々な関数のやりとりが標準化されています。あなたが呼んでいる関数がdacs_putであっても、dacs_recvであっても、Wait識別子を関数に渡しておけば、dacs_test関数を呼ぶことで、すでに処理が完了しているのか、あるいはdacs_waitが処理が完了するまでブロックしている状態なのか、 知ることができます。
リモート・ダイレクトメモリアクセス
大きなブロックのデータを取り扱う場合、断然お奨めなのは、リモートメモリアクセスです。最初は手順はやや複雑に見えますが、実際上は非常に便利です。特に、Cell/B.E. プロセッサーには全てのコアに共通の共有メモリマップはありません。SPEにはローカルストアーがありますが、通常はメインメモリにアクセスすることはできません。同様に、PPEからあるSPEのローカルストアーにアクセスすることは通常はできません。リモートメモリ領域にアクセスするための唯一の方法はDMA転送を通しておこなうことです。
DaCSでは、これらはmemory regionという概念で抽象化されます。memory regionは、実際にメモリを持っているDE上で生成され、他のDEによって分配されます。例えば、PPEがシステムメインメモリの一部を分配し、あるSPEによってacceptedされたとします。いったんメモリの一部がacceptされれば、その領域はdacs_put やdacs_get 関数(あるいは、多重転送をするためのリスト転送のようなもの)を使ってアクセスできるようになります。
この機能の典型的な使用方法としては、PPEにいくつかのバッファーを確保しておき、SPEに対してそれらを分配するというやり方です。SPEは必要に応じてこれらのバッファーからデータを取り出すことができます。一つ以上のSPEが同じ共有memory region にアクセスすることも可能です。もしあなたが、このような領域をたくさん管理するのが嫌だと思うならば、原理的には、一つの大きなmemory regionを作って、読み出しや書き込みを異なる部分で行うことも可能です。しかしながら多数の分割されたバッファーを作った方が、おそらく扱いやすいと思います。
メッセージパッシング
データ量がより小さい場合、DaCSにはメッセージパッシングという送受信システムがあります。メッセージは非同期で送信され、dacs_wait という対応するWait識別子がリモート側がメッセージを受信するまでブロックします。
ある特殊な場合には以下の注意が必要です。もし、受信側の特定のバッファーが全てのメッセージを受信するのに十分なサイズでなかったら、オペレーションは音も無く失敗します(将来のライブラリリリースでは、音も無く失敗するのではなくて、せめてエラーを発生させてほしいものです)。 このため、もっとも簡単なのは、使用する予定のバッファーよりも大きなサイズのデーターを送ってしまうことの無いようにすることでしょう。ある標準のメッセージサイズを決めてしまうのが良いかもしれませんね。
メッセージは、それがどんな種類のメッセージであるのかを区別するための32bitの値をもつストリームとして送信することもできます。dacs_recv呼び出しは、ある特定のストリームにあるメッセージだけしかチェックしないか、あるいは、待ち状態にある全てのメッセージを受け付けるために、魔法の値DACS_STREAM_ALL を特定することもできます。(ストリーム識別子は0から定義済みの定数DACS_STREAM_UBの間でなければいけません)
Mailboxも
DaCSライブラリはCell/B.E.システムにおけるMailbox機能の抽象化された関数を提供します。もし単純な32ビット値を送信する場合は、効率がかなり良くなっているでしょう。さらに、アラインメントの要求がいくらかゆるいので、32ビット値の配列から特定の数を抜き出して送る場合に、アラインメントの心配をせずにすみます。Mailboxは自動的にブロッキングオペレーションになってしまい、非同期で実行できないところが大きな弱点です。また、Mailboxはバイトスワップを行うこともできません。
読み出しや書き込みの処理がブロックしているかどうかを確認するには、dacs_mailbox_test を使うべきです。しかし、注意してください。ほとんどのオペレーションで、これはタスクの競争状態になりえます。もし、6つすべてのチャイルドDEが、ブロッキング無しに書き込みができるかどうかを同時に確認したとしたら、全てが可能と判断しすぐに書き込みをはじめてしまうでしょう。
結論
DaCS は驚くほど多様なツールを提供してくれます。それらはかなり特殊なものであるので、なにか有益なプロトコルの設計をはじめる前に、知るべきことがたくさんあります。メッセージパッシングやMailbox機能無しに、リモートメモリへのアクセス機能を実装するのは難しいです。もしWait識別子が無かったら、メールボックスを利用したデータの送り返しや強制的送信以外にはあまりできることはなくなってしまいます。
このシリーズの次の記事では、これらの様々なツールがどのように結合されていくのか、"The little broadband engine that could: Use multiple SPEs for a single task" で取り扱ったフラクタルプログラムのDaCS版を作ることで紹介します。
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | 
|  | Peter Seebach has always wanted to use the phrase "novel architecture" in a sentence. He has been programming recreationally long enough to remember that the phrase used to refer to the 68000. Besides, offloading work just sounds like a good concept to him. |
記事の評価
|