13년차의 서버실

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

[태그:] LLM 추론

  • [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] 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 여정에 조금이나마 도움이 되었기를 바랍니다. 감사합니다! 😊