Inférence GPU à la demande en Europe avec Terraform et Cloud Run

Comment nous avons mis en place une infrastructure GPU serverless pour la transcription vocale et la diarisation : à quoi ça ressemble, combien ça coûte, et ce qui n'a pas marché du premier coup.

Apr 10, 2026

Inférence GPU à la demande en Europe

Nous faisons tourner plusieurs modèles ML en production pour nos clients : Whisper pour la transcription vocale, un service de diarisation (identification des locuteurs), et un pipeline d'inférence multi-étapes. Ils traitent des messages vocaux, principalement depuis WhatsApp, pour des entreprises en Europe.

Un nœud GPU L4 allumé en permanence pour Whisper ou n'importe quel modèle 7B coûte environ 5 000 $/mois. Pendant un temps, c'est ce qu'on payait. Cet article raconte comment on a fait baisser ce chiffre.


Le point de départ

Le service Whisper était déployé sur un cluster GKE avec un nœud GPU dédié, provisionné 24h/24, indépendamment du trafic. La plupart du temps, personne n'envoyait de messages vocaux. Le service de diarisation (identification des locuteurs via sherpa-onnx) tournait en deux réplicas permanents sur le même cluster, en CPU uniquement.

Ces charges de travail sont intrinsèquement irrégulières : le trafic est concentré pendant les heures de bureau, puis tombe à quasi zéro la nuit. On avait besoin du GPU quand un utilisateur envoyait un message vocal, pas les 23 autres heures de la journée.

La réponse évidente, c'était le serverless : descendre à zéro quand il n'y a rien, démarrer à la demande.


Le passage à Cloud Run v2

Les deux services tournent maintenant sur Google Cloud Run v2. Whisper utilise un GPU NVIDIA L4 ; la diarisation est CPU-only mais utilise le même module Terraform par souci de cohérence.

La configuration clé :

scaling {
  min_instance_count = 0   # Aucune instance au repos
  max_instance_count = 2   # Limité par le quota GPU
}

resources {
  limits = {
    cpu              = "4000m"
    memory           = "16Gi"
    "nvidia.com/gpu" = "1"
  }
}

Les deux services descendent à zéro instance quand il n'y a pas de trafic et démarrent à la demande. Ils communiquent en gRPC.

Nous avons construit un module Terraform unique (cloud_run_gpu) qui gère à la fois les déploiements GPU et CPU-only. Ajouter un nouveau service représente un bloc d'environ 20 lignes dans le fichier d'environnement.

Cette partie était assez directe. La première vraie contrainte est venue de la région.


Disponibilité GPU en Europe

Notre infrastructure principale tourne dans europe-west9 (Paris), mais les GPU L4 sur Cloud Run n'y étaient pas disponibles. Nous avons dû déployer les services GPU dans europe-west1 (Belgique) et relier les deux avec un VPC Serverless Connector.

Ce n'est pas un problème majeur opérationnellement, mais c'est le genre de chose qu'on veut vérifier avant de concevoir le reste de l'architecture. La disponibilité GPU en Europe reste limitée par rapport aux US.

La région réglée, la question suivante était la performance : si le service démarre de zéro, combien de temps l'utilisateur attend-il ?


Démarrage à froid

Quand une instance Cloud Run avec GPU démarre de zéro, elle doit initialiser CUDA, charger le modèle en mémoire GPU (Whisper large-v3 fait ~3 Go), et passer les health checks. D'après nos logs de production, ça prend systématiquement environ 30 secondes, du "Starting new instance" au passage de la startup probe. La diarisation (CPU-only, modèles ONNX) est autour de 10 secondes.

Mieux que prévu, mais pour y arriver, il a fallu séparer les startup probes des liveness probes. Notre première configuration avait un délai initial court et un seuil de tolérance bas. La liveness probe tuait le conteneur avant que le modèle ait fini de se charger :

startup_probe {
  grpc { port = 50051 }
  initial_delay_seconds = 30
  period_seconds        = 10
  failure_threshold     = 30   # Tolère jusqu'à ~300s d'initialisation
}

liveness_probe {
  grpc { port = 50051 }
  initial_delay_seconds = 10
  period_seconds        = 5
  failure_threshold     = 3
}

La startup probe autorise jusqu'à 5 minutes, bien plus que les ~30 secondes réellement nécessaires, mais cette marge ne coûte rien et évite des arrêts intempestifs lors de démarrages occasionnellement plus lents. La liveness probe ne s'active qu'après le succès du démarrage.

Les démarrages à froid étaient le problème le plus visible, mais pas le seul. Voici ce qui est apparu en mettant tout ça en production.


Ce qui nous a aussi posé problème

Incompatibilité de protocole des health checks. Le service de diarisation expose à la fois un port HTTP (8080) et un port gRPC (50051). La sonde de santé interrogeait le port gRPC avec une requête HTTP. gRPC répond avec un préambule HTTP/2, que la sonde HTTP interprète comme invalide. Le service était constamment marqué comme défaillant et redémarré. La solution : déclarer le port HTTP en premier (les sondes Kubernetes utilisent par défaut le premier port) et utiliser des health checks gRPC pour les services gRPC.

Limites de quota GPU. Le quota GPU L4 de Cloud Run en Europe est plafonné à 2 instances par région sous le pool sans redondance zonale. Nous désactivons explicitement la redondance zonale dans Terraform. Sans ce flag, le plafond était plus bas. C'est suffisant pour notre charge actuelle, mais c'est un plafond dur.

gpu_zonal_redundancy_disabled = true

Dérive d'état Terraform. Les noms de révision Cloud Run et les digests d'image changent à chaque déploiement CI/CD (Cloud Build, déclenché par les releases GitHub). Nous avons dû dire à Terraform d'ignorer les changements d'image et d'empêcher le comportement create-before-destroy, sinon Terraform essaie de créer la nouvelle révision avant de supprimer l'ancienne, ce qui dépasse le quota GPU et fait échouer le déploiement.

lifecycle {
  create_before_destroy = false
  ignore_changes = [
    template[0].containers[0].image,
  ]
}

Mémoire pour les modèles ONNX. Le service de diarisation charge deux modèles (~250 Mo au total). On l'avait initialement déployé avec 512 Mo en staging et il s'est fait OOMKill. Pas d'erreur côté application, le conteneur disparaissait simplement. On est passé à 1 Go en staging, 4 Go en production. Les modèles ont besoin de bien plus de marge que ce que leur taille sur disque suggère.

Chaîne de timeouts. Whisper a un timeout Cloud Run d'une heure parce que les fichiers audio longs peuvent prendre plus de 20 minutes à transcrire. Mais le client gRPC en amont était configuré à 30 secondes par défaut. Les transcriptions échouaient silencieusement. Chaque maillon de la chaîne doit avoir des timeouts cohérents.

Tout ça réglé, voici à quoi ressemble le système aujourd'hui.


Fonctionnement de bout en bout

Un message vocal arrive via WhatsApp. Le bot de messagerie stocke l'audio dans GCS. Le classifieur appelle le service de diarisation (Cloud Run, CPU) pour identifier les locuteurs, puis appelle Whisper (Cloud Run, GPU) pour transcrire. La transcription est envoyée au service RAG pour l'embedding et le stockage.

Si les deux services sont à zéro instance, le premier message après une période d'inactivité prend environ 30 à 40 secondes (le démarrage à froid du GPU domine). Les messages suivants dans la même session touchent des instances chaudes et se terminent en quelques secondes. Pour du traitement asynchrone de messages vocaux, c'est un compromis acceptable.

Reste la question de départ : combien ça coûte maintenant ?


Ce que ça coûte maintenant

Pour rappel, les 5 000 $/mois pour un nœud GPU permanent. La diarisation sur GKE avec deux réplicas permanents ajoutait ~500 $/mois en plus, pour un service dont le trafic se concentre sur les heures de bureau et reste calme le reste du temps.

Après le passage à Cloud Run avec scale-to-zero, la diarisation est tombée à ~20 $/mois. Whisper ne facture que pendant les transcriptions, soit 2-3 heures par jour dans notre cas. On ne déploie pas non plus de services GPU en staging. Le staging pointe vers les endpoints de production, ce qui divise par deux l'utilisation du quota GPU et évite les coûts en double.

AvantAprès
Diarisation~500 $/mois (2 réplicas GKE, 24h/24)~20 $/mois (Cloud Run, scale-to-zero)
Whisper~5 000 $/mois (nœud GPU L4 permanent)Facturé à l'usage (~2-3 h/jour)
Type de GPUn/aNVIDIA L4 (europe-west1)
Instances GPU maxn/a2 (limite de quota régionale)
Démarrage à froid (GPU)Aucun (toujours allumé)~30s (d'après les logs de prod)
Démarrage à froid (CPU)Aucun (toujours allumé)~10s (d'après les logs de prod)
TerraformConfigs séparées par service1 module partagé

Ce qu'on ferait différemment

Pas grand-chose. On vérifierait la disponibilité GPU par région plus tôt. On partait du principe que notre région principale aurait des L4, et on a dû revoir le réseau quand ça n'a pas été le cas.

On mettrait aussi en place la séparation startup/liveness probe dès le premier jour au lieu de déboguer des faux positifs de health check après le déploiement.

Le reste a été itératif. L'infrastructure a évolué sur plusieurs mois d'utilisation en production, et la plupart des corrections décrites dans cet article viennent d'incidents réels.


Nous sommes une petite équipe qui construit des outils IA pour les entreprises. C'est un morceau de l'infrastructure derrière tout ça. Si c'est pertinent par rapport à ce sur quoi vous travaillez, n'hésitez pas à nous contacter.