このページではサンプルプログラム ibverbs-sample1.c を通じて InfiniBand Verbs プログラムの仕掛かりを説明する。 サンプルプログラムは単一のプロセス内に 2 つの RC サービスタイプ Queue Pair(QP) を作って SEND-RECEIVE オペレーションの通信を 1 回だけ行う。 一つのノードで完結しているのでマシンは 1 台でよい。
説明は Linux を前提とするが、他のプラットフォームでもだいたい同様に進められるはずである。
以降、InfiniBand Verbs は名称として長いので以降は IB Verbs と呼ぶことにする。
以下は関連ページ。
更新履歴
(2014.04.05) 作成。
(2014.05.11) API にリンクを貼る
目次
- 1. プログラムをはじめるための準備
- 2. ヘッダーとリンク
- 3. IB デイバスを列挙する
- 4. IB デイバスをオープンして IB Verbs のオブジェクトを作る
- 5. 2 つの QP を通信可能状態に設定する
- 6. Work Request を投入する
- 7. CQ をチェックする
- 8. Shared Receive Queue (SRQ) と Completion Channel も使ってみる
- 9. 非同期エラーを監視する
- 10. まとめ、あるいはここまでの解説でわざと書かなかったこと
- コメント
1. プログラムをはじめるための準備
IB Verbs プログラムを作って動作させるためには InfiniBand ハードウェアが必要になる。 実機がない場合は Pseudo InfiniBand HCA driver (pib) を使うことで実験可能だ。
その上で IB Verbs プログラムをするためには、いくつか追加のパッケージが必要となる。 RedHat 系のディストリビューションであれば、最低以下のパッケージをインストールすること。
- libibverbs
- libibverbs-devel
動作確認のために以下のパッケージも入れておいたほうがよい。
- libibverbs-utils
- infiniband-diags
ibstat コマンド(infiniband-diags パッケージに含まれている)を実行し、HCA が存在すること、また Base lid の項目が 0 以外になっており LID の割り当てが終わっていることを確認すれば準備が完了である。 Base lid が 0 なら OpenSM を立ち上げてない可能性が高い。
2. ヘッダーとリンク
IB Verbs のプログラムを書く場合、/usr/include/infiniband/verbs.h のヘッダーファイルを読み込むことになる。
#include <infiniband/verbs.h>
リンク時には -libverbs
をして IB Verbs の共有ライブラリをリンクする。
gcc -o a.out -libverbs test.c
3. IB デイバスを列挙する
システム内に存在する IB デバイスを列挙するには ibv_get_device_list() を用いる。
ibv_get_device_list() の戻り値は struct ibv_device
へのポインタの配列となる。
これはシステム内に存在する IB デバイス数 + 1 の配列で、最後の要素が NULL で終わっている。
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <inttypes.h> #include <infiniband/verbs.h> int main(int argc, char **argv) { int i, ret; ret = ibv_fork_init(); if (ret) { fprintf(stderr, "Failure: ibv_fork_init (errno=%d)\n", ret); exit(EXIT_FAILURE); } struct ibv_device **dev_list; dev_list = ibv_get_device_list(NULL); if (!dev_list) { int errsave = errno; fprintf(stderr, "Failure: ibv_get_device_list (errno=%d)\n", errsave); exit(EXIT_FAILURE); } for (i=0 ; dev_list[i] ; i++) { struct ibv_device *device = dev_list[i]; printf("%s GUID:%016" PRIx64 "\n", ibv_get_device_name(device), ibv_get_device_guid(device)); } ibv_free_device_list(dev_list); return 0; }
使い終わったら ibv_free_device_list() で返す。
4. IB デイバスをオープンして IB Verbs のオブジェクトを作る
IB Verbs プログラムは次に、IB Verbs を動かすのに必要なオブジェクトを生成することになる。 オブジェクトの概念のほとんどは基本的な概念編で説明しているが、それが Table 1 のような構造体に対応している。 IB Verbs の API を使って順番に生成してゆく。
IB デバイス | struct ibv_device |
ユーザーコンテキスト | struct ibv_context |
Protection Domain | struct ibv_pd |
Memory Region | struct ibv_mr |
Completion Channel | struct ibv_comp_channel |
Completion Queue(CQ) | struct ibv_cq |
Shared Received Queue(SRQ) | struct ibv_srq |
Queue Pair(QP) | struct ibv_qp |
4.1 ユーザーコンテキスト
IB Verbs プログラムは ibv_get_device_list() でられた IB デバイスに対して、ibv_open_device() を実行し、ユーザーコンテキスト を作成する。
ユーザーコンテキストは struct ibv_context
へのポインタとして渡される。
複数のプログラムが同じ HCA (IB デバイス) をオープンしても、ユーザーコンテキストは別々のものとなる。 また HCA が複数刺さっている環境で、複数の HCA を同時に使いたい場合、ibv_open_device() も複数呼ぶのでユーザーコンテキストは別々のものとなる。
さらに言えば、同一のプログラムが同一の IB デバイスを複数回 ibv_open_device() 呼べば、ユーザーコンテキストも複数できるが、これは実用上メリットがない。
struct ibv_context *context;
context = ibv_open_device(device);
ibv_open_device() は /sys/class/infiniband_verbs/uverbsN/ibdev を open()
して開き、そのファイルハンドラを獲得する。
/sys/class/infiniband_verbs/uverbsN/ibdev のアクセス権限は InfiniBand の仕様には定義されておらず、実機の InfiniBand ハードウェアの中には root 権限を必要とするものがある。
そのため ibv_open_device() は root 権限がないと失敗することがある。
pib はそのような制限がなく、だれでも ibv_open_device() を実行可能である。 そのため実機と pib ではエラーの出方が違うことがあるので注意が必要となる。
4.2 Protection Domain
次に基本的な概念編で説明したプロテクション・ドメインを作成する。 プロテクション・ドメインは ibv_alloc_pd() で作成するが、特に引数などはない。
struct ibv_pd *pd;
pd = ibv_alloc_pd(context);
4.3 Memory Region
プロテクション・ドメインの中に memory region を作成する。 Memory region の対象となるメモリは事前に確保されている必要がある。 Memory region の作成は ibv_reg_mr() で行う。
struct ibv_mr *mr; int access = IBV_ACCESS_LOCAL_WRITE; mr = ibv_reg_mr(pd, address, length, access);
四番目の引数 access には、この memory region の使用方法をフラグの論理和で指定する。 とりあえず RECV オペレーションの対象とするだけなので、IBV_ACCESS_LOCAL_WRITE のみを指定する。
4.4 Completion Queue(CQ)
ユーザーコンテキストの中に CQ を作成する。 CQ の作成は ibv_create_cq() で行う。
struct ibv_cq *cq;
int cqe = 64;
void *cq_context = NULL,
cq = ibv_create_cq(context, cqe, cq_context,
NULL /* struct ibv_comp_channel を指定 */,
0 /* comp_vector */);
この関数から引数が多くなる。
- cqe には、この CQ の最大 CQE 数を指定する。IB Verbs は最低でも cqe 数は領域を確保する。
- cq_context には、プログラムが自由な値を設定する。CQ が作成された後は cq->cq_context でアクセスできる。使わないので NULL を指定する。
- 第4引数には completion channel を指定する。ここが NULL の場合は completion channel を使わないことを意味する。
- 第5引数には基本的な概念編で説明した completion vector を指定する。Completion vector は最小 1 個あるので、0 は必ず指定できる。
4.5 Queue Pair(QP)
プロテクション・ドメインの中に QP を作成する。 QP の作成には ibv_create_qp() を使う。
struct ibv_qp *qp;
struct ibv_qp_init_attr qp_init_attr = {
.qp_type = IBV_QPT_RC,
.qp_context = NULL,
.send_cq = cq,
.recv_cq = cq,
.srq = NULL, /* SRQ を使わない */
.cap = {
.max_send_wr = 32,
.max_recv_wr = 32,
.max_send_sge = 1,
.max_recv_sge = 1,
},
.sq_sig_all = 1,
};
qp = ibv_create_qp(pd, &qp_init_attr);
ibv_create_qp() は様々なパラメータをとるため、struct ibv_qp_init_attr
構造体にパラメータを詰めて渡す。
- qp_type は InfiniBand のサービスを指定する。今回は RC サービスを使うので IBV_QPT_RC になる。
- qp_context には、プログラムが自由な値を設定する。QP が作成された後は qp->qp_context でアクセスできる。使わないので NULL を指定する。
- send_cq と recv_cq には、それぞれ Send WR と Receive WR を処理した後の CQ を設定する。ここが NULL なのは許されない。
- srq には、SRQ を指定する。今回は使用しないので NULL を指定する。
- cqp は
struct ibv_qp_cap
構造体型である。- max_send_wr は SQ の最大 WQE 数を指定する。
- max_recv_wr は RQ の最大 WQE 数を指定する。
- max_send_sge は Send WR の scatter/gather の最大組数を指定する。
- max_recv_sge は Receive WR の scatter/gather の最大組数を指定する。
- sq_sig_all には Send WR が成功した時に、それを send_cq に格納するかどうかを指定する。 実は Receive WR は完了時に必ず CQ に載るが、Send WR は成功時には CQ に載せるのを省く最適化ができる。 sq_sig_all が非 0 なら全ての Send WR は CQ に載せることになる。
QP には QP 番号が割り当てられるが、これは qp->qp_num で参照できる。
5. 2 つの QP を通信可能状態に設定する
4. までの処理で QP を二つ作成することができるが、これではまだ通信はできない。 QP を通信可能にするにはいくつかの状態遷移が必要で、通信に関係するパラメータを与えながら QP 内部のステート(State)を遷移させる必要があるからだ。
QP ステートの遷移は全て ibv_modify_qp() を使って実行する。
QP の内部状態は ibv_create_qp() で生成直後は Reset というステートである。 これを送信側は Init → Ready To Receive(RTR)、Ready To Send(RTS) と遷移させる必要がある。 受信側は受信だけ行うなら RTR だけでもよい。
以降は RC サービスタイプの QP を RTS に遷移させるための方法を端寄って説明する。 QP のステートの詳細は「InfiniBand の QP ステートの遷移を理解する」で説明する。
5.1 Reset → Init
Reset から Init への遷移させる場合、使用する P_Key のインデックス、ポート番号、アクセスフラグを設定する。 Init ステートに遷移後はまだ受信も送信もできないが、ibv_post_recv() によって Receive WR を登録することは可能になる。
struct ibv_qp_attr init_attr = {
.qp_state = IBV_QPS_INIT,
.pkey_index = 0,
.port_num = port,
.qp_access_flags = IBV_ACCESS_LOCAL_WRITE,
};
ret = ibv_modify_qp(qp, &init_attr,
IBV_QP_STATE|IBV_QP_PKEY_INDEX|IBV_QP_PORT|IBV_QP_ACCESS_FLAGS);
ibv_modify_qp() も様々なパラメータをとるため、struct ibv_qp_attr
構造体にパラメータを詰めて渡す。
第二引数は struct ibv_qp_attr
のうちどのパラメータを変更したのかをフラグの論理和で示す。
変更するパラメータは遷移先の QP ステートと密接に関わっているので、勝手に増やしたり、勝手に減らしたりはできない。
- qp_state は遷移先が Init ステートを示す
IBV_QPS_INIT を設定する。 - pkey_index は、この QP が通信に使う Partition Key Table のインデックス番号を指定する。Partition Key Table は何なのかは概念編を参照のこと。
- port_num は HCA のポートのどちらを使って通信するかを指定する。HCA は 2 ポートまでなので、1 か 2 を指定することになる。0 はエラーとなる。当然、通信相手はここで指定したポートの先に存在する必要がある。
- qp_access_flags は、この QP のアクセスフラグを指定する。ここで指定するフラグの意味は 4.3 Memory Region と同じである。ここでは SEND-RECV オペレーションをする気なので IBV_ACCESS_LOCAL_WRITE のみを指定する。
5.2 Init → RTR
Init から RTR へ遷移させると、受信可能状態となり受信が開始される。
struct ibv_qp_attr rtr_attr = { .qp_state = IBV_QPS_RTR, .path_mtu = IBV_MTU_4096, .dest_qp_num = 通信相手の QP 番号, .rq_psn = 通信相手の SQ の PSN, .max_dest_rd_atomic = 0, .min_rnr_timer = 0, .ah_attr = { .is_global = 0, .dlid = 通信相手の LID, .sl = 0, .src_path_bits = 0, .port_num = port, }, }; ret = ibv_modify_qp(qp, &rtr_attr, IBV_QP_STATE|IBV_QP_AV|IBV_QP_PATH_MTU|IBV_QP_DEST_QPN|IBV_QP_RQ_PSN|IBV_QP_MAX_DEST_RD_ATOMIC|IBV_QP_MIN_RNR_TIMER);
ibv_modify_qp() に渡すパラメータの説明をする。
- qp_state は遷移先が RTR ステートを示す IBV_QPS_RTR を設定する。
- path_mtu は、Path MTU を指定する。Path MTU は QP 同士で決めてよいパケットの最大長である。Path MTU は、送信元から受信先までにある全 HCA & スイッチの Active MTU 以下である必要がある。ただし現在の InfiniBand では特に考えずに IBV_MTU_4096 を指定してもよい。この値は送信・受信で一致している必要がある。
- dest_qp_num は、通信相手の QP 番号を指定する。
- rq_psn は、通信相手の SQ の PSN 番号を指定する。PSN は基本的な概念編で説明している。
- max_dest_rd_atomic は、RDMA READ と ATOMIC Operations の最大受信数を設定する。 意味は「InfiniBand の再送制御を理解する」で説明する。 ここでは 0 に設定する。
- min_rnr_timer は、0〜31 ならなんでもよい。
- ah_attr は通信相手を特定するためのデータを格納する
struct ibv_ah_attr
構造体である。- is_global は Global Routing Header(GRH) を使うかどうかを決定する。0 で未使用でよい。
- dlid は、通信相手の LID (Destination LID) を指定する。
- sl は、ここでは盲目的に 0 に設定して。
- src_path_bits は、ここでは盲目的に 0 に設定して。
- port_num は HCA のポートのどちらを使って通信するかを指定する。HCA は 2 ポートまでなので、1 か 2 を指定することになる。0 はエラーとなる。
5.1 で指定した port_num とこの値は一致している必要がある。
RTR ステートの遷移は通信相手のパラメータを要求する。 ここで必要になるのは、通信相手の LID、QP番号、SQ の PSN である(場合によっては Path MTU も)。 ibverbs-sample.c は、一つのプロセスの中に QP が二つあるだけなのでメモリを介してパラメータの交換をしている。
5.3 RTR → RTS
RTR から RTS へ遷移させると、送信も可能になる。
struct ibv_qp_attr rts_attr = { .qp_state = IBV_QPS_RTS, .timeout = 0, .retry_cnt = 7, .rnr_retry = 7, .sq_psn = 0 から 224 - 1 までの自由な値, .max_rd_atomic = 0, }; ret = ibv_modify_qp(qp, &rts_attr, IBV_QP_STATE|IBV_QP_TIMEOUT|IBV_QP_RETRY_CNT|IBV_QP_RNR_RETRY|IBV_QP_SQ_PSN|IBV_QP_MAX_QP_RD_ATOMIC);
ibv_modify_qp() に渡すパラメータの説明をする。
- qp_state は遷移先が RTS ステートを示す IBV_QPS_RTS を設定する。
- timeout は、0〜31 の値であれば何でもよい。
- retry_cnt は、0〜7 の値であれば何でもよい。
- min_rnr_timer は、0〜31 の値であれば何でもよい。
- sq_psn は、SQ の PSN を設定する。PSN は 24 ビット値であり 0 〜 16,777,215 を指定できる。
- max_rd_atomic は、RDMA READ と ATOMIC Operations の最大送信数を設定する。 意味は「InfiniBand の再送制御を理解する」で説明する。 ここでは 0 に設定する。
sq_psn の PSN、5.2 のように通信相手が RTR へ遷移させる時に設定する必要があるので、RTS へ遷移させるよりも前に決定しておく必要がある。
6. Work Request を投入する
5. の処理で通信が可能になった。 実際に通信を行ってみる。
6.1 RQ に Receive Work Request を投入する
まず受信側が Receive WR を RQ に投入する。 これには ibv_post_recv() を使う。 ibv_post_recv() は 1 回の呼び出しで、を数珠繋ぎにした複数の Receive WR を登録できるが、この例では 1 個だけを登録している。
struct ibv_sge sge = { .addr = 受信メモリ領域の開始アドレス, .length = 受信メモリ領域のバイト長, .lkey = mr->lkey, }; struct ibv_recv_wr recv_wr = { .wr_id = (uint64_t)(uintptr_t)sge.addr, .next = NULL, .sg_list = &sge, .num_sge = 1, }; struct ibv_recv_wr *bad_wr; ret = ibv_post_recv(qp, &recv_wr, &bad_wr);
ibv_post_recv() に渡すパラメータの説明をする。
- 第二引数は最初の Receive WR へのポインタである。
- 第三引数はエラーが発生した時に、エラーを起こした Receive WR をポインタとして格納する。複数の Receive WR を登録しようとした場合、エラー発生箇所を知ることができる。
struct ibv_sge
の addr は受信メモリ領域の開始アドレスを指定する。いずれかの memory region で登録した範囲である必要がある。struct ibv_sge
の length は受信メモリ領域のバイト長を指定する。いずれかの memory region で登録した範囲である必要がある。struct ibv_sge
の key には memory region の L_Key を指定する。L_Key はこれまで説明してなかったが、R_Key 同様に ibv_reg_mr() で生成時に mr->lkey で渡される。struct ibv_recv_wr
の wr_id にはプログラムが任意の値を指定できる 64 ビットフィールドである。サンプルでは受信メモリ領域の開始アドレスを入れておく。struct ibv_recv_wr
の next は、複数の Receive WR を登録する場合に、次の Receive WR を指す。NULL なら次はない。struct ibv_recv_wr
の sg_list は、scatter/gather データの先頭ポインタを指定する。struct ibv_recv_wr
の num_sge は、scatter/gather の組数を指定する。
いったん ibv_post_recv() へ投入した Receive WR は、完了が返ってくるまで受信メモリ領域の内容を変更してはならない。
ただし ibv_post_recv() の引数で渡す struct ibv_sge
や struct ibv_recv_wr
は関数内で内部にコピーするので、すぐに関数復帰後はすぐに破棄してもよい。
6.2 SQ に Send Work Request を投入する
次に送信側が Send WR を SQ に投入する。
これには ibv_post_send() を使う。
ibv_post_send() も 1 回の呼び出しで、を数珠繋ぎにした複数の Receive WRSend WR を登録できるが、この例では 1 個だけを登録している。
struct ibv_sge sge = { .addr = 送信メモリ領域の開始アドレス, .length = 送信メモリ領域のバイト長, .lkey = mr->lkey, }; struct ibv_send_wr send_wr = { .wr_id = (uint64_t)(uintptr_t)sge.addr, .next = NULL, .sg_list = &sge, .num_sge = 1, .opcode = IBV_WR_SEND_WITH_IMM, .send_flags = 0, .imm_data = 任意の32ビット値 }; struct ibv_send_wr *bad_wr; ret = ibv_post_send(qp, &send_wr, &bad_wr);
ibv_post_send() に渡すパラメータの説明をする。
- 第二引数は最初の Send WR へのポインタである。
- 第三引数はエラーが発生した時に、エラーを起こした Send WR をポインタとして格納する。複数の Send WR を登録しようとした場合、エラー発生箇所を知ることができる。
struct ibv_sge
は送信メモリ領域を指定する点を除けば、ibv_post_recv() と同じである。struct ibv_send_wr
の wr_id にはプログラムが任意の値を指定できる 64 ビットフィールドである。サンプルでは送信メモリ領域の開始アドレスを入れておく。struct ibv_send_wr
の next は、複数の Send WR を登録する場合に、次の Send WR を指す。NULL なら次はない。struct ibv_send_wr
の sg_list は、scatter/gather データの先頭ポインタを指定する。struct ibv_send_wr
の num_sge は、scatter/gather の組数を指定する。struct ibv_send_wr
の opcode にはオペレーション種類を指定する。SEND with Immediate オペレーションを実行するために IBV_WR_SEND_WITH_IMM を指定する。struct ibv_send_wr
の send_flags の説明は割愛する。ここでは盲目的に 0 を指定する。
いったん ibv_post_send() へ投入した Send WR は、QP が RTS なら送信を開始する。 ただし完了が返ってくるまでは送信メモリ領域の内容を変更してはならない。
7. CQ をチェックする
Send WR と Receive WR の完了を CQ に対して ibv_poll_cq() を発行しながらポーリングする。
int i, ret;
struct ibv_wc wc;
retry:
ret = ibv_poll_cq(cq, 1, &wc);
if (ret == 0)
goto retry; /* polling */
if (ret < 0) {
fprintf(stderr, "Failure: ibv_poll_cq\n");
exit(EXIT_FAILURE);
}
if (wc.status != IBV_WC_SUCCESS) {
fprintf(stderr, "Completion errror\n");
exit(EXIT_FAILURE);
}
switch (wc.opcode) {
case IBV_WC_SEND:
goto retry;
case IBV_WC_RECV:
printf("Success: wr_id=%016" PRIx64 " byte_len=%u, imm_data=%x\n", wc.wr_id, wc.byte_len, wc.imm_data);
break;
default:
exit(EXIT_FAILURE);
}
8. Shared Receive Queue (SRQ) と Completion Channel も使ってみる
8.1 Shared Receive Queue (SRQ) を使ってみる
SRQ を作成する
基本的な概念編で紹介した Share Receive Queue(SRQ) は、QP と同じようにプロテクションドメイン内に作成する。 SRQ の作成には ibv_create_srq() を使う。
struct ibv_srq *srq;
struct ibv_srq_init_attr srq_init_attr = {
.srq_context = NULL,
.attr = {
.max_wr = 64,
.max_sge = 1,
.srq_limit = 0,
},
};
srq = ibv_create_srq(pd, &srq_init_attr);
ibv_create_srq() も初期値を struct ibv_srq_init_attr
構造体にパラメータを詰めて渡す。
- srq_context には、プログラムが自由な値を設定する。SRQ が作成された後は srq->srq_context でアクセスできる。使わないので NULL を指定する。
- attr は
struct ibv_srq_attr
構造体のである。- max_wr は SRQ の最大 WQE 数を指定する。
- max_sge は SRQ に投入する Receive WR の scatter/gather の最大組数を指定する。
- srq_limit は ibv_create_srq() では 0 を指定する。
4.5 で述べた QP の作成は、以下のように変更する必要がある。
struct ibv_qp *qp; struct ibv_qp_init_attr qp_init_attr = { .qp_type = IBV_QPT_RC, .qp_context = NULL, .send_cq = cq, .recv_cq = cq, .srq = srq, .cap = { .max_send_wr = 32, .max_recv_wr = 0, .max_send_sge = 1, .max_recv_sge = 0, }, .sq_sig_all = 1, }; qp = ibv_create_qp(pd, &qp_init_attr);
ibv_create_qp() に渡す qp_init_attr の srq に作成した SRQ を指定する。 また QP に個別の RQ はないので、cap の中の max_recv_wr と max_recv_sge は無視されるが、とりあえず 0 に設定する。
Mellanox ConnectX-2/3 の場合、SQ や SRQ の scatter/gather 最大組数は 32 だが、SRQ の s/g 最大組数は 31 と一つ小さい。 何故だかは分からない。
SRQ に Receive Work Request を投入する
SRQ への Receive WR の投入には ibv_post_srq_recv() を使う。 第一引数に QP ではなく SRQ を指定することをのぞけば、6.1 の ibv_post_recv() と同じなので詳細は割愛する。
ret = ibv_post_srq_recv(srq, &recv_wr, &bad_wr);
SRQ の残量を調べる
ところで IB Verbs の仕様として SQ や RQ にどれだけ WQE が積まれているか、あるいは CQ にどれだけ CQE が積まれているかを調べる方法は存在しない。 それでも SQ や RQ は QP 毎にあるので、自分が ibv_post_send()、ibv_post_recv() で登録した Work Request 数を数えていれば対応できる。 CQ に詰まれた CQE 数が分からなくても、全部取り出すのであれば問題ない。
しかし SRQ は複数の QP から共有されているため残量が気になる。 残り WQE 数が減れば、すぐに補充する必要がある。
が、しかし IB Verbs の仕様として SRQ にどれだけ WQE が積まれているかを直接調べる方法も存在しない。 ただ間接的に SRQ の WQE の残量がある閾値を下回ったらそれをアラートする機能が備わっている。
閾値は ibv_modify_srq() で IBV_SRQ_LIMIT で指定する。
ここでは ibv_create_srq() では使わなかった struct ibv_srq_attr
構造体の srq_limit を指定することになる。
この値は ibv_create_srq() で指定した max_wr 以下でないと意味がない。
struct ibv_srq_attr srq_attr = { .srq_limit = 32, }; ret = ibv_modify_srq(srq, &srq_attr, IBV_SRQ_LIMIT);
アラートは SRQ の残 WQE 数が srq_limit 未満になると、SRQ Limit Reached 非同期エラー(IBV_EVENT_SRQ_LIMIT_REACHED)で報告される。 なので IBV_EVENT_SRQ_LIMIT_REACHED は、非同期エラーというよりは非同期イベントである。 プログラムは非同期イベントを監視することで、このタイミングを知ることができる。
SRQ に残っている WQE 数が SRQ Limit 値を下回ると SRQ Limit Reached 非同期エラーはすぐに発生するので、順番としては ibv_post_srq_recv() で Receive WR を登録 → ibv_modify_srq() で IBV_SRQ_LIMIT を設定の順に行う必要があった。
SRQ Limit Reached 非同期エラーが一度発生した後は、ibv_modify_srq() で IBV_SRQ_LIMIT を再設定しないと、次の SRQ Limit Reached 非同期エラーは発生しなくなる。
8.2 Completion Channel を使ってみる
Completion Channel の作成
基本的な概念編で紹介した Completion Channel は、CQ の作成前に ibv_create_comp_channel() を使って作成する。
struct ibv_comp_channel *channel;
channel = ibv_create_comp_channel(context);
ibv_create_comp_channel() には、ユーザーコンテキストだけを指定し、特殊な引数はない。
struct ibv_cq *cq; int cqe = 64; void *cq_context = NULL, cq = ibv_create_cq(context, cqe, cq_context, channel, comp_vector);
作成した completion channel は ibv_create_cq() の第四引数に指定する。
また completion channel を指定した場合は、基本的な概念編で紹介した completion vector の指定が意味を持つ。
comp_vector に指定できる completion vector の最大数は ibv_open_device() の戻り値となる struct ibv_context
構造体の num_comp_vectors に格納されている。
Completion Channel を使った監視
Completion channel を使った完了イベントの到着の監視は、ibv_req_notify_cq() で CQ に「監視」を指定した瞬間から始まる。
int solicited_only = 0;
ibv_req_notify_cq(cq, solicited_only);
ibv_req_notify_cq() の第一引数には CQ を指定する。 第二引数の solicited_only はここでは盲目的に 0 を設定するとさせて欲しい。
ibv_req_notify_cq() を設定した後に完了イベントが到着した CQ があれば、それを ibv_get_cq_event で取り出せる。 この関数を呼び出すと、(シグナルの割り込みが入るなどの要因がなければ)完了イベントを待ってずっと待機状態に入る。 タイムアウト指定はない。
struct ibv_cq *cq; void *cq_context; ret = ibv_get_cq_event(channel, &cq, &cq_context); // ibv_req_notify_cq を再設定する ibv_req_notify_cq(cq, 0); // cq には完了イベントが到着しているので ibv_poll_cq で取り出す ibv_ack_cq_events(cq, 1);
ibv_get_cq_event() は、戻り値が 0 で復帰した場合は CQ の取り出しが成功している。 成功の場合は、cq に CQ へのポインタが、cq_context に ibv_create_cq() で指定した cq_context が入る。 失敗すると戻り値は -1 になっている。
CQ が取り出せたら、この completion channel による完了イベント監視はワン・ショット・トリガー(one shot trigger)なので ibv_req_notify_cq() で監視を再設定する。 その上で ibv_poll_cq() で Work Completion を全部取り出すことになる。 ibv_req_notify_cq() を指定する前に、同一の CQ が ibv_get_cq_event() で二度取り出せないことは保証されている。
最後に ibv_get_cq_event() で取り出した CQ イベントに対して、ibv_ack_cq_events() で CQ イベントの利用終了を通知する。 第二引数は普通 1 を指定する。 1 以外を指定するのは、ibv_get_cq_event() と ibv_ack_cq_events() をペアで使っていない時である。 ibv_get_cq_event() で同一の CQ を N 回取り出したのであれば、ibv_ack_cq_events() は第二引数を N に指定して 1 回だけ呼び出せばよい。
InfiniBand と IB Verbs の仕様は、ibv_req_notify_cq() を設定した後に CQ に完了イベントが到着した場合は ibv_get_cq_event() でそれを検知できることを保証している。 しかし実装上、CQ にすでに CQE が積まれている場合に ibv_req_notify_cq() が呼ばれると、新たな完了イベントが発生しなくても ibv_get_cq_event() で取り出せるようになる。
そのため ibv_get_cq_event() → ibv_poll_cq() で全部取り出し → ibv_req_notify_cq() → もう一度確認のために ibv_poll_cq() で取り出し → ibv_ack_cq_events() のように二重取りした方がよい。
ibv_ack_cq_events() はマルチスレッド対策のためにあると考えてよい。 CQ の内部には参照カウンターがあり、ibv_get_cq_event() で取り出した CQ は参照カウンターがカウントアップしている。 そして他のスレッドが CQ を破壊する操作(ibv_destroy_cq())を実行できないようにガードしている。 ibv_ack_cq_events() はこの参照カウンターをダウンさせる操作である。
逆に言うと ibv_get_cq_event() で取り出した CQ を ibv_ack_cq_events() を呼び出した後も使うと、その struct ibv_cq
へのポインタはダングリング・ポインタ(dangling pointer)となっている。
Completion Channel の file descriptor を使った監視
冷静に考えると ibv_get_cq_event() にタイムアウト指定がなく、CQ に完了が来るまで待ち続けるという仕様はまともなプログラムは開発するなと言っているようなものである。 マルチスレッドを使って ibv_get_cq_event() で待機する処理を分離するというアイデアもあり、GlusterFS などが実装して例もあるが、これも筋悪である。
ではどうするか?
ibv_create_comp_channel() の戻り値となる struct ibv_comp_channel
構造体には fd というメンバ変数があり、IB Verbs のカーネル部から貰ったファイルディスクリプタを記録している。
この channel->fd は物理的なファイルには結び付けられていないリードだけできる仮想ファイルのファイルディスクリプタである。
ibv_get_cq_event() は、この channel->fd を read()
しているでけであり、select()
や poll()
を適用することもできる。
これをうまくやるには、まず channel->fd に non-blocking モードを設定する。
flags = fcntl(channel->fd, F_GETFL); rc = fcntl(channel->fd, F_SETFL, flags | O_NONBLOCK);
この上で channel->fd が ready to input 状態になるのをタイムアウト制限のある API で待てばよい。 複数の completion channel をソケットやその他のファイル操作と並列に処理することも可能となる。
struct pollfd pollfd = { .fd = channel->fd, .events = POLLIN, .revents = 0, }; do { rc = poll(&pollfd, 1, ms_timeout); } while (rc == 0); if (rc < 0) { fprintf(stderr, "Failure: poll (errno=%d)\n", errno); exit(EXIT_FAILURE); } // すでに CQ イベントの到着を確認しているので、ibv_get_cq_event が必ず成功する ret = ibv_get_cq_event(channel, &cq, &cq_context);
channel->fd を non-blocking にした場合、ibv_get_cq_event() の動作が変わってしまう。 取り出すべき CQ がなければ、ibv_get_cq_event() は戻り値として -1 を返し、errno は EAGAIN を返すようになる。 ただしこの動作は IB Verbs の仕様に定義されていない。
9. 非同期エラーを監視する
基本的な概念編で紹介した非同期エラーは ibv_get_async_event() で取り出す。
第一引数は非同期エラーを取り出すユーザーコンテキストを指定する。
もし非同期エラーが存在した場合は、第二引数に指定した struct ibv_async_event
のポインタへ格納される。
struct ibv_async_event event;
ret = ibv_get_async_event(context, &event);
struct ibv_async_event
構造体は以下のような定義になっている。
event_type が非同期イベントの種類を決めている。
struct ibv_async_event { union { struct ibv_cq *cq; /* CQ that got the event */ struct ibv_qp *qp; /* QP that got the event */ struct ibv_srq *srq; /* SRQ that got the event */ int port_num; /* port number that got the event */ } element; enum ibv_event_type event_type; /* type of the event */ };
ibv_get_async_event() で取り出した非同期イベントは ibv_ack_async_event
で返却する。
ibv_ack_async_event
が呼び出されるまでの間は、struct ibv_async_event
に含まれている CQ/QP/SRQ は ibv_destroy_cq()/ibv_destroy_qp()/ibv_destroy_srq() で破壊できなくなっている。
逆に ibv_ack_async_event が呼び出された後に、struct ibv_async_event
の先にあるポインタはダングリング・ポインタになっている。
ibv_ack_async_event(&event);
ibv_get_async_event() も ibv_get_cq_event() と同様に呼び出すと、(シグナルの割り込みが入るなどの要因がなければ)完了イベントを待ってずっと待機状態に入る。 これを防ぐために completion channel の channel->fd のように、ユーザーコンテキストの中にある async_fd を使った監視が可能である。
まず context->async_fd の non-blocking モードに変更することができる。 これによって(仕様外なのだが)ibv_get_async_event()は返すべき非同期エラーがない場合は戻り値が -1、errno が EAGAIN を返すようになる。
flags = fcntl(context->async_fd, F_GETFL); rc = fcntl(context->async_fd, F_SETFL, flags | O_NONBLOCK);
context->async_fd が ready-to-input 状態になるまで select()
や poll()
で監視することも可能である。
Completion channel の channel->fd とやり方は一緒なので、詳細は割愛する。
10. まとめ、あるいはここまでの解説でわざと書かなかったこと
InfiniBand の通信に最低限必要な機能をいろいろ端寄って説明すると以上のようになる。
しかしこの文書では非常に重要なことが一つ抜けている。 QP による通信を始めるためには、RC サービスの場合には通信相手の LID、QP 番号、SQ の PSN の 3 つのパラメータの交換が必要になる。 これは UD サービスの場合には通信相手の LID、QP 番号、Q_Key の 3 つに変わるが、やはりパラメータ交換が必要である。
InfiniBand では QP と QP を相互パラメータ設定することをコミュニケーション(Communication)と呼ぶ。 コネクションと呼ばないのは、RC/RD/UD/UC を合わせた概念だからだと思われる。 では IB Verbs プログラムはコミュニケーションを確立するのに必要な情報をどうやって「通信」するのだろうか?
残念ながらスマートな解決は現在の InfiniBand にはなく、Internet Protocol(IP)の力を借りると言うところに落ち着いている。 InfiniBand の Internet Protocol over InfiniBand(IPoIB) は InfiniBand 上で IP ネットワークを構成することができる。 IPoIB はカーネル層で動作する InfiniBand プログラムで、IPoIB 用に UD QP を作成する。 この UD QP の QP 番号は当然動的生成だが、IB マルチキャストを使ってサブネット内に配信するので InfiniBand の完結したコミュニケーションは確立できている。 その上で TCP や UDP のソケットを開いて QP パラメータを交換すれば、一応 IB Verbs レベルのコミュニケーション確立を実現できる。 ただし何か負けたような気分になる方法である。
もう一つは RDMA Communication Manager(CM) を使う方法である。
RDMA CM はコミュニケーションの確立・切断とエラーハンドリングをラッピングすることのできる、InfiniBand の上位サービスである。
ただし RDMA CM は IB Verbs に似ているが、IB Verbs ではない。
IB Verbs で ibv_post_send() を使っていたところを、RDMA CM では rdma_post_send()
を使うのように API 体系が異なっている。
そして通信相手の識別にはやはり IP アドレスを使っているので、負けたような気分はぬぐえない。
次に送信側が Send WR を SQ に投入する。 これには ibv_post_send() を使う。 ibv_post_send() も 1 回の呼び出しで、を数珠繋ぎにした複数の Receive WR を登録できるが、この例では 1 個だけを登録している。
ーーーーーーー
"複数の Receive WR”じゃなくて、”複数の Send WR" ではないでしょうか?