Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

vLLM

vLLM은 대규모 언어 모델(LLM) 추론/서빙을 위한 고성능 엔진입니다.
특히 PagedAttentionContinuous Batching을 통해, 동일한 GPU 자원에서 더 높은 처리량을 제공합니다.

왜 vLLM을 사용하는가?

LLM 서빙 성능은 크게 아래 요소에 의해 결정됩니다.

처리량동시 요청 수×생성 토큰 수요청 지연 시간\text{처리량} \propto \frac{\text{동시 요청 수} \times \text{생성 토큰 수}}{\text{요청 지연 시간}}

vLLM은 KV Cache를 효율적으로 관리해 요청 간 메모리 낭비를 줄이고, 배치 스케줄링을 최적화해 실제 서비스 처리량을 높입니다.


Ollama vs vLLM

주요 특성 비교

항목OllamavLLM
설치 복잡도매우 간단 (바이너리 설치)중간 (Python 환경 필요)
사용 난이도매우 쉬움 (ollama run)보통 (CLI/API 옵션 많음)
동시 요청 처리기본적 (병렬화 제한)우수 (최적화된 배칭)
처리량 (대량 요청)낮음높음
지연시간 (단일 요청)중간낮음
GPU 최적화제한적우수 (PagedAttention)
OpenAI 호환제한적완전함
메모리 효율성기본적우수
프로덕션 준비도낮음높음

활용 포인트

Ollama 권장 사용 사례

예: 개인 개발, 중소 기업의 간단한 챗봇

vLLM 권장 사용 사례

예: SaaS 서비스, 기업 내부 LLM 인프라, 고트래픽 서비스

전환 가이드

Ollama에서 vLLM으로 전환하는 신호:


환경 설정

Docker 기반 vLLM 환경을 설정합니다. CUDA 드라이버와 컨테이너 런타임만 갖춰져 있으면 됩니다.

최신 vLLM 이미지를 가져옵니다.

docker pull vllm/vllm-openai:latest

모델 서버

Docker를 이용해 Qwen3 모델을 vLLM으로 서빙합니다. 단일 GPU 기준 설정입니다.

아래 예시는 Qwen/Qwen3-8B-Instruct 모델을 활용합니다.

# 단일 GPU 기준 Qwen3 모델을 vLLM 서버로 실행합니다.
docker run --gpus all \
  -p 8000:8000 \
  --ipc=host \
  -v ~/.cache/huggingface:/root/.cache/huggingface \
  vllm/vllm-openai:latest \
    Qwen/Qwen3-8B-Instruct \
    --host 0.0.0.0 \
    --port 8000 \
    --dtype bfloat16 \
    --max-model-len 8192 \
    --gpu-memory-utilization 0.90 \
    --seed 42

위 명령을 compose.yaml로 관리하면 옵션을 파일로 유지하고 반복 실행이 편해집니다.

# compose.yaml
services:
  vllm:
    image: vllm/vllm-openai:latest
    ports:
      - "8000:8000"
    ipc: host
    volumes:
      - ~/.cache/huggingface:/root/.cache/huggingface
    gpus: all
    command: >
      Qwen/Qwen3-8B-Instruct
      --host 0.0.0.0
      --port 8000
      --dtype bfloat16
      --max-model-len 8192
      --gpu-memory-utilization 0.90
      --seed 42
# 서버 시작
docker compose up -d

# 로그 확인
docker compose logs -f vllm

# 서버 종료
docker compose down

실행 옵션

옵션설명
모델 ID(위치 인자)모델 이름 또는 경로를 지정합니다.
--hostAPI 서버 바인딩 주소입니다.
--portAPI 서버 포트입니다. 기본값은 8000입니다.
--dtype가중치/활성화 정밀도를 지정합니다(auto, float16, bfloat16 등).
--max-model-len최대 컨텍스트 길이(입력+출력)를 지정합니다.
--gpu-memory-utilizationGPU 메모리 사용 비율(기본 0.9)입니다.
--seed재현성을 위한 시드입니다(기본 0).
--max-num-seqs한 iteration에서 처리할 최대 시퀀스 수입니다.
--max-num-batched-tokens한 iteration에서 처리할 최대 토큰 수입니다.
--swap-spaceGPU당 CPU swap 공간(GiB)입니다.
--tensor-parallel-size (-tp)텐서 병렬 그룹 수(멀티 GPU 핵심)입니다.

주요 응용 사례에 자주 쓰는 추가 옵션

응용 사례에 직접적인 옵션만 추렸습니다.

옵션권장 시작값주로 쓰는 상황메모
--enable-chunked-prefill활성화긴 프롬프트/RAG 입력이 많은 경우prefill을 나눠 지연시간 급증을 완화합니다.
--async-scheduling활성화동시 요청이 많은 온라인 서비스GPU 유휴 구간을 줄여 처리량 개선에 도움됩니다.
--performance-modethroughput 또는 balanced처리량 우선 vs 지연시간 우선interactivity는 저지연 중심입니다.
--pipeline-parallel-size (-pp)1~2단일 GPU 메모리로 큰 모델이 어려울 때TP와 함께 병렬 전략을 조합합니다.
--data-parallel-size (-dp)2+고QPS 수평 확장복제된 엔진으로 요청 처리량을 높입니다.
--api-server-count (-asc)1~4프론트엔드 병목 완화미지정 시 data_parallel_size를 따릅니다.
--kv-cache-memory-bytes예: 10GKV 캐시 메모리를 정밀 제어하고 싶을 때설정 시 --gpu-memory-utilization보다 우선합니다.
--cpu-offload-gb8~16GPU 메모리가 부족한 환경CPU-GPU 연결 대역폭이 중요합니다.
--quantization (-q)모델/하드웨어별 상이메모리 절감 및 더 큰 모델 서빙정확도/속도 트레이드오프를 확인해야 합니다.
--api-key운영 정책에 맞게외부 노출 API 운영OpenAI 호환 API 보호에 필수적입니다.

옵션 조합 예시

다양한 배포 시나리오에 대한 실전 명령어들입니다.

단일 GPU 동시 요청 최적화

동시 요청을 효율적으로 처리하기 위해 배치 크기와 메모리 활용 비율을 조정합니다.

services:
  vllm:
    image: vllm/vllm-openai:latest
    ports:
      - "8000:8000"
    ipc: host
    volumes:
      - ~/.cache/huggingface:/root/.cache/huggingface
    gpus: all
    command: >
      Qwen/Qwen3-8B-Instruct
      --host 0.0.0.0
      --port 8000
      --dtype bfloat16
      --max-model-len 8192
      --gpu-memory-utilization 0.92
      --max-num-seqs 32
      --max-num-batched-tokens 4096
      --enable-chunked-prefill
      --async-scheduling
      --performance-mode throughput
      --seed 42

2-GPU 텐서 병렬

두 개 GPU를 활용해 더 큰 모델이나 긴 컨텍스트를 처리합니다.

services:
  vllm:
    image: vllm/vllm-openai:latest
    ports:
      - "8000:8000"
    ipc: host
    volumes:
      - ~/.cache/huggingface:/root/.cache/huggingface
    gpus:
      - count: 2
    command: >
      Qwen/Qwen3-8B-Instruct
      --tensor-parallel-size 2
      --host 0.0.0.0
      --port 8000
      --dtype bfloat16
      --max-model-len 8192
      --gpu-memory-utilization 0.90
      --max-num-seqs 64
      --max-num-batched-tokens 8192
      --performance-mode throughput
      --seed 42

4-GPU 고동시성

텐서 병렬화와 데이터 병렬화를 결합하여 높은 처리량을 달성합니다.

services:
  vllm:
    image: vllm/vllm-openai:latest
    ports:
      - "8000:8000"
    ipc: host
    volumes:
      - ~/.cache/huggingface:/root/.cache/huggingface
    gpus: all
    command: >
      Qwen/Qwen3-8B-Instruct
      --tensor-parallel-size 2
      --data-parallel-size 2
      --api-server-count 2
      --host 0.0.0.0
      --port 8000
      --dtype bfloat16
      --max-model-len 8192
      --gpu-memory-utilization 0.90
      --max-num-seqs 128
      --max-num-batched-tokens 16384
      --enable-chunked-prefill
      --async-scheduling
      --performance-mode throughput

메모리 제약 환경

제한된 GPU 메모리에서 모델을 서빙하기 위해 CPU 오프로드와 캐시 최적화를 활용합니다.

services:
  vllm:
    image: vllm/vllm-openai:latest
    ports:
      - "8000:8000"
    ipc: host
    volumes:
      - ~/.cache/huggingface:/root/.cache/huggingface
    gpus: all
    command: >
      Qwen/Qwen3-8B-Instruct
      --host 0.0.0.0
      --port 8000
      --dtype float16
      --max-model-len 4096
      --kv-cache-memory-bytes 10G
      --cpu-offload-gb 8
      --swap-space 16

API 호출 테스트

실행 중인 vLLM 서버에 프롬프트를 보내고 응답을 받습니다. curl과 Python의 두 가지 방법을 소개합니다.

curl로 호출

# OpenAI 호환 Chat Completions API 호출
curl http://127.0.0.1:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Qwen/Qwen3-8B-Instruct",
    "messages": [
      {"role": "system", "content": "당신은 딥러닝 튜터입니다."},
      {"role": "user", "content": "vLLM의 장점을 3가지로 설명해줘."}
    ],
    "temperature": 0.0,
    "max_tokens": 256
  }'

Python으로 호출

OpenAI SDK를 이용해 vLLM과 상호작용합니다.

# OpenAI SDK를 vLLM 엔드포인트에 연결
from openai import OpenAI

client = OpenAI(
    base_url="http://127.0.0.1:8000/v1",
    api_key="EMPTY",  # vLLM 로컬 서버는 임의 문자열 사용 가능
)

# 재현 가능성을 위해 temperature를 0으로 설정
response = client.chat.completions.create(
    model="Qwen/Qwen3-8B-Instruct",
    messages=[
        {"role": "system", "content": "당신은 딥러닝 튜터입니다."},
        {"role": "user", "content": "Qwen3를 서비스할 때 주의점을 알려줘."},
    ],
    temperature=0.0,
    max_tokens=256,
)

print(response.choices[0].message.content)

성능 튜닝 포인트

GPU 메모리, 배치 크기, 정밀도 등을 최적화하여 처리량과 지연시간 사이의 균형을 맞웁니다.

항목옵션설명
최대 컨텍스트 길이--max-model-len길이를 줄이면 메모리 사용량 감소
GPU 사용 비율--gpu-memory-utilizationOOM 방지를 위해 0.85~0.95 범위에서 조정
정밀도--dtypebfloat16/float16로 메모리 절감
병렬화--tensor-parallel-size멀티 GPU 사용 시 처리량 향상
재현성--seed, temperature=0결과 일관성 확보

멀티 GPU 예시:

# 2개 GPU를 사용해 Qwen3 대형 모델 서빙
docker run --gpus '"device=0,1"' \
  -p 8000:8000 \
  --ipc=host \
  -v ~/.cache/huggingface:/root/.cache/huggingface \
  vllm/vllm-openai:latest \
    Qwen/Qwen3-32B-Instruct \
    --tensor-parallel-size 2 \
    --host 0.0.0.0 \
    --port 8000

Ollama vs vLLM 동시 요청 처리 속도 비교

Ollama와 vLLM의 동시 요청 처리 성능을 실제로 측정하고 비교합니다. 동일한 Qwen3 8B 계열 모델로 테스트합니다.

비교 조건

항목
프롬프트동일 문장 1개
생성 길이max_tokens=128 (num_predict=128)
샘플링temperature=0.0
동시 요청 수1, 4, 8, 16, 32
측정 지표req/s, p95 latency(ms)

서버 실행

ollama pull qwen3:8b
OLLAMA_NUM_PARALLEL=8 ollama serve
docker run --gpus all \
  -p 8000:8000 \
  --ipc=host \
  -v ~/.cache/huggingface:/root/.cache/huggingface \
  vllm/vllm-openai:latest \
    Qwen/Qwen3-8B-Instruct \
    --host 0.0.0.0 \
    --port 8000 \
    --dtype bfloat16 \
    --max-model-len 8192 \
    --gpu-memory-utilization 0.90 \
    --seed 42

벤치마크 스크립트

import argparse
import asyncio
import statistics
import time

import httpx

PROMPT = "vLLM과 Ollama의 동시 요청 처리 방식 차이를 3문장으로 설명해줘."

async def one_request(client: httpx.AsyncClient, target: str, max_tokens: int) -> float:
    if target == "vllm":
        url = "http://127.0.0.1:8000/v1/chat/completions"
        payload = {
            "model": "Qwen/Qwen3-8B-Instruct",
            "messages": [{"role": "user", "content": PROMPT}],
            "temperature": 0.0,
            "max_tokens": max_tokens,
        }
    else:
        url = "http://127.0.0.1:11434/api/generate"
        payload = {
            "model": "qwen3:8b",
            "prompt": PROMPT,
            "stream": False,
            "options": {"temperature": 0.0, "num_predict": max_tokens},
        }

    t0 = time.perf_counter()
    resp = await client.post(url, json=payload, timeout=180.0)
    resp.raise_for_status()
    return (time.perf_counter() - t0) * 1000.0

async def run_bench(target: str, concurrency: int, total_requests: int, max_tokens: int):
    sem = asyncio.Semaphore(concurrency)
    latencies = []

    async with httpx.AsyncClient() as client:
        async def worker():
            async with sem:
                return await one_request(client, target, max_tokens)

        start = time.perf_counter()
        results = await asyncio.gather(
            *[worker() for _ in range(total_requests)],
            return_exceptions=True,
        )
        elapsed = time.perf_counter() - start

    for r in results:
        if not isinstance(r, Exception):
            latencies.append(r)

    success = len(latencies)
    rps = success / elapsed if elapsed > 0 else 0.0
    p50 = statistics.median(latencies) if latencies else 0.0
    p95 = (
        statistics.quantiles(latencies, n=100)[94]
        if len(latencies) >= 20
        else (max(latencies) if latencies else 0.0)
    )

    print(
        f"[{target}] c={concurrency:>2} | success={success:>3}/{total_requests:<3} "
        f"| req/s={rps:>6.2f} | p50={p50:>7.1f}ms | p95={p95:>7.1f}ms"
    )

async def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--target", choices=["ollama", "vllm"], required=True)
    parser.add_argument("--concurrency", type=int, nargs="+", default=[1, 4, 8, 16, 32])
    parser.add_argument("--total-requests", type=int, default=64)
    parser.add_argument("--max-tokens", type=int, default=128)
    args = parser.parse_args()

    for c in args.concurrency:
        await run_bench(args.target, c, args.total_requests, args.max_tokens)

if __name__ == "__main__":
    asyncio.run(main())

실행:

pip install -U httpx

python benchmark_concurrency.py --target ollama --concurrency 1 4 8 16 32 --total-requests 64

python benchmark_concurrency.py --target vllm --concurrency 1 4 8 16 32 --total-requests 64

결과 기록 템플릿

서버동시 요청 수req/sp95(ms)
Ollama1
Ollama4
Ollama8
Ollama16
Ollama32
vLLM1
vLLM4
vLLM8
vLLM16
vLLM32

일반적으로 동시 요청 수가 커질수록 vLLM의 처리량 증가폭이 더 크게 나타납니다.


자주 발생하는 문제

배포 과정에서 마주할 수 있는 주요 이슈와 해결 방법을 정리합니다.

  1. CUDA/드라이버 오류

    • NVIDIA 드라이버, CUDA, PyTorch/vLLM 버전 호환성을 먼저 확인합니다.

  2. OOM(Out of Memory)

    • --max-model-len 축소

    • 작은 모델 사용

    • 동시 요청 수 제한

  3. 응답 지연 증가

    • 긴 입력 프롬프트 축소

    • 배치 전략 및 GPU 수 조정


다음 단계

vLLM 배포 이후 실전에서 활용할 수 있는 방향들을 소개합니다.

배포 전 체크리스트

vLLM 서버 운영을 위한 사전 확인 사항입니다.

다중 모델 서빙

여러 버전의 Qwen3 모델을 동시에 서빙할 수 있습니다.

services:
  vllm-8b:
    image: vllm/vllm-openai:latest
    ports:
      - "8000:8000"
    ipc: host
    volumes:
      - ~/.cache/huggingface:/root/.cache/huggingface
    gpus:
      - count: 1
    command: >
      Qwen/Qwen3-8B-Instruct
      --host 0.0.0.0
      --port 8000

  vllm-32b:
    image: vllm/vllm-openai:latest
    ports:
      - "8001:8000"
    ipc: host
    volumes:
      - ~/.cache/huggingface:/root/.cache/huggingface
    gpus:
      - count: 2
    command: >
      Qwen/Qwen3-32B-Instruct
      --tensor-parallel-size 2
      --host 0.0.0.0
      --port 8000

프론트엔드는 요청 특성에 따라 8B(빠름)와 32B(정확함) 중 선택합니다.

RAG 통합

RAG 파이프라인과 연결하여 검색 증강 생성(RAG) 파이프라인을 구성합니다.

파이프라인 구조:

문서 저장소 → 벡터 임베딩 → 벡터 DB → 검색기 → 프롬프트 구성 → vLLM API → 최종 응답

핵심 단계:

  1. 임베딩: 문서를 벡터로 변환 (예: sentence-transformers)

  2. 검색: 사용자 쿼리 관련 문서 상위 K개 반환

  3. 컨텍스트: 검색된 문서를 프롬프트에 포함

  4. 생성: vLLM 서버로 최종 응답 생성

프로덕션 배포 팁

안정적인 운영을 위한 실전 노하우입니다.