- 스케줄러가 매 스텝에 푸는 문제
- 대기 큐와 실행 큐
- 선점: 이미 돌던 요청을 뒤로 물리기
- recompute와 swap
- 스케줄링 정책: fcfs와 priority
- 선점이 보이면 무엇을 해야 하는가
- 직접 해보기
- 참고 자료
스케줄러가 매 스텝에 푸는 문제
3편에서 vLLM이 스텝마다 배치를 다시 구성한다는 것을 봤습니다. 그 결정을 실제로 내리는 것이 스케줄러입니다. 매 스텝 스케줄러는 두 가지 한도를 동시에 만족하는 조합을 찾아야 합니다.
하나는 토큰 예산입니다. 이번 스텝에 처리할 토큰 수의 합이 상한을 넘으면 안 됩니다. V1 스케줄러 소스를 보면 요청을 하나 넣을 때마다 예산에서 그만큼을 빼 나가고, 전체 합이 상한을 넘지 않도록 확인합니다.
다른 하나는 KV 캐시 블록입니다. 이쪽이 훨씬 까다롭습니다. 토큰 예산은 계산하면 나오지만, 블록은 지금 실제로 비어 있어야 합니다. 그리고 이미 실행 중인 요청들도 매 스텝 새 토큰을 만들면서 블록을 더 달라고 합니다. 즉 스케줄러가 상대하는 자원은 가만히 있지 않고 계속 줄어듭니다.
내용은 2026-08-12에 공식 문서·소스에서 확인했습니다. vLLM은 변화가 빠르니 설정값과 동작은 사용 중인 버전의 문서로 다시 확인하세요.
대기 큐와 실행 큐
구조 자체는 단순합니다. V1 스케줄러 소스에는 대기 중인 요청을 담는 큐와 실행 중인 요청 목록이 나란히 있습니다. 실행 목록은 평범한 리스트이고, 대기 큐는 스케줄링 정책에 따라 다른 형태로 만들어집니다.
[대기 큐] 아직 한 번도 안 돌았거나, 물러난 요청들
│
│ 블록이 확보되면 승격
▼
[실행 목록] 이번 스텝 배치에 들어가는 요청들
│
│ 블록이 모자라면 선점되어 되돌아감
└──────────────▶ 다시 대기 큐 (맨 앞자리로)
되돌아가는 화살표가 이 글의 주인공입니다. 그리고 중요한 세부가 하나 있습니다. 선점된 요청은 대기 큐의 맨 뒤가 아니라 맨 앞에 놓입니다. 소스에서 요청을 큐 앞쪽에 다시 넣는 동작으로 확인할 수 있습니다. 방금 밀려난 요청이 곧바로 다시 후보가 된다는 뜻이고, 덕분에 특정 요청이 계속 밀려 굶는 상황을 피합니다.
선점: 이미 돌던 요청을 뒤로 물리기
KV 캐시가 모자라면 스케줄러는 새 요청을 안 받는 데서 그치지 않고, 이미 돌고 있던 요청을 물러나게 합니다. 이것이 선점입니다.
왜 이렇게까지 하느냐면 대안이 없기 때문입니다. 실행 중인 요청 전부가 다음 토큰을 만들려면 블록이 더 필요한데 남은 블록이 없다면, 누군가는 나가야 나머지가 전진합니다. 아무도 안 내보내면 전부 멈춥니다.
공식 최적화 문서는 이때 나오는 경고를 예시로 보여 줍니다.
WARNING 05-09 00:49:33 scheduler.py:1057 Sequence group 0 is preempted by
PreemptionMode.RECOMPUTE mode because there is not enough KV cache space.
로그 형식과 줄 번호는 버전마다 다르지만, 봐야 할 부분은 마지막 구절입니다. KV 캐시 공간이 부족하다는 것. 이 줄이 로그에 반복해서 찍히고 있다면 그 배포는 이미 용량을 넘겨 돌고 있는 상태입니다.
선점이 무서운 이유는 비용이 눈에 잘 안 띄기 때문입니다. 요청이 실패하지 않습니다. 에러도 안 납니다. 그저 느려집니다. 그것도 평균이 아니라 꼬리에서 느려집니다. 대시보드의 평균 지연은 멀쩡한데 일부 사용자만 유난히 오래 기다리는 상황이 만들어집니다.
recompute와 swap
물러난 요청의 KV 캐시는 어떻게 할까요. 역사적으로 두 가지 방법이 있었습니다.
재계산은 그냥 버리는 쪽입니다. 나중에 다시 실행될 때 프롬프트부터 다시 계산합니다. 메모리를 즉시 온전히 회수하는 대신 그동안의 계산을 날립니다. V1 스케줄러 소스를 보면 선점된 요청의 상태를 표시하고 지금까지 계산한 토큰 수를 0으로 되돌립니다. 진행 상황이 초기화된다는 사실이 이 한 줄에 그대로 드러납니다.
스왑은 KV 캐시를 CPU 메모리로 옮겨 두었다가 되돌리는 쪽입니다. 계산은 아끼지만 GPU와 CPU 사이로 데이터를 왕복시키는 비용이 듭니다.
지금 기준으로 알아야 할 것은 이것입니다. 공식 문서는 vLLM V1의 기본 선점 방식이 스왑이 아니라 재계산이며, V1 구조에서는 재계산 쪽의 부담이 더 작다고 밝히고 있습니다. 그리고 V1 가이드는 GPU와 CPU 사이의 KV 캐시 스왑이 제거된 기능 목록에 올라 있다고 적고 있습니다. 그러니 오래된 글에서 본 스왑 관련 설정을 지금 그대로 적용하려 들면 안 됩니다. 사용 중인 버전에서 그런 인자가 실제로 존재하는지부터 확인하세요.
스케줄링 정책: fcfs와 priority
스케줄러 설정에는 정책 항목이 있습니다. main 브랜치 SchedulerConfig 소스에서 policy의 기본값은 fcfs이고, 선착순 처리를 뜻합니다. 다른 선택지는 priority로, 요청에 주어진 우선순위를 따릅니다. OpenAI 호환 서버의 추가 파라미터 목록에도 priority가 들어 있습니다.
정책은 선점 대상을 고르는 방식까지 바꿉니다. V1 스케줄러 소스를 보면 우선순위 정책에서는 실행 중인 요청 가운데 우선순위와 도착 시각을 기준으로 가장 뒤에 놓이는 요청을 골라 물러나게 하고, 그 외 정책에서는 목록의 마지막 요청을 꺼냅니다.
실무에서 이 선택이 갈리는 지점은 분명합니다. 모든 요청이 대등하면 선착순으로 충분합니다. 반면 대화형 요청과 대량 배치 작업이 한 엔드포인트를 공유한다면, 우선순위를 쓰지 않는 한 배치 작업이 대화형 사용자를 밀어냅니다. 다만 정책을 바꾸기 전에 먼저 물어야 할 질문이 있습니다. 지금 겪는 문제가 순서 문제인지 용량 문제인지입니다. 용량이 모자란 상태에서 순서만 바꾸면 누가 손해를 볼지가 달라질 뿐 총량은 그대로입니다.
선점이 보이면 무엇을 해야 하는가
공식 최적화 문서가 제시하는 대응은 네 가지입니다. gpu_memory_utilization을 올려 KV 캐시에 쓸 공간을 늘리기, max_num_seqs나 max_num_batched_tokens를 줄여 한 스텝에 살아 있는 요청을 줄이기, tensor_parallel_size를 올리기, pipeline_parallel_size를 올리기입니다.
앞의 둘과 뒤의 둘은 성격이 다릅니다. 앞의 둘은 지금 GPU 안에서 배분을 바꾸는 일이고, 뒤의 둘은 GPU를 더 쓰는 일입니다. 순서대로 시도하는 편이 낫습니다. 대부분의 배포는 배분만 고쳐도 선점이 사라집니다. 여기서 max_num_seqs를 줄이는 것이 처리량을 떨어뜨릴 것 같지만 실제로는 반대인 경우가 많습니다. 선점이 반복되는 상태는 이미 재계산으로 낭비하고 있는 상태라서, 동시 요청 수를 줄여 선점을 없애는 편이 순처리량에 유리합니다.
직접 해보기
- LLM GPU 메모리(VRAM) 계산기 — 선점은 결국 KV 캐시 용량 문제입니다. 지금 설정에서 몇 개의 시퀀스가 동시에 살 수 있는지 계산해 보세요.
- K8s 실습 랩 — 쿠버네티스 위에 모델 서버를 올린다면 리소스 요청과 한도, 롤아웃 같은 기본기를 여기서 손으로 익혀 두면 좋습니다.
- 이전 편: vLLM 내부 구조 (3) — 연속 배칭이 GPU를 놀게 두지 않는 방법
- 다음 편: vLLM 내부 구조 (5) — 접두사 캐싱과 KV 재사용
참고 자료
- vLLM Optimization and Tuning (docs.vllm.ai) — 선점 경고 예시, V1의 기본 선점 방식이 재계산이라는 서술, 대응 네 가지의 출처입니다.
- vLLM V1 Scheduler 소스 (GitHub main) — 대기 큐와 실행 목록, 선점 시 계산 토큰 수 초기화, 정책별 선점 대상 선택을 읽은 곳입니다.
- vLLM V1 Guide (docs.vllm.ai) — GPU와 CPU 사이 KV 캐시 스왑이 제거되었다는 서술의 출처입니다.
- vLLM SchedulerConfig 소스 (GitHub main) —
policy기본값과 선택지를 읽은 곳입니다.
현재 단락 (1/38)
3편에서 vLLM이 스텝마다 배치를 다시 구성한다는 것을 봤습니다. 그 결정을 실제로 내리는 것이 스케줄러입니다. 매 스텝 스케줄러는 두 가지 한도를 동시에 만족하는 조합을 찾아야...