« PS3用Fedora 7 kernelの再構築 | トップページ | SPEプログラムのビルド方法 »

Cellのmailbox機能

行列計算のプログラムを作成した際に、SPUプログラムを繰り返し呼び出す場合、spe_context_run()関数の呼び出しは効率が悪いということを書きました。そのため、今回はCellのMailbox機能を使用してPPU - SPU間の同期処理を実験してみました。

例えば、SPUにデーターの変換処理を依頼する場合、PPUのメモリー上に変換前のデーターを準備し、spe_context_run()関数の引数としてデーターの開始番地をSPUに通知するのが一般的です。処理すべきデーターがひとかたまりの場合、SPUをいったん起動した後、PPUはSPUの処理完了(SPUプログラムの終了)を待つだけとなるためspe_context_run()関数のオーバーヘッドは問題になりません。

一方以下のケースでは、PPU-SPU間で同期処理が必要になります:

  • データーがPPUのメモリーに入りきらないため、データーのかたまり単位に、繰り返しSPUに処理を依頼する必要がある → 画像変換など
  • 入力データーが変化する毎に、繰り返しSPUに処理を依頼する必要がある → ゲームの座標変換など?

このケースで毎回spe_context_run()関数呼び出すとオーバーヘッドが大きくなります。上記のように、繰り返しSPUに処理を依頼する場合の高速化が今回のテーマです。

これまでの処理

SPUに処理を依頼する毎に、spe_context_run()関数を使用してSPU側のプログラム自体を新規に起動していました。例えば、以下の処理イメージとなります。

  for (i = 0; i < 50000; i++) {  //5万回SPUプログラムを起動する
    // entryはspe_context_runで書き換わるため毎回設定要
    entry = SPE_DEFAULT_ENTRY;
    spe_context_run(spe_ctx1, &entry, 0, NULL, NULL, &stop_info);
  }

Mailbox機能の使用

Cellでは、PPU-SPU間で簡単なメッセージのやりとりを行うことで、PPU-SPU間の同期を行うためのmailbox機能があります。今回の例では、spe_context_run()はSPUプログラムの開始時に一回だけ呼び出し、以後SPUに処理を依頼する際にはmailbox機能を使用しました。処理の概要は以下となります:

  1. PPU --> SPU : spe_context_run()によるSPUプログラムの起動
  2. PPU <-- SPU : SPUのbusy状態を通知(spu_write_out_mboxを使用)
  3. PPU <-- SPU : SPUの処理終了後idle状態を通知(spu_write_out_mbox)
  4. PPU --> SPU : SPUに次の処理を依頼(spe_in_mbox_writeを使用)
  5. SPUプログラムは2の状態に戻る

2,3,4ではmailboxメッセージを送信する側の処理を記載していますが、受信側の処理も同様に必要となります。以下に送信受信の処理について記載します。

SPU->PPUへの状態の通知

マクロ・関数名にout_mboxという名前がついている場合、SPU->PPUへの送信 mbox(SPU outbound: SPUから見て送信)を意味します。

SPU(送信)側では、spu_mfcio.hに定義されている以下のマクロを使用します。

spu_write_out_mbox (uint32_t data)
→ spu outbound mobxの書き込み(即ち送信)を意味します
  dataにPPUに通知するSPUの状態(Busy/Idle)を設定します。

PPU(受信)側では、libspe2.hに定義されている以下のライブラリー関数を使用しました。ライブラリー関数を使用する以外に、Cellのmailbox関連レジスター(MMIO)を直接読み出すさらに高速な方法もあります。

spe_out_mbox_status(spe_context_ptr_t spe)
→spuが送信したmboxキューの受信待ち状態を確認
 spe: メッセージを読み取るSPE context

spe_out_mbox_read (spe_context_ptr_t spe, unsigned int *mbox_data,
int count)
→spuが送信したmboxメッセージの読み取り
 spe: メッセージを読み取るSPE context
 mbox_data: メッセージを格納する変数
 count: 一回の関数コールで読み出すメッセージ数

PPU->SPUへの処理依頼

マクロ・関数名にin_mboxという名前がついている場合、PPU->SPUへの送信 mbox(SPU inbound: SPUから見て受信)を意味します。

PPU(送信)側では、libspe2.hに定義されている以下のライブラリー関数を使用しました。

spe_in_mbox_write (spe_context_ptr_t spe, unsigned int *mbox_data,
int count, unsigned int behavior)
→spu inbound mobxの書き込み(即ち送信)を意味します
 spe: メッセージを送信するSPE context
 mbox_data: 送信メッセージを格納する変数
 count: 一回の関数コールで読み出すメッセージ数
 behavior: mbox書き込み処理完了まで待ち合わせを行うかの指定

SPU(受信)側では、spu_mfcio.hに定義されている以下のマクロを使用します。

spu_stat_in_mbox
→SPUの受信mboxエントリー数を返します

spu_read_in_mbox
→受信したmboxメッセージを返します。受信キューが空の場合、メッセージを受信するまでSPUが停止します。そのため、事前にspu_stat_in_mboxマクロにて受信メッセージがあることを確認します

プログラムの解説

プログラムの全体はこちらです:「spe_mbox.zip」をダウンロード

PPU側のプログラム

mainの以下の部分でSPUログラムを実行するためのスレッドを生成

    arg1.spe_ctx = spe_ctx1;
    ret = pthread_create(&thread1, NULL, run_spe_thread, &arg1);
    pthread_join(thread1, NULL);

run_spe_thread関数でspe_context_run()を呼び出しています。spe_context_run()を呼び出すと、SPUプログラムが終了するまで呼び出し側の処理は停止するため、以下のように子スレッドを生成して、子スレッド内でmboxのハンドリングを行いました。

    // SPUとの同期を制御する子threadを起動
    pthread_t thread2;
    ret = pthread_create(&thread2, NULL, rum_mobx_thread, arg);

    // SPUプログラムを起動
    entry = SPE_DEFAULT_ENTRY;
    ret = spe_context_run(arg->spe_ctx, &entry, 0, NULL, NULL, &stop_info);

run_mobx_thread関数でSPUとのメッセージハンドリングを実施。mboxを使用して5万回SPUプログラムを起動します。

void *run_mobx_thread(void *thread_arg)
{
    thread_arg_t *arg = (thread_arg_t *) thread_arg;
    int i;

    for (i = 0; i < 50000; i++) {
        // SPUのアイドル待ち
        wait_spu_idle(arg->spe_ctx);

        // SPUの再起動
        send_spe_cmd(arg->spe_ctx, SPU_RUN_CMD);
    }

    // SPUの停止
    wait_spu_idle(arg->spe_ctx);
    send_spe_cmd(arg->spe_ctx, SPU_STOP_CMD);
    return 0;   
}

inline void wait_spu_idle(spe_context_ptr_t spe_ctx){
    int ret;
    unsigned int mbox_data;
   
    do {
        do {
        } while (!spe_out_mbox_status(spe_ctx));

        ret = spe_out_mbox_read(spe_ctx, &mbox_data, 1);
        if (ret < 0) {
            perror("SPE Mbox read error");
            exit(1);       
        }
    } while (mbox_data != SPU_IDLE);
   
    return;
}

inline void send_spe_cmd(spe_context_ptr_t spe_ctx, unsigned int cmd) {
    int ret;
    unsigned int mbox_data;

    mbox_data = cmd;
    ret = spe_in_mbox_write(spe_ctx, &mbox_data, 1, SPE_MBOX_ALL_BLOCKING);
    if (ret < 0) {
        perror("SPE Mbox write error");
        exit(1);       
    }
    return;
}

SPU側のプログラム

int main(unsigned long long spe, unsigned long long argp)
{
    unsigned int spu_state;
    unsigned int ppu_cmd;
    printf("SPU started\n");

    do {
        spu_state= SPU_BUSY;
        spu_write_out_mbox(spu_state);
        // SPUの処理 →今回はやることなし

        spu_state=SPU_IDLE;
        spu_write_out_mbox(spu_state);

        // PPUからの起動待ち
        do {
        } while (!spu_stat_in_mbox());

        // PPUからのコマンドがSTOP以外の場合は処理を先頭に戻って処理を繰り返す
        ppu_cmd = spu_read_in_mbox();
    } while (ppu_cmd == SPU_RUN_CMD);

    printf("SPU stopped\n");
    return 0;
}

性能測定

プログラムの実行時間を以下に示します:

  • spe_context_runを5万回呼び出すプログラム: 6.9s
  • 今回のプログラム(mailbox使用): 0.58s

10倍以上性能が向上したことが分かります。

« PS3用Fedora 7 kernelの再構築 | トップページ | SPEプログラムのビルド方法 »

Cellプログラミング」カテゴリの記事

コメント

始めまして、私も最近になってCellプログラミングを始めたばかりで、今のところCellシミュレーターで少しいじっているところです。
さてspe_context_run( )が遅いと言うことですが、本当でしょうか?繰り返しの処理をさせるためには、1)SPUプログラムでspu_stop( )でPPUに制御を戻す、2)PPU側ではspe_context_run( )の戻り値を確認して完全に処理が終わったのか、spu_stop( )で自主的に停止しているのか確認し、必要なら再度spe_context_run( )でSPUを起動する、という手順になるのだと思います。こちらでは、まだPS3が動かせないので、もしよろしければ試してみてください。

satow さんコメントありがとうございました。
以下のコードで実行時間を測定しました。

## PPE側(抜粋) ##
for (i = 0; i < 50000; i++) {
  entry = SPE_DEFAULT_ENTRY; // entryはspe_context_runで書き換わるため毎回設定要
  ret = spe_context_run(spe_ctx1, &entry, 0, NULL, NULL, &stop_info);
    if (ret < 0) {
     perror("spe_context_run");
     exit(1);
    }
}

# SPE側 #
int i;
int main(void)
{
  while (1) {
// printf("i = %d\n", i++);
   spu_stop(1);
  }

  return 0;
}

やはり7.9s程度の実行時間でした。

SPEに計算処理を行わせる場合、以下の点を加味する必要があると思います。

(1)PPEでは、データーをできるだけ大きな単位でSPEに割り当て、spe_context_runで先頭アドレスを通知する
(2)計算データーをメインメモリーからDMAでSPEに転送する際は、転送開始アドレスを毎回PPEから教えてもらうのではなく、SPE内で計算する
(3)例えば行列の足し算では、SPE0は偶数行、SPE1は奇数行を計算するようにしてSPEを一度起動したら全データーの計算が終わるまでSPEプログラムを終了しない
(4)PPEとの連携が必要な場合、より高速なmboxやmutexを使う

私が以前作成したmatrix_addのプログラムは、上記の考え方に従ったのですが(16KBのDMA毎ににPPEへ処理を戻すことはしていなかったのですが)、同じ計算を1000回ループさせる際に、ループ毎にSPEプログラムをpthread_create + spe_context_runで起動したため、その分のオーバーヘッドが大きくなってしまいました。

試して戴いてありがとうございます。spe_context_run( )はやはり遅いのですね。ついでといっては何ですが、もうひとつ。PPU側のスレッドでentry を毎回書き換えないとしたらどうでしょうか?entry にはSPU側の停止したアドレスが入っており、再初期化しなければ、spu_stop( )直後から再開されると思うのですが。
追伸、先日、YLD6.0をダウンロードしてみました。インストールできたら、またお知らせします。

satowさん、
ご指摘の、entry point変更を行ったら劇的に早くなりました。spe_context_runを使用しても中断点から再開する方法は有効ですね。勉強になりました。

以下に示す修正版のコードは、0.7sで実行できました。

# PPU側 #
// デフォルトのentry point(SPEプログラムの開始位置)を指定
entry = SPE_DEFAULT_ENTRY;

for (i = 0; i < 50000; i++) {
 //2回目以降のrunでは前回中断したentry pointから再開
 ret = spe_context_run(spe_ctx1, &entry, 0, NULL, NULL, &stop_info);
  if (ret < 0) {
   perror("spe_context_run");
   exit(1);
  }
}

# SPE側 #
int main(void)
{
int i = 0;

 while (1) {
// printf("i = %d\n", i++);
  spu_stop(1);
 }

return 0;
}

printf文のコメントを外すと、iがイクリメントして表示されることから、中断点から再開している(iを毎回初期化しない)ことが分かります。

おお、素晴らしい!。これでmailBox使わずにすみそうです(面倒くさがり屋な者で)。
ついでにもう一つ。spe_context_run( )は2つのパラメータを渡せますが、私はこれをiniParamとrunTimeParam(初期化パラメータ=SPUのループの外で一回だけ使用するパラメータ。実行パラメータ=ループに中でDMA転送元アドレスなど毎回変るパラメータ)と使い分けています。

第2引、第3引数を使い分けるのですね。なるほどです。
以前作成した行列和(matrix_add)のプログラムを今回いただいたコメントを参考にしてチューニングしてみたいと思います。

最近仕事で使うツールをVisual Basicで作っており、Cellの方がお留守になっておりました。Visual C++ & MFCはクラスライブラリーやフレームワークの使い方が難解で挫折したのですが、.NET Framework + Visual Basicは分かりやすく、GUIを使用したプログラムが簡潔に作成できるのに感激して、最近はこちらで遊んでおりました。個人的には、VBよりCの方がソースが読みやすいので、C#で書き直そうかなどと考えており、もう少し時間がかかりそうです。

satowさんもPS3 Linuxの開発環境を構築されたようですので、今後もコメントありましたら書き込みお願いいたします。

この記事へのコメントは終了しました。

« PS3用Fedora 7 kernelの再構築 | トップページ | SPEプログラムのビルド方法 »

2018年10月
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      
無料ブログはココログ