[SGLang] NCCL & MSCCL++: 집합 통신 라이브러리 통합
들어가며
SGLang의 분산 통신 스택에서 NCCL과 MSCCL++는 Custom AllReduce의 상위 계층, 즉 대규모 텐서와 CUDA Graph 환경에서의 기본 백엔드로 동작한다. pynccl.py는 NVIDIA NCCL을 ctypes로 직접 래핑하고, pymscclpp.py는 Microsoft의 MSCCL++ 라이브러리를 통합한다.
두 라이브러리 모두 torch.distributed를 우회하여 직접 NCCL/MSCCL++ C API를 호출한다. 이를 통해 CUDA Graph 캡처 중에도 통신이 가능하다.
구조도
GroupCoordinator
│
├── pynccl_comm: PyNcclCommunicator
│ │
│ ├── NCCLLibrary (ctypes 래퍼)
│ │ ├── ncclAllReduce
│ │ ├── ncclAllGather
│ │ ├── ncclReduceScatter
│ │ ├── ncclSend / ncclRecv
│ │ └── ncclBroadcast
│ │
│ └── ncclComm_t (커뮤니케이터 핸들)
│
└── pymscclpp_comm: PyMscclppCommunicator
│
├── mscclpp_init_context()
├── mscclpp_allreduce()
└── pre_tune_config() (자동 튜닝)
핵심 코드 분석: PyNcclCommunicator
초기화
class PyNcclCommunicator:
def __init__(self, group, device, library_path=None):
if not isinstance(group, StatelessProcessGroup):
assert dist.get_backend(group) != dist.Backend.NCCL, \
"PyNcclCommunicator should be attached to a non-NCCL group."
self.rank = dist.get_rank(group)
self.world_size = dist.get_world_size(group)
self.nccl = NCCLLibrary(library_path)
if self.rank == 0:
self.unique_id = self.nccl.ncclGetUniqueId()
else:
self.unique_id = ncclUniqueId()
# Unique ID를 broadcast로 공유
tensor = torch.ByteTensor(list(self.unique_id.internal))
dist.broadcast(tensor, src=ranks[0], group=group)
핵심 설계: Gloo(CPU) 그룹에 부착한다. torch.distributed의 NCCL 백엔드와 별도의 NCCL 커뮤니케이터를 생성함으로써, CUDA Graph 캡처 중에도 독립적으로 동작할 수 있다.
Warmup
초기화 직후 소규모 AllReduce로 워밍업한다.
with torch.cuda.device(device):
self.comm = self.nccl.ncclCommInitRank(
self.world_size, self.unique_id, self.rank)
warmup_stream = torch.cuda.Stream()
with torch.cuda.stream(warmup_stream):
data = torch.zeros(1, device=device)
self.all_reduce(data)
warmup_stream.synchronize()
del data
# 기본적으로 비활성 상태 (프로파일링, prefill 단계)
self.disabled = True
생성 후 disabled=True로 설정된다. CUDA Graph 캡처 시 change_state(enable=True) 컨텍스트 매니저로 활성화한다.
AllReduce (in-place)
def all_reduce(self, tensor, op=ReduceOp.SUM):
if self.disabled:
return
assert tensor.device == self.device
stream = self._resolve_stream()
self.nccl.ncclAllReduce(
buffer_type(tensor.data_ptr()), # sendbuff
buffer_type(tensor.data_ptr()), # recvbuff (same = in-place)
tensor.numel(),
ncclDataTypeEnum.from_torch(tensor.dtype),
ncclRedOpTypeEnum.from_torch(op),
self.comm,
cudaStream_t(stream.cuda_stream),
)
sendbuff와 recvbuff가 같은 포인터이므로 in-place 연산이다.
AllReduce (out-of-place)
def outplace_all_reduce(self, in_tensor, out_tensor=None, op=ReduceOp.SUM):
if out_tensor is None:
out_tensor = torch.empty_like(in_tensor)
stream = self._resolve_stream()
self.nccl.ncclAllReduce(
buffer_type(in_tensor.data_ptr()), # sendbuff
buffer_type(out_tensor.data_ptr()), # recvbuff - DIFFERENT pointer
in_tensor.numel(), ...)
return out_tensor
CUDA Graph에서는 out-of-place가 필요하다. Graph 캡처 시 입력 텐서의 주소가 고정되어야 하므로, 결과를 별도 텐서에 기록한다.
Send/Recv (P2P 통신)
Pipeline Parallelism에서 사용되는 점대점 통신이다.
def send(self, tensor, dst):
stream = self._resolve_stream()
self.nccl.ncclSend(
buffer_type(tensor.data_ptr()), tensor.numel(),
ncclDataTypeEnum.from_torch(tensor.dtype),
dst, self.comm, cudaStream_t(stream.cuda_stream))
def recv(self, tensor, src):
stream = self._resolve_stream()
self.nccl.ncclRecv(
buffer_type(tensor.data_ptr()), tensor.numel(),
ncclDataTypeEnum.from_torch(tensor.dtype),
src, self.comm, cudaStream_t(stream.cuda_stream))
상태 전환
@contextmanager
def change_state(self, enable=None):
if enable is None:
enable = self.available
old_disable = self.disabled
self.disabled = not enable
try:
yield
finally:
self.disabled = old_disable
핵심 코드 분석: PyMscclppCommunicator
지원 범위
class PyMscclppCommunicator:
_SUPPORTED_WORLD_SIZES = [8, 16]
_MAX_BYTES = mscclpp_convert_to_bytes(os.getenv("SGLANG_MSCCLPP_MAX_BYTES", "1MB"))
_SUPPORTED_DTYPE = [torch.float, torch.float16, torch.bfloat16]
MSCCL++는 8-GPU 또는 16-GPU 구성에서만 동작한다. 최대 크기 기본값은 1MB로, NCCL보다 빠른 영역이 매우 작다.
자동 튜닝
MSCCL++의 핵심 차별점은 nthreads x nblocks 조합을 자동 튜닝하는 것이다.
def pre_tune_config(self, dtype=torch.bfloat16):
nthreads_to_try = [256, 512, 1024]
nblocks_to_try = [21, 42, 84]
for msg_size in self.msg_size_for_finetune:
best_config, best_time = None, None
for nthreads in nthreads_to_try:
for nblocks in nblocks_to_try:
cur_cost = mscclpp_bench_time(
lambda: ops.mscclpp_allreduce(
self._context, mock_inp, mock_outp, nthreads, nblocks))
if best_time is None or cur_cost < best_time:
best_config = (nthreads, nblocks)
best_time = cur_cost
self.msg_size2best_config[msg_size] = best_config
메시지 크기별로 9가지 조합(3 x 3)을 벤치마크하여 최적 구성을 찾는다. 이 결과는 rank 0에서 broadcast되어 모든 랭크가 동일한 설정을 사용한다.
AllReduce 실행
def all_reduce(self, tensor, op=ReduceOp.SUM):
msg_size = tensor.numel() * tensor.itemsize
index = bisect.bisect_left(self.msg_size_for_finetune, msg_size)
msg_size_finetune = self.msg_size_for_finetune[index]
nthreads, nblocks = self.msg_size2best_config[msg_size_finetune]
result = torch.empty_like(tensor)
ops.mscclpp_allreduce(self._context, tensor, result, nthreads, nblocks)
return result
bisect로 가장 가까운 튜닝된 크기를 찾아 해당 설정으로 실행한다.
비교: NCCL vs MSCCL++
| 특성 | PyNcclCommunicator | PyMscclppCommunicator |
|---|---|---|
| 지원 world_size | 제한 없음 | 8, 16만 |
| 최적 크기 범위 | 대규모 | 1MB 이하 |
| 자동 튜닝 | 없음 | nthreads x nblocks 벤치마크 |
| CUDA Graph | 지원 (change_state) | 지원 (change_state) |
| Eager 모드 | 기본 비활성 | 기본 비활성 |
설계 근거
왜 torch.distributed를 우회하는가? torch.distributed는 CUDA Graph 캡처 중 사용할 수 없다. NCCL C API를 직접 호출함으로써 Graph 내에서도 통신 커널을 녹화할 수 있다.
왜 Gloo 그룹에 부착하는가? 초기화 시 unique ID를 broadcast해야 하는데, 이미 NCCL 그룹이 있으면 충돌한다. Gloo는 CPU 기반이므로 독립적으로 조율 가능하다.
관련 포스트
- SGLang Custom All-Reduce: NCCL 너머의 최적화된 집합 통신
- SGLang 통신 연산: AllReduce, Broadcast, AllGather 구현
- SGLang 하드웨어별 통신: HPU, NPU, XPU 커뮤니케이터
참고
관련 포스트
- [SGLang] 통신 연산: AllReduce, Broadcast, AllGather 구현
- [sglang] DeepSeek-V4의 Latency 최적화: Fused mHC Post/Pre Kernel 도입
- [sglang] sglang ROCm MXFP4 어텐션에서 불필요한 contiguous copy 제거를 통한 성능 최적화
- [sglang] sglang의 torch.compile 활용: Advanced Indexing Gather 최적화로 LLM 추론 가속화
- [sglang] sglang diffusion 모델 성능 향상: Cache-DiT와 torch.compile의 최적화된 적용 순서
SGLang 의 다른글
- 이전글 [SGLang] Custom All-Reduce: NCCL 너머의 최적화된 집합 통신
- 현재글 : [SGLang] NCCL & MSCCL++: 집합 통신 라이브러리 통합
- 다음글 [SGLang] Data Parallel Controller: 다중 인스턴스 조율
댓글