정적 배칭이 GPU를 놀리는 이유
LLM 추론에서 가장 흔한 병목은 GPU 연산량이 아니라 배치 구성 방식이다. 정적 배칭(static batching)은 배치에 들어온 모든 요청이 끝날 때까지 다음 배치를 시작하지 않는다. 문제는 요청마다 출력 토큰 길이가 크게 다르다는 점이다. 20토큰이면 끝나는 요청과 800토큰을 생성하는 요청이 같은 배치에 묶이면, 짧은 요청이 끝난 자리(슬롯)는 긴 요청이 끝날 때까지 빈 채로 남는다. 이 빈 슬롯 동안 GPU는 사실상 놀고 있고, 실측 이용률이 30~40%대에 머무는 경우가 흔하다.
연속 배칭의 핵심: 반복 단위 스케줄링
연속 배칭(continuous batching)은 배치 단위가 아니라 디코딩 반복(iteration) 단위로 스케줄링한다. 매 토큰 생성 스텝마다 완료된 요청을 배치에서 빼고, 대기 큐에서 새 요청을 즉시 채워 넣는다. 슬롯이 비는 순간 다음 요청이 진입하므로 GPU 점유율이 유지된다. vLLM, TGI, TensorRT-LLM이 모두 이 방식을 기본으로 쓴다.
| 방식 | 스케줄 단위 | 빈 슬롯 | 지연/처리량 |
|---|---|---|---|
| 정적 배칭 | 배치 완료 | 많음 | 지연 큼, 처리량 낮음 |
| 동적 배칭 | 타임아웃 취합 | 중간 | 절충 |
| 연속 배칭 | 토큰 반복 | 거의 없음 | 처리량 높음 |
큐와 KV 캐시가 실제 상한을 정한다
연속 배칭의 동시 처리량은 GPU 코어가 아니라 KV 캐시 메모리가 결정한다. 요청마다 (프롬프트+생성) 토큰 수에 비례해 KV 캐시를 점유하기 때문이다. vLLM의 PagedAttention은 캐시를 페이지 블록으로 나눠 단편화를 줄이지만, 총 블록 수를 넘는 요청은 대기 큐에 머문다. 따라서 큐 관리는 곧 메모리 예산 관리다.
from vllm import LLM, SamplingParams
llm = LLM(
model="meta-llama/Llama-3.1-8B-Instruct",
# KV 캐시로 쓸 GPU 메모리 비율. 0.90이 실무 기본선
gpu_memory_utilization=0.90,
# 한 반복에서 처리할 최대 토큰 수(prefill+decode 합)
max_num_batched_tokens=8192,
# 동시 진행 시퀀스 상한. 큐 대기 임계를 좌우
max_num_seqs=256,
)
params = SamplingParams(temperature=0.7, max_tokens=512)
outputs = llm.generate(["요약해줘: ..."], params)
서빙 설정으로 큐 임계 조정
OpenAI 호환 서버로 띄울 때는 위 파라미터를 그대로 CLI에 넘긴다. max_num_seqs를 올리면 동시성은 늘지만 요청당 KV 캐시가 줄어 긴 컨텍스트가 선점(preemption)당하고, 낮추면 지연은 안정되나 처리량이 준다.
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3.1-8B-Instruct \
--gpu-memory-utilization 0.90 \
--max-num-batched-tokens 8192 \
--max-num-seqs 256 \
--max-model-len 8192 \
--enable-chunked-prefill # 긴 프롬프트를 잘라 디코딩과 섞어 처리
prefill과 decode의 간섭 다루기
새 요청의 프롬프트 처리(prefill)는 연산 집약적이라, 진행 중인 디코딩 스텝에 끼어들면 순간 지연(TTFT, TPOT)이 튄다. chunked prefill은 긴 프롬프트를 조각내 디코딩 반복 사이에 나눠 넣어 이 간섭을 완화한다. 프롬프트가 길고 동시성이 높은 워크로드일수록 효과가 크다.
운영 시 봐야 할 지표
튜닝은 감이 아니라 지표로 한다. 최소한 다음을 관측한다.
- 대기 큐 길이: 지속 증가하면
max_num_seqs또는 GPU 증설 필요. - 선점(preemption) 횟수: 잦으면 KV 캐시 부족 신호. 동시성을 낮춘다.
- TTFT / TPOT: prefill 간섭과 배치 크기의 균형 확인.
- KV 캐시 사용률: 90% 상시 초과면 컨텍스트 상한을 조인다.
주의점
처리량과 지연은 상충한다. 배치를 키우면 총 처리량은 오르지만 개별 요청의 TPOT는 나빠질 수 있어, SLA가 지연 중심이면 max_num_seqs를 보수적으로 잡아야 한다. 또한 gpu_memory_utilization을 0.95 이상으로 올리면 활성화·통신 버퍼 여유가 사라져 OOM이 난다. 스트리밍 응답을 쓰면 짧은 요청의 슬롯 회수가 빨라져 큐 회전이 개선되니, 가능하면 스트리밍을 기본값으로 두는 편이 유리하다. 마지막으로 벤치마크는 반드시 실제 프롬프트 길이 분포로 부하를 재현해야 한다. 균일 길이로 측정한 처리량은 운영 환경에서 그대로 재현되지 않는다.