프리필이 왜 병목인가
LLM 추론은 입력 토큰 전체의 KV 캐시를 한 번에 계산하는 프리필(prefill)과, 토큰을 하나씩 생성하는 디코드(decode)로 나뉜다. 프리필은 어텐션 특성상 시퀀스 길이에 대해 대략 O(n²) 연산이 붙고, 32K~128K 같은 긴 입력에서는 첫 토큰까지의 지연(TTFT)이 수 초로 뛴다. 디코드가 메모리 대역폭에 묶이는 것과 달리 프리필은 연산(compute) 바운드이고, 하나의 긴 요청이 GPU를 통째로 점유해 다른 요청의 디코드를 굶기는 문제가 생긴다.
청크 단위 프리필의 원리
해결의 핵심은 긴 프리필을 고정 크기 청크로 쪼개서, 프리필 조각과 진행 중인 디코드 요청을 같은 배치에 섞는 것이다(continuous batching + chunked prefill). 이렇게 하면 긴 요청이 스케줄러를 독점하지 않고, GPU 연산 유닛과 메모리 대역폭을 동시에 채워 처리량과 TTFT를 함께 개선한다. vLLM·TensorRT-LLM 등 최신 엔진이 기본 지원한다.
# vLLM: chunked prefill 활성화
from vllm import LLM, SamplingParams
llm = LLM(
model="meta-llama/Llama-3.1-8B-Instruct",
enable_chunked_prefill=True,
max_num_batched_tokens=2048, # 한 스텝에서 처리할 프리필 청크 상한
max_num_seqs=64,
gpu_memory_utilization=0.90,
)
out = llm.generate(["아주 긴 문서 ..."], SamplingParams(max_tokens=256))
청크 크기 튜닝
max_num_batched_tokens가 핵심 손잡이다. 너무 작으면 프리필이 여러 스텝으로 늘어져 긴 요청의 TTFT가 나빠지고, 너무 크면 디코드 요청이 다시 지연된다. 실측 기반으로 워크로드에 맞춰 잡는다.
| 청크 크기 | TTFT(긴 입력) | 디코드 지연 | 적합 상황 |
|---|---|---|---|
| 작음(512) | 나쁨 | 좋음 | 대화형, 짧은 응답 다수 |
| 중간(2048) | 보통 | 보통 | 혼합 트래픽 기본값 |
| 큼(8192) | 좋음 | 나쁨 | 배치·오프라인 처리 |
프리픽스 캐시로 재계산 제거
RAG나 시스템 프롬프트처럼 여러 요청이 앞부분을 공유하면, 그 구간의 KV를 캐시해 프리필을 통째로 건너뛸 수 있다. 공통 접두사가 길수록 절감 효과가 크므로, 고정 프롬프트를 앞에, 가변 입력을 뒤에 배치하는 것이 원칙이다.
# APC(Automatic Prefix Caching) 켜서 서빙
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--enable-prefix-caching \
--enable-chunked-prefill \
--max-num-batched-tokens 2048 \
--max-model-len 65536
애플리케이션 레벨 청킹
엔진 프리필 청킹과 별개로, 모델 컨텍스트를 넘는 문서는 애플리케이션에서 잘라 넣어야 한다. 이때 문장·문단 경계를 지키고 청크 간 오버랩을 둬 경계에서 의미가 끊기지 않게 한다.
def chunk_text(text, size=1200, overlap=150):
chunks, start = [], 0
while start < len(text):
end = min(start + size, len(text))
# 문장 경계에서 자르기
if end < len(text):
dot = text.rfind(". ", start, end)
if dot > start:
end = dot + 1
chunks.append(text[start:end])
start = end - overlap # 오버랩 유지
return chunks
주의점
- 캐시 무효화: 접두사가 1토큰만 달라져도 프리픽스 캐시는 그 지점부터 재계산한다. 타임스탬프·랜덤 ID를 프롬프트 앞부분에 넣지 말 것.
- 메모리 압박: 프리픽스 캐시와 KV 캐시는 같은 GPU 메모리를 놓고 경쟁한다.
gpu_memory_utilization을 과하게 잡으면 OOM이나 요청 선점(preemption)이 늘어난다. - 측정 지표 분리: TTFT와 토큰당 지연(TPOT)을 따로 본다. 평균만 보면 청킹으로 개선된 꼬리 지연을 놓친다.
- 청킹 정확도: 무의미한 고정 길이 절단은 검색 품질을 떨어뜨린다. 오버랩과 경계 보존이 없으면 RAG 재현율이 눈에 띄게 낮아진다.
정리
긴 컨텍스트의 병목은 프리필이고, 대응은 두 층위다. 서빙 엔진에서는 chunked prefill로 프리필과 디코드를 섞고 prefix caching으로 공통 구간을 재사용한다. 애플리케이션에서는 경계를 지키는 청킹으로 컨텍스트 한계와 검색 품질을 관리한다. 두 층위를 실측 지표로 함께 튜닝할 때 지연과 처리량이 동시에 안정된다.