같은 모델에 비슷한 질문을 넣었는데 어떤 날은 곧바로 답이 나오고 어떤 날은 한참 기다리게 됩니다. 모델이 바뀐 게 아니라, 내 요청이 다른 사람들의 요청과 어떻게 묶이는지가 달라진 것.

MachineLearningMastery가 LLM 추론에서 쓰이는 배칭 세 가지를 작동 방식과 한계까지 정리했습니다. 배칭은 여러 요청을 묶어 한 번에 모델을 통과시키는 기법이죠. 어떻게 묶느냐에 따라 같은 하드웨어에서 응답 속도와 처리량이 크게 달라집니다.
출처: Static vs. Dynamic vs. Continuous Batching in LLM Inference – MachineLearningMastery
GPU는 대부분 놀고 있다
AI 모델을 돌리는 GPU는 상당 시간을 아무것도 하지 않고 보내죠. 요청 하나가 들어오면 처리하고, 다음 요청이 올 때까지 비어 있습니다. 거의 같은 비용으로 여러 개를 동시에 처리할 수 있는데도 그렇습니다.
여기서 비용의 정체는 가중치 로드죠. 모델 가중치를 GPU 메모리에서 불러오는 작업이 비싼데, 요청 여덟 개를 각각 처리하면 이걸 여덟 번 해야 합니다. 한 번 불러와 여덟 개를 함께 통과시키면 그만큼 아낍니다.
LLM에서는 이 문제가 더 까다롭죠. 어떤 요청은 토큰 몇 개로 끝나고 어떤 요청은 천 개까지 갑니다. 길이가 제각각인 일감을 다뤄야 한다는 점이 같은 크기의 작업이 줄지어 오는 경우와 다릅니다.
여덟 개가 찰 때까지 기다린다
정적 배칭은 가장 단순한 형태죠. 정해둔 개수가 다 모일 때까지 기다렸다가 한꺼번에 돌립니다. 배치 크기를 여덟로 잡았다면, 일곱 번째로 도착한 요청은 여덟 번째가 올 때까지 아무것도 못 합니다.
문제는 두 겹이죠. 먼저 도착한 요청이 나머지 자리가 찰 때까지 기다려야 하고, 일단 배치가 시작되면 가장 느린 요청이 끝날 때까지 전부 붙잡혀 있습니다. 짧은 요청 다섯 개가 긴 요청 하나에 발이 묶이는 상황.
그래서 정적 배칭은 실시간 서비스에 맞지 않습니다. 대신 저장해둔 데이터셋을 한꺼번에 처리하는 것처럼, 개별 응답을 기다리는 사람이 없고 전체가 빨리 끝나기만 하면 되는 작업에 어울리죠.
타이머를 붙이면 절반이 풀린다
동적 배칭은 같은 발상에 제한 시간을 더합니다. 최대 배치 크기와 타임아웃 창을 함께 두고, 둘 중 먼저 걸리는 쪽이 배치를 출발시키죠. 첫 요청이 도착하는 순간 타이머가 돌기 시작합니다.
정원이 먼저 차면 바로 실행하고, 타이머가 먼저 끝나면 모인 만큼만 내보내는 식. 배치가 시작되기까지의 최악의 대기 시간이 이렇게 묶이죠. 타임아웃을 짧게 잡으면 지연은 줄지만 배치가 작아져 효율이 떨어지고, 길게 잡으면 반대가 됩니다.
다만 두 번째 문제는 그대로 남죠. 배치가 일단 돌기 시작하면 가장 느린 요청이 끝날 때까지 나머지는 여전히 기다립니다. 이미지 생성기처럼 출력마다 걸리는 단계 수가 비슷하면 별문제가 아니지만, 토큰 다섯 개짜리와 오백 개짜리가 섞이는 LLM에서는 얘기가 다릅니다.
요청이 아니라 토큰 단위로 끊는다
연속 배칭은 스케줄링의 단위를 아예 바꾸죠. 요청이 아니라 디코딩 한 스텝을 기준으로 삼습니다. 배치 안의 각 시퀀스를 따로 추적하면서 한 번에 토큰 하나씩 나아갑니다.
작동은 이렇게 흐르죠.
- 매 반복마다 한 번의 순전파로 활성 시퀀스 전부의 다음 토큰을 뽑습니다
- 어떤 시퀀스가 종료 토큰을 내놓으면 그 즉시 배치에서 빠집니다
- 비워진 자리에 대기 중인 새 요청이 바로 다음 반복부터 들어갑니다
고정된 배치가 통째로 끝나기를 기다릴 필요가 없어지죠. 활성 시퀀스의 구성이 거의 매 스텝 바뀌는 셈이고, 그래서 GPU가 무언가를 기다리며 노는 시간이 크게 줄어듭니다.
물론 공짜는 아니죠. 새 요청이 들어올 때의 프리필, 그러니까 프롬프트 전체를 한 번에 읽어들이는 계산이 무거워서 그 순간 다른 시퀀스들의 토큰 생성이 밀립니다. 긴 프롬프트를 잘게 쪼개 여러 스텝에 나눠 처리하는 방식이 이 때문에 나왔죠. vLLM과 TensorRT-LLM, TGI 같은 LLM 서빙 프레임워크가 연속 배칭을 기본값으로 두는 이유입니다.
첫 토큰이 늦는 것과 이후가 느린 것은 다르다
같은 글의 다른 편은 무엇을 재야 하는지를 다룹니다. 지연 하나로는 부족하다는 게 요지죠. 첫 토큰이 나오기까지 걸리는 시간과, 그 뒤 토큰이 이어지는 간격은 서로 다른 지표입니다.
프롬프트 2,000토큰에 출력 20토큰인 요청과, 프롬프트 20토큰에 출력 2,000토큰인 요청을 생각해보죠. 총 토큰 수는 비슷해도 전자는 프리필을, 후자는 디코딩을 압박합니다. 성능 성격이 아예 다른 셈.
평균만 보면 놓치는 것도 있습니다. 대부분 1초에 끝나는데 몇 건이 10초씩 걸린다면 사용자는 그 몇 건을 기억하죠. 상위 백분위 지연을 함께 봐야 최악의 경우가 드러납니다.
내가 겪는 지연은 어느 쪽인가
이 구조를 알면 체감이 달리 읽히죠. 답이 시작되기까지 오래 걸리는 것과, 시작된 뒤 글자가 느리게 흐르는 것은 원인이 다르니까 말입니다. 앞쪽은 대기열과 프리필의 문제고, 뒤쪽은 디코딩 처리량의 문제입니다.
트래픽이 몰리는 시간대에 첫 글자가 유독 늦는다면 내 요청이 다른 요청들과 자리를 다투고 있다는 신호입니다. 반대로 트래픽이 한산할 때는 요청 단위 동적 배칭 쪽이 첫 토큰을 더 빨리 내놓기도 하죠. 연속 배칭이 언제나 이기는 건 아닌 셈.
모델을 고를 때 벤치마크 점수만 보게 되지만, 실제 체감 속도는 그 모델을 누가 어떤 방식으로 서빙하느냐에서 갈립니다. 속도를 기준으로 모델을 고르는 흐름이 생기는 배경에도 이 층이 깔려 있죠.
참고자료:
- Measuring Performance of Transformer Inference – MachineLearningMastery

답글 남기기