まず InfiniBand Verbs プログラムを作成してみよう

作成日:2014.04.05
修正日:2014.05.11

このページではサンプルプログラム 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. プログラムをはじめるための準備

IB Verbs プログラムを作って動作させるためには InfiniBand ハードウェアが必要になる。 実機がない場合は Pseudo InfiniBand HCA driver (pib) を使うことで実験可能だ。

その上で IB Verbs プログラムをするためには、いくつか追加のパッケージが必要となる。 RedHat 系のディストリビューションであれば、最低以下のパッケージをインストールすること。

動作確認のために以下のパッケージも入れておいたほうがよい。

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 を使って順番に生成してゆく。

Table 1 InfiniBand の概念の IB Verbs におけるデータ型
IB デバイスstruct ibv_device
ユーザーコンテキストstruct ibv_context
Protection Domainstruct ibv_pd
Memory Regionstruct ibv_mr
Completion Channelstruct 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 */);

この関数から引数が多くなる。

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 には QP 番号が割り当てられるが、これは qp->qp_num で参照できる。

5. 2 つの QP を通信可能状態に設定する

4. までの処理で QP を二つ作成することができるが、これではまだ通信はできない。 QP を通信可能にするにはいくつかの状態遷移が必要で、通信に関係するパラメータを与えながら QP 内部のステート(State)を遷移させる必要があるからだ。

QP ステートの遷移は全て ibv_modify_qp() を使って実行する。

QP の内部状態は ibv_create_qp() で生成直後は Reset というステートである。 これを送信側は InitReady 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 ステートと密接に関わっているので、勝手に増やしたり、勝手に減らしたりはできない。

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() に渡すパラメータの説明をする。

RTR ステートの遷移は通信相手のパラメータを要求する。 ここで必要になるのは、通信相手の LIDQP番号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() に渡すパラメータの説明をする。

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() に渡すパラメータの説明をする。

いったん ibv_post_recv() へ投入した Receive WR は、完了が返ってくるまで受信メモリ領域の内容を変更してはならない。 ただし ibv_post_recv() の引数で渡す struct ibv_sgestruct 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() に渡すパラメータの説明をする。

いったん 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 構造体にパラメータを詰めて渡す。

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_attrsrq に作成した SRQ を指定する。 また QP に個別の RQ はないので、cap の中の max_recv_wrmax_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.1ibv_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_contextibv_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->fdread() しているでけであり、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 を返し、errnoEAGAIN を返すようになる。 ただしこの動作は 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、errnoEAGAIN を返すようになる。

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 アドレスを使っているので、負けたような気分はぬぐえない。

コメント

コメントを書き込む
[1] [Sun Han] 2016-08-30 15:25:59
6.2 SQ に Send Work Request を投入する

次に送信側が Send WR を SQ に投入する。 これには ibv_post_send() を使う。 ibv_post_send() も 1 回の呼び出しで、を数珠繋ぎにした複数の Receive WR を登録できるが、この例では 1 個だけを登録している。

ーーーーーーー
"複数の Receive WR”じゃなくて、”複数の Send WR" ではないでしょうか?

TOP    掲示板    戻る
Written by NAKAMURA Minoru, Email: nminoru atmark nminoru dot jp, Twitter:@nminoru_jp