はじめにのはじめに

この記事は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でのインストールをおいておきます。

「こんなパッケージ入れたところでファブリック間接続されてる環境なんてないやい!」と思った諸兄も安心してほしください。
libfabricではTCP/IPによる通信サポートしているため動作テスト程度ならご自宅のPCでもOKです。
また、自宅にInfinibandで接続されたマシンをお持ちの方はlibverbsあたりを入れておくとverbs API経由で通信できるはずです(もってない)。

通信の初期化

libfabricでの通信の初期化は次のようなステップを踏みます

  1. 通信相手を発見するためのヒントを指定
  2. fi_getinfoで通信先の情報のリストを取得
  3. fi_fabricでネットワークを取得
  4. fi_domainでNICを取得
  5. fi_endpointでエンドポイントを取得
  6. fi_av_openでアドレスベクトルを作成
  7. 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;
};

後述する通信手段のリストも同じ構造体で帰ってくるため全てのメンバをヒントに使う訳ではないです。

今回ヒントとして与えた情報としては以下の通りです。

この内容通りに値を設定した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);

第一引数にはファブリックの種類を指定します。こちらはさっき取得した情報fifabric_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_IsendMPI_Irecvと同じような使い方となっています。
しかし、完了待ちにfi_cq(Completion Queueの略)を使う点が結構違うのでそこも含めて解説します。

片方向通信

片方向通信(俗に言うRMA)はfi_write及びfi_readを用いて行います。

まとめ

いかがでしたか?(クソ記事定型文)

この記事ではlibfabricで1対1通信をする方法を説明しました。
このライブラリを実際に使った情報はまだまだ少ないので、この記事を読んで実際に使ったみなさんも知見を共有してもらえるとうれしいです。

参考文献