← Back to Presentations

vLLM Cheatsheet

Serving LLMs at Scale — Single GPU, Multi-GPU & Multi-Node with Ray

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/cu124

Docker (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.3

Conda setup

conda create -n vllm python=3.11 -y
conda activate vllm
pip install vllm

Verify 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 ArchitectureCompute CapabilityExamples
Ampere8.0 / 8.6A100, A10G, RTX 3090
Ada Lovelace8.9RTX 4090, L4, L40S
Hopper9.0H100, H200
Blackwell10.0RTX 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 4096

Python 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)

Llama 3/3.1/3.2/3.3Mistral/MixtralQwen 2/2.5DeepSeek V2/V3/R1Gemma 2Phi-3/4Command R+YiInternLMChatGLMFalconStarCoder2

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
FlagPurposeDefault
--gpu-memory-utilizationFraction of GPU memory to use0.90
--max-model-lenMax sequence length (tokens)Model default
--dtypeModel weight precisionauto
--quantizationQuantization methodNone
--enforce-eagerDisable CUDA graphsFalse
--swap-spaceCPU swap for KV cache (GB)4
--kv-cache-dtypeKV cache precisionauto
--max-num-seqsMax concurrent sequences256
--disable-log-requestsSilence request loggingFalse

💡 --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 2

Pipeline 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

ModelPrecisionVRAM NeededFits 2×32GB?
Llama 3.1 8BFP16~18 GB✅ 1 GPU enough
Llama 3.1 8BFP8~10 GB✅ 1 GPU enough
Qwen 2.5 32BFP16~66 GB⚠️ Tight with TP=2
Qwen 2.5 32BAWQ 4-bit~18 GB✅ 1 GPU enough
Llama 3.1 70BFP16~140 GB❌ Need 4+ GPUs
Llama 3.1 70BAWQ 4-bit~37 GB✅ TP=2
Llama 3.1 70BFP8~72 GB⚠️ Tight with TP=2
DeepSeek R1 671BFP16~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 --version

Step 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 2

Network Requirements

NetworkBandwidthLatency ImpactRecommendation
InfiniBand HDR200 Gbps~0.5-1ms★★★ Best for TP
InfiniBand EDR100 Gbps~1-2ms★★★ Great
100GbE100 Gbps~1-2ms★★☆ Good
25GbE25 Gbps~2-5ms★★☆ Minimum for TP
10GbE10 Gbps~5-15ms★☆☆ PP only
1GbE1 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 ms

Docker 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)?

ModelPrecisionVRAMConfig
Llama 3.1 70BAWQ 4-bit~37 GBTP=2 on single node (overkill for 4)
Llama 3.1 70BFP16~140 GBTP=4 across both nodes (tight)
Llama 3.1 70BFP8~72 GBTP=4 (comfortable, 56GB KV cache)
Qwen 2.5 72BAWQ 4-bit~38 GBTP=2 on single node
DeepSeek V3 671BFP8 (MoE)~350 GB❌ Needs 8+ GPUs
Llama 3.1 405BAWQ 4-bit~210 GB❌ Needs 8× 32GB
Llama 3.1 405BFP8~410 GB❌ Needs 16+ GPUs
Mixtral 8x22BFP16~280 GB❌ Too large
Mixtral 8x7BFP16~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-Instruct

6. Quantization

MethodBitsQualitySpeedBest For
FP16/BF1616★★★★★BaselineWhen VRAM allows
FP88★★★★☆~1.5× fasterBest quality/speed tradeoff
AWQ4★★★★☆~2× fasterMost popular 4-bit
GPTQ4★★★☆☆~2× fasterOlder but well-tested
SqueezeLLM4★★★☆☆~1.8× fasterSparse quantization
GGUF2-8VariesVariesllama.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-Instruct

Quantize 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-requests

API Endpoints

EndpointPurpose
/v1/chat/completionsChat completions (ChatGPT-compatible)
/v1/completionsText completions
/v1/modelsList available models
/v1/embeddingsText embeddings (embedding models only)
/healthHealth check
/metricsPrometheus metrics
/versionvLLM 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 throughput

Speculative 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 2

Prefix 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_rate

Benchmarking

# 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 500

Continuous 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 GPUs

gpu-memory-utilization sweet spots

ValueUse Case
0.70 - 0.80Development / shared GPU with other processes
0.85 - 0.90Production default (safe buffer)
0.90 - 0.95Dedicated GPU, maximize throughput
0.95 - 0.98Aggressive — 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-stopped

systemd 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.target

Kubernetes 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: ClusterIP

Prometheus 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 64

Request 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-loras controls 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 traces

Common 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 requests

NCCL 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 test

Model 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-code

Slow 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')
"