아키텍처 개요 (Architecture Overview)
아키텍처 개요 (Architecture Overview)
이 문서는 vLLM 아키텍처의 전체적인 그림을 잡아 주는 문서예요. vLLM을 쓰기만 해도 상관없지만, 어떤 식으로 돌아가는지 알면 배포할 때 CPU 자원을 어떻게 책정해야 할지 같은 판단을 더 정확하게 내릴 수 있어요.
진입점 (Entrypoints)
vLLM은 시스템과 상호작용하는 여러 진입점을 제공해요. 크게 두 가지로 나뉘는데, 각각이 담당하는 방식이 달라요.
LLM 클래스
LLM 클래스는 오프라인 추론(별도의 모델 추론 서버 없이 모델과 상호작용하는 방식)을 위한 기본 Python 인터페이스예요.
LLM 클래스 사용 예시를 볼게요.
from vllm import LLM, SamplingParams
# Define a list of input prompts
prompts = [
"Hello, my name is",
"The capital of France is",
"The largest ocean is",
]
# Define sampling parameters
sampling_params = SamplingParams(temperature=0.8, top_p=0.95)
# Initialize the LLM engine with the OPT-125M model
llm = LLM(model="facebook/opt-125m")
# Generate outputs for the input prompts
outputs = llm.generate(prompts, sampling_params)
# Print the generated outputs
for output in outputs:
prompt = output.prompt
generated_text = output.outputs[0].text
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
더 자세한 API 내용은 API 문서의 오프라인 추론 섹션에서 볼 수 있어요. LLM 클래스 코드는 vllm/entrypoints/llm.py에 있고요.
온라인 서빙
두 번째 주요 인터페이스는 vLLM의 온라인 서버예요. 이 서버는 vllm serve 명령으로 시작할 수 있어요.
vllm serve <model>
vllm CLI 코드는 vllm/entrypoints/cli/main.py에 있어요. API 서버에 대한 더 자세한 내용은 온라인 서빙 문서에서 확인할 수 있어요.
V1 프로세스 아키텍처
vLLM V1은 관심사를 분리하고 처리량을 극대화하기 위해 멀티 프로세스 아키텍처를 사용해요. 이 구조를 이해하는 게 배포에서 CPU 자원 크기를 제대로 정하는 데 중요하니까, 핵심 프로세스들을 하나씩 볼게요.
API 서버 프로세스
API 서버 프로세스는 HTTP 요청(예: OpenAI 호환 API)을 처리하고, 입력 처리(토크나이제이션, 멀티모달 데이터 로딩)를 수행하며, 결과를 클라이언트로 스트리밍해요. 엔진 코어 프로세스와는 ZMQ 소켓으로 통신해요.
기본적으로 API 서버 프로세스는 1개인데, 데이터 병렬을 쓰면 API 서버 수가 데이터 병렬 크기에 맞춰 자동으로 늘어나요. --api-server-count 플래그로 수동 설정할 수도 있고요. 각 API 서버는 ZMQ로 모든 엔진 코어에 many-to-many 토폴로지로 연결돼서, 어떤 API 서버든 어떤 엔진 코어로든 요청을 라우팅할 수 있어요. 각 API 서버 프로세스는 미디어 로딩에 여러 CPU 스레드를 사용하는데, 이는 VLLM_MEDIA_LOADING_THREAD_COUNT(기본 8)로 제어해요.
코드는 vllm/entrypoints/launchers/api_server와 vllm/v1/utils.py에서 볼 수 있어요.
엔진 코어 프로세스
엔진 코어 프로세스는 스케줄러를 돌리고, KV 캐시를 관리하며, GPU 워커들에 걸친 모델 실행을 조율해요. 요청을 계속 스케줄링하고 GPU 워커로 작업을 분배하는 바쁜 루프를 돌리죠.
데이터 병렬 랭크마다 엔진 코어 프로세스가 1개 있어요. 예를 들어 --data-parallel-size 4면 엔진 코어 프로세스가 4개예요.
코드는 vllm/v1/engine/core.py와 vllm/v1/engine/utils.py에 있어요.
GPU 워커 프로세스
각 GPU는 전용 워커 프로세스가 관리해요. 워커 프로세스는 모델 가중치를 로딩하고, 포워드 패스를 실행하며, GPU 메모리를 관리해요. 워커는 자기 자신을 소유한 엔진 코어 프로세스와 통신하죠.
GPU마다 워커 프로세스가 1개 있어요. 엔진 코어당 GPU 워커 프로세스 총 수는 tensor_parallel_size x pipeline_parallel_size와 같아요.
코드는 vllm/v1/executor/multiproc_executor.py와 vllm/v1/worker/gpu_worker.py에 있어요.
DP 코디네이터 프로세스 (조건부)
데이터 병렬(--data-parallel-size > 1)을 쓸 때는 코디네이터 프로세스가 추가로 하나 생겨요. DP 랭크 간 로드 밸런싱을 관리하고, MoE 모델의 동기화된 포워드 패스를 조율하죠.
DP 코디네이터 프로세스는 1개예요(데이터 병렬이 활성화될 때만). 코드는 vllm/v1/engine/coordinator.py에 있어요.
프로세스 수 요약
N개의 GPU, TP 텐서 병렬 크기, DP 데이터 병렬 크기, A API 서버 수인 배포를 생각해 볼게요.
| 프로세스 유형 | 개수 | 설명 |
|---|---|---|
| API Server | A (기본 DP) |
HTTP 요청 처리와 입력 처리 |
| Engine Core | DP (기본 1) |
스케줄러와 KV 캐시 관리 |
| GPU Worker | N (= DP x PP x TP) |
GPU당 하나, 모델 포워드 패스 실행 |
| DP Coordinator | DP > 1이면 1, 아니면 0 |
DP 랭크 간 로드 밸런싱 |
| 총계 | A + DP + N (DP > 1이면 +1) |
예를 들어, 4개 GPU를 쓰는 전형적인 단일 노드 배포(vllm serve -tp=4)는 다음과 같아요.
- 1 API 서버 + 1 엔진 코어 + 4 GPU 워커 = 6개 프로세스
데이터 병렬 배포에서 8개 GPU를 쓴다면 (vllm serve -tp=2 -dp=4):
- 4 API 서버 + 4 엔진 코어 + 8 GPU 워커 + 1 DP 코디네이터 = 17개 프로세스
CPU 자원 크기 지정 권장 사항은 GPU 배포의 CPU 자원 문서를 참고해 주세요.
LLM 엔진
LLMEngine과 AsyncLLMEngine 클래스는 vLLM 시스템 동작의 중심이에요. 모델 추론과 비동기 요청 처리를 담당하죠.
LLMEngine
LLMEngine 클래스는 vLLM 엔진의 핵심 컴포넌트예요. 클라이언트로부터 요청을 받아 모델의 출력을 생성하는 책임을 가져요. LLMEngine은 입력 처리, 모델 실행(여러 호스트/GPU에 분산됐을 수 있음), 스케줄링, 출력 처리를 모두 포함해요.
- 입력 처리: 지정된 토크나이저로 입력 텍스트를 토크나이제이션해요.
- 스케줄링: 각 단계에서 어떤 요청을 처리할지 고르죠.
- 모델 실행: 여러 GPU에 분산된 실행을 포함해 언어 모델의 실행을 관리해요.
- 출력 처리: 모델이 생성한 출력을 처리해서, 언어 모델의 토큰 ID를 사람이 읽을 수 있는 텍스트로 디코딩해요.
LLMEngine 코드는 vllm/engine/llm_engine.py에 있어요.
AsyncLLMEngine
AsyncLLMEngine 클래스는 LLMEngine 클래스의 비동기 래퍼예요. asyncio로 백그라운드 루프를 만들어 들어오는 요청을 계속 처리하죠. AsyncLLMEngine은 온라인 서빙용으로 설계돼서, 여러 동시 요청을 처리하고 출력을 클라이언트로 스트리밍할 수 있어요.
OpenAI 호환 API 서버는 AsyncLLMEngine을 사용해요. 더 단순한 예시로 examples/applications/api_server/server.py에 데모 API 서버도 있어요. AsyncLLMEngine 코드는 vllm/engine/async_llm_engine.py에 있고요.
워커 (Worker)
워커는 모델 추론을 실행하는 프로세스예요. vLLM은 하나의 프로세스가 하나의 가속기(GPU 등)를 제어하는 일반적인 방식을 따르죠. 예를 들어 텐서 병렬 크기 2, 파이프라인 병렬 크기 2로 쓰면 총 4개의 워커가 생겨요. 워커는 rank와 local_rank로 식별되는데, rank는 전역 오케스트레이션에, local_rank는 주로 가속기 장치 할당과 파일 시스템·공유 메모리 같은 로컬 리소스 접근에 사용돼요.
모델 러너 (Model Runner)
모든 워커는 모델을 로딩하고 실행하는 역할을 맡은 모델 러너 객체를 하나씩 가져요. 입력 텐서 준비나 cudagraph 캡처 같은 모델 실행 로직의 상당 부분이 여기에 있어요.
모델 (Model)
모델 러너 객체마다 모델 객체가 하나씩 있고, 이게 실제 torch.nn.Module 인스턴스예요. 다양한 설정이 최종적으로 어떤 클래스를 얻게 되는지는 huggingface_integration에서 확인할 수 있어요.
클래스 계층 (Class Hierarchy)
이 클래스 계층 뒤에는 몇 가지 중요한 설계 선택이 숨어 있어요.
-
확장성(Extensibility): 계층의 모든 클래스는 필요한 정보를 모두 담은 설정 객체를 받아요. VllmConfig 클래스가 주변으로 전달되는 메인 설정 객체예요. 클래스 계층이 꽤 깊어서 각 클래스는 관심 있는 설정을 읽어야 하는데, 모든 설정을 하나의 객체로 캡슐화하면 설정 객체를 쉽게 전달하고 필요한 값을 쉽게 접근할 수 있어요. 예를 들어 모델 러너만 건드리는 새 기능을 추가한다고 해볼게요(LLM 추론 분야가 워낙 빠르게 진화해서 이런 일이 흔해요).
VllmConfig클래스에 새 설정 옵션을 추가하면 되고, config 객체 전체를 넘기므로 엔진·워커·모델 생성자를 바꿀 필요 없이 모델 러너가 그 옵션을 바로 접근할 수 있어요. -
통일성(Uniformity): 모델 러너는 모델을 생성·초기화하는 통일된 인터페이스가 필요해요. vLLM은 50개 이상의 인기 오픈소스 모델을 지원하는데, 각 모델은 자기만의 초기화 로직이 있어요. 생성자 시그니처가 모델마다 다르면 모델 러너는 복잡하고 오류가 나기 쉬운 검사 로직 없이는 생성자를 어떻게 호출할지 알 수 없어요. 생성자를 통일하면 모델 러너는 특정 모델 타입을 몰라도 모델을 쉽게 생성·초기화할 수 있죠. 모델을 조합할 때도 유용해요. 비전-언어 모델은 종종 비전 모델과 언어 모델로 구성되는데, 생성자가 통일돼 있으면 비전 모델과 언어 모델을 쉽게 만들고 조합해 비전-언어 모델로 만들 수 있거든요.
참고: 이 변경을 지원하기 위해 모든 vLLM 모델의 시그니처가 아래로 업데이트됐어요.
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):잘못된 인자를 실수로 넘기지 않도록 생성자가 이제 키워드 전용(keyword-only)이에요. 즉 이전 설정을 넘기면 생성자가 에러를 냅니다. vLLM 개발자들은 vLLM 내 모든 모델에 이미 이 변경을 적용했어요. 저장소 바깥(out-of-tree)에 등록된 모델이라면 개발자가 모델을 업데이트해야 하는데, 예를 들어 이전 생성자 시그니처를 새 것으로 맞추는 shim 코드를 추가하는 방식으로요.
class MyOldModel(nn.Module): def __init__( self, config, cache_config: Optional[CacheConfig] = None, quant_config: Optional[QuantizationConfig] = None, lora_config: Optional[LoRAConfig] = None, prefix: str = "", ) -> None: ... from vllm.config import VllmConfig class MyNewModel(MyOldModel): def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""): config = vllm_config.model_config.hf_config cache_config = vllm_config.cache_config quant_config = vllm_config.quant_config lora_config = vllm_config.lora_config super().__init__(config, cache_config, quant_config, lora_config, prefix) from packaging import version if version.parse(__version__) >= version.parse("0.6.4"): MyModel = MyNewModel else: MyModel = MyOldModel이렇게 하면 모델이 vLLM의 이전/최신 버전 모두에서 동작해요.
- 초기화 시 샤딩·양자화(Sharding and Quantization at Initialization): 일부 기능은 모델 가중치를 바꿔야 해요. 예를 들어 텐서 병렬은 모델 가중치를 샤딩해야 하고, 양자화는 모델 가중치를 양자화해야 하죠. 이 기능을 구현하는 방법은 두 가지인데, 하나는 모델 초기화 후에 가중치를 바꾸는 것, 다른 하나는 모델 초기화 과정에서 가중치를 바꾸는 거예요. vLLM은 후자를 선택했어요. 첫 번째 방식은 큰 모델에 확장하기 어려워요. 약 810GB 가중치의 405B 모델을 16개의 H100 80GB GPU로 돌린다고 해볼게요. 이상적으로는 GPU마다 50GB 가중치만 로딩해야 해요. 그런데 초기화 후에 가중치를 바꾸면 매 GPU에 810GB 전체를 로딩한 뒤 샤딩해야 해서 메모리 오버헤드가 엄청 커져요. 반면 초기화 과정에서 샤딩하면 각 레이어가 필요한 샤드만 만들기 때문에 메모리 오버헤드가 훨씬 작아요. 양자화에도 같은 원리가 적용돼요. 모델 생성자에
prefix인자를 추가해서 prefix에 따라 모델이 다르게 초기화되게 하는데, 이는 모델의 서로 다른 부분이 다르게 양자화되는 비균일 양자화에 유용해요.prefix는 보통 최상위 모델에선 빈 문자열, 하위 모델에선"vision"이나"language"같은 문자열이에요. 일반적으로 체크포인트 파일에서 모듈의 state dict 이름과 일치하죠.
이 설계의 단점 하나는 vLLM의 개별 컴포넌트에 대한 유닛 테스트를 쓰기 어렵다는 거예요. 모든 컴포넌트가 완전한 설정 객체로 초기화돼야 하기 때문이죠. 이를 해결하기 위해 모든 필드를 None으로 설정한 기본 설정 객체를 만드는 기본 초기화 함수를 제공해요. 테스트하려는 컴포넌트가 설정 객체의 몇몇 필드만 신경 쓴다면, 기본 설정 객체를 만들어 관심 있는 필드만 설정하면 돼요. 이렇게 하면 컴포넌트를 격리해 테스트할 수 있어요. 많은 vLLM 테스트가 전체 시스템을 테스트하는 end-to-end 테스트라 큰 문제는 아니니 걱정하지 마세요.
요약하면, 완전한 설정 객체 VllmConfig는 모든 vLLM 클래스가 공유하는 엔진 수준의 전역 상태로 볼 수 있어요.