はじめにのはじめに
この記事は2年くらい前に書こうとしたけど途中で面倒になって書き途中で放置されていたものです
なので一部(特に後半)に記述が不十分な箇所があります
が、現状のlibfabricの日本語情報の無さを考えるとこれでも有益な情報な気がしたのでネットに放出しておきます
この記事はKogakuin Univ Advent Calendar 2022 - Adventar 23日目です。
圧倒的大遅刻ですごめんなさい。もう年越しちゃった…
はじめに
あけましておめでとうございます。metarinです
今日は研究でちょっと使ったライブラリであるlibfabricについて書きたいと思います。
このライブラリ、メジャーなライブラリに採用されている割にまともな資料が公式ドキュメントくらいしかないという大変初学者に優しくないライブラリとなっており、僕もあまりの情報の少なさに泣きを見ました。^1
これ以上ドキュメント不足で不幸になる人間を増やさないためにも、1対1通信でRMAを叩けるようになるところまでの情報を載せておきます。
What is libfabric
libfabricはmpiのバックエンドにも使われているファブリック間通信を提供するAPI及びその実装です。
一般的にファブリック間通信を行うにはその通信アーキテクチャ専用のライブラリを書く必要があります(ex. Infiniband verbs、 Amazon EFA)。
しかし、これでHPC用のライブラリ等を作ろうとした場合、移植性やコードの保守性があまりに終わってしまうのでそれらを抽象化するラッパライブラリを作ろうという機運が高まった結果生まれたものっぽいです。
このライブラリはIntelやCiscoといったような錚々たるメンバーによってメンテナンスされ、MPICHやMPICHベースのIntel MPIのバックエンド等有名なライブラリに使われています。
しかし、用途がいささかニッチであるためか界隈に「完全理解している人間」と「なんもわからん人間」おらず、公式ドキュメント以外の情報があまりに無です。
前提
使用していた環境はOPA100(psm2)インターコネクトで接続されています。
verbsやEFA等のその他のアーキテクチャでは細部について異なる可能性があることはあらかじめご承知下さい。
また、バージョンによる仕様の違い^2や筆者の勘違いによって嘘が書かれている場合もあります。
「なんかおかしくね?」と思ったら https://github.com/ofiwg/ofi-guide/blob/master/OFIGuide.md とか https://ofiwg.github.io/libfabric/v1.16.1/man/ を見ましょう。
導入
libfabric自体はメジャーなライブラリなので(直接触っている人間がいっぱいいるとは言っていない)だいたいのディストリビューションで容易に導入できます。
例としてUbuntuとArch Linuxでのインストールをおいておきます。
- Ubuntu
sudo apt install libfabric1 libfabric-dev
- Arch Linux
sudo pacman -S libfabric
「こんなパッケージ入れたところでファブリック間接続されてる環境なんてないやい!」と思った諸兄も安心してほしください。
libfabricではTCP/IPによる通信サポートしているため動作テスト程度ならご自宅のPCでもOKです。
また、自宅にInfinibandで接続されたマシンをお持ちの方はlibverbsあたりを入れておくとverbs API経由で通信できるはずです(もってない)。
通信の初期化
libfabricでの通信の初期化は次のようなステップを踏みます
- 通信相手を発見するためのヒントを指定
- fi_getinfoで通信先の情報のリストを取得
- fi_fabricでネットワークを取得
- fi_domainでNICを取得
- fi_endpointでエンドポイントを取得
- fi_av_openでアドレスベクトルを作成
- fi_enableでエンドポイントを有効化
書いてて思いましたがあまりに長いですね。が、悲しいことにこれらの概念はだいたい後で使うので順を追って説明します。
通信相手を発見するためのヒントを指定
まず最初に通信相手への経路情報を取得したいのですが、そのためのヒントを与える必要があります。^3
このヒントはfi_info
という構造体に格納します。fi_info
構造体はfi_allocinfo
関数で確保できます。
struct fi_info {
struct fi_info *next;
uint64_t caps;
uint64_t mode;
uint32_t addr_format;
size_t src_addrlen;
size_t dest_addrlen;
void *src_addr;
void *dest_addr;
fid_t handle;
struct fi_tx_attr *tx_attr;
struct fi_rx_attr *rx_attr;
struct fi_ep_attr *ep_attr;
struct fi_domain_attr *domain_attr;
struct fi_fabric_attr *fabric_attr;
struct fid_nic *nic;
};
後述する通信手段のリストも同じ構造体で帰ってくるため全てのメンバをヒントに使う訳ではないです。
今回ヒントとして与えた情報としては以下の通りです。
- ep_attr: エンドポイントの種類。psm2ではコネクションレスなものしかサポートされないため
FI_EP_DGRAM
(信頼性が低い)かFI_EP_RDM
(信頼性が高い)を使用する必要があります。RMAを使用する関係上メッセージ順序を保証したいので今回はFI_EP_RDM
を使用 - caps: サポートする機能。RMAによるアクセスを行いたいので
FI_RMA
と、RMAのためのアドレス共有に使う通信用にFI_MSG
を指定 - mode: psm2における
FI_MSG
にはFI_CONTEXT
が要求されるので指定
この内容通りに値を設定したfi_infoを生成するコードはこんな感じです
struct fi_info *hints;
hints = fi_allocinfo();
hints->ep_attr->type = FI_EP_RDM;
hints->caps = FI_MSG | FI_RMA;
hints->mode = FI_CONTEXT;
fi_getinfoで通信先情報のリストを取得
次にさっき設定したヒントを使って接続先のリストを取得します。
接続先のリストはfi_getinfo
関数で取得可能です。
#include <rdma/fabric.h>
int fi_getinfo(int version, const char *node, const char *service,
uint64_t flags, const struct fi_info *hints, struct fi_info **info);
第一引数version
にはインターフェースに要求するバージョンを指定します。基本的な機能を使う分にはFI_VERSION(1, 0)
で困ることはないと思います。
第二引数node
には通信相手を指定します。この値にはNULLを指定できますが、全部をそうすると大体解決でコケるので1プロセスのみNULL、他はそのノードのアドレスを指定します(アドレスの取得方法は後述)。
第三引数service
には通信に用いるポート番号を指定します。これは他と被っていないそのアーキテクチャで解釈可能な値であれば何でもOKです。僕は適当に"12345"
にしました。
第四引数flags
はfi_getinfoの動作を制御するための引数です。ここは第二引数でNULLを指定したものはFI_SOURCE
、そうでない場合はNULL
を指定しました。
FI_SOURCE
はアドレス解決に関係するパラメータのようです(詳細は調べられず)。
第五引数hints
はさっき作ったヒントを指定します。
第六引数info
は取得した接続先のヒントのリストが格納されるポインタを指定します。
これは適当に用意して下さい。
ちなみにメモリの確保は自動で行われます。
上記の通りのfi_getinfo
の呼び出しは以下のようになります
struct fi_info *fi;
// !is_client なプロセス(server)はFI_SOURCEを指定、他はserverのアドレスを指定
if (is_client) {
fi_getinfo(version, server, "12345", 0, hints, &fi);
} else {
fi_getinfo(version, NULL, "12345", FI_SOURCE, hints, &fi);
}
fi_fabricでファブリックドメインを取得
ファブリックドメインは誤解を恐れずに説明するとネットワークに存在するデバイスの集合、つまりノード郡のことです。
ファブリックドメインは構造体fid_fabric
に保持され、取得は関数fi_fabric
で行います。
#include <rdma/fabric.h>
int fi_fabric(struct fi_fabric_attr *attr,
struct fid_fabric **fabric, void *context);
第一引数にはファブリックの種類を指定します。こちらはさっき取得した情報fi
のfabric_attr
を使います。
第二引数にはファブリックドメインを保持する構造体を指定します。これは適当に用意して下さい。
第三引数にはコンテキストを指定します。コンテキストはEvent-Drivenなことをしたい場合に使いますが、今回のプログラムではファブリックドメインから発せられるイベントは使わないのでNULLでOKです。
上記の通りのfi_fabric
の呼び出しは以下のようになります
struct fid_fabric *fabric;
fi_fabric(fi->fabric_attr, &fabric, NULL);
fi_domainでアクセスドメインを取得
アクセスドメイン(access domain, resource domainとも呼ばれる)はNICまたはハードウェアポートに対応する概念です。
アクセスドメインは構造体fid_domain
に保持され、取得は関数fi_domain
で行います。
#include <rdma/fabric.h>
#include <rdma/fi_domain.h>
int fi_domain(struct fid_fabric *fabric, struct fi_info *info,
struct fid_domain **domain, void *context);
第一引数にはファブリックドメインを指定します。これはさっき取得したファブリックドメインfabric
を使います。
第二引数には通信先情報のリストを指定します。これは通信先情報のリストの取得で取得した構造体fi
を使います。
第三引数にはアクセスドメインを保持する構造体を指定します。これは適当に用意して下さい。
第四引数にはコンテキストを指定します。
fi_endpointでエンドポイントを取得
fi_av_openでアドレスベクトルを作成
fi_enableでエンドポイントを有効化
エンドポイントはデフォルトでは無効なので明示的に有効にする必要があります。
エンドポイントはfi_enable
で有効化できます。
使い方は第一引数に取得したepを渡すだけです。
fi_enable(ep);
これでようやく通信を行う準備ができました。長かったですね。
さてここから実際に通信を行っていきます。
双方向通信
双方向通信はfi_send
及びfi_recv
を用いて行います。
基本的にはMPIのMPI_Isend
、MPI_Irecv
と同じような使い方となっています。
しかし、完了待ちにfi_cq
(Completion Queueの略)を使う点が結構違うのでそこも含めて解説します。
片方向通信
片方向通信(俗に言うRMA)はfi_write
及びfi_read
を用いて行います。
まとめ
いかがでしたか?(クソ記事定型文)
この記事ではlibfabricで1対1通信をする方法を説明しました。
このライブラリを実際に使った情報はまだまだ少ないので、この記事を読んで実際に使ったみなさんも知見を共有してもらえるとうれしいです。