
vLLM 내부 구조: 고처리량 LLM 추론 시스템의 해부 (2025)
요약
고처리량 LLM 추론 시스템인 vLLM의 내부 구조와 핵심 구성 요소를 심층 분석합니다. 엔진 코어, 페이지드 어텐션, 연속 배칭 등 고성능 추론을 위한 기술적 메커니즘을 단계적으로 설명합니다.
핵심 포인트
- vLLM의 핵심 기술인 PagedAttention과 Continuous Batching 분석
- 오프라인 추론에서 온라인/분산 서빙 시스템으로의 확장 과정 설명
- 엔진 구성 요소(Config, Processor, Engine Core)의 역할 정의
- 멀티 GPU 및 멀티 노드 환경을 위한 스케일링 전략 제시
이 포스트에서는 현대적인 고처리량 (high-throughput) LLM 추론 시스템을 구성하는 모든 핵심 시스템 구성 요소와 고급 기능들을 단계적으로 소개하겠습니다. 특히 vLLM [1]이 어떻게 작동하는지 상세히 분석할 것입니다.
이 포스트는 시리즈의 첫 번째 글입니다. 세부 사항에 매몰되지 않고 전체 시스템에 대한 정확한 상위 수준의 멘탈 모델 (mental model)을 형성할 수 있도록, 넓은 범위에서 시작하여 점진적으로 세부 사항을 쌓아가는 방식 (역피라미드 접근법)을 따릅니다.
이후 포스트에서는 특정 서브시스템 (subsystems)을 깊이 있게 다룰 예정입니다.
이 포스트는 다섯 부분으로 구성됩니다:
- LLM 엔진 및 엔진 코어: vLLM의 기초 (스케줄링 (scheduling), 페이지드 어텐션 (paged attention), 연속 배칭 (continuous batching) 등)
- 고급 기능: 청크드 프리필 (chunked prefill), 프리픽스 캐싱 (prefix caching), 가이드 및 투기적 디코딩 (guided & speculative decoding), 분리된 P/D (disaggregated P/D)
- 스케일링 업 (Scaling up): 단일 GPU에서 멀티 GPU 실행까지
- 서빙 레이어 (Serving layer): 분산 / 동시 웹 스캐폴딩 (web scaffolding)
- 벤치마크 및 자동 튜닝: 지연 시간 (latency) 및 처리량 (throughput) 측정
LLM 엔진 및 엔진 코어
LLM 엔진은 vLLM의 근본적인 빌딩 블록 (building block)입니다. 엔진 자체만으로도 이미 고처리량 추론을 가능하게 하지만, 이는 오직 오프라인 (offline) 환경에서만 가능합니다. 아직 웹을 통해 고객에게 서비스할 수는 없습니다.
다음의 오프라인 추론 코드 스니펫을 실행 예제로 사용하겠습니다 (basic.py에서 수정됨).
from vllm import LLM, SamplingParams
prompts = [
"Hello, my name is",
...
이 설정은 다음과 같습니다:
- 오프라인 (offline) (웹/분산 시스템 스캐폴딩 없음)
- 동기식 (synchronous) (모든 실행이 단일 블로킹 프로세스에서 발생)
- 단일 GPU (single-GPU) (데이터/모델/파이프라인/전문가 병렬성 없음; DP/TP/PP/EP = 1)
- 표준 트랜스포머 (transformer) [2] 사용 (Jamba와 같은 하이브리드 모델을 지원하려면 더 복잡한 하이브리드 KV-캐시 메모리 할당기가 필요함)
여기서부터 우리는 표준 트랜스포머를 사용하면서도, 점진적으로 온라인, 비동기(async), 멀티 GPU, 멀티 노드 추론 시스템으로 구축해 나갈 것입니다.
이 예제에서 우리는 두 가지 작업을 수행합니다:
- 엔진 인스턴스화 (Instantiate an engine)
- 주어진 프롬프트로부터 샘플링하기 위해 엔진의
generate호출
이제 생성자 (constructor) 분석을 시작해 보겠습니다.
LLM 엔진 생성자 (LLM Engine constructor)
엔진의 주요 구성 요소는 다음과 같습니다:
- vLLM 설정 (vLLM config): 모델, 캐시, 병렬 처리(parallelism) 등을 설정하기 위한 모든 노브(knobs)를 포함합니다.
- 프로세서 (processor): 검증(validation), 토큰화(tokenization), 처리를 통해 원시 입력(raw inputs)을
EngineCoreRequests로 변환합니다. - 엔진 코어 클라이언트 (engine core client): 현재 실행 중인 예제에서는 기본적으로
EngineCore와 동일한InprocClient를 사용하고 있습니다. 이후 대규모 서빙(serving at scale)을 가능하게 하는DPLBAsyncMPClient로 점진적으로 확장해 나갈 것입니다. - 출력 프로세서 (output processor): 원시
EngineCoreOutputs를 사용자가 보는RequestOutput으로 변환합니다.
엔진 코어(Engine core) 자체는 여러 하위 구성 요소로 이루어져 있습니다:
- 모델 실행기 (Model Executor): 모델의 순전파(forward passes)를 구동합니다. 현재는 단일 GPU에서 단일
Worker프로세스를 갖는UniProcExecutor를 다루고 있습니다. 이후 여러 GPU를 지원하는MultiProcExecutor로 점진적으로 확장할 것입니다. - 구조화된 출력 관리자 (Structured Output Manager): 가이드 디코딩(guided decoding)에 사용됩니다 (이 내용은 나중에 다룹니다).
- 스케줄러 (Scheduler): 어떤 요청을 다음 엔진 단계로 보낼지 결정합니다. 스케줄러는 다음과 같은 요소를 추가로 포함합니다:
- 정책 설정 (policy setting): FCFS(First Come First Served, 선입선출) 또는 priority(우선순위, 높은 우선순위 요청을 먼저 처리) 방식 중 하나를 선택할 수 있습니다.
waiting(대기) 및running(실행) 큐- KV 캐시 관리자 (KV cache manager): Paged Attention [3]의 핵심입니다.
KV 캐시 관리자는 free_block_queue를 유지합니다:
- 사용 가능한 KV 캐시 블록의 풀 (VRAM 크기와 블록 크기에 따라 종종 수십만 개 단위에 달함). Paged Attention 동안, 이 블록들은 토큰을 계산된 KV 캐시 블록으로 매핑하는 인덱싱 구조(indexing structure) 역할을 합니다.

모델 실행기(model executor) 생성 과정에서 Worker 객체가 생성되며, 세 가지 주요 절차가 실행됩니다. (나중에 MultiProcExecutor를 사용할 때는, 이와 동일한 절차들이 서로 다른 GPU에 있는 각 워커 프로세스에서 독립적으로 실행됩니다.)
- 장치 초기화 (Init device):
- 워커에 CUDA 장치(예: "cuda:0")를 할당하고 모델 데이터 타입(dtype, 예: bf16)이 지원되는지 확인합니다.
- 요청된
gpu_memory_utilization을 고려하여 충분한 VRAM이 사용 가능한지 확인합니다.
(예: 0.8 → 전체 VRAM의 80%) - 분산 설정 (DP / TP / PP / EP 등) 구성
model_runner인스턴스화
(sampler, KV cache, 그리고 input_ids, positions 등과 같은 순전파 (forward-pass) 버퍼를 보유)
-
InputBatch객체 인스턴스화 (CPU 측 순전파 버퍼, KV-cache 인덱싱을 위한 블록 테이블, 샘플링 메타데이터 등을 보유) -
모델 로드:
- 모델 아키텍처 인스턴스화
- 모델 가중치 (weights) 로드
model.eval()호출 (PyTorch의 추론 모드)- 선택 사항: 모델에
torch.compile()호출
-
KV cache 초기화
- 레이어별 KV-cache 사양 (spec) 획득. 역사적으로 이는 항상
FullAttentionSpec(균질한 트랜스포머)이었으나, 하이브리드 모델 (sliding window, Jamba와 같은 Transformer/SSM)이 등장하면서 더 복잡해졌습니다 (Jenga [5] 참조) - 더미/프로파일링 순전파 (forward pass)를 실행하고 GPU 메모리 스냅샷을 찍어 사용 가능한 VRAM에 얼마나 많은 KV cache 블록이 들어가는지 계산
- KV cache 텐서를 할당, 재구성(reshape)하고 어텐션 레이어에 바인딩
- 이후 순전파 중 커널(kernel)에서 소비될 어텐션 메타데이터 준비 (예: 백엔드를 FlashAttention으로 설정)
--enforce-eager가 제공되지 않는 한, 각 워밍업 배치 크기에 대해 더미 실행을 수행하고 CUDA 그래프를 캡처합니다. CUDA 그래프는 GPU 작업의 전체 시퀀스를 DAG (Directed Acyclic Graph)로 기록합니다. 이후 순전파 중에 미리 구워진 (pre-baked) 그래프를 실행/재생하여 커널 실행 오버헤드를 줄이고 지연 시간 (latency)을 개선합니다.
- 레이어별 KV-cache 사양 (spec) 획득. 역사적으로 이는 항상
-
레이어별 KV-cache 사양 (spec) 획득. 역사적으로 이는 항상
여기서 많은 저수준 (low-level) 세부 사항들을 추상화했습니다. 하지만 이어지는 섹션에서 반복적으로 참조할 것이므로, 이제 핵심 구성 요소들을 소개하겠습니다.
엔진 초기화가 완료되었으므로 generate 함수로 넘어가겠습니다.
Generate 함수
첫 번째 단계는 요청을 검증하고 엔진에 전달하는 것입니다. 각 프롬프트에 대해 다음을 수행합니다:
- 고유한 요청 ID를 생성하고 도착 시간을 기록
- 프롬프트를 토큰화하고
prompt,prompt_token_ids, 그리고type을 포함하는 딕셔너리를 반환하는 입력 전처리기 (input preprocessor) 호출
(text, tokens, embeds 등) - 이 정보를 EngineCoreRequest에 담습니다.
우선순위(priority), 샘플링 파라미터(sampling params) 및 기타 메타데이터를 추가한 뒤, 해당 요청을 엔진 코어(engine core)로 전달합니다. 엔진 코어는 이를 Request 객체로 감싸고 상태를 WAITING으로 설정합니다. 이후 이 요청은 스케줄러(scheduler)의 waiting 큐에 추가됩니다 (FCFS 방식이면 append, 우선순위 방식이면 heap-push).
이 시점에서 엔진에 데이터가 공급되었으며 실행을 시작할 수 있습니다. 동기식 엔진(synchronous engine) 예제의 경우, 이 초기 프롬프트들이 우리가 처리할 유일한 요청들입니다. 실행 중간에 새로운 요청을 주입하는 메커니즘은 없습니다. 반면, 비동기식 엔진(asynchronous engine)은 이를 지원하며, 이를 연속 배치 (continuous batching) [6]라고 합니다. 각 단계(step)가 끝날 때마다 새로운 요청과 기존 요청이 모두 고려됩니다.
다음으로, 처리할 요청이 있는 한 엔진은 step() 함수를 반복적으로 호출합니다. 각 단계는 세 가지 스테이지로 구성됩니다:
- 스케줄링 (Schedule): 이번 단계에서 실행할 요청(디코드(decode) 및/또는 (청크 단위) 프리필(chunked prefill))을 선택합니다.
- 순전파 (Forward pass): 모델을 실행하고 토큰을 샘플링합니다.
- 후처리 (Postprocess): 샘플링된 토큰 ID를 각
Request에 추가하고, 디토크나이즈(detokenize)하며, 중단 조건(stop conditions)을 확인합니다. 요청이 완료되면 정리 작업을 수행하고(예: 해당 KV-캐시 블록을free_block_queue로 반환), 출력을 조기에 반환합니다.

다음으로, 스케줄링에 대해 더 자세히 살펴보겠습니다.
스케줄러 (Scheduler)
추론 엔진이 처리하는 워크로드(workload)에는 크게 두 가지 유형이 있습니다:
프리필 (Prefill) 요청 — 모든 프롬프트 토큰에 대해 순전파를 수행합니다. 이들은 대개 연산 제한적 (compute-bound) 입니다 (임계값은 하드웨어와 프롬프트 길이에 따라 달라집니다). 마지막에 마지막 토큰 위치의 확률 분포로부터 단일 토큰을 샘플링합니다.
디코드 (Decode) 요청 — 가장 최근의 토큰에 대해서만 순전파를 수행합니다. 이전의 모든 KV 벡터는 이미 캐시되어 있습니다. 단 하나의 토큰을 계산하기 위해서도 모든 LLM 가중치(및 KV 캐시)를 로드해야 하므로, 이들은 메모리 대역폭 제한적 (memory-bandwidth-bound) 입니다.
V1 스케줄러는 더 스마트한 설계 덕분에 동일한 단계에서 두 유형의 요청을 혼합할 수 있습니다. 이와 대조적으로, V0 엔진은 한 번에 프리필 또는 디코드 중 하나만 처리할 수 있었습니다.
스케줄러는 디코드 (decode) 요청, 즉 이미 running 큐에 있는 요청을 우선시합니다. 각 요청에 대해 다음을 수행합니다:
- 생성할 새로운 토큰의 수를 계산합니다 (추측 디코딩 (speculative decoding) 및 비동기 스케줄링 (async scheduling)으로 인해 항상 1은 아닙니다 — 이에 대해서는 나중에 자세히 다룹니다).
- KV-캐시 매니저의
allocate_slots함수를 호출합니다 (상세 내용은 아래 참조). - 1단계에서 계산된 토큰 수를 차감하여 토큰 예산 (token budget)을 업데이트합니다.
waiting 큐에 있는 요청의 경우, 다음을 수행합니다:
- 계산된 블록의 수를 가져옵니다 (프리픽스 캐싱 (prefix caching)이 비활성화된 경우 0을 반환합니다 — 이에 대해서는 나중에 다룹니다).
- KV-캐시 매니저의
allocate_slots함수를 호출합니다. - 대기(waiting) 상태에서 요청을 꺼내(pop) 실행(running) 상태로 이동시키고, 상태를
RUNNING으로 설정합니다. - 토큰 예산을 업데이트합니다.
allocate_slots가 수행하는 작업은 다음과 같습니다:
블록 수 계산— 할당해야 하는 새로운 KV-캐시 블록의 수(n)를 결정합니다. 각 블록은 기본적으로 16개의 토큰을 저장합니다. 예를 들어, 프리필 (prefill) 요청에 17개의 새로운 토큰이 있다면, ceil(17/16) = 2개의 블록이 필요합니다.
가용성 확인— 매니저의 풀(pool)에 블록이 충분하지 않으면 조기에 종료합니다. 디코드 요청인지 프리필 요청인지에 따라, 엔진은 우선순위가 낮은 요청을 축출(evicting)함으로써 재계산 선점 (recompute preemption)을 시도할 수 있습니다 (우선순위가 낮은 요청을 축출할 때 kv_cache_manager.free를 호출하여 KV 블록을 블록 풀로 반환하며, V0에서는 스왑 선점 (swap preemption)이 지원되었습니다). 또는 스케줄링을 건너뛰고 실행을 계속할 수도 있습니다.
블록 할당— KV-캐시 매니저의 코디네이터 (coordinator)를 통해, 블록 풀(앞서 언급한 이중 연결 리스트인 free_block_queue)에서 처음 n개의 블록을 가져옵니다. 각 request_id를 해당 KV-캐시 블록 리스트에 매핑하는 딕셔너리인 req_to_blocks에 저장합니다.

포워드 패스 (Run forward pass) 실행
모델 실행기 (model executor)의 execute_model을 호출하며, 이는 Worker에게 위임되고, 다시 모델 러너 (model runner)에게 위임됩니다.
주요 단계는 다음과 같습니다:
상태 업데이트— input_batch에서 완료된 요청을 제거(prune)합니다.
; 기타 순전파 (fwd pass) 관련 메타데이터를 업데이트합니다 (예: 페이지드 KV 캐시 (paged KV cache) 메모리 인덱싱에 사용될 요청당 KV 캐시 블록 수).입력 준비 (Prepare inputs)— CPU에서 GPU로 버퍼를 복사합니다; 위치 (positions)를 계산합니다; slot_mapping을 구축합니다 (이에 대한 자세한 내용은 예제에서 다룹니다); 어텐션 메타데이터 (attention metadata)를 구성합니다.순전파 (Forward pass)— 커스텀 페이지드 어텐션 (paged attn) 커널을 사용하여 모델을 실행합니다. 모든 시퀀스는 평탄화(flattened)되어 하나의 긴 "슈퍼 시퀀스 (super sequence)"로 연결됩니다. 위치 인덱스 (position indices)와 어텐션 마스크 (attention masks)는 각 시퀀스가 자신의 토큰에만 어텐션을 수행하도록 보장하며, 이를 통해 우측 패딩 (right-padding) 없이 연속 배치 (continuous batching)가 가능해집니다.마지막 토큰 상태 수집 (Gather last-token states)— 각 시퀀스의 마지막 위치에 대한 은닉 상태 (hidden states)를 추출하고 로짓 (logits)을 계산합니다.샘플링 (Sample)— 샘플링 설정 (greedy, temperature, top-p, top-k 등)에 따라 계산된 로짓에서 토큰을 샘플링합니다.
순전파 (Forward-pass) 단계 자체에는 두 가지 실행 모드가 있습니다:
이거 모드 (Eager mode)— 이거 실행 (eager execution)이 활성화되었을 때 표준 PyTorch 순전파를 실행합니다."캡처된 (Captured)" 모드— 이거 실행이 강제되지 않을 때 미리 캡처된 CUDA 그래프 (CUDA Graph)를 실행/재생합니다 (KV 캐시 초기화 절차 중 엔진 구축 단계에서 이를 캡처했음을 기억하세요).
다음은 연속 배치 (continuous batching)와 페이지드 어텐션 (paged attention)을 명확하게 이해할 수 있는 구체적인 예시입니다:

고급 기능 — 핵심 엔진 로직 확장
기본적인 엔진 흐름이 갖춰졌으므로, 이제 고급 기능들을 살펴볼 수 있습니다.
우리는 이미 선점 (preemption), 페이지드 어텐션 (paged attention), 그리고 연속 배치 (continuous batching)에 대해 논의했습니다.
다음으로 다음 항목들을 자세히 살펴보겠습니다:
- 청크드 프리필 (Chunked prefill)
- 프리픽스 캐싱 (Prefix caching)
- 가이드 디코딩 (Guided decoding) (문법 제약 유한 상태 기계 (grammar-constrained finite-state machines)를 통해 수행)
- 투기적 디코딩 (Speculative decoding)
- 분리된 P/D (Disaggregated P/D) (prefill/decoding)
청크드 프리필 (Chunked prefill)
청크드 프리필 (Chunked prefill)은 프리필 (prefill) 단계를 더 작은 청크 (chunks)로 나누어 긴 프롬프트를 처리하는 기술입니다. 이 기술이 없다면, 단일 매우 긴 요청이 하나의 엔진 단계를 독점하여 다른 프리필 요청이 실행되는 것을 막을 수 있습니다. 이는 모든 다른 요청을 지연시키고 지연 시간 (latency)을 증가시킬 것입니다.
예를 들어, 각 청크가 n을 포함한다고 가정해 봅시다.
(=8) 토큰이며, 소문자로 라벨링되고 "-"로 구분됩니다. 긴 프롬프트 P는 다음과 같이 보일 수 있습니다:
x-y-z
여기서 z는 불완전한 청크 (예: 2개 토큰)입니다. P에 대해 전체 프리필 (prefill)을 실행하려면 ≥ 3번의 엔진 단계 (engine steps)가 소요될 것이며 (하나의 단계에서 실행되도록 스케줄링되지 않으면 > 발생할 수 있음), 마지막 청크드 프리필 (chunked prefill) 단계에서만 하나의 새로운 토큰을 샘플링 (sample)하게 됩니다.
동일한 예시를 시각화하면 다음과 같습니다:

AI 자동 생성 콘텐츠
본 콘텐츠는 HN AI Posts의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기