LLM 엔지니어링16장 · vLLM 서빙 벤치마킹과 RAG 최적화6 / 22← 이전목차다음 →MetaContents

16장. vLLM 서빙 벤치마킹과 RAG 최적화

연속 배치

놀이공원의 기차를 생각해 봅니다. 열 칸이 다 찰 때까지 기다렸다가 출발하고, 한 바퀴를 다 돌아야 모두 내린다면 짧게 타려던 사람도 끝까지 묶여 있습니다. 내리는 사람이 생길 때마다 그 자리에 다음 사람을 바로 태우는 순환 열차라면 빈자리가 거의 없습니다.

요청을 하나씩 처리, 정한 묶음으로 처리, 끝난 자리를 바로 채우는 연속 배치를 나란히 놓은 비교 그림

왼쪽은 요청을 하나씩 처리해 기다리는 줄이 길어지는 방식입니다. 가운데 정적 배치는 묶음을 한 번에 처리하지만 가장 긴 요청이 끝날 때까지 나머지 자리가 놀고 있습니다. 오른쪽 연속 배치는 토큰 한 걸음마다 끝난 요청을 빼고 기다리던 요청을 넣습니다. 기차의 칸이 배치의 자리, 한 바퀴가 묶음 하나의 생성입니다.

세 방식 견주기

방식처리량짧은 요청의 지연쓰는 곳
하나씩낮다앞 요청이 끝날 때까지 기다린다이 PC 의 Ollama 기본 설정(실습 2)
정적 배치높다묶음의 가장 긴 요청만큼 기다린다generate 에 여러 문장을 넣을 때(실습 3)
연속 배치가장 높다자기 길이만큼만vLLM, TGI, SGLang

걸음 수 흉내 내기

Python
outs = [20, 200, 30, 25, 180, 40, 15, 35]   # 요청 여덟 개의 출력 토큰 수(예시)
slots = 4                                   # 한 번에 함께 계산할 수 있는 자리

# 정적 배치: 넷씩 묶고, 묶음마다 가장 긴 요청만큼 걸음을 간다
static = sum(max(outs[i:i + slots]) for i in range(0, len(outs), slots))

# 연속 배치: 걸음마다 자리 넷을 채우고, 끝난 요청은 빼고 다음 요청을 넣는다
waiting, running, steps = list(outs), [], 0
while waiting or running:
    while waiting and len(running) < slots:
        running.append(waiting.pop(0))
    running = [r - 1 for r in running if r > 1]   # 한 걸음 가면 남은 토큰이 하나 준다
    steps += 1

print("하나씩:", sum(outs), "걸음 · 정적 배치:", static, "걸음 · 연속 배치:", steps, "걸음")

코드에 나오는 함수

코드쉬운 뜻
slots = 4slot 은 "자리, 홈"이라는 뜻이다. 한 걸음에 함께 계산할 수 있는 요청 수다
outs[i:i + slots]요청을 앞에서부터 넷씩 잘라 한 묶음으로 만든다
max(...)묶음 안에서 가장 긴 요청의 토큰 수를 고른다. 정적 배치는 이만큼 걸음을 간다
waiting.pop(0)기다리는 줄의 맨 앞 요청을 꺼낸다
[r - 1 for r in running if r > 1]자리에 있는 요청마다 남은 토큰을 하나씩 줄이고, 이번 걸음에 끝난 요청(남은 것이 1이던 요청)은 뺀다
steps += 1걸음 수를 하나 센다. 한 걸음은 모든 자리가 토큰 하나씩 만드는 계산 한 번이다

이 코드가 뜻하는 것

놀이공원 기차를 숫자로 돌려 본 것입니다. 손님 여덟 명이 타려는 바퀴 수가 저마다 다르고, 기차 자리는 넷입니다. 여기서 손님이 요청, 탈 바퀴 수가 출력 토큰 수, 기차가 한 바퀴 도는 것이 한 걸음입니다.

줄하는 일쉽게 말하면
sum(outs)545걸음한 명씩 태워 차례로 돈다
static = ...묶음 둘의 가장 긴 요청 200 + 180 = 380걸음넷이 다 내릴 때까지 다음 손님을 태우지 않는다
while waiting or running:200걸음내리는 사람이 생기면 그 자리에 바로 다음 사람을 태운다

연속 배치의 200걸음은 가장 긴 요청(200토큰)의 길이와 같습니다. 짧은 요청들이 긴 요청 옆 빈자리를 번갈아 채웠기 때문에, 가장 긴 요청이 끝나는 때에 모두 끝났습니다. 비유와 다른 점은, 실제 엔진에서는 묶음이 클수록 한 걸음의 시간도 조금씩 길어진다는 것입니다.

작게 줄여서 눈으로 보기

요청 셋, 자리 둘로 줄여서 걸음마다 누가 자리에 있는지 찍어 봅니다. 숫자는 설명용입니다.

Python
waiting = [("가", 3), ("나", 1), ("다", 2)]   # (요청 이름, 남은 토큰 수) - 설명용
running, step = [], 0
while waiting or running:
    while waiting and len(running) < 2:          # 자리는 둘
        running.append(waiting.pop(0))
    step += 1
    print(f"{step}걸음: 자리에 {[name for name, _ in running]}")
    running = [(name, left - 1) for name, left in running if left > 1]
출력
1걸음: 자리에 ['가', '나']
2걸음: 자리에 ['가', '다']
3걸음: 자리에 ['가', '다']
결과읽는 법
1걸음: 자리에 ['가', '나']앞의 둘이 먼저 자리에 앉는다. 나는 토큰 하나라 이 걸음에 끝난다
2걸음: 자리에 ['가', '다']나가 비운 자리에 기다리던 다가 바로 들어온다
3걸음: 자리에 ['가', '다']가(3토큰)와 다(2토큰)가 함께 끝난다. 모두 3걸음이다

정적 배치였다면 가·나 묶음이 3걸음, 다 혼자 2걸음으로 모두 5걸음이 걸렸을 것입니다.

주의할 점

  • 배치가 커지면 처리량은 늘지만 요청 하나의 지연도 조금씩 늘어납니다. 묶을수록 한 걸음에 계산할 것이 많아지기 때문입니다.
  • 이 PC 의 Ollama 는 기본 설정에서 요청을 하나씩 처리했습니다(실습 2). Ollama 에도 함께 처리할 요청 수를 정하는 환경 변수 OLLAMA_NUM_PARALLEL 이 있지만, 늘린 만큼 KV 캐시 메모리가 더 듭니다. 이 PC 에서는 서버 설정을 바꾸지 않았습니다.
← 이전 절16장 · KV 캐시와 PagedAttention다음 절 →16장 · 서빙 성능 지표