AI Sparkup

최신 AI 쉽게 깊게 따라잡기⚡

LLM 추론 튜토리얼 – Continuous Batching으로 다중 사용자 요청 효율화하기

llm-inference에서 다룬 것처럼, LLM은 프리필(prefill) 후 토큰을 하나씩 디코드한다. 현실의 추론 서버는 수백~수천 개의 요청을 동시에 처리해야 한다. 서버가 이 요청들을 어떻게 스케줄링하느냐가 GPU가 유용한 일을 하느냐 아니면 공회전하느냐를 결정한다. Continuous Batching은 정적 배칭의 두 가지 낭비를 해결해 처리량을 극적으로 향상시킨다.

정적 배칭의 두 가지 문제

1. 짧은 요청이 긴 요청을 기다린다

정적 배칭은 배치 내 모든 요청이 완료될 때까지 슬롯을 개방하지 않는다. 4개 요청 중 하나가 1000 토큰 출력을 생성 중이라면, 나머지 3개가 100 토큰에서 끝났어도 새 요청은 대기한다.

2. 패딩으로 GPU 사이클을 낭비한다

배치 내 요청마다 길이가 다르면 짧은 요청에 패딩 토큰을 채워 가장 긴 것에 맞춰야 한다. 어텐션 연산의 복잡도가 O(N²)이므로, 패딩 토큰 하나도 실제 계산 비용을 발생시킨다.

Continuous Batching의 해결책

동적 스케줄링 (Dynamic Scheduling)

요청이 완료되는 즉시 그 슬롯에 새 요청을 받는다. 배치 전체가 끝날 때까지 기다리지 않는다. 구현하면:

class DynamicScheduler:
    def __init__(self, max_batch_size):
        self.max_batch_size = max_batch_size
        self.active = []   # 현재 디코딩 중인 요청
        self.queue = []    # 대기 중인 요청

    def step(self):
        # 완료된 요청 제거
        self.active = [r for r in self.active if not r.is_done()]
        # 빈 슬롯에 새 요청 채우기
        while len(self.active) < self.max_batch_size and self.queue:
            self.active.append(self.queue.pop(0))
        return self.active

래기드 배칭 (Ragged Batching)

패딩 없이 서로 다른 길이의 프롬프트를 하나의 배치로 처리한다. 배치가 스텝마다 변경되며 인과 마스크(causal mask)도 함께 갱신된다.

def ragged_batch_forward(requests):
    # 패딩 없이 실제 토큰만 연결
    input_ids = torch.cat([r.current_tokens for r in requests])
    # 요청별 위치 정보 유지
    position_ids = torch.cat([r.positions for r in requests])
    return model(input_ids, position_ids=position_ids)

정적 배칭 vs Continuous Batching

항목정적 배칭Continuous Batching
슬롯 개방 시점배치 전체 완료 후개별 요청 완료 즉시
패딩 토큰최장 요청 기준 패딩없음 (래기드)
GPU 활용률낮음 (패딩·대기 낭비)높음
지연(latency)긴 요청에 묶임독립적
구현 복잡도단순중간

실제 vLLM, TGI 등 프로덕션 추론 서버는 모두 Continuous Batching을 기본으로 채택한다.

어텐션 복잡도와의 관계

어텐션은 O(N²) 복잡도를 가진다. 패딩 토큰을 제거하면 배치당 실제 처리하는 토큰 수 N이 줄어 연산 시간이 비선형적으로 감소한다. 같은 GPU 메모리에서 더 많은 요청을 동시에 처리할 수 있다.

더 읽을 자료

참고 자료



AI Sparkup 구독하기

최신 게시물 요약과 더 심층적인 정보를 이메일로 받아 보세요! (무료)