🤖 AI/ML

vLLM

Virtual Large Language Model

LLM 추론 최적화 라이브러리. PagedAttention으로 처리량 극대화.

📖 상세 설명

vLLM은 UC Berkeley에서 개발한 고성능 LLM 추론 및 서빙 라이브러리입니다. 2023년 공개 이후 PagedAttention이라는 혁신적인 메모리 관리 기법으로 기존 대비 2~24배 높은 처리량(throughput)을 달성하며 업계 표준으로 자리잡았습니다. OpenAI API와 호환되는 서버를 쉽게 구축할 수 있어, 프로덕션 LLM 서비스 구축에 필수적인 도구가 되었습니다.

vLLM의 핵심 기술인 PagedAttention은 운영체제의 가상 메모리 페이징에서 영감을 받았습니다. 기존 LLM 추론에서는 KV 캐시(Key-Value Cache)가 연속적인 메모리 공간을 차지해야 해서, 시퀀스 길이에 따라 메모리를 사전 할당하고 남는 공간은 낭비되었습니다. PagedAttention은 KV 캐시를 고정 크기 블록으로 나누어 비연속적으로 저장함으로써 메모리 낭비를 60~80% 줄입니다.

vLLM은 Continuous Batching을 통해 동적으로 요청을 배치합니다. 전통적인 정적 배칭은 가장 긴 시퀀스가 끝날 때까지 GPU가 대기해야 했지만, vLLM은 완료된 요청을 즉시 제거하고 새 요청을 추가합니다. 또한 Tensor Parallelism과 Pipeline Parallelism을 지원해 다중 GPU 환경에서 대형 모델(70B+)을 효율적으로 서빙할 수 있습니다.

실무에서 vLLM은 Llama, Mistral, Qwen, Yi 등 대부분의 오픈소스 LLM을 지원하며, AWQ/GPTQ 양자화 모델도 서빙 가능합니다. Kubernetes 환경에서의 배포, Prometheus 메트릭 노출, OpenAI 호환 API 등 프로덕션에 필요한 기능을 기본 제공합니다. 2025년 현재 H100에서 Llama 8B 기준 2,300~2,500 tokens/s의 처리량을 기록하며, Ollama 대비 20배 이상의 처리량 차이를 보입니다.

💻 코드 예제

# vLLM 완전 가이드: 서버 구축, 클라이언트, 벤치마킹
# pip install vllm openai

# ============================================
# 1. vLLM 서버 실행 (터미널에서)
# ============================================
# 기본 실행 (단일 GPU)
# python -m vllm.entrypoints.openai.api_server \
#     --model meta-llama/Llama-3.1-8B-Instruct \
#     --port 8000

# 멀티 GPU (Tensor Parallelism)
# python -m vllm.entrypoints.openai.api_server \
#     --model meta-llama/Llama-3.1-70B-Instruct \
#     --tensor-parallel-size 4 \
#     --port 8000

# 양자화 모델 서빙 (메모리 절약)
# python -m vllm.entrypoints.openai.api_server \
#     --model TheBloke/Llama-2-70B-Chat-AWQ \
#     --quantization awq \
#     --port 8000

# ============================================
# 2. Python에서 vLLM 직접 사용
# ============================================
from vllm import LLM, SamplingParams

def basic_vllm_inference():
    """vLLM으로 오프라인 배치 추론"""
    # 모델 로드
    llm = LLM(
        model="meta-llama/Llama-3.1-8B-Instruct",
        tensor_parallel_size=1,  # GPU 수
        gpu_memory_utilization=0.9,  # GPU 메모리 사용률
        max_model_len=4096  # 최대 컨텍스트 길이
    )

    # 샘플링 파라미터
    sampling_params = SamplingParams(
        temperature=0.7,
        top_p=0.95,
        max_tokens=512,
        stop=["", "[/INST]"]
    )

    # 배치 프롬프트
    prompts = [
        "[INST] Python으로 피보나치 함수를 작성해주세요. [/INST]",
        "[INST] 머신러닝과 딥러닝의 차이를 설명해주세요. [/INST]",
        "[INST] Docker의 장점을 3가지 알려주세요. [/INST]",
    ]

    # 추론 (자동 배칭)
    outputs = llm.generate(prompts, sampling_params)

    for output in outputs:
        prompt = output.prompt
        generated_text = output.outputs[0].text
        print(f"Prompt: {prompt[:50]}...")
        print(f"Output: {generated_text[:200]}...")
        print("-" * 50)

    return outputs

# ============================================
# 3. OpenAI 호환 클라이언트 사용
# ============================================
from openai import OpenAI

def vllm_openai_client():
    """vLLM 서버를 OpenAI 클라이언트로 호출"""
    # vLLM 서버 주소 (로컬)
    client = OpenAI(
        base_url="http://localhost:8000/v1",
        api_key="dummy"  # vLLM은 API 키 검증 안 함
    )

    # Chat Completion
    response = client.chat.completions.create(
        model="meta-llama/Llama-3.1-8B-Instruct",
        messages=[
            {"role": "system", "content": "당신은 유능한 AI 어시스턴트입니다."},
            {"role": "user", "content": "vLLM의 장점을 설명해주세요."}
        ],
        temperature=0.7,
        max_tokens=500
    )

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

# ============================================
# 4. 스트리밍 응답
# ============================================
def vllm_streaming():
    """스트리밍으로 토큰 단위 응답 받기"""
    client = OpenAI(
        base_url="http://localhost:8000/v1",
        api_key="dummy"
    )

    stream = client.chat.completions.create(
        model="meta-llama/Llama-3.1-8B-Instruct",
        messages=[
            {"role": "user", "content": "Python의 GIL에 대해 설명해주세요."}
        ],
        stream=True,
        max_tokens=300
    )

    full_response = ""
    for chunk in stream:
        if chunk.choices[0].delta.content:
            content = chunk.choices[0].delta.content
            print(content, end="", flush=True)
            full_response += content

    return full_response

# ============================================
# 5. 벤치마킹 및 성능 측정
# ============================================
import time
import asyncio
import aiohttp

async def benchmark_vllm(
    base_url: str = "http://localhost:8000",
    num_requests: int = 100,
    concurrency: int = 10
):
    """vLLM 서버 벤치마킹"""
    prompt = "What is the capital of France? Answer in one word."

    async def single_request(session):
        start = time.time()
        async with session.post(
            f"{base_url}/v1/completions",
            json={
                "model": "meta-llama/Llama-3.1-8B-Instruct",
                "prompt": prompt,
                "max_tokens": 50,
                "temperature": 0
            }
        ) as response:
            result = await response.json()
            latency = time.time() - start
            tokens = result.get("usage", {}).get("completion_tokens", 0)
            return latency, tokens

    # 동시 요청 실행
    async with aiohttp.ClientSession() as session:
        semaphore = asyncio.Semaphore(concurrency)

        async def bounded_request():
            async with semaphore:
                return await single_request(session)

        start_time = time.time()
        results = await asyncio.gather(*[
            bounded_request() for _ in range(num_requests)
        ])
        total_time = time.time() - start_time

    # 결과 분석
    latencies = [r[0] for r in results]
    total_tokens = sum(r[1] for r in results)

    print(f"=== vLLM Benchmark Results ===")
    print(f"Total requests: {num_requests}")
    print(f"Concurrency: {concurrency}")
    print(f"Total time: {total_time:.2f}s")
    print(f"Throughput: {num_requests / total_time:.2f} req/s")
    print(f"Token throughput: {total_tokens / total_time:.2f} tokens/s")
    print(f"Avg latency: {sum(latencies) / len(latencies) * 1000:.2f} ms")
    print(f"P50 latency: {sorted(latencies)[len(latencies)//2] * 1000:.2f} ms")
    print(f"P99 latency: {sorted(latencies)[int(len(latencies)*0.99)] * 1000:.2f} ms")

# ============================================
# 6. 프로덕션 설정 예시 (Docker Compose)
# ============================================
DOCKER_COMPOSE_EXAMPLE = """
# docker-compose.yml
version: '3.8'
services:
  vllm:
    image: vllm/vllm-openai:latest
    ports:
      - "8000:8000"
    volumes:
      - ~/.cache/huggingface:/root/.cache/huggingface
    environment:
      - HUGGING_FACE_HUB_TOKEN=${HF_TOKEN}
    command: >
      --model meta-llama/Llama-3.1-8B-Instruct
      --tensor-parallel-size 1
      --gpu-memory-utilization 0.9
      --max-model-len 8192
      --enable-prefix-caching
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
"""

# 실행 예시
if __name__ == "__main__":
    # 오프라인 추론 (vLLM 직접 사용)
    # basic_vllm_inference()

    # 서버 클라이언트 사용
    # vllm_openai_client()

    # 스트리밍
    # vllm_streaming()

    # 벤치마크 실행
    # asyncio.run(benchmark_vllm())

    print("vLLM 코드 예제 준비 완료!")
    print("서버 실행: python -m vllm.entrypoints.openai.api_server --model ")

📊 성능 & 비용

vLLM 추론 성능 벤치마크 (2025년 기준)

모델 GPU 처리량 (tokens/s) P99 지연시간 비고
Llama 3.1 8B H100 80GB 2,300~2,500 80ms 단일 GPU
Llama 3.1 70B 4x A100 80GB 400~600 150ms TP=4
Qwen2.5 7B 3x V100 32GB 1,782~2,474 ~100ms TP=2
Gemma3 4B A100 40GB 3,000+ 50ms 소형 모델
Mistral 7B AWQ RTX 4090 24GB 800~1,200 120ms 양자화

vLLM vs 경쟁 솔루션 비교

솔루션 피크 처리량 P99 지연시간 메모리 효율
vLLM 793 TPS 80ms 60~80% 절감
Ollama 41 TPS 673ms 기본
HuggingFace TGI ~500 TPS ~120ms Flash Attention
TensorRT-LLM ~900 TPS ~70ms NVIDIA 최적화

GPU 요구사항 가이드

모델 크기 FP16 VRAM AWQ/GPTQ 권장 GPU
7B ~14GB ~4GB RTX 4090 / A10
13B ~26GB ~7GB A100 40GB
34B ~68GB ~18GB A100 80GB
70B ~140GB ~35GB 2x A100 80GB
405B ~810GB ~100GB 8x H100 80GB
참고: vLLM 0.6.0 기준 이전 버전 대비 2.7배 처리량 향상, 5배 지연시간 감소를 달성했습니다. H100에서 Llama-3.3-70B FP8 추론 시 토큰당 0.39 joules로 에너지 효율이 뛰어납니다. KV 캐시를 위해 모델 VRAM 외 추가 40GB+ 여유가 필요합니다.

🗣️ 실무에서 이렇게 말하세요

💬 회의에서

"LLM API 비용이 너무 높아서 자체 서빙을 검토 중인데요." - "vLLM으로 셀프호스팅하면 비용을 90% 이상 줄일 수 있어요. H100 한 대로 Llama 8B를 서빙하면 초당 2,000토큰 이상 처리 가능하고, OpenAI API와 100% 호환돼서 코드 변경도 base_url만 바꾸면 됩니다. 초기 GPU 비용이 있지만, 월 10만 요청 이상이면 6개월 내 회수돼요."

💬 면접에서

"vLLM의 PagedAttention이 왜 중요한가요?" - "기존 LLM 추론에서 KV 캐시는 시퀀스 최대 길이만큼 메모리를 미리 할당해야 했어요. 실제로 사용하는 건 일부인데 나머지는 낭비되죠. PagedAttention은 OS의 가상 메모리처럼 필요한 만큼만 블록 단위로 할당해서 메모리 낭비를 60~80% 줄입니다. 덕분에 같은 GPU로 더 많은 동시 요청을 처리할 수 있어요."

💬 기술 토론에서

"vLLM vs TensorRT-LLM 어떤 걸 써야 하나요?" - "vLLM은 설치가 쉽고 다양한 모델을 바로 지원해서 빠른 프로토타이핑에 좋아요. TensorRT-LLM은 NVIDIA가 만들어서 H100에서 최적 성능이 나오지만, 모델 변환 과정이 필요하고 NVIDIA GPU 전용이에요. 처음 시작이거나 다양한 모델을 테스트할 땐 vLLM, 프로덕션에서 최대 성능이 필요하면 TensorRT-LLM을 고려하세요."

⚠️ 흔한 실수 & 주의사항

GPU 메모리 과다 할당

gpu_memory_utilization을 0.95 이상으로 설정하면 KV 캐시 공간이 부족해 OOM이 발생합니다. 0.85~0.90이 안전하며, 긴 컨텍스트가 필요하면 max_model_len을 명시적으로 제한하세요.

Tensor Parallelism 크기 불일치

tensor_parallel_size는 모델의 attention head 수로 나눠떨어져야 합니다. 예를 들어 32개 head 모델에 TP=3은 불가능합니다. 또한 GPU 간 통신 대역폭이 낮으면 오히려 성능이 떨어질 수 있어 NVLink 연결을 권장합니다.

올바른 접근 방법

프로덕션에서는 --enable-prefix-caching 옵션으로 시스템 프롬프트 캐싱을 활성화하세요. Prometheus 메트릭(/metrics)을 모니터링하고, HuggingFace 모델은 사전 다운로드(huggingface-cli download)로 콜드 스타트를 방지하세요. 양자화 모델(AWQ/GPTQ)로 메모리 대비 처리량을 최적화할 수 있습니다.

🔗 관련 용어

📚 더 배우기