IB Session

Download

현재는 라이브러리를 외부에 공개하지 않으며 InfoLab의 일원에게만 배포하고 있습니다.

Strength

  • InfiniBand에 대한 지식이 없어도 높은 성능을 활용할 수 있다.

  • Socket API와 굉장히 유사하여 배우기가 쉽다.

  • 현존하는 InfiniBand wrapping library 중에 가장 성능이 뛰어나다.

  • 다중 쓰레드 환경에서도 조심해서 짜놓으면 잘 돌아간다.

Basic Concept

세션이라는 개념을 씁니다. 세션은 오브젝트로 생성되며, 생성되는 즉시 원격지와의 연결을 시도합니다. 생성된 세션끼리는 서로 독립적이며 간섭하지 않습니다.

기본적으로 다음과 같은 순서가 지켜져야 합니다.

  1. 세션 오브젝트를 만든다.

  2. 메모리를 할당하고, 이것을 등록한다.

  3. 송/수신을 진행한다.

How to import

모든 실행 환경은 CentOS와 gcc기준입니다.

라이브러리 없이 코드로만 이용하는 경우

ib_session.hib_session.cpp파일을 받으시고, 다음과 같이 파일을 include 해주세요.

#include "ib_session.h"

gcc로 컴파일 하실때, -libverbs--std=c++11 옵션을 넣어주시면 됩니다.

동적 라이브러리를 이용하는 경우

현재 지원되지 않습니다. (구현중입니다)

Functions

Constructor
ib_session::ib_session(const size_t ib_device_num, const int ib_physical_port_num, const char* dest_ip_addr, const uint16_t ip_port, const bool is_responder)

인피니밴드 세션의 생성자입니다. 생성자에서 인피니밴드 카드의 초기화 및 상대방 호스트와의 연결과정까지 모두 처리합니다. 세션은 1:1로만 연결됩니다 (1:N, N:N 불가)

ib_device_num

인피니밴드 카드의 번호입니다. 현재 연구실 시스템에는 각 컴퓨터마다 1개씩 카드가 장착되어 있으니, 0을 넣으시면 됩니다.

ib_physical_port_num

인피니밴드 카드가 가진 물리적 포트의 번호입니다. 1에서 3 사이의 숫자가 들어갑니다. 현재 연구실 시스템의 인피니밴드 카드는 포트가 1개이니, 1을 넣으시면 됩니다.

dest_ip_addr

세션을 만들 상대방의 인피니밴드 카드의 아이피입니다. requester에만 해당되는 사항이며, responder의 경우 nullptr를 넣으면 됩니다.

ip_port

1024에서 65535 사이의 포트번호를 넣어줍니다. (0부터 1023까지는 시스템에 예약되어 있음) 상대방 호스트와 동일한 포트번호를 입력하셔야 합니다.

is_responder

true면 responder가 되고, false면 requester로 작동합니다. 두 개의 호스트 사이에 반드시 한 쪽은 requester이고, 반대쪽은 responder여야 합니다.

Register Memory
int ib_session::mem_reg(void* host_mem_addr, const size_t host_mem_size)

인피니밴드가 이용할 수 있도록 메인 메모리에 미리 할당된 주소와 크기를 등록합니다. 등록된 메모리는 보조기억장치로 swap되지 않습니다.

host_mem_addr

메인 메모리에 malloc()등으로 할당된 메모리의 시작 주소입니다.

host_mem_size

메인 메모리에 malloc()등으로 할당된 메모리의 크기입니다.

성공하면 인피니밴드에 등록한 메모리에 대한 handle이 반환됩니다. handle의 값은 int로, 가장 먼저 등록한 메모리가 0, 그 다음이 1과 같은 식으로 0부터 시작하여 순차적으로 올라가게끔 부여됩니다. ib_session::sending(), ib_session::receiving(), ib_session::mem_dereg()와 같은 함수들은 모두 handle을 이용해 처리합니다. 실패하면 -1을 반환합니다.

Deregister Memory
void ib_session::mem_dereg(const size_t host_mem_handle)

인피니밴드가 더이상 사용할 수 없도록 메모리를 해제합니다.

host_mem_handle

ib_session::mem_reg() 함수로 등록한 메모리의 핸들입니다.

리턴값은 없습니다.

Send Message
int ib_session::sending(const size_t host_mem_handle, const size_t host_mem_offset, const size_t message_size)

등록한 메모리에서, 지정한 오프셋부터 특정 사이즈만큼의 내용을 원격 호스트로 보냅니다.

host_mem_handle

등록한 메모리의 핸들입니다.

host_mem_offset

등록한 메모리 내부에서 어디서부터 읽어올 지 결정하는 오프셋입니다. 예를 들어 시작 주소가 0x100이고 오프셋이31(십진법)이면 두 개를 더한 0x11F부터 읽게 됩니다.

message_size

송신할 메시지 사이즈입니다.

성공하면 0이, 실패하면 -1이 반환됩니다. 비동기화가 구현되어있지 않습니다. 따라서 함수의 실행이 끝날때까지 다음 라인으로 넘어가지 않습니다.

Receive Message
int ib_session::receiving(const size_t host_mem_handle, const size_t host_mem_offset, const size_t message_size)

등록한 메모리에서, 원격 호스트가 보낸 메시지를 지정한 오프셋부터 특정 사이즈만큼 받습니다.

host_mem_handle

등록한 메모리의 핸들입니다.

host_mem_offset

등록한 메모리 내부에서 어디서부터 써 넣을지 결정하는 오프셋입니다. 예를 들어 시작 주소가 0x100이고 오프셋이 16(십진법)이면 두 개를 더한 0x110부터 쓰게 됩니다.

message_size

수신할 메시지 사이즈입니다.

성공하면 0이, 실패하면 -1이 반환됩니다. 비동기화가 구현되어있지 않습니다. 따라서 함수의 실행이 끝날때까지 다음 라인으로 넘어가지 않습니다.

Thread-Safe & Synchronize to Remote Host

이전에도 언급했듯, 해당 라이브러리는 현재 쓰레드-안전과 원격지와의 연결 순서에 조금 문제가 있습니다. 우선 추천드리는 방법은 다음과 같습니다.

  • 방법 1: 미리 ib_session 오브젝트들을 쓰레드 개수만큼 메인 루틴에서 생성한다. 생성 순서는 직접 설정해주어야 하는데, 순서가 맞지 않으면 엉뚱한 세션과 연결되어버린다. 그 다음 쓰레드를 생성하고, 만들어진 오브젝트의 참조를 넘겨 쓰레드 내부에서 ib_session::sending()등의 함수를 실행한다. RAII가 되어 있으므로 오브젝트의 제거는 쓰레드나 메인 루틴에서 신경쓰지 않아도 된다. (스택과 힙에서 메모리 누수가 없다)

쓰레드가 전환되기 때문에, 인피니밴드에서 오는 MSI-X 인터럽트가 제대로 처리되지 못해 큰 병목이 생깁니다!

(쓰레드 전환을 방지하시려면 Thread Affinity 관련 함수를 사용하십시요)

(테스트 결과 최대 전송 성능의 절반정도 나옵니다.)

Node 1 (Master)
void test_thread(ib_session& session, int& handle) {
    session.sending(handle, 0, message_size);
}

int main() {
    ib_session s_1(0, 1, nullptr, 8282, true);
    ib_session s_2(0, 1, nullptr, 8282, true);

    size_t message_size = 12;

    uint8_t* buffer_1 = (uint8_t*)malloc(message_size);
    uint8_t* buffer_2 = (uint8_t*)malloc(message_size);

    int handle_1 = s_1.mem_reg(buffer_1);
    int handle_2 = s_2.mem_reg(buffer_2);

    auto t_1 = std::async(std::launch::async, [&]{ test_thread(s_1, handle_1); } );
    auto t_2 = std::async(std::launch::async, [&]{ test_thread(s_2, handle_2); } );

    t_1.get();
    t_2.get();

    free(buffer_1);
    free(buffer_2);

    return 0;
}
Node 2 (Slave)
void test_thread(ib_session& session, int& handle) {
    session.receiving(handle, 0, message_size);
}

int main() {
    ib_session s_1(0, 1, "172.30.1.17", 8282, false);
    ib_session s_2(0, 1, "172.30.1.17", 8282, false);

    size_t message_size = 12;

    uint8_t* buffer_1 = (uint8_t*)malloc(message_size);
    uint8_t* buffer_2 = (uint8_t*)malloc(message_size);

    int handle_1 = s_1.mem_reg(buffer_1);
    int handle_2 = s_2.mem_reg(buffer_2);

    auto t_1 = std::async(std::launch::async, [&]{ test_thread(s_1, handle_1); } );
    auto t_2 = std::async(std::launch::async, [&]{ test_thread(s_2, handle_2); } );

    t_1.get();
    t_2.get();

    free(buffer_1);
    free(buffer_2);

    return 0;
}
  • 방법 2: 단 하나의 ib_session 오브젝트를 생성하고, 전송에 필요한 모든 메모리를 등록한다. 통신을 담당하는 단 하나의 쓰레드를 생성하고, 송수신 순서를 일일이 손으로 정해준다. 순서가 틀리면 다른 메모리에 내용이 전달되어 버린다.

현재 반복 전송에서 가장 높은 전송 성능을 보여줍니다.

Node 1 (Master)
void test_thread(ib_session& session, int& handle_1, int& handle_2) {
    session.sending(handle_1, 0, message_size);
    session.sending(handle_2, 0, message_size);
}

int main() {
    ib_session s(0, 1, nullptr, 8282, true);

    size_t message_size = 12;

    uint8_t* buffer_1 = (uint8_t*)malloc(message_size);
    uint8_t* buffer_2 = (uint8_t*)malloc(message_size);

    int handle_1 = s.mem_reg(buffer_1);
    int handle_2 = s.mem_reg(buffer_2);

    auto t = std::async(std::launch::async, [&]{ test_thread(s, handle_1, handle_2); } );
    t.get();

    free(buffer);

    return 0;
}
Node 2 (Slave)
void test_thread(ib_session& session, int& handle_1, int& handle_2) {
    session.receiving(handle_1, 0, message_size);
    session.receiving(handle_2, 0, message_size);
}

int main() {
    ib_session s(0, 1, "172.30.1.17", 8282, false);

    size_t message_size = 12;

    uint8_t* buffer_1 = (uint8_t*)malloc(message_size);
    uint8_t* buffer_2 = (uint8_t*)malloc(message_size);

    int handle_1 = s.mem_reg(buffer_1);
    int handle_2 = s.mem_reg(buffer_2);

    auto t = std::async(std::launch::async, [&]{ test_thread(s, handle_1, handle_2); } );
    t.get();

    free(buffer);

    return 0;
}

이 방법 이외에는 추천하지 않습니다. malloc()의 위치는 쓰레드 안이든, 밖이든 상관이 없습니다. 반드시 송수신 되기 전에 할당된 메모리가 등록되어있고, 그것의 핸들을 알고있으면 됩니다.