Documents
Home>Documents>AI>Inference

Prefill-Decode 분리 이후의 문제: KV 캐시 전송 비용은 왜 병목이 되는가

14 min readAug 20, 2026Aug 20, 2026

P/D disaggregation은 분명히 효과가 있다. Prefill 노드와 Decode 노드를 분리하면 두 단계의 간섭이 사라지고 각자에 최적화된 병렬 전략을 독립적으로 적용할 수 있다. DistServeSplitwise가 이를 입증했다.

그런데 분리해서 실제 운영해보면 기대만큼 나오지 않는 경우가 생긴다. TTFT를 줄이겠다고 분리했는데 오히려 TTFT가 늘거나, GPU 활용률을 보면 Decode 노드가 멍하니 놀고 있다. 분리 아이디어 자체의 문제가 아니다 — KV 캐시 전송이 숨어 있던 병목으로 드러나는 상황이다.

KV 캐시는 반드시 이동해야 한다

Prefill 노드가 입력 토큰 전체를 처리하면 각 레이어마다 Key, Value 텐서가 생성된다. 이 KV 캐시는 Decode 단계에서 attention 계산에 그대로 쓰이므로 Prefill 완료 즉시 Decode 노드로 넘어가야 한다. 스케줄링을 아무리 잘 짜도 이 전송은 피할 수 없다. 분리 서빙의 구조적 비용이다.

KV 캐시 전송량을 직접 계산하면

전송해야 할 바이트는 수식 하나로 표현된다.

size = 2 (K+V) × L × H_kv × D × S × B × dtype_bytes
  • L: 트랜스포머 레이어 수
  • H_kv: KV 헤드 수 (GQA에서는 Q 헤드보다 훨씬 적음)
  • D: 헤드 차원 (head_dim)
  • S: 시퀀스 길이 (토큰)
  • B: 배치 크기
  • dtype_bytes: FP16이면 2

Llama-3 70B를 넣으면 (L=80, H_kv=8, D=128, FP16):

size = 2 × 80 × 8 × 128 × S × B × 2 bytes
     = 327,680 × S × B bytes

시퀀스 1K 토큰, 단일 요청 (B=1):

327,680 × 1,024 = 335,544,320 bytes ≈ 336 MB

배치 32 (B=32):

336 MB × 32 ≈ 10.7 GB

여기서 짚고 넘어갈 게 있다. Llama-3 70B는 GQA(Grouped Query Attention)를 써서 KV 헤드가 8개뿐이다. Q 헤드 64개를 그대로 쓰면 전송량이 8배 과산정된다. 실제 구현에서 KV 캐시 크기를 계산할 때 반드시 num_kv_heads를 확인해야 하는 이유가 여기 있다.

전송량은 시퀀스 길이와 배치 크기 모두에 선형이다. 두 값이 동시에 커지는 고처리량 시나리오에서 전송량은 곱으로 폭증한다.

네트워크 토폴로지와 현실의 충돌

위 전송량을 각 인터커넥트에 대입하면:

인터커넥트실효 대역폭단일 요청, 1K tok (336 MB)배치 32, 1K tok (10.7 GB)
NVLink 4 (H100)900 GB/s~0.4 ms~11.9 ms
IB NDR~50 GB/s (400 Gb/s)~6.7 ms~214 ms
IB HDR~25 GB/s (200 Gb/s)~13.4 ms~428 ms
100GbE~12.5 GB/s~26.9 ms~856 ms

Llama-3 70B decode 단계 하나의 지연은 TP=8 A100 환경에서 대략 20~50ms다. 이 숫자와 표를 나란히 놓으면 문제가 바로 보인다.

NVLink는 배치 32에서도 12ms로 decode 1 step 이하다. 이 환경에서 전송이 병목이 될 가능성은 낮다. IB NDR은 단일 요청 6.7ms로 허용 범위지만 배치 32에서 214ms가 된다 — decode step의 4~10배. IB HDR은 단일 요청 13ms로 이미 하나의 decode step에 근접하고, 배치가 커지면 상황은 빠르게 악화된다.

100GbE는 단일 요청 1K 토큰에서 이미 27ms다. Decode 노드가 KV를 기다리며 decode step 하나를 통째로 날리는 셈이다. 배치 32에서 856ms — 토큰을 수십 개 생성할 시간 동안 전송만 기다린다.

임계 시퀀스 길이를 계산해보면 더 직관적이다. 100GbE 환경에서 단일 요청의 KV 전송 시간이 decode 1 step(~30ms)을 넘는 지점:

336 MB × (S / 1024) / 12.5 GB/s > 30ms
→ S > 30ms × 12.5 GB/s × 1024 / 336 MB
→ S > 약 1,143 tokens

1K 컨텍스트가 넘어가는 요청을 일반 이더넷으로 서빙하면 분리 서빙의 이득은 전송 비용에 잠식된다.

TTFT와 TPOT에 어떻게 나타나는가

전송 지연이 지표에 미치는 영향은 TTFT와 TPOT에서 다르게 나타난다.

TTFT는 직격탄을 맞는다. 요청이 들어온 후 첫 토큰이 나오기까지의 구간은 Prefill 시간 + KV 전송 시간 + 첫 Decode 스텝으로 구성된다. KV 전송이 완료되기 전까지 Decode 노드는 아무 일도 하지 못한다. 위 표의 수치가 TTFT에 그대로 더해진다고 보면 된다.

TPOT는 Decode 노드 내부 연산이므로 전송 지연의 직접 영향을 받지 않는다. 하지만 Decode 노드의 idle time이 누적되면 스케줄러가 다음 요청을 밀어 넣지 못하고, 전체 throughput이 저하되면서 tail latency가 올라간다. TPOT p99가 튀는 원인을 찾다가 Decode 노드의 활용률 수치를 보면 답이 나오는 경우가 여기서 생긴다.

DistServe은 이 구간을 "migration overhead"로 명시하며 NVLink 기반 동일 노드 전송을 권장한다. Mooncake는 한 발 더 나아가 CPU DRAM, SSD, RDMA를 활용한 분산 KV 캐시 스토어를 구성해 전송 경로 자체를 재설계했다 — Moonshot AI의 Kimi 서비스가 이 위에 돌아가고 있고, 수천 노드 규모에서 하루 1천억 토큰 이상을 처리한다고 밝혔다.

이 구간을 측정하지 않으면 분리 서빙의 실제 이득을 과대 추정하게 된다. 프로파일러가 "Prefill X ms, Decode Y ms"만 보여줄 때 그 사이의 gap이 빠져 있으면 전체 TTFT가 왜 그 값이 나오는지 설명이 안 된다.

완화 전략과 트레이드오프

KV 압축 (INT8 양자화)

전송 직전에 FP16 KV 캐시를 INT8로 양자화하면 전송량이 절반으로 줄어든다. 위 표의 숫자가 전부 2로 나뉜다. 단점은 Decode 노드에 도착한 뒤 FP16으로 복원(dequantize)하는 비용이 추가된다는 점이다.

vLLM의 FP8 KV 캐시는 KV를 GPU 메모리에 FP8로 상시 저장하는 방식이다. 전송용 임시 압축과 근본적으로 다르다 — 저장 양자화는 메모리 압력을 줄이기 위해 상시 적용되고, 전송 압축은 네트워크 대역폭을 줄이기 위해 전송 구간에만 적용된다. 두 기법을 함께 쓸 수도 있지만, 전송 압축의 정확도 손실은 저장 양자화와 별개의 경로에서 발생한다.

청킹과 파이프라이닝

전송을 레이어 단위 청크로 나누면 Decode 노드가 앞 레이어의 KV가 도착하는 순간부터 해당 레이어의 attention 계산을 시작할 수 있다. 전송 지연 일부를 연산과 겹쳐 숨기는 방식이다. Splitwise가 탐구하는 방향이고, 구현 복잡도가 올라가지만 긴 시퀀스 환경에서 TTFT를 의미 있게 줄일 수 있다.

토폴로지 선택

가장 확실한 해결책은 NVLink로 연결된 노드 안에서 Prefill-Decode를 처리하는 것이다. H100 DGX 내 NVLink 속도는 900 GB/s로 배치 32에서도 12ms 이내다. 랙 간 IB를 쓰면 이 숫자가 10배 이상 커진다.

다만 NVLink 구성은 한 서버 내에서 GPU를 나눠 쓰는 형태가 되어 Prefill/Decode 각각의 GPU 수를 독립적으로 조절하기 어렵다. IB 기반 멀티 노드는 탄력적 스케일링이 자유롭지만 대역폭 제약이 따른다. 운영 유연성과 전송 비용이 트레이드오프다.

언제 분리가 실제로 이득인가

전송 비용을 고려하면 분리 서빙이 이득이 되는 조건은 생각보다 좁다.

조건분리 서빙통합 서빙
prefill > 512 tok, BW > 200 Gb/s, B < 16TTFT 개선상대적 열세
prefill < 256 tok, 고빈도 요청전송 오버헤드 지배우세
100GbE, B > 8, seq > 1K tok전송이 TTFT 압도강력 우세

짧은 컨텍스트를 고빈도로 서빙하는 환경 — 챗봇 쿼리 평균 128 토큰 이하 — 에서는 전송 오버헤드가 분리 이득을 완전히 상쇄한다. 분리 서빙을 도입하기 전에 자신의 요청 길이 분포 히스토그램을 먼저 뽑아야 한다.

KV 전송 병목을 실제로 측정하는 법

Prefill 완료 시각과 첫 Decode 토큰 생성 시각 사이의 gap을 별도 메트릭으로 추출하지 않으면 전송 병목은 TTFT 숫자 안에 묻힌다. 가장 직접적인 방법은 두 시각을 Prometheus metric으로 노출하는 것이다.

import time
from prometheus_client import Gauge

kv_transfer_gap = Gauge(
    "llm_kv_transfer_gap_seconds",
    "Gap between prefill completion and first decode step",
    ["request_id"]
)

# Prefill 노드: KV 전송 시작 직전
prefill_done_ts = time.monotonic()

# Decode 노드: 첫 forward 직전에 gap 기록
kv_transfer_gap.labels(request_id=req_id).set(
    time.monotonic() - prefill_done_ts
)

NCCL 레벨에서 직접 보고 싶으면 nsys를 쓴다.

nsys profile \
  --trace=cuda,nvtx,nccl \
  --output=kv_transfer_profile \
  python serve.py

nccl 트레이스의 Send / Recv 구간 타임스탬프를 보면 KV 전송이 몇 ms를 차지하는지 레이어별로 분해된다. InfiniBand 환경이라면 ib_send_bw / ib_read_bw로 실제 대역폭을 먼저 확인하고 위 이론치와 비교하는 게 순서다.

KV 전송 gap이 TTFT의 20% 이상을 차지하고 있다면, 그 시스템은 네트워크 토폴로지나 배치 전략을 바꾸지 않는 한 분리 서빙 이득의 상당 부분을 반납하고 있는 상태다.

Tags
LLMInferenceGPU서빙아키텍처모니터링InfiniBand