- Published on
vLLM 내부 구조 (3) — 연속 배칭이 GPU를 놀게 두지 않는 방법
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 정적 배칭이 GPU를 놀리는 방식
- 이터레이션 단위 스케줄링
- prefill과 decode는 성격이 다르다
- 청크드 프리필: 두 성격을 한 배치에 섞기
- 토큰 예산이라는 하나의 손잡이
- 무엇을 먼저 만질 것인가
- 직접 해보기
- 참고 자료
정적 배칭이 GPU를 놀리는 방식
2편에서 메모리를 봤으니 이번에는 시간을 봅니다. GPU를 얼마나 쉬지 않게 하느냐의 문제입니다.
GPU는 한 번에 많은 일을 해야 효율이 납니다. 그래서 요청을 모아 배치로 묶습니다. 여기까지는 누구나 같습니다. 문제는 그다음입니다. 소박한 구현은 배치를 한 번 묶으면 그 배치가 다 끝날 때까지 구성을 바꾸지 않습니다.
정적 배칭 — 배치가 전부 끝날 때까지 자리를 비워 둔다
스텝 → 1 2 3 4 5 6 7 8 9 10 11 12
요청 A ■ ■ ■ ✓
요청 B ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ✓
요청 C ■ ■ ■ ■ ■ ✓
요청 D ■ ■ ✓
■ 계산 중 ✓ 완료 빈칸 = GPU가 아무 일도 안 하는 자리
요청 D는 3스텝 만에 끝났지만 그 자리는 12스텝까지 비어 있습니다. 더 나쁜 것은, 지금 대기 큐에 요청 E가 들어와 있어도 이 배치가 끝날 때까지 시작조차 못 한다는 점입니다. LLM에서 이 손실이 유독 큰 이유는 응답 길이의 편차가 극단적이기 때문입니다. 같은 서비스 안에서 어떤 답은 20토큰이고 어떤 답은 2000토큰입니다.
내용은 2026-08-12에 공식 문서·소스에서 확인했습니다. vLLM은 변화가 빠르니 설정값과 동작은 사용 중인 버전의 문서로 다시 확인하세요.
이터레이션 단위 스케줄링
연속 배칭의 발상은 간단합니다. 배치 구성을 요청 단위가 아니라 스텝 단위로 다시 정합니다. 모델을 한 번 돌릴 때마다 스케줄러가 다시 묻습니다. 끝난 요청이 있나, 있으면 빼고, 그 자리에 대기 중인 요청을 넣자.
연속 배칭 — 빈자리가 생기는 즉시 다음 요청이 들어온다
스텝 → 1 2 3 4 5 6 7 8 9 10 11 12
요청 A ■ ■ ■ ✓
요청 B ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ✓
요청 C ■ ■ ■ ■ ■ ✓
요청 D ■ ■ ✓
요청 E ■ ■ ■ ■ ✓
요청 F ■ ■ ■ ■ ■ ■ ✓
요청 G ■ ■ ■ ■ ■ ✓
같은 12스텝 안에 요청 세 개가 더 들어갔습니다. GPU가 한 일의 총량은 비슷한데 처리한 요청 수가 늘었습니다. 이것이 vLLM 처리량의 핵심입니다.
여기서 자연스럽게 따라오는 제약이 하나 있습니다. 새 요청을 중간에 끼워 넣으려면 그 요청의 KV 캐시 블록을 그 자리에서 확보할 수 있어야 합니다. 2편의 블록 구조가 없으면 이 유연함이 성립하지 않습니다. 연속 배칭과 PagedAttention은 사실 한 세트입니다.
prefill과 decode는 성격이 다르다
여기까지는 어느 서빙 엔진 설명에나 나오는 이야기입니다. 실제 튜닝은 그다음 사실에서 갈립니다. 요청 하나의 생애에는 성격이 완전히 다른 두 구간이 있습니다.
프리필은 입력 프롬프트 전체의 KV를 한꺼번에 계산하는 구간입니다. 토큰 4000개짜리 프롬프트라면 4000개를 병렬로 처리합니다. 계산량이 많고 GPU 연산 유닛이 바쁘게 돌아갑니다.
디코드는 그 뒤로 토큰을 하나씩 만들어 내는 구간입니다. 요청 하나당 이번 스텝에 처리하는 새 토큰이 딱 하나입니다. 계산량은 적은데 모델 가중치와 KV 캐시를 전부 읽어야 합니다. 연산보다 메모리를 읽어 오는 쪽이 병목이 됩니다.
이 차이가 실무에서 이렇게 나타납니다. 누군가 4만 토큰짜리 문서를 붙여 넣고 요약을 요청하면, 그 프리필이 도는 동안 다른 사용자들의 토큰 출력이 눈에 띄게 멈칫합니다. 프리필 한 덩어리가 스텝 하나를 통째로 차지해 버리기 때문입니다.
청크드 프리필: 두 성격을 한 배치에 섞기
해법은 긴 프리필을 통째로 처리하지 않고 잘라서 여러 스텝에 나눠 넣는 것입니다. 이것이 chunked prefill입니다. 그러면 한 스텝의 배치가 "긴 프롬프트의 프리필 일부 + 여러 요청의 디코드 토큰"으로 섞입니다.
청크드 프리필 없음
스텝 N : [40000토큰 프리필] ← 다른 요청 전부 대기
스텝 N+1 : [디코드 · 디코드 · 디코드 · 디코드]
청크드 프리필 있음
스텝 N : [프리필 조각 · 디코드 · 디코드 · 디코드]
스텝 N+1 : [프리필 조각 · 디코드 · 디코드 · 디코드]
스텝 N+2 : [프리필 조각 · 디코드 · 디코드 · 디코드]
V1이 이것을 자연스럽게 하는 이유는 스케줄러 설계에 있습니다. 공식 V1 가이드에 따르면 V1의 통합 스케줄러는 프롬프트 토큰과 출력 토큰을 구분하지 않고 똑같이 다루며, 요청별로 이번 스텝에 몇 토큰을 줄지를 정해 고정된 토큰 예산을 나눠 씁니다. 프리필 단계와 디코드 단계를 엄격히 가르지 않기 때문에 청크드 프리필과 접두사 캐싱 같은 기능이 한 틀에서 돌아갑니다.
기본값도 달라졌습니다. V1 가이드는 청크드 프리필이 가능한 한 언제나 기본으로 켜진다고 밝히고 있고, V0에서는 모델 특성에 따라 조건부로 켜졌다고 대비해 설명합니다. main 브랜치의 SchedulerConfig 소스에서도 enable_chunked_prefill의 선언 기본값이 참으로 되어 있습니다.
토큰 예산이라는 하나의 손잡이
그래서 이 스텝에 넣을 토큰의 총량을 정하는 값이 max_num_batched_tokens이고, 동시에 처리할 시퀀스 수의 상한이 max_num_seqs입니다. main 브랜치 SchedulerConfig에는 각각의 클래스 기본값이 2048과 128로 선언되어 있습니다. 다만 실제로 적용되는 값은 사용 맥락과 환경에 따라 조정될 수 있으니 기동 로그로 확인하세요.
공식 최적화 문서가 알려 주는 방향은 이렇습니다. max_num_batched_tokens를 2048처럼 작게 두면 토큰 사이 간격, 즉 스트리밍이 끊기지 않는 느낌이 좋아집니다. 크게 두면 첫 토큰까지의 시간이 좋아집니다. 처리량을 우선한다면 문서는 이 값을 8192보다 크게 두기를 권하며, 특히 큰 GPU에 작은 모델을 올린 경우에 그렇다고 덧붙입니다.
주의할 조건도 문서에 명시되어 있습니다. 청크드 프리필을 끈 경우에는 max_num_batched_tokens가 max_model_len보다 커야 합니다. 그렇지 않으면 최대 길이의 프롬프트가 들어왔을 때 한 스텝에 담을 방법이 없습니다.
무엇을 먼저 만질 것인가
정리하면 순서는 이렇습니다.
스트리밍이 뚝뚝 끊긴다는 불만이 있으면 max_num_batched_tokens를 줄이는 쪽을 봅니다. 긴 프롬프트가 짧은 요청들을 막고 있을 가능성이 큽니다.
첫 응답이 늦다는 불만이면 반대로 키우는 쪽을 봅니다. 다만 이때 개별 사용자의 토큰 출력은 조금 덜 매끄러워질 수 있습니다. 두 지표는 같은 예산을 나눠 쓰는 관계라 한쪽을 좋게 하면 다른 쪽이 밀립니다.
동시 접속이 많은데 처리량이 안 오르면 max_num_seqs와 KV 캐시 여유를 함께 봅니다. 시퀀스 상한이 아니라 캐시가 부족해서 못 넣는 상황이면, 다음 편에서 다룰 선점이 이미 일어나고 있을 수 있습니다.
직접 해보기
- LLM GPU 메모리(VRAM) 계산기 — 동시에 몇 개의 요청을 살려 둘 수 있는지는 결국 KV 캐시 용량이 정합니다. 배치 크기를 올리기 전에 여기서 먼저 확인하세요.
- LLM API 비용 계산기 — 처리량이 오르면 요청당 비용이 어떻게 달라지는지 워크로드를 넣어 비교해 보세요.
- 이전 편: vLLM 내부 구조 (2) — PagedAttention은 KV 캐시를 왜 페이지로 쪼갰는가
- 다음 편: vLLM 내부 구조 (4) — 스케줄러와 선점
참고 자료
- vLLM V1 Guide (docs.vllm.ai) — 통합 스케줄러가 프롬프트와 출력 토큰을 같게 다룬다는 설명, 청크드 프리필의 기본 활성화 서술의 출처입니다.
- vLLM Optimization and Tuning (docs.vllm.ai) —
max_num_batched_tokens조정 방향과 청크드 프리필을 끈 경우의 제약이 적힌 곳입니다. - vLLM SchedulerConfig 소스 (GitHub main) —
enable_chunked_prefill기본값과 토큰 예산 관련 클래스 기본값을 읽은 곳입니다.