# SR-IOV InfiniBand를 파드에 붙이기 - 확장 리소스, CNI, 그리고 세 조각의 정합성
GPU 여러 대로 분산 학습을 돌리면 노드 간 통신이 병목이 됩니다. 이더넷 대신 InfiniBand를 깔았는데, 파드 안에서는 그 인터페이스가 보이지 않습니다. 노드에는 분명히 있습니다.
컨테이너는 자기 네트워크 네임스페이스를 갖고, 기본 CNI가 붙여 주는 인터페이스 하나만 봅니다. 물리 장치를 추가로 넣으려면 스케줄링 · 네트워크 연결 · 파드 스펙 세 곳이 동시에 맞아야 합니다. 이 글에서는 그 세 조각이 각각 무엇을 하는지, 어디서 어긋나는지 정리합니다.
# 1. 세 조각
파드에 SR-IOV VF를 붙이는 데 필요한 것이 셋입니다.
| 조각 | 역할 | 실패 시 증상 |
|---|---|---|
| 디바이스 플러그인 | VF를 확장 리소스로 광고 | 파드가 Pending |
| 메타 CNI(Multus) + SR-IOV CNI | 할당된 VF를 파드 네임스페이스에 연결 | 파드는 뜨는데 인터페이스 없음 |
| 파드 스펙 | 리소스 요청 + 네트워크 애너테이션 | 둘 중 하나만 있으면 어긋남 |
세 조각이 각각 다른 이름으로 같은 것을 가리키기 때문에 어긋나기 쉽습니다. 어긋나면 오류 메시지가 원인을 직접 알려 주지 않습니다.
# 2. 디바이스 플러그인이 하는 일 - 리소스로 광고
먼저 스케줄러가 "이 노드에 VF가 몇 개 남았는지" 알아야 합니다. 이 정보는 Kubernetes가 스스로 알 수 없으므로 디바이스 플러그인이 알려 줍니다.
디바이스 플러그인 프레임워크의 성질부터 짚고 갑니다.
Extended resources are only supported as integer quantities. Devices cannot be shared between containers. Cannot be overcommitted. Requests equal limits.
정수이고, 나눌 수 없고, 오버커밋되지 않으며, request와 limit이 같습니다. GPU와 같은 규칙입니다. 이 성질이 왜 그런지와 스케줄링에 어떤 영향을 주는지는 GPU는 쪼개 쓸 수 없다 - 확장 리소스와 디바이스 플러그인 (opens new window)에 정리했습니다. SR-IOV VF도 정확히 같은 틀 위에 있습니다.
SR-IOV 네트워크 디바이스 플러그인은 호스트의 VF를 찾아 풀로 묶고 확장 리소스 이름을 만듭니다.
{
"resourceList": [
{
"resourceName": "sriov_ib",
"resourcePrefix": "example.com",
"selectors": {
"vendors": ["15b3"],
"isRdma": true,
"pfNames": ["ibp1s0f0#0-7"]
}
}
]
}
이 설정이 만드는 확장 리소스 이름은 example.com/sriov_ib입니다. resourcePrefix와 resourceName을 슬래시로 이은 것입니다. 기본 접두사가 intel.com이라는 점을 모르면 왜 그 이름이 나왔는지 헷갈립니다.
주의할 제약이 하나 있습니다.
resourceName ... must not contain special characters including hyphens
하이픈을 쓸 수 없습니다. 다른 리소스 이름 관례를 따라 sriov-ib로 적으면 그 풀이 만들어지지 않고, 노드에 리소스가 안 나타나는 상태가 됩니다.
셀렉터에서 중요한 것이 isRdma입니다.
isRdma: Mount RDMA resources
InfiniBand로 RDMA 통신을 하려면 VF의 네트워크 인터페이스만으로는 부족합니다. RDMA 장치 노드가 컨테이너 안에 마운트되어야 합니다. isRdma: true를 빼면 인터페이스는 보이는데 RDMA 동작만 안 되는 상태가 되고, 증상이 "성능이 안 나온다"로 나타나 원인 파악이 오래 걸립니다.
deviceType도 결정 지점입니다. netdevice는 커널 드라이버를 통해 일반 네트워크 인터페이스로 보이고, vfio-pci는 사용자 공간 드라이버가 직접 장치를 잡습니다. 후자는 커널 스택을 우회하지만 컨테이너 안에서 그 장치를 다룰 코드가 필요합니다. 일반적인 RDMA 라이브러리를 쓴다면 netdevice + isRdma가 맞습니다.
# 3. CNI가 하는 일 - 파드에 연결
리소스를 받았다고 인터페이스가 생기지는 않습니다. 스케줄링과 네트워크 연결은 별개입니다.
Meta-plugin (Multus or DANM): Retrieves allocated network device information of a Pod SR-IOV CNI: Configures allocated VFs during pod creation and resets them upon deletion
기본 CNI는 파드에 인터페이스 하나만 붙입니다. 두 번째 인터페이스를 붙이려면 메타 CNI가 필요하고, Multus가 그 역할을 합니다. Multus는 파드 애너테이션을 보고 추가 네트워크를 붙이는데, 무엇을 붙일지는 NetworkAttachmentDefinition이라는 커스텀 리소스에 적혀 있습니다.
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
name: ib-net
annotations:
# 이 이름이 2절의 확장 리소스 이름과 정확히 같아야 한다
k8s.v1.cni.cncf.io/resourceName: example.com/sriov_ib
spec:
config: |
{
"cniVersion": "0.3.1",
"type": "ib-sriov",
"ipam": { "type": "whereabouts", "range": "10.56.0.0/16" }
}
애너테이션의 resourceName이 연결 고리입니다. Multus가 이 값을 보고 "이 파드가 받은 example.com/sriov_ib 장치를 이 네트워크에 쓴다"고 판단합니다. 오타가 나면 파드는 정상적으로 뜨고 리소스도 할당되는데 인터페이스만 안 생깁니다.
# 4. 파드 스펙에서 둘을 함께 요청한다
마지막 조각입니다. 파드는 리소스와 네트워크를 둘 다 적어야 합니다.
apiVersion: v1
kind: Pod
metadata:
name: training-worker
annotations:
k8s.v1.cni.cncf.io/networks: ib-net # 3절의 NAD 이름
spec:
containers:
- name: trainer
image: registry.example.com/trainer:1.0
resources:
limits:
nvidia.com/gpu: 8
example.com/sriov_ib: 1 # 2절의 확장 리소스 이름
securityContext:
capabilities:
add: ["IPC_LOCK"] # RDMA 메모리 고정에 필요
셋의 이름이 서로 다른 층에 있다는 점이 이 구성의 핵심 난점입니다.
파드 애너테이션 ─ ib-net ──────────→ NetworkAttachmentDefinition
│ resourceName 애너테이션
▼
파드 resources ─ example.com/sriov_ib ─→ 디바이스 플러그인 설정
(resourcePrefix + resourceName)
애너테이션만 있고 리소스 요청이 없으면 VF가 할당되지 않아 CNI가 붙일 장치를 못 찾습니다. 리소스만 있고 애너테이션이 없으면 VF는 잡아 두고 아무 데도 안 씁니다. 후자가 더 나쁩니다. 오류 없이 VF만 소모되어 다른 파드가 스케줄되지 못합니다.
IPC_LOCK도 자주 빠지는 항목입니다. RDMA는 메모리를 페이지 아웃되지 않게 고정해야 하는데, 이 권한이 없으면 등록이 실패합니다. 컨테이너가 뜨고 인터페이스도 보이는데 통신 초기화에서 실패하는 형태입니다.
# 5. NUMA 정렬이 성능을 가른다
여기까지 하면 동작은 합니다. 그런데 기대만큼 안 빠른 경우가 있습니다.
디바이스 플러그인은 장치의 토폴로지 정보를 kubelet에 넘길 수 있습니다.
Device plugins support NUMA-aware allocation through the Topology Manager via
TopologyInfoin device metadata, enabling topology-aware device scheduling.
멀티 소켓 노드에서 GPU와 NIC이 서로 다른 NUMA 노드에 붙어 있으면, 데이터가 소켓 간 링크를 한 번 더 건너갑니다. 8-GPU 노드에서 이 차이는 무시할 수 없습니다.
kubelet의 Topology Manager 정책을 켜야 정렬이 강제됩니다.
# kubelet 설정
topologyManagerPolicy: single-numa-node
topologyManagerScope: pod
single-numa-node는 파드의 모든 장치가 한 NUMA 노드에서 나오도록 요구하고, 만족할 수 없으면 파드를 배치하지 않습니다. 성능은 보장되지만 스케줄 실패가 늘어납니다. restricted나 best-effort는 더 느슨합니다.
트레이드오프가 명확합니다. 정렬을 강제하면 성능이 예측 가능해지는 대신 노드 활용률이 떨어지고 Pending이 늘어납니다. 학습 잡처럼 성능이 곧 비용인 워크로드에서는 강제하는 편이 맞고, 잡 종류가 섞인 클러스터에서는 노드 풀을 나누는 편이 낫습니다.
# 6. 장치가 고장 났을 때
물리 장치를 다루므로 고장은 실제로 일어나는 시나리오입니다. 최근 Kubernetes는 장치 상태를 파드 상태에 노출합니다.
When a device fails, the kubelet decreases the allocatable count (but not capacity) for that resource, preventing new pod scheduling while existing pods remain assigned.
할당 가능 수만 줄고 용량은 그대로입니다. 새 파드는 안 들어오지만 이미 그 장치를 쓰던 파드는 계속 그 상태로 남습니다. 자동으로 재배치되지 않습니다.
그래서 운영 측면에서 필요한 것이 둘입니다.
capacity와allocatable의 차이를 감시합니다. 벌어지면 장치가 죽은 것입니다.- 워크로드 쪽에서 통신 실패를 감지해 스스로 종료하게 만듭니다. 분산 학습 프레임워크의 타임아웃 설정이 이 역할을 합니다. 설정이 없으면 한 워커가 응답하지 않는 채로 전체 잡이 무한정 대기합니다.
# 7. 직접 확인하는 방법
세 조각을 순서대로 확인합니다.
# 1) 리소스가 노드에 광고됐는가
kubectl get nodes -o json | jq -r '.items[] |
"\(.metadata.name) capacity=\(.status.capacity["example.com/sriov_ib"] // "none") allocatable=\(.status.allocatable["example.com/sriov_ib"] // "none")"'
# 2) 디바이스 플러그인이 무엇을 찾았는가
kubectl -n kube-system logs ds/sriov-device-plugin --tail=50 | grep -i 'discover\|resource\|error'
# 3) NAD와 확장 리소스 이름이 일치하는가
kubectl get network-attachment-definitions -A \
-o jsonpath='{range .items[*]}{.metadata.name}{" → "}{.metadata.annotations.k8s\.v1\.cni\.cncf\.io/resourceName}{"\n"}{end}'
1번이 비면 디바이스 플러그인 문제(2절), 3번의 이름이 1번과 다르면 연결 고리 문제(3절)입니다.
파드가 뜬 뒤에는 안에서 확인합니다.
# 인터페이스가 실제로 들어왔는가
kubectl exec training-worker -- ip -br link
# Multus가 붙인 네트워크 목록 - 파드 상태 애너테이션에 남는다
kubectl get pod training-worker \
-o jsonpath='{.metadata.annotations.k8s\.v1\.cni\.cncf\.io/network-status}' | jq
# RDMA 장치가 마운트됐는가 - isRdma 확인
kubectl exec training-worker -- ls /dev/infiniband/
kubectl exec training-worker -- ibv_devinfo
ip link에는 인터페이스가 보이는데 /dev/infiniband/가 비어 있으면 isRdma가 빠진 것입니다.
NUMA 정렬은 이렇게 봅니다.
# 파드가 받은 장치와 그 NUMA 노드
kubectl exec training-worker -- sh -c '
for d in /sys/class/infiniband/*/device; do
echo "$(basename $(dirname $d)) numa=$(cat $d/numa_node)"
done'
kubectl exec training-worker -- nvidia-smi topo -m
nvidia-smi topo -m의 GPU-NIC 행에서 SYS가 보이면 소켓을 건너가는 경로이고, PIX나 PXB면 같은 쪽입니다.
실제 대역폭도 재 봐야 합니다. 설정이 맞아도 링크 속도가 낮게 협상됐을 수 있습니다.
kubectl exec training-worker -- ibstat | grep -E 'Rate|State|Physical'
# 8. 트러블슈팅
| 증상 | 원인 | 조치 |
|---|---|---|
| 파드가 계속 Pending | 리소스가 광고되지 않음 | 플러그인 로그, 셀렉터 확인 |
| 노드에 리소스가 안 보임 | resourceName에 하이픈 사용 | 이름에서 특수문자 제거 |
| 파드는 뜨는데 인터페이스 없음 | NAD의 resourceName 불일치 | 두 이름을 정확히 맞춤 |
| VF만 소모되고 안 쓰임 | 애너테이션 누락 | k8s.v1.cni.cncf.io/networks 추가 |
| 인터페이스는 있는데 RDMA 실패 | isRdma 미설정 | 플러그인 설정에 추가 |
| RDMA 메모리 등록 실패 | IPC_LOCK 없음 | capability 추가 |
| 성능이 기대보다 낮음 | GPU와 NIC의 NUMA 불일치 | Topology Manager 정책 설정 |
| 정책을 켜니 Pending 증가 | single-numa-node의 엄격함 | 노드 풀 분리 또는 완화된 정책 |
| 장치 고장 후 파드가 그대로 | 기존 할당은 유지됨 | 용량·할당가능 차이 감시, 잡 타임아웃 |
# 9. 마무리
- 파드에 물리 NIC을 붙일 때 스케줄링(확장 리소스)과 연결(CNI)은 별개의 작업입니다. 둘 다 필요하고, 이름으로 이어져 있습니다.
- 확장 리소스 이름은
resourcePrefix/resourceName이고 하이픈을 쓸 수 없습니다. 기본 접두사가 벤더 이름이라는 점도 헷갈리는 지점입니다. NetworkAttachmentDefinition의resourceName애너테이션이 두 세계를 잇습니다. 여기가 어긋나면 오류 없이 인터페이스만 안 생깁니다.- RDMA에는
isRdma와IPC_LOCK이 함께 필요합니다. 빠지면 "동작은 하는데 느리다"거나 "초기화에서만 실패한다"로 나타납니다. - NUMA 정렬은 성능과 스케줄 가능성의 교환입니다. 강제하면 예측 가능해지고 활용률이 떨어집니다.
- 장치가 고장 나도 이미 할당된 파드는 그대로 남습니다. 워크로드 쪽 타임아웃이 없으면 잡이 무한정 대기합니다.