정적 배칭의 한계를 분석하고, Continuous Batching의 iteration-level 스케줄링 원리와 vLLM, TGI, TensorRT-LLM의 구현 차이를 비교합니다.
1장에서 Decode 단계가 메모리 바운드라고 설명했습니다. 토큰 하나를 생성하기 위해 모델 가중치 전체를 HBM에서 읽어와야 하는데, 실제 연산량은 매우 적습니다. 이것은 GPU의 연산 자원이 크게 낭비되고 있다는 뜻입니다.
**배칭(Batching)**은 여러 요청을 묶어 한 번에 처리함으로써 이 문제를 완화합니다. 모델 가중치를 HBM에서 한 번 읽어올 때, 여러 요청의 토큰을 동시에 연산하면 메모리 읽기 비용을 공유할 수 있습니다.
배치 크기가 커질수록 산술 강도(Arithmetic Intensity)가 높아져서, 메모리 바운드에서 컴퓨트 바운드로 전환됩니다. 이것이 배칭이 추론 최적화의 핵심인 이유입니다.
가장 단순한 배칭 방식은 **정적 배칭(Static Batching)**입니다. 여러 요청을 모아 하나의 배치로 구성하고, 배치 내 모든 요청이 완료될 때까지 기다렸다가 다음 배치를 처리합니다.
시간 →
요청 A: [████████████████████████████████] (128토큰 생성)
요청 B: [████████________padding________] (32토큰 생성, 96토큰 대기)
요청 C: [████████████____padding________] (48토큰 생성, 80토큰 대기)
↑ ↑
B 완료 A 완료 → 전체 배치 완료정적 배칭에는 세 가지 근본적인 문제가 있습니다.
배치 내 요청들의 출력 길이가 다르면, 짧은 요청은 긴 요청이 끝날 때까지 빈 연산(패딩)으로 GPU 자원을 낭비합니다. 실제 워크로드에서 출력 길이의 분산은 매우 크기 때문에, 이 낭비는 상당합니다.
하나의 매우 긴 요청이 배치 전체를 지연시킵니다. 배치 내 3개 요청이 이미 완료되었더라도, 나머지 1개 요청이 끝나기 전에는 새로운 요청을 받을 수 없습니다.
배치 간 전환 시점에 GPU가 유휴 상태가 되고, 패딩 연산도 실질적으로 유휴 상태와 다르지 않습니다.
정적 배칭의 핵심 문제는 "배치"를 원자적(Atomic) 단위로 취급한다는 점입니다. 배치 전체가 시작되고, 배치 전체가 완료될 때까지 아무것도 변경할 수 없습니다.
Continuous Batching은 배치를 원자적 단위가 아닌, iteration(단일 Decode 스텝) 단위로 관리합니다. 매 iteration마다 스케줄러가 배치의 구성을 재평가하고, 완료된 요청을 빼고 새로운 요청을 넣을 수 있습니다.
Iteration 1: [A, B, C, D] ← 4개 요청 처리
Iteration 2: [A, B, C, D] ← B 완료 (EOS 토큰 생성)
Iteration 3: [A, E, C, D] ← B 제거, 대기열의 E 투입
Iteration 4: [A, E, C, D] ← D 완료
Iteration 5: [A, E, C, F] ← D 제거, F 투입
...매 iteration이 끝날 때마다 다음을 결정합니다.
Continuous Batching이 처리량을 극대화하는 핵심 메커니즘을 분석합니다.
정적 배칭에서는 배치 전환 시점과 패딩 구간에서 GPU가 유휴 상태가 됩니다. Continuous Batching은 항상 처리할 요청이 있는 한 GPU가 쉬지 않습니다.
메모리가 허용하는 범위에서 배치 크기가 자동으로 조절됩니다. 짧은 요청이 많이 들어오면 배치가 커지고, 긴 컨텍스트 요청이 들어오면 배치가 작아집니다. KV 캐시 메모리가 부족해지면 새 요청의 투입을 지연시키는 배압(Backpressure) 메커니즘이 작동합니다.
고급 스케줄러에서는 한 iteration 안에서 일부 요청은 Prefill, 일부는 Decode를 동시에 수행하는 **혼합 배칭(Mixed Batching)**을 지원합니다. 이를 통해 새 요청의 TTFT를 줄이면서도 기존 요청의 Decode를 계속 진행할 수 있습니다.
Prefill과 Decode의 혼합은 연산 특성이 매우 다르기 때문에 주의가 필요합니다. Prefill은 컴퓨트 바운드, Decode는 메모리 바운드이므로, 혼합 비율에 따라 전체 시스템의 성능 특성이 달라집니다. 일부 시스템은 Prefill과 Decode를 별도 GPU로 분리하는 disaggregated serving 접근법을 사용하기도 합니다.
Continuous Batching의 또 다른 핵심 이점은 요청별 독립 완료입니다. 각 요청은 자신의 생성이 끝나는 즉시 결과를 반환합니다.
# Continuous Batching의 요청별 독립 완료 시뮬레이션
class ContinuousBatchScheduler:
def __init__(self, max_batch_size: int, max_memory_blocks: int):
self.max_batch = max_batch_size
self.max_blocks = max_memory_blocks
self.active: list[Request] = []
self.waiting: list[Request] = []
self.used_blocks = 0
def step(self) -> list[Request]:
"""한 iteration 수행"""
completed = []
# 1. 완료된 요청 제거
for req in self.active[:]:
if req.is_finished():
self.active.remove(req)
self.used_blocks -= req.num_blocks
completed.append(req)
# 2. 새 요청 투입
while (self.waiting
and len(self.active) < self.max_batch
and self.used_blocks + self.waiting[0].initial_blocks
<= self.max_blocks):
new_req = self.waiting.pop(0)
self.active.append(new_req)
self.used_blocks += new_req.initial_blocks
# 3. 활성 요청 Decode 수행
for req in self.active:
req.decode_one_token()
return completed이 방식은 사용자 경험에도 직접적인 영향을 미칩니다. 정적 배칭에서는 응답이 "뭉치(Burst)"로 돌아오지만, Continuous Batching에서는 각 요청의 응답이 준비되는 즉시 스트리밍됩니다.
vLLM의 스케줄러는 PagedAttention과 긴밀하게 통합되어 있습니다. 블록 매니저가 보고하는 가용 블록 수를 기반으로 배치 크기를 동적으로 결정합니다.
Hugging Face의 TGI는 Rust 기반의 고성능 서버로, Continuous Batching을 Token Streaming과 결합합니다.
NVIDIA의 TensorRT-LLM은 하드웨어 최적화에 중점을 둡니다.
| 특성 | vLLM | TGI | TensorRT-LLM |
|---|---|---|---|
| 언어 | Python + CUDA | Rust + Python | C++ + CUDA |
| PagedAttention | 네이티브 지원 | 지원 | 자체 구현 |
| 모델 지원 폭 | 매우 넓음 | 넓음 | NVIDIA 최적화 모델 |
| GPU 최적화 깊이 | 중간 | 중간 | 매우 깊음 |
| 배포 편의성 | 높음 | 높음 | 중간 |
| 커뮤니티 | 매우 활발 | 활발 | NVIDIA 주도 |
배치 크기는 지연시간과 처리량의 트레이드오프를 결정하는 핵심 파라미터입니다.
배치 크기 | 지연시간(TPOT) | 처리량(TPS) | GPU 활용률
1 | 최소 | 낮음 | 낮음 (메모리 바운드)
4 | 약간 증가 | 중간 | 중간
16 | 증가 | 높음 | 높음
64 | 크게 증가 | 매우 높음 | 매우 높음 (컴퓨트 바운드)
256+ | 매우 큼 | 포화 | 포화 (메모리 한계)지연시간 민감 서비스 (챗봇): 배치 크기를 작게 유지하되, 최소 4-8로 설정해 기본적인 효율을 확보합니다. TPOT SLO를 기준으로 상한을 설정합니다.
처리량 우선 서비스 (배치 처리): 메모리가 허용하는 최대 배치 크기를 사용합니다. KV 캐시 메모리가 병목이 되는 시점까지 배치를 키웁니다.
혼합 워크로드: Continuous Batching의 동적 조절에 의존하되, max_num_seqs와 max_num_batched_tokens 파라미터로 상한을 제어합니다.
vLLM에서 --max-num-seqs 파라미터는 동시에 처리할 수 있는 최대 요청 수를, --max-num-batched-tokens는 한 iteration에서 처리할 최대 토큰 수를 제한합니다. 프로덕션 환경에서는 이 두 값을 SLO와 GPU 메모리에 맞게 조절하는 것이 중요합니다.
이 장에서는 Continuous Batching의 원리와 구현을 살펴보았습니다.
다음 장에서는 자기회귀 생성의 순차적 한계를 우회하는 영리한 접근법인 Speculative Decoding을 다룹니다. 작은 모델의 "추측"을 큰 모델이 검증하는 방식으로 어떻게 수학적으로 동일한 출력을 유지하면서 2-3배의 속도 향상을 달성하는지 알아보겠습니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
Draft-Verify 패러다임으로 자기회귀 디코딩을 가속하는 Speculative Decoding의 원리, 수학적 보장, 그리고 Medusa, Eagle 등 변형 기법을 분석합니다.
OS 가상 메모리에서 영감받은 PagedAttention의 원리를 설명하고, vLLM의 아키텍처와 Automatic Prefix Caching, 계층적 KV 캐시를 분석합니다.
시스템 프롬프트 캐싱, Prefix-aware 스케줄링, RadixAttention의 원리를 분석하고, 멀티턴 대화와 평가 워크플로우에서의 성능 개선을 다룹니다.