Skip to content
Published on

vLLM 내부 구조 (1) — 요청 하나가 토큰이 되어 나오기까지

공유하기
Authors

이 시리즈가 다루는 것

LLM을 직접 서빙해 본 사람은 대개 비슷한 순서로 벽을 만납니다. 처음에는 그냥 돌아가기만 해도 다행이고, 그다음에는 왜 이렇게 느린지가 궁금해지고, 결국에는 왜 갑자기 메모리가 터지는지를 알아야만 합니다. 이 세 질문의 답은 전부 vLLM 안쪽에 있습니다.

이 시리즈는 vLLM을 "빠른 서버"라는 결과가 아니라 구조로 봅니다. 모두 7편이고 1편인 이 글은 지도에 해당합니다. 요청 하나가 들어와 토큰이 되어 나가는 경로를 처음부터 끝까지 한 번 훑고, 나머지 편에서 각 구간을 확대합니다.

시작 전에 버전 이야기를 먼저 해야 합니다. vLLM은 변화가 빠르고 내부를 크게 다시 쓴 적이 있습니다. 공식 V1 가이드는 스케줄러, KV 캐시 매니저, 워커, 샘플러, API 서버가 다시 설계되었다고 밝히고 있습니다. 그래서 이 시리즈의 설명은 V1 이후 구조를 기준으로 합니다. 오래된 글과 설명이 어긋난다면 대개 그 글이 V0 시절 이야기입니다.

내용은 2026-08-12에 공식 문서·소스에서 확인했습니다. vLLM은 변화가 빠르니 설정값과 동작은 사용 중인 버전의 문서로 다시 확인하세요.

요청 하나의 전체 경로

말로 풀기 전에 그림으로 먼저 봅니다.

클라이언트 HTTP 요청
[API 서버 프로세스]   HTTP 수신 · 입력 처리 · 토큰화
[엔진 코어 프로세스]   스케줄러 + KV 캐시 매니저
        │             "이번 스텝에 누구를 몇 토큰씩 넣을까"
[GPU 워커 프로세스]    ModelRunner → 모델 forward → 로짓
      샘플러           로짓에서 다음 토큰 하나를 고름
        ├───────────▶ 스트리밍으로 클라이언트에 한 조각 전달
        └───────────▶ 아직 안 끝난 요청은 다시 스케줄러로

이 그림에서 가장 중요한 것은 마지막 화살표입니다. 토큰 하나가 나온 뒤에도 요청은 끝나지 않고 다시 스케줄러로 돌아갑니다. 요청 하나를 끝까지 처리하고 다음 요청을 보는 구조가 아니라, 살아 있는 모든 요청이 매 스텝마다 조금씩 전진합니다. vLLM의 성능 이야기는 결국 이 루프를 어떻게 빈틈없이 채우느냐의 문제입니다.

API 서버: 요청을 엔진의 언어로 바꾸는 곳

vLLM에는 진입점이 두 개 있습니다. 공식 아키텍처 문서에 따르면 오프라인 추론은 LLM 클래스가 담당하고, 온라인 서빙은 vllm serve 명령이나 OpenAI 호환 API 서버가 담당합니다. 그리고 이 OpenAI 호환 서버는 AsyncLLMEngine을 사용합니다. 비동기 래퍼가 백그라운드 루프를 돌리기 때문에, HTTP 요청이 여러 개 동시에 들어와도 각각이 서로를 기다리지 않습니다.

여기서 하는 일 자체는 화려하지 않습니다. 채팅 템플릿을 적용하고, 문자열을 토큰 열로 바꾸고, 샘플링 설정을 정리해서 엔진이 이해하는 요청 객체로 만듭니다. 다만 이 단계에서 이미 성능이 갈립니다. 프롬프트를 어떻게 조립했느냐가 5편에서 다룰 접두사 캐시 적중 여부를 결정하고, 입력 길이가 6편에서 다룰 길이 한도에 그대로 걸립니다.

엔진 코어: 스케줄러와 KV 캐시 매니저

V1의 프로세스 구성은 공식 문서에 이렇게 정리되어 있습니다. HTTP를 받는 API 서버 프로세스가 있고, 스케줄러를 돌리며 KV 캐시를 관리하는 엔진 코어 프로세스가 데이터 병렬 랭크마다 하나 있고, GPU 하나당 워커 프로세스가 하나 있습니다. 문서가 든 예시로 GPU 4장짜리 배포는 API 서버 1 + 엔진 코어 1 + GPU 워커 4, 합쳐서 6개 프로세스가 됩니다.

프로세스를 나눈 이유는 단순합니다. HTTP 파싱과 토큰화는 파이썬이 CPU를 쓰는 일이고, 모델 실행은 GPU를 쓰는 일입니다. 둘이 같은 프로세스에 있으면 CPU 작업이 GPU 루프를 막습니다. 실제로 처리량이 안 나오는 배포를 들여다보면 GPU 사용률은 낮은데 CPU 한 코어가 100퍼센트인 경우가 흔합니다.

엔진 코어 안에서 스케줄러와 KV 캐시 매니저는 한 몸처럼 움직입니다. 스케줄러가 "이 요청을 이번 스텝에 넣겠다"고 정하려면 그만큼의 KV 캐시 블록을 실제로 확보할 수 있어야 하기 때문입니다. 확보하지 못하면 스케줄러는 이미 돌고 있던 요청을 뒤로 물리기까지 합니다. 이 선점 동작이 4편의 주제입니다.

워커와 샘플러: 실제로 GPU를 만지는 층

아키텍처 문서의 정의는 간결합니다. Worker는 모델 추론을 돌리는 프로세스이고, ModelRunner는 모델을 적재하고 실행하는 역할이며, 그 안의 Model이 실제 torch 모듈 인스턴스입니다.

워커가 forward를 한 번 돌리면 배치 안의 각 요청에 대해 다음 토큰의 로짓이 나옵니다. 샘플러는 여기에 temperature, top_p, top_k 같은 설정을 적용해 토큰 하나를 고릅니다. 요청마다 샘플링 설정이 다를 수 있는데도 같은 배치에 섞일 수 있다는 점이 중요합니다. 배치는 모델 계산을 공유할 뿐이고, 무엇을 뽑을지는 요청별로 따로 결정됩니다.

설정이 한 객체로 모이는 이유

문서가 강조하는 설계 하나가 VllmConfig입니다. 필요한 정보를 전부 담은 설정 객체를 만들어 두고 이것을 넘긴다는 발상입니다. 덕분에 모델을 다 올린 뒤에 가중치를 고치는 대신, 초기화 시점에 샤딩과 양자화를 함께 적용할 수 있습니다. 거대한 모델에서는 이 차이가 결정적입니다. 한 장에 안 들어가는 모델을 일단 올린 다음 쪼갤 수는 없기 때문입니다.

이 구조는 운영에도 영향을 줍니다. --max-model-len이나 --gpu-memory-utilization 같은 값이 기동 시점에 굳어져 이후 전부에 영향을 주는 이유가 여기 있습니다. 그래서 vLLM 튜닝은 런타임 조절이 아니라 대부분 기동 인자 설계입니다.

이제 지도는 그려졌습니다. 다음 편부터는 이 경로에서 가장 많은 사람이 걸려 넘어지는 구간을 하나씩 확대합니다.

직접 해보기

참고 자료