1. Installation & Setup
pip install (recommended)
# Requires Python 3.9-3.12, CUDA 12.1+
pip install vllm
# With specific CUDA version
pip install vllm --extra-index-url https://download.pytorch.org/whl/cu124Docker (production-ready)
# OpenAI-compatible API server
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-3.1-8B-Instruct
# With specific version
docker pull vllm/vllm-openai:v0.7.3Conda setup
conda create -n vllm python=3.11 -y
conda activate vllm
pip install vllmVerify GPU
# Check NVIDIA drivers & CUDA
nvidia-smi
# Check PyTorch CUDA
python -c "import torch; print(torch.cuda.is_available()); print(torch.cuda.device_count())"
# Check vLLM installation
python -c "import vllm; print(vllm.__version__)"| GPU Architecture | Compute Capability | Examples |
|---|---|---|
| Ampere | 8.0 / 8.6 | A100, A10G, RTX 3090 |
| Ada Lovelace | 8.9 | RTX 4090, L4, L40S |
| Hopper | 9.0 | H100, H200 |
| Blackwell | 10.0 | RTX 5090 (32GB), B100, B200 |
2. Quick Start
Serve a model (OpenAI-compatible)
# Start API server
vllm serve meta-llama/Llama-3.1-8B-Instruct
# With custom settings
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--host 0.0.0.0 \
--port 8000 \
--max-model-len 4096Python API (offline inference)
from vllm import LLM, SamplingParams
llm = LLM(model="meta-llama/Llama-3.1-8B-Instruct")
params = SamplingParams(temperature=0.7, max_tokens=256, top_p=0.95)
prompts = ["Explain quantum computing in simple terms"]
outputs = llm.generate(prompts, params)
for output in outputs:
print(output.outputs[0].text)Call via curl (OpenAI API)
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "meta-llama/Llama-3.1-8B-Instruct",
"messages": [{"role": "user", "content": "Hello!"}],
"temperature": 0.7,
"max_tokens": 256
}'Use with OpenAI Python SDK
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")
response = client.chat.completions.create(
model="meta-llama/Llama-3.1-8B-Instruct",
messages=[{"role": "user", "content": "Hello!"}],
temperature=0.7,
)
print(response.choices[0].message.content)Supported Models (non-exhaustive)
Full list: vllm.readthedocs.io/en/latest/models/supported_models.html
3. Single GPU Configuration
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--gpu-memory-utilization 0.90 \ # Use 90% of VRAM (default 0.90)
--max-model-len 8192 \ # Max context length
--dtype bfloat16 \ # auto|float16|bfloat16|float32
--quantization awq \ # awq|gptq|squeezellm|fp8|None
--enforce-eager \ # Disable CUDA graphs (debug/save memory)
--swap-space 4 \ # CPU swap space in GB for KV cache
--kv-cache-dtype auto # auto|fp8|fp8_e5m2|fp8_e4m3| Flag | Purpose | Default |
|---|---|---|
| --gpu-memory-utilization | Fraction of GPU memory to use | 0.90 |
| --max-model-len | Max sequence length (tokens) | Model default |
| --dtype | Model weight precision | auto |
| --quantization | Quantization method | None |
| --enforce-eager | Disable CUDA graphs | False |
| --swap-space | CPU swap for KV cache (GB) | 4 |
| --kv-cache-dtype | KV cache precision | auto |
| --max-num-seqs | Max concurrent sequences | 256 |
| --disable-log-requests | Silence request logging | False |
💡 --enforce-eager vs PagedAttention
By default vLLM uses CUDA graphs for optimized execution. Use --enforce-eager to disable them (saves ~1-2GB VRAM, useful for debugging or tight memory). PagedAttention manages KV cache efficiently regardless.
4. Multi-GPU (Single Node)
Tensor Parallelism (recommended for latency)
# Split model across 2 GPUs on the same machine
vllm serve meta-llama/Llama-3.1-70B-Instruct \
--tensor-parallel-size 2 \
--gpu-memory-utilization 0.90 \
--max-model-len 4096
# Select specific GPUs
CUDA_VISIBLE_DEVICES=0,1 vllm serve meta-llama/Llama-3.1-70B-Instruct \
--tensor-parallel-size 2Pipeline Parallelism (for throughput)
# Pipeline parallelism — layers split sequentially across GPUs
vllm serve meta-llama/Llama-3.1-70B-Instruct \
--pipeline-parallel-size 2
# Combined: 4 GPUs = 2 tensor × 2 pipeline
vllm serve meta-llama/Llama-3.1-70B-Instruct \
--tensor-parallel-size 2 \
--pipeline-parallel-size 2🧮 Memory Calculation Guide
# Formula: VRAM needed = Model Params × Bytes per Param + KV Cache
#
# Example: Llama 3.1 70B in FP16 on 2× RTX 5090 (32GB each = 64GB total)
# Model weights: 70B × 2 bytes (FP16) = 140 GB ← Does NOT fit in 64GB!
# Solution: Use quantization (AWQ/GPTQ)
#
# Example: Llama 3.1 70B AWQ (4-bit) on 2× RTX 5090
# Model weights: 70B × 0.5 bytes (4-bit) = ~35 GB ← Fits easily!
# KV cache: ~2-8 GB depending on context length
# Total: ~37-43 GB across 2 GPUs = ~18-21 GB per GPU ✓
#
# Example: Llama 3.1 8B in FP16 on 1× RTX 5090
# Model weights: 8B × 2 bytes = 16 GB
# KV cache: ~2-4 GB
# Total: ~18-20 GB on 32 GB GPU ✓Practical: 2× NVIDIA RTX 5090 32GB
| Model | Precision | VRAM Needed | Fits 2×32GB? |
|---|---|---|---|
| Llama 3.1 8B | FP16 | ~18 GB | ✅ 1 GPU enough |
| Llama 3.1 8B | FP8 | ~10 GB | ✅ 1 GPU enough |
| Qwen 2.5 32B | FP16 | ~66 GB | ⚠️ Tight with TP=2 |
| Qwen 2.5 32B | AWQ 4-bit | ~18 GB | ✅ 1 GPU enough |
| Llama 3.1 70B | FP16 | ~140 GB | ❌ Need 4+ GPUs |
| Llama 3.1 70B | AWQ 4-bit | ~37 GB | ✅ TP=2 |
| Llama 3.1 70B | FP8 | ~72 GB | ⚠️ Tight with TP=2 |
| DeepSeek R1 671B | FP16 | ~1.3 TB | ❌ Way too large |
5. Multi-Node Setup with Ray
🎯 Target Setup: 2 machines × 2× RTX 5090 32GB = 128GB total VRAM
With 4 GPUs across 2 nodes, you can run 70B FP16 models (~140GB with KV cache — tight but works with TP=4), or comfortably run 70B quantized with massive KV cache headroom.
Step 1: Install Ray on all nodes
pip install ray[default]
# Verify
ray --versionStep 2: Start Ray cluster
# On HEAD node (machine 1):
ray start --head --port=6379 --dashboard-host=0.0.0.0
# On WORKER node (machine 2):
ray start --address='<HEAD_NODE_IP>:6379'
# Verify cluster
ray status
# Should show: 4 GPUs total (2 per node)Step 3: Launch vLLM with tensor parallelism across nodes
# On the HEAD node — vLLM auto-discovers Ray cluster
vllm serve meta-llama/Llama-3.1-70B-Instruct \
--tensor-parallel-size 4 \
--gpu-memory-utilization 0.90 \
--max-model-len 8192 \
--host 0.0.0.0 \
--port 8000
# Combined parallelism example: TP=2, PP=2
# Each node handles 2 tensor-parallel shards of a pipeline stage
vllm serve meta-llama/Llama-3.1-70B-Instruct \
--tensor-parallel-size 2 \
--pipeline-parallel-size 2Network Requirements
| Network | Bandwidth | Latency Impact | Recommendation |
|---|---|---|---|
| InfiniBand HDR | 200 Gbps | ~0.5-1ms | ★★★ Best for TP |
| InfiniBand EDR | 100 Gbps | ~1-2ms | ★★★ Great |
| 100GbE | 100 Gbps | ~1-2ms | ★★☆ Good |
| 25GbE | 25 Gbps | ~2-5ms | ★★☆ Minimum for TP |
| 10GbE | 10 Gbps | ~5-15ms | ★☆☆ PP only |
| 1GbE | 1 Gbps | ~20-50ms+ | ❌ Too slow |
NCCL Environment Variables
# Essential NCCL tuning for multi-node
export NCCL_SOCKET_IFNAME=eth0 # Network interface (check with: ip addr)
export NCCL_DEBUG=INFO # Debug logging (WARN in production)
export NCCL_IB_DISABLE=0 # 0 = use InfiniBand if available
export NCCL_P2P_DISABLE=0 # 0 = enable peer-to-peer (same node)
export NCCL_NET_GDR_LEVEL=5 # GPUDirect RDMA level
export NCCL_IB_GID_INDEX=3 # InfiniBand GID index (if RoCE)
# For TCP networks (no InfiniBand):
export NCCL_IB_DISABLE=1
export NCCL_SOCKET_IFNAME=eth0
export NCCL_SOCKET_NTHREADS=4
export NCCL_NSOCKS_PERTHREAD=4
# Timeout (increase for slow networks)
export NCCL_TIMEOUT=1800 # 30 min timeout for initial setup
export VLLM_RPC_TIMEOUT=30000 # vLLM RPC timeout in msDocker Compose for Multi-Node
# docker-compose.yml — HEAD NODE (machine 1)
services:
ray-head:
image: vllm/vllm-openai:latest
runtime: nvidia
network_mode: host
ipc: host
environment:
- CUDA_VISIBLE_DEVICES=0,1
- NCCL_SOCKET_IFNAME=eth0
- NCCL_DEBUG=WARN
- HF_TOKEN=${HF_TOKEN}
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface
command: >
bash -c "ray start --head --port=6379 --dashboard-host=0.0.0.0 &&
sleep 30 &&
vllm serve meta-llama/Llama-3.1-70B-Instruct
--tensor-parallel-size 4
--gpu-memory-utilization 0.90
--host 0.0.0.0
--port 8000"
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 2
capabilities: [gpu]
---
# docker-compose.yml — WORKER NODE (machine 2)
services:
ray-worker:
image: vllm/vllm-openai:latest
runtime: nvidia
network_mode: host
ipc: host
environment:
- CUDA_VISIBLE_DEVICES=0,1
- NCCL_SOCKET_IFNAME=eth0
- NCCL_DEBUG=WARN
- RAY_HEAD_ADDRESS=<HEAD_NODE_IP>:6379
- HF_TOKEN=${HF_TOKEN}
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface
command: >
bash -c "ray start --address=${RAY_HEAD_ADDRESS} --block"
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 2
capabilities: [gpu]What fits on 2 machines × 2× RTX 5090 (128GB total)?
| Model | Precision | VRAM | Config |
|---|---|---|---|
| Llama 3.1 70B | AWQ 4-bit | ~37 GB | TP=2 on single node (overkill for 4) |
| Llama 3.1 70B | FP16 | ~140 GB | TP=4 across both nodes (tight) |
| Llama 3.1 70B | FP8 | ~72 GB | TP=4 (comfortable, 56GB KV cache) |
| Qwen 2.5 72B | AWQ 4-bit | ~38 GB | TP=2 on single node |
| DeepSeek V3 671B | FP8 (MoE) | ~350 GB | ❌ Needs 8+ GPUs |
| Llama 3.1 405B | AWQ 4-bit | ~210 GB | ❌ Needs 8× 32GB |
| Llama 3.1 405B | FP8 | ~410 GB | ❌ Needs 16+ GPUs |
| Mixtral 8x22B | FP16 | ~280 GB | ❌ Too large |
| Mixtral 8x7B | FP16 | ~96 GB | ✅ TP=4 (32GB headroom) |
⚡ Latency: Multi-Node vs Single Node
Cross-node tensor parallelism adds ~2-5ms per token on 25GbE, ~0.5-1ms on InfiniBand. For throughput-oriented workloads this is negligible. For latency-sensitive chat, prefer pipeline parallelism across nodes (less inter-node communication) or keep everything on one node with quantization.
Troubleshooting Multi-Node
# NCCL timeout — increase timeout, check network
export NCCL_TIMEOUT=3600
export NCCL_DEBUG=INFO
# Firewall rules — open these ports between nodes:
# - 6379: Ray GCS
# - 8265: Ray Dashboard
# - 10001-10999: Ray object store & workers
# - Random high ports: NCCL communication
sudo ufw allow from <OTHER_NODE_IP> to any
# Check Ray cluster connectivity
ray status
# NFS for model weights (avoid downloading on each node)
# On NFS server:
sudo apt install nfs-kernel-server
echo "/models *(rw,sync,no_subtree_check)" >> /etc/exports
sudo exportfs -a
# On worker nodes:
sudo mount <HEAD_IP>:/models /models
# Or use Hugging Face cache sharing
export HF_HOME=/shared/huggingface # NFS-mounted path
# Common issue: models download twice (once per node)
# Solution: Pre-download on shared storage
huggingface-cli download meta-llama/Llama-3.1-70B-Instruct6. Quantization
| Method | Bits | Quality | Speed | Best For |
|---|---|---|---|---|
| FP16/BF16 | 16 | ★★★★★ | Baseline | When VRAM allows |
| FP8 | 8 | ★★★★☆ | ~1.5× faster | Best quality/speed tradeoff |
| AWQ | 4 | ★★★★☆ | ~2× faster | Most popular 4-bit |
| GPTQ | 4 | ★★★☆☆ | ~2× faster | Older but well-tested |
| SqueezeLLM | 4 | ★★★☆☆ | ~1.8× faster | Sparse quantization |
| GGUF | 2-8 | Varies | Varies | llama.cpp compat |
Using pre-quantized models
# AWQ (most common on Hugging Face)
vllm serve TheBloke/Llama-2-70B-Chat-AWQ \
--quantization awq
# GPTQ
vllm serve TheBloke/Llama-2-70B-Chat-GPTQ \
--quantization gptq
# FP8 (Hopper/Ada/Blackwell GPUs)
vllm serve neuralmagic/Meta-Llama-3.1-70B-Instruct-FP8 \
--quantization fp8
# GGUF (limited support)
vllm serve ./model.gguf \
--tokenizer meta-llama/Llama-3.1-8B-InstructQuantize yourself
# AutoAWQ — quantize to AWQ 4-bit
pip install autoawq
python -c "
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer
model_path = 'meta-llama/Llama-3.1-70B-Instruct'
quant_path = './llama-70b-awq'
model = AutoAWQForCausalLM.from_pretrained(model_path)
tokenizer = AutoTokenizer.from_pretrained(model_path)
model.quantize(tokenizer, quant_config={'zero_point': True, 'q_group_size': 128, 'w_bit': 4})
model.save_quantized(quant_path)
tokenizer.save_pretrained(quant_path)
"
# auto-gptq — quantize to GPTQ 4-bit
pip install auto-gptq optimum
python -c "
from transformers import AutoModelForCausalLM, AutoTokenizer, GPTQConfig
model_id = 'meta-llama/Llama-3.1-8B-Instruct'
quantization_config = GPTQConfig(bits=4, dataset='c4', tokenizer=AutoTokenizer.from_pretrained(model_id))
model = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=quantization_config)
model.save_pretrained('./llama-8b-gptq')
"💡 For 2× RTX 5090 (64GB): Best quantization choices
- 70B models: AWQ 4-bit (~37GB) fits comfortably on TP=2
- 70B models: FP8 (~72GB) works with TP=2 but tight on KV cache
- 8-13B models: FP16 (~16-26GB) fits on a single GPU
- 32B models: FP16 (~66GB) needs TP=2, or AWQ for single GPU
7. API Server & OpenAI Compatibility
Server options
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--host 0.0.0.0 \
--port 8000 \
--api-key my-secret-key \
--served-model-name llama-3.1 \
--chat-template ./template.jinja \
--response-role assistant \
--max-num-seqs 256 \
--disable-log-requestsAPI Endpoints
| Endpoint | Purpose |
|---|---|
| /v1/chat/completions | Chat completions (ChatGPT-compatible) |
| /v1/completions | Text completions |
| /v1/models | List available models |
| /v1/embeddings | Text embeddings (embedding models only) |
| /health | Health check |
| /metrics | Prometheus metrics |
| /version | vLLM version info |
Streaming
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer my-secret-key" \
-d '{
"model": "llama-3.1",
"messages": [{"role": "user", "content": "Write a poem"}],
"stream": true
}'Guided decoding (JSON mode)
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "llama-3.1",
"messages": [{"role": "user", "content": "Give me a person with name and age"}],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "person",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
}
}
}
}'Nginx load balancer (multiple instances)
# /etc/nginx/conf.d/vllm.conf
upstream vllm_backend {
least_conn;
server 10.0.0.1:8000; # Node 1
server 10.0.0.2:8000; # Node 2
}
server {
listen 80;
server_name llm.example.com;
location / {
proxy_pass http://vllm_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off; # Important for streaming
proxy_read_timeout 300s;
}
}8. Performance Tuning
🧠 PagedAttention — vLLM's Secret Sauce
Instead of pre-allocating a contiguous KV cache per sequence, vLLM manages KV cache in pages(like OS virtual memory). This eliminates fragmentation and enables near-zero waste — up to 24× more throughput than naive implementations.
Key performance flags
vllm serve meta-llama/Llama-3.1-70B-Instruct \
--tensor-parallel-size 2 \
--max-num-seqs 256 \ # Max concurrent requests
--max-num-batched-tokens 32768 \ # Max tokens per batch
--enable-prefix-caching \ # Cache common prefixes (system prompts)
--enable-chunked-prefill \ # Overlap prefill and decode
--gpu-memory-utilization 0.95 # Push to 95% for throughputSpeculative decoding
# Use a small "draft" model to predict tokens, verify with main model
# Can give 2-3× speedup for greedy/low-temp generation
vllm serve meta-llama/Llama-3.1-70B-Instruct \
--speculative-model meta-llama/Llama-3.2-1B-Instruct \
--num-speculative-tokens 5 \
--tensor-parallel-size 2Prefix caching (saves compute for repeated system prompts)
# Enable prefix caching — huge win when many requests share
# the same system prompt or few-shot examples
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--enable-prefix-caching
# Monitor cache hit rate in Prometheus metrics
# vllm:cache_hit_rateBenchmarking
# Throughput benchmark (offline)
python -m vllm.entrypoints.openai.api_server & # start server first
python benchmarks/benchmark_throughput.py \
--model meta-llama/Llama-3.1-8B-Instruct \
--input-len 512 \
--output-len 128 \
--num-prompts 1000
# Serving benchmark (online, with load)
python benchmarks/benchmark_serving.py \
--backend vllm \
--model meta-llama/Llama-3.1-8B-Instruct \
--endpoint /v1/completions \
--request-rate 10 \
--num-prompts 500Continuous Batching
vLLM dynamically adds/removes requests from running batch. No need to wait for longest sequence to finish.
Chunked Prefill
Long prompts are split into chunks, interleaved with decode steps. Reduces time-to-first-token for waiting requests.
9. Memory Management
VRAM calculation formula
# Total VRAM = Model Weights + KV Cache + Activation Memory + CUDA Overhead
#
# Model Weights:
# FP32: params × 4 bytes
# FP16: params × 2 bytes
# FP8: params × 1 byte
# INT4: params × 0.5 bytes
#
# KV Cache per token (per layer):
# 2 × num_heads × head_dim × dtype_bytes
# Llama 70B: 2 × 8 × 128 × 2 bytes = 4 KB per token per layer
# Llama 70B (80 layers): 4 KB × 80 = 320 KB per token
# 8192 context × 320 KB = 2.56 GB per concurrent sequence
#
# Practical VRAM breakdown (Llama 70B FP16, TP=2):
# Model weights: 140 GB ÷ 2 GPUs = 70 GB per GPU ← exceeds 32GB!
# → Must use quantization or more GPUsgpu-memory-utilization sweet spots
| Value | Use Case |
|---|---|
| 0.70 - 0.80 | Development / shared GPU with other processes |
| 0.85 - 0.90 | Production default (safe buffer) |
| 0.90 - 0.95 | Dedicated GPU, maximize throughput |
| 0.95 - 0.98 | Aggressive — risk of OOM under load spikes |
OOM troubleshooting
# Quick fixes for CUDA Out of Memory:
# 1. Reduce context length
--max-model-len 4096 # instead of 8192 or higher
# 2. Lower memory utilization
--gpu-memory-utilization 0.80
# 3. Disable CUDA graphs (saves ~1-2GB)
--enforce-eager
# 4. Use quantization
--quantization awq
# 5. Reduce concurrent sequences
--max-num-seqs 64 # default is 256
# 6. CPU offloading (slow but works)
--cpu-offload-gb 10 # offload 10GB of weights to CPU RAM
# 7. Add more GPUs via tensor parallelism
--tensor-parallel-size 2⚠️ CPU offloading: why it's slow on NVIDIA
On NVIDIA GPUs, system RAM and GPU VRAM are separate pools connected via PCIe (~32 GB/s for PCIe 4.0 x16, ~64 GB/s for PCIe 5.0). Every offloaded tensor requires a PCIe transfer. This is fundamentally different from Apple Silicon where CPU and GPU share unified memory— no copy needed. On NVIDIA, --cpu-offload-gb should be a last resort. Quantization or more GPUs is always better.
Practical: fitting models in 64GB VRAM (2× RTX 5090)
# Best configs for 2× RTX 5090 32GB (64GB total):
# 70B AWQ — comfortable, lots of KV cache room
vllm serve TheBloke/Llama-2-70B-Chat-AWQ \
--tensor-parallel-size 2 \
--quantization awq \
--max-model-len 8192 \
--gpu-memory-utilization 0.90
# ~37GB model + ~20GB KV cache headroom
# 70B FP8 — tighter but better quality
vllm serve neuralmagic/Meta-Llama-3.1-70B-Instruct-FP8 \
--tensor-parallel-size 2 \
--quantization fp8 \
--max-model-len 4096 \
--gpu-memory-utilization 0.95
# ~72GB model, leaves ~18GB for KV cache (shorter context)
# Multiple small models (different ports)
vllm serve meta-llama/Llama-3.1-8B-Instruct --port 8000 &
CUDA_VISIBLE_DEVICES=1 vllm serve mistralai/Mistral-7B-Instruct-v0.3 --port 8001 &10. Production Deployment
Docker Compose (single node)
# docker-compose.yml
services:
vllm:
image: vllm/vllm-openai:latest
runtime: nvidia
ports:
- "8000:8000"
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface
environment:
- HF_TOKEN=${HF_TOKEN}
- CUDA_VISIBLE_DEVICES=0,1
ipc: host
command: >
--model meta-llama/Llama-3.1-70B-Instruct
--tensor-parallel-size 2
--gpu-memory-utilization 0.90
--max-model-len 8192
--api-key ${VLLM_API_KEY}
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 2
capabilities: [gpu]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 120s
restart: unless-stoppedsystemd service file
# /etc/systemd/system/vllm.service
[Unit]
Description=vLLM OpenAI-compatible API Server
After=network.target docker.service
Requires=docker.service
[Service]
Type=simple
ExecStart=/usr/bin/docker compose -f /opt/vllm/docker-compose.yml up
ExecStop=/usr/bin/docker compose -f /opt/vllm/docker-compose.yml down
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetKubernetes Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm
spec:
replicas: 1
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"
- "--gpu-memory-utilization"
- "0.90"
ports:
- containerPort: 8000
resources:
limits:
nvidia.com/gpu: 1
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 120
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 60
periodSeconds: 10
env:
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token
key: token
volumeMounts:
- name: model-cache
mountPath: /root/.cache/huggingface
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: model-cache-pvc
---
apiVersion: v1
kind: Service
metadata:
name: vllm
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
prometheus.io/path: "/metrics"
spec:
selector:
app: vllm
ports:
- port: 8000
targetPort: 8000
type: ClusterIPPrometheus metrics & Grafana
# vLLM exposes Prometheus metrics at /metrics
# Key metrics to monitor:
# Request throughput
vllm:num_requests_running # Currently running requests
vllm:num_requests_waiting # Queued requests
vllm:request_success_total # Total successful requests
# Latency
vllm:e2e_request_latency_seconds # End-to-end latency histogram
vllm:time_to_first_token_seconds # Time to first token (TTFT)
vllm:time_per_output_token_seconds # Time per output token (TPOT)
# GPU utilization
vllm:gpu_cache_usage_perc # KV cache utilization
vllm:cpu_cache_usage_perc # CPU swap cache utilization
# Model info
vllm:num_preemptions_total # Preempted requests (VRAM pressure)
vllm:avg_generation_throughput_toks_per_s # Tokens/sec
# Scrape config for Prometheus
scrape_configs:
- job_name: 'vllm'
static_configs:
- targets: ['vllm-host:8000']Model warmup
# Warmup script — run after server starts to JIT compile CUDA kernels
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "llama-3.1", "messages": [{"role": "user", "content": "Hello"}], "max_tokens": 1}'
# Or in Python
import requests
for _ in range(3):
requests.post("http://localhost:8000/v1/chat/completions", json={
"model": "llama-3.1",
"messages": [{"role": "user", "content": "warmup"}],
"max_tokens": 1
})11. LoRA & Fine-tuned Models
Serve with LoRA adapters
# Single LoRA adapter
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--enable-lora \
--lora-modules my-lora=./path/to/lora/adapter
# Multiple LoRA adapters simultaneously
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--enable-lora \
--lora-modules \
sql-expert=./loras/sql-adapter \
code-helper=./loras/code-adapter \
medical=./loras/medical-adapter \
--max-loras 3 \
--max-lora-rank 64Request a specific LoRA
# Use the LoRA name as the model in the API call
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "sql-expert",
"messages": [{"role": "user", "content": "Write a SQL query..."}]
}'
# Or use the base model (no LoRA)
curl http://localhost:8000/v1/chat/completions \
-d '{"model": "meta-llama/Llama-3.1-8B-Instruct", "messages": [...]}'💡 LoRA memory overhead
- LoRA adapters are tiny: typically 10-100MB each (rank 16-64)
- Base model is loaded once; LoRA weights are added on top
- Multiple LoRAs share the base model's KV cache
--max-lorascontrols how many can be active simultaneously- Inactive LoRAs are swapped in/out dynamically
Dynamic LoRA loading (via API)
# Load a new LoRA adapter at runtime (if supported by your version)
# Start with --enable-lora but no --lora-modules
vllm serve meta-llama/Llama-3.1-8B-Instruct --enable-lora
# Then load adapters via the API
curl -X POST http://localhost:8000/v1/load_lora_adapter \
-H "Content-Type: application/json" \
-d '{
"lora_name": "new-adapter",
"lora_path": "/models/loras/new-adapter"
}'12. Troubleshooting & FAQ
Debug logging
# Enable verbose logging
export VLLM_LOGGING_LEVEL=DEBUG
vllm serve meta-llama/Llama-3.1-8B-Instruct
# Or per-component
export VLLM_TRACE_FUNCTION=1 # Trace function calls
export CUDA_LAUNCH_BLOCKING=1 # Sync CUDA for better error tracesCommon errors & fixes
CUDA Out of Memory
# Error: torch.cuda.OutOfMemoryError
# Fixes (in order of preference):
--quantization awq # Use quantized model
--max-model-len 4096 # Reduce context length
--gpu-memory-utilization 0.80 # Lower memory target
--enforce-eager # Disable CUDA graphs
--tensor-parallel-size 2 # Add more GPUs
--max-num-seqs 32 # Fewer concurrent requestsNCCL Timeout (multi-node)
# Error: NCCL timeout / ProcessGroupNCCL watchdog
# Fixes:
export NCCL_TIMEOUT=1800 # Increase timeout (seconds)
export NCCL_DEBUG=INFO # Get detailed logs
export NCCL_SOCKET_IFNAME=eth0 # Correct network interface
export NCCL_IB_DISABLE=1 # Disable InfiniBand if not available
# Check connectivity between nodes:
ping <other-node-ip>
nc -zv <other-node-ip> 6379 # Ray port
iperf3 -s / iperf3 -c <ip> # Bandwidth testModel not supported
# Error: Model architecture XYZ is not supported
# Check supported models:
python -c "from vllm import ModelRegistry; print(ModelRegistry.get_supported_archs())"
# Common fix: update vLLM
pip install --upgrade vllm
# Use --trust-remote-code for custom architectures
vllm serve <model> --trust-remote-codeSlow tokenizer warning
# Warning: Using slow tokenizer
# Fix: install tokenizers backend
pip install protobuf sentencepiece
# Or specify tokenizer mode
--tokenizer-mode auto # or "mistral" for Mistral tokenizer📌 RAM vs VRAM — Why System RAM ≠ GPU VRAM
NVIDIA (Discrete GPU)
- GPU has its own dedicated VRAM
- Data must be copied over PCIe bus
- PCIe 4.0 x16: ~32 GB/s bandwidth
- PCIe 5.0 x16: ~64 GB/s bandwidth
- System RAM (128GB+) can't compensate for low VRAM
- CPU offloading works but adds massive latency
Apple Silicon (Unified Memory)
- CPU and GPU share the same RAM
- No data copying needed — zero-copy access
- M4 Max: 128GB unified = 128GB for GPU
- Memory bandwidth: ~546 GB/s (M4 Max)
- Great for large models that fit in RAM
- But GPU compute is much slower than NVIDIA
Bottom line: On NVIDIA, the only numbers that matter are your VRAM totals. Having 512GB system RAM with 32GB VRAM still means you can only fit ~32GB of model + KV cache at full speed.
Useful profiling commands
# Monitor GPU usage in real-time
watch -n 1 nvidia-smi
# Detailed GPU memory breakdown
nvidia-smi --query-gpu=name,memory.total,memory.used,memory.free,utilization.gpu \
--format=csv -l 1
# vLLM internal profiling
export VLLM_TORCH_PROFILER_DIR=./profiler_output
vllm serve <model>
# Then open Chrome and navigate to chrome://tracing
# Check model size before loading
python -c "
from transformers import AutoConfig
config = AutoConfig.from_pretrained('meta-llama/Llama-3.1-70B-Instruct')
params = sum(p for p in [config.hidden_size * config.vocab_size,
config.num_hidden_layers * (4 * config.hidden_size * config.hidden_size +
3 * config.hidden_size * config.intermediate_size)])
print(f'~{params/1e9:.1f}B params, ~{params*2/1e9:.1f} GB FP16')
"