GPU Direct RDMA 소개

  • by

Infiniband/RDMA 프로그래밍 기본 개념

InfiniBand는 이더넷과 동일하게 근거리 통신망에 고속 네트워크 전송을 수행하는 단말과 네트워크 장치을 위한 데이터 링크 계층의 네트워크 규격이다. 현재 InfiniBand Trade Association이 관리하고 있다.
데이터 전송에 있어 높은 throughput 과 낮은 latency가 특징인 InfiniBand는 원격 노드의 메모리 어드레스를 지정해서 데이터를 기록하고 읽어오는(RDMA WRITE/READ) RDMA 기능이 있다. InfiniBand는 송신처 프로그램이 malloc() 이나 mmap()으로 확보한 메모리를 수신처 프로그램의 메모리에 직접 전달 할 수 있다. 이 사이에 불필요한 데이터 복사가 발생하지 않는 Zero Copy 기능 때문에 낮은 latency가 가능하다.
InfiniBand는 OSI 참조 모델에서 물리계층부터 트랜스포트 계층까지를 일괄 제공하고 있다. InfiniBand 사용자와의 접점이 되는 트랜스포트 계층은 신뢰성 있음 (reliable)과 신뢰성 없음 (unreliable), 연결(Connection)과 데이터그램(Datagram)으로 구분되어, Reliable Connection (RC), Reliable Datagram (RD), Unreliable Datagram (UD), Unreliable Connection (UC)의 4가지 서비스 유형을 가지고 있다.
Reliable은 데이터 에러시에 자동적으로 재전송하고 리커버리 하는 기능이 있는 서비스이다.
Unreliable에는 리커버리 기능이 없다. 다만 Unreliable이라도 UC는 데이터 손실을 검출하는 기능이 존재한다. UD는 데이터 손실 검출도 스스로 해야 할 필요가 있다. Connection은 정해진 2개의 QP 사이에서 1대1통신을 한다. Datagram은 통신 때마다 대상을 지정하고, 여러 QP에 보내고, 여러 QP에서부터 수신 가능하다. 즉,다대다 통신을 한다.

Zero Copy와 RDMA

일반적인 TCP 통신 모델은 send()로 송신 버퍼에 데이터를 쓰면, NIC는 자동적으로 통신 상대에 데이터를 전송하고, 통신 상대로부터 전송된 데이터는 수신 버퍼에 저장되며, 프로그램이 recv()로 그것을 가져오게 된다. UDP는 일대일 통신 모델은 아니지만, 송신,수신 버퍼가 있어 sendmsg(), recvmsg()가 있는 점에서는 같다. TCP와 UDP 모두 송수신 하고자 하는 데이터를 일단 커널의 송신/수신 버퍼에 복사할 필요가 있다.
InfiniBand 경우, 송신측은 사용자 프로그램이 malloc()이나 mmap()으로 확보한 메모리를 송신 타겟으로 지정한다. 이는 수신측도 마찬가지 이다. 하지만, 실제 패킷 송수신을 수행하는 것은 HCA이다. HCA는 PCI express 버스를 통해서 물리 메모리 어드레스에 DMA 전송하므로, 사용자 프로그램의 가상 메모리 어드레스 공간을 이해할 수 없다. InfiniBand 프로그램에서는 데이터를 송수신하기 전에 자신의 프로그램 중 HCA가 액세스 가능한 영역을 Memory Region으로 OS에 등록한다. OS에 등록된 memory region은 가상 메모리 페이지와 물리 메모리 페이지의 관계를 핀으로 고정한다. 따라서 memory region 내의 가상 메모리는 스왑 등으로 디스크로 밀려나는 일이 없어지게 된다.
또한 memory region 등록 시에 가상 메모리 페이지와 물리 메모리 페이지의 변환표를 작성하고, 그것을 HCA에 전달하여 HCA가 사용자 프로그램의 가상 메모리 어드레스 공간의 일부를 인식할 수 있게 된다. HCA는 송신시에도 수신시에도 이 변환표를 참조하여 실제의 DMA 액세스 해야할 물리 메모리 어드레스를 결정할 수 있다. 따라서 호스트의 CPU를 사용하지 않고 데이터 전송이 가능하게 된다.

Infiniband/RDMA 통신 매카니즘

가상 메모리 페이지와 물리 페이지의 변환표 메커니즘을 사용하는 것으로, InfiniBand는 리모트 사용자 프로그램의 어드레스를 직접 지정해서 데이터를 복사하는 Remote Data Memory Address (RDMA)를 실행할 수 있다. RDMA는 리모트 노드에 데이터를 쓰는 것이 RDMA WRITE이며, 리모트 노드에서 데이터를 읽어 들이는 것이 RDMA READ가 된다. 이때 RDMA는 메모리 노드의 CPU를 일절 통하지 않고 해당 메모리 전송을 수행한다. (다만, RDMA WRITE는 데이터를 다 쓰고난 타이밍을 통지하는 기능이 있다).

Queue Pair

TCP나 UDP에서는 소켓이 통신의 종단점이었지만, InfiniBand에서는 Queue Pair (QP)가 그 역할을 한다. TCP와 UDP는 하나의 네트워크 인터페이스 내에 각각의 포트 번호(1~65535)에 할당된 소켓을 작성할 수 있었지만, InfiniBand는 이론상 HCA 마다 최대 224개의 QP를 만들 수 있다.
소켓이 송신 버퍼(send buffer)와 수신 버퍼(receive buffer)를 갖고 있는 것과 같이, QP는 Send Queue (SQ)와 Receive Queue (RQ)를 가지고 있다. 따라서 Queue Pair는 SQ와 RQ 2가지를 쌍으로 가지고 있는 것에서 유래한다.

Infiniband 송/수신과 Queue Pair

소켓 통신 버퍼와 수신 버퍼는 데이터 자체를 저장하는 링 버퍼이지만, RQ와 SQ는 송신 요구와 수신 요구를 저장하는 First-In Fir-Out (FIFO) 의 Queue 이다. 송신/수신 요구는 Work Request (WR)이라고 불린다. 송신 요구는 Send WR이며, 수신 요구는 Receive WR이다. 양쪽 모두 memory region 내의 메모리 영역이나, 송수신에 관련된 세세한 파라미터를 가지고 있는 구조체 이다. 그리고 Send WR은 ibv_post_send()를 이용해 SQ에, Receive WR은 ibv_post_recv()를 이용해 RQ에 쌓는다. SQ 또는 RQ에 쌓인 WR은 Work Queue Element (WQE)로 관리 된다. WR도 WQE도 개념적으로는 차이가 없다.
HCA는 SQ의 WQE 정보에 따라 데이터 송신을 시작한다. 이 처리는 사용자 프로그램의 ibv_post_send()를 호출하면 비동기적으로 이루어진다. 수신한 데이터는 RQ의 WQE를 처음부터 꺼내서 그 중에 지정된 메모리 영역에 기록한다. 이 처리도 사용자 프로그램의 ibv_post_recv()도 비동기적으로 이루어진다.

Completion Queue(CQ)

InfiniBand는 WR 처리가 끝난 것을 완료(completion)이란 개념으로 인식한다. Send WR이나 Receive WR이 성공으로 처리가 끝난 경우 성공적 완료(Successful Completion)이고, 처리가 에러로 끝난 경우에는 완료 에러(Completion Error)가 된다.
완료는 QP 외부에서 Completion Queue (CQ)로 불리는 데이터 구조에 쌓인다. 이것도 FIFO이다. 송신과 수신 WQE가 처리되는 경우는, 각각 설정된 CQ에 Completion Queue Entry (CQE)가 쌓인다.
원칙적으로, 1개의 WQE에 대해 CQE는1개가 쌓인다. 사용자는 CQ에 ibv_poll_cq()를 통해, 쌓인 CQE를 꺼낼 수 있다. 이것이 WR을 처리한 결과가 된다. 꺼내진 CQE는 Work Completion (WC)로 불린다. CQE도 WE도 개념적으로는 거의 차이가 없다.

송수신 완료 인식 - Completion Queue

프로그램이 정기적으로 ibv_poll_cq()을 실행해서 완료를 기다리는 것 이외에, 이 후에 언급할 completion channel을 사용해서 완료를 인터럽트로 감지하는 방법이 있다.

Work Request와 Work Completion

Send WR 이나 Receive WR은 아래와 같은 데이터 구조를 가진다.

Field Type Description
wr_id uint64_t 프로그램이 임의의 값을 포함할 수 있는 필
sg_list struct ibv_sge Scatter/gather 리스트 배열로의 포인터
num_sge int Scatter/gather 리스트 배열의 구성요소 수
opcode ibv_wr_opcode Send WR에만 존재하며, 추후 설명할 통신 방법의 종류를 지정
imm_data uint32_t Send WR에만 존재하며, 추후 설명할 통신 방법의 일부에서 이용

WR의 버퍼는 sg_list와 num_sge를 사용해 Scatter/Gather리스트로 지정한다. S/G리스트는 memory region 내의 연속한 영역을 하나의 S/G항목으로 하고,그 S/G 항목을 여러개 늘어놓을 수 있다. 따라서 사용자 프로그램 내에서 연속되지 않은 메모리 영역을 한번에 송신하거나, 역으로 연속되지 않은 메모리 영역에 데이터를 수신하는 것이 복사 없이 가능하다.

Scatter/Gather리스트 지정

Work Completion은 아래와 같은 데이터 구조이다.

Field Type Description
wr_id uint64_t WR의 wr_id 복사본
status enum
ibv_wc_status
성공/에러 결과값. 에러인 경우에는 에러 유형도 보고
opcode enum
ibv_wc_opcode
WR의 통신 방식의 종류
byte_len uint32_t Receive WR에 대한 완료가 성공한 경우, 전송한 데이터 크기가 들어감
imm_data uint32_t 통신 방식의 일부에서 이용하는 데이터
qp_num uint32_t WR이 소속되어 있는 QP를 QP번호로 보고

Unreliable 서비스인 UD의 경우, HCA가 Send WQE를 꺼내 그것을 패브릭에 던지면 완료된다. 그대로 송신 CQ에 CQE가 쌓인다. 데이터가 수신측에 도착하는지는 확인하지 않는다.

Unreliable 서비스의 Work Completion 과정

Reliable 서비스인 RC의 경우, 리모트에서 ACK가 도착하고, 그것을 로컬 HCA가 수신한 시점에 완료가 된다. ACK을 받지 못한 경우, 일정 규칙에 따라 로컬에서 재전송을 반복한다. 재전송을 반복하여 한계에 도달한 경우, 타임아웃이 발생한다. 이 경우, 로컬은 완료에러로 송신 CQ에 CQE를 쌓는다.

Reliable 서비스의 Work Completion 과정

RC가 타임아웃하는 패턴은 2종류가 있다. 한가지는 패브릭에 장애가 발생하고, 통신 불능이 된 경우이다. 또 한가지는 리모트가 RQ에 쌓아야 하는 WQE를 끊어버린 경우이다. 이 때 리모트는 수신 준비가 완료되지 않은 RNR(Receiver Not Ready)라는 상태의 Negative ACK (NAK)를 로컬에 돌려준다. RNR NAK를 받은 로컬은 일정 시간을 두고 재전송 하지만, 규정 횟수의 재전송이 실패한 경우에는 타임 아웃 처리 한다.
TCP의 경우에는 수신측이 ibv_post_recv()를 실행하지 않기 때문에, 수신 버퍼가 넘치는 경우가 있어도, 네트워크에 연결되어 있다면, 송신측은 계속 기다려 준다. 하지만 InfiniBand에서는 네트워크가 완전한 상태라도, 리모트가 빨리 처리하지 않으면, 타임아웃 에러가 될 수 있다.

Completion Channel

송신이나 수신 완료는 ibv_poll_cq()를 사용하여 CQ를 체크하는 것으로 수행된다고 썼으나, 이것은 일반적으로 폴링(polling)이라 불리우는 방식으로, 완료를 기다리고 있는 동안에도 CPU 파워를 낭비한다. 폴링 방식과는 별도로, 다른 스레드에게 CPU 사용권을 대기리스트에 넣고, 완료 이벤트가 발생한 시점에 깨어나는 인터럽트 방식이 존재 한다. 여기에는 Completion Channel을 사용한다.
Completion channel은 ibv_create_comp_channel()로 만들어진다. CQ는 ibv_create_cq()로 작성할 때는 이미 존재하고 있는 completion channel을 한 개 지정할 수 있다. 일대다 관계이므로, 여러 개의 CQ를 단일 completion channel에 할당하는 것도 가능하다.
Completion channel은 ibv_get_cq_event()를 호출하면, 완료 이벤트가 발생한 CQ를 불러 낼 수 있다. 완료 이벤트가 발생하지 않은 CQ가 없는 경우, 자신이 관리하고 있는 CQ 중 어느 하나에 완료 이벤트가 발생할 때 까지 커널 내에 대기하게 된다.
Completion channel에는 조금 더 기교적인 사용 방법이 있는데, select()나 poll()으로 기다릴 수 있다.