gpumode · 강의 아카이브
《GPU Mode》 L017 2024 · APR · 27 High priority transcript · available

NCCL

DDP 모델을 두 GPU 위에 띄우면 — 사용자가 직접 부르지 않은 ncclAllReduce 가 backward 안에서 자동으로 launch 된다. NCCL kernel 은 사실 “일반 CUDA kernel” 이지만 — “많은 GPU 가 서로 데이터를 보내며 누산” 한다는 의미가 일반 kernel 과 다르다. Dan Johnson 이 — DDP 의 trace 부터 ncclAllReduce 의 ring 알고리즘까지 — host 측 launch 와 device 측 동작을 모두 깐다.

NCCL all-reduce ring algorithm DDP backward NVLink · IB tree algorithm double-binary tree chunk size nccl-tests
D
Speaker
Dan Johnson
NVIDIA · NCCL 팀
강의 번호
L017
스피커
Dan Johnson
학습 우선순위
High · 정독
코드
repo · ddp_simple.py · ddp_example.py
§ 01강의가 풀려는 문제· why this lecture exists

“PyTorch DDP 의 backward 안에 ncclAllReduce 가 있는데 — 사용자는 한 번도 부른 적이 없다”

Dan 이 강의를 시작하는 자리. 사용자가 PyTorch 의 DDP wrapper 를 쓰면 — backward 끝에 모든 gradient 가 자동으로 all-reduce 된다. 그런데 사용자 코드에 nccl 이 한 줄도 없다. 이게 어떻게 일어나는가, 그리고 그 collective 가 정확히 무엇을 하는가.

강의가 풀려는 세 질문.

  1. NCCL 의 collective 들이 어떤 의미를 가지는가 — all-reduce, broadcast, reduce-scatter, all-gather 의 정확한 정의.
  2. 그것을 GPU 들이 ring 위에서 어떻게 도는가 — ring all-reduce 알고리즘 step by step.
  3. 실제 application 에서 NCCL 의 cost 가 어디 들어가는가 — DDP backward 의 trace 위에서 직접.
강의의 인지적 frame

Dan 의 입장 — “NCCL kernel 은 device 입장에서는 그냥 또 다른 CUDA kernel” 이다. 다른 kernel 과 SM 을 두고 경쟁하고, register 와 shared memory 를 쓴다. 다만 그 안에 “여러 GPU 가 협력하는” 의미가 들어 있을 뿐. 이 시각이 디버깅과 성능 진단의 mental model.

“NCCL 이 magic 이 아니다 — 너의 모델 코드와 같은 GPU 위에서 같은 자원을 두고 도는 또 다른 kernel 이다.”Dan Johnson · 강의 paraphrase
§ 02collective 의 종류와 의미· all-reduce / broadcast / …

같은 데이터가 GPU 사이에서 무엇이 되는가 — 6 개의 표준 collective

NCCL 이 제공하는 collective 들. 각각 “before / after” 가 명확히 정의된 단순한 contract.

all-reduce모든 GPU 가 합산된 결과를 가짐 N 개 GPU 각자 다른 buffer A_i 를 가지고 시작. 끝나면 모두가 같은 결과 (예: sum) Σ A_i 를 가진다. DDP 의 gradient 동기화에서 가장 자주 쓰임. [A,B] [C,D] [E,F]
→ [A+C+E, B+D+F] (all)
reduce한 GPU 만 결과 받음 all-reduce 와 같지만 결과가 root 한 GPU 에만. 잘 안 쓰임 (보통 all-reduce 가 자연스러움). → root 만 [A+C+E, B+D+F]
broadcast한 GPU 의 데이터를 모두에게 root GPU 의 buffer 가 모든 GPU 로 복제. 학습 시작 시 모델 weight 동기화에 자주 사용. root [A,B] → 모두 [A,B]
all-gather각자의 일부를 모두가 모음 N 개 GPU 각자 다른 chunk 를 가지고 시작. 끝나면 모두가 N 개 chunk 를 다 가진다. ZeRO 의 weight gather 패턴. [A] [B] [C] → 모두 [A,B,C]
reduce-scatter합산 후 잘라서 나눔 all-reduce 의 절반 — N 개 GPU 의 입력을 합산한 뒤 결과를 N 개 chunk 로 잘라서 나눠 가진다. ZeRO 의 gradient reduction. [A,B] [C,D] [E,F]
→ [A+C+E] [B+D+F] [...]
all-to-all모두가 모두에게 각 GPU 가 N 개의 chunk 를 가지고 시작 — i 번째 chunk 가 i 번째 GPU 에게 간다. tensor parallelism, MoE expert routing 에서 사용. [A→0,B→1] [C→0,D→1]
→ [A,C] [B,D]
all-reduce = reduce-scatter + all-gather

NCCL 의 영리한 점 — all-reduce 를 수학적으로 reduce-scatter 와 all-gather 의 합성으로 표현. 두 단계가 모두 ring 위에서 N-1 step 으로 도니까 — all-reduce 의 ring 구현이 자연스럽게 2(N-1) step.

§ 03ring algorithm· all-reduce 의 표준

3 GPU 위에서 step by step — ABC, DEF, GHI 가 어떻게 합산되는가

Dan 의 강의에서 직접 그린 다이어그램의 형태. 3 GPU 의 buffer 를 [A,B,C], [D,E,F], [G,H,I] 로 시작 — all-reduce 후에 모두가 [A+D+G, B+E+H, C+F+I] 를 가져야 한다.

init 3 GPU. 각 buffer 를 N=3 개 chunk 로 나눔.
GPU 0: [A, B, C] · GPU 1: [D, E, F] · GPU 2: [G, H, I]
시작 상태
phase 1reduce-scatter step 1: GPU 0→1 보내기 chunk[0] (A); GPU 1→2 chunk[1] (E); GPU 2→0 chunk[2] (I).
받은 GPU 가 자기 chunk 에 누산.
step 2: GPU 1→2 chunk[0] (now A+D); GPU 2→0 chunk[1] (now E+H); GPU 0→1 chunk[2] (now I+C).
2 step 후 — 각 GPU 의 한 chunk 위치에 그 chunk 의 전체 합산이 모임.
N-1 = 2 step
phase 2all-gather 이제 GPU 0 의 chunk[1] 자리에 (B+E+H) 가 있다. GPU 1 의 chunk[2] 자리에 (C+F+I), GPU 2 의 chunk[0] 자리에 (A+D+G).
step 3: 각자 자기의 “완성된 chunk” 를 ring 으로 한 칸 보냄.
step 4: 한 번 더 보냄.
2 step 후 — 모두가 [A+D+G, B+E+H, C+F+I].
N-1 = 2 step
총합 2(N-1) step. 각 step 에서 보내는 데이터 양은 chunk 한 개 = total/N.
총 통신 = 2(N-1)/N × total ≈ 2× total.
중요한 점 — N 이 커져도 “2× total” 한계는 거의 일정. ring 이 scale 한다.
2(N-1) steps
왜 ring 이 좋은가

모든 step 에서 — 모든 link 가 동시에 사용됨. GPU 0 → 1 이 가는 동안 GPU 1 → 2, GPU 2 → 0 도 동시에. 결과: 한 ring step 의 시간 = (chunk 크기 / link 대역폭) 아니라 (chunk 크기 / link 대역폭). 사실상 단일 link 처럼 움직인다 — 다만 모두가 동시.

“ring 의 우아함은 — N 이 커져도 send/recv 한 번에 같은 양만 흐른다는 것. all-to-all 처럼 N² 통신이 아니다.”학습 노트 paraphrase
§ 04tree / double-binary tree· latency 최적화

작은 message 에서는 ring 이 느리다 — tree 가 답하는 자리

Ring algorithm 은 큰 message 에서 거의 optimal — bandwidth 를 최대로 쓰니까. 그런데 작은 message (수 KB ~ MB) 에서는 ring 이 비효율. ring 의 step 수가 2(N-1) 이라서 latency 가 N 에 비례. 이때 tree algorithm 이 답.

tree all-reduce. N 개 GPU 를 binary tree 로 — log(N) 단계의 reduce 후 같은 단계만큼 broadcast.

  • step 수: 2 log(N). ring 의 2(N-1) 보다 훨씬 적음. N=64 면 ring 은 126 step, tree 는 12 step.
  • 그러나 — 각 link 에서 흐르는 데이터가 ring 보다 N× 많다. 큰 message 에서는 그 cost 가 ring 의 step 수 cost 를 초과.

NCCL 은 message size 에 따라 자동 선택. 작으면 tree, 크면 ring. cross-over 점은 보통 수 MB 근처.

double-binary tree — tree 의 한 link 가 idle 한 시간을 reverse direction 의 다른 tree 가 메우는 변형. 두 개의 binary tree 가 동시에 도는데 root 가 다르고 — link 사용률이 거의 100%.

NCCL 의 default. nsys 에서 NCCL kernel 이름이 ncclTreeAllReduce 또는 ncclRingAllReduce 로 뜸 — 어떤 algorithm 이 dispatch 되었는지 정보.

algorithm 선택 강제

환경변수 NCCL_ALGO=Ring 또는 NCCL_ALGO=Tree 로 강제. 디버깅이나 benchmark 비교 시 유용. production 에서는 default(자동) 가 거의 항상 best.

§ 05NVLink · PCIe · IB 매핑· topology aware

같은 N=8 GPU 라도 — interconnect 가 ring 의 속도를 결정한다

Ring algorithm 의 throughput 은 ring 위 link 의 가장 약한 한 곳으로 묶인다. 그래서 NCCL 이 자기가 도는 hardware 의 topology 를 인지하고 ring 을 적합하게 그려야 함.

FIG · interconnect 별 대역폭 비교per direction, 측정 환경별
NVLink 4 (H100)intra-node, NVSwitch
~ 450 GB/s
fastest
NVLink 3 (A100)intra-node, 12 link
~ 300 GB/s
PCIe Gen4 x16intra-node, no NVLink
~ 32 GB/s
10× slower
InfiniBand HDRinter-node, 200 Gbps
~ 25 GB/s
node 간 limit
Ethernet 100Ginter-node 일반
~ 12 GB/s
slow
한 노드 안 (NVLink/NVSwitch) 와 노드 간 (IB/Ethernet) 의 차이가 한 자릿수 이상. ring 이 노드 경계를 넘으면 그 step 이 dominant cost.

NCCL 의 처방.

  • hierarchical ring — node 안에서 partial reduce, node 간에 한 번 inter-node, 다시 node 안에서 broadcast. 각 단계가 자기 layer 의 가장 적합한 algorithm.
  • topology auto-detectNCCL_DEBUG=INFO 로 NCCL 이 자기가 어떤 ring 을 그렸는지 출력. 잘못 그렸으면 환경변수로 hint.
NCCL 환경변수 — 자주 쓰는 것

NCCL_DEBUG=INFO (가장 먼저), NCCL_DEBUG_SUBSYS=ALL, NCCL_TOPO_DUMP_FILE=topo.xml, NCCL_RINGS (수동 ring), NCCL_P2P_DISABLE=1 (PCIe 강제 — 디버깅용). PyTorch 의 init_process_group 호출 전에 설정.

§ 06chunk 크기와 latency hiding· message size sweep

큰 message 를 작은 chunk 로 — pipeline 이 가능해진다

NCCL 의 또 다른 영리한 점 — “보낼 데이터를 한 번에 다 보내지 않는다”. message 를 chunk 로 자르고, 한 chunk 가 step 1 위에 있을 때 다음 chunk 가 step 0 위에서 도는 — pipelined ring.

FIG · pipelined ring — 한 message 의 chunk 들이 동시에 다른 step3 GPU, 4 chunk
시간 → GPU 0→1 chunk0 chunk1 chunk2 chunk3 GPU 1→2 chunk0 chunk1 chunk2 chunk3 GPU 2→0 chunk0 chunk1 chunk2 chunk3
같은 색이 같은 chunk. chunk0 이 GPU 2 에 도착할 때, chunk3 이 아직 GPU 0 에서 출발한다. 모든 link 이 동시에 활성. message 시간 = (전체 크기 / link 대역폭) + 작은 latency overhead. 깐 chunk 가 작을수록 pipeline 깊이 ↑.
chunk 크기의 trade-off

너무 작으면 — 각 chunk 의 launch 와 sync overhead 가 dominate. 너무 크면 — pipeline 깊이가 얕아져 link 이 비는 시간이 생김. NCCL 의 default 가 4 MB ~ 8 MB 근처로 자동 조정. NCCL_BUFFSIZE 로 강제 가능 (디버깅용).

§ 07NCCL kernel 자체는 일반 CUDA· SM 경합

NCCL kernel 은 GEMM 과 같은 SM 위에서 돈다 — 둘이 경쟁할 수도

강의의 가장 중요한 mental model — NCCL kernel 도 device 입장에서는 일반 CUDA kernel. 자기 block ID, thread ID 가 있고, register 와 SM 을 차지한다. 다른 kernel 과 동시에 도는 게 가능하지만 — 같은 SM 자원을 두고 경쟁한다.

device 측에서 일어나는 일.

  1. NCCL kernel 이 launch 되면 — 일부 SM 에 떠서 ring 안 GPU 들과 send/recv 시작.
  2. 그 동안 backward 의 GEMM 이 돌고 있다면 — 두 kernel 이 SM 을 분배해서 도는 중.
  3. NCCL kernel 의 SM 수가 너무 많으면 GEMM throughput 이 떨어짐. 너무 적으면 NCCL 이 link 을 다 못 채움.

NCCL 이 사용하는 SM 수는 자동 결정 — 보통 큰 message 에서 더 많은 SM. 이 자동 결정이 가끔 backward GEMM 과 안 맞아서 손해가 나는 경우가 있다.

overlap 의 의미

DDP 의 “gradient 와 backward 의 overlap” 은 — 두 kernel 이 같은 SM 위에서 시간을 쪼개는 게 아니라, 다른 SM 에서 동시에 도는 것이다. 실제로 그렇게 되려면 NCCL 이 자기 SM 을 적게 잡아야 함. 큰 모델일수록 이 trade-off 가 critical.

NVSHMEM 과의 경계

NCCL 보다 더 fine-grained 한 multi-GPU 통신 — NVSHMEM. shared address space 모델. NCCL 이 collective 단위 launch 라면 NVSHMEM 은 thread 단위 put/get. ML 학습에서는 NCCL 이 표준. NVSHMEM 은 더 세밀한 algorithm (예: FA-3 의 inter-node 통신) 에서 사용.

§ 08DDP backward 안에서 자동 launch· bucket gradient

“사용자가 안 부른 NCCL” 이 어떻게 trace 위에 나타나는가

강의의 라이브 시연 자리. 강의 repo 의 ddp_simple.pyddp_example.py 위에서 — DDP 의 backward 안에 nccl 호출이 어떻게 박혀 있는지를 nsys timeline 으로 본다.

# ddp_simple.py — 강의 repo 그대로
import torch.distributed as dist
from torch.nn.parallel import \
    DistributedDataParallel as DDP

class ToyModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.w = nn.Parameter(torch.tensor(5.0))
    def forward(self, x):
        return self.w * 7.0 * x

def demo_basic():
    dist.init_process_group("nccl")
    rank = dist.get_rank()
    model = ToyModel().to(rank)
    ddp_model = DDP(model, device_ids=[rank])

    with profile() as prof:
        x = torch.tensor(rank, dtype=torch.float)
        y = ddp_model(x)
        y.backward()
        # 여기 ↑ 안에 ncclAllReduce 가 자동 발생
    if rank == 0:
        prof.export_chrome_trace("trace_ddp_simple.json")

실행: torchrun --nproc_per_node=2 ddp_simple.py.

trace 위에서 보이는 것.

  • backward 안에 autograd::CppNode 가 gradient 를 만들면서 — Reducer::push_rebuilt_params_for_all_indices 같은 PyTorch 내부 hook 이 발동.
  • 그 hook 이 ProcessGroupNCCL::allreduce 호출.
  • 그게 device 에 ncclAllReduce kernel 을 launch.
  • flow event 가 CPU 의 backward → CPU 의 nccl 호출 → GPU 의 nccl kernel 의 chain 을 그린다.

bucket gradient — DDP 가 모든 gradient 를 하나하나 all-reduce 하지 않고 N MB 단위 bucket 으로 묶어 한 번에. DDP(model, bucket_cap_mb=25) 의 25 MB 가 그 단위. 강의의 ddp_example.py 가 이 패턴.

“DDP 의 magic 은 — gradient 가 만들어지는 동안 이미 다른 gradient 의 all-reduce 가 도는 것. 한 모델의 backward 가 깊을수록 마지막 gradient 의 latency 가 숨는다.”학습 노트 paraphrase
§ 09디버깅· hang · mismatched ops

NCCL 가 멈추는 흔한 자리 — 그 진단의 시퀀스

distributed 학습의 가장 큰 함정 — NCCL hang. 한 GPU 가 다른 GPU 를 무한 대기하면서 학습이 멈춘다. timeout 안 걸리면 영원히. Dan 이 강의에서 가장 자주 보는 원인 카탈로그.

mismatched ops
한 GPU 가 all-reduce 를 부르고 다른 GPU 가 broadcast 를 부르면 — NCCL 이 영원히 기다린다. 보통 if 문 안에서 collective 를 부르는데 한쪽 branch 만 들어가는 경우.
mismatched dtype/shape
같은 collective 를 불렀는데 buffer 의 dtype 또는 shape 가 GPU 별로 다르다. compile-time 에 잡히지 않는 함정.
tensor 가 다른 device
rank 0 의 tensor 가 cuda:0, rank 1 의 tensor 가 cpu — NCCL 은 cuda 만 다룬다. 사용자가 forget 한 .to(rank).
multiple stream 의 race
사용자 stream 위에서 NCCL 호출 하면서 default stream sync 가 빠진 경우. nccl 의 buffer 가 아직 안 ready.
non-deterministic ordering
같은 모델인데 모듈 등록 순서가 process 마다 다르다. DDP 의 bucket 매핑이 일치 안 함.
timeout
기본 timeout 10 분. init_process_group(timeout=...) 으로 조정. 짧게 잡으면 hang 이 빨리 보이지만 느린 step (큰 batch) 에서 false positive.
진단 도구

NCCL_DEBUG=INFO 로 모든 NCCL 호출 log. NCCL_DEBUG_SUBSYS=COLL 로 collective 만. 그래도 안 보이면 — TORCH_NCCL_BLOCKING_WAIT=1 로 nccl 호출이 sync 로 떨어져서 어떤 호출이 hang 했는지 stack trace 가 잡힘. TORCH_NCCL_DESYNC_DEBUG=1 으로 process 별 진행 상황 비교.

nccl-tests

NVIDIA 의 official microbenchmark — NVIDIA/nccl-tests. ./build/all_reduce_perf -b 8 -e 256M -f 2 -g 8 같은 식으로 다양한 message 크기에서 실측. 자기 hardware 의 NCCL baseline 을 알기 위해 production 전에 한 번 돌리는 게 표준.

§ 10기억할 메모와 코드· key takeaways · repo
6 표준 collective
all-reduce / reduce / broadcast / all-gather / reduce-scatter / all-to-all. all-reduce = reduce-scatter + all-gather.
ring algorithm
2(N-1) step. 각 step 마다 모든 link 동시 사용. 큰 message 에서 거의 optimal bandwidth.
tree algorithm
2 log(N) step. 작은 message 의 latency 에 좋음. NCCL 이 size 에 따라 자동 선택.
topology mapping
ring 의 logical 순서가 NVLink 와 일치해야 함. NCCL_DEBUG=INFO 로 자동 detect 결과 확인.
chunked pipelined ring
큰 message 를 chunk 로 잘라 ring 안에서 pipeline. 모든 link 이 동시에 활성.
NCCL = 일반 CUDA kernel
SM 자원 경쟁. backward GEMM 과 overlap 하려면 다른 SM 에서 동시에 돌아야 함.
DDP 의 자동 allreduce
backward 의 hook 이 bucket 단위 (default 25 MB) 로 ncclAllReduce. gradient 와 backward overlap 가 핵심.
hang 의 6 원인
mismatched op / dtype / device / stream / bucket order / timeout. TORCH_NCCL_BLOCKING_WAIT=1 로 진단.

손에 새기기 — 실습 시퀀스

  1. 2-GPU DDP toyddp_simple.py 를 그대로 실행. trace_ddp_simple.json 을 chrome://tracing 에 올려 backward 안의 ncclAllReduce 위치 확인.
  2. bucket size 영향ddp_example.pybucket_cap_mb 를 5, 25, 100 으로 변경하면서 한 step 의 wall-clock 측정. overlap 효과 시각화.
  3. nccl-tests 실측 — 자기 노드에서 all_reduce_perf 를 8 B ~ 256 MB sweep. bandwidth 가 message 크기에 따라 어떻게 saturate 되는지 그래프.
  4. NCCL_ALGO 강제 비교NCCL_ALGO=Tree vs Ring 강제로 같은 nccl-test 측정. cross-over 점 직접 찾기.
  5. topology dump 확인NCCL_TOPO_DUMP_FILE=topo.xml 로 자기 노드의 topology 시각화. ring 이 NVLink 와 일치하는지 검증.
  6. 일부러 hang 만들기 — if rank==0 만 collective 부르는 코드. TORCH_NCCL_BLOCKING_WAIT=1 로 hang 의 stack trace 확인.
  7. backward overlap 측정 — nsys 로 DDP train 의 backward 와 NCCL kernel 의 동시 실행 자리 측정. SM 사용률.
  8. multi-node 시도 — 가능하면 두 노드에서 NCCL all-reduce. inter-node 통신 비용이 intra-node 와 어떻게 다른지.
§ 12열린 질문· open questions
  • NCCL 2.20+ 의 새 기능 — 강의 시점 (2024 April) 이후의 변화. user buffer registration, async kernel launch 등. NCCL release notes 직접 확인 필요.
  • NVLink 5 (Blackwell) + NVSwitch 변화 — § 05 의 대역폭 표가 새 hardware 에서 어떻게 바뀌는지.
  • NCCL vs NVSHMEM 의 정확한 분기점 — 어떤 algorithm 에서 NVSHMEM 의 fine-grained 가 의미 있는가. FA-3 의 사례 외에.
  • HBM-direct DMA — GPUDirect RDMA, GPUDirect Storage. NCCL 이 이런 hardware feature 를 어떻게 활용하는지.
  • multi-tenant 환경의 NCCL — kubernetes 같은 환경에서 ring 매핑이 깨지는 자리. 동적 topology.
  • FSDP vs DDP 의 NCCL pattern — FSDP 는 reduce-scatter + all-gather 를 매 forward/backward 단위로. DDP 의 all-reduce 와 통신량 비교.
  • collective 의 numerical precision — fp16 / bf16 의 sum 이 GPU 별 누산 순서에 따라 살짝 다를 수 있다. ddp 학습의 numerical determinism.
검증 메모

이 노트의 § 05 대역폭 수치는 NVIDIA spec sheet 의 paraphrase. 실측은 자기 노드에서 nccl-tests 로 직접 — 보통 spec 의 70-80% 가 실측 sustained bandwidth. NVIDIA/nccl-tests 가 다음 회독의 마지막 step.

← Lecture 016 On Hands Profiling — single GPU 진단에서 multi-GPU 통신으로 Lecture 018 → Fusing Kernels — distributed 의 자리에서 다시 single GPU fusion 으로