vLLM 0.25 Model Runner V2: 비동기 추론 경로의 재설계와 마이그레이션
이번 변경을 단순한 버전 업데이트로 보면 안 되는 이유
vLLM 0.25.0은 2026년 7월 11일 공개되었고, Model Runner V2(MRV2)를 dense 생성 모델의 기본 실행 경로로 전환했다. 운영자가 별도 플래그를 주지 않아도 기존 Model Runner V1 대신 새 경로가 선택될 수 있다는 뜻이다.
이 변화의 핵심은 새로운 attention kernel 하나가 아니다. 스케줄러가 넘긴 요청을 GPU 실행에 필요한 tensor로 바꾸고, 모델을 실행하고, sampling 결과를 다시 요청별 상태에 반영하는 서빙 런타임의 중심 루프를 다시 설계한 것이다. 기존 경로는 기능이 늘어날 때마다 persistent batch, async scheduling, CUDA graph, speculative decoding을 덧붙였다. MRV2는 이들을 처음부터 함께 작동하도록 재구성했다.
업그레이드 판단도 "새 버전이 더 빠른가"에서 끝나지 않는다. 다음 네 가지를 확인해야 한다.
- 우리 모델과 옵션이 실제로 MRV2를 선택하는가.
- 선택되지 않을 때 V1으로 조용히 fallback하는가, 아니면 실패하는가.
- 같은 출력 계약과 latency 분포를 유지하는가.
- 문제가 생겼을 때
VLLM_USE_V2_MODEL_RUNNER=0으로 빠르게 되돌릴 수 있는가.
이 장은 vLLM 0.25.0 소스와 release note를 기준으로 한다. 이후 버전에서는 기본 선택 조건과 미지원 기능이 달라질 수 있으므로, 실제 배포 버전의
vllm/config/vllm.py를 함께 확인해야 한다.
Model Runner는 어디에 있는가
서빙 시스템에서 scheduler와 model kernel 사이에는 생각보다 많은 일이 있다. 매 step마다 완료·중단·선점된 요청을 제거하고, 새 요청에 slot을 배정하고, block table과 token 위치를 갱신하고, sampling parameter를 요청별 tensor로 만들어야 한다. 그 뒤에야 model forward와 sampling을 실행할 수 있다.
MRV2가 바꾼 것은 이 중간 계층이다.
재설계 1: 요청 상태의 위치와 실행 순서를 분리한다
V1도 매 step마다 tensor 전체를 다시 만들지 않기 위해 persistent batch를 사용했다. 문제는 persistent tensor 자체가 곧 model과 sampler의 입력이었다는 점이다. 실행 순서가 바뀌면 tensor 행도 함께 재배치해야 했고, 아직 살아 있는 요청의 행이 덮일 수 있어 CachedRequestState라는 별도 사본까지 유지했다.
MRV2는 두 종류의 순서를 분리한다.
- 요청 상태의 물리적 위치: 요청이 활성 상태인 동안 고정 slot을 사용한다. 대부분의 플랫폼에서 기본 최대 요청 수는 1,024개다.
- 현재 step의 실행 순서: attention backend와 scheduler가 정한 순서에 맞춰 필요한 행을 gather한다.
요청 A가 7번 slot에 있다면 다음 step에서 배치의 첫 번째가 되더라도 상태를 0번 행으로 옮기지 않는다. idx_mapping이 "이번 배치의 첫 번째 요청은 persistent slot 7"이라고 가리키고, GPU가 필요한 값을 모은다. 선점(preemption)은 기존 slot에서 완료된 것으로 처리하고, 재개할 때 새 요청처럼 다시 등록한다.
이 분리로 얻는 것은 단순한 코드 정리가 아니다.
- 요청 join/finish 때 tensor 전체를 재정렬하는 비용과 복잡도가 줄어든다.
- async scheduling 중 CPU와 GPU가 같은 행을 서로 다른 의미로 해석할 가능성이 작아진다.
- model state, LoRA state, sampling state, block table이 공통 request index를 기준으로 모듈화된다.
대신 고정 slot에는 상한이 있다. max_num_reqs와 실제 동시 요청 수의 관계를 확인해야 하며, "slot이 고정"이라는 말은 요청이 선점된 뒤에도 같은 slot을 보장한다는 뜻이 아니다.
재설계 2: barrier로 보호하지 말고 race가 생기지 않는 소유권을 만든다
비동기 스케줄링의 목표는 GPU가 step N을 실행하는 동안 CPU가 step N+1을 준비하는 것이다. 이때 pinned CPU buffer를 non_blocking=True로 GPU에 복사한 직후 CPU가 같은 buffer를 수정하면 race가 발생한다.
V1은 critical section 주변에 async barrier를 두는 방식으로 이를 막았다. 하지만 새 buffer가 추가될 때 보호 범위를 빠뜨릴 수 있고, barrier가 넓어질수록 CPU와 GPU를 겹쳐 실행하는 이점이 줄어든다.
MRV2는 수정 가능한 persistent CPU state와 GPU가 읽는 임시 pinned copy를 분리한다.
# 개념을 단순화한 MRV2 방식
persistent = torch.zeros(max_num_reqs, dtype=torch.int32, device="cpu")
persistent[req_idx] = new_value
staging = persistent.pin_memory()
device_state = staging.to("cuda", non_blocking=True)GPU는 staging을 읽고 CPU는 persistent를 수정하므로 같은 메모리를 동시에 건드리지 않는다. 큰 tensor를 매번 통째로 복사하면 이 방식도 비싸다. 그래서 block table처럼 큰 상태에는 별도 전략이 필요하다.
재설계 3: 큰 block table은 staged diff만 보낸다
MRV2의 StagedWriteTensor는 큰 tensor의 기준본을 GPU에 두고 CPU에서는 변경분만 모은다.
- CPU가
(row, start, value)형태로 여러 변경을 기록한다. - 변경분을 연속된 staging buffer로 묶는다.
- 작은 buffer만 GPU로 비동기 복사한다.
- 하나의 kernel이 GPU 기준본에 변경을 적용한다.
요청 하나에 KV cache block 몇 개가 추가되었다고 해서 max_num_reqs × max_num_blocks 전체 table을 다시 보내지 않는다. 실제 GPUModelRunner.execute_model() 경로도 요청 추가와 갱신 뒤 block_tables.apply_staged_writes()를 호출하고, 현재 step의 idx_mapping으로 필요한 block table만 gather한다.
여기서 vLLM 0.25.0 release note의 "PagedAttention 제거"를 오해하면 안 된다. PR #47361에서 삭제된 것은 오래된 paged_attention_v1.cu, paged_attention_v2.cu와 해당 custom op다. MRV2 소스에는 여전히 block table, slot mapping, KV cache block 관리가 존재한다. 즉, paged KV cache라는 메모리 관리 아이디어를 버린 것이 아니라 legacy attention 구현을 제거한 것에 가깝다.
재설계 4: input 준비와 sampling을 GPU 쪽으로 옮긴다
서빙 step은 작은 Python 연산이 매우 자주 반복된다. 요청별 token 수를 누적해 query_start_loc을 만들고, input_ids, positions, seq_lens, speculative token mapping을 조합하는 비용이 각각 작아 보여도 high-throughput 환경에서는 CPU 병목이 된다.
MRV2는 이 계산의 상당 부분을 Triton kernel로 옮긴다.
input_ids,positions,seq_lens, slot mapping 준비- sampled token과 draft token의 결합
- Gumbel-max 기반 sampling
- 전체 vocabulary의 logprob를 먼저 만들지 않는 top-k logprobs
- 긴 prompt의 logprob를 더 작은 단위로 나누는 처리
특히 speculative decoding에서는 GPU가 직전 step의 수락·거절 결과를 알고 있지만 CPU는 아직 모를 수 있다. GPU resident state와 indirection mapping을 사용하면 결과를 CPU로 되가져온 뒤 다음 입력을 만드는 왕복을 줄일 수 있다.
MRV2는 CUDA graph 관리도 별도 CUDAGraphManager로 명시한다. 0.25.0에서는 dynamic speculative decoding과 full CUDA graph를 함께 쓸 수 있도록 가능한 draft 길이에 맞는 graph shape을 준비한다. 다만 graph capture가 많아지면 startup 시간과 memory가 늘 수 있으므로, "지원된다"와 "우리 트래픽에 유리하다"를 같은 뜻으로 해석하면 안 된다.
0.25.0에서 실제로 어떤 경로가 선택되는가
환경변수를 지정하지 않으면 vLLM이 모델과 기능 조합을 보고 선택한다. release note의 "all dense models"에는 실행 조건이 붙는다.
| 조건 | 기본 선택 결과 |
|---|---|
runner_type == generate인 dense model | MRV2 후보 |
| hybrid model 또는 attention-free model | V1 |
| Triton을 사용할 수 없음 | 경고 후 V1 fallback |
| MRV2 미지원 기능을 사용 | 경고 후 V1 fallback |
| 일부 명시된 MoE architecture | MRV2 후보 |
| 그 밖의 MoE model | V1 |
VLLM_USE_V2_MODEL_RUNNER=1 | MRV2 강제, 미지원 조합이면 validation error |
VLLM_USE_V2_MODEL_RUNNER=0 | V1 강제 |
0.25.0 소스에서 기본 MRV2 대상으로 명시된 MoE architecture는 DeepseekV2ForCausalLM, Qwen2MoeForCausalLM, GraniteMoeForCausalLM이다. 이 목록은 버전마다 달라질 수 있다.
운영에서 가장 위험한 경우는 업그레이드는 했지만 실제 canary가 V1 fallback으로 실행되어 MRV2를 전혀 검증하지 못한 경우다. 테스트 환경에서는 먼저 MRV2를 강제해 미지원 기능을 오류로 드러내는 편이 안전하다.
# 1. MRV2 호환성 검증: fallback을 허용하지 않는다.
VLLM_USE_V2_MODEL_RUNNER=1 vllm serve "$MODEL" \
--port 8001 \
--max-model-len 8192
# 2. 같은 이미지와 모델로 rollback 기준선을 만든다.
VLLM_USE_V2_MODEL_RUNNER=0 vllm serve "$MODEL" \
--port 8002 \
--max-model-len 8192프로덕션에서 환경변수를 생략해 자동 선택을 쓸 수는 있다. 그러나 배포 manifest에는 선택 결과를 명시적으로 남기는 편이 재현성과 rollback에 유리하다.
현재 제한을 배포 전에 읽어야 한다
0.25.0의 _get_v2_model_runner_unsupported_features()는 다음 조합을 MRV2 미지원으로 판정한다. 목록을 외워서는 안 된다. 배포 버전을 올릴 때마다 소스나 startup validation으로 다시 확인해야 한다.
| 영역 | 0.25.0에서 확인할 제한 |
|---|---|
| 병렬화 | prefill context parallelism, TP와 함께 쓰는 sequence parallelism, external_launcher에서 PP 2 이상, dual batch overlap, elastic expert parallelism |
| compile | stock torch.compile mode |
| speculative decoding | ngram, ngram_gpu, 지원 목록 밖의 method, 일부 EAGLE parallel drafting, EAGLE3와 pipeline parallelism 조합 |
| 확장 지점 | custom logits processor와 plugin, routed experts capture |
| 입력·출력 | prompt embeds, raw_logits 또는 processed_logits logprobs mode |
| cache·transfer | KV sharing fast prefill, EC transfer |
thinking_token_budget는 별도 경고 대상이다. MRV2를 강제했을 때 이 요청 parameter가 기대대로 적용된다고 가정하지 말고 API contract test를 추가해야 한다.
이 제한은 MRV2가 실패작이라는 뜻이 아니다. 오히려 자동 선택 로직이 모르는 조합을 억지로 실행하지 않도록 경계를 둔 것이다. 다만 자동 fallback은 가용성을 지켜주지만 검증의 진실성을 가릴 수 있다. CI와 canary에서는 강제 모드, 안정화된 production에서는 명시적으로 선택한 모드를 쓰는 이유다.
마이그레이션은 성능 시험이 아니라 동등성 시험부터 시작한다
새 runner의 목적이 효율 개선이어도 첫 gate는 성능이 아니다. 같은 요청이 같은 서비스 계약을 지키는지 먼저 확인한다.
1단계: 정적 호환성
- 사용 중인 model architecture가 dense, MoE, hybrid 중 어디에 해당하는지 확인한다.
- quantization, LoRA, prefix caching, speculative method, parallelism 조합을 inventory로 만든다.
- production과 동일한 옵션으로
VLLM_USE_V2_MODEL_RUNNER=1startup을 실행한다. - validation error와 warning을 배포 실패로 취급한다. warning을 로그에만 남기고 진행하지 않는다.
2단계: 출력 계약
고정 seed만 비교해서는 부족하다. sampling은 backend와 수치 경로에 따라 bitwise 동일하지 않을 수 있다. 대신 서비스가 요구하는 계약을 층별로 확인한다.
- greedy decoding의 token sequence
- stop token과 EOS 처리
- structured output의 schema 준수
- tool call과 reasoning parser 출력
- top-k logprobs의 token, rank, 정규화 오차
- streaming chunk 순서와 마지막 flush
- abort, timeout, client disconnect 이후 request 정리
- prefix cache hit와 miss에서 결과 동등성
3단계: 부하와 분포
평균값 하나로 runner를 비교하지 않는다.
| 지표 | 확인 이유 |
|---|---|
| TTFT p50/p95/p99 | prefill, queueing, input 준비 회귀 탐지 |
| TPOT와 ITL p50/p95/p99 | decode jitter와 async scheduling 효과 확인 |
| request throughput, output tokens/s | 처리량 변화 확인 |
| GPU memory peak와 KV cache usage | OOM과 batch 수용량 변화 확인 |
| CPU utilization과 event-loop delay | MRV2의 CPU 경로 개선이 실제로 나타나는지 확인 |
| startup와 CUDA graph capture 시간 | 배포·autoscaling 준비 시간 확인 |
| abort/preemption/error rate | 요청 수명주기 누수와 race 탐지 |
짧은 prompt, 긴 prefill, 긴 decode, mixed length, burst traffic을 분리한다. 같은 총 token 수라도 batch shape이 다르면 CUDA graph와 persistent batch의 효과가 달라진다.
4단계: canary와 rollback
- 동일 모델 artifact와 quantization 설정으로 V1·MRV2 두 deployment를 만든다.
- replay 가능한 내부 traffic부터 MRV2에 보낸다.
- golden request의 결과 계약과 latency 분포를 비교한다.
- 작은 비율의 production traffic으로 확장한다.
- error, OOM, p99, startup SLO 중 하나라도 기준을 넘으면 V1로 되돌린다.
- rollback 후 MRV2 pod의 request state가 남아 있지 않은지와 client retry 중복을 확인한다.
runner 전환은 저장 데이터 migration이 아니므로 rollback 자체는 단순해 보인다. 하지만 streaming 중인 요청과 retry가 겹치면 사용자에게 중복 응답이나 끊긴 응답을 줄 수 있다. connection draining과 재시도 정책까지 rollback runbook에 포함해야 한다.
성능 수치를 일반화하지 말아야 하는 이유
MRV2 설계 문서는 CPU overhead 감소, 비동기 overlap, sampling memory 절감을 설명하지만 모든 모델과 트래픽에 대한 단일 speedup 수치를 약속하지 않는다. 성능은 다음 변수에 크게 좌우된다.
- prefill과 decode 비율
- 평균·최대 sequence length
- 동시 요청 수와 batch shape
- attention backend와 GPU 세대
- quantization과 speculative decoding 방식
- CUDA graph mode
- tensor, pipeline, data parallel 조합
- logprobs, structured output, LoRA 같은 부가 기능
따라서 "MRV2가 기본값이 되었으니 더 빠르다"가 아니라 기본 실행 경로가 바뀌었으니 기존 SLO와 비용 기준을 다시 측정해야 한다가 맞다. 새 구조의 장점은 benchmark 한 번보다 장기적으로 기능을 추가하고 race를 피하기 쉬운 실행 모델에 있다.
도입 체크리스트
VLLM_USE_V2_MODEL_RUNNER=1로 실행해 silent fallback을 차단한다.VLLM_USE_V2_MODEL_RUNNER=0을 명시한다.정리
vLLM 0.25.0의 중요한 변화는 dense model 지원 목록이 늘어난 것이 아니라, 요청 상태와 실행 순서를 분리하고 CPU-GPU 동기화를 구조적으로 줄인 MRV2가 기본 경로가 되었다는 점이다.
MRV2는 고정 request slot, GPU gather, staged write, Triton input preparation과 sampler, 명시적 CUDA graph 관리로 이어지는 하나의 실행 모델을 만든다. 이 구조는 async scheduling과 speculative decoding을 기존 코드에 예외로 덧붙이는 대신 중심 경로에서 다룬다.
운영자는 두 가지를 기억하면 된다. 첫째, 자동 fallback을 성공적인 MRV2 검증으로 착각하지 않는다. 둘째, 새 기본값을 곧바로 성능 향상으로 해석하지 않고 출력 계약, 요청 수명주기, tail latency, startup, rollback을 함께 검증한다. 기본값 변경은 채택 권고가 아니라 재검증 신호다.
References
- vLLM v0.25.0 Release Notes — 2026-07-11
- vLLM Model Runner V2 Design Document — v0.25.0 source
- PR #44443: Enable Model Runner V2 by default for all dense models — merged 2026-07-02
- Issue #41286: Migration from Model Runner V1 to Model Runner V2 — opened 2026-04-29
- vLLM 0.25.0 runner selection and unsupported-feature validation source
- vLLM 0.25.0 MRV2 GPUModelRunner source
- PR #47361: Delete legacy PagedAttention implementation — merged 2026-07-02
- PR #45953: Dynamic speculative decoding with full CUDA graphs in MRV2 — merged 2026-07-04