분산 서빙과 통신
학습 목표 #
- 집합 통신 연산이 실제로 무엇을 주고받는지를 계산하고 all-reduce, all-to-all의 전송량을 텐서크기와 gpu수에서 유도하고 그 값이 지연에 얼마나 영향을 미치는지 환산한다.
nvidia-smi topo -m출력을 읽고 어느 gpu 쌍이 빠른 경로로 붙어 있는지 판정한다 그 결과로 병렬화 배치 값을 정할 수 있는 방법을 알아보자- NCCL이 링과 트리중 무엇을 골랐는가? 왜 그걸 골랐는가 같은 개념을 알아볼 것이다
- 노드 간 kv 전송이 왜 별도인지 문제를 이해하고 전송 백엔드를 골라본다.
- EP를 노드 안에 가두려는 이유를 전송량 계산 관련하여 설명해본다.
대역폭 절벽 #
대역폭 계층과 노드 경계 #
gpu에서 데이터를 옮길 대 목적지가 멀어질 수록 대역폭이 계단식으로 떨어지게 된다.
대역폭 비고
GPU 내부 HBM 3,350~8,000 GB/s 자기 메모리
다이 간 NV-HBI 10,000 GB/s Blackwell 듀얼 다이
NVLink 5 1,800 GB/s 같은 노드 안 GPU끼리
PCIe 5.0 x16 64 GB/s CPU ↔ GPU
InfiniBand NDR 50 GB/s 노드 간 (400Gb/s 포트)
이더넷 100GbE 12.5 GB/s 노드 간
GPU끼리 데이터를 주고받는 경로만 보면 노드 안이 1,800 GB/s 노드 밖이 50GB/s로 36배 차이가 난다 PCIe도 64GB/s 수준이라 노드를 벗어나는 순간 어느 경로를 타든 느리다.
인접한 경로끼리 배수를 내보면 노드 경계에서 크게 벌어지는걸 볼 수 있다
HBM → NVLink 2~4배
NVLink → PCIe 28배 ← 노드 경계
PCIe → InfiniBand 1.3배
노드 경계에서 36배 지연 증가 #
텐서 병렬로 예를 들어보자 레이어마다 all-reduce가 한 번씩 일어나고, 8 gpu에서 100mb 텐서를 다룬다고 해보자
전송량은 뒤에서 유도하겠지만 결과만 먼저 사용하면 gpu 하나가 175mb를 주고받게 된다.
NVLink 5 (1,800 GB/s) 175 MB ÷ 1,800 GB/s = 0.097 ms
InfiniBand NDR (50 GB/s) 175 MB ÷ 50 GB/s = 3.5 ms
80레이어 모델이면 토큰 하나당 이게 80번 반복되는것이다
NVLink 경로 0.097 × 80 = 7.8 ms
InfiniBand 경로 3.5 × 80 = 280 ms
디코드 한 스텝이 수밀리초인 상황에서 통신에 280ms를 쓴다? 이러면 계산은 의미가 없어지고 텐서 병렬을 노드 안에 가두라는 규칙이 왜 중요한지 여기서 알 수 있게 되는것이다.
지침 - 통신 빈도 기준 병렬화 배치 #
어쨋든 핵심은 통신이 잦은 병렬화는 노드 안에 두면 된다.
통신이 잦음 TP 레이어마다 all-reduce → NVLink 안쪽
EP MoE 레이어마다 all-to-all → 가능하면 NVLink 안쪽
통신이 드묾 PP 구간 경계에서 한 번 → 노드를 넘어도 됨
DP 어텐션 레이어는 통신 없음 → 노드를 넘어도 됨
GPU 8장 짜리 서버 한대에서 NVLink 배선이 그 섀시 안에서 끝나기 때문에 9번째 gpu에서 다른 섀시에 있고 거기로 가려면 InfiniBand를 타야한다
GB200 NVL72같은 랙 규모 구성은 이 범위를 넓혀 랙 안에 72장을 NVSwitch로 전부 묶어서 72장 중 언 쌍이든 NVLink로 직접 통신한다.
일반 8장 서버 NVLink로 묶이는 범위 8장 9번째부터 InfiniBand
NVL72 랙 NVLink로 묶이는 범위 72장 73번째부터 InfiniBand
집합 통신 #
집합 통신의 정의 #
여러 GPU가 함께 참여해서 하나의 결과를 만드는 통신이다 한쪽이 보내고 한쪽이 받는 점대점 통신과 달리 모든 참여자가 동시에 보내고 받게 된다
병렬화마다 쓰는 연산이 다른데 아래와 같다
연산 무엇을 하나 쓰는 곳
all-reduce 각자의 값을 전부 더해 모두가 같은 결과 TP
all-gather 각자의 조각을 모아 모두가 전체를 보유 TP의 열 분할, DP
reduce-scatter 전부 더한 뒤 각자 자기 몫만 보유 all-reduce의 전반부
all-to-all 각자가 각자에게 서로 다른 데이터 전송 EP
broadcast 한 GPU의 데이터를 전부에게 복사 가중치 배포
all-reduce #
TP에서 가장 자주 쓰이는데 각 gpu가 부분합을 갖고있고 전부 더해서 모두가 같은 완성값을 갖게 만든다
시작
GPU0: [a0] GPU1: [a1] GPU2: [a2] GPU3: [a3]
끝
GPU0: [S] GPU1: [S] GPU2: [S] GPU3: [S]
S = a0 + a1 + a2 + a3
TP로 행렬을 잘랐을때 각 gpu가 계산한 것은 부분합이라 이 연산을 거쳐야 완성된다
all-to-all #
각 gpu가 각 gpu에게 서로 다른 데이터를 보낸다
시작
GPU0: [→G0][→G1][→G2][→G3] 각 칸이 다른 목적지용
GPU1: [→G0][→G1][→G2][→G3]
GPU2: [→G0][→G1][→G2][→G3]
GPU3: [→G0][→G1][→G2][→G3]
끝
GPU0: [G0←][G1←][G2←][G3←] 전부에게서 받은 것
...
MoE에서 토큰 담당 전문가에게 보낼 때 이 패턴이 나오는데 어느 토큰이 어느 전문가로 갈지 라우터가 정하므로 목적지가 제각각이다
all-reduce vs all-to-all #
TP는 all reduce, EP는 all to all을 쓰고 선택은 병렬화 방식이 정하지만 다만 성격이 달라서 운영에서 신경 쓸 지점이 다르다.
all-to-all이 더 나은 부분은
전송량 all-reduce GPU당 약 2S 이동
all-to-all GPU당 약 S 이동 절반
참여 범위 all-reduce 전원이 동기화. 한 랭크가 늦으면 전부 대기
all-to-all 보낼 곳과 받을 곳만 관여
옮기는 것 all-reduce 계산 결과. 모두가 같은 값을 갖게 만들어야 함
all-to-all 토큰. 필요한 곳에만 보내면 끝
전문가 256개 중 토큰당 8개만 쓰는 구조인데 전문가를 잘라서 모든 gpu에 흩어놓고 all-reduce로 합치면 32장 전부가 계산에 동원된다 전문가를 통째로 배정하고 all to all로 토큰을 보내면 그 8개를 가진 gpu만 일을 하고 나머지는 다른 토큰을 처리한다 일부만 켜서 계산을 아끼자는 MoE 설계가 이 연산 덕에 유지된다.
문제 - 두 연산의 성격차이 #
전송량만 보면 all to all이 더 가볍다. 근데 여기서 고민해야할 포인트는
크기 예측 all-reduce 텐서 크기 고정. 미리 버퍼를 잡고 스케줄 가능
all-to-all 어느 GPU에 얼마나 갈지 라우터가 정할 때까지 모름
대칭성 all-reduce 모두가 같은 양을 주고받음
all-to-all 인기 전문가를 가진 GPU에 몰림. 가장 느린 링크가 전체를 결정
계산과 겹치기 all-reduce 겹치기 쉬움
all-to-all 라우터 출력을 기다려야 시작 가능
통신량이 많아서가 아니라 예측 불가능하고 불균형함의 숙제가 존재해서 이 문제를 해결해야한다.
해법 #
- 크기 예측이 안되는 문제: 전용 통신 라이브러리가 다루는데 DeepEP같은 구현 라우터 출력이 나오는 즉시 전송을 시작하도록 커널을 짜고 서버 내부는 NVLink로 서버 간은 RDMA로 경로를 나ㅝ 동시에 흘린다 범용 집합 통신 라이브러리로는 이 패턴을 표현할 수 없어 별도 구현ㅇㅣ 나왔다.
- 부하가 기우는 문제: 전문가 배치를 다시 계산해 완화한다 라우팅 통곌ㄹ 보고 인기 전문가를 여러 gpu에 복제하면 그쪽으로 갈 토큰이 나뉜다 SGLang
--enable-eplb가 이 기능이다.
--enable-eplb --eplb-algorithm deepseek --ep-num-redundant-experts 32
전문가 슬롯이 256개에서 288개로 늘어 가중치 메모리가 12.5%로 증가하는 대신 편중이 완화되어 가장 느린 gpu를 기다리는 시간을 줄인다.
- 계산과 겹치기 어려운 문제는 배치를 쪼개서 우회한다 배치를 둘로 나눠 한쪽이 통신하는 동안 다른쪽이 계산하면 라우터 출력을 기다리는 구간이 다른 배치의 계산으로 채워진다 배치가 충분히 클때 사용할 수 있는방법
쪼개지 않으면 [라우터][all-to-all 대기][전문가 계산][all-to-all 대기]
쪼개면 배치A [라우터][통신 ][계산 ][통신 ]
배치B [라우터][통신 ][계산 ][통신 ]
↑ 서로의 대기 구간을 메움
링과 트리 #
단일 GPU 집중 방식의 병목 #
all-reduce를 가장 단순하게 구현하면 모두가 0번 gpu에 데이터를 보내고 0번이 전부 더해서 다시 뿌리는 것이다
N개 GPU, 버퍼 크기 S
GPU0이 받는 양 (N-1) × S
GPU0이 보내는 양 (N-1) × S
N=8, S=100MB → GPU0 혼자 1.4GB를 주고받음
나머지 7장은 각자 200MB
0번 gpu의 링크가 병목이 될거고 gpu를 늘리면 오히려 나빠질것이다. (N이 커지니가)
링 알고리즘 #
GPU를 논리적 원형으로 배치하여 데이터를 N조각으로 쪼개 옆으로만 넘겨 두 바퀴를 돌게 한다
1단계 reduce-scatter (N-1회)
각 GPU가 오른쪽 이웃에게 조각 하나씩 보내며 누적
끝나면 각 GPU가 서로 다른 조각의 완성값을 하나씩 보유
2단계 all-gather (N-1회)
완성된 조각을 다시 한 바퀴 돌려 모두에게 배포
각 단계에서 gpu 하나가 보내는 양은 조각 하나 즉 S/N 이다
총 전송량 = 2(N-1) 단계 × S/N = 2(N-1)/N × S
N=8, S=100MB → 175 MB
N=32, S=100MB → 194 MB
N이 커져도 2S에 수렴한다 단일 gpu 집중 방식이 (N-1)S와 달리 gpu수에 거의 무관해졌다. gpu가 많아진다고 특정 gpu에 병목이 늘지 않는다는것
소형 메시지에서의 링 열세 #
링의 전송량은 gpu 수와 거의 무관하지만 여전히 단계 수는 gpu수에 비례하게 된다
reduce-scatter에 N - 1회 all-gather에서 N-1회 총 2(N-1)
N=8 → 14단계
N=32 → 62단계
각 단계는 앞 단계가 끝나야 시작된다 옆 gpu에서 받을 조각을 누적해서 닷 ㅣ옆으로 넘기는 구조라 순소를 건너뛸수없어 그래서 단계마다 고정비용이 붙는다 붙는다
한 단계의 고정 비용
커널 실행 준비
이웃과의 동기화
링크 전파 지연
→ 합쳐서 NVLink 기준 대략 2μs
전체 소요시간을 두 항으로 나눠보면 이렇다
소요 시간 = 단계 비용 + 전송 비용
= 2(N-1) × 2μs + 2(N-1)/N × S ÷ 1,800 GB/
N=32에 버퍼 크기를 바꿔가며 넣어보자
버퍼 크기 S 단계 비용 전송 비용 지배하는 쪽
64 KB 124 μs 0.07 μs 단계 비용 (1,800배)
1 MB 124 μs 1.1 μs 단계 비용 (110배)
100 MB 124 μs 108 μs 비슷
1 GB 124 μs 1,078 μs 전송 비용 (8.7배)
버퍼가 작으면 실제로 데이터를 옮기는 시간보다 단계를 넘기는 시간이 훨신 크다
64kb를 옮기는데 62번의 동기화를 거치면서 통신의 99퍼를 대기하게 된다.
트리 알고리즘 해법 #
단계 수를 줄이려면 옆으로만 넘기는 구조를 버려야하고 트리는 gpu를 이진 트리로 배치하고 위 아래로 오간다.
reduce 단계 (아래에서 위로)
GPU0
╱ ╲
GPU1 GPU2
╱ ╲ ╱ ╲
G3 G4 G5 G6
깊이 3 → 3단계면 GPU0에 전부 모임
같은 깊이의 GPU들은 동시에 진행
broadcast 단계 (위에서 아래로)
같은 트리를 반대 방향으로 3단계
이진 트리의 깊이는 log₂N으로 링이 이웃을 하나씩 거치는동안 트리는 절반씩 줄여 올라가게 된다.
링 단계 2(N-1) N=32 → 62단계 → 124 μs
트리 단계 2log₂N N=32 → 10단계 → 20 μs
앞의 표에서 64KB일 때 링이 124μs를 쓰는데, 트리는 20μs로 끝이난다
그러나 문제점 하나 트리는 링크를 다 못사용함 #
트리가 항상 나은것은 아니다 링은 매 단계에서 모든 gpu가 동시에 보내고 받으므로 링크를 전부 활용한다
트리는 같은 깊이의 gpu만 움직이므로 일부 링크가 놀게 된다
링 32장이 각자 오른쪽 이웃에게 전송 32개 링크 전부 활성
트리 깊이 1에서는 2개 링크만 활성 나머지는 대기
깊이 2에서는 4개, 깊이 3에서는 8개
그래서 버퍼가 커지면 전송 비용이 지배하는데 그 구간에서는 링크를 다 쓰는 링이유리하다 NCCL은 이중 이진 트리로 이 손실을 절반쯤 회복하지만 링을 완전히 따라잡지는 못한다
버퍼가 작다 단계 비용이 지배된다 -> 트리
버퍼가 크다 전송 비용이 지배된다 -> 링
NCCL은 메시지 크기를 보고 알아서 고르는데 경계는 대략 256KiB이고, 위 표에서 단계 비용과 전송 비용이 뒤집히는 지점 근처다
실측 대역폭 #
DGX H100 8 GPU 기준 실측값
링 (NVLink 직결) 약 700 GB/s
NVSwitch 전이분할 활용 약 900 GB/s
링 700 GB/s는 인접 gpu끼리 주고받는 구조에서 나온값이다 NVSwitch는 모든 쌍을 동시에 연결할 수 있는데 링은 그 능력을 다 쓰지 못한다 스위치를 활용하는 알고리즘으로 900 GB/s까지 나온다
측정은 nccl-tests로 한다.
$ ./build/all_reduce_perf -b 8 -e 1G -f 2 -g 8
# size(B) time(us) algbw(GB/s) busbw(GB/s)
1048576 58.2 18.02 31.53
16777216 238.4 70.38 123.16
268435456 3021.7 121.36 212.38
-b 8: 시작 버퍼 크기 8바이트-e 1G: 끝 버퍼 크기 1gb-f 2: 크기를 두배씩 늘려가며 측정-g 8: gpu 8장 사용
두 대역폭 열의 차이를 알아둬야한다
algbw 버퍼 크기 ÷ 시간. 사용자가 체감하는 값
busbw 실제 링크를 흐른 양 ÷ 시간. 하드웨어 활용도
all-reduce는 busbw = algbw × 2(N-1)/N
링크 이론값과 비교할 대상은 busbw로 algbw만 보면 낮아 보여도 정상인 경우가 많다.
NCCL과 토폴리지 #
NCCL(Nvidia Collective Communications Library)은 집합통신을 gpu에서 실행하는 라이브러리다. pytorch에 init_process_group(backend='nccl') 이 이걸 쓴다
토폴로지 탐지 어느 GPU가 어떤 링크로 붙어 있는지 조사
알고리즘 선택 메시지 크기와 토폴로지를 보고 링/트리 중 선택
커널 실행 통신을 GPU 커널로 수행. SM을 일부 소비
세 번째가 서빙에서 의미를 가지는데. 통신이 SM을 먹으므로 계산과 자원을 다툰다 통신중 계산이 완전히 멈추지는 않지만 서로 느려진다
선택된 알고리즘 조회법 #
$ NCCL_DEBUG=INFO python train.py 2>&1 | grep -E "NCCL INFO (Ring|Trees|Channel|NET)"
NCCL INFO Channel 00/04 : 0 1 2 3 4 5 6 7
NCCL INFO Ring 00 : 7 -> 0 via NVLINK
NCCL INFO Trees [0] 1/-1/-1->0->-1
NCCL INFO NET/IB : Using [0]mlx5_0:1/RoCE
Ring 00 : 7 -> 0 via NVLINK: 링 구성과 각 홉이 쓰는 링크 via P2P, via SHM 같이 나오면 같은 서버에 gpu인데도 NVLink를 못 쓰는 상태다. 카드가 NVLink 브리지 없이 꽂혀있거나 컨테이너에 일부 gpu만 주입되어 링을 못 만드는 경우다Channel 00/04: 병렬 채널 수. 많을수록 링크를 잘 채운다NET/IBL Using [0]mlx5_0: 서버간 경로와 사용중인 NIC,NET/Socket이 나오면 InfiniBand를 못찾아 이더넷으로 떨어진 것이다.
환경 변수 상시 고정 #
NCCL은 토폴리지를 보고 알아서 고르는데 다음 변수들이 진단용으로 쓰인다
NCCL_ALGO=Ring: 알고리즘 강제. 링으로NCCL_P2P_DISABLE=1: P2P 비활성, 문제 격리용NCCL_IB_DISABLE=1: InifniBand 비활성, 이더넷으로 떨어짐NCCL_DEBUG=INFO: 로그 출력. 성능에 영향은 없
상시 설정에 필요한 경우는 둘로
NCCL_SOCKET_IFNAME=eth0: 서버에 인터페이스가 여러개고 NCCL이 관리용 인터페이스를 골라버릴때 지정NCCL_IB_HCA=mlx5_0,mlx5_1: NIC가 여러개인데 일부만 쓸때 명시