#はじめに
多数のFPGAボードをネットワークで接続して高性能な並列処理がしたい、という場合に問題になるのが、複数のFPGAを統括的に制御する方法です。
基本的に市販のFPGAボードを動かす場合、PCIeで接続されたサーバからローカルにFPGAを制御することになります。
そうなると、FPGAクラスタを組んで多数のFPGAに協調的な動作をさせたい場合、1台のサーバのPCIeスロットには限りがあるため、必然的に複数台のサーバを制御しなければなりません。
この記事では、複数台のホストサーバに接続された複数のFPGAを、MPIを使って一台のサーバから操作する方法の例を紹介します。
使用するx86サーバのOSはCentOS7.7、FPGAボードはIntel FPGA プログラマブル・アクセラレーション・カード(PAC) D5005です。
各サーバには2枚のPACがPCIeで接続され、全てのサーバは同じネットワークに接続しています。
FPGA側も別のネットワークで接続されていますが、FPGA間の直接通信を行わない場合、特に必要ありません。
必要となるのは、サーバとそれに接続されたFPGAボード、サーバ同士を接続しているネットワークです。
また、今回の例ではPAC D5005を使っていますが、MPIで動かすだけであれば他のFPGAボードでも問題ないはずです。
#MPIについて
MPIはMessage Passing Interfaceの略で、高性能の分散メモリ並列アプリケーションのための規格です。
複数のノード上で並列的に処理を行ったり、プロセス間でのデータ交換や集団通信等の機能があり、スーパーコンピュータ等の高性能な並列システムでよく利用されています。
MPI_Send/Recvなどプロセス間のデータ交換や集団通信のために用いられることが多いのですが、この記事ではマルチノードのFPGAクラスタ制御を主目的とします。
MPIにはOpen MPIやMPICHなどいくつかの利用できる実装がありますが、今回はMPICHを使っていきます。
#MPI環境構築の下準備
MPI環境のセットアップについては、以下のページが非常に参考になります。
MPIの環境構築や基本コマンドのまとめ
https://qiita.com/kkk627/items/49c9c35301465f6780fa
一応、筆者の環境での手順を以下に記載します。
まず、MPIで制御する全サーバーにおいて、パスフレーズ無しのRSA認証の設定と、firewall等のセキュリティ機能の停止が必要です。
また、全てのサーバに同じ名前のユーザとしてログインできるようにしておきます。
(以下の手順は必要ない場合は飛ばしてください)
全サーバに同名のユーザを追加します。
$ useradd ユーザ名
必要な場合は、全サーバにopenssh-serverをインストールします。
$ sudo yum -y install openssh openssh-server
また、並列処理の制御を行う1台のサーバ(マスターサーバ)にはopenssh-clientsが必要です。
$ sudo yum -y install openssh-clients
SSH公開鍵認証の設定は以下のページを参考にしてください。
SSH公開鍵認証で接続するまで
https://qiita.com/kazokmr/items/754169cfa996b24fcbf5
公開鍵認証が完了してパスワード無しで各サーバにアクセスできるようになったら、マスターサーバから全てのサーバに一度ずつsshでアクセスし、known_hostsに登録しておきます。
全サーバのfirewallを停止します。
$ sudo systemctl stop firewalld
$ sudo systemctl disable firewalld # 永続的に無効化
SELinux等も停止しておきましょう。
次にファイルシステムの設定を行い、各サーバから同じディレクトリ構造のファイルシステムを参照できるようにします。
長くなりますので、ファイルシステムの設定が必要な場合、以下のページを参考にNFSの設定を行ってください。
既にNFSやautofs等でストレージを共有している場合は必要ありません。
設定が完了したら、全てのサーバから同じファイルシステムが見れることを確認してください。
#MPICHのインストール
ここまでの作業でMPIを利用するための下準備が完了しました。
FPGAクラスタにおいて利用する全てのサーバに、MPICHを導入します。
MPIプログラムのコンパイルに必要なので、mpich_develも同時にインストールしておきます。
$ sudo yum -y install mpich mpich-devel
インストール完了後、.bash_profileに以下の行を追加し、/usr/lib64/mpich/bin/
にパスを通しておきます。
export PATH=$PATH:/usr/lib64/mpich/bin/
#MPIのテスト
MPICHのインストールが完了したら、まずは簡単なテストプログラムでMPIの動作を確認します。
筆者の環境に合わせて、以降はc++のコードの例を挙げていきます。
#include <stdio.h>
#include <mpi.h>
int main(int argc, char **argv){
int rank, proc;
int name_length = 10;
char name[name_length];
MPI_Init(&argc, &argv); // MPIの初期化
MPI_Comm_rank(MPI_COMM_WORLD, &rank); // rankの取得
MPI_Comm_size(MPI_COMM_WORLD, &proc); // プロセス数の取得
MPI_Get_processor_name(name, &name_length); // ノード名の取得
printf("%s : %d of %d\n", name, rank, proc);
MPI_Finalize(); // MPIの終了処理
return 0;
}
このテストコードを以下のコマンドでコンパイルします。
$ mpic++ -o sample sample.cpp
まずは1台のサーバで複数プロセスを起動してみます。
マスターサーバ上で以下のコマンドを実行してみてください。
$ mpirun -np 4 ./sample
4がプロセス数です。うまくいくと以下のような出力が得られます。
fpgaserv1 : 0 of 4
fpgaserv1 : 1 of 4
fpgaserv1 : 2 of 4
fpgaserv1 : 3 of 4
fpgaserv1
は実行したサーバ名で、次の数字がプロセスごとに割り振られるrank、最後の数字が全プロセスの数を表しています。
全てのプロセスがfpgaserv1の1台で実行されていることが分かります。
次に複数のサーバにプロセスを割り当てて実行してみます。
実行の前に、利用するサーバのIPアドレスが書かれたhost.txt
を作成し、実行するプログラムと同じディレクトリに保存しておきます。
192.168.10.11 #fpgaserv1
192.168.10.12 #fpgaserv2
192.168.10.13 #fpgaserv3
192.168.10.14 #fpgaserv4
上記の例に倣って、実際のipアドレスでhost.txtを作成してください。
サーバ間が2つ以上のネットワークによって接続されている場合は、対応するIPアドレスを指定することで利用するネットワークを切替可能です。
host.txtを作成したら、以下のコマンドをマスターサーバで実行します。
(サーバが4台より少ない場合は、プロセス数をサーバの数に合わせてください)
$ mpirun -np 4 -f host.txt ./sample
こちらもうまくいくと以下のような出力が得られます。
fpgaserv1 : 0 of 4
fpgaserv3 : 2 of 4
fpgaserv2 : 1 of 4
fpgaserv4 : 3 of 4
ここでは、4つのサーバにプロセスを割り当てています。
複数のサーバが並列に処理を行うので、出力の順番もランダムで変化します。
また、rankがhost.txtの上から0、1、2...となっていることが分かると思います。
ここまでで、MPIの実行環境の構築は完了です。
#複数FPGAのMPIプログラム例
MPIの環境が整ったところで、FPGAの動作プログラムをMPIを使って記述してみます。
とはいえ、FPGAを動かす環境は様々ですので、今回は筆者の環境における例を使って説明します。
筆者らのグループでは、Open Programmable Acceleration Engine (OPAE) をベースとした独自のソフトウェアライブラリを使ってFPGAの制御プログラムを記述しています。
このライブラリについての詳しい説明はこちらに記載されています。
以下の例では、このライブラリを用いてFPGAを制御するソフトウェアを記述しています。
まず、全てのFPGAに同じ動作をさせる場合のコードの一部を示します。
...
MPI_Init(&argc, &argv); // MPI初期化
...
afush_class afush0("AFUSH0", "AFUSH0:"); // FPGAオブジェクトafush0の作成
...
if (!afush0.open(0, nullout)) { // afush0をオープン
cout << "+ " << afush0.name << " was not opened. Abort\n";
return 0;
}
...
afush0.mod[afush::ENTIRE_SPACE].writeMMIO32(0x1000, 0x0040); // FPGA内のレジスタに32ビットのデータを書き込み
afush0.close(); // afush0をクローズ
MPI_Barrier(MPI_COMM_WORLD); // 全てのプロセスで処理が完了するまで待つ
MPI_Finalize(); // MPI終了処理
このコードでは、各サーバに接続されたFPGAのうち1つをオープンし、レジスタに値を書き込み、FPGAをクローズする処理を行っています。
mpirunを用いて実行すると、記述された処理が全サーバで並列に行われます。
最後から2行目のMPI_Barrier
によって、全ノードの処理が終わるのを待ち合わせています。
この例では特にMPI_Barrierを使う意味はありませんが、FPGA間通信の完了処理など、全てのFPGAで処理を同期させる必要がある場合に役立ちます。
MPIのrankを利用して、FPGAごとに異なる処理を行わせることもできます。
int rank;
int name_length = 10;
char name[name_length]; // サーバ名
...
MPI_Init(&argc, &argv); // MPI初期化
MPI_Comm_rank(MPI_COMM_WORLD, &rank); // rankの取得
MPI_Get_processor_name(name, &name_length); // ノード名の取得
...
if (rank == 0){ // rank0のサーバを指定
afush_class afush0("AFUSH0", "AFUSH0:"); // PAC0
afush_class afush1("AFUSH0", "AFUSH0:"); // PAC1
if (!afush0.open(0, nullout)) {
cout << "+ " << afush0.name << " was not opened. Abort\n";
return 0;
}
if (!afush0.open(0, nullout)) {
cout << "+ " << afush0.name << " was not opened. Abort\n";
return 0;
}
afush0.mod[afush::ENTIRE_SPACE].writeMMIO32(0x1000, 0x0040);
afush1.mod[afush::ENTIRE_SPACE].writeMMIO32(0x1000, 0x4000);
afush0.close();
afush1.close();
}
else if (rank == 2){ // rank2のサーバを指定
printf("%s : %d\n", name, rank);
}
MPI_Barrier(MPI_COMM_WORLD); // 全てのプロセスで処理が完了するまで待つ
MPI_Finalize(); // MPI終了処理
この例では、rank0のサーバで2枚のFPGAの処理を行いつつ、rank2では自分のサーバ名とランクを表示させています。
MPIプログラムのデバッグを行う場合は、上の例のようにprintfによりサーバ名とrankを出力させ、どのノードのプロセスに問題があるのかを調べる方法が有効です。
rankを使った個々のFPGA制御と、MPI_Barrierによる同期機能を使えば、FPGA同士の細かな通信制御なども可能です。
#cmakeによるMPIプログラムのビルド
MPIを使ったプログラムの例を示しましたが、コンパイルしないと動作させることができません。
必要なライブラリをリンクしてやれば手動でもコンパイル可能なはずですが、さすがにそれでは非効率です。
筆者の環境では、cmakeを使ってFPGAを動作させるソフトウェアをビルドしています。
というわけで、最後にこのcmake環境にMPIを組み込んでFPGA制御プログラムをビルドする方法を調べてみました。
筆者はcmakeについては初心者なのですが、一応現在の環境でMPIプログラムをビルドできているCMakelists.txtを載せておきます。
cmake_minimum_required(VERSION 3.9)
project(MPI_FPGA)
set(TGT MPI_FPGA)
add_executable(${TGT}
sample.cpp
)
find_package(MPI REQUIRED)
target_include_directories(${TGT} PUBLIC ../../afushell_class/include ${MPI_INCLUDE_PATH}) # MPIライブラリのパスをインクルード
target_link_libraries(${TGT} ${AFUSHELL_LIB} ) # FPGAクラスタ制御用ライブラリ
target_link_libraries(${TGT} opae-c ) # opaeライブラリ
target_compile_options(${TGT} PRIVATE -O3 -std=c++11 -g) # c++11を指定
SET(CMAKE_CXX_COMPILER mpicxx)
SET(CMAKE_C_COMPILER mpicc)
install(TARGETS ${TGT} RUNTIME DESTINATION bin)
リンクするライブラリなどは環境に合わせて変更してください。
使用しているcmakeのバージョンは3.21です。
cmake3.9以降であれば、上記のようにMPIを比較的簡単に組み込めるようです。
ビルドしたプログラムは、テストプログラムと同様にノード数やhostファイルを指定して実行します。
またコマンドライン引数がある場合、以下のように実行コマンドに続けて入力します。
$ mpirun -np <ノード数> -f host.txt <実行コマンド> <引数1> <引数2> ...
#まとめ
MPIを使ってFPGAクラスタを制御する方法を、簡単ではありますが紹介しました。
MPIを使えば、例えばOPAEのfpgaconfコマンドをmpirunで実行し、複数のサーバ上で一斉にfpgaを書き換えるといった便利なこともできたりします。
本当はFPGA間通信の話なども載せたかったのですが、かなり長くなりそうなので、今回はここまでとさせていただきます。