정적 배칭이 버리는 것
한 줄 요약
정적 배칭은 배치 안에서 가장 긴 요청이 끝날 때까지 나머지 슬롯을 놀린다. 연속 배칭은 끝난 슬롯을 즉시 새 요청으로 채운다. 그 차이가 처리량 2~5배다.
왜 이게 필요했나
이미지 분류 같은 작업에서 배칭은 단순합니다. 입력 크기가 같고 처리 시간도 같으니 32개를 모아 한 번에 돌리면 됩니다.
LLM 생성은 다릅니다. 각 요청의 출력 길이가 제각각입니다. 어떤 요청은 10토큰, 어떤 요청은 1,000토큰입니다. 정적 배칭으로 32개를 묶으면, 31개가 10토큰에서 끝나도 1,000토큰짜리가 끝날 때까지 그 31개 슬롯이 비어 있습니다. GPU 는 32개 분량의 자원을 잡고 1개를 처리합니다.
어떻게 동작하나
연속 배칭(continuous batching, 또는 in-flight batching)은 배치를 고정된 묶음이 아니라 유동적인 슬롯 집합으로 봅니다. 매 디코딩 반복마다 스케줄러가 확인합니다 — 끝난 시퀀스가 있으면 슬롯에서 빼고, 대기 큐에 요청이 있으면 그 슬롯에 넣습니다.
그러면 GPU 는 항상 최대치에 가까운 배치를 유지합니다. 벤치마크에서 정적 배칭 대비 처리량이 2~5배 향상되는 것으로 보고됩니다.
여기서 조절할 수 있는 값이 둘입니다. max_num_seqs 는 동시에 처리할 시퀀스 수의 상한이고, max_num_batched_tokens 는 한 반복에서 처리할 토큰 수의 상한입니다. 전자는 KV 캐시 메모리에 묶이고 후자는 연산 능력에 묶입니다.
트레이드오프가 있습니다. 동시성을 올리면 처리량이 오르지만 개별 요청의 지연이 늘어납니다. 그리고 KV 캐시가 부족해지면 스케줄러가 일부 시퀀스를 선점(preempt)해서 내보냈다가 나중에 다시 넣습니다. 이때 그 시퀀스의 KV 캐시를 버리고 다시 계산하거나 CPU 로 스왑하는데, 둘 다 비쌉니다. 선점이 자주 일어나면 처리량이 오히려 떨어집니다.
또 하나의 후보가 프리픽스 캐싱입니다. 여러 요청이 같은 시스템 프롬프트로 시작하면 그 부분의 KV 캐시를 재사용할 수 있습니다. 이득은 공통 접두어 길이, 재사용률, 구현과 메모리 정책에 따라 달라지며, 프롬프트가 매번 완전히 다르면 효과가 거의 없습니다. 실제 요청 분포에서 캐시 적중률과 TTFT 를 함께 측정해야 합니다.
현장에서 만나는 모습
큐 깊이와 꼬리 TTFT 의 관계를 보면 운영 감각이 생깁니다. 유입이 처리 능력에 가까워지면 대기 큐가 자라고 높은 백분위 지연이 평균보다 먼저 나빠집니다. 제품의 사용자 경험과 트래픽 규모에 맞춰 p95 또는 p99 같은 꼬리 백분위를 SLO 지표로 선택합니다.
그리고 목표를 정할 때 순서가 있습니다. 먼저 제품과 워크로드에 맞는 TTFT SLO 를 정하고(이 실습에서는 계산을 위해 200ms 를 예시로 사용), 그것을 만족하는 최대 동시성을 찾고, 그 동시성에서의 처리량을 용량 계획의 근거로 씁니다. 처리량만 먼저 최대화하면 꼬리 지연 SLO 를 놓치기 쉽습니다.
KV 캐시가 진짜 한계선이다
연속 배칭의 동시성을 무엇이 막는지 물으면 대개 연산 능력이라고 답하지만, 실제로 먼저 바닥나는 것은 거의 항상 메모리다. 생성 중인 시퀀스는 지금까지의 모든 토큰에 대한 키와 값을 들고 있어야 하고, 그 크기는 토큰 수에 비례해 계속 자란다. 요청 하나가 4,000토큰까지 가면 그 요청의 캐시도 처음의 몇 배가 된다.
그래서 배치 크기를 정하는 계산은 이렇게 흘러간다. 모델 가중치가 차지한 나머지가 캐시에 쓸 수 있는 몫이고, 그것을 요청 하나가 평균적으로 쓰는 캐시 크기로 나눈 값이 동시에 담을 수 있는 시퀀스 수다. 여기서 평균이 아니라 긴 쪽을 봐야 한다. 대부분의 요청이 짧아도 긴 요청 몇 개가 캐시를 점유하면 그만큼 슬롯이 줄어든다.
캐시가 부족해지면 스케줄러가 시퀀스를 선점해 내보내는데, 이 순간이 운영에서 가장 위험한 지점이다. 내보낸 시퀀스는 나중에 다시 들어와 처음부터 다시 계산되므로, 이미 한 일을 또 하게 된다. 부하가 조금 늘었을 뿐인데 처리량이 갑자기 꺾이는 절벽이 여기서 생긴다. 지켜볼 지표는 셋이다.
| 지표 | 무엇을 말하나 | 나쁜 신호 |
|---|---|---|
| KV 캐시 사용률 | 메모리 여유 | 90% 근처에서 오래 머문다 |
| 선점 횟수 | 되돌린 작업의 양 | 0이 아니면 이미 한계를 넘었다 |
| 대기 큐 길이 | 받아 놓고 못 하는 양 | 계속 자라면 유입을 막아야 한다 |
대응은 세 갈래다. 요청당 최대 출력 길이를 제한해 캐시가 무한정 자라지 못하게 하고, max_num_seqs 를 선점이 일어나지 않는 값까지 낮추고, 그래도 모자라면 인스턴스를 늘린다. 선점을 감수하고 동시성을 높게 두는 선택은 거의 언제나 손해다. 겉으로는 더 많이 받는 것처럼 보이지만 실제로는 같은 계산을 두 번 하고 있기 때문이다.
한 가지 더 짚어 둘 것이 있다. 긴 요청과 짧은 요청을 같은 인스턴스에 섞으면, 긴 요청이 캐시를 점유하는 동안 짧은 요청의 지연이 함께 나빠진다. 대화형 응답과 배치 성격의 긴 생성을 다른 인스턴스로 나누는 것은 자원 낭비가 아니라 꼬리 지연을 지키는 가장 확실한 방법이다.
다음 실습에서 할 것
정적 배칭과 연속 배칭 스케줄러를 각각 시뮬레이션으로 구현해 처리량을 비교합니다. 동시성 상한을 적용하고, 큐 깊이와 p99 대기의 관계를 관찰하고, prefill 과 decode 의 비용을 분리해 모델링한 뒤, 실습용 예시 SLO 인 TTFT 200ms 를 만족하는 최대 동시성을 탐색합니다.