[active] vLLM 부하 테스트: GPU 용량 한계 실전 가이드

발산동휘발류 Lv.1
02-24 17:02 · 조회 9 · 추천 0

vLLM 부하 테스트: GPU 용량 한계 실전 가이드

💡 왜 부하 테스트가 필수인가

개발 환경에서는 잘 작동하던 LLM 서비스가 프로덕션에서 갑자기:

[ERROR] CUDA out of memory. Tried to allocate 2.34 GiB
[WARNING] KV cache exhausted, dropping requests
[ERROR] Request timeout after 30s (queue full)

부하 테스트 없이 배포하면:

  • GPU 메모리 부족 (OOM)
  • 요청 큐 포화 (타임아웃)
  • 레이턴시 폭증 (사용자 이탈)
  • 예상치 못한 다운타임

이 글의 목표:
vLLM 부하 테스트 방법, GPU별 성능 한계, 실전 튜닝 전략을 실제 벤치마크 데이터와 함께 제시한다.


📊 vLLM 기본 개념

PagedAttention과 KV 캐시

일반 LLM 추론:

입력: "안녕하세요"
출력: "안녕" → "안녕하세요" → "안녕하세요,"

각 토큰 생성 시 전체 컨텍스트를 다시 계산 → 비효율적

vLLM의 PagedAttention:

  • 이전 토큰의 KV 캐시 (Key-Value pairs) 저장
  • 새 토큰만 계산 → 속도 향상
  • OS 가상 메모리처럼 페이지 단위 관리

KV 캐시 크기 계산:

KV cache size = batch_size × seq_length × num_layers × hidden_size × 2 (K+V)

예시: Llama 3.1 8B
- num_layers: 32
- hidden_size: 4096
- seq_length: 8192
- batch_size: 10
- dtype: float16 (2 bytes)

KV = 10 × 8192 × 32 × 4096 × 2 × 2 bytes
  = 42.9 GB (!)

문제:
KV 캐시가 GPU 메모리 대부분 차지 → 동시 처리 제한


🔍 GPU별 성능 비교

단일 사용자 성능

GPU VRAM Llama 3.1 8B (FP16) Llama 3.1 8B (4bit) 토큰/초
RTX 4090 24GB ✅ (16GB 사용) ✅ (4GB 사용) 80~120
RTX 5090 32GB 130~213
A100 80GB 80GB 100~138
H100 80GB 80GB 140~180

출처:

주요 발견:

  1. RTX 5090이 A100 80GB보다 빠름 (소규모 모델)
  2. RTX 4090은 8B 모델까지 안정적
  3. H100은 최고 성능, 하지만 비싸

동시 사용자 성능

시나리오: Llama 3.1 8B (4bit), 평균 응답 512 토큰

GPU 최대 동시 요청 처리량 (req/s) 평균 레이턴시 병목 지점
RTX 4090 (24GB) ~16 2.5~3.0 3~5s KV cache (20GB)
2× RTX 4090 (48GB) ~64 5.5~7.0 2~4s PCIe 대역폭
A100 80GB ~32 4.0~5.6 2~3s KV cache (70GB)
H100 80GB ~48 6.0~8.0 1.5~2.5s 최고 성능

출처:

주요 발견:

  1. 동시 요청 수는 VRAM 크기에 비례
  2. 듀얼 GPU는 선형 확장 안 됨 (PCIe 병목)
  3. A100은 10명 동시 처리도 OOM 가능 (긴 컨텍스트 시)

🚀 부하 테스트 방법

1. GuideLLM (권장)

설치:

pip install guidellm

기본 벤치마크:

guidellm \
  --url http://localhost:8000/v1 \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --rate 10 \  # 초당 10 요청
  --duration 300  # 5분

출력:

┌─────────────────────────────────────┐
│ GuideLLM Benchmark Results         │
├─────────────────────────────────────┤
│ Total Requests: 3000               │
│ Successful: 2950 (98.3%)           │
│ Failed: 50 (1.7%)                  │
│                                     │
│ Throughput: 9.83 req/s             │
│ Avg Latency: 2.3s                  │
│ P95 Latency: 4.8s                  │
│ P99 Latency: 8.2s                  │
│                                     │
│ Tokens/sec: 5,120                  │
│ GPU Util: 87%                      │
│ KV Cache: 18.2 GB / 20 GB (91%)    │
└─────────────────────────────────────┘

2. vLLM 내장 벤치마크

throughput 테스트:

python -m vllm.benchmarks.benchmark_throughput \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --num-prompts 1000 \
  --input-len 512 \
  --output-len 512

출력:

Throughput: 2,450 tokens/s
Total Time: 209.2s
GPU Memory: 18.5 GB / 24 GB

latency 테스트:

python -m vllm.benchmarks.benchmark_latency \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --input-len 512 \
  --output-len 512 \
  --batch-size 16

출력:

TTFT (Time To First Token): 0.12s
TPOT (Time Per Output Token): 0.018s
Total Latency: 9.4s (512 tokens)

3. 커스텀 부하 테스트 (Python)

import asyncio
import time
from openai import AsyncOpenAI

client = AsyncOpenAI(
    base_url="http://localhost:8000/v1",
    api_key="dummy"
)

async def single_request(prompt_id):
    """단일 요청 측정"""
    start = time.time()
    
    try:
        response = await client.chat.completions.create(
            model="meta-llama/Llama-3.1-8B-Instruct",
            messages=[
                {"role": "user", "content": f"Question {prompt_id}: Tell me about AI"}
            ],
            max_tokens=512,
            temperature=0.7
        )
        
        latency = time.time() - start
        tokens = len(response.choices[0].message.content.split())
        
        return {
            "success": True,
            "latency": latency,
            "tokens": tokens,
            "tokens_per_sec": tokens / latency
        }
    
    except Exception as e:
        return {
            "success": False,
            "error": str(e),
            "latency": time.time() - start
        }

async def load_test(concurrent_users, num_requests):
    """부하 테스트 실행"""
    results = []
    
    # concurrent_users만큼 동시 요청
    for batch_start in range(0, num_requests, concurrent_users):
        batch = []
        
        for i in range(concurrent_users):
            if batch_start + i < num_requests:
                batch.append(single_request(batch_start + i))
        
        # 동시 실행
        batch_results = await asyncio.gather(*batch)
        results.extend(batch_results)
    
    # 통계 계산
    successes = [r for r in results if r["success"]]
    failures = [r for r in results if not r["success"]]
    
    avg_latency = sum(r["latency"] for r in successes) / len(successes)
    avg_tokens_per_sec = sum(r["tokens_per_sec"] for r in successes) / len(successes)
    
    p95_latency = sorted([r["latency"] for r in successes])[int(len(successes) * 0.95)]
    
    print(f"""
=== Load Test Results ===
Total Requests: {num_requests}
Concurrent Users: {concurrent_users}
Successful: {len(successes)} ({len(successes)/num_requests*100:.1f}%)
Failed: {len(failures)} ({len(failures)/num_requests*100:.1f}%)

Avg Latency: {avg_latency:.2f}s
P95 Latency: {p95_latency:.2f}s
Avg Tokens/sec: {avg_tokens_per_sec:.1f}
""")
    
    if failures:
        print(f"\nFailure Reasons:")
        for f in failures[:5]:  # 처음 5개만
            print(f"  - {f['error']}")

# 실행
asyncio.run(load_test(concurrent_users=16, num_requests=100))

실행 예시:

python load_test.py

출력:

=== Load Test Results ===
Total Requests: 100
Concurrent Users: 16
Successful: 94 (94.0%)
Failed: 6 (6.0%)

Avg Latency: 3.45s
P95 Latency: 6.2s
Avg Tokens/sec: 148.2

Failure Reasons:
  - Request timeout after 30s
  - CUDA out of memory

⚙️ vLLM 튜닝 파라미터

핵심 파라미터

1. gpu_memory_utilization

의미:
GPU 메모리의 몇 %를 KV 캐시로 사용할지 결정.

기본값: 0.9 (90%)

튜닝:

from vllm import LLM

# 메모리 부족 시 → 낮춤 (다른 프로세스와 공유)
llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    gpu_memory_utilization=0.85  # 85%만 사용
)

# 단독 사용 시 → 높임 (최대 성능)
llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    gpu_memory_utilization=0.95  # 95% 사용 (OOM 위험)
)

영향:

  • 높일수록: 더 많은 동시 요청 처리
  • 낮출수록: OOM 위험 감소, 동시 처리 감소

2. max_num_seqs

의미:
한 번에 처리할 수 있는 최대 동시 시퀀스(요청) 수.

기본값: 256

튜닝:

# RTX 4090 (24GB) - 8B 모델
llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    max_num_seqs=16,  # 16개 동시 처리
    gpu_memory_utilization=0.9
)

# A100 80GB - 8B 모델
llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    max_num_seqs=64,  # 64개 동시 처리
    gpu_memory_utilization=0.9
)

# 메모리 부족 시
llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    max_num_seqs=8,  # 줄여서 안정성 확보
)

KV 캐시 계산:

KV cache = max_num_seqs × max_model_len × KV_size_per_token

RTX 4090 예시:
- max_num_seqs=16
- max_model_len=8192
- KV_size=~1.5 MB/token (Llama 8B)

KV = 16 × 8192 × 1.5 MB = 196 GB (!!)

주의:
실제로는 PagedAttention 덕분에 196GB 전부 안 쓰지만, 메모리 예약은 함.


3. max_model_len

의미:
최대 컨텍스트 길이 (입력 + 출력 합계).

기본값: 모델 설정 (Llama 8B = 8192)

튜닝:

# 짧은 대화만 (FAQ 챗봇)
llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    max_model_len=2048,  # 8192 → 2048 (75% 메모리 절감)
)

# 긴 컨텍스트 (문서 요약)
llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    max_model_len=16384,  # 8192 → 16384 (2배 메모리 필요)
)

영향:

  • 낮출수록: 더 많은 동시 요청 가능, 메모리 절감
  • 높일수록: 긴 대화 지원, 메모리 증가

4. max_num_batched_tokens

의미:
한 배치에 포함될 최대 토큰 수.

기본값: 없음 (무제한)

튜닝:

# 처리량 최적화
llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    max_num_batched_tokens=8192,  # 큰 배치 → 높은 처리량
)

# 레이턴시 최적화 (빠른 응답)
llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    max_num_batched_tokens=2048,  # 작은 배치 → 낮은 레이턴시
)

파라미터 조합 예시

RTX 4090 (24GB) - 고성능 설정:

llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    tensor_parallel_size=1,
    gpu_memory_utilization=0.95,  # 거의 전부 사용
    max_num_seqs=16,  # 16명 동시
    max_model_len=4096,  # 짧은 대화
    max_num_batched_tokens=8192,  # 처리량 우선
)

RTX 4090 (24GB) - 안정성 설정:

llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    tensor_parallel_size=1,
    gpu_memory_utilization=0.85,  # 여유 확보
    max_num_seqs=8,  # 8명 동시
    max_model_len=2048,  # 짧은 대화만
    max_num_batched_tokens=4096,
)

A100 80GB - 대규모 처리:

llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    tensor_parallel_size=1,
    gpu_memory_utilization=0.9,
    max_num_seqs=64,  # 64명 동시
    max_model_len=8192,  # 풀 컨텍스트
    max_num_batched_tokens=16384,
)

🔥 병목 현상 및 해결

1. KV Cache Exhausted

증상:

WARNING: All KV cache blocks are full. Dropping requests.

원인:
동시 요청이 많아서 KV 캐시 메모리 부족.

해결:

# 1. max_num_seqs 줄이기
llm = LLM(model="...", max_num_seqs=8)  # 16 → 8

# 2. max_model_len 줄이기
llm = LLM(model="...", max_model_len=2048)  # 8192 → 2048

# 3. gpu_memory_utilization 높이기
llm = LLM(model="...", gpu_memory_utilization=0.95)  # 0.9 → 0.95

# 4. 모델 양자화
llm = LLM(
    model="...",
    quantization="awq",  # FP16 → 4bit (75% 메모리 절감)
)

2. OOM (Out of Memory)

증상:

torch.cuda.OutOfMemoryError: CUDA out of memory

원인:
모델 + KV 캐시가 GPU VRAM 초과.

해결:

# 1. 양자화
llm = LLM(model="...", quantization="awq")  # 16GB → 4GB

# 2. Tensor Parallelism (멀티 GPU)
llm = LLM(
    model="meta-llama/Llama-3.1-70B-Instruct",
    tensor_parallel_size=4,  # 4개 GPU에 분산
)

# 3. max_model_len 줄이기
llm = LLM(model="...", max_model_len=1024)  # 최소화

3. Request Timeout

증상:

ERROR: Request timeout after 30s

원인:
요청 큐가 가득 차서 처리 대기 중.

해결:

# 1. max_num_seqs 늘리기 (메모리 있으면)
llm = LLM(model="...", max_num_seqs=32)  # 16 → 32

# 2. 타임아웃 늘리기 (클라이언트)
response = await client.chat.completions.create(
    ...,
    timeout=60  # 30s → 60s
)

# 3. 오토스케일링 (Kubernetes)
kubectl scale deployment vllm --replicas=3  # 1 → 3 인스턴스

4. 낮은 GPU 사용률

증상:

nvidia-smi: GPU Util: 30%

원인:
배치 크기가 작아서 GPU가 놀고 있음.

해결:

# 1. max_num_batched_tokens 늘리기
llm = LLM(model="...", max_num_batched_tokens=16384)

# 2. max_num_seqs 늘리기
llm = LLM(model="...", max_num_seqs=64)

# 3. enforce_eager=False (CUDA Graphs 활성화)
llm = LLM(model="...", enforce_eager=False)

🎯 실전 튜닝 워크플로우

1단계: 기본 벤치마크

# 기본 설정으로 시작
python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --port 8000

# 부하 테스트
guidellm \
  --url http://localhost:8000/v1 \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --rate 5 \
  --duration 60

결과 확인:

  • Throughput: 2.3 req/s
  • Avg Latency: 4.2s
  • GPU Util: 45%
  • KV Cache: 12 GB / 20 GB (60%)

분석:
GPU 사용률 낮음 → 배치 크기 증가 여지 있음.


2단계: max_num_seqs 증가

python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --max-num-seqs 32 \  # 16 → 32
  --port 8000

guidellm --url http://localhost:8000/v1 --rate 10

결과:

  • Throughput: 4.8 req/s (+108%)
  • Avg Latency: 4.5s (+7%)
  • GPU Util: 78%
  • KV Cache: 18 GB / 20 GB (90%)

분석:
처리량 2배 증가, 레이턴시 약간 증가 → 성공!


3단계: max_model_len 최적화

# 실제 사용자 대화 분석
# 평균 대화: 입력 200 토큰 + 출력 300 토큰 = 500 토큰
# → max_model_len=2048이면 충분

python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --max-num-seqs 32 \
  --max-model-len 2048 \  # 8192 → 2048 (75% 절감)
  --port 8000

결과:

  • Throughput: 6.2 req/s (+29%)
  • KV Cache: 14 GB / 20 GB (70% → 더 많은 요청 가능)

4단계: gpu_memory_utilization 미세 조정

python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --max-num-seqs 32 \
  --max-model-len 2048 \
  --gpu-memory-utilization 0.95 \  # 0.9 → 0.95
  --port 8000

결과:

  • Throughput: 6.5 req/s (+5%)
  • KV Cache: 18 GB / 20 GB (95% → 한계)

최종 설정 (RTX 4090):

--max-num-seqs 32
--max-model-len 2048
--gpu-memory-utilization 0.95

성능:

  • Throughput: 6.5 req/s (vs 2.3 초기)
  • Latency: 4.8s (허용 범위)
  • Success Rate: 98%

📈 확장 전략

수평 확장 (Horizontal Scaling)

단일 GPU 한계:

  • RTX 4090: ~6 req/s
  • 목표: 30 req/s
  • 필요: 5대 인스턴스

로드 밸런싱 (Kubernetes):

# vllm-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm
spec:
  replicas: 5  # 5대 인스턴스
  selector:
    matchLabels:
      app: vllm
  template:
    metadata:
      labels:
        app: vllm
    spec:
      containers:
      - name: vllm
        image: vllm/vllm-openai:latest
        args:
          - --model=meta-llama/Llama-3.1-8B-Instruct
          - --max-num-seqs=32
          - --max-model-len=2048
          - --gpu-memory-utilization=0.95
        resources:
          limits:
            nvidia.com/gpu: 1  # GPU 1개
---
apiVersion: v1
kind: Service
metadata:
  name: vllm-service
spec:
  selector:
    app: vllm
  ports:
  - port: 8000
  type: LoadBalancer

배포:

kubectl apply -f vllm-deployment.yaml

결과:

  • Throughput: 30 req/s (5×6)
  • 고가용성 (1대 다운되어도 OK)

수직 확장 (Vertical Scaling)

Tensor Parallelism (멀티 GPU):

# 70B 모델 - 단일 GPU 불가
# 4× RTX 4090 = 96GB

llm = LLM(
    model="meta-llama/Llama-3.1-70B-Instruct",
    tensor_parallel_size=4,  # 4개 GPU에 분산
    max_num_seqs=16,
)

비용 vs 성능: | 구성 | GPU | 월 비용 | Throughput | 비고 | |------|-----|---------|------------|------| | 5× 8B (단일 GPU) | 5× RTX 4090 | $1,600 | 30 req/s | 수평 확장 | | 1× 70B (멀티 GPU) | 4× RTX 4090 | $1,280 | 8 req/s | 고품질 |

추천:

  • 처리량 우선: 수평 확장 (5× 8B)
  • 품질 우선: 수직 확장 (1× 70B)

⚠️ 프로덕션 체크리스트

배포 전

  • [ ] 실제 사용자 패턴으로 부하 테스트 완료
  • [ ] P95 레이턴시 < 목표치 (예: 5초)
  • [ ] Success Rate > 99%
  • [ ] GPU 메모리 사용률 < 95% (여유 확보)
  • [ ] 오토스케일링 설정 (Kubernetes HPA)
  • [ ] 모니터링 대시보드 (Prometheus + Grafana)

모니터링 메트릭

필수 메트릭:

# Prometheus exporter
from prometheus_client import Counter, Histogram, Gauge

requests_total = Counter('vllm_requests_total', 'Total requests')
requests_failed = Counter('vllm_requests_failed', 'Failed requests')
latency_seconds = Histogram('vllm_latency_seconds', 'Request latency')
gpu_memory_used = Gauge('vllm_gpu_memory_used_bytes', 'GPU memory used')
kv_cache_used = Gauge('vllm_kv_cache_used_bytes', 'KV cache used')
queue_size = Gauge('vllm_queue_size', 'Request queue size')

알람 설정:

# Prometheus alert rules
groups:
  - name: vllm_alerts
    rules:
      - alert: HighLatency
        expr: histogram_quantile(0.95, vllm_latency_seconds) > 10
        annotations:
          summary: "P95 latency > 10s"
      
      - alert: HighFailureRate
        expr: rate(vllm_requests_failed[5m]) / rate(vllm_requests_total[5m]) > 0.05
        annotations:
          summary: "Failure rate > 5%"
      
      - alert: KVCacheExhausted
        expr: vllm_kv_cache_used_bytes / vllm_gpu_memory_total_bytes > 0.95
        annotations:
          summary: "KV cache > 95%, consider scaling"

📚 참고 자료

공식 문서

벤치마크

커뮤니티


작성일: 2026-02-25
데이터 기준: 2024-2025년 벤치마크

💬 0 로그인 후 댓글 작성
첫 댓글을 남겨보세요!