LLM 추론 요청 하나가 GPU를 지나가는 동안 두 번의 전혀 다른 연산이 일어난다. Prefill은 입력 토큰 전체를 한 번에 처리하고, Decode는 출력 토큰을 하나씩 순차적으로 뽑아낸다. 표면적으로는 같은 트랜스포머 연산이지만, GPU 자원을 소모하는 방식이 근본적으로 다르다. 이 차이를 이해하지 못하면 chunked prefill 파라미터를 어떻게 잡아야 하는지, disaggregation이 언제 필요한지를 감으로 결정할 수밖에 없다.
Arithmetic Intensity: 두 단계가 다른 이유
GPU 성능의 병목이 어디에 있는지는 arithmetic intensity(FLOP/Byte 비율)로 결정된다. 연산량 대비 메모리에서 읽어야 하는 데이터가 많으면 memory-bandwidth-bound, 반대면 compute-bound다. A100 80GB SXM4의 경우 BF16 기준 312 TFLOPS의 연산 능력과 약 2 TB/s(2,039 GB/s)의 메모리 대역폭을 가지므로, 리지 포인트(ridge point)는 약 156 FLOP/Byte다. 이 숫자보다 높은 arithmetic intensity면 compute-bound, 낮으면 memory-bandwidth-bound다.
Prefill 단계에서는 입력 시퀀스의 모든 토큰이 동시에 행렬 곱으로 처리된다. 토큰 수 T가 늘어날수록 행렬 곱의 차원이 커지고 arithmetic intensity도 함께 올라간다. 1024 토큰 시퀀스의 prefill에서 attention QKV projection 한 레이어만 봐도 수십 FLOP/Byte를 넘기가 수월하다. 충분히 긴 시퀀스의 prefill은 A100을 compute-bound 영역에서 돌린다.
Decode는 전혀 다른 그림이다. 출력 토큰 한 개를 뽑을 때 모든 레이어의 가중치 행렬을 메모리에서 읽어야 하고, 이미 생성된 모든 토큰의 KV cache도 읽어야 한다. 배치 크기가 1이면 행렬-벡터 곱이므로 arithmetic intensity는 1~2 FLOP/Byte 수준에 머문다. 156 FLOP/Byte 리지 포인트와 비교하면 대략 100배 차이다. GPU 연산 유닛의 대부분이 메모리 전송을 기다리며 놀고 있다는 의미다. 배치 크기를 키우면 나아지긴 하지만, KV cache 크기가 VRAM을 압박하기 때문에 decode 배치를 무한정 늘리기도 어렵다.
이 비대칭이 모든 설계 결정의 출발점이다.
같은 GPU 풀에 섞으면 생기는 일
Continuous batching이 등장하기 전에는 한 요청의 prefill이 끝나야 다음 iteration으로 넘어갔기 때문에 긴 prefill이 들어오면 다른 요청의 decode가 그냥 멈췄다. Continuous batching은 iteration마다 배치를 재조정해서 이 문제를 완화했지만 제거하지는 못했다.
문제는 iteration의 길이가 균일하지 않다는 데 있다. Decode iteration 하나는 수 밀리초 안에 끝나지만, 긴 시퀀스의 prefill iteration은 수십~수백 밀리초를 소모한다. Prefill 요청이 배치에 섞이면 그 iteration 동안 decode 중이던 요청들은 다음 토큰을 받지 못하고 대기한다. 요청 하나의 TPOT(Time Per Output Token)이 그 prefill iteration 길이만큼 튀는 것이다.
실시간 채팅 서비스에서 이게 어떻게 보이냐면 — 스트리밍 출력이 잘 나오다가 갑자기 멈추고, 잠시 후 다시 쏟아진다. TPOT p99를 모니터링하면 평균은 괜찮아 보이지만 꼬리 분포가 크게 벌어진 것을 확인할 수 있다.
반대 방향도 있다. Decode 배치가 꽉 찬 상태에서 새 요청이 들어오면 prefill이 지연된다. TTFT(Time To First Token)가 높아진다. 결과적으로 긴 prefill 요청이 많이 섞이면 TTFT와 TPOT가 동시에 나빠지는 상황이 발생한다. Continuous batching이 throughput을 높여준다는 사실과, 그 안에서 prefill-decode 충돌이 여전히 일어난다는 사실은 모순이 아니다 — 처리량과 latency 꼬리 분포는 별개 지표다.
Chunked Prefill: 시간으로 분산하는 방법
핵심 아이디어는 단순하다. Prefill을 한 번에 다 처리하지 않고 고정 chunk 크기로 나눠서, decode iteration 사이사이에 끼워 넣는다. 4096 토큰 prefill이 있으면 512 토큰씩 8번에 걸쳐 처리하고, 각 chunk 사이마다 decode iteration이 들어간다. 긴 prefill이 만들던 "decode 차단" 이벤트가 짧은 방해 여러 번으로 분산된다.
Sarathi-Serve(Agrawal et al., OSDI 2024)가 이 접근을 체계화했다. 동일한 TBT(Time Between Tokens) p99 SLO를 지키면서 처리할 수 있는 요청 수 기준으로 vLLM 대비:
| 모델 | 설정 | serving capacity 향상 |
|---|---|---|
| Mistral-7B | 1×A100 | 2.6x |
| Yi-34B | 2×A100 (TP2) | 3.7x |
| Falcon-180B | 8×A100 (TP4×PP2) | 5.6x |
chunk size는 TTFT와 TPOT 사이의 dial이다. 값을 줄이면 prefill chunk가 decode를 덜 방해하므로 TPOT 안정성이 올라가지만, 같은 prefill을 더 많은 iteration에 걸쳐 처리하므로 TTFT가 늘어난다. 값을 키우면 반대다. Sarathi 실험에서 C=512 토큰 chunk를 쓸 때 추가 계산 오버헤드는 3% 미만이었고, chunk size 최적값은 모델 크기에 따라 달랐다 — Mistral-7B에서는 약 512 토큰, LLaMA2-70B에서는 약 1024 토큰 근방이었다.
vLLM에서는 --enable-chunked-prefill로 활성화하고 --max-num-batched-tokens로 chunk size를 제어한다.
# chunk size 2048 — vLLM 현재 기본값 (ITL 최적화)
vllm serve meta-llama/Llama-3-8B \
--enable-chunked-prefill \
--max-num-batched-tokens 2048
# TPOT 안정성 우선 — 작은 chunk
vllm serve meta-llama/Llama-3-8B \
--enable-chunked-prefill \
--max-num-batched-tokens 512
# TTFT 우선, 또는 throughput 우선 — 큰 chunk
# (소형 모델 + 대형 GPU 조합에서는 8192 이상 권장)
vllm serve meta-llama/Llama-3-8B \
--enable-chunked-prefill \
--max-num-batched-tokens 8192
vLLM 공식 문서 기준 현재 기본값은 2048 토큰이다(v0.4.2까지는 512였다). 이 기본값이 항상 최적은 아니다. 입력이 주로 짧은 대화라면 chunk size보다 decode 배치 크기가 더 큰 영향을 미치고, 입력이 수천 토큰 문서 요약이라면 기본값으로도 TTFT가 눈에 띄게 늘어날 수 있다.
SGLang은 다른 방식으로 접근한다. overlap scheduler를 통해 prefill chunk와 decode 배치를 같은 GPU에서 파이프라인 방식으로 겹쳐 실행한다. vLLM이 prefill chunk → decode를 순차 실행하는 구조인 것과 달리, SGLang은 prefill chunk를 처리하는 동안 직전 decode 결과의 일부 연산을 병행한다. 이론적으로는 처리량이 더 높지만, 구현 복잡도와 메모리 사용 패턴이 달라지므로 모델·설정에 따라 비교 우위가 바뀐다.
Prefill-Decode Disaggregation: 다른 GPU 풀로 분리
Chunked prefill이 충돌을 시간으로 분산하는 전략이라면, disaggregation은 충돌 자체를 없애는 전략이다. Prefill을 처리하는 GPU 풀과 Decode를 처리하는 GPU 풀을 아예 분리하고, prefill이 끝난 뒤 생성된 KV cache를 네트워크를 통해 decode 노드로 전송한다.
DistServe(Zhong et al., OSDI 2024)가 이 구조를 체계적으로 실험했다. 동일한 GPU 수 기준으로 vLLM 대비 최대 7.4x 더 많은 요청을 처리하거나, 동일 요청량에서 12.6x 더 엄격한 SLO를 달성했다. 워크로드별로 보면 OPT-13B ShareGPT에서 2.0x, OPT-175B에서 4.6x, 긴 문서 요약(LongBench)에서 12.6x로 시퀀스가 길수록 이득이 컸다. KV cache 네트워크 전송 오버헤드는 OPT-175B 실험에서 전체 latency의 0.1% 미만이었고, 95% 이상의 요청에서 KV 전송 지연이 30ms 미만이었다.
Mooncake(Qin et al., 2024 — FAST 2025 Best Paper)는 이 구조를 Kimi 프로덕션에 적용한 사례다. 20대의 단일 인스턴스 대신 10 Prefill + 10 Decode 분리 구조로 전환했을 때 동일 GPU 수에서 75% 더 많은 요청을 처리했다. vLLM 기준 TBT SLO 달성률이 57%에 불과했던 것이 Mooncake에서는 100%에 가까워졌다. RDMA 800 Gbps 인터커넥트를 사용했으며, 실제 달성 전송 대역폭은 단일 경로 87 GB/s, 본딩 190 GB/s였다.
여기서 전제 조건을 짚어야 한다. KV cache 전송 비용이 0.1% 수준으로 유지되는 것은 RDMA 고속 인터커넥트가 있을 때다. 일반 이더넷으로 KV cache를 전송하면 전송 지연이 prefill 자체보다 길어질 수 있다. 또한 시퀀스가 짧으면 KV cache 크기도 작아 전송 비용 절대치가 낮지만, 수천 토큰짜리 긴 시퀀스를 주로 처리하는 workload에서는 그 전송 비용이 새로운 병목이 된다. Mooncake가 특히 효과적이었던 이유 중 하나가 Kimi의 workload가 긴 context를 많이 다뤘기 때문이다.
어떤 상황에서 무엇을 선택하는가
| 조건 | Chunked Prefill | Disaggregation |
|---|---|---|
| 클러스터 규모 | 단일 노드 ~ 소규모 멀티노드 | 멀티노드, 수십 GPU 이상 |
| 노드 간 대역폭 | 무관 | RDMA / InfiniBand 사실상 필수 |
| 요청 길이 분포 | 짧거나 중간 (수백 ~ 수천 토큰) | 긴 시퀀스 혼합 workload |
| SLA 목표 | TPOT 안정성 + TTFT 균형 | TTFT와 TPOT를 독립적으로 최적화 |
| 운영 복잡도 | 낮음 (플래그 하나) | 높음 (별도 오케스트레이션 필요) |
| 주요 이득 | Decode stall 감소, 처리량 증가 | Prefill/Decode 리소스 독립 스케일링 |
단일 노드나 소규모 클러스터에서는 chunked prefill이 거의 항상 현실적인 첫 선택이다. --enable-chunked-prefill 플래그 하나면 되고, 추가 인프라가 필요 없다. disaggregation은 RDMA 인터커넥트가 이미 있고 클러스터 규모가 충분히 커서 prefill 전용 노드와 decode 전용 노드를 분리 관리할 여력이 될 때 고려할 만하다. Mooncake 논문의 결과도 Kimi 규모의 프로덕션 환경에서 나온 것이다 — 소규모 팀이 같은 구조를 그대로 도입하면 운영 비용이 이득을 초과할 가능성이 높다.
운영에서 놓치기 쉬운 함정
Prefill과 Decode의 충돌이 심해지면 두 지표가 동시에 오른다. Prefill queue depth(대기 중인 prefill 요청 수)와 decode stall rate(decode iteration이 예상보다 길게 걸리는 비율)다. 둘 다 올라간다면 prefill이 decode를 밀어내고 있다는 신호다.
vLLM이 노출하는 Prometheus 지표로:
# 현재 prefill 중인 요청 수
vllm:num_prefill_tokens_requests
# 스케줄러 대기열 깊이 (prefill 포기 전 대기 중)
vllm:num_waiting_requests
# 초당 생성 토큰 수 (decode throughput proxy)
rate(vllm:generation_tokens_total[1m])
prefill queue depth와 num_waiting_requests가 동시에 올라가면서 generation_tokens_total 증가율이 떨어진다면, prefill 부하가 decode throughput을 잠식하고 있는 상태다.
prefix caching이 활성화된 상태에서 chunked prefill을 같이 쓰면 주의해야 할 부분이 있다. Prefix cache hit는 해당 prefix의 KV cache를 재활용하므로 prefill 계산을 건너뛰는데, chunked prefill 경계가 prefix cache 블록 경계와 맞지 않으면 캐시 히트율이 떨어질 수 있다. vLLM은 chunk 경계를 prefix cache 블록 크기에 맞추는 방향으로 처리하지만, 설정 조합이 나쁘면 캐시 효율이 저하된다.
Prefill이 길어질수록 KV cache preemption도 문제가 된다. 새 요청의 prefill을 처리하려는데 VRAM이 부족하면 기존 decode 중인 요청의 KV cache를 CPU로 swap하거나 처음부터 재계산해야 한다. 이 재계산 비용은 원래 prefill 비용과 동일하다. preemption이 자주 발생한다면 chunk size를 줄이는 것보다 --max-num-seqs(동시 처리 시퀀스 수)를 줄이는 편이 효과적인 경우가 많다. chunk size는 iteration당 prefill 부하를 조절하지, VRAM 총 점유량을 줄이지는 않기 때문이다.