프리필이 왜 병목인가

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으로 공통 구간을 재사용한다. 애플리케이션에서는 경계를 지키는 청킹으로 컨텍스트 한계와 검색 품질을 관리한다. 두 층위를 실측 지표로 함께 튜닝할 때 지연과 처리량이 동시에 안정된다.