디코드는 메모리 대역폭에 묶여 있어서 한 스텝에 요청을 여러 개 묶을수록 처리량이 오른다. 요청을 묶는 가장 단순한 방법은 몇 개를 모아 한 묶음으로 실행하고, 묶음이 다 끝나면 다음 묶음을 받는 것이다. 이 방식을 정적 배치(static batching)라고 부른다. 구현이 쉽고 묶음 안에서는 GPU를 효율적으로 쓰지만 생성 길이가 요청마다 다른 언어 모델에서는 계산과 시간을 함께 버린다.
동작 방식
서버는 들어온 요청을 대기열에 쌓는다. 대기열에 요청이 N개 모이거나 정해 둔 대기 시간이 지나면 그때까지 모인 요청으로 묶음을 만든다. 묶음 안의 프롬프트를 한 번에 프리필하고 묶음 전체가 끝날 때까지 디코드 스텝을 반복한다. 묶음 안의 마지막 요청이 끝나야 다음 묶음을 시작한다.
묶음은 하나의 텐서로 계산하므로 요청들의 길이를 맞춰야 한다. 프롬프트가 짧은 요청은 가장 긴 프롬프트 길이까지 패딩 토큰으로 채우고, 생성이 먼저 끝난 요청도 묶음이 끝날 때까지 자리를 비우지 않는다. 두 경우 모두 GPU는 결과를 버릴 토큰까지 계산한다.
패딩
프롬프트 길이가 50, 300, 2,000토큰인 요청 셋을 한 묶음으로 프리필하면 세 요청 모두 2,000토큰짜리로 계산한다. 실제로 필요한 토큰은 2,350개인데 계산하는 토큰은 6,000개다. 프리필은 연산 성능에 묶인 단계라 버리는 토큰 수만큼 시간이 그대로 늘어난다.
필요한 토큰 50 + 300 + 2,000 = 2,350
계산한 토큰 2,000 × 3 = 6,000
쓸모 있는 비율 2,350 ÷ 6,000 ≈ 39%
길이가 비슷한 요청끼리 묶으면 패딩을 줄일 수 있다. 대기열의 요청을 프롬프트 길이 순으로 정렬해 묶는 버킷팅이 그 방법이다. 다만 길이가 맞는 요청이 모일 때까지 기다리는 시간이 생기고 출력 길이는 요청이 끝나기 전에는 알 수 없으므로 디코드 쪽 낭비는 그대로 남는다.
낙오자 문제
출력 길이 차이는 패딩보다 더 큰 낭비를 만든다. 요청 8개를 묶었는데 하나가 500토큰을 생성하고 나머지 7개가 20토큰에서 끝났다고 하자. 7개는 20스텝 만에 끝나지만 묶음은 500스텝 동안 돈다. 그동안 빈 자리 7개도 계속 계산에 포함된다.
필요한 토큰 500 + 7 × 20 = 640
계산한 토큰 8 × 500 = 4,000
쓸모 있는 비율 640 ÷ 4,000 = 16%
디코드는 대역폭에 묶여 있으므로 빈 자리를 계산하는 비용 자체는 크지 않다. 스텝 시간은 가중치를 읽는 시간이 정하고 빈 자리 7개를 빼도 거의 줄지 않는다. 문제는 그 자리를 다른 요청이 쓸 수 없다는 점이다. 20스텝 뒤에 비는 자리 7개에 대기열의 요청을 넣었다면 같은 500스텝 동안 훨씬 많은 요청을 처리할 수 있었다. 정적 배치는 끝난 요청의 자리를 묶음이 끝날 때까지 돌려주지 않아서, 배치를 키워 얻으려던 처리량을 길이가 가장 긴 요청 하나가 깎아 먹는다.
대화형 서비스의 출력 길이는 몇 토큰짜리 짧은 답부터 수천 토큰짜리 긴 설명까지 넓게 퍼진다. 분포가 넓을수록 묶음 안에서 가장 긴 요청과 나머지의 차이가 커지고, 낭비도 그만큼 커진다.
대기 시간
정적 배치에서는 요청이 묶음 단위로 시작하고 끝난다. 이 구조가 첫 토큰까지의 시간(TTFT)을 두 번 늘린다.
묶음이 찰 때까지 기다린다. 서버는 요청이 N개 모이거나 대기 시간이 지나야 묶음을 시작한다. 대기 시간을 짧게 두면 묶음이 덜 차서 처리량이 떨어지고 길게 두면 한가한 시간대에 혼자 들어온 요청이 그 시간만큼 첫 토큰을 늦게 받는다. 트래픽이 일정하면 적당한 값을 찾을 수 있지만 실제 트래픽은 시간대마다 크게 오르내린다.
앞 묶음이 끝날 때까지 기다린다. 묶음이 도는 동안 들어온 요청은 다음 묶음을 기다려야 한다. 앞 묶음에 500토큰을 생성하는 요청이 있고 디코드 스텝이 5ms라면 앞 묶음은 2.5초 동안 돈다. 그 사이에 들어온 요청은 짧은 질문이라도 최대 2.5초를 기다린 뒤에야 프리필을 시작한다.
앞 묶음 시간 ≈ 가장 긴 출력 길이 × 스텝 시간 = 500 × 5ms = 2.5초
두 번째 대기는 대기 시간 설정으로 줄일 수 없다. 묶음이 끝나는 시점을 가장 긴 출력이 정하기 때문이다.
정적 배치가 맞는 경우
정적 배치가 늘 나쁜 것은 아니다. 묶음 안의 요청 길이가 비슷하고 지연 목표가 없으면 단순한 구현이 오히려 장점이 된다.
- 임베딩·분류 모델: 출력이 토큰 하나 또는 벡터 하나라서 디코드 단계가 없다. 프롬프트 길이로 버킷팅하면 패딩 낭비도 작다
- 출력 길이를 고정한 오프라인 작업: 최대 생성 길이를 짧게 제한하고 길이가 비슷한 입력을 모아 돌리는 일괄 처리
- 실험·평가 스크립트: 처리량보다 구현의 단순함과 재현성이 중요한 경우
대화형 서비스처럼 출력 길이가 넓게 퍼지고 요청이 불규칙하게 들어오는 부하에서는 낙오자 문제와 묶음 대기를 피할 수 없다. 이 두 문제는 묶음 단위로 시작하고 끝내는 구조에서 나오므로 해결하려면 스케줄링 단위를 묶음이 아니라 스텝으로 바꿔야 한다.