본문 바로가기

LLM

SGLang PD 분리 배포 완전 가이드 — Prefill/Decode 분리로 처리량 5배 올리기

반응형

LLM 추론에는 두 단계가 있어요.

Prefill (프리필):
- 입력 프롬프트 전체를 처리
- 연산 집약적 (Compute-bound)
- KV 캐시 생성
- 보통 수백~수천 토큰을 한 번에 처리

Decode (디코드):
- 토큰을 하나씩 생성
- 메모리 집약적 (Memory-bound)
- KV 캐시를 매 스텝마다 읽음
- 요청당 수십~수백 번 반복

전통적인 통합 엔진에서는 이 두 단계가 같은 GPU에서 경쟁해요. 그래서 두 가지 심각한 문제가 생겨요.

문제 1: Prefill 방해(Prefill Interruption)

기존 통합 엔진:
[디코딩 중...토큰 생성 중...]
          ↑
     새 요청 들어옴!
          ↓
[프리필 처리... (디코딩 멈춤)]
[디코딩 재개...]
[프리필 처리... (또 멈춤)]

→ 토큰 생성이 계속 끊김 → TPOT(토큰 생성 시간) 급증

문제 2: DP 어텐션 불균형

기존 통합 엔진 (DP 어텐션 2개):
DP Worker 0: [프리필 처리 중]  ← 연산 집약적
DP Worker 1: [디코딩 처리 중]  ← 메모리 집약적

→ 서로 다른 특성 때문에 동기화 대기 발생 → 레이턴시 증가

PD 분리(Disaggregation) 는 이 두 단계를 완전히 다른 서버로 분리해서 각각 최적화해요.

PD 분리 아키텍처:
클라이언트 요청
    ↓
[로드 밸런서]
    ↓
[Prefill 서버] → KV 캐시 생성 후 RDMA로 전송 →
                                                  [Decode 서버] → 토큰 생성 → 클라이언트

실제 DeepSeek 모델 96 GPU 배포에서 Prefill 3.8배, Decode 4.8배 처리량 향상이 확인됐어요.


PD 분리가 왜 빠른가

Prefill 서버 최적화

Prefill 특성:
- 연산 집약적 → 빠른 GPU 연산 필요
- 배치 처리에 유리 → 여러 요청 한 번에 처리
- KV 캐시 생성 후 바로 다음 요청으로 넘어감

→ 고성능 계산용 GPU에 배치
→ 텐서 병렬 크게 설정
→ 배치 크기 최대화

Decode 서버 최적화

Decode 특성:
- 메모리 집약적 → 큰 KV 캐시 필요
- 매 스텝마다 KV 캐시 읽기
- 연속 토큰 생성

→ 메모리 많은 GPU에 배치
→ 많은 동시 요청 처리
→ KV 캐시 공간 최대화

RDMA 기반 KV 캐시 전송

Prefill이 KV 캐시를 생성하면 Decode로 넘겨줘야 해요. 이 전송이 빠르지 않으면 PD 분리 효과가 없어요. SGLang은 **RDMA(Remote Direct Memory Access)**로 GPU 메모리 간 직접 전송을 해요.

일반 네트워크 전송:
GPU메모리 → CPU메모리 → 네트워크 → CPU메모리 → GPU메모리
(복사 4번, CPU 관여)

RDMA 전송:
GPU메모리 → [InfiniBand/RoCE] → GPU메모리
(직접 전송, CPU 관여 없음, 제로 카피)

요청 흐름 — 단계별 동작

1. 클라이언트 → Decode 서버로 요청 전송
2. Decode → Prefill에 bootstrap_room ID 요청 (KV 캐시 수신 공간 예약)
3. Decode → GPU 메모리 페이지 할당
4. Decode → Prefill로 요청 포워딩
5. Prefill → 프롬프트 전체 처리, KV 캐시 생성
6. Prefill → RDMA로 KV 캐시를 Decode GPU 메모리에 직접 기록
7. Prefill → 완료 신호 전송, 전송 메타데이터 정리
8. Decode → KV 캐시 수신 확인 후 토큰 생성 시작
9. Decode → 생성된 토큰을 스트리밍으로 클라이언트에 전달

설치 — 전송 백엔드 준비

PD 분리에는 KV 캐시 전송 백엔드가 필요해요. Mooncake 또는 NIXL 중 하나를 사용해요.

Mooncake 설치 (AMD GPU 포함 모든 환경 권장)

pip install mooncake-transfer-engine

# 또는 소스에서 빌드
git clone https://github.com/kvcache-ai/Mooncake.git
cd Mooncake
pip install -e .

# 설치 확인
python -c "import mooncake; print(mooncake.__version__)"

NIXL 설치 (NVIDIA GPU 권장, RDMA 최적화)

# pip 설치
pip install nixl

# 소스 빌드 (UCX가 이미 설치된 경우 필요)
git clone https://github.com/ai-dynamo/nixl.git
cd nixl
pip install .

어느 걸 쓸까?

Mooncake:
✅ AMD GPU 포함 모든 GPU 지원
✅ 설치 간단
✅ InfiniBand/RoCE 네트워크 지원
→ AMD MI300X, 멀티벤더 환경

NIXL:
✅ NVIDIA GPU 전용 최적화
✅ CUDA IPC 지원
✅ 더 낮은 전송 레이턴시 (NVIDIA 환경)
→ NVIDIA H100/A100/GB200 순수 환경

단일 노드 PD 분리 (기본 설정)

가장 간단한 구성이에요. 같은 서버에서 Prefill과 Decode를 분리해요.

# Prefill 서버 (GPU 0, 1)
CUDA_VISIBLE_DEVICES=0,1 python -m sglang.launch_server \
  --model-path meta-llama/Llama-3.1-8B-Instruct \
  --disaggregation-mode prefill \
  --disaggregation-transfer-backend mooncake \
  --host 0.0.0.0 \
  --port 30000 \
  --tp 2

# Decode 서버 (GPU 2, 3)
CUDA_VISIBLE_DEVICES=2,3 python -m sglang.launch_server \
  --model-path meta-llama/Llama-3.1-8B-Instruct \
  --disaggregation-mode decode \
  --disaggregation-transfer-backend mooncake \
  --host 0.0.0.0 \
  --port 30001 \
  --tp 2

로드 밸런서 설정

Prefill과 Decode 서버 앞단에 라우터를 놓아요. SGLang 공식 라우터를 사용해요.

# pip 설치
pip install sglang-router

# 라우터 실행
python -m sglang_router.launch_router \
  --pd-disaggregation \
  --prefill-hosts http://localhost:30000 \
  --decode-hosts http://localhost:30001 \
  --host 0.0.0.0 \
  --port 8080
# 클라이언트에서는 라우터 주소만 사용
from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8080/v1",
    api_key="EMPTY"
)

response = client.chat.completions.create(
    model="meta-llama/Llama-3.1-8B-Instruct",
    messages=[{"role": "user", "content": "안녕"}]
)

멀티 노드 PD 분리 (대규모 배포)

여러 노드에 걸쳐 Prefill과 Decode를 분리해요.

Llama 70B — 2노드 PD 분리

# ===== Prefill 노드 (노드 0) =====
python -m sglang.launch_server \
  --model-path meta-llama/Llama-3.3-70B-Instruct \
  --disaggregation-mode prefill \
  --disaggregation-transfer-backend mooncake \
  --host 0.0.0.0 \
  --port 30000 \
  --tp 4 \
  --dtype bfloat16 \
  --mem-fraction-static 0.85

# ===== Decode 노드 (노드 1) =====
python -m sglang.launch_server \
  --model-path meta-llama/Llama-3.3-70B-Instruct \
  --disaggregation-mode decode \
  --disaggregation-transfer-backend mooncake \
  --host 0.0.0.0 \
  --port 30001 \
  --tp 4 \
  --dtype bfloat16 \
  --mem-fraction-static 0.88  # Decode는 KV 캐시 공간 더 확보

# ===== 라우터 (별도 노드 또는 마스터 노드) =====
python -m sglang_router.launch_router \
  --pd-disaggregation \
  --prefill-hosts http://prefill-node-ip:30000 \
  --decode-hosts http://decode-node-ip:30001 \
  --host 0.0.0.0 \
  --port 8080

DeepSeek-V3 — 대규모 PD 분리 (96 GPU)

실제 LMSYS가 공개한 DeepSeek V3 96 GPU 배포 설정이에요.

# ===== Prefill 서버 — 노드 0 (마스터) =====
python -m sglang.launch_server \
  --model-path deepseek-ai/DeepSeek-V3-0324 \
  --disaggregation-mode prefill \
  --disaggregation-transfer-backend mooncake \
  --disaggregation-ib-device mlx5_0  # InfiniBand NIC 이름 (ibstat로 확인)
  --host ${local_ip} \
  --port 30000 \
  --trust-remote-code \
  --dist-init-addr ${prefill_master_ip}:5000 \
  --nnodes 2 \
  --node-rank 0 \
  --tp 16 \
  --dp 8 \
  --enable-dp-attention \
  --moe-a2a-backend deepep \
  --mem-fraction-static 0.80

# ===== Prefill 서버 — 노드 1 (워커) =====
python -m sglang.launch_server \
  --model-path deepseek-ai/DeepSeek-V3-0324 \
  --disaggregation-mode prefill \
  --disaggregation-transfer-backend mooncake \
  --disaggregation-ib-device mlx5_0 \
  --host ${local_ip} \
  --port 30000 \
  --trust-remote-code \
  --dist-init-addr ${prefill_master_ip}:5000 \
  --nnodes 2 \
  --node-rank 1 \
  --tp 16 \
  --dp 8 \
  --enable-dp-attention \
  --moe-a2a-backend deepep \
  --mem-fraction-static 0.80

# ===== Decode 서버 — 노드 2 (마스터) =====
python -m sglang.launch_server \
  --model-path deepseek-ai/DeepSeek-V3-0324 \
  --disaggregation-mode decode \
  --disaggregation-transfer-backend mooncake \
  --disaggregation-ib-device mlx5_0 \
  --host ${local_ip} \
  --port 30001 \
  --trust-remote-code \
  --dist-init-addr ${decode_master_ip}:5001 \
  --nnodes 2 \
  --node-rank 0 \
  --tp 16 \
  --dp 8 \
  --enable-dp-attention \
  --moe-a2a-backend deepep \
  --mem-fraction-static 0.85

# ===== Decode 서버 — 노드 3 (워커) =====
python -m sglang.launch_server \
  --model-path deepseek-ai/DeepSeek-V3-0324 \
  --disaggregation-mode decode \
  --disaggregation-transfer-backend mooncake \
  --disaggregation-ib-device mlx5_0 \
  --host ${local_ip} \
  --port 30001 \
  --trust-remote-code \
  --dist-init-addr ${decode_master_ip}:5001 \
  --nnodes 2 \
  --node-rank 1 \
  --tp 16 \
  --dp 8 \
  --enable-dp-attention \
  --moe-a2a-backend deepep \
  --mem-fraction-static 0.85

PD 분리 전용 파라미터 상세

--disaggregation-mode

서버 역할을 지정해요.

# Prefill 전용 서버
--disaggregation-mode prefill

# Decode 전용 서버
--disaggregation-mode decode

# 설정 안 하면 기존 통합 모드 (기본값)

--disaggregation-transfer-backend

KV 캐시 전송 백엔드예요.

# Mooncake (범용, AMD 포함)
--disaggregation-transfer-backend mooncake

# NIXL (NVIDIA 전용, 더 빠름)
--disaggregation-transfer-backend nixl

--disaggregation-ib-device

InfiniBand NIC 이름이에요. 명시 안 하면 자동 감지.

# InfiniBand NIC 목록 확인
ibstat

# 특정 NIC 지정
--disaggregation-ib-device mlx5_0

# 여러 NIC 사용
--disaggregation-ib-device mlx5_0,mlx5_1

--disaggregation-prefill-dp-size

Prefill 전용 DP 크기예요. Prefill과 Decode가 다른 DP 크기를 가질 수 있어요.

# Decode DP보다 Prefill DP를 더 크게 설정
python -m sglang.launch_server \
  --model-path meta-llama/Llama-3.1-70B-Instruct \
  --disaggregation-mode prefill \
  --tp 4 \
  --dp 4 \
  --disaggregation-prefill-dp-size 4

환경 변수로 타임아웃 조정

# Bootstrap 타임아웃 (기본 30초, 느린 네트워크에서 늘리기)
export SGLANG_DISAGGREGATION_BOOTSTRAP_TIMEOUT=600  # 10분

# 대기 타임아웃
export SGLANG_DISAGGREGATION_WAITING_TIMEOUT=600

Prefill : Decode 비율 설정

워크로드 특성에 따라 비율을 조절해요.

짧은 프롬프트, 긴 응답 (예: 코드 생성):
Prefill 1 : Decode 3~4
→ Decode 서버가 더 많은 GPU 필요

긴 프롬프트, 짧은 응답 (예: 문서 요약):
Prefill 2~3 : Decode 1
→ Prefill 서버가 더 많은 GPU 필요

균형 잡힌 워크로드 (예: 챗봇):
Prefill 1 : Decode 1~2
# 짧은 프롬프트, 긴 응답 워크로드
# Prefill 1개 서버 (GPU 2개)
CUDA_VISIBLE_DEVICES=0,1 python -m sglang.launch_server \
  --model-path meta-llama/Llama-3.1-8B-Instruct \
  --disaggregation-mode prefill \
  --tp 2 --port 30000

# Decode 3개 서버 (GPU 6개)
CUDA_VISIBLE_DEVICES=2,3 python -m sglang.launch_server \
  --disaggregation-mode decode --tp 2 --port 30001

CUDA_VISIBLE_DEVICES=4,5 python -m sglang.launch_server \
  --disaggregation-mode decode --tp 2 --port 30002

CUDA_VISIBLE_DEVICES=6,7 python -m sglang.launch_server \
  --disaggregation-mode decode --tp 2 --port 30003

# 라우터에서 여러 Decode 서버 등록
python -m sglang_router.launch_router \
  --pd-disaggregation \
  --prefill-hosts http://localhost:30000 \
  --decode-hosts http://localhost:30001 http://localhost:30002 http://localhost:30003 \
  --port 8080

Docker Compose로 PD 분리 배포

# docker-compose.pd.yml
version: "3.8"

services:
  prefill:
    image: lmsysorg/sglang:latest
    runtime: nvidia
    environment:
      - NVIDIA_VISIBLE_DEVICES=0,1
    volumes:
      - ~/.cache/huggingface:/root/.cache/huggingface
    ports:
      - "30000:30000"
    command: >
      python -m sglang.launch_server
      --model-path meta-llama/Llama-3.1-8B-Instruct
      --disaggregation-mode prefill
      --disaggregation-transfer-backend mooncake
      --tp 2
      --host 0.0.0.0
      --port 30000
    network_mode: host

  decode:
    image: lmsysorg/sglang:latest
    runtime: nvidia
    environment:
      - NVIDIA_VISIBLE_DEVICES=2,3
    volumes:
      - ~/.cache/huggingface:/root/.cache/huggingface
    ports:
      - "30001:30001"
    command: >
      python -m sglang.launch_server
      --model-path meta-llama/Llama-3.1-8B-Instruct
      --disaggregation-mode decode
      --disaggregation-transfer-backend mooncake
      --tp 2
      --host 0.0.0.0
      --port 30001
      --mem-fraction-static 0.88
    network_mode: host
    depends_on:
      - prefill

  router:
    image: sglang-router:latest
    ports:
      - "8080:8080"
    command: >
      python -m sglang_router.launch_router
      --pd-disaggregation
      --prefill-hosts http://localhost:30000
      --decode-hosts http://localhost:30001
      --host 0.0.0.0
      --port 8080
    network_mode: host
    depends_on:
      - prefill
      - decode
docker compose -f docker-compose.pd.yml up -d

성능 비교 — 통합 모드 vs PD 분리

96 GPU에서 DeepSeek V3 기준 실측값이에요.

지표 통합 모드 PD 분리 향상

Prefill 처리량 기준 3.8배 +280%
Decode 처리량 기준 4.8배 +380%
TTFT (첫 토큰) 높음 낮음 대폭 개선
TPOT (토큰 생성) 불규칙 안정적 안정화

PD 분리 사용해야 할 때 vs 아닐 때

PD 분리를 써야 할 때

✅ GPU 8개 이상 보유
✅ 고트래픽 프로덕션 환경
✅ TTFT와 TPOT SLA가 엄격함
✅ InfiniBand 네트워크 있음
✅ DeepSeek, Llama 70B+ 대형 모델

통합 모드가 더 나을 때

❌ GPU 4개 이하 소규모 환경
❌ 단순 개발/테스트 환경
❌ 네트워크 대역폭 부족 (KV 전송 병목)
❌ 8B 이하 소형 모델 (PD 분리 오버헤드가 이득보다 큼)

자주 발생하는 문제와 해결법

# 문제 1: KV 캐시 전송 타임아웃
# 해결: 타임아웃 늘리기
export SGLANG_DISAGGREGATION_BOOTSTRAP_TIMEOUT=300

# 문제 2: InfiniBand NIC 찾지 못함
# 해결: 수동 지정
ibstat  # 사용 가능한 NIC 확인
--disaggregation-ib-device mlx5_0

# 문제 3: Decode 서버 OOM
# 해결: mem-fraction-static 낮추기 (KV 캐시 더 줄이기)
--mem-fraction-static 0.80

# 문제 4: Prefill 서버가 느림
# 해결: max-prefill-tokens 늘리기
--max-prefill-tokens 32768

# 문제 5: 멀티 노드 데드락
# 해결: CUDA 그래프 비활성화
--disable-cuda-graph

# 문제 6: RDMA 연결 실패
# 해결: UCX/NCCL 환경 변수 설정
export NCCL_IB_DISABLE=0
export NCCL_IB_GID_INDEX=3
export UCX_TLS=rc,cuda_copy,cuda_ipc

공식 문서 링크


마무리

PD 분리를 한 줄로 요약하면 이래요.

"Prefill은 계산이 빠른 GPU에서 몰아치고, Decode는 메모리 많은 GPU에서 여유 있게 처리해라."

GPU가 충분하고 고트래픽 환경이라면 PD 분리는 선택이 아니라 필수예요. DeepSeek-V3 수준의 모델을 프로덕션에서 서빙하려면 PD 분리 없이는 SLA를 맞추기 어려워요. 😄

 

반응형