soobook
GPU

NIC, RDMA, and InfiniBand

GPU 서버가 서로 통신할 때 NIC, RDMA, InfiniBand가 맡는 역할

InfiniBand 이해해보기

NVLink와 NVSwitch는 한 서버 안의 GPU들이 빠르게 통신하는 경로를 설명한다.

서버 밖으로 나가면 좀 다른데, 이제 데이터는 GPU package와 PCIe slot을 지나 network adapter로 가고, switch를 거쳐 다른 서버의 adapter로 들어간다.

Multi-node training에서는 이 경로가 자주 병목이 된다.

한 서버 안에서는 NVLink가 gradient나 activation 이동을 줄여주더라도, 서버 사이에서는 network가 같은 일을 맡아야 한다.

{
  "diagram": "html-diagram",
  "title": "Node-to-node path",
  "description": "서버 밖으로 나가는 GPU data는 NIC/HCA와 network fabric을 지난다.",
  "width": 960,
  "height": 512,
  "mobileWidth": 900,
  "regions": [
    {
      "id": "server-a",
      "label": "GPU server A\\nlocal machine",
      "x": 36,
      "y": 42,
      "width": 344,
      "height": 428
    },
    {
      "id": "fabric",
      "label": "network fabric",
      "x": 410,
      "y": 88,
      "width": 140,
      "height": 336
    },
    {
      "id": "server-b",
      "label": "GPU server B\\nremote machine",
      "x": 580,
      "y": 42,
      "width": 344,
      "height": 428
    }
  ],
  "nodes": [
    {
      "id": "a-gpu",
      "kind": "gpu",
      "label": "GPU\\nHBM",
      "x": 130,
      "y": 150,
      "width": 108,
      "height": 66
    },
    {
      "id": "a-cpu",
      "kind": "host",
      "label": "CPU\\nHost RAM",
      "x": 292,
      "y": 150,
      "width": 118,
      "height": 66
    },
    {
      "id": "a-nic",
      "kind": "nic",
      "label": "NIC / HCA\\nnetwork adapter",
      "x": 212,
      "y": 324,
      "width": 154,
      "height": 72
    },
    {
      "id": "ib-switch",
      "kind": "switch",
      "label": "InfiniBand\\nSwitch",
      "x": 480,
      "y": 256,
      "width": 118,
      "height": 72
    },
    {
      "id": "b-gpu",
      "kind": "gpu",
      "label": "GPU\\nHBM",
      "x": 668,
      "y": 150,
      "width": 108,
      "height": 66
    },
    {
      "id": "b-cpu",
      "kind": "host",
      "label": "CPU\\nHost RAM",
      "x": 830,
      "y": 150,
      "width": 118,
      "height": 66
    },
    {
      "id": "b-nic",
      "kind": "nic",
      "label": "NIC / HCA\\nnetwork adapter",
      "x": 748,
      "y": 324,
      "width": 154,
      "height": 72
    }
  ],
  "links": [
    {
      "id": "a-gpu-nic",
      "points": [[130, 183], [130, 244], [177, 324]],
      "flow": {
        "speed": "normal",
        "emphasis": "soft"
      }
    },
    {
      "id": "a-cpu-nic",
      "points": [[292, 183], [292, 244], [247, 324]],
      "flow": {
        "speed": "normal",
        "emphasis": "soft"
      }
    },
    {
      "id": "a-nic-switch",
      "points": [[289, 324], [410, 256]],
      "flow": {
        "speed": "fast",
        "count": 2,
        "emphasis": "strong"
      }
    },
    {
      "id": "switch-b-nic",
      "points": [[550, 256], [671, 324]],
      "flow": {
        "speed": "fast",
        "count": 2,
        "emphasis": "strong"
      }
    },
    {
      "id": "b-nic-gpu",
      "points": [[713, 324], [668, 244], [668, 183]],
      "flow": {
        "speed": "normal",
        "emphasis": "soft"
      }
    },
    {
      "id": "b-nic-cpu",
      "points": [[783, 324], [830, 244], [830, 183]],
      "flow": {
        "speed": "normal",
        "emphasis": "soft"
      }
    }
  ],
  "labels": [
    {
      "text": "PCIe 내부 경로",
      "x": 142,
      "y": 260
    },
    {
      "text": "DMA 대상\\nhost memory 또는 GPU memory",
      "x": 292,
      "y": 250
    },
    {
      "text": "서버 간 traffic",
      "x": 480,
      "y": 188
    },
    {
      "text": "remote node",
      "x": 748,
      "y": 410
    }
  ]
}

NIC

NIC는 Network Interface Card의 약자다. Ethernet NIC도 있고 InfiniBand adapter도 있다. InfiniBand 문맥에서는 NIC 대신 HCA, Host Channel Adapter라는 표현을 자주 쓴다.

Ethernet NIC card with network ports

일반 PC나 서버에서 볼 수 있는 Ethernet NIC는 위 이미지처럼 RJ45 port가 달린 카드 형태가 많다. GPU server나 InfiniBand cluster에서 쓰는 HCA는 RJ45 대신 QSFP, OSFP 같은 고속 port를 쓰는 경우가 많지만, “서버를 network에 붙이는 adapter”라는 역할은 같다.

NIC의 역할은 단순히 cable을 꽂는 포트가 아니다. Packet을 보내고 받고, DMA engine을 통해 host memory와 데이터를 주고받고, queue에 올라온 work request를 처리한다. CPU가 모든 byte를 직접 옮기는 것이 아니라, CPU는 주로 준비와 제어를 맡고 실제 data movement는 NIC와 memory subsystem이 맡는다.

GPU server에서 NIC는 보통 PCIe에 붙는다. 그래서 network 성능을 볼 때도 PCIe topology가 같이 나온다. NIC가 어느 CPU socket 아래에 있는지, GPU와 같은 PCIe root complex를 공유하는지, NUMA 경로가 어떻게 되는지에 따라 host-to-network 또는 GPU-to-network 경로가 달라진다.

RDMA

RDMA는 Remote Direct Memory Access의 약자다.

remote machine의 memory와 데이터를 주고받을 때 CPU와 kernel의 per-packet 개입을 줄이기 위한 방식이다.

쉽게 말하면 이름 그대로 “원격에서 직접 메모리에 액세스하겠다” 라는 방식이다.

일반적인 TCP/IP receive path에서는 packet이 NIC로 들어온 뒤 kernel network stack을 거치고, kernel buffer와 application buffer 사이의 copy가 발생한다.

Packet이 많아지면 copy, context switch, interrupt 처리 비용이 커진다.

RDMA는 application이 미리 등록한 memory region을 NIC가 알고 있게 만든다. 이후 NIC는 들어온 data를 해당 buffer에 직접 놓을 수 있다.

CPU와 kernel이 사라지는 것은 아니다.

Connection setup, memory registration, protection, queue 관리에는 여전히 OS와 driver가 필요하다.

다만 data path에서 kernel copy와 CPU 개입이 크게 줄어든다.

{
  "diagram": "html-diagram",
  "title": "Receive path",
  "description": "일반 TCP/IP receive path와 RDMA receive path의 차이.",
  "width": 960,
  "height": 546,
  "mobileWidth": 900,
  "regions": [
    {
      "id": "tcp-region",
      "label": "TCP/IP receive\\nkernel data path",
      "x": 36,
      "y": 46,
      "width": 408,
      "height": 454
    },
    {
      "id": "rdma-region",
      "label": "RDMA receive\\nregistered buffer",
      "x": 516,
      "y": 46,
      "width": 408,
      "height": 454
    }
  ],
  "nodes": [
    {
      "id": "tcp-wire",
      "kind": "note",
      "label": "Network\\npacket",
      "x": 100,
      "y": 150,
      "width": 94,
      "height": 60
    },
    {
      "id": "tcp-nic",
      "kind": "nic",
      "label": "NIC",
      "x": 226,
      "y": 150,
      "width": 80,
      "height": 58
    },
    {
      "id": "tcp-kernel",
      "kind": "host",
      "label": "Kernel\\nnetwork stack",
      "x": 368,
      "y": 150,
      "width": 120,
      "height": 66
    },
    {
      "id": "tcp-kbuf",
      "kind": "memory",
      "label": "Kernel\\nbuffer",
      "x": 346,
      "y": 310,
      "width": 116,
      "height": 64
    },
    {
      "id": "tcp-app",
      "kind": "memory",
      "label": "App\\nbuffer",
      "x": 226,
      "y": 430,
      "width": 116,
      "height": 64
    },
    {
      "id": "rdma-wire",
      "kind": "note",
      "label": "Network\\npacket",
      "x": 586,
      "y": 150,
      "width": 94,
      "height": 60
    },
    {
      "id": "rdma-nic",
      "kind": "nic",
      "label": "NIC / HCA",
      "x": 720,
      "y": 150,
      "width": 112,
      "height": 58
    },
    {
      "id": "rdma-app",
      "kind": "memory",
      "label": "Registered\\nApp buffer",
      "x": 842,
      "y": 310,
      "width": 132,
      "height": 68
    },
    {
      "id": "rdma-control",
      "kind": "host",
      "label": "OS / Driver\\nsetup path",
      "x": 704,
      "y": 430,
      "width": 130,
      "height": 66
    }
  ],
  "links": [
    {
      "id": "tcp-wire-nic",
      "points": [[147, 150], [186, 150]],
      "flow": {
        "speed": "normal",
        "emphasis": "normal"
      }
    },
    {
      "id": "tcp-nic-kernel",
      "points": [[266, 150], [308, 150]],
      "flow": {
        "speed": "normal",
        "emphasis": "normal"
      }
    },
    {
      "id": "tcp-kernel-kbuf",
      "points": [[368, 183], [368, 278]],
      "flow": {
        "speed": "slow",
        "emphasis": "soft"
      }
    },
    {
      "id": "tcp-copy",
      "points": [[314, 310], [258, 390], [226, 398]],
      "dashed": true,
      "flow": {
        "speed": "slow",
        "emphasis": "muted"
      }
    },
    {
      "id": "rdma-wire-nic",
      "points": [[633, 150], [664, 150]],
      "flow": {
        "speed": "fast",
        "emphasis": "strong"
      }
    },
    {
      "id": "rdma-direct",
      "points": [[776, 175], [842, 276]],
      "flow": {
        "speed": "fast",
        "count": 2,
        "emphasis": "strong"
      }
    },
    {
      "id": "rdma-control-path",
      "points": [[704, 397], [704, 245], [776, 175]],
      "dashed": true,
      "flow": false
    }
  ],
  "labels": [
    {
      "text": "kernel 처리",
      "x": 362,
      "y": 230
    },
    {
      "text": "copy to user buffer",
      "x": 178,
      "y": 354
    },
    {
      "text": "data path가\\napp buffer로 직접 향함",
      "x": 834,
      "y": 220
    },
    {
      "text": "control path는 남음",
      "x": 650,
      "y": 334
    }
  ]
}

RDMA를 쓰려면 memory registration이 필요하다. NIC가 임의의 user memory에 마음대로 쓰면 안 되기 때문이다.

Application은 사용할 buffer를 등록하고, driver와 NIC는 그 memory region을 식별할 key와 address 정보를 관리한다.

이 때문에 RDMA는 “빠른 socket” 정도로 보면 부족하다. NIC, driver, memory registration, queue pair, completion queue 같은 요소가 같이 움직이는 통신 모델이다.

InfiniBand

InfiniBand는 high-performance interconnect다.

IBTA는 InfiniBand를 server, storage, embedded system 등을 연결하는 input/output architecture specification으로 설명한다.

구조적으로는 switched point-to-point channel을 사용하고, reliable messaging과 RDMA semantics를 제공한다.

Ethernet이 범용 network의 기본값이라면, InfiniBand는 HPC와 AI cluster에서 낮은 latency와 높은 bandwidth를 목표로 설계된 fabric에 가깝다.

여기서 fabric은 cable 하나가 아니라 HCA, switch, link, routing, subnet manager, QoS 정책이 합쳐진 network 전체를 말한다.

{
  "diagram": "html-diagram",
  "title": "InfiniBand fabric",
  "description": "여러 GPU server가 HCA와 switch를 통해 하나의 low-latency fabric에 붙는다.",
  "width": 960,
  "height": 520,
  "mobileWidth": 900,
  "regions": [
    {
      "id": "fabric-region",
      "label": "InfiniBand fabric\\nHCA, switch, routing, QoS",
      "x": 44,
      "y": 44,
      "width": 872,
      "height": 430
    }
  ],
  "nodes": [
    {
      "id": "spine-a",
      "kind": "switch",
      "label": "Spine\\nSwitch",
      "x": 360,
      "y": 128,
      "width": 122,
      "height": 66
    },
    {
      "id": "spine-b",
      "kind": "switch",
      "label": "Spine\\nSwitch",
      "x": 600,
      "y": 128,
      "width": 122,
      "height": 66
    },
    {
      "id": "leaf-a",
      "kind": "switch",
      "label": "Leaf\\nSwitch",
      "x": 320,
      "y": 270,
      "width": 122,
      "height": 66
    },
    {
      "id": "leaf-b",
      "kind": "switch",
      "label": "Leaf\\nSwitch",
      "x": 640,
      "y": 270,
      "width": 122,
      "height": 66
    },
    {
      "id": "server-0",
      "kind": "nic",
      "label": "GPU Server 0\\nHCA",
      "x": 170,
      "y": 410,
      "width": 132,
      "height": 66
    },
    {
      "id": "server-1",
      "kind": "nic",
      "label": "GPU Server 1\\nHCA",
      "x": 330,
      "y": 410,
      "width": 132,
      "height": 66
    },
    {
      "id": "server-2",
      "kind": "nic",
      "label": "GPU Server 2\\nHCA",
      "x": 630,
      "y": 410,
      "width": 132,
      "height": 66
    },
    {
      "id": "server-3",
      "kind": "nic",
      "label": "GPU Server 3\\nHCA",
      "x": 790,
      "y": 410,
      "width": 132,
      "height": 66
    },
    {
      "id": "sm",
      "kind": "note",
      "label": "Subnet Manager\\nrouting 관리",
      "x": 480,
      "y": 390,
      "width": 150,
      "height": 66
    }
  ],
  "links": [
    {
      "id": "leaf-a-spine-a",
      "points": [[320, 237], [360, 161]],
      "flow": {
        "speed": "normal",
        "emphasis": "normal"
      }
    },
    {
      "id": "leaf-a-spine-b",
      "points": [[350, 237], [570, 161]],
      "flow": {
        "speed": "normal",
        "emphasis": "normal"
      }
    },
    {
      "id": "leaf-b-spine-a",
      "points": [[610, 237], [390, 161]],
      "flow": {
        "speed": "normal",
        "emphasis": "normal"
      }
    },
    {
      "id": "leaf-b-spine-b",
      "points": [[640, 237], [600, 161]],
      "flow": {
        "speed": "normal",
        "emphasis": "normal"
      }
    },
    {
      "id": "server-0-leaf-a",
      "points": [[170, 377], [285, 303]],
      "flow": {
        "speed": "fast",
        "emphasis": "strong"
      }
    },
    {
      "id": "server-1-leaf-a",
      "points": [[330, 377], [320, 303]],
      "flow": {
        "speed": "fast",
        "emphasis": "strong"
      }
    },
    {
      "id": "server-2-leaf-b",
      "points": [[630, 377], [640, 303]],
      "flow": {
        "speed": "fast",
        "emphasis": "strong"
      }
    },
    {
      "id": "server-3-leaf-b",
      "points": [[790, 377], [675, 303]],
      "flow": {
        "speed": "fast",
        "emphasis": "strong"
      }
    },
    {
      "id": "sm-leaf-a",
      "points": [[445, 390], [368, 303]],
      "dashed": true,
      "flow": false
    },
    {
      "id": "sm-leaf-b",
      "points": [[515, 390], [592, 303]],
      "dashed": true,
      "flow": false
    }
  ],
  "labels": [
    {
      "text": "uplink",
      "x": 414,
      "y": 214
    },
    {
      "text": "uplink",
      "x": 546,
      "y": 214
    },
    {
      "text": "RDMA traffic",
      "x": 248,
      "y": 346
    },
    {
      "text": "RDMA traffic",
      "x": 712,
      "y": 346
    },
    {
      "text": "routing은 fabric 전체 기준",
      "x": 480,
      "y": 468
    }
  ]
}

InfiniBand switch는 Ethernet switch와 비슷하게 여러 endpoint를 묶지만, 사용하는 protocol과 transport semantics가 다르다. RDMA, congestion control, adaptive routing, QoS 같은 기능이 fabric 운영의 일부로 들어온다.

그래서 InfiniBand를 볼 때는 “빠른 Ethernet 케이블”보다 “RDMA를 전제로 한 cluster interconnect”로 보는 편이 낫다.

RoCE

RDMA가 항상 InfiniBand 위에서만 동작하는 것은 아니다.

RoCE는 RDMA over Converged Ethernet의 약자이고, Ethernet network 위에서 RDMA를 제공한다. iWARP도 IP network 위에서 RDMA를 구현하는 방식이다.

다만 AI cluster 문맥에서는 InfiniBand와 RoCE를 구분해야 한다.

RoCE는 Ethernet ecosystem을 활용할 수 있지만, lossless에 가까운 fabric 구성, congestion control, PFC 같은 운영 이슈가 중요해진다.

InfiniBand는 RDMA fabric을 전제로 설계된 별도 interconnect다.

GPU Cluster

GPU 서버 한 대 안에서는 NVLink와 NVSwitch가 GPU memory 사이 통신을 빠르게 만든다. 서버가 여러 대로 늘어나면 NIC와 network fabric이 그 역할의 바깥쪽을 맡는다.

예를 들어 distributed training에서 node마다 여러 GPU가 있고, node 사이에서 gradient shard를 교환해야 한다고 하자. 한 node 안에서는 NCCL이 NVLink/NVSwitch를 활용할 수 있다. Node 밖으로 나가면 NCCL은 NIC와 InfiniBand 또는 RoCE fabric을 통해 collective communication을 수행한다.

여기서 중요한 점은 RDMA가 “GPU끼리 직접 HBM을 읽는다”는 뜻은 아니라는 것이다. 일반 RDMA는 host memory를 중심으로 설명하는 모델이다.

NIC가 GPU memory까지 직접 접근하는 이야기는 GPUDirect RDMA에서 다룬다.

NVIDIA의 GPUDirect RDMA 문서는 network interface 같은 third-party PCIe device와 GPU 사이에 직접 data exchange path를 만들 수 있다고 설명한다.

다만 이 경로도 PCIe root complex, driver, memory mapping 같은 제약을 가진다.

Summary

NIC는 서버를 network에 붙이는 장치다. InfiniBand adapter 문맥에서는 HCA라고 부르는 경우가 많고, PCIe를 통해 host와 연결된다.

RDMA는 remote memory와 데이터를 주고받을 때 kernel copy와 CPU 개입을 줄이는 통신 방식이다. 빠른 이유는 network가 아니라 memory movement path가 짧아지기 때문이다.

InfiniBand는 RDMA와 low-latency communication을 중심에 둔 cluster interconnect다. HCA, switch, link, subnet manager, routing policy가 합쳐져 하나의 fabric을 만든다.

References