Continuous batching은 iteration마다 요청을 추가하거나 제거할 수 있어 GPU 활용률을 높였다. Orca(OSDI '22)가 제안한 iteration-level scheduling 덕분에, 특정 요청이 끝날 때까지 전체 배치를 멈춰야 했던 request-level batching의 비효율은 크게 줄었다. 그런데 긴 컨텍스트 요청이 트래픽에 섞이기 시작하면 p99 TPOT이 갑자기 치솟는다. Continuous batching이 해소했다고 믿었던 latency 오염이 다른 형태로 다시 나타나는 것이다.
한 요청이 배치 전체를 멈추는 경로
LLM 추론은 prefill과 decode 두 단계로 나뉜다. Prefill은 입력 토큰 전체를 처리해 KV 캐시를 채우고, decode는 이후 매 iteration마다 토큰 하나씩 생성한다. Continuous batching 스케줄러는 매 iteration 전에 배치 구성을 결정하는데, 새로 도착한 prefill 요청과 decode 중인 기존 요청을 같은 iteration에 섞어서 처리할 수 없다. Prefill은 입력 전체 토큰을 한 번에 통과시켜야 하기 때문이다.
스케줄러는 prefill 요청이 들어오면 해당 iteration을 prefill 전용으로 편성한다. 입력이 짧으면 그 iteration 자체가 빨리 끝나지만, 8K나 32K짜리 입력이라면 그 한 번의 iteration이 매우 오래 걸린다. Prefill 단계는 compute-bound이라 처리 토큰 수에 거의 선형으로 비례하는 시간을 소비한다. 그 동안 decode 중이던 다른 요청들은 다음 iteration을 기다리며 블록된다.
이게 prefill stall이다. Continuous batching은 request-level stall을 없앴지만, iteration-level stall은 그대로 남겨뒀다.
p99 TPOT이 치솟는 이유가 바로 여기다. TPOT은 iteration 소요 시간을 직접 반영한다. 32K 입력 요청이 배치에 들어오는 순간, 동시에 decode 중인 요청 전부가 그 stall을 고스란히 맞는다. 긴 prefill 요청이 간헐적으로 들어오는 혼합 트래픽에서 p99 latency 꼬리가 올라가는 것은 이 구조가 원인이다.
스케줄러가 prefill을 먼저 처리하는 이유
일반적인 continuous batching 스케줄러는 prefill 요청에 높은 우선순위를 준다. Decode 중인 요청들은 이미 KV 캐시를 GPU 메모리에 점유하고 있다. Prefill을 미루면 그 요청은 큐에서 기다리면서도 메모리를 차지하지 않으므로, 먼저 처리해 KV 캐시를 채우는 쪽이 메모리 자원 배분 관점에서 합리적으로 보인다.
TPOT 관점에서는 이 정책이 decode 중인 요청들에 불리하다. Prefill iteration 동안 decode 요청들은 한 iteration 전체를 잃는다. 짧은 prefill은 stall이 작아 큰 상관이 없지만, 수백 ms짜리 긴 prefill은 그 iteration에서 기다린 모든 decode 요청의 TPOT을 직접 끌어올린다.
Chunked Prefill의 동작
Sarathi(Agrawal et al., 2023)가 처음 제안한 chunked prefill의 핵심은 prefill을 고정 크기 chunk로 분할해서 매 iteration에 chunk 하나씩만 처리하는 것이다. 나머지 배치 슬롯에 decode 요청을 채워 함께 실행한다 — 논문에서 "piggybacking decodes with chunked prefills"라고 부르는 방식이다.
chunk 크기 2048로 32K prefill이 들어오면, 이 요청은 16번의 iteration에 나눠서 처리된다. 각 iteration은 prefill chunk 2048 토큰과 기존 decode 요청들의 토큰을 묶어 실행한다. 기존 방식이라면 32K iteration 하나가 decode 전체를 멈췄겠지만, chunked prefill에서는 각 iteration의 stall이 chunk 크기 수준으로 제한된다. 대신 총 prefill 완료 시간은 늘어난다. 32K를 한 번에 처리하는 것보다 chunk 16번으로 나누면 커널 호출과 attention 재구성 overhead가 붙는다.
chunk가 너무 작으면 GPU가 논다
Transformer attention은 시퀀스 내 토큰 간의 연산이 dense해서 compute-intensive하다. Chunk를 256 토큰처럼 매우 작게 쪼개면 attention이 담당하는 연산량이 줄고 커널 launch와 메모리 접근 overhead의 비중이 상대적으로 커진다. Arithmetic intensity가 낮아지면서 GPU SM이 놀게 된다.
Sarathi 논문은 이 tradeoff를 처음 명시적으로 측정했다. 최적 chunk 크기에서 벗어나면 개별 decode TPOT은 좋아지지만 시스템 전체 throughput이 떨어지는 지점이 존재한다. 논문 기준으로 LLaMA-13B를 A6000에서 운영했을 때 최적 chunk 크기에서 decode throughput이 10배 향상됐고, LLaMA-33B를 A100에서는 4.25배였다.
TTFT, TPOT, Throughput이 chunk 크기에 반응하는 방식
| 지표 | chunk 클수록 | chunk 작을수록 |
|---|---|---|
| TTFT | 나쁘다 (prefill 완료까지 iteration 수 증가) | 좋다 |
| p99 TPOT | 좋다 (iteration당 stall 감소) | 나쁘다 |
| Throughput | 일정 수준까지 개선 → 과도하면 stall 복귀 | 나쁘다 (GPU utilization 하락) |
chunk를 키우면 한 번의 iteration에서 더 많은 prefill 토큰을 처리하므로 TTFT가 나빠진다. 동시에 iteration당 stall이 줄어 decode p99 TPOT이 안정된다. chunk를 줄이면 반대다 — TTFT가 빨라지지만 GPU를 충분히 채우지 못해 throughput이 손해다. 세 지표는 동시에 최적화할 수 없고, chunk 크기가 그 균형점을 결정하는 단 하나의 파라미터가 된다.
vLLM과 SGLang 설정
vLLM V1에서 chunked prefill은 기본으로 켜져 있다. 핵심 파라미터는 --max-num-batched-tokens로, iteration당 처리 가능한 최대 토큰 수를 제한하며 실질적으로 chunk 크기를 결정한다.
# TPOT 우선 — decode-heavy 혼합 트래픽
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--max-num-batched-tokens 2048
# Throughput 우선 — 소형 모델을 대형 GPU에서
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--max-num-batched-tokens 8192
V0 스타일에서는 --enable-chunked-prefill 플래그를 명시해야 한다. vLLM 공식 문서는 기본값 2048을 제시하고, 소형 모델을 대형 GPU에서 throughput 최대화 목적으로 운영할 때 8192 이상을 권장한다.
SGLang에서는 --chunked-prefill-size로 동일 개념을 설정한다.
python -m sglang.launch_server \
--model meta-llama/Llama-3.1-8B-Instruct \
--chunked-prefill-size 2048
SGLang은 기본적으로 prefill과 decode를 분리 처리하므로, chunked prefill을 명시적으로 활성화해야 한다. 8K 이상 입력이 주를 이루는 워크로드에서 chunk 크기가 너무 작으면 prefill-bound 상태가 된다. --chunked-prefill-size 32768로 높였을 때 8K 입력 / 1K 출력 기준 8×H200에서 output throughput이 34-78% 향상되고 TTFT가 39-59% 단축된다는 측정이 있다.
Sarathi-Serve(arXiv:2403.02310)에서 chunk 기반 stall-free 스케줄링의 serving capacity를 측정한 결과: Mistral-7B 단일 A100에서 vLLM 대비 2.6배, Yi-34B 2×A100에서 3.7배, Falcon-180B 파이프라인 병렬에서 5.6배였다.
워크로드에 따른 판단
Chunked prefill이 효과적인 상황과 그렇지 않은 상황은 입력 길이 분포로 갈린다.
챗봇이나 코드 완성처럼 짧은 입력이 대부분인데 긴 컨텍스트 요청이 간헐적으로 섞이는 패턴에서 효과가 가장 크다. 긴 prefill이 decode 배치를 오염시키는 구조가 이 패턴에서 가장 심하게 드러나기 때문이다. 입력 p95 / p50 비율이 클수록 — 분포가 넓을수록 — chunked prefill의 가치가 커진다.
모든 요청의 입력 길이가 균일한 트래픽에서는 효과가 미미하다. 누구나 비슷한 stall을 만들고 비슷하게 받으므로, chunk로 분산해도 전체 분포가 달라지지 않는다.
요약이나 RAG처럼 prefill이 원래 긴 서비스에서 chunk를 너무 작게 설정하면 TTFT가 크게 늘어난다. TTFT SLA가 엄격하다면 chunk 크기를 키우거나 prefill을 단일 iteration으로 처리하는 편이 낫다. 긴 prefill이 지배적이면서 TTFT와 TPOT 둘 다 엄격하게 제어해야 한다면 chunked prefill 대신 prefill-decode 분리 서빙을 검토할 시점이다. 단일 GPU 풀 내 스케줄링으로 해결할 수 있는 범위를 넘어선다.
KV 캐시와 prefix caching
Chunked prefill이 켜지면 동일 요청의 KV 캐시가 여러 iteration에 걸쳐 점진적으로 쌓인다. chunk 하나가 처리될 때마다 해당 chunk의 KV 블록이 할당된다. PagedAttention 기반 시스템에서는 물리 블록을 필요할 때 할당하므로 이 점진적 증가 자체는 큰 문제가 아니다.
prefix caching과 함께 쓸 때는 주의할 점이 있다. Prefix caching은 여러 요청이 공유하는 앞부분 KV 캐시를 재사용하는 기능인데, chunked prefill로 공통 prefix가 여러 iteration에 나눠 처리되면 공통 prefix 블록이 완성되는 타이밍이 iteration 경계와 어긋날 수 있다. vLLM에서 두 기능을 동시에 켤 때 cache hit rate를 모니터링하면서 chunk 크기를 조정하는 게 좋다.