« 2007年6月 | トップページ | 2007年9月 »

2007年7月の記事

SPEプログラムのビルド方法

前回のCell Mailboxに関する投稿で使用したサンプルプログラムから、SPE(SPU)プログラムのビルド方法を変更しています。今回はこの点について記載します。

これまでの方法

SPE(SPU)プログラムを独立した実行ファイルとしてビルドし、PPE(PPU)プログラムから、1)SPEイメージのオープン、2)SPE Contextの生成、3)SPEへのプログラムロード、4)SPE Contextの起動、5)SPE Contextの廃棄、6)SPEイメージのクローズ、を実行していました。

サンプルコード的には以下となります。

int main(int argc, char **argv)
{
    int ret;

    spe_context_ptr_t spe_ctx;
    spe_program_handle_t *spe_prog;
    unsigned int entry;
    spe_stop_info_t stop_info;

// 1)SPEイメージのロード
// spe_image_open関数の戻り値を、spe_program_handle_tへのポインターに
// 代入する
    spe_prog = spe_image_open("hello_spe");
    if (!spe_prog) {
        perror("spe_image_open");
        exit(1);
    }

// 2)SPE Contextの生成
    spe_ctx = spe_context_create(0, NULL);
    if (!spe_ctx) {
        perror("spe_context_create");
        exit(1);
    }

// 3)SPEへのプログラムロード
    ret = spe_program_load(spe_ctx, spe_prog);
    if (ret) {
        perror("spe_program_load");
        exit(1);
    }

    entry = SPE_DEFAULT_ENTRY;
// 4)SPE Contextの起動
// ただし、SPEが停止するまで呼び出し元をブロックするため
// 別threadを生成しその中でspe_context_run()を呼び出すのが一般的
    ret = spe_context_run(spe_ctx, &entry, 0, NULL, NULL, &stop_info);
    if (ret < 0) {
        perror("spe_context_run");
        exit(1);
    }

// 5)SPE Contextの廃棄
    ret = spe_context_destroy(spe_ctx);
    if (ret) {
        perror("spe_context_destroy");
        exit(1);
    }

// 6)SPEイメージのクローズ
    ret = spe_image_close(spe_prog);
    if (ret) {
        perror("spe_image_close");
        exit(1);
    }

    return 0;
}

新しい方法 ー Embeded SPE Object

SPEプログラムを、Cell Embedded SPE Objectというフォーマットでビルドし、PPEプログラム中に埋め込む(embedする)ことができます。こうすることで、これまでの方法で必要とされた1)6)(SPEイメージファイルのopen/close)が不要となります。

また、ファイルが1つになるため、PCのクロス開発環境でビルドしたプログラムをPS3に転送する手間(大した手間ではありませんが、、)や、Eclipse IDEでデバッグを行う際に必要となるCellのシミュレーターにSPEプログラムを転送する設定が割愛できます(こちらは結構重宝します)。

PPEプログラムにEmbedded SPE Objectを埋め込むためには、ppu-embedspuというツールを使用するか、Eclipse IDEで以下の設定を行います。
 PPEプログラムのProperty→  C/C++ Build→ PPU GNU 32bit Embed SPU
   → Input → 埋め込むSPEプログラムを指定

また、プログラムコード的には埋め込んだSPEプログラムとのリンクを取るために以下のコーディングが必要です。

// SPEプログラムのシンボル名を宣言
// 通常はSPEプログラムのオブジェクト名 = シンボル名となる
extern spe_program_handle_t hello_spe;

int main(int argc, char **argv)
{
    int ret;

    spe_context_ptr_t spe_ctx;
    unsigned int entry;
    spe_stop_info_t stop_info;

    printf("Hello, SPE!\n");

// a)SPE Contextの生成
   spe_ctx = spe_context_create(0, NULL);
    if (!spe_ctx) {
        perror("spe_context_create");
        exit(1);
    }

// b)SPEへのプログラムロード
// リンクしたSPEプログラムのシンボル名を指定する
   ret = spe_program_load(spe_ctx, &hello_spe);
    if (ret) {
        perror("spe_program_load");
        exit(1);
    }

    entry = SPE_DEFAULT_ENTRY;
// c)SPE Contextの起動
    ret = spe_context_run(spe_ctx, &entry, 0, NULL, NULL, &stop_info);
    if (ret < 0) {
        perror("spe_context_run");
        exit(1);
    }

// d)SPE Contextの廃棄
    ret = spe_context_destroy(spe_ctx);
    if (ret) {
        perror("spe_context_destroy");
        exit(1);
    }

    return 0;
}

余談 ー SPE vs SPU、PPE vs PPU

Cellのドキュメントを見ていると、SPEという用語とSPUという用語が出てきます。同様にPPEとPPUも出てきます。「PlayStation3 Linux完全攻略ガイド」によると、SPE/PPEは単一の処理エレメントを示す場合に使用し、SPU/PPUは複数の処理エレメントの総称であるとされていますが、用語の使い分けがうまくいきません。このブログの中でもSPE/SPUの使い分けがうまくできず、用語的に混乱している部分があります。

2007/7/20 追記
Cell SDKに添付のドキュメント「Cell Broadband Engine Architecture」をチェックしたところ、PPE/SPEは以下の定義となっていました。

  • SPEs, which are the combination of an SPU, a local storage area, an MFC, and an RMT → 即ち、SPU(演算コア)・LS・MFC・RMTから構成されるプロセッサ全体をSPEと呼ぶ
  • The PPEs are 64-bit PowerPC processor units (PPUs) with associated caches that conform to PowerPC Architecture → こちらも、PPU(演算コアとL1キャッシュ)・L2キャッシュから構成されるプロセッサ全体をPPEと呼ぶことになります

すなわち、SPEとSPUの関係は、SPEがSPUを包含する関係であり、「PlayStation3 Linux完全攻略ガイド」の説明は誤りということになります。

もっと言ってしまうと、ライブラリやツールでも以下の用語が見受けられます。

  • libsep2では、spe_context_createなどのように、関数名がspeで始まる
  • spu_mfcio.hでは、spu_write_out_mboxのように、マクロ名がspuで始まる
  • コンパイラなどのツール名は、ppu-gcc, spu-gccのようにppu/spuで始まる

上記も「Cell Broadband Engine Architecture」ドキュメントの定義に従うと、以下の解釈が成り立つように思えます;

  • libspeは、Synergistic Processor全体を制御するためspeで始まる
  • mailboxは、PPEとSPUがMFCを介して通信するため主体がspuである
    詳細は、
    「Developing Code For Cell - Mailboxes」のP7を参照下さい
  • コンパイラは演算コア用のコードを生成するためPPU/SPU?

PPE/SPEの用語の方が示す範囲が広いのでこちらを使っておけば無難なように思えますが、あえてPPU/SPUの用語を使用するべきケースがまだよく分かりません。なんでこんなややこしい名前にしたんだろう、、

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の再構築

PS3 LinuxをFedora 7に更新して問題なく使えているのですが、shutdown/rebootが途中で停止してしまう既知問題をなんとかしたいと思っています。Yumで一度カーネルの更新を行ったのですが問題は解消しませんでした。

LinuxHQで最新のソースを調べると、PS3用のvideoドライバであるps3fb.cにkernel-2-6-21-git5で修正が行われていることが分かりました。ひょっとしてこの修正パッチを当てると問題が解決しないかと期待したのですが、Fedora 7のkernel source rpmを展開したところ、このパッチは既に取り込み済でした。

そのため、単に最新のパッチをあててカーネルをビルドし直せば問題解決という目論見は崩れ去ったのですが、PS3用のカーネル再構築にトライすることにします。

以下にカーネル再構築の手順を示します。カーネルの再構築を行うような方はそれなりの経験者だと思いますが、ひとつ間違うと、二度とシステムが起動しなくなる場合もありますので、カーネルの再構築を行う場合は十分ご注意ください。

カーネルソースの入手

最初は、最新のカーネルをベースに再構築を行うのがよかろうと思い、kernel-2.6.21-5のソースをLinux Kernel Archivesからダウンロードしたのですが、このソースツリーではconfigをどういじってもkernelをブートすることができませんでした。まったくkernelが起動する気配がないので、ディスクドライバ周りに問題ありかと思い色々と調べてみると、2.6.21-5のソースツリーには、ps3_strage.cというHDDのドライバが入っていないことが分かりました。そのほかにも、snd_ps3.c、gelic_net.cなどPS3に必要なソースコードが欠けているようです。

kernel-2.6.21からPS3のコードが統合されたと思っていたのですが、まだ部分的な統合にとどまるようで、Fedora 7のカーネルは不足分を独自に取り込んでビルドされていることが分かりました。

そのため、PS3用カーネルの再構築のためにはFedora 7のsrc.rpmが必須になります。

src.rpmの入手と展開

src.rpmの展開方法はこちらのサイトを参考にしました。

1. 最新版のsrc.rpmをダウンロードします
    # yumdownloader --source kernel
    kernel-2.6.21-1.3228.fc7. 100% |=========================|  45 MB

2. rpmファイルの展開
    # rpm -Uvh kernel-2.6.21-1.3228.fc7.src.rpm
       以下の警告が出ますが、気にせず進めます
  警告: グループ kojibuilder は存在しません - root を使用します

3. カーネルソースの展開
    rpmbuildを実行するために以下のパッケージをインストール。
  # yum install sparse

    # cd /usr/src/redhat/SPECS/
    # rpmbuild -bp --target $(uname -m) kernel-2.6.spec
    ビルド対象プラットフォーム: ppc64
    ターゲット ppc64 用にビルド中
         (以下略)
/usr/src/redhat/BUILD/kernel-2.6.21/linux-2.6.21.ppc64 にソースが展開されます。/usr/src/redhat/SOURCESに展開されたパッチファイルを見ると、PS3関連で21本のパッチを当てていることが分かります。ディストリビューションとしてまとめる際は、単純にソースツリーを持ってくるだけではなく色々と手を入れているのですね。

4. ソースディレクトリにシンボリックリンクを設定
    # ln -s /usr/src/redhat/BUILD/kernel-2.6.21/linux-2.6.21.ppc64 /usr/src/linux
これからのmake等は、/usr/src/linux をホームに作業を行います。

Makeの準備

1. /usr/src/linux/Makefile に以下の定義を追加
    EXTRAVERSION = -1.3228.ps3
    CROSS_COMPILE   ?= ppu-
     → cell SDKのppu-gccでコンパイルします(多少最適化する?)

2. configファイルの設定
   # cd /usr/src/linux
 # cp /boot/config-2.6.21-1.3228.fc7  .config
 # make menuconfig →  リモートPCから行っているためmenuconfigです

/bootからコピーしたFedora 7のconfig設定が読み込まれた状態になります。オリジナルのconfigはどんなマシン構成にも適合するように、あらゆるドライバーがModuleとして指定されているため、PS3では必要がないモジュールの選択を外してカーネルサイズのダイエットとコンパイル時間の短縮化を図ります。

/usr/src/linux/arch/powerpc/configs/ps3_defconfig がPS3のための最小レベルのconfigになりますが、このconfigではカーネルを起動できませんでした。冒頭に示したps3_strage等の設定が抜けているため追加したのですが、まだ不足があるようです。そのため、Fedora 7のconfigをベースに不要な設定を抜いていきました。

今回のビルドで使用したcofigはこちらです。

Makeとカーネルイメージのコピー

1. Make
   # make
   # make modules_install

2. カーネルイメージのコピー
   # cp -p arch/powerpc/boot/zImage.ps3 /boot/vmlinuz-2.6.21-1.3228.ps3
   # cp -p System.map /boot/System.map-2.6.21-1.3228.ps3
   # mkinitrd -v /boot/initrd-2.6.21-1.3228.ps3.img 2.6.21-1.3228.ps3

3. yaboot.confの編集
/boot/etc/yaboot.confに以下の行(青字の部分)を追加します。

default=2.6.21-1.3228.fc7
image=/vmlinuz-2.6.21-1.3228.fc7 → 起動実績がある旧カーネル
        label=2.6.21-1.3228.fc7
        read-only
        initrd=/initrd-2.6.21-1.3228.fc7.img
        root=/dev/VolGroup00/LogVol00
        append="video=720p"
image=/vmlinuz-2.6.21-1.3228.ps3 → 新規にビルドしたカーネル
        label=2.6.21-1.3228.ps3
        read-only
        initrd=/initrd-2.6.21-1.3228.ps3.img
        root=/dev/VolGroup00/LogVol00
        append="video=720p"

上記のように、必ず起動実績のあるカーネルイメージのエントリを残しして、いざというときは旧イメージで起動できるようにします。

PS3 Linuxのカーネル

このwebページにあるように、PS3のハードウェアはLinuxに対してHypervisorで仮想化されています。HDD/BDはATA接続ですが、HyprevisorとLinuxのドライバ(ps3_strage)を介して、Linux上ではSCSIディスク(/dev/sda)して見えています。そのため、カーネルのconfigでATA関連のドライバを全て無効にしても問題なく動作します。

ネットワークは、gelic_net 、gelic_wireless というPS3専用のドライバが提供されているため、その他のドライバ(intel/realtekなど)は全て無効にできます。

オリジナルFedora 7カーネルと比較して

不要なドライバーの削除をできる限り行ったところ、カーネルのサイズは以下に示すようにかなり小さくなりました。
 7567248  vmlinuz-2.6.21-1.3228.fc7 → オリジナル
 5764512  vmlinuz-2.6.21-1.3228.ps3 → 今回ビルド

しかしながら、ドライバーモジュールは以下のように、サイズが大きくなってしまいました。
   90392   ps3_storage.ko  → オリジナル
 397083 ps3_storage.ko  → 今回ビルド

Fedora7オリジナルのドライバーイメージは圧縮を行っているように思えるのですが、モジュールコンパイル時に圧縮を行うオプションが分からず、ドライバーのサイズを小さくする方法は不明です。ドライバーの数は大幅に削りましたが、合計のディスク占有量では今回ビルド分の方が大きくなってしまいました。

カーネルのサイズ自体は今回ビルドしたカーネルの方が小さいのですが、起動時(runlevel=3で起動)のメモリー量(freeコマンドの結果)を比べるとオリジナルカーネルの方が小さくなりました。
  オリジナルカーネル起動時の空きメモリー      : 146,644
  今回ビルドしたカーネル起動時の空きメモリー: 130,640

オリジナルカーネルは無駄なドライバーが多くメモリー効率が悪いと思っていたのですが意外な結果です。これだと、無理にカーネルを再構築するメリットはないと思います。

ps3fb_shudown問題のパッチ

冒頭の問題に関連しそうなパッチがLinuxppc-devのM/Lアーカイブにありましたが、Fedora 7のソースにはうまくパッチがあたりませんでした。そのため、shutdown時の問題を解消するためのパッチは断念しました。

ということで今回は、Fedora(ディストリビューション)添付のカーネルは結構優れモノであることがよく分かったというのが成果(?)でした。

« 2007年6月 | トップページ | 2007年9月 »

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      
無料ブログはココログ