13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[태그:] vLLM

  • [AI] 로컬 LLM 성능: vLLM 벤치마크 측정 방법론

    [AI] 로컬 LLM 성능: vLLM 벤치마크 측정 방법론

    로컬 LLM 성능: vLLM 벤치마크 측정 방법론

    로컬 LLM 성능은 GPU 이름보다 측정 방식에서 갈립니다

    로컬 LLM 성능을 비교할 때 가장 위험한 문장은 ‘이 GPU면 충분히 빠르겠지’입니다. 실제 운영에 가까워질수록 병목은 GPU 코어 하나로 설명되지 않거든요. 프롬프트를 읽는 prefill, 토큰을 하나씩 생성하는 decode, KV cache가 차지하는 메모리, tokenizer와 HTTP 클라이언트, 동시 요청 스케줄링이 같이 움직입니다.

    저는 벤치마크를 볼 때 숫자 하나만 믿지 않습니다. 초당 토큰 수가 좋아도 첫 토큰이 늦으면 채팅 UX는 답답합니다. 반대로 TTFT가 괜찮아도 동시 요청을 올렸을 때 p95 지연 시간이 크게 튀면 내부 서비스로 쓰기 불안하더라고요.

    이 글은 vLLM을 띄우는 법을 소개하는 데서 멈추지 않습니다. 어떤 값을 고정하고, 무엇을 바꿔야 비교가 성립하는지, vLLM 옵션이 성능·비용·안정성에 어떤 영향을 주는지, 결과를 보고 운영선을 어떻게 잡는지까지 정리합니다. 특정 GPU의 성능 수치는 지어내지 않겠습니다. 대신 여러분 장비에서 재현할 수 있는 로컬 LLM 성능 측정 절차와 판단 기준을 남기겠습니다.

    로컬 LLM 성능 벤치마크 전체 아키텍처 다이어그램

    로컬 GPU 서버에서 vLLM API 서버를 띄우고, 벤치마크 클라이언트가 요청을 보내며, GPU/시스템 지표를 함께 수집하는 흐름입니다.

    vLLM 벤치마크 전에 성능 지표를 운영 관점으로 잡기

    vLLM은 OpenAI 호환 API 서버로 사용할 수 있는 LLM 추론 엔진입니다. 핵심은 여러 요청을 효율적으로 스케줄링하고, KV cache를 관리하며, 서버형 추론에서 처리량을 끌어올리는 데 있습니다. 여기서 성능 지표는 사전식 정의보다 ‘어떤 장애를 미리 알려주는가’로 보는 편이 훨씬 쓸모 있습니다.

    • TTFT(Time To First Token): 사용자가 요청한 뒤 첫 토큰이 나오기까지의 시간입니다. 길면 모델이 똑똑해도 느리게 느껴집니다. 긴 입력, prefill 부하, 큐 대기 시간이 주로 영향을 줍니다.
    • TPOT(Time Per Output Token): 첫 토큰 이후 출력 토큰 하나를 생성하는 평균 시간입니다. 답변이 길어질수록 체감 속도를 좌우합니다.
    • ITL(Inter Token Latency): 토큰 사이 간격입니다. 스트리밍 응답이 뚝뚝 끊기는지 확인할 때 봅니다.
    • E2E Latency: 요청부터 최종 응답 완료까지의 전체 시간입니다. 배치 요약, 문서 처리, 자동화 워크플로에서는 이 값이 더 중요할 수 있습니다.
    • Throughput: 초당 처리한 요청 수 또는 토큰 수입니다. 사용자 수를 늘릴 때 비용 효율을 판단하는 기준입니다.
    • Concurrency: 동시에 처리 중인 요청 수입니다. vLLM의 장점은 보통 단일 요청보다 이 구간에서 더 잘 드러납니다.
    • GPU Memory: 모델 가중치, KV cache, CUDA graph, 런타임 오버헤드가 함께 차지하는 메모리입니다. OOM은 대개 모델 크기 하나가 아니라 컨텍스트 길이와 동시성이 합쳐져 발생합니다.

    현장에서 가장 많이 본 착시는 ‘초당 토큰이 높으니 빠르다’는 판단입니다. 문서 요약처럼 입력이 긴 워크로드는 prefill 비용이 커서 TTFT가 먼저 무너질 수 있습니다. 반대로 짧은 채팅을 많이 받는 서비스는 decode보다 큐 대기와 스케줄링이 더 큰 문제가 되기도 합니다. 그래서 LLM 벤치마크는 항상 워크로드를 먼저 정하고 들어가야 합니다.

    LLM 벤치마크 환경은 README처럼 고정하세요

    LLM 벤치마크에서 비교가 깨지는 가장 흔한 이유는 실험 조건이 매번 조금씩 달라지는 겁니다. 모델만 같아도 토크나이저 버전, dtype, max model length, 요청 분포, temperature, 스트리밍 여부가 바뀌면 숫자는 달라집니다. 실험을 시작하기 전에 아래 항목을 먼저 적어두세요. 이거 진짜 편합니다. 나중에 결과가 이상할 때 기록이 없으면 원인 추적이 거의 감으로 변하거든요.

    항목 기록할 내용 왜 중요한가
    vLLM 버전 <code>vllm –version, 설치 방식, 컨테이너 태그 CLI 옵션과 스케줄러 동작은 버전에 따라 바뀔 수 있습니다.
    GPU/드라이버 nvidia-smi 출력, CUDA 런타임, GPU 개수 메모리 용량과 커널 지원 여부가 추론 안정성에 직접 영향을 줍니다.
    모델 Hugging Face ID, 로컬 경로, revision, 양자화 여부 가중치와 토크나이저가 다르면 같은 이름처럼 보여도 비교가 아닙니다.
    입력 길이 평균/최대 prompt token, 테스트 데이터셋 긴 입력은 prefill 비용과 KV cache 사용량을 키웁니다.
    출력 길이 max_tokens, 고정 출력 길이 사용 여부 decode 성능을 비교하려면 출력 길이를 통제해야 합니다.
    동시성 1, 2, 4, 8, 16처럼 단계별 증가 처리량이 늘다가 지연만 커지는 지점을 찾아야 합니다.
    서버 옵션 dtype, max-model-len, gpu-memory-utilization, max-num-seqs 메모리, 큐잉, 처리량, OOM 위험이 여기서 크게 갈립니다.
    요청 방식 chat/completions 여부, streaming 여부, timeout 클라이언트 측 지연과 서버 처리 지연을 분리해 봐야 합니다.

    ‘테스트 데이터셋’이라는 말에 너무 겁먹을 필요는 없습니다. 사내 문서 요약용이면 실제 문서에서 민감정보를 제거한 샘플 30~100개, 코딩 보조용이면 짧은 질문·중간 길이 수정 요청·긴 파일 기반 요청을 나눠 준비하면 됩니다. 핵심은 매번 같은 입력 분포를 쓰는 것입니다.

    vLLM 서버는 옵션 파일로 남기세요

    설치는 환경 의존성이 큽니다. CUDA, PyTorch, 드라이버 조합은 공식 설치 문서를 확인하는 편이 안전합니다. 아래 명령은 NVIDIA GPU가 있는 Linux 환경에서 시작점을 잡는 예시로 보세요. 벤치마크 재현성을 위해 서버 실행 옵션은 셸 히스토리에만 남기지 말고 YAML 파일로 고정하는 걸 권합니다.

    python3 -m venv .venv
    source .venv/bin/activate
    python -m pip install --upgrade pip
    pip install vllm
    vllm --version
    python - <<'PY'
    import torch
    print('cuda_available=', torch.cuda.is_available())
    print('gpu_count=', torch.cuda.device_count())
    PY

    아래는 단일 GPU에서 시작할 때의 보수적인 예시입니다. 모델 경로는 여러분 환경에 맞게 바꾸세요. vLLM의 --config는 YAML 설정 파일을 읽을 수 있으며, 같은 값이 CLI와 설정 파일에 동시에 있으면 CLI 인자가 우선합니다. 그래서 실제 운영에서는 중복을 줄이고, 실험에서는 어떤 값이 적용됐는지 로그로 한 번 더 확인하는 습관이 좋습니다.

    # vllm-serve.yaml
    model: /models/your-llm-model
    host: 0.0.0.0
    port: 8000
    dtype: auto
    max_model_len: 4096
    gpu_memory_utilization: 0.85
    max_num_seqs: 16
    enable_prefix_caching: true
    disable_log_requests: true
    vllm serve --config vllm-serve.yaml

    max_model_len은 서버가 받아들일 입력+출력의 최대 토큰 길이입니다. 무작정 크게 잡으면 긴 문서를 받을 수는 있지만 KV cache 여유가 줄어 동시성이 줄고 OOM 가능성이 커집니다. gpu_memory_utilization은 vLLM이 모델 실행에 사용할 GPU 메모리 비율입니다. 높이면 더 큰 KV cache를 확보할 수 있지만, 드라이버·라이브러리·다른 프로세스가 쓸 여유 공간은 줄어듭니다.

    max_num_seqs는 한 번의 스케줄링 반복에서 처리할 수 있는 시퀀스 수의 상한으로 이해하면 쉽습니다. 짧은 채팅을 많이 받는 서버라면 늘려볼 가치가 있고, 긴 문서 요약처럼 요청 하나가 큰 경우에는 과하게 늘릴수록 지연 시간과 메모리 압박이 커질 수 있습니다. enable_prefix_caching은 같은 시스템 프롬프트나 반복 prefix가 많은 워크로드에서 효과를 기대할 수 있지만, 요청 prefix가 매번 완전히 다르면 체감이 작을 수 있습니다.

    vLLM API 서버와 벤치마크 클라이언트 구성도

    vLLM 서버 실행 옵션과 벤치마크 클라이언트가 연결되는 구조를 한눈에 볼 수 있는 구성도입니다.

    추론 성능 측정은 워밍업, 단일 요청, 동시성 순서로 갑니다

    서버가 떠도 바로 측정값을 믿으면 안 됩니다. 첫 요청에는 모델 로딩 이후 남은 초기화, CUDA kernel 준비, 캐시 관련 비용이 섞일 수 있습니다. 저는 보통 헬스 체크, 워밍업, 단일 요청 확인, 동시성 테스트 순서로 갑니다. 이 순서를 지키면 서버가 느린 건지, 첫 요청만 느린 건지, 클라이언트가 병목인지 분리하기 쉽습니다.

    curl -s http://127.0.0.1:8000/v1/models | jq .
    
    for i in 1 2 3; do
      curl -s http://127.0.0.1:8000/v1/chat/completions \
        -H 'Content-Type: application/json' \
        -d '{
          "model": "/models/your-llm-model",
          "messages": [{"role": "user", "content": "warmup"}],
          "max_tokens": 16,
          "temperature": 0
        }' > /dev/null
      echo warmup-$i-done
    done

    간단한 smoke test에서는 응답이 오는지만 보지 말고 usage 필드도 확인하세요. 프롬프트 토큰과 completion 토큰이 예상보다 크게 다르면 테스트 입력이 통제되지 않은 것입니다. 특히 한국어와 영어는 토크나이저에서 토큰 수가 다르게 나올 수 있으니, 같은 글자 수가 곧 같은 부하라는 착각을 피해야 합니다.

    curl -s http://127.0.0.1:8000/v1/chat/completions \
      -H 'Content-Type: application/json' \
      -d '{
        "model": "/models/your-llm-model",
        "messages": [
          {"role": "system", "content": "간결하게 답하세요."},
          {"role": "user", "content": "로컬 LLM 성능 측정에서 TTFT가 왜 중요한지 설명해줘."}
        ],
        "max_tokens": 128,
        "temperature": 0
      }' | jq '{id, usage, finish_reason: .choices[0].finish_reason}'

    직접 만든 부하 스크립트도 유용합니다. 아래 코드는 스트리밍 응답에서 첫 chunk가 도착하는 시점과 전체 완료 시간을 분리해 기록합니다. 전문 도구를 대체하진 않지만, TTFT와 E2E가 왜 다르게 움직이는지 눈으로 확인하기 좋습니다.

    import asyncio
    import json
    import time
    import httpx
    
    URL = 'http://127.0.0.1:8000/v1/chat/completions'
    MODEL = '/models/your-llm-model'
    
    payload = {
        'model': MODEL,
        'messages': [
            {'role': 'user', 'content': 'vLLM으로 로컬 LLM 성능을 측정하는 기준을 설명해줘.'}
        ],
        'max_tokens': 128,
        'temperature': 0,
        'stream': True,
    }
    
    async def one_request(client, idx):
        start = time.perf_counter()
        first_chunk_at = None
        chunks = 0
        async with client.stream('POST', URL, json=payload, timeout=120) as response:
            response.raise_for_status()
            async for line in response.aiter_lines():
                if not line.startswith('data: '):
                    continue
                data = line.removeprefix('data: ').strip()
                if data == '[DONE]':
                    break
                chunks += 1
                if first_chunk_at is None:
                    first_chunk_at = time.perf_counter()
        end = time.perf_counter()
        ttft = None if first_chunk_at is None else first_chunk_at - start
        return {'request': idx, 'ttft_sec': ttft, 'e2e_sec': end - start, 'chunks': chunks}
    
    async def run(concurrency):
        limits = httpx.Limits(max_connections=concurrency, max_keepalive_connections=concurrency)
        async with httpx.AsyncClient(limits=limits) as client:
            results = await asyncio.gather(*(one_request(client, i) for i in range(concurrency)))
        for item in results:
            print(json.dumps(item, ensure_ascii=False))
    
    if __name__ == '__main__':
        asyncio.run(run(concurrency=4))

    이 스크립트를 concurrency=1, 2, 4, 8로 바꿔가며 돌려보면 패턴이 보입니다. TTFT만 급격히 늘면 큐 대기나 prefill 경쟁을 의심하고, TTFT는 괜찮은데 E2E가 길어지면 decode 구간의 경쟁이나 출력 길이 분포를 봅니다. 에러가 섞이면 평균값은 잠깐 내려놓고 실패율부터 봐야 합니다.

    vLLM 내장 벤치마크로 AI 성능 비교 기준선 만들기

    직접 만든 스크립트는 원인을 좁히는 데 좋고, 반복 가능한 비교에는 vLLM의 벤치마크 명령이 편합니다. 버전에 따라 세부 옵션은 달라질 수 있으니 vllm bench serve --help를 먼저 확인하세요. 핵심은 backend, endpoint, model, dataset 또는 synthetic 입력 조건, 동시성, 프롬프트 수를 명시하는 것입니다.

    vllm bench serve --help
    
    vllm bench serve \
      --backend openai-chat \
      --base-url http://127.0.0.1:8000 \
      --endpoint /v1/chat/completions \
      --model /models/your-llm-model \
      --num-prompts 100 \
      --max-concurrency 4 \
      --percentile-metrics ttft,tpot,itl,e2el

    여기서 주의할 점은 ‘벤치마크 클라이언트에서 측정한 지연 시간’이라는 사실입니다. 서버 내부 처리 시간, 네트워크, 클라이언트 이벤트 루프, JSON 파싱이 한 덩어리로 섞일 수 있습니다. 그래서 같은 서버에서 한 번, 다른 머신에서 한 번, 가능하면 클라이언트 CPU 사용률까지 같이 보는 편이 좋습니다.

    데이터셋 기반 테스트를 할 때는 입력 길이 분포를 꼭 기록하세요. 평균 토큰만 보면 안 됩니다. p95 입력 길이가 긴 데이터셋은 짧은 요청 다수보다 훨씬 거칠게 서버를 흔듭니다. 특히 문서 요약형 워크로드는 평균보다 꼬리가 문제입니다. 운영 장애는 보통 평균 요청이 아니라 긴 요청 몇 개가 큐를 막을 때 시작됩니다.

    로컬 LLM 성능 테스트에서 자주 보이는 실패 모드

    OOM과 느린 첫 응답은 표면 증상입니다. 같은 OOM이라도 원인은 모델이 너무 큰 경우, 컨텍스트를 과하게 연 경우, 동시성을 너무 높인 경우, logprobs 같은 옵션이 메모리를 더 쓰는 경우로 나뉩니다. 해결책도 다릅니다. 무조건 작은 모델로 내리면 문제는 사라지지만, 품질과 비용의 균형점을 찾을 기회를 잃습니다.

    watch -n 1 nvidia-smi
    
    nvidia-smi --query-gpu=timestamp,name,index,utilization.gpu,utilization.memory,memory.used,memory.total,power.draw \
      --format=csv -l 1
    증상 가능성이 높은 근본 원인 먼저 볼 것 우선 조치
    첫 요청만 유독 느림 워밍업 부족, 커널 초기화, 캐시 미형성 워밍업 전후 TTFT 차이 측정 전 워밍업 요청을 제외하고 기록합니다.
    동시성 증가 시 TTFT가 먼저 튐 큐 대기, 긴 prefill 경쟁, max_num_seqs 과다 입력 토큰 분포, p95 TTFT 긴 입력과 짧은 입력을 분리 측정하고 동시성 상한을 낮춥니다.
    출력 중간부터 느려짐 decode 병목, 긴 출력 요청 혼재 max_tokens, 실제 completion token 출력 길이를 고정한 테스트와 실제 분포 테스트를 따로 돌립니다.
    GPU 메모리가 가득 찬 뒤 OOM KV cache 증가, 과한 max_model_len, 동시 요청 과다 memory.used 추이, 요청별 입력 길이 max_model_len, max_num_seqs, 동시성을 순서대로 줄입니다.
    GPU 사용률이 낮은데 응답이 느림 클라이언트 병목, tokenizer/CPU 병목, 연결 재사용 실패 클라이언트 CPU, HTTP keep-alive, 서버 로그 비동기 클라이언트, 연결 풀, 클라이언트 분리 실행을 확인합니다.
    평균은 좋은데 p95가 나쁨 긴 요청 꼬리, 큐 대기 증가 요청 길이 p50/p95, 실패 요청 로그 워크로드를 길이별로 나누고 운영 한계는 평균이 아니라 p95로 잡습니다.

    운영에서는 ‘성능을 더 뽑는 옵션’보다 ‘실패할 때 예측 가능한 옵션’이 더 가치 있을 때가 많습니다. 예를 들어 gpu_memory_utilization을 올리면 실험실 처리량은 좋아질 수 있지만, 같은 GPU에서 모니터링 에이전트나 다른 프로세스가 함께 돌면 여유 메모리가 부족해질 수 있습니다. 벤치마크가 목적이면 한계를 밀어보고, 서비스가 목적이면 일부러 여유를 남기는 선택이 맞습니다.

    검증과 결과 해석: 평균값보다 꺾이는 지점을 찾으세요

    추론 성능 측정에서 제가 가장 신뢰하는 그림은 동시성별 TTFT, TPOT, E2E latency, tokens/sec, 실패율을 한 표에 놓은 것입니다. 평균만 있으면 위험합니다. 최소한 p50과 p95를 같이 보세요. p50은 평소 체감, p95는 불만이 시작되는 구간에 가깝습니다.

    1. 단일 요청에서 TTFT가 제품 UX에 맞는가?
    2. 동시성을 올렸을 때 처리량이 증가하는 구간과 지연만 늘어나는 구간이 어디인가?
    3. 먼저 오는 한계가 GPU 메모리인지, decode 처리량인지, 클라이언트 병목인지 구분했는가?
    4. 실패율이 0에 가까운 구간과 단순히 평균이 좋은 구간이 같은가?

    예를 들어 내부 문서 요약 챗봇을 만든다고 해보겠습니다. 사용자는 한 번에 긴 문서를 넣고 답변을 기다립니다. 이 경우 짧은 프롬프트로 초당 토큰 수만 재면 거의 도움이 안 됩니다. 2천 토큰, 8천 토큰, 16천 토큰처럼 입력 길이를 나누고, 출력 길이는 고정한 뒤 TTFT와 E2E를 봐야 합니다. 반면 짧은 고객 응대 챗봇이면 입력 길이보다 동시 요청과 p95 TTFT가 더 중요합니다.

    vLLM 로컬 LLM 성능 벤치마크 결과 대시보드

    동시성 증가에 따른 응답 시간, 처리량, GPU 메모리 사용량을 함께 보여주는 결과 대시보드 이미지입니다.

    비교 목적 고정할 값 바꿀 값 판단 기준
    모델 후보 비교 프롬프트, max_tokens, 동시성, vLLM 옵션 모델 경로 또는 revision 품질 평가는 별도로 두고, 여기서는 지연·처리량·메모리만 비교합니다.
    동시성 한계 확인 모델, 입력 길이, 출력 길이 max-concurrency, 클라이언트 수 처리량 증가가 둔화되고 p95 지연이 급등하기 직전이 운영 후보입니다.
    긴 컨텍스트 영향 모델, 출력 길이, 동시성 입력 토큰 길이, max_model_len TTFT와 GPU 메모리가 함께 증가하는지 봅니다.
    서버 옵션 튜닝 모델, 테스트 데이터, 클라이언트 gpu_memory_utilization, max_num_seqs 평균 처리량보다 실패율과 p95 안정성을 우선합니다.
    prefix cache 효과 확인 시스템 프롬프트, 모델, 동시성 반복 prefix 여부, enable_prefix_caching 같은 prefix가 많은 워크로드에서만 의미 있게 비교합니다.

    설정값 선택 기준: 빠르게보다 맞게 고르는 게 먼저입니다

    vLLM 옵션은 레버처럼 움직입니다. 하나를 올리면 다른 쪽에서 비용을 냅니다. max_model_len을 키우면 긴 입력을 받을 수 있지만 KV cache 예산이 커지고, max_num_seqs를 키우면 동시 처리 가능성은 늘지만 요청 하나의 지연 시간이 튈 수 있습니다. 그래서 설정은 ‘최대 성능’이 아니라 ‘내 워크로드에 맞는 실패 방식’을 고르는 일에 가깝습니다.

    상황 추천 방향 피해야 할 선택
    개인 실험, 단일 사용자 작은 max_num_seqs, 필요한 만큼의 max_model_len, 단일 요청 TTFT 확인 운영도 안 할 긴 컨텍스트를 크게 열어 메모리를 낭비하는 것
    짧은 채팅을 여러 사용자가 사용 동시성 단계 테스트, p95 TTFT 기준 운영선 설정 평균 tokens/sec만 보고 동시성 상한을 높게 잡는 것
    긴 문서 요약 입력 길이 버킷별 측정, max_model_len 보수적 설정, 긴 요청 별도 큐 고려 짧은 프롬프트 벤치마크 결과를 긴 문서 처리에 적용하는 것
    동일 시스템 프롬프트 반복 enable_prefix_caching 효과를 별도 실험 prefix가 매번 다른데 캐시 효과를 기대하는 것
    한 GPU에 다른 프로세스도 실행 gpu_memory_utilization을 낮춰 여유 확보 벤치마크 순간 최대 처리량만 보고 메모리를 끝까지 쓰는 것

    제 기준의 기본값은 이렇습니다. 처음에는 max_model_len을 실제 요청 p95보다 약간 큰 수준으로 잡고, gpu_memory_utilization은 여유를 남긴 값에서 시작합니다. 그다음 동시성을 올리며 p95 TTFT와 실패율을 봅니다. 처리량이 조금 더 나오더라도 p95가 급격히 튀는 지점은 운영선으로 잡지 않습니다.

    자주 묻는 질문

    Q. vLLM만 쓰면 무조건 빨라지나요?

    아닙니다. vLLM은 서버형 추론, 특히 동시 요청 처리에서 강점이 큽니다. 하지만 단일 요청을 짧게 돌리는 실험에서는 차이가 작을 수 있고, 모델이 GPU 메모리에 빠듯하게 올라가는 환경에서는 설정을 잘못 잡으면 오히려 불안정해질 수 있습니다.

    Q. 로컬 LLM 성능 비교에서 가장 먼저 볼 지표는 뭔가요?

    채팅 UX라면 TTFT와 p95 TTFT를 먼저 봅니다. 배치 요약이나 내부 자동화라면 E2E latency와 처리량을 더 봅니다. 여러 사용자가 붙는 서비스라면 평균보다 실패율과 p95가 우선입니다.

    Q. 블로그에 있는 벤치마크 숫자를 믿어도 되나요?

    참고는 됩니다. 다만 그대로 의사결정에 쓰면 위험합니다. GPU, 드라이버, vLLM 버전, 모델 revision, 입력 길이, 출력 길이, 동시성, 스트리밍 여부가 다르면 결과가 바뀝니다. 같은 절차로 내 장비에서 다시 재는 것이 가장 확실합니다.

    Q. 양자화 모델은 항상 좋은 선택인가요?

    메모리 절감에는 유리할 수 있지만 품질, 지원 커널, decode 성능, 운영 안정성을 함께 봐야 합니다. ‘올라간다’와 ‘서비스하기 좋다’는 다릅니다. 양자화 여부를 바꿀 때는 모델 품질 평가와 추론 성능 평가를 분리하세요.

    Q. GPU 사용률이 낮으면 vLLM 설정 문제인가요?

    항상 그렇지는 않습니다. 클라이언트가 요청을 충분히 못 넣거나, CPU 토크나이징이 막히거나, 네트워크/HTTP 연결이 병목일 수 있습니다. 서버 옵션을 바꾸기 전에 클라이언트가 실제로 원하는 동시성을 만들고 있는지 확인하는 편이 빠릅니다.

    마지막 판단: 운영선은 가장 빠른 지점이 아니라 덜 흔들리는 지점입니다

    로컬 LLM 성능에서 중요한 질문은 ‘어떤 모델이 제일 빠른가’가 아니라 ‘내 서버가 어떤 요청 분포까지 예측 가능하게 버티는가’입니다. 저는 단일 최고 점수보다 반복 측정에서 비슷하게 나오는 구간을 더 신뢰합니다. 운영자는 평균값이 아니라 나쁜 날의 p95를 상대해야 하니까요.

    개인 실험용이면 작은 모델부터 시작해 TTFT와 GPU 메모리 여유를 확인하세요. 여러 사용자가 붙는 내부 서비스라면 vLLM 서버를 띄운 뒤 동시성별 p50/p95 TTFT, E2E latency, tokens/sec, 실패율을 함께 기록하세요. 긴 문서 요약처럼 입력이 긴 워크로드라면 짧은 채팅 벤치마크를 버리고 입력 길이 버킷을 따로 만들어야 합니다.

    제가 실제로 권하는 절차는 단순합니다. 워밍업을 제외하고, 같은 프롬프트 세트와 같은 출력 길이로, 동시성을 1에서 시작해 단계적으로 올립니다. 처리량이 더 늘지 않거나 p95 지연 시간이 갑자기 커지거나 실패가 섞이는 지점이 나오면, 그 직전 단계를 운영 후보로 잡습니다. 그다음 max_model_len, max_num_seqs, gpu_memory_utilization을 하나씩만 바꿔 다시 측정합니다.

    다음 글에서는 Prometheus와 Grafana를 붙여 vLLM 추론 서버의 요청 지표, GPU 메모리, 지연 시간 분포를 시각화하는 방법을 다룰 예정입니다. 관련 글로는 GPU 모니터링, 모델 서빙 아키텍처, LLM 비용 최적화 글을 함께 연결하면 독자가 다음 단계로 넘어가기 좋습니다. 벤치마크는 한 번 찍는 스크린샷이 아니라 운영 기준을 만드는 과정입니다.

    TTFT, 처리량, 동시성, GPU 메모리 기준으로 로컬 LLM 성능 비교 방법을 요약한 인포그래픽입니다.

    제 기준의 한 줄 권고는 이렇습니다. 빠른 숫자 하나를 찾지 말고, 같은 조건에서 동시성을 올리며 p95 지연 시간과 실패율이 꺾이는 지점을 찾으세요. 그 직전이 여러분 로컬 LLM 서버의 현실적인 운영선입니다.

  • [AI] LLM 추론 비용 절감 핵심: 프롬프트 캐싱 원리와 실제 적용 사례 분석

    [AI] LLM 추론 비용 절감 핵심: 프롬프트 캐싱 원리와 실제 적용 사례 분석

    목차

    [인프라] 프롬프트 캐싱으로 LLM 비용 줄이는 법

    프롬프트 캐싱은 요즘 LLM 비용, 응답 속도, GPU 자원 효율을 같이 잡을 때 거의 빠지지 않는 주제입니다. 특히 시스템 프롬프트(system prompt), 공통 지시문(shared instruction), RAG(Retrieval-Augmented Generation, 검색 증강 생성)에서 반복되는 긴 컨텍스트를 계속 넣고 있다면 비용이 생각보다 빨리 불어나거든요. 저도 홈랩에서 vLLM으로 여러 실험을 돌리다가, 모델이 똑같은 앞부분을 계속 읽고 있다는 걸 보고 나서야 이 문제를 제대로 체감했습니다. 처음엔 “어차피 토큰은 토큰 아닌가?” 싶었는데, 실제로 써보니까 반복되는 prefix(프리픽스, 앞부분 문맥)를 얼마나 재사용하느냐가 체감 성능에 꽤 큰 차이를 만들더라고요.

    특히 운영 환경에서는 단순히 LLM 비용만 줄이는 게 아니라, LLM Latency까지 안정적으로 줄이는 방향으로 봐야 합니다. 요청이 몰릴 때 매번 긴 지시문을 처음부터 다시 처리하면 지연이 늘고, GPU 메모리도 금방 빡빡해지거든요. 그래서 이번 글에서는 프롬프트 캐싱의 원리, vLLM에서 생각해야 할 포인트, 그리고 실제 서비스에 붙일 때 어떤 식으로 구조를 잡으면 좋은지 제 경험 기준으로 풀어보겠습니다.

    공통 시스템 프롬프트와 사용자 요청이 합쳐지고, 반복되는 prefix가 캐시 재사용되는 전체 흐름을 보여주는 이미지입니다.

    왜 프롬프트 캐싱이 중요한가: 비용보다 먼저 병목이 옵니다

    많이들 비용 절감부터 떠올리시는데, 제가 직접 운영해보니 문제는 그보다 먼저 병목으로 드러났습니다. 예를 들어 팀 공용 챗봇을 만든다고 해볼게요.

    • 모든 요청 앞에 긴 역할 지시문을 붙입니다.
    • 보안 정책, 응답 형식, 금지어 목록도 같이 넣습니다.
    • RAG 문서 헤더나 도메인 설명도 반복됩니다.

    이렇게 되면 사용자 질문은 짧은데도 앞단 프롬프트가 너무 길어집니다. 그러면 모델 입장에서는 매 요청마다 같은 앞부분을 다시 prefill(프리필, 입력 토큰을 읽어 내부 상태를 만드는 단계)해야 하죠. 여기서 시간이 들고, 비용이 들고, GPU 사용량도 올라갑니다. 특히 동시 요청이 많아지면 “왜 모델은 놀고 있지 않은데 응답은 느리지?” 같은 상황이 생깁니다. 저도 처음엔 네트워크 문제인 줄 알고 한참 봤었는데, 근본 원인은 반복 prefix였습니다. 삽질 좀 했습니다 ㅎㅎ

    프롬프트 캐싱 원리: 쉽게 말해 같은 머리말을 다시 읽지 않게 하는 겁니다

    쉽게 말해 프롬프트 캐싱은 모델이 이미 처리한 앞부분 문맥을 재활용하는 방식입니다. 구현 방식은 엔진마다 조금씩 다르지만, 핵심 개념은 비슷합니다.

    KV Cache(Key-Value Cache, 어텐션 계산 재사용용 캐시)와의 관계

    트랜스포머 기반 모델은 입력 토큰을 처리하면서 attention(어텐션) 계산에 필요한 내부 상태를 만듭니다. 이 상태를 흔히 KV Cache라고 부르는데, 같은 prefix가 다시 들어오면 이 계산 결과를 재사용할 수 있습니다. 그러면 매번 처음부터 다 계산하지 않아도 됩니다.

    구분 캐싱 전 캐싱 후
    반복 시스템 프롬프트 매 요청마다 다시 처리 같은 prefix면 재사용 가능
    LLM 비용 반복 토큰 처리 비용 누적 반복 처리 감소 기대
    LLM Latency prefill 구간이 길어짐 초반 처리 시간 단축 기대
    효과가 큰 상황 짧은 프롬프트 위주 긴 공통 지시문, RAG 헤더, 에이전트 템플릿

    여기서 중요한 포인트!

    캐싱은 완전히 같은 prefix일수록 잘 먹힙니다. 문장 하나, 공백 하나, 타임스탬프 하나가 달라도 캐시 히트(hit)가 깨질 수 있습니다. 그래서 실무에서는 모델 엔진만 보는 게 아니라, 애플리케이션 레이어에서 프롬프트를 얼마나 안정적으로 고정하느냐가 정말 중요합니다.

    어떤 요청이 캐싱에 잘 맞나: 적용 대상부터 고르셔야 합니다

    모든 요청에 프롬프트 캐싱이 큰 효과를 주는 건 아닙니다. 제가 실제로 써보니 아래 패턴에서 특히 체감이 컸습니다.

    1. 시스템 프롬프트가 길고 거의 고정된 챗봇
    2. 출력 형식이 엄격한 JSON 생성 작업
    3. RAG 공통 헤더가 긴 문서 QA
    4. 멀티턴 에이전트에서 고정 정책이 반복되는 경우

    반대로 매 요청마다 앞부분이 크게 달라지는 워크로드는 효과가 제한적일 수 있습니다. 예를 들어 사용자별 정책 블록이 길고 자주 바뀌면 캐시 재사용률이 떨어집니다. 그래서 먼저 해야 할 일은 “우리 요청 중 어디가 반복되나?”를 보는 겁니다.

    프롬프트 캐싱을 위한 고정 prefix와 가변 입력 분리 구조 이미지

    캐시가 잘 먹도록 공통 prefix와 사용자별 가변 영역을 분리한 프롬프트 설계 예시입니다.

    실전 구현 1: 프롬프트를 캐시 친화적으로 재구성하기

    엔진 설정 전에 먼저 프롬프트 구조를 손보는 게 맞습니다. 이걸 안 하고 옵션만 켜면 기대보다 효과가 작아요. 저도 처음엔 서버 설정만 바꿨다가 별 차이가 없어서 한참 헤맸거든요.

    1. 공통 prefix를 템플릿으로 고정합니다

    SYSTEM_PREFIX = """You are an infrastructure assistant.
    Follow the security policy below:
    1. Do not output secrets.
    2. Answer in Korean unless asked otherwise.
    3. Use concise technical explanations.
    Output must be valid JSON when requested.
    """
    
    DOMAIN_PREFIX = """Service context:
    - Environment: self-hosted GPU
    - Gateway: internal API
    - Logging: request metadata only
    """
    
    def build_prompt(user_question: str) -> str:
        return f"{SYSTEM_PREFIX}\n{DOMAIN_PREFIX}\nUser: {user_question}\nAssistant:"

    핵심은 SYSTEM_PREFIX와 DOMAIN_PREFIX를 자주 바꾸지 않는 겁니다. 날짜, 요청 ID, 사용자 이름처럼 매번 바뀌는 값은 뒤쪽으로 빼세요.

    2. 가변 값은 prefix 뒤로 보냅니다

    • 좋은 예: 정책, 역할, 출력 형식은 앞에 고정
    • 주의할 예: 현재 시각, 세션 ID, A/B 테스트 라벨은 뒤로 이동
    • 나쁜 예: 공통 머리말 중간에 매 요청마다 바뀌는 값 삽입

    3. 문자열 정규화(normalization, 형식 통일)를 적용합니다

    def normalize_prompt_block(text: str) -> str:
        lines = [line.rstrip() for line in text.strip().splitlines()]
        return "\n".join(lines)
    
    SYSTEM_PREFIX = normalize_prompt_block(SYSTEM_PREFIX)
    DOMAIN_PREFIX = normalize_prompt_block(DOMAIN_PREFIX)

    공백, 줄바꿈, 들여쓰기가 달라져도 캐시 히트율이 떨어질 수 있어서, 이런 정규화는 생각보다 효과가 있습니다. 사소해 보여도 운영에서는 꽤 중요하더라고요.

    실전 구현 2: vLLM 기반 추론 서버에 붙이는 방식

    vLLM은 OpenAI 호환 API 형태로 많이 붙이기 좋고, 반복 prefix 재사용 관점에서도 자주 언급되는 엔진입니다. 세부 옵션은 배포 방식에 따라 다를 수 있으니 공식 문서와 현재 빌드를 꼭 같이 확인하셔야 하지만, 운영 구조는 대체로 비슷합니다.

    예시 배포 흐름

    1. 모델 서버를 띄웁니다.
    2. 애플리케이션에서 공통 prefix를 최대한 고정합니다.
    3. OpenAI 호환 엔드포인트로 요청을 보냅니다.
    4. 로그에서 prefill 지연과 처리량 변화를 확인합니다.
    vllm serve meta-llama/Meta-Llama-3-8B-Instruct \
      --host 0.0.0.0 \
      --port 8000

    위 예시는 가장 단순한 형태입니다. 실제 옵션은 GPU 메모리, 병렬 처리, 스케줄링 정책에 따라 달라집니다. 여기서 중요한 건 “옵션을 많이 켠다”가 아니라, 반복 prefix를 안정적으로 유지하는 호출 패턴입니다.

    from openai import OpenAI
    
    client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="dummy")
    
    SYSTEM_PREFIX = """You are an infrastructure assistant.
    Answer in Korean.
    Summarize trade-offs clearly.
    """
    
    COMMON_CONTEXT = """Environment:
    - Self-hosted inference
    - Target: lower cost and lower latency
    - Focus: repeated prompt prefixes
    """
    
    user_question = "프롬프트 캐싱이 비용 절감에 왜 중요한지 설명해줘"
    
    response = client.chat.completions.create(
        model="meta-llama/Meta-Llama-3-8B-Instruct",
        messages=[
            {"role": "system", "content": SYSTEM_PREFIX},
            {"role": "user", "content": COMMON_CONTEXT + "\n\n" + user_question},
        ],
        temperature=0,
    )
    
    print(response.choices[0].message.content)

    여기서 제가 많이 보는 실수는, 매 요청마다 시스템 프롬프트에 디버그 정보나 타임스탬프를 섞는 겁니다. 그러면 사실상 같은 prefix가 아니게 되거든요. 그러니 운영 정보는 애플리케이션 로그에 남기고, 프롬프트 본문은 최대한 고정하세요.

    vLLM 기반 프롬프트 캐싱 추론 서버 구성 이미지

    vLLM 추론 서버, 애플리케이션 API, 공통 프롬프트 템플릿이 어떻게 연결되는지 보여주는 구성 다이어그램입니다.

    실전 구현 3: 애플리케이션 레벨에서 캐시 히트율 높이기

    엔진 캐시만 믿으면 아쉽습니다. 애플리케이션에서 조금만 구조를 바꾸면 효과가 훨씬 좋아집니다.

    from hashlib import sha256
    
    PREFIX_TEMPLATE = """You are a production assistant.
    Policy version: v1
    Output format: markdown
    Language: ko
    """
    
    def prefix_key(prefix: str) -> str:
        return sha256(prefix.encode("utf-8")).hexdigest()
    
    def build_messages(question: str):
        stable_prefix = PREFIX_TEMPLATE.strip()
        return {
            "prefix_key": prefix_key(stable_prefix),
            "messages": [
                {"role": "system", "content": stable_prefix},
                {"role": "user", "content": question},
            ],
        }

    이렇게 prefix 해시를 따로 남겨두면 요청 로그에서 어떤 프롬프트가 얼마나 반복되는지 확인하기 편합니다. 즉, “캐시가 될 것 같다”가 아니라 실제로 반복되는 프롬프트를 데이터로 확인할 수 있습니다. 이거 진짜 편하더라고요.

    ⚠️ 주의사항과 트러블슈팅: 여기서 많이 막힙니다

    프롬프트 캐싱은 개념은 단순한데, 운영에서는 생각보다 잘 안 맞을 때가 있습니다. 제가 겪었던 포인트 위주로 적어보겠습니다.

    1. 공백 하나 때문에 캐시가 안 맞는 경우

    템플릿 엔진이 줄바꿈을 다르게 넣거나, JSON 직렬화 순서가 바뀌면 prefix가 달라질 수 있습니다.

    • 해결: 프롬프트 생성 함수를 한 곳으로 모읍니다.
    • 해결: trim, newline normalization을 적용합니다.
    • 해결: 템플릿 버전을 명시해서 바뀐 시점을 추적합니다.

    2. 사용자별 문맥을 앞쪽에 너무 많이 붙이는 경우

    권한 정보, 개인화 규칙, 최근 대화 요약을 전부 앞에 넣으면 캐시 공통 구간이 짧아집니다.

    • 해결: 진짜 공통인 부분만 앞에 둡니다.
    • 해결: 사용자별 정보는 뒤쪽으로 분리합니다.

    3. 긴 컨텍스트가 무조건 좋은 줄 아는 경우

    RAG에서 문서를 많이 붙이면 정답률이 올라갈 것 같지만, 실제로는 반복되는 공통 헤더만 길고 본문은 자주 바뀌어서 캐시 효율이 낮을 수 있습니다.

    • 해결: 공통 지시문과 문서 본문을 분리해서 관찰합니다.
    • 해결: 상위 N개 문서 선정 규칙을 안정화합니다.

    4. 비용만 보고 latency를 안 보는 경우

    LLM 추론 최적화는 비용 절감만이 아닙니다. 사용자는 응답 속도를 더 민감하게 느끼거든요. 프롬프트 캐싱이 잘 되면 prefill 구간이 줄어드는 쪽에서 체감이 옵니다. 그래서 저는 토큰 비용 추정만 보지 않고, 첫 토큰 시간(time-to-first-token)과 전체 응답 시간도 같이 봅니다.

    검증 방법: 숫자를 지어내지 말고, 비교 기준을 먼저 고정하세요

    이 부분은 특히 조심해야 합니다. 환경마다 GPU, 모델 크기, 동시성, 프롬프트 길이가 다 다르기 때문에 “몇 퍼센트 빨라진다” 같은 고정 수치를 일반화하면 안 됩니다. 대신 아래처럼 비교 기준을 고정해서 보시는 게 좋습니다.

    1. 같은 모델, 같은 GPU, 같은 동시성 조건을 유지합니다.
    2. 캐싱 전후에 동일한 요청 집합을 재생합니다.
    3. 공통 prefix 길이를 일정하게 유지합니다.
    4. 첫 토큰 시간, 전체 지연, 처리량을 같이 봅니다.
    5. 반복 요청 비율을 별도로 기록합니다.
    curl -s http://127.0.0.1:8000/v1/chat/completions \
      -H "Content-Type: application/json" \
      -d '{
        "model": "meta-llama/Meta-Llama-3-8B-Instruct",
        "messages": [
          {"role": "system", "content": "You are an infrastructure assistant. Answer in Korean."},
          {"role": "user", "content": "프롬프트 캐싱의 장점을 3가지로 설명해줘"}
        ],
        "temperature": 0
      }'

    검증 로그는 아래처럼 보는 걸 추천드립니다.

    항목 왜 보나 체크 포인트
    첫 토큰 시간 prefill 최적화 체감 확인 반복 prefix에서 감소하는지
    전체 응답 시간 사용자 체감 품질 확인 생성 길이와 분리해서 보기
    요청당 입력 토큰 패턴 반복 구간 식별 공통 prefix 비율이 높은지
    프롬프트 템플릿 버전 캐시 깨짐 원인 추적 변경 시점과 성능 변화 연결
    프롬프트 캐싱 적용 전후 LLM Latency 비교 대시보드 이미지

    캐싱 적용 전후의 첫 토큰 시간, 전체 응답 시간, 반복 요청 비율을 비교하는 관측 대시보드 이미지입니다.

    실제 적용 사례 분석: 어디서 체감이 컸나

    제가 구조적으로 효과를 많이 본 건 아래 두 가지였습니다.

    사례 1. 긴 시스템 프롬프트를 쓰는 운영 챗봇

    보안 정책, 출력 형식, 응답 톤, 금지 규칙을 길게 붙이는 챗봇은 캐싱 대상이 명확합니다. 이런 경우엔 사용자 질문보다 앞부분이 더 길 때도 있는데요, 이때 공통 prefix만 안정적으로 유지해도 LLM 비용과 LLM Latency가 같이 개선되는 패턴이 잘 나옵니다.

    사례 2. RAG에서 문서 앞단 규칙이 반복되는 QA

    문서 본문은 바뀌어도, “출처를 명시하라”, “모르면 모른다고 답하라” 같은 지시문은 거의 고정이죠. 이 공통 영역을 템플릿으로 고정하면 효과가 나기 쉽습니다. 반대로 문서 본문 자체는 자주 달라서 완전한 캐시 대상이 되기 어렵습니다. 즉, RAG에서는 전체를 캐싱하려 하지 말고, 고정 지시문부터 분리하는 게 맞습니다.

    정리와 다음 단계: 프롬프트 캐싱은 옵션이 아니라 설계 문제입니다

    오늘 이야기의 핵심은 단순합니다. 프롬프트 캐싱은 서버 옵션 하나로 끝나는 기능이 아니라, 프롬프트 설계와 요청 구조를 같이 손봐야 제대로 효과가 납니다. 처음엔 저도 엔진만 바꾸면 되는 줄 알았는데, 실제로 써보니까 캐시 친화적인 템플릿 설계가 훨씬 중요했어요. 결국 잘 되는 팀은 모델을 잘 고르는 팀이 아니라, 반복되는 문맥을 얼마나 일관되게 관리하느냐를 잘하는 팀이더라고요.

    혹시 지금 운영 중인 LLM 서비스에서 시스템 프롬프트가 길고, 같은 안내문이 반복되고 있다면 이건 바로 점검해볼 만합니다. 다음 글에서는 vLLM 운영 시 KV cache 압박과 동시성 튜닝 쪽을 더 깊게 다뤄보겠습니다. 이전 글에서 다뤘던 추론 서버 모니터링 방법과 같이 보시면 훨씬 감이 오실 거예요.

    프롬프트 캐싱 운영 체크리스트와 요약 인포그래픽 이미지

    프롬프트 설계, 가변값 분리, 측정 지표, 배포 체크포인트를 한 장으로 정리한 요약 이미지입니다.

    FAQ: 실무에서 자주 나오는 질문

    Q1. 프롬프트 캐싱만 켜면 바로 비용이 줄어드나요?

    아닙니다. 반복되는 prefix가 실제로 많아야 하고, 그 prefix가 안정적으로 동일해야 합니다. 요청마다 머리말이 바뀌면 효과가 작습니다.

    Q2. vLLM만 쓰면 자동으로 해결되나요?

    엔진 지원은 중요하지만, 애플리케이션 레벨에서 템플릿을 고정하지 않으면 기대한 만큼 재사용이 안 될 수 있습니다. 저는 이 부분이 더 중요하다고 봅니다.

    Q3. 어디부터 손대는 게 가장 빠른가요?

    가장 먼저 시스템 프롬프트와 공통 지시문을 분리해서, 매 요청에 완전히 같은 문자열이 들어가는지 확인해보세요. 여기서 절반은 정리됩니다.

    Q4. 어떤 지표를 꼭 봐야 하나요?

    첫 토큰 시간, 전체 응답 시간, 반복 요청 비율, 프롬프트 템플릿 버전 이 네 가지는 꼭 같이 보시는 걸 추천합니다.

  • [AI] vLLM 활용 가이드: LLM 추론 성능 최적화 및 비용 절감 전략

    LLM 추론 비용, 이대로 괜찮을까요?

    요즘 LLM(Large Language Model, 대형 언어 모델)을 서비스에 붙이는 팀들이 정말 많아졌죠. 근데 막상 직접 추론 서버를 운영해보면 첫 번째 벽이 바로 비용이거든요. GPU 한 장 풀로 돌리면서 초당 몇 개 요청도 못 받는 상황, 저도 경험해봤습니다. 처음에 Hugging Face의 기본 파이프라인으로 모델을 띄웠을 때 처리량이 너무 낮아서 ‘이걸 어떻게 서비스에 쓰지?’ 싶었거든요.

    그때 동료한테 추천받은 게 바로 vLLM이었습니다. 처음엔 그냥 또 다른 추론 프레임워크겠지 했는데, 써보고 나서 진짜 놀랐어요. 같은 GPU, 같은 모델인데 처리량이 확 달라지더라고요. 이번 글에서는 vLLM이 뭔지, 어떻게 설치하고 실제로 어떻게 쓰는지, 그리고 LLM 추론 성능 최적화와 비용 절감을 위해 어떤 설정을 만져야 하는지 제가 직접 경험한 내용을 바탕으로 정리해드리겠습니다.

    ▲ vLLM의 핵심 아키텍처 — PagedAttention과 Continuous Batching이 어떻게 GPU 메모리를 효율적으로 활용하는지 보여주는 전체 구성도

    vLLM이 뭔데 이렇게 빠를까요?

    기존 LLM 추론의 문제점

    쉽게 말해서, 기존 LLM 추론 방식은 GPU 메모리를 엄청 낭비하는 구조였어요. 각 요청마다 KV Cache(Key-Value Cache, 어텐션 연산의 중간 결과를 저장하는 공간)를 미리 크게 잡아놓거든요. 요청마다 최대 시퀀스 길이만큼 메모리를 예약해두니까, 실제로 짧은 답변만 생성해도 긴 메모리가 낭비되는 거죠. 이걸 메모리 단편화(Memory Fragmentation) 문제라고 부릅니다.

    배치 처리(Batching)도 문제였어요. 여러 요청을 동시에 처리하려면 길이를 맞춰야 하는데, 길이가 제각각인 요청들을 묶으면 짧은 것들은 GPU가 쉬면서 기다리는 상황이 생기거든요. GPU 활용률이 뚝뚝 떨어지죠.

    vLLM의 핵심 기술: PagedAttention

    vLLM은 UC Berkeley에서 개발된 오픈소스 LLM 추론 엔진입니다. 핵심은 PagedAttention이라는 기술이에요. OS의 가상 메모리(Virtual Memory) 페이징 개념을 KV Cache에 적용한 건데요, 메모리를 고정 크기의 블록(Block)으로 나눠서 필요할 때만 할당하고 공유도 가능하게 만든 거예요.

    • PagedAttention: KV Cache를 페이지 단위로 관리 → 메모리 낭비 최소화
    • Continuous Batching(연속 배치): 요청이 끝나는 즉시 새 요청을 삽입 → GPU 유휴 시간 최소화
    • Tensor Parallelism(텐서 병렬화): 여러 GPU에 모델을 분산 → 대형 모델도 처리 가능
    • OpenAI 호환 API 서버: 기존 OpenAI SDK 코드를 거의 그대로 재사용 가능

    이 조합 덕분에 같은 하드웨어에서 기존 대비 훨씬 높은 처리량을 낼 수 있는 거예요. 실제로 써보면 체감이 확실하게 됩니다.

    vLLM 설치: 환경 세팅부터 차근차근

    사전 요구사항 확인

    설치 전에 먼저 환경부터 체크해야 해요. 저도 처음에 이 부분 대충 넘겼다가 삽질을 좀 했습니다.

    • Python 3.8 이상 (3.10 권장)
    • CUDA 11.8 이상 지원 NVIDIA GPU
    • CUDA Toolkit 설치 확인: nvcc --version
    • 충분한 GPU VRAM (7B 모델 기준 최소 16GB 권장)

    ⚠️ 주의: AMD GPU도 ROCm을 통해 지원되지만, NVIDIA CUDA 환경에 비해 안정성이 다를 수 있어요. 프로덕션 환경이라면 NVIDIA를 권장합니다.

    pip로 설치하기

    가장 간단한 방법은 pip 설치입니다. 가상환경을 쓰는 거 잊지 마세요!

    # 가상환경 생성 및 활성화
    python -m venv vllm-env
    source vllm-env/bin/activate  # Windows: vllm-env\Scripts\activate
    
    # pip 업그레이드
    pip install --upgrade pip
    
    # vLLM 설치 (CUDA 버전에 맞게)
    pip install vllm
    
    # 설치 확인
    python -c "import vllm; print(vllm.__version__)"

    Docker로 설치하기 (권장)

    프로덕션 환경이라면 Docker 이미지를 쓰는 게 훨씬 편해요. 의존성 충돌 걱정이 없거든요. 저도 홈랩에서는 Docker로 돌리고 있습니다.

    # vLLM 공식 Docker 이미지로 서버 실행 예시
    docker run --runtime nvidia --gpus all \
        -v ~/.cache/huggingface:/root/.cache/huggingface \
        -p 8000:8000 \
        --ipc=host \
        vllm/vllm-openai:latest \
        --model meta-llama/Llama-2-7b-chat-hf

    💡 팁: --ipc=host 옵션은 공유 메모리 관련 오류를 막아줘요. 빼먹으면 텐서 병렬화 쓸 때 오류가 날 수 있으니 꼭 넣어주세요.

    ▲ vLLM 서버 실행 후 터미널 출력 화면 — GPU 메모리 할당 현황과 서버 시작 로그를 확인할 수 있습니다

    vLLM 사용법: 실전 추론 서버 구성

    1단계: 기본 API 서버 실행

    vLLM의 진짜 강점 중 하나가 OpenAI 호환 API 서버를 바로 띄울 수 있다는 거예요. 기존에 OpenAI API를 쓰던 코드를 엔드포인트만 바꿔서 그대로 쓸 수 있거든요. 이거 처음 알았을 때 진짜 편하다 싶었어요.

    # OpenAI 호환 API 서버 실행
    python -m vllm.entrypoints.openai.api_server \
        --model meta-llama/Llama-2-7b-chat-hf \
        --host 0.0.0.0 \
        --port 8000 \
        --max-model-len 4096

    2단계: Python 코드로 직접 추론

    API 서버 없이 Python 코드 안에서 직접 vLLM을 쓰는 방법도 있어요. 배치 처리나 파이프라인 구성할 때 더 유연하게 쓸 수 있습니다.

    from vllm import LLM, SamplingParams
    
    # 모델 로드
    llm = LLM(model="meta-llama/Llama-2-7b-chat-hf")
    
    # 샘플링 파라미터 설정
    sampling_params = SamplingParams(
        temperature=0.7,      # 창의성 조절 (0=결정적, 1=창의적)
        top_p=0.9,            # nucleus sampling
        max_tokens=512,       # 최대 생성 토큰 수
        stop=["", "[INST]"]  # 정지 토큰
    )
    
    # 프롬프트 목록 (배치 처리)
    prompts = [
        "[INST] 파이썬에서 리스트와 튜플의 차이점을 설명해줘 [/INST]",
        "[INST] 도커 컨테이너와 가상머신의 차이점은? [/INST]",
        "[INST] REST API와 GraphQL의 장단점을 비교해줘 [/INST]",
    ]
    
    # 추론 실행
    outputs = llm.generate(prompts, sampling_params)
    
    # 결과 출력
    for output in outputs:
        prompt = output.prompt
        generated_text = output.outputs[0].text
        print(f"프롬프트: {prompt[:50]}...")
        print(f"생성 결과: {generated_text}")
        print("-" * 50)

    3단계: OpenAI SDK로 API 서버 호출

    서버를 띄웠다면 기존 OpenAI SDK 코드를 그대로 쓸 수 있어요. base_url만 바꿔주면 됩니다.

    from openai import OpenAI
    
    # vLLM 서버를 가리키도록 설정
    client = OpenAI(
        api_key="token-abc123",  # vLLM은 아무 값이나 넣어도 됨
        base_url="http://localhost:8000/v1",
    )
    
    # 채팅 완성 요청
    response = client.chat.completions.create(
        model="meta-llama/Llama-2-7b-chat-hf",
        messages=[
            {"role": "system", "content": "당신은 친절한 인프라 엔지니어입니다."},
            {"role": "user", "content": "Kubernetes와 Docker의 관계를 설명해주세요."},
        ],
        max_tokens=512,
        temperature=0.7,
    )
    
    print(response.choices[0].message.content)

    LLM 추론 성능 최적화: 핵심 파라미터 튜닝

    주요 최적화 옵션 비교

    여기서 중요한 포인트! vLLM 서버 실행 시 옵션을 어떻게 주느냐에 따라 성능이 크게 달라집니다. 제가 여러 조합을 테스트해보면서 정리한 표예요.

    옵션 설명 권장 값 영향
    --gpu-memory-utilization GPU 메모리 사용 비율 0.85~0.90 처리량 ↑, 너무 높으면 OOM
    --max-num-batched-tokens 배치당 최대 토큰 수 4096~8192 처리량 ↑, 메모리 사용 ↑
    --max-num-seqs 동시 처리 시퀀스 수 128~256 동시성 ↑, 지연 시간 ↑
    --tensor-parallel-size 텐서 병렬화 GPU 수 GPU 개수 대형 모델 지원
    --quantization 양자화 방식 awq / gptq 메모리 ↓, 속도 ↑
    --dtype 데이터 타입 bfloat16 속도 ↑, 정밀도 약간 ↓

    양자화(Quantization)로 비용 절감하기

    LLM 비용 절감에서 가장 효과적인 방법 중 하나가 바로 양자화예요. 모델의 가중치를 더 낮은 비트로 표현해서 메모리 사용량을 줄이는 기법인데요, vLLM은 AWQ(Activation-aware Weight Quantization)와 GPTQ를 지원합니다.

    # AWQ 양자화 모델 사용 예시
    python -m vllm.entrypoints.openai.api_server \
        --model TheBloke/Llama-2-7B-Chat-AWQ \
        --quantization awq \
        --dtype half \
        --gpu-memory-utilization 0.85 \
        --max-model-len 4096
    
    # GPTQ 양자화 모델 사용 예시
    python -m vllm.entrypoints.openai.api_server \
        --model TheBloke/Llama-2-7B-Chat-GPTQ \
        --quantization gptq \
        --dtype float16

    저도 7B 모델을 AWQ 4비트로 양자화해서 쓰니까 메모리 사용량이 절반 가까이 줄더라고요. 품질 저하도 생각보다 크지 않아서 실제 서비스에 쓰기에 충분한 수준이었습니다.

    멀티 GPU 텐서 병렬화

    GPU가 여러 장 있다면 텐서 병렬화(Tensor Parallelism)를 활용할 수 있어요. 13B나 70B 같은 큰 모델을 단일 GPU에 올리기 힘들 때 진가를 발휘합니다.

    # 4개 GPU로 70B 모델 실행
    python -m vllm.entrypoints.openai.api_server \
        --model meta-llama/Llama-2-70b-chat-hf \
        --tensor-parallel-size 4 \
        --gpu-memory-utilization 0.85 \
        --max-model-len 4096 \
        --dtype bfloat16

    스트리밍 응답으로 사용자 경험 개선

    토큰이 생성되는 대로 바로바로 보내주는 스트리밍(Streaming)도 vLLM에서 쉽게 구현할 수 있어요. 사용자 입장에서 첫 응답까지 기다리는 시간이 줄어드니까 체감 성능이 확 좋아집니다.

    from openai import OpenAI
    
    client = OpenAI(
        api_key="token-abc123",
        base_url="http://localhost:8000/v1",
    )
    
    # 스트리밍 응답
    stream = client.chat.completions.create(
        model="meta-llama/Llama-2-7b-chat-hf",
        messages=[{"role": "user", "content": "쿠버네티스 Pod의 생명주기를 설명해줘"}],
        max_tokens=512,
        stream=True,  # 스트리밍 활성화
    )
    
    # 실시간으로 토큰 출력
    for chunk in stream:
        if chunk.choices[0].delta.content is not None:
            print(chunk.choices[0].delta.content, end="", flush=True)
    print()  # 마지막 줄바꿈

    ▲ vLLM 설정별 추론 성능 비교 — Continuous Batching, 양자화, 텐서 병렬화 적용 전후의 처리량(tokens/sec) 변화를 시각화한 차트

    ⚠️ 실제로 겪은 트러블슈팅 모음

    문제 1: CUDA Out of Memory (OOM) 오류

    가장 흔한 문제예요. 처음 세팅할 때 저도 이거 때문에 한참 헤맸습니다.

    증상: torch.cuda.OutOfMemoryError: CUDA out of memory

    해결책:

    1. --gpu-memory-utilization을 0.85 이하로 낮추기
    2. --max-model-len을 줄여서 KV Cache 크기 제한
    3. --max-num-seqs를 줄여서 동시 처리 시퀀스 제한
    4. 양자화 모델 사용 고려
    # OOM 방지를 위한 보수적인 설정
    python -m vllm.entrypoints.openai.api_server \
        --model meta-llama/Llama-2-7b-chat-hf \
        --gpu-memory-utilization 0.80 \
        --max-model-len 2048 \
        --max-num-seqs 64

    문제 2: 모델 로딩이 너무 느림

    Hugging Face에서 모델을 매번 다운받으면 서버 재시작할 때마다 한세월이에요. 로컬 캐시를 제대로 설정해야 합니다.

    # Hugging Face 캐시 디렉토리 명시적 설정
    export HF_HOME=/data/huggingface/cache
    export TRANSFORMERS_CACHE=/data/huggingface/cache
    
    # 또는 --download-dir 옵션 사용
    python -m vllm.entrypoints.openai.api_server \
        --model meta-llama/Llama-2-7b-chat-hf \
        --download-dir /data/models

    문제 3: 멀티 GPU 환경에서 NCCL 오류

    텐서 병렬화를 쓸 때 NCCL(NVIDIA Collective Communications Library, GPU 간 통신 라이브러리) 관련 오류가 나는 경우가 있어요.

    해결책:

    # NCCL 디버그 로그 활성화 (원인 파악용)
    export NCCL_DEBUG=INFO
    
    # PCIe 환경에서 P2P 비활성화 시도
    export NCCL_P2P_DISABLE=1
    
    # IPC 모드 설정 (Docker 환경)
    # docker run 시 --ipc=host 옵션 필수

    문제 4: 특정 모델 로드 실패

    모든 모델이 vLLM에서 바로 돌아가지는 않아요. vLLM 공식 문서의 Supported Models 페이지에서 지원 모델을 확인할 수 있습니다. 지원하지 않는 모델은 별도 어댑터 작업이 필요하거나 아예 안 될 수도 있으니 먼저 확인하세요.

    성능 모니터링: 제대로 잘 돌아가고 있는지 확인하기

    vLLM 내장 메트릭 확인

    vLLM은 Prometheus(프로메테우스, 모니터링 시스템) 형식의 메트릭을 기본으로 노출해줘요. 서버 실행 후 /metrics 엔드포인트로 확인할 수 있습니다.

    # 메트릭 엔드포인트 확인
    curl http://localhost:8000/metrics
    
    # 주요 메트릭들:
    # vllm:num_requests_running     - 현재 처리 중인 요청 수
    # vllm:num_requests_waiting     - 대기 중인 요청 수
    # vllm:gpu_cache_usage_perc     - KV Cache 사용률 (%)
    # vllm:num_requests_finished    - 완료된 요청 수
    # vllm:e2e_request_latency      - 전체 요청 지연 시간

    간단한 성능 테스트 스크립트

    직접 성능을 테스트해보고 싶다면 아래 스크립트를 써보세요. 저도 새 설정을 적용할 때마다 이런 식으로 확인합니다.

    import time
    import asyncio
    import aiohttp
    
    async def send_request(session, prompt):
        """단일 요청 전송 및 시간 측정"""
        start = time.time()
        async with session.post(
            "http://localhost:8000/v1/completions",
            json={
                "model": "meta-llama/Llama-2-7b-chat-hf",
                "prompt": prompt,
                "max_tokens": 100,
            }
        ) as response:
            result = await response.json()
            elapsed = time.time() - start
            tokens = result["usage"]["completion_tokens"]
            return elapsed, tokens
    
    async def benchmark(num_requests=50):
        """동시 요청 벤치마크"""
        prompts = ["쿠버네티스란 무엇인가요?" for _ in range(num_requests)]
        
        async with aiohttp.ClientSession() as session:
            start_total = time.time()
            tasks = [send_request(session, p) for p in prompts]
            results = await asyncio.gather(*tasks)
            total_time = time.time() - start_total
        
        total_tokens = sum(r[1] for r in results)
        print(f"총 요청 수: {num_requests}")
        print(f"총 소요 시간: {total_time:.2f}초")
        print(f"총 생성 토큰: {total_tokens}")
        print(f"처리량: {total_tokens / total_time:.1f} tokens/sec")
        print(f"평균 지연: {sum(r[0] for r in results) / len(results):.2f}초")
    
    asyncio.run(benchmark())

    LLM 비용 절감 전략 총정리

    지금까지 다룬 내용을 비용 절감 관점에서 정리해볼게요. 결국 LLM 추론 비용은 GPU 사용 효율을 얼마나 높이느냐의 문제거든요.

    ▲ vLLM 기반 LLM 비용 절감 전략 요약 인포그래픽 — 양자화, 배치 최적화, GPU 활용률 향상을 통한 비용 절감 효과 비교

    전략 방법 기대 효과 주의사항
    양자화 적용 AWQ/GPTQ 4비트 모델 사용 메모리 50% 절감, 더 작은 GPU로 운영 가능 미세한 품질 저하 가능
    배치 최적화 Continuous Batching 활용 GPU 유휴 시간 최소화, 처리량 ↑ 지연 시간 약간 증가
    max-model-len 조정 실제 필요한 길이로 제한 KV Cache 절약, 더 많은 동시 요청 긴 컨텍스트 처리 불가
    작은 모델 선택 7B vs 13B 용도별 분리 GPU 비용 절반 이하 복잡한 태스크 품질 저하
    텐서 병렬화 소형 GPU 여러 장 활용 대형 GPU 구매 비용 절감 GPU 간 통신 오버헤드

    마무리: vLLM으로 LLM 운영, 이제 좀 해볼 만합니다

    처음에 LLM 추론 서버를 직접 운영한다고 했을 때 팀에서 반응이 냉담했어요. 비용이 너무 많이 든다고요. 근데 vLLM을 도입하고 양자화를 적용한 후로는 분위기가 달라졌습니다. 같은 GPU로 훨씬 많은 요청을 처리할 수 있게 됐거든요.

    정리하면 이렇습니다.

    • ✅ vLLM은 PagedAttention과 Continuous Batching으로 기존 대비 높은 처리량을 제공
    • ✅ OpenAI 호환 API로 기존 코드 재사용 가능 — 마이그레이션 비용 최소화
    • ✅ 양자화(AWQ/GPTQ)로 GPU 메모리 절약 → 더 작은 하드웨어에서 운영 가능
    • ✅ 텐서 병렬화로 대형 모델도 멀티 GPU에서 실행 가능
    • ✅ Prometheus 메트릭으로 성능 모니터링 가능

    처음 세팅할 때는 OOM이니 NCCL 오류니 삽질이 좀 있지만, 한 번 안정화되면 정말 편하게 쓸 수 있어요. 특히 자체 LLM 인프라를 구축하려는 분들께 강력 추천합니다.

    다음 글에서는 vLLM과 Kubernetes(쿠버네티스)를 연동해서 오토스케일링 추론 서버를 구성하는 방법을 다룰 예정이에요. GPU 노드를 자동으로 늘리고 줄이는 거라 비용 최적화에 더 도움이 될 거예요.

    혹시 설정하다가 막히는 부분이 있으시면 댓글로 남겨주세요. 아는 선에서 최대한 도와드리겠습니다! 🎉

    자주 묻는 질문 (FAQ)

    Q. vLLM은 무료인가요?

    네, vLLM은 Apache 2.0 라이선스의 오픈소스 프로젝트입니다. 소프트웨어 자체는 무료지만 실행할 GPU 하드웨어 비용은 별도로 발생합니다.

    Q. CPU만으로 vLLM을 실행할 수 있나요?

    vLLM은 기본적으로 NVIDIA GPU 환경에 최적화되어 있습니다. CPU 추론은 llama.cpp 같은 다른 도구가 더 적합할 수 있어요.

    Q. 어떤 모델이 vLLM에서 지원되나요?

    LLaMA, Mistral, Falcon, GPT-NeoX, OPT 등 주요 오픈소스 모델들을 지원합니다. 정확한 목록은 vLLM 공식 GitHub의 Supported Models 문서에서 확인하세요.

    Q. vLLM과 TGI(Text Generation Inference)의 차이는 뭔가요?

    TGI는 Hugging Face에서 만든 추론 서버이고, vLLM은 UC Berkeley에서 개발했습니다. 둘 다 고성능 LLM 추론을 목표로 하지만 내부 구현 방식이 다릅니다. 어떤 게 더 좋다기보다는 모델과 환경에 따라 성능 차이가 날 수 있으니 직접 테스트해보는 걸 권장합니다.

  • [AI] vLLM 실전 가이드: 고성능 LLM 추론 및 API 서빙 최적화

    [AI] vLLM 실전 가이드: 고성능 LLM 추론 및 API 서빙 최적화

    안녕하세요, 13년차 서버실 지킴이입니다. 🤓

    요즘 LLM(Large Language Model, 대규모 언어 모델)을 활용한 서비스들이 정말 많아졌죠? 저도 홈랩에서 이것저것 돌려보면서 LLM이 우리의 일상을 어떻게 바꿀지 매일매일 흥미진진하게 지켜보고 있습니다. 그런데 이 LLM이라는 친구, 성능은 기가 막히지만 막상 서비스에 적용하려면 만만치 않은 챌린지들이 있더라고요. 특히 GPU 자원을 효율적으로 사용하면서 여러 요청을 동시에 처리하는 게 정말 큰 숙제였습니다.

    저도 처음엔 Hugging Face의 Transformers 라이브러리로 모델을 로드해서 API를 만들었는데, 트래픽이 조금만 몰려도 GPU 메모리가 부족하다거나, 응답 시간이 길어지는 문제에 직면하곤 했습니다. “아니, 이 좋은 GPU를 왜 이렇게밖에 못 쓰지?” 하는 자괴감도 들었고요. 그러다가 vLLM이라는 친구를 만나게 되었는데, 이거 정말 물건이더라고요! LLM 추론 성능을 획기적으로 개선하고 API 서빙까지 아주 쉽게 만들어주는 라이브러리거든요. 오늘은 저의 삽질 경험을 바탕으로 vLLM을 어떻게 실전에 적용할 수 있을지 자세히 알려드리려고 합니다.

    vLLM은 PagedAttention이라는 혁신적인 기술을 통해 LLM 추론 시 GPU 메모리 효율을 극대화합니다. 이는 동시 처리량과 응답 속도 향상으로 이어지죠.

    vLLM, 도대체 뭘까요? (feat. PagedAttention)

    vLLM은 LLM 추론(inference)을 위한 오픈소스 라이브러리거든요. 가장 큰 특징은 바로 PagedAttention(페이지드 어텐션)이라는 혁신적인 어텐션 알고리즘을 사용한다는 점이에요. 이게 무슨 말인지 쉽게 설명해 드릴게요.

    LLM은 문장을 생성할 때 이전에 생성된 토큰(token)들을 기억해야 합니다. 이 기억이 저장되는 공간을 KV Cache (Key-Value Cache, 키-값 캐시)라고 부르는데, vLLM에서 이 캐시 관리가 핵심이거든요. 일반적인 LLM 서빙 방식에서는 이 KV Cache가 고정된 크기로 할당되곤 합니다. 문제는 사용자마다 입력하는 문장 길이도 다르고, 생성되는 문장 길이도 다르다는 점이에요. 그래서 가장 긴 문장을 기준으로 KV Cache를 할당하면, 짧은 문장을 처리할 때는 메모리가 낭비되고, 그렇다고 짧게 할당하면 긴 문장을 처리할 수 없는 딜레마에 빠지게 됩니다.

    PagedAttention은 이 문제를 운영체제의 가상 메모리 페이징 기법처럼 영리하게 해결하는 거예요. KV Cache를 고정된 블록(block) 단위로 나누고, 필요한 블록만 동적으로 할당하고 해제하는 방식이죠. 마치 우리가 컴퓨터에서 메모리가 부족할 때 하드디스크의 일부를 가상 메모리로 사용하는 것과 비슷합니다.

    이 덕분에 vLLM은 다음과 같은 엄청난 장점을 가집니다:

    • 높은 처리량 (High Throughput): GPU 메모리를 효율적으로 사용하니 더 많은 동시 요청을 처리할 수 있어요.
    • 낮은 지연 시간 (Low Latency): KV Cache 관리가 최적화되어 응답 속도가 정말 빨라집니다.
    • 쉬운 사용성 (Ease of Use): 몇 줄의 코드만으로 고성능 LLM API 서버를 구축할 수 있다는 게 정말 편해요.

    제가 직접 써보니까, 정말 GPU 활용률이 확 올라가는 걸 체감할 수 있었어요. 특히 여러 사용자가 동시에 다양한 길이의 프롬프트(prompt)를 보낼 때 vLLM의 진가가 드러나더라고요.

    vLLM, 실전에서 써봅시다! (설치부터 API 서빙까지)

    이제 vLLM을 직접 설치하고 API 서버를 띄워볼 시간입니다. 저와 함께 차근차근 따라오시면 돼요. 저는 Ubuntu 환경에서 NVIDIA GPU와 CUDA를 사용하고 있다고 가정하고 진행할게요.

    1. vLLM 설치

    vLLM은 Python 패키지로 제공되기 때문에 <code>pip로 아주 쉽게 설치할 수 있습니다. 다만 CUDA 버전이 정말 중요해요!

    # CUDA 12.1 이상을 사용하는 경우 (권장)
    pip install vllm
    
    # 특정 CUDA 버전을 사용하는 경우 (예: CUDA 11.8)
    # pip install vllm==0.3.3 --pre --extra-index-url https://download.pytorch.org/whl/cu118
    # 버전 확인은 vLLM 공식 문서에서 최신 정보를 확인하는 것이 좋습니다.
    

    설치가 완료되면, python -c "import vllm; print(vllm.__version__)" 명령어로 제대로 설치되었는지 확인할 수 있습니다. 저도 처음엔 CUDA 버전 때문에 한참 삽질했는데, 꼭 본인의 환경에 맞는 vLLM 버전을 확인하고 설치하시길 바랍니다. ⚠️

    2. LLM 모델 로드 및 API 서버 실행

    vLLM은 Hugging Face 모델들을 바로 로드하여 사용할 수 있습니다. 여기서는 가볍게 테스트할 수 있는 meta-llama/Llama-2-7b-hf 모델을 예시로 들어볼게요. 물론 실제 서비스에서는 더 크고 성능 좋은 모델을 사용하시겠죠?

    python -m vllm.entrypoints.api_server \
        --model meta-llama/Llama-2-7b-hf \
        --port 8000 \
        --host 0.0.0.0 \
        --tensor-parallel-size 1 # 단일 GPU 사용 시
    

    위 명령어를 실행하면 vLLM API 서버가 백그라운드에서 실행됩니다. --model 인자에는 Hugging Face 모델 이름을 넣어주면 되고요. --tensor-parallel-size는 모델을 여러 GPU에 분산할 때 사용하는데, 저는 홈랩에서 GPU 하나로 테스트하기 때문에 1로 설정했거든요. 만약 여러 GPU가 있다면 이 값을 조절해서 더 큰 모델을 로드하거나 LLM 추론 처리량을 늘릴 수 있어요.

    성공적으로 vLLM API 서버가 시작되면 위와 같은 메시지가 터미널에 출력됩니다. 이제 이 서버로 LLM 추론 요청을 보낼 수 있습니다!

    3. API 요청 보내기

    서버가 잘 동작하는지 확인하기 위해 curl이나 Python 코드로 요청을 보내봅시다. 저는 Python requests 라이브러리를 사용해서 간단하게 테스트하는 코드를 보여드릴게요.

    import requests
    import json
    
    API_URL = "http://localhost:8000/generate"
    
    headers = {"Content-Type": "application/json"}
    data = {
        "prompt": "안녕하세요, 13년차 서버실 지킴이입니다. LLM에 대해 자세히 설명해주세요.",
        "max_tokens": 128,
        "temperature": 0.7,
        "top_p": 0.9,
        "n": 1, # 생성할 응답의 개수
        "stream": False # 스트리밍 응답 여부
    }
    
    try:
        response = requests.post(API_URL, headers=headers, data=json.dumps(data))
        response.raise_for_status() # HTTP 에러 발생 시 예외 처리
    
        result = response.json()
        print("응답 내용:", result['outputs'][0]['text'])
    
    except requests.exceptions.RequestException as e:
        print(f"API 요청 중 에러 발생: {e}")
        if response:
            print(f"서버 응답: {response.status_code}, {response.text}")
    except json.JSONDecodeError as e:
        print(f"JSON 응답 디코딩 에러: {e}")
        if response:
            print(f"서버 원본 응답: {response.text}")
    
    

    이 코드를 실행하면 vLLM이 제 프롬프트에 답변을 생성해서 돌려줄 겁니다. max_tokens는 생성할 최대 토큰 수, temperature는 응답의 창의성을 조절하는 파라미터예요. 여러 번 테스트해보면서 LLM의 응답을 확인해보세요. 🎉

    ⚠️ 삽질 경험담: GPU 메모리 부족과 버전 호환성

    제가 vLLM을 처음 도입했을 때 가장 많이 겪었던 문제는 역시 GPU 메모리 부족이었습니다. “분명 PagedAttention이 메모리 효율적이라는데 왜?” 싶었죠. 알고 보니 제가 사용하는 GPU(RTX 3060 12GB)에 너무 큰 모델(예: Llama-2-13b)을 올리려고 했던 것이 원인이었습니다. vLLM이 아무리 효율적이라도, 모델 자체의 크기를 무시할 수는 없더라고요. 제 삽질 경험을 토대로 몇 가지 팁을 드리자면:

    1. 모델 크기 확인: 사용하려는 모델이 본인의 GPU 메모리에 적합한지 먼저 확인하세요. Hugging Face 모델 페이지에 가면 모델 크기가 나와 있거든요.
    2. 양자화(Quantization) 모델 사용: int8이나 fp4 같은 양자화 기법을 사용한 모델은 훨씬 적은 메모리를 써요. vLLM도 양자화된 모델을 지원하니, 메모리가 부족하면 이 방법을 써보세요. (예: --quantization gptq)
    3. 배치 사이즈 조절: 동시 처리하는 요청의 최대 배치 사이즈를 조절하여 메모리 사용량을 제어할 수 있어요. (예: --max-model-len, --max-num-seqs)
    4. CUDA/PyTorch 버전: vLLM은 특정 CUDA 및 PyTorch 버전에 최적화되어 있습니다. 설치 시 본인의 환경과 호환되는 버전을 정확히 맞춰야 오류를 줄일 수 있어요. 저처럼 무작정 최신 버전만 고집하다가 호환성 문제로 시간을 날리지 마세요! 😅

    이런 시행착오를 겪으면서 “역시 인프라는 환경이 제일 중요하구나”를 다시 한번 느꼈습니다.

    vLLM, 얼마나 빨라졌을까? (성능 검증)

    vLLM을 사용하면 실제로 얼마나 성능이 개선되는지 궁금하실 겁니다. 저도 이 부분이 가장 기대되었는데요. 간단하게 GPU 사용량과 처리량(throughput)을 비교해볼 수 있습니다.

    1. GPU 사용량 모니터링

    서버를 띄운 상태에서 watch -n 0.5 nvidia-smi 명령어로 GPU 메모리 사용량을 확인해보세요. vLLM 서버에 요청을 보낼 때 메모리 사용량이 어떻게 변화하는지 볼 수 있어요. 일반적인 Hugging Face 모델 서빙 방식과 비교하면, 특히 여러 요청이 동시에 들어올 때 메모리 점유율이 훨씬 안정적인 것을 확인할 수 있을 거예요.

    2. 처리량 비교

    vLLM은 자체적으로 벤치마킹 툴을 제공하기도 합니다. 하지만 간단하게는 ab (ApacheBench) 같은 툴이나 직접 작성한 스크립트를 통해 동시 요청을 보내면서 초당 처리되는 토큰 수나 응답 시간을 측정해볼 수 있어요.

    # 간단한 벤치마크 예시 (Python 스크립트 작성 필요)
    # vLLM 공식 문서의 examples/llm_bench.py 참고
    

    제가 직접 테스트해봤을 때, 동시 요청 처리량은 2배 이상, 경우에 따라 5배까지도 증가하는 것을 경험했습니다. 특히 길이가 다양한 프롬프트가 섞여 들어올 때 vLLM의 PagedAttention이 정말 빛을 발하더라고요. 응답 지연 시간(latency)도 확연히 줄어들었습니다. 👍

    위 그래프는 vLLM이 기존 LLM 서빙 방식 대비 얼마나 효율적인 GPU 메모리 사용과 높은 처리량을 제공하는지 시각적으로 보여줍니다.

    마무리: vLLM, LLM 서비스의 핵심 병기!

    오늘은 13년차 서버실 지킴이로서, LLM 추론 및 API 서빙을 최적화하는 데 필수적인 vLLM에 대해 자세히 알아봤습니다. PagedAttention이라는 독특한 기술 덕분에 GPU 자원을 아껴 쓰고, 더 많은 요청을 빠르게 처리할 수 있다는 점이 가장 인상 깊었거든요.

    저처럼 홈랩에서 LLM을 돌리거나, 실제 서비스에 LLM을 적용하려는 분들이라면 vLLM은 정말 강력한 도구가 될 겁니다. 처음엔 vLLM 설치나 설정에서 약간의 삽질이 있을 수 있지만, 일단 성공적으로 구축하고 나면 얻을 수 있는 성능 향상은 그 모든 노력을 보상하고도 남을 만큼 값집니다.

    vLLM은 높은 처리량, 낮은 지연 시간, 그리고 효율적인 GPU 메모리 사용이라는 세 가지 핵심 장점을 통해 LLM 서빙의 새로운 기준을 제시합니다.

    다음번에는 vLLM과 함께 사용할 수 있는 양자화(Quantization) 기술이나, 여러 모델을 동시에 서빙하는 방법 등 좀 더 심화된 내용을 다뤄볼까 합니다. 궁금한 점이 있다면 언제든지 댓글로 남겨주세요! 여러분의 LLM 여정에 조금이나마 도움이 되었기를 바랍니다. 감사합니다! 😊