Solution française • Hébergement souverain • Conformité européenne Blog IA souveraine

FinOps IA — optimiser les coûts GPU et d'inférence en entreprise

Le GPU est la ressource la plus chère de l'infrastructure IA — et souvent la moins bien gérée. Des études de terrain montrent que les clusters GPU d'entreprise fonctionnent en moyenne à 25-35% d'utilisation réelle, signifiant que 65 à 75% des coûts sont gaspillés. Ce guide FinOps IA détaille les coûts réels, les techniques d'optimisation qui fonctionnent en production, et les indicateurs pour piloter les dépenses GPU à la direction.

Coûts réels GPU H100 en France (2026)

Les coûts GPU varient considérablement selon le mode d'acquisition. Voici une analyse réaliste basée sur les grilles tarifaires des fournisseurs français et européens actifs en 2026.

Location GPU (cloud souverain français)

FournisseurGPUPrix/heure (€ HT)Prix/mois (€ HT)Prix/an (€ HT)
OVHcloud (FR)H100 PCIe 80GB~2.50–3.50~1 800–2 520~21 600–30 240
Scaleway (FR)H100 SXM5 80GB~3.50–4.50~2 520–3 240~30 240–38 880
Outscale / DassaultA100 80GB~1.80–2.50~1 296–1 800~15 552–21 600
Clever Cloud (FR)A100 40GB~1.20–1.80~864–1 296~10 368–15 552
30-40k€Coût/an location H100 (FR)
280-350k€Achat H100 SXM5 serveur complet
25-35%Utilisation GPU réelle moyenne
3 ansPoint mort achat vs location

Achat GPU (on-premise)

ConfigurationGPUPrix serveur complet (€)Durée amortissementCoût/an amorti
1× DGX H1008× H100 SXM5 80GB~280 000–350 0005 ans56 000–70 000
Custom HGX H1004× H100 SXM5 80GB~140 000–180 0005 ans28 000–36 000
Serveur A100 PCIe4× A100 80GB~80 000–110 0005 ans16 000–22 000
Serveur L40S4× L40S 48GB~50 000–70 0004 ans12 500–17 500

À ces coûts d'achat matériel, il faut ajouter : espace datacenter (300-500 W/GPU, soit ~2000 W pour un serveur 4× GPU → ~2 100 €/an d'électricité à 0.12 €/kWh), maintenance et support constructeur (~10% du prix d'achat/an), et colocation ou refroidissement si on-premise (~500-1000 €/mois selon prestataire).

TCO comparé sur 3 ans

# tco_calculator.py — Calculateur TCO GPU sur 3 ans

def calculate_tco_3y(
    num_gpus: int,
    gpu_model: str = "H100",
    mode: str = "cloud",  # "cloud", "on-premise"
    utilization_pct: float = 0.70  # Utilisation visée
):
    """Calcule le TCO sur 3 ans pour une infrastructure GPU LLM."""
    
    costs = {
        "H100": {
            "cloud_per_gpu_year": 32000,  # OVHcloud/Scaleway moy.
            "onprem_server_cost": 165000,  # 4× H100 SXM5 serveur
            "onprem_gpus_per_server": 4,
        },
        "A100": {
            "cloud_per_gpu_year": 18000,
            "onprem_server_cost": 90000,
            "onprem_gpus_per_server": 4,
        }
    }
    c = costs[gpu_model]
    
    if mode == "cloud":
        compute_3y = num_gpus * c["cloud_per_gpu_year"] * 3
        ops_3y = 0  # Inclus dans le cloud
        total = compute_3y
    else:  # on-premise
        servers_needed = (num_gpus + c["onprem_gpus_per_server"] - 1) // c["onprem_gpus_per_server"]
        capex = servers_needed * c["onprem_server_cost"]
        electricity_year = num_gpus * 300 * 8760 / 1000 * 0.12  # ~kWh × tarif
        ops_year = capex * 0.12  # Maintenance 12%/an
        datacenter_year = servers_needed * 8000  # Colo/refroidissement
        opex_3y = (electricity_year + ops_year + datacenter_year) * 3
        total = capex + opex_3y
    
    cost_per_gpu_year = total / (num_gpus * 3)
    
    return {
        "total_3y_eur": round(total),
        "cost_per_gpu_year": round(cost_per_gpu_year),
        "cost_per_gpu_month": round(cost_per_gpu_year / 12)
    }

# Exemples comparatifs
print("=== TCO 4× H100 sur 3 ans ===")
print("Cloud FR:", calculate_tco_3y(4, "H100", "cloud"))
print("On-premise:", calculate_tco_3y(4, "H100", "on-premise"))

# Cloud FR:      {'total_3y_eur': 384000, 'cost_per_gpu_year': 32000, ...}
# On-premise:    {'total_3y_eur': 312000, 'cost_per_gpu_year': 26000, ...}
# → Achat rentable dès l'année 3 à utilisation > 70%

Quantization : INT8/INT4 — gains réels et pertes de qualité

La quantization réduit la précision numérique des poids du modèle pour diminuer la consommation VRAM et accélérer l'inférence. C'est la technique d'optimisation au meilleur ratio impact/effort pour les équipes.

PrécisionVRAM ELODIE 32BDébit (tok/s) 1× H100Perte qualité (MMLU)Recommandation
BF16 (référence)64 GB~45 tok/s0% (baseline)Dev/évaluation
INT8 (bitsandbytes)32 GB~60 tok/s (+33%)-0.5 à -1.5%Production standard
GPTQ INT4 (4-bit)18 GB~90 tok/s (+100%)-1 à -3%Production coûts serrés
AWQ INT4 (4-bit)18 GB~95 tok/s (+111%)-0.5 à -2%Production recommandé INT4
GGUF Q5_K_M22 GB~40 tok/s (-11%)-0.3 à -1%CPU/hybride CPU-GPU
# Quantization AWQ INT4 avec autoawq
pip install autoawq

python3 - <<'EOF'
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer

model_path = "/models/elodie-32b-bf16"
quant_path = "/models/elodie-32b-awq-int4"

quant_config = {
    "zero_point": True,
    "q_group_size": 128,
    "w_bit": 4,
    "version": "GEMM"  # GEMM optimisé pour inférence
}

tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoAWQForCausalLM.from_pretrained(
    model_path,
    low_cpu_mem_usage=True,
    use_cache=False
)

# Calibration sur données représentatives
calib_data = [
    "Résume ce document juridique : ...",
    "Analyse ce rapport financier : ...",
    # 128 exemples représentatifs de vos cas d'usage
]

model.quantize(tokenizer, quant_config=quant_config, calib_data=calib_data)
model.save_quantized(quant_path)
tokenizer.save_pretrained(quant_path)
print(f"Modèle quantisé sauvegardé dans {quant_path}")
EOF

# Démarrage vLLM avec modèle AWQ
python -m vllm.entrypoints.openai.api_server \
  --model /models/elodie-32b-awq-int4 \
  --quantization awq \
  --dtype float16 \
  --max-model-len 8192 \
  --tensor-parallel-size 1 \
  --gpu-memory-utilization 0.85

Validation qualité obligatoire

Ne déployez jamais un modèle quantisé en production sans le valider sur vos cas d'usage réels. La perte sur MMLU (benchmark généraliste) peut masquer des dégradations plus sévères sur des domaines spécifiques (juridique, médical, technique). Utilisez un ensemble de test métier représentatif et mesurez la qualité avec au moins 3 évaluateurs humains sur 100 questions clés avant tout passage en production.

Batching dynamique : jusqu'à +3x throughput

Le continuous batching (batching dynamique) est la fonctionnalité qui a révolutionné l'efficacité des serveurs LLM. Au lieu de traiter une requête à la fois ou d'attendre un batch complet, vLLM insère dynamiquement de nouvelles requêtes dans les slots libérés par les requêtes terminées — multipliant le débit par 2 à 4×.

SANS continuous batching (approche naïve) :

Temps → 0  1  2  3  4  5  6  7  8  9  10
Slot1 : [===REQ1===] [===REQ3===]
Slot2 : [==REQ2==]   [====REQ4====]
         ↑ vide ↑
         Gaspillage

AVEC continuous batching (vLLM) :

Temps → 0  1  2  3  4  5  6  7  8  9  10
Slot1 : [===REQ1===][REQ5][====REQ7====]
Slot2 : [==REQ2==][==REQ6==][=REQ8=]
Slot3 : [REQ3][====REQ9====]
         ↑ Zéro gaspillage ↑
# Configuration vLLM optimisée pour le batching
# Les paramètres clés pour maximiser le débit

python -m vllm.entrypoints.openai.api_server \
  --model /models/elodie-32b \
  --max-num-seqs 256 \
  # Nombre max de séquences simultanées dans le batch
  --max-num-batched-tokens 32768 \
  # Tokens max traités en un seul batch (prompt+génération)
  --enable-chunked-prefill \
  # Découpe les longs prefills pour éviter les blocages
  --scheduler-delay-factor 0.05 \
  # Délai d'attente (ms) pour agréger les requêtes
  --enable-prefix-caching \
  # Cache les prefixes communs (system prompts)
  --max-paddings 256 \
  # Padding max pour alignement batch
  --block-size 16 \
  # Taille des blocs KV cache (16 = bon compromis)
  --swap-space 4 \
  # GB de RAM CPU pour le KV swap (overflow VRAM)

Benchmark batching : impact mesurable

# benchmark_batching.py — Mesure de l'impact du batching
import asyncio
import httpx
import time

async def send_concurrent_requests(n: int, endpoint: str, api_key: str):
    """Envoie N requêtes simultanées et mesure le débit total."""
    prompts = [f"Explique le concept numéro {i} en 100 mots." for i in range(n)]
    
    start = time.perf_counter()
    async with httpx.AsyncClient(timeout=120) as client:
        tasks = [
            client.post(
                f"{endpoint}/v1/completions",
                json={"model": "elodie-32b", "prompt": p, "max_tokens": 100},
                headers={"Authorization": f"Bearer {api_key}"}
            )
            for p in prompts
        ]
        responses = await asyncio.gather(*tasks)
    
    elapsed = time.perf_counter() - start
    total_tokens = sum(
        r.json()["usage"]["total_tokens"]
        for r in responses if r.status_code == 200
    )
    
    print(f"Concurrence: {n} requêtes")
    print(f"Temps total: {elapsed:.1f}s")
    print(f"Débit: {total_tokens/elapsed:.0f} tokens/s")
    print(f"Tokens par requête: {total_tokens/n:.0f}")

# Test avec différents niveaux de concurrence
for concurrency in [1, 4, 16, 32, 64]:
    asyncio.run(send_concurrent_requests(concurrency,
      "http://vllm-service", "key"))

KV Cache et Prefix Caching

Le KV (Key-Value) cache stocke les représentations intermédiaires de l'attention calculées lors du prefill — évitant de les recalculer pour chaque token généré. Le Prefix Caching va plus loin : il réutilise ces représentations entre plusieurs requêtes partageant le même début de prompt (typiquement le system prompt).

# Mesure de l'impact du prefix caching
# Exemple : system prompt de 500 tokens partagé par toutes les requêtes

SYSTEM_PROMPT = """Tu es ELODIE, l'assistant IA souverain d'Intelligence Privée.
Tu analyses uniquement les documents internes de l'entreprise.
Tu réponds toujours en français, de manière professionnelle et précise.
Tu ne divulgues jamais d'informations confidentielles en dehors du contexte autorisé.
[... 400 tokens supplémentaires de contexte système ...]
"""

# Sans prefix caching : 500 tokens recalculés à chaque requête
# Coût : 500 tokens × latence attention = ~50ms overhead par requête

# Avec prefix caching (--enable-prefix-caching) :
# Premier appel : 500 tokens calculés et mis en cache
# Appels suivants : 0 tokens recalculés → TTFT divisé par 2-3

# Estimation du gain :
# 1000 requêtes/jour × 50ms économisés = 50 secondes GPU libérées
# = ~1.4% de capacité GPU récupérée gratuitement

Right-sizing : quand le 32B suffit à la place du 70B

Un modèle 70B ne délivre pas systématiquement de meilleures performances qu'un 32B sur tous les cas d'usage. Le right-sizing permet d'utiliser le modèle le plus petit qui satisfait les exigences de qualité — divisant les coûts par 2 à 3.

Cas d'usageModèle minimum satisfaisantGain cost vs 70B
Résumé de documentELODIE 32B / Mistral 32B-60% VRAM, -55% coût
Q&A sur documents internes (RAG)ELODIE 32B-60% VRAM
Génération de code Python/SQLCode Llama 34B / ELODIE 32B-55% VRAM
Analyse juridique complexeLlama 3.3 70B / KEVINA 32Bvariable
Raisonnement multi-étapesLlama 3.3 70B ou Qwen2.5 72BN/A (70B nécessaire)
Traduction FR/ENMistral 7B / NLLB-3.3B-90% VRAM
Classification/extractionMistral 7B fine-tuné-90% VRAM
# router_llm.py — Routage automatique vers le bon modèle
from enum import Enum

class ModelTier(Enum):
    SMALL = "mistral-7b"    # Tâches simples
    MEDIUM = "elodie-32b"   # Majorité des usages
    LARGE = "llama-70b"     # Raisonnement complexe

def route_request(request: dict) -> ModelTier:
    """
    Sélectionne automatiquement le modèle selon la complexité estimée.
    Économise 40-60% des coûts GPU en moyenne.
    """
    messages = request.get("messages", [])
    last_message = messages[-1].get("content", "") if messages else ""
    
    # Tâches simples → modèle léger
    simple_keywords = [
        "résume", "traduis", "corrige", "reformule",
        "extrait", "classe", "catégorise"
    ]
    if any(kw in last_message.lower() for kw in simple_keywords):
        if len(last_message) < 500:
            return ModelTier.SMALL
    
    # Raisonnement complexe → grand modèle
    complex_keywords = [
        "analyse juridique", "stratégie", "raisonne",
        "compare et explique", "évalue les risques"
    ]
    if any(kw in last_message.lower() for kw in complex_keywords):
        return ModelTier.LARGE
    
    # Par défaut : modèle medium (bon compromis)
    return ModelTier.MEDIUM

Scheduling intelligent : batch la nuit, interactif le jour

# kubernetes-priority-classes.yaml
# Deux classes de priorité : interactive (haute) vs batch (basse)
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: llm-interactive
value: 1000
globalDefault: false
description: "Inférence interactive utilisateur — haute priorité"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: llm-batch
value: 100
globalDefault: false
description: "Jobs batch IA — basse priorité, préemptable"
---
# CronJob pour traitement batch nocturne (23h-7h)
apiVersion: batch/v1
kind: CronJob
metadata:
  name: llm-batch-processor
  namespace: llm-production
spec:
  schedule: "0 23 * * *"  # Démarrage à 23h00
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          priorityClassName: llm-batch
          containers:
          - name: batch-processor
            image: registry.interne/llm-batch:latest
            env:
            - name: VLLM_ENDPOINT
              value: http://vllm-service.llm-production
            - name: BATCH_MODE
              value: "true"
            - name: MAX_TOKENS_PER_DOC
              value: "4096"
            resources:
              requests:
                cpu: "4"
                memory: "8Gi"  # Pas de GPU alloué — utilise l'API vLLM

Chargeback par département

-- Rapport mensuel de chargeback IA par département
-- Exécuter le 1er du mois pour le mois précédent

WITH monthly_usage AS (
  SELECT
    department,
    model,
    COUNT(*) as request_count,
    SUM(prompt_tokens) as prompt_tokens_total,
    SUM(completion_tokens) as completion_tokens_total,
    SUM(prompt_tokens + completion_tokens) as total_tokens,
    AVG(latency_ms) as avg_latency_ms,
    PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY latency_ms) as p95_latency_ms
  FROM llm_usage_logs
  WHERE
    timestamp >= date_trunc('month', CURRENT_DATE - INTERVAL '1 month')
    AND timestamp < date_trunc('month', CURRENT_DATE)
  GROUP BY department, model
),
costs AS (
  SELECT
    department,
    model,
    request_count,
    total_tokens,
    avg_latency_ms,
    p95_latency_ms,
    ROUND(
      CASE model
        WHEN 'elodie-32b' THEN total_tokens * 0.000004  -- 0.004€/1k tokens
        WHEN 'kevina-32b' THEN total_tokens * 0.000004
        WHEN 'llama-70b'  THEN total_tokens * 0.000012
        ELSE total_tokens * 0.000005
      END, 2
    ) AS cost_eur
  FROM monthly_usage
)
SELECT
  department,
  SUM(request_count) as total_requests,
  SUM(total_tokens) as total_tokens,
  SUM(cost_eur) as total_cost_eur,
  ROUND(SUM(cost_eur) / SUM(request_count), 4) as avg_cost_per_request,
  ROUND(SUM(total_tokens) / SUM(request_count), 0) as avg_tokens_per_request
FROM costs
GROUP BY department
ORDER BY total_cost_eur DESC;

KPIs FinOps IA à reporter à la direction

KPIFormuleCibleFréquence
Coût par requête (€)coût_total / nb_requêtes< 0.01 €Hebdomadaire
Utilisation GPU (%)DCGM_FI_DEV_GPU_UTIL moyen> 60%Quotidien
Coût par utilisateur/mois (€)coût_total / DAU / 30< 15 €/userMensuel
Tokens per GPU-hourtotal_tokens / GPU-heures> 150k tok/GPU-hHebdomadaire
Taux idle GPU (%)temps GPU util.<10% / temps total< 20%Quotidien
ROI adoption (%)(bénéfices_productivité - coûts) / coûts> 150%Trimestriel
# Script de rapport FinOps hebdomadaire
#!/bin/bash
# Généré chaque lundi matin à 8h via CronJob

GPU_UTIL=$(kubectl exec -n monitoring deploy/prometheus -- \
  promtool query instant \
  'avg(avg_over_time(DCGM_FI_DEV_GPU_UTIL[7d]))' | \
  grep -oP '(?<=value: )\d+\.?\d*')

COST_WEEK=$(psql -U llm_tracker -t -c \
  "SELECT ROUND(SUM(cost_eur)::numeric, 2) FROM llm_usage_logs \
   WHERE timestamp > NOW() - INTERVAL '7 days'")

REQUESTS_WEEK=$(psql -U llm_tracker -t -c \
  "SELECT COUNT(*) FROM llm_usage_logs \
   WHERE timestamp > NOW() - INTERVAL '7 days'")

echo "=== RAPPORT FINOPS IA — $(date +%Y-%m-%d) ==="
echo "Utilisation GPU moyenne (7j) : ${GPU_UTIL}%"
echo "Coût total semaine : ${COST_WEEK}€"
echo "Requêtes traitées : ${REQUESTS_WEEK}"
echo "Coût par requête : $(echo "scale=4; $COST_WEEK / $REQUESTS_WEEK" | bc)€"

Ce qu'il faut retenir

  • La quantization AWQ INT4 est l'optimisation avec le meilleur ratio effort/gain : -50% VRAM, +100% débit, -1 à -2% qualité seulement.
  • Le continuous batching vLLM (activé par défaut) est responsable de 60% des gains de débit — mais il faut bien le configurer (max-num-seqs, max-num-batched-tokens).
  • Le routing multi-modèle (7B/32B/70B selon complexité) peut réduire les coûts de 40-60% avec un impact qualité transparent pour l'utilisateur.
  • L'utilisation GPU < 60% est un signal de sur-dimensionnement — consolidez vos workloads ou réduisez le nombre de GPU loués.
  • Sans chargeback par département, les équipes n'ont aucune incitation à optimiser leurs prompts. Facturez en interne pour responsabiliser.

Audit FinOps IA : trouvez vos économies cachées

Nos experts FinOps analysent votre infrastructure GPU, identifient les gisements d'optimisation et implementent les changements — quantization, batching, routing — avec garantie de résultat.

Demander mon audit FinOps IA →

FAQ

Quelle quantization choisir : GPTQ ou AWQ ?

AWQ (Activation-aware Weight Quantization) est généralement supérieure à GPTQ pour les modèles 32B+ en 2026. AWQ préserve mieux la qualité car elle tient compte de l'activation lors de la quantization (pas seulement les poids). Elle génère des modèles légèrement plus rapides à l'inférence sur H100. Utilisez AWQ avec w_bit=4, q_group_size=128 pour la meilleure qualité en INT4. GPTQ reste valide si vous avez déjà un pipeline existant.

Le swap KV cache (CPU offloading) est-il viable en production ?

Le swap KV cache (paramètre --swap-space dans vLLM) permet de décharger le KV cache de la VRAM vers la RAM CPU quand la VRAM est pleine. C'est viable pour les requêtes à très long contexte (32k+ tokens) dont vous pouvez accepter une latence plus élevée. En revanche, pour les conversations interactives, le swap ajoute 100-500ms de latence supplémentaire par décharge — inacceptable pour les utilisateurs. Utilisez-le exclusivement pour les jobs batch.

Comment justifier le FinOps IA auprès du ComEx ?

Présentez trois métriques : (1) coût par utilisateur actif/mois comparé à la valeur produite (économie en heures-homme), (2) taux d'utilisation GPU vs baseline cloud (prouvez que vous faites mieux que la moyenne), (3) trajectoire ROI sur 24 mois avec les optimisations planifiées. Évitez les métriques purement techniques (tokens/s) — traduisez toujours en impact business mesurable.