먼저 InfiniBand Verbs 프로그램을 작성해 보자

작성일 :2014.04.05
수정일 :2014.05.11

이 페이지에서는 샘플 프로그램 ibverbs-sample1.c 을 통해서 InfiniBand Verbs 프로그램의 개요를 설명한다. 샘플 프로그램은 단일 프로세스 내에 2개의 RC 서비스 타입 Queue Pair (QP)를 만들어 SEND-RECEIVE 오퍼레이션 통신을 1회 수행한다. 하나의 노드에서 끝나므로 머신은 1대만 있으면 된다.

설명은 Linux를 전제로 하지만, 다른 플랫폼에서도 대체로 유사하게 진행될 것이다.


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을 지정한다.
  • 네번째 인수에는 completion channel 을 지정한다. 여기가 NULL인 경우는 completion channel 을 사용하지 않는 것을 의미한다.
  • 다섯번재 인수에는 기본개념편에 설명할 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. 두개의 QP를 통신 가능 상태로 설정

4. 까지의 처리에서 QP를 2개 만들 수 있었지만, 그것으로는 아직 통신할 수 없다. QP를 통신 가능하게 하는 것에은 몇가지의 상태 전이가 필요하고, 통신에 관계되는 파라미터를 주면서 QP내부의상태(Status)를 전이 시켜야 할 필요가 있기 때문이다.

QP 상태 전이는 전부 ibv_modify_qp()를 사용해서 실행한다.

QP 내부 상태는 ibv_create_qp()로 생성 직후는Reset상태이다. 이것을 송신측은InitReady To Receive(RTR), Ready To Send(RTS) 로 전이 시킬 필요가 있다. 수신측은 수신만 할 경우 RTR만으로도 좋다.

이후에는 RC 서비스 타입의 QP를 RTR로 전이 시키기 위한 방법을 설명한다. 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의 PS,

    .max_dest_rd_atomic     = 0,
    .min_rnr_timer          = 0,
    .ah_attr                = {
        .is_global          = 0,
        .dlid               = 통신상대의 LI,

        .sl                 = 0,
        .src_path_bits      = 0,
        .port_num           = port,
    },
};

ret = ibv_modify_q(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 오퍼레이션들의 최대 수신 수를 지정한다. 의미는 '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가 2개 있는 것이므로 메모리를 통해 파리미터를 교환한다.

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 에서 2^24 - 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 는 전이 대상이 RTR 상태를 표시하는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 오퍼레이션들의 최대 송신수를 설정한다. 의미는 '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 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로 1개 적다. 왜인지는 모른다.

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 잔량이 임계치1이하이면 그것을 경고하는 기능이 있다.

임계치는 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는 여기서는 무조건 -을 설정하도록 했으면 한다.

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으로 지정하고 한번만 호출하면 된다.

InfiniBand와 IB Verbs의 규격은 ibv_reg_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를 파기하는 동작(igv_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()로 꺼낸다. 첫번째 인수는 비동기 에러를 꺼내는 사용자 컨텍스트를 지정한다. 만약 비동기 에러가 존재하는 경우, 두번째 인수에 지정한 structibv_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_sync_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 주소를 사용하므로, 지는 듯한 기분은 지울 수 없다.

results matching ""

    No results matching ""