vLLM : Servir des LLM à haute performance

tl;dr: vLLM = framework d'inférence ultra-optimisé pour LLM. Utilise PagedAttention (gestion KV cache) et continuous batching. Débit 10-24x supérieur aux solutions classiques. Compatible API OpenAI. Sert Llama, Mistral, GPT en production.

💡

Qu’est-ce que vLLM ?

vLLM (Very Large Language Model) est un framework open-source d’inférence haute performance pour les LLM. Développé par UC Berkeley, il utilise des techniques innovantes comme PagedAttention et continuous batching pour servir des modèles avec un throughput 10-24x supérieur aux implémentations classiques.

Compatible OpenAI API : Drop-in replacement pour vos applications existantes.

Servir des LLM en production est un défi majeur : latence élevée, faible throughput, coûts GPU astronomiques. Les frameworks traditionnels (HuggingFace Transformers, PyTorch) ne sont pas optimisés pour l’inférence à grande échelle.

Illustration détaillée de vLLM pour l’inférence optimisée de modèles

Le problème : goulots d’étranglement de l’inférence

# Approche naïve avec HuggingFace
from transformers import AutoModelForCausalLM, AutoTokenizer

model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf")

# Problème 1 : Batch size limité
# → Mémoire GPU saturée rapidement par KV cache

# Problème 2 : Pas de batching dynamique
# → GPU idle pendant les réponses courtes

# Problème 3 : Gestion inefficace de la mémoire
# → Fragmentation, allocation statique

# Résultat : Throughput faible (~10-20 req/s sur A100)

vLLM résout ces problèmes :

MétriqueHuggingFacevLLMAmélioration
Throughput10-20 req/s100-250 req/s10-24x
LatenceÉlevéeFaible2-3x plus rapide
Utilisation GPU30-50%80-95%2x meilleur
Batch sizeLimité (4-8)Dynamique (100+)Flexible

PagedAttention : L’innovation clé

Le problème du KV Cache

Les Transformers stockent les key/value tensors des tokens précédents (KV cache) pour éviter de recalculer à chaque nouveau token.

# Génération autoregressive classique

def generate_token(model, past_tokens):
    """
    Génère le prochain token
    """
    # Problème : KV cache grossit linéairement avec la séquence
    # Pour un modèle 7B avec batch de 32 :
    # - Séquence de 2048 tokens
    # - KV cache ≈ 2048 * 32 * 4096 (hidden) * 2 (K+V) * 2 bytes (FP16)
    # - Total ≈ 1 GB par requête !

    # Avec batch de 32 requêtes : 32 GB juste pour KV cache
    # → Laisse peu de mémoire pour le modèle lui-même

    outputs = model(past_tokens, use_cache=True)
    kv_cache = outputs.past_key_values  # Énorme !

    return next_token, kv_cache

Problèmes traditionnels :

  1. Fragmentation : Allocation mémoire inefficace
  2. Over-allocation : Alloue max_seq_len dès le départ
  3. Pas de partage : Impossible de partager KV cache entre requêtes similaires

PagedAttention : mémoire virtuelle pour KV Cache

vLLM emprunte le concept de mémoire virtuelle des OS modernes.

# Concept PagedAttention

class PagedAttention:
    """
    Gère le KV cache comme des pages mémoire
    """

    def __init__(self, block_size=16):
        """
        Args:
            block_size: Nombre de tokens par bloc (page)
        """
        self.block_size = block_size
        self.blocks = []  # Pool de blocs physiques
        self.block_table = {}  # Mapping logique → physique

    def allocate_blocks(self, sequence_id, num_tokens):
        """
        Alloue des blocs pour une séquence
        """
        num_blocks_needed = (num_tokens + self.block_size - 1) // self.block_size

        allocated_blocks = []
        for _ in range(num_blocks_needed):
            # Récupère un bloc libre du pool
            if self.blocks:
                block = self.blocks.pop()
            else:
                block = self.create_new_block()

            allocated_blocks.append(block)

        # Mapping logique
        self.block_table[sequence_id] = allocated_blocks

        return allocated_blocks

    def share_blocks(self, seq1_id, seq2_id):
        """
        Partage des blocs entre séquences (ex: même prompt)
        """
        # Si seq2 a le même préfixe que seq1
        # → Partage les blocs du préfixe commun (copy-on-write)

        shared_blocks = self.block_table[seq1_id][:num_common_blocks]
        new_blocks = self.allocate_blocks(seq2_id, remaining_tokens)

        self.block_table[seq2_id] = shared_blocks + new_blocks

        # Économie de mémoire massive pour batch avec même prompt !

Avantages :

Zero fragmentation : Blocs de taille fixe

Allocation dynamique : Alloue seulement ce qui est nécessaire

Partage mémoire : Copy-on-write pour prompts communs

Swapping : Peut déplacer KV cache vers CPU si nécessaire

Comparaison visuelle

# Approche classique
# Séquence 1 : [████████████████░░░░░░░░] 16 tokens utilisés, 8 gaspillés
# Séquence 2 : [██████░░░░░░░░░░░░░░░░░░] 6 tokens utilisés, 18 gaspillés
# → Fragmentation énorme !

# PagedAttention
# Séquence 1 : [████|████|████|████]      4 blocs de 4 tokens chacun
# Séquence 2 : [████|██░░]                1.5 blocs
# → Zero gaspillage, allocation précise

Continuous Batching

Batching Statique vs Dynamique

# Batching statique (classique)

def static_batching(requests):
    """
    Attend d'avoir un batch complet avant de traiter
    """
    batch = []

    while len(batch) < BATCH_SIZE:
        batch.append(wait_for_request())  # Attend...

    # Traite tout le batch
    # Problème : Certaines requêtes finissent plus tôt
    # → GPU idle en attendant la plus longue

    results = model.generate(batch, max_length=512)
    return results

# Continuous batching (vLLM)

def continuous_batching(scheduler):
    """
    Ajoute/retire des requêtes dynamiquement
    """
    active_batch = []

    while True:
        # Ajoute de nouvelles requêtes si de la place
        while scheduler.has_pending() and can_fit_more():
            active_batch.append(scheduler.get_next())

        # Génère 1 token pour toutes les requêtes actives
        outputs = model.generate_one_token(active_batch)

        # Retire les requêtes terminées
        active_batch = [
            req for req in active_batch
            if not req.is_finished(outputs)
        ]

        # GPU toujours occupé à 100% !

Avantage clé : Le GPU génère continuellement, jamais idle.

Installation et démarrage rapide

Installation

# Installation via pip
pip install vllm

# Ou depuis source pour dernières features
git clone https://github.com/vllm-project/vllm.git
cd vllm
pip install -e .

# Vérifier CUDA
python -c "import torch; print(torch.cuda.is_available())"

Inférence Python

from vllm import LLM, SamplingParams

# Initialiser le modèle
llm = LLM(
    model="meta-llama/Llama-2-7b-chat-hf",
    tensor_parallel_size=1,  # Nombre de GPUs
    gpu_memory_utilization=0.9,  # Utilise 90% de la VRAM
    max_model_len=4096  # Context window
)

# Paramètres de génération
sampling_params = SamplingParams(
    temperature=0.7,
    top_p=0.9,
    max_tokens=256,
    stop=["</s>"]
)

# Prompts
prompts = [
    "Explique la relativité en termes simples :",
    "Écris une fonction Python pour trier une liste :",
    "Quels sont les avantages de l'IA ?"
]

# Génération (batching automatique !)
outputs = llm.generate(prompts, sampling_params)

# Afficher les résultats
for output in outputs:
    prompt = output.prompt
    generated_text = output.outputs[0].text
    print(f"Prompt: {prompt}")
    print(f"Réponse: {generated_text}\n")

Performance :

  • Llama-2-7B sur 1x A100
  • Batch de 100 requêtes : ~150 tokens/sec
  • vs ~15-20 tokens/sec avec HuggingFace

Serveur API compatible OpenAI

# Lancer le serveur vLLM
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-2-7b-chat-hf \
    --host 0.0.0.0 \
    --port 8000 \
    --tensor-parallel-size 1

# Le serveur expose une API compatible OpenAI !

Client Python :

from openai import OpenAI

# Pointer vers votre serveur vLLM local
client = OpenAI(
    base_url="http://localhost:8000/v1",
    api_key="not-needed"  # vLLM ne nécessite pas de clé
)

# Utiliser comme l'API OpenAI standard
response = client.chat.completions.create(
    model="meta-llama/Llama-2-7b-chat-hf",
    messages=[
        {"role": "system", "content": "Tu es un assistant utile."},
        {"role": "user", "content": "Explique les trous noirs."}
    ],
    temperature=0.7,
    max_tokens=300
)

print(response.choices[0].message.content)

Avantage : Remplacez base_url et vos apps existantes fonctionnent immédiatement !

# Migration d'OpenAI vers vLLM local

# Avant
client = OpenAI(api_key="sk-...")  # API OpenAI

# Après (zero code change)
client = OpenAI(
    base_url="http://your-vllm-server:8000/v1",
    api_key="not-needed"
)

# Tout le reste du code reste identique !

Configuration avancée

Optimisation GPU

from vllm import LLM, SamplingParams

llm = LLM(
    model="mistralai/Mixtral-8x7B-Instruct-v0.1",

    # Parallélisme tensoriel (multi-GPU)
    tensor_parallel_size=4,  # Distribue le modèle sur 4 GPUs

    # Utilisation mémoire
    gpu_memory_utilization=0.95,  # Utilise 95% de la VRAM

    # Context window
    max_model_len=32768,  # Mixtral supporte 32K tokens

    # Quantization
    quantization="awq",  # Active AWQ 4-bit

    # Optimisations
    enforce_eager=False,  # Utilise CUDA graphs (plus rapide)
    enable_prefix_caching=True,  # Cache les préfixes communs
)

Multi-GPU et Tensor Parallelism

# Distribuer un gros modèle sur plusieurs GPUs

# Llama-2-70B nécessite ~140 GB en FP16
# → Ne tient pas sur 1 GPU A100 (80 GB)

llm = LLM(
    model="meta-llama/Llama-2-70b-chat-hf",
    tensor_parallel_size=2,  # Distribue sur 2x A100
    dtype="float16",
    gpu_memory_utilization=0.9
)

# vLLM découpe automatiquement :
# - GPU 0 : Couches 0-39
# - GPU 1 : Couches 40-79
# Communication automatique via NCCL

Configuration optimale par modèle :

ModèleTailleGPUs RecommandésConfig
Llama-2-7B~14 GB1x A10/A100tensor_parallel_size=1
Llama-2-13B~26 GB1x A100tensor_parallel_size=1
Llama-2-70B~140 GB2x A100 80GBtensor_parallel_size=2
Mixtral 8x7B~90 GB2x A100tensor_parallel_size=2
Llama-3.1-405B~810 GB8x A100tensor_parallel_size=8

Quantization intégrée

# vLLM supporte plusieurs méthodes de quantization

# AWQ (4-bit)
llm = LLM(
    model="TheBloke/Llama-2-7B-Chat-AWQ",
    quantization="awq",
    dtype="float16"
)

# GPTQ (4-bit)
llm = LLM(
    model="TheBloke/Llama-2-7B-Chat-GPTQ",
    quantization="gptq",
    dtype="float16"
)

# SqueezeLLM
llm = LLM(
    model="squeeze-ai-lab/sq-llama-2-7b-w4a16",
    quantization="squeezellm",
    dtype="float16"
)

# Économie mémoire : 4-bit ≈ 4x moins de VRAM
# Llama-2-70B : ~140 GB → ~35 GB avec AWQ !

Prefix Caching

# Économiser du calcul pour prompts avec préfixes communs

llm = LLM(
    model="meta-llama/Llama-2-7b-chat-hf",
    enable_prefix_caching=True  # Active le cache de préfixes
)

# Système prompt commun
system_prompt = """Tu es un expert en Python.
Réponds toujours avec du code propre et documenté."""

prompts = [
    f"{system_prompt}\n\nÉcris une fonction de tri.",
    f"{system_prompt}\n\nÉcris une fonction de recherche.",
    f"{system_prompt}\n\nÉcris une fonction de hash."
]

# vLLM détecte le préfixe commun (system_prompt)
# → Calcule KV cache UNE SEULE FOIS
# → Partage entre les 3 requêtes
# → Accélération massive !

outputs = llm.generate(prompts, sampling_params)

Économie :

  • Sans prefix caching : 3 × coût system_prompt
  • Avec prefix caching : 1 × coût system_prompt + 3 × coût requête unique
  • Gain : ~2-3x pour prompts longs

Benchmarks et performances

Comparaison Throughput

# Benchmark vLLM vs HuggingFace vs TensorRT-LLM

import time
from vllm import LLM, SamplingParams

def benchmark(framework, model_name, num_requests=100):
    """
    Benchmark throughput
    """
    prompts = [
        "Écris une histoire courte sur l'IA."
    ] * num_requests

    start = time.time()

    if framework == "vllm":
        llm = LLM(model=model_name)
        outputs = llm.generate(prompts, SamplingParams(max_tokens=100))

    elif framework == "huggingface":
        # Implémentation HuggingFace standard
        model = AutoModelForCausalLM.from_pretrained(model_name)
        tokenizer = AutoTokenizer.from_pretrained(model_name)

        for prompt in prompts:
            inputs = tokenizer(prompt, return_tensors="pt")
            outputs = model.generate(**inputs, max_new_tokens=100)

    end = time.time()

    throughput = num_requests / (end - start)
    return throughput

# Résultats (Llama-2-7B sur A100)
results = {
    "vLLM": benchmark("vllm", "meta-llama/Llama-2-7b-hf"),
    "HuggingFace": benchmark("huggingface", "meta-llama/Llama-2-7b-hf"),
}

print(f"vLLM: {results['vLLM']:.1f} req/s")
print(f"HuggingFace: {results['HuggingFace']:.1f} req/s")
print(f"Speedup: {results['vLLM'] / results['HuggingFace']:.1f}x")

# Résultats typiques :
# vLLM: 120-150 req/s
# HuggingFace: 8-12 req/s
# Speedup: 10-15x

Résultats réels

Configuration : Llama-2-13B, A100 80GB, batch requests variées

MétriqueHuggingFaceText-Gen-InferencevLLM
Throughput12 req/s45 req/s142 req/s
Latence P50850ms320ms180ms
Latence P992100ms780ms420ms
GPU Util35%62%88%
Max Batch832128

Observations :

  • vLLM : 3x faster que Text-Gen-Inference
  • vLLM : 12x faster que HuggingFace
  • Utilisation GPU quasi-optimale

Scaling multi-GPU

# Performance scaling avec tensor parallelism

# Llama-2-70B sur différentes configs GPU

configs = {
    "1x A100": {
        "impossible": "Modèle trop gros (140 GB > 80 GB)"
    },
    "2x A100": {
        "throughput": "45 req/s",
        "latency_p50": "280ms",
        "cost_per_hour": "$6.00"
    },
    "4x A100": {
        "throughput": "82 req/s",  # Pas 2x (overhead communication)
        "latency_p50": "160ms",
        "cost_per_hour": "$12.00"
    },
    "8x A100": {
        "throughput": "135 req/s",
        "latency_p50": "95ms",
        "cost_per_hour": "$24.00"
    }
}

# Scaling efficiency
# 2→4 GPUs : 1.82x throughput (91% efficiency)
# 4→8 GPUs : 1.65x throughput (82% efficiency)

# Sweet spot : 2-4 GPUs pour la plupart des cas

Déploiement en production

Docker Deployment

# Dockerfile pour serveur vLLM

FROM nvidia/cuda:12.1.0-devel-ubuntu22.04

# Installer Python et dépendances
RUN apt-get update && apt-get install -y \
    python3.10 \
    python3-pip \
    git

# Installer vLLM
RUN pip install vllm

# Télécharger le modèle (optionnel, peut être fait au runtime)
RUN python3 -c "from huggingface_hub import snapshot_download; \
    snapshot_download('meta-llama/Llama-2-7b-chat-hf')"

# Exposer le port
EXPOSE 8000

# Commande de démarrage
CMD ["python3", "-m", "vllm.entrypoints.openai.api_server", \
     "--model", "meta-llama/Llama-2-7b-chat-hf", \
     "--host", "0.0.0.0", \
     "--port", "8000"]

Build et run :

# Build l'image
docker build -t vllm-server .

# Run avec GPU
docker run --gpus all -p 8000:8000 vllm-server

# Test
curl http://localhost:8000/v1/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "meta-llama/Llama-2-7b-chat-hf",
    "prompt": "Bonjour, comment vas-tu ?",
    "max_tokens": 100
  }'

Kubernetes Deployment

# vllm-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-llama2
spec:
  replicas: 2  # 2 instances pour load balancing
  selector:
    matchLabels:
      app: vllm-llama2
  template:
    metadata:
      labels:
        app: vllm-llama2
    spec:
      containers:
      - name: vllm
        image: vllm-server:latest
        ports:
        - containerPort: 8000
        resources:
          limits:
            nvidia.com/gpu: 1  # 1 GPU par pod
            memory: "32Gi"
          requests:
            nvidia.com/gpu: 1
            memory: "16Gi"
        env:
        - name: CUDA_VISIBLE_DEVICES
          value: "0"
        args:
        - "--model"
        - "meta-llama/Llama-2-7b-chat-hf"
        - "--tensor-parallel-size"
        - "1"
        - "--gpu-memory-utilization"
        - "0.9"

---
apiVersion: v1
kind: Service
metadata:
  name: vllm-service
spec:
  selector:
    app: vllm-llama2
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8000
  type: LoadBalancer

Deploy :

kubectl apply -f vllm-deployment.yaml

# Vérifier
kubectl get pods
kubectl get svc

# Accéder au service
export SERVICE_IP=$(kubectl get svc vllm-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}')

curl http://$SERVICE_IP/v1/models

Monitoring et observability

# vLLM expose des métriques Prometheus

from prometheus_client import start_http_server, Counter, Histogram

# Métriques custom
request_counter = Counter('vllm_requests_total', 'Total requests')
latency_histogram = Histogram('vllm_latency_seconds', 'Request latency')

# Middleware pour tracking
class MonitoredVLLM:
    def __init__(self, model_name):
        self.llm = LLM(model=model_name)

    @latency_histogram.time()
    def generate(self, prompts, sampling_params):
        request_counter.inc(len(prompts))
        outputs = self.llm.generate(prompts, sampling_params)
        return outputs

# Démarrer serveur Prometheus
start_http_server(9090)

# vLLM natif expose aussi :
# - vllm_num_requests_running
# - vllm_num_requests_waiting
# - vllm_gpu_cache_usage_perc
# - vllm_num_preemptions_total

# Scrape config Prometheus
"""
scrape_configs:
  - job_name: 'vllm'
    static_configs:
      - targets: ['localhost:8000']
"""

Dashboard Grafana :

{
  "panels": [
    {
      "title": "Requests per Second",
      "targets": [{
        "expr": "rate(vllm_requests_total[1m])"
      }]
    },
    {
      "title": "P95 Latency",
      "targets": [{
        "expr": "histogram_quantile(0.95, vllm_latency_seconds)"
      }]
    },
    {
      "title": "GPU Cache Usage",
      "targets": [{
        "expr": "vllm_gpu_cache_usage_perc"
      }]
    }
  ]
}

Cas d’usage avancés

RAG avec vLLM

from vllm import LLM, SamplingParams
from sentence_transformers import SentenceTransformer
import numpy as np

class RAGSystem:
    """
    Système RAG optimisé avec vLLM
    """

    def __init__(self, llm_model="meta-llama/Llama-2-7b-chat-hf"):
        # Embeddings
        self.embedder = SentenceTransformer('all-MiniLM-L6-v2')

        # vLLM pour génération
        self.llm = LLM(
            model=llm_model,
            gpu_memory_utilization=0.9,
            enable_prefix_caching=True  # Cache le contexte récupéré
        )

        self.sampling_params = SamplingParams(
            temperature=0.7,
            max_tokens=512
        )

        # Documents (simulation)
        self.documents = []
        self.doc_embeddings = None

    def add_documents(self, docs):
        """Ajoute des documents à la base"""
        self.documents.extend(docs)
        self.doc_embeddings = self.embedder.encode(docs)

    def retrieve(self, query, top_k=5):
        """Récupère les documents pertinents"""
        query_emb = self.embedder.encode([query])[0]

        # Similarité cosinus
        similarities = np.dot(self.doc_embeddings, query_emb)
        top_indices = np.argsort(similarities)[-top_k:][::-1]

        return [self.documents[i] for i in top_indices]

    def answer(self, questions):
        """
        Répond à plusieurs questions (batching automatique)
        """
        prompts = []

        for question in questions:
            # Récupérer contexte
            context_docs = self.retrieve(question, top_k=3)
            context = "\n\n".join(context_docs)

            # Construire prompt
            prompt = f"""Contexte :
{context}

Question : {question}

Réponds en te basant uniquement sur le contexte fourni."""

            prompts.append(prompt)

        # vLLM batch automatiquement
        outputs = self.llm.generate(prompts, self.sampling_params)

        return [output.outputs[0].text for output in outputs]

# Utilisation
rag = RAGSystem()

# Ajouter documents
rag.add_documents([
    "Paris est la capitale de la France.",
    "La Tour Eiffel mesure 330 mètres.",
    "Le Louvre est le plus grand musée du monde."
])

# Poser plusieurs questions en parallèle
questions = [
    "Quelle est la capitale de la France ?",
    "Quelle est la hauteur de la Tour Eiffel ?",
    "Quel est le plus grand musée ?"
]

answers = rag.answer(questions)

for q, a in zip(questions, answers):
    print(f"Q: {q}\nA: {a}\n")

Multi-LoRA Serving

vLLM peut servir plusieurs adaptateurs LoRA depuis un seul modèle de base.

from vllm import LLM, SamplingParams
from vllm.lora.request import LoRARequest

# Modèle de base
llm = LLM(
    model="meta-llama/Llama-2-7b-hf",
    enable_lora=True,  # Active le support LoRA
    max_lora_rank=64
)

# Plusieurs adaptateurs LoRA pour différents domaines
lora_adapters = {
    "legal": LoRARequest("legal-lora", 1, "/path/to/legal-lora"),
    "medical": LoRARequest("medical-lora", 2, "/path/to/medical-lora"),
    "code": LoRARequest("code-lora", 3, "/path/to/code-lora"),
}

# Requêtes avec différents LoRA
prompts_and_loras = [
    ("Rédige un contrat de bail", lora_adapters["legal"]),
    ("Explique le diabète", lora_adapters["medical"]),
    ("Écris une fonction de tri", lora_adapters["code"]),
]

# vLLM charge/décharge dynamiquement les LoRA
outputs = llm.generate(
    [p for p, _ in prompts_and_loras],
    SamplingParams(max_tokens=200),
    lora_request=[lora for _, lora in prompts_and_loras]
)

# Un seul modèle de base, plusieurs spécialisations !
# Économie massive de mémoire GPU

Speculative Decoding

vLLM supporte le speculative decoding pour accélérer la génération.

from vllm import LLM, SamplingParams

# Modèle principal (gros, lent)
main_llm = LLM(
    model="meta-llama/Llama-2-70b-chat-hf",
    tensor_parallel_size=2
)

# Modèle draft (petit, rapide)
draft_llm = LLM(
    model="meta-llama/Llama-2-7b-chat-hf",
    tensor_parallel_size=1
)

# Speculative decoding :
# 1. Draft model génère K tokens rapidement
# 2. Main model vérifie en parallèle
# 3. Accepte les bons, rejette les mauvais
# 4. Continue avec les tokens acceptés

# Résultat : 2-3x speedup sur génération longue !

# Configuration
llm = LLM(
    model="meta-llama/Llama-2-70b-chat-hf",
    tensor_parallel_size=2,
    speculative_model="meta-llama/Llama-2-7b-chat-hf",
    num_speculative_tokens=5  # Génère 5 tokens spéculatifs
)

outputs = llm.generate(prompts, sampling_params)

# Accélération sans perte de qualité !

Comparaison avec alternatives

vLLM vs Text-Generation-Inference (TGI)

FonctionnalitévLLMTGI (HuggingFace)
PagedAttention✅ Oui❌ Non
Continuous Batching✅ Oui✅ Oui
Throughput⭐⭐⭐⭐⭐⭐⭐⭐⭐
Latence⭐⭐⭐⭐⭐⭐⭐⭐⭐
Support modèlesLargeTrès large
QuantizationAWQ, GPTQ, SqueezeLLMGPTQ, bitsandbytes
Multi-LoRA✅ Oui❌ Non
Prefix caching✅ Oui❌ Non
OpenAI API✅ Oui✅ Oui
Facilité setup⭐⭐⭐⭐⭐⭐⭐⭐⭐

Verdict : vLLM pour maximum performance, TGI pour facilité d’utilisation.

vLLM vs TensorRT-LLM

FonctionnalitévLLMTensorRT-LLM (NVIDIA)
Performance⭐⭐⭐⭐⭐⭐⭐⭐⭐
Flexibilité⭐⭐⭐⭐⭐⭐⭐⭐
SetupSimpleComplexe
GPU supportNVIDIANVIDIA uniquement
Open-source✅ Apache 2.0✅ Apache 2.0
PyTorch compat✅ Natif❌ Nécessite conversion

Verdict : vLLM pour développement rapide, TensorRT-LLM pour dernière performance absolue.

Limitations et considérations

Mémoire GPU minimum

# Exigences mémoire approximatives (FP16)

requirements = {
    "Llama-2-7B": {
        "model": "14 GB",
        "kv_cache": "6-10 GB (selon batch)",
        "total_min": "24 GB",
        "gpu_recommended": "A10/A100"
    },
    "Llama-2-13B": {
        "model": "26 GB",
        "kv_cache": "8-15 GB",
        "total_min": "40 GB",
        "gpu_recommended": "A100"
    },
    "Llama-2-70B": {
        "model": "140 GB",
        "kv_cache": "30-50 GB",
        "total_min": "180 GB",
        "gpu_recommended": "2x A100 80GB"
    }
}

# Si pas assez de VRAM :
# → Utiliser quantization (AWQ/GPTQ)
# → Réduire max_model_len
# → Réduire gpu_memory_utilization

Latence vs throughput

# Trade-off latence vs throughput

# Configuration low-latency (chatbot interactif)
llm_low_latency = LLM(
    model="meta-llama/Llama-2-7b-chat-hf",
    max_num_seqs=4,  # Petit batch
    gpu_memory_utilization=0.6
)
# → Latence : 80-120ms
# → Throughput : 20-30 req/s

# Configuration high-throughput (batch processing)
llm_high_throughput = LLM(
    model="meta-llama/Llama-2-7b-chat-hf",
    max_num_seqs=256,  # Gros batch
    gpu_memory_utilization=0.95
)
# → Latence : 300-500ms
# → Throughput : 150-200 req/s

# Choisir selon cas d'usage !

Support des modèles

vLLM supporte la plupart des architectures, mais pas toutes :

Supportés ✅ :

  • Llama 1/2/3 (toutes variantes)
  • Mistral, Mixtral
  • GPT-2, GPT-J, GPT-NeoX
  • OPT, BLOOM
  • Falcon, Baichuan, Qwen
  • Phi, Gemma

Non supportés ❌ :

  • Modèles multimodaux (vision + texte) - support limité
  • Certains modèles avec architectures custom

Ressources et outils

Documentation et tutoriels

# Documentation officielle
https://docs.vllm.ai/

# GitHub
https://github.com/vllm-project/vllm

# Discord communautaire
https://discord.gg/vllm

# Papier académique (PagedAttention)
https://arxiv.org/abs/2309.06180

Outils complémentaires

# vLLM + LangChain
from langchain.llms import VLLM

llm = VLLM(
    model="meta-llama/Llama-2-7b-chat-hf",
    trust_remote_code=True,
    max_new_tokens=512,
    temperature=0.7
)

# Utiliser dans chains LangChain
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate

prompt = PromptTemplate.from_template("Résume ce texte : {text}")
chain = LLMChain(llm=llm, prompt=prompt)

result = chain.run(text="Long texte à résumer...")

Benchmarking scripts

# Script de benchmark complet

import time
import numpy as np
from vllm import LLM, SamplingParams

def comprehensive_benchmark(
    model_name,
    num_prompts=100,
    prompt_length=50,
    output_length=100
):
    """
    Benchmark complet d'un modèle
    """
    # Générer prompts
    prompts = [
        f"Prompt {i}: " + "test " * prompt_length
        for i in range(num_prompts)
    ]

    # Initialiser vLLM
    llm = LLM(model=model_name, gpu_memory_utilization=0.9)
    sampling_params = SamplingParams(
        max_tokens=output_length,
        temperature=0.7
    )

    # Warmup
    _ = llm.generate(prompts[:5], sampling_params)

    # Benchmark
    latencies = []

    for i in range(0, num_prompts, 10):
        batch = prompts[i:i+10]

        start = time.time()
        outputs = llm.generate(batch, sampling_params)
        end = time.time()

        batch_latency = (end - start) / len(batch)
        latencies.append(batch_latency)

    return {
        "model": model_name,
        "throughput_req_s": num_prompts / sum(latencies),
        "latency_p50": np.percentile(latencies, 50),
        "latency_p95": np.percentile(latencies, 95),
        "latency_p99": np.percentile(latencies, 99),
    }

# Run benchmark
results = comprehensive_benchmark(
    "meta-llama/Llama-2-7b-chat-hf",
    num_prompts=100
)

print(f"""
Modèle : {results['model']}
Throughput : {results['throughput_req_s']:.1f} req/s
Latence P50 : {results['latency_p50']*1000:.0f}ms
Latence P95 : {results['latency_p95']*1000:.0f}ms
Latence P99 : {results['latency_p99']*1000:.0f}ms
""")

Conclusion

vLLM est devenu l’outil de référence pour servir des LLM en production :

Performances exceptionnelles :

  • 10-24x throughput vs solutions classiques
  • Utilisation GPU > 90%
  • Latence minimale

Innovations techniques :

  • PagedAttention : gestion mémoire révolutionnaire
  • Continuous batching : GPU toujours occupé
  • Prefix caching : économies massives

Production-ready :

  • API compatible OpenAI
  • Multi-GPU support
  • Monitoring intégré
  • Docker/K8s friendly
💡

Quand utiliser vLLM ?

Idéal si :

  • Vous servez des LLM en production
  • Vous avez besoin de throughput élevé
  • Vous voulez optimiser les coûts GPU
  • Vous utilisez Llama, Mistral, Mixtral, etc.

⚠️ Alternatives si :

  • Modèle très custom/expérimental → HuggingFace
  • Performance absolue critique → TensorRT-LLM
  • Développement rapide sans GPU → Text-Gen-Inference

Prochaines étapes

  1. Installer vLLM et tester avec votre modèle
  2. Benchmarker vs votre solution actuelle
  3. Optimiser la configuration (batch size, mémoire, etc.)
  4. Déployer en production avec monitoring

Pour aller plus loin :