[active] vLLM 부하 테스트: GPU 용량 한계 실전 가이드
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 |
출처:
주요 발견:
- RTX 5090이 A100 80GB보다 빠름 (소규모 모델)
- RTX 4090은 8B 모델까지 안정적
- 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 | 최고 성능 |
출처:
주요 발견:
- 동시 요청 수는 VRAM 크기에 비례
- 듀얼 GPU는 선형 확장 안 됨 (PCIe 병목)
- 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년 벤치마크