Bonnes pratiques pour l'inférence par lot sur GKE

Ce document présente les bonnes pratiques pour exécuter des charges de travail d'inférence par lot sur Google Kubernetes Engine (GKE). L'inférence par lot consiste à utiliser un modèle de machine learning pour générer des prédictions sur de grands ensembles de données, en privilégiant le haut débit et la rentabilité plutôt que les réponses immédiates à faible latence.

Ce guide distingue l'inférence par lot du traitement par lot des requêtes (ou traitement par lot dynamique), une technique côté serveur utilisée dans des moteurs tels que vLLM ou SGLang, qui regroupe les requêtes simultanées en temps réel pour optimiser l'efficacité de l'accélérateur. Vous pouvez appliquer le traitement par lot des requêtes aux charges de travail d'inférence par lot.

Les bonnes pratiques de ce guide couvrent deux types courants de modèles d'inférence par lot :

  • Inférence par lot en temps quasi réel : traite les données par blocs peu de temps après leur génération. Avec une latence typique de quelques secondes à quelques minutes, cette approche équilibre le besoin de données récentes avec l'efficacité du traitement simultané de plusieurs éléments.
  • Inférence par lot hors connexion : traite de grands volumes de données accumulées à des intervalles planifiés (par exemple, toutes les nuits ou toutes les semaines). La latence varie généralement de quelques heures à quelques jours, car ces jobs sont souvent planifiés pendant les périodes creuses pour maximiser la disponibilité des ressources.

Ces recommandations constituent une couche d'optimisation spécialisée basée sur les principes de base décrits dans la présentation des bonnes pratiques d'inférence sur GKE. Avant d'optimiser les charges de travail par lot, assurez-vous d'avoir suivi les bonnes pratiques de base pour la sélection de modèles, la quantification et le choix d'accélérateurs.

Choisir un modèle d'architecture pour le traitement de l'inférence par lot

Le choix du bon modèle architectural est la décision la plus importante à prendre pour déployer vos charges de travail d'inférence par lot, car il affecte le compromis entre latence, débit et coût. Pour maintenir l'efficacité, assurez-vous que votre débit d'inférence dépasse le taux de requêtes entrantes pendant les heures creuses afin d'éviter que les files d'attente ne s'allongent indéfiniment.

Utiliser l'inférence par lot en quasi-temps réel pour les charges de travail par rafales

Le traitement par lot quasi en temps réel est adapté aux cas d'utilisation qui nécessitent des mises à jour fréquentes et incrémentielles, comme les suivants :

  • Mise à jour des profils de recommandation des utilisateurs toutes les quelques minutes en fonction des interactions récentes.
  • Traitement des mentions sur les réseaux sociaux à intervalles d'une minute pour une surveillance en temps réel.
  • Détecter les signaux de marché à partir de flux de données financières à haute fréquence.
  • Effectuer une analyse des sentiments sur les commentaires des clients ou les flux d'actualités entrants.

Choisissez ce modèle si votre charge de travail peut tolérer une latence allant de plusieurs secondes à quelques minutes.

Lorsque vous implémentez l'inférence par lot en temps quasi réel, tenez compte des caractéristiques suivantes :

  • Latence : le temps de latence du premier jeton peut varier de quelques dizaines de secondes à quelques minutes.
  • Sources de données : vous traitez généralement des ensembles de données allant de quelques mégaoctets à quelques gigaoctets, comme les messages de Pub/Sub ou les fichiers de Cloud Storage accumulés sur une courte période.
  • Modèle de calcul : votre infrastructure doit prendre en charge un service continu qui gère les pics de charge de travail fréquents.
  • Optimisation des coûts : ce modèle offre un équilibre entre l'inférence en temps réel à faible latence et le traitement hors connexion à haut débit.

Utiliser l'inférence par lot hors connexion pour les ensembles de données volumineux

Le traitement par lot hors connexion est idéal pour les jobs épisodiques à grande échelle qui peuvent tolérer des retards de plusieurs heures ou jours, comme les suivants :

  • Génération de rapports d'évaluation des risques nocturnes basés sur les transactions financières de la veille.
  • Créer des embeddings de produits pour l'ensemble d'un catalogue afin d'alimenter les systèmes de recherche et de recommandation en aval.
  • Étiqueter de grands ensembles de données d'images pour l'entraînement des modèles ou la catégorisation des archives.

Choisissez ce modèle si vous traitez de grands volumes de données et que vous pouvez tolérer des latences allant de quelques heures à plusieurs jours.

Lorsque vous implémentez l'inférence par lot hors connexion, tenez compte des caractéristiques suivantes :

  • Latence : la latence de démarrage des charges de travail varie généralement de quelques minutes à quelques jours, car les tâches sont souvent planifiées pendant les heures creuses.
  • Sources de données : vous traitez de grands ensembles de données (de gigaoctets à pétaoctets), généralement stockés dans des tables Cloud Storage ou BigQuery.
  • Schéma de calcul : vous utilisez des jobs épisodiques et ponctuels qui initialisent et traitent les données, puis se terminent.
  • Optimisation des coûts : ce modèle est hautement optimisable avec un modèle de paiement à l'utilisation. Étant donné que les jobs hors connexion ont des délais d'exécution flexibles, nous vous recommandons d'utiliser des VM Spot pour réduire les coûts.

Optimiser le débit et la rentabilité

Les charges de travail d'inférence par lot sont particulièrement adaptées aux infrastructures économiques qui peuvent impliquer des interruptions.

Utiliser des VM Spot pour réduire les coûts de calcul

Utilisez les remises sur les VM Spot pour les jobs par lot. Les charges de travail d'inférence par lot tolèrent généralement la latence et les interruptions. Elles sont donc de bons candidats pour les tarifs réduits de la capacité Spot.

Assurez-vous que votre code d'inférence par lot implémente la vérification des points de contrôle pour gérer les éventuels événements de préemption. Si une VM Spot est préemptée, vous pouvez créer un nœud et reprendre votre charge de travail à partir du dernier lot traité au lieu de recommencer à zéro.

Ajuster la taille des lots de charge de travail et de requête

Pour éviter la contention des ressources et les délais d'expiration des jobs, assurez-vous que le nombre d'éléments envoyés à votre moteur (lot de charge de travail) est au moins aussi important que le nombre de requêtes simultanées que le serveur peut traiter (lot de requêtes) afin d'éviter la sous-utilisation des accélérateurs.

Ajuster la taille du lot de votre charge de travail

La taille du lot de charge de travail correspond au nombre total d'éléments envoyés à votre moteur d'inférence dans une seule unité de travail. Pour ce faire, configurez la logique d'envoi de votre client ou la configuration du job Kubernetes en fragmentant vos données ou en regroupant plusieurs éléments dans une même requête.

Pour déterminer la taille de lot de charge de travail optimale, utilisez les limites suivantes :

  • Calculez la taille de lot minimale : assurez-vous que la taille de lot de votre charge de travail est au moins aussi importante que celle de votre requête. Par exemple, l'envoi d'un seul élément à un serveur pouvant traiter 256 éléments simultanément entraîne une sous-utilisation importante. Pour trouver votre taille minimale, vérifiez la configuration de votre serveur d'inférence, comme l'argument max_num_seqs dans vLLM. Vous pouvez configurer votre logique client pour regrouper plusieurs éléments dans une seule requête, ou vous pouvez fragmenter vos données afin que chaque job reçoive une quantité minimale de données qui correspond à la taille du lot de requêtes ou la dépasse.
  • Calculez la taille de lot maximale : assurez-vous que la taille de lot de votre charge de travail permet au pod de se terminer avant d'atteindre le délai d'expiration activeDeadlineSeconds défini dans votre tâche Kubernetes. Estimez le temps nécessaire pour traiter un lot de requêtes et définissez la taille de la charge de travail afin que le pod se termine bien avant la date limite. Par exemple, si votre activeDeadlineSeconds est de 3 600 secondes et que votre surcharge de démarrage est de 600 secondes, assurez-vous que la durée d'exécution maximale permet au pod de se terminer en moins de 3 000 secondes.

Si la taille du lot de votre charge de travail est trop petite, votre job perdra du temps en raison de la surcharge de démarrage du pod (téléchargement des poids, provisionnement, initialisation de l'accélérateur). Si elle est trop grande, vous risquez que le job soit arrêté par GKE en raison du délai d'attente activeDeadlineSeconds, ce qui entraînera l'échec du job et la perte de sa progression.

Ajuster la taille de votre lot de requêtes

La taille du lot de requêtes correspond au nombre de requêtes simultanées que le serveur d'inférence traite simultanément sur l'accélérateur. Vous pouvez optimiser ce paramètre en ajustant les indicateurs spécifiques au serveur dans la configuration de votre serveur d'inférence (par exemple, l'indicateur --max-num-seqs pour vLLM).

Votre objectif est de maximiser l'utilisation du GPU sans déclencher d'erreurs de mémoire insuffisante (OOM). Si la taille du lot de requêtes n'est pas calibrée, votre système sous-utilisera l'accélérateur ou plantera le serveur de modèles. Pour vLLM, vous pouvez utiliser des outils tels que le script vLLM auto_tune pour trouver les meilleures valeurs pour les paramètres max_num_seqs et max_num_batched_tokens pour votre matériel spécifique. Pour en savoir plus, consultez Optimiser la configuration de votre serveur d'inférence dans le guide "Présentation des bonnes pratiques d'inférence sur GKE".

Implémenter des composants asynchrones pour le traitement par lot en temps quasi réel

Pour le traitement par lot quasi en temps réel, nous vous recommandons d'utiliser des tampons de messagerie pour dissocier votre couche d'ingestion de votre couche d'inférence.

Le schéma d'architecture suivant illustre un exemple de plate-forme d'inférence par lot quasi en temps réel. Cette architecture protège les serveurs d'inférence contre les pics de trafic, gère les arriérés de travail et garantit une utilisation élevée des accélérateurs.

Le diagramme montre le flux de Pub/Sub vers les abonnés, une passerelle d'inférence et un serveur d'inférence, avec les résultats conservés dans AlloyDB et les messages ayant échoué envoyés à une rubrique de lettres mortes.

Plate-forme d'inférence par lot quasi en temps réel sur GKE.

L'architecture se compose des éléments suivants :

  • Le sujet Pub/Sub sert de tampon persistant pour les messages client entrants, avec une période de conservation de 7 à 31 jours.
  • Abonné : composant qui lit les lots de messages, envoie des requêtes au serveur d'inférence et confirme le traitement.
  • HPA de l'abonné : met à l'échelle le déploiement de l'abonné en fonction de la métrique num_undelivered_messages (nombre de messages non confirmés).
  • Stockage : conservez les résultats d'inférence à l'aide d'une base de données (telle qu'AlloyDB) ou d'un stockage d'objets (tel que Cloud Storage) .
  • Passerelle d'inférence : expose les charges de travail d'inférence à l'abonné.
  • Serveur d'inférence : traite les requêtes d'inférence par lot (par exemple, vLLM).
  • HPA du serveur : ajuste l'échelle du moteur d'inférence en fonction de métriques spécifiques au moteur, comme vllm:num_requests_waiting.
  • Sujet de lettres mortes : capture les messages dont le traitement échoue après un nombre défini de nouvelles tentatives avec intervalle exponentiel.

Pour en savoir plus, consultez l'implémentation de référence sur GitHub.

Mettre en mémoire tampon et agréger les requêtes

Pour gérer le flux de demandes, procédez comme suit :

  • Utilisez Pub/Sub comme tampon durable : implémentez Pub/Sub pour stocker les demandes d'inférence de manière durable. Cette configuration agit comme un tampon FIFO qui contient les requêtes jusqu'à ce qu'un consommateur ait la capacité de les traiter, ce qui empêche la surcharge du serveur en cas de trafic irrégulier.
  • Utilisez des abonnements pull avec un contrôle de flux côté client : configurez un modèle d'abonnement Pull. Cela permet à votre application d'abonné de demander explicitement des messages uniquement lorsqu'elle est en mesure de les traiter, ce qui vous donne un contrôle total sur le taux de consommation.
  • Regroupez les messages pour atteindre la taille de lot du serveur : évitez d'envoyer un message Pub/Sub en tant que requête d'inférence. Au lieu de cela, l'abonné doit regrouper plusieurs messages dans une seule requête par lot qui correspond à la taille de lot optimale de votre serveur d'inférence (par exemple, en faisant correspondre les paramètres max_num_seqs dans vLLM). Cette approche permet de s'assurer que les accélérateurs sont entièrement saturés et de maximiser le débit. Plus précisément, configurez le paramètre d'extraction max_messages de votre abonné sur un multiple de max_num_seqs pour vous assurer que chaque propagation avant du modèle est entièrement saturée.

Autoscaling des abonnés et des serveurs

Pour une inférence par lot efficace, il est nécessaire de mettre à l'échelle les abonnés (liés au processeur) différemment des serveurs d'inférence (liés au GPU ou au TPU).

  • Adapter les abonnés en fonction du backlog de travail : configurez l'HorizontalPodAutoscaler (AHP) pour votre déploiement d'abonnés en fonction de la métrique num_undelivered_messages de Pub/Sub. Pour en savoir plus, consultez Optimiser l'autoscaling des pods en fonction des métriques. Calculez le nombre d'instances dupliquées que vous souhaitez utiliser à l'aide de l'équation suivante :

    \[ desiredReplicas = \frac{num\_undelivered\_messages}{target\_latency\_seconds \times throughput\_per\_replica} \]

  • Respectez les quotas d'infrastructure : limitez explicitement le nombre maximal de répliques de vos abonnés en configurant le paramètre maxReplicas dans votre AHP. Ne dépassez pas le nombre d'abonnés que le quota de GPU ou de TPU de vos serveurs d'inférence peut prendre en charge. Le surprovisionnement des abonnés déplacera le goulot d'étranglement vers le serveur d'inférence, ce qui augmentera la contention des ressources sans augmenter le débit.

  • Mettre à l'échelle les serveurs d'inférence en fonction des métriques du moteur : mettez à l'échelle le déploiement de votre serveur d'inférence en fonction des métriques exportées directement par le moteur d'inférence (pas seulement via le processeur/la mémoire). Par exemple, utilisez le paramètre vllm:num_requests_waiting pour vLLM, qui mesure directement le backlog de traitement au niveau du serveur de modèle. Pour en savoir plus, consultez Mettre à l'échelle automatiquement vos pods.

Gérer les erreurs et les délais d'attente

Pour gérer les erreurs et les délais d'expiration, procédez comme suit :

  • Étendez de manière proactive les délais de confirmation : configurez votre abonné pour qu'il étende de manière proactive le délai de confirmation Pub/Sub pour les messages en cours de traitement afin d'éviter les boucles de nouvelle distribution et le traitement en double. Cette approche est nécessaire, car les tâches d'inférence prennent souvent plus de temps que les délais d'attente par défaut. En règle générale, définissez une période d'extension plus longue que le temps d'inférence par lot le plus long.
  • Isoler les échecs avec un file d'attente de lettres mortes mortes : activez un file d'attente de lettres mortes pour isoler automatiquement les messages mal formés qui n'ont pas pu être distribués à plusieurs reprises. Cette approche empêche les messages "poison pill" de bloquer la file d'attente et d'interrompre l'ensemble de votre pipeline.
  • Implémentez des stratégies d'intervalle entre les tentatives : si le serveur d'inférence renvoie des erreurs 429 (Trop de requêtes) ou 503 (Service indisponible), l'abonné doit les intercepter et implémenter une stratégie d'intervalle exponentiel entre les tentatives, en suspendant temporairement la consommation à partir de Pub/Sub jusqu'à ce que le serveur se rétablisse.

Orchestrer des jobs par lot hors connexion à grande échelle

Suivez ces bonnes pratiques pour maximiser le débit, assurer la rentabilité, implémenter une traçabilité complète pour l'audit, et appliquer une gestion avancée des quotas et une priorisation des jobs, lorsque vous traitez des ensembles de données volumineux.

Utiliser JobSet pour l'inférence distribuée multinœud

Nous vous recommandons d'utiliser la ressource Kubernetes JobSet pour orchestrer les charges de travail d'inférence distribuées qui nécessitent la coopération de plusieurs nœuds, comme les grands modèles s'exécutant sur des pods TPU ou des clusters GPU multinœuds. Les jobs Kubernetes standards ne peuvent pas garantir que tous les pods requis démarrent simultanément, ce qui peut entraîner des blocages dans les charges de travail distribuées.

JobSet est une API native de Kubernetes qui gère des groupes de Jobs en tant qu'unité et offre les avantages suivants pour l'inférence par lot :

  • La planification de groupe permet de s'assurer que toutes les ressources requises, telles que les tranches de TPU ou les nœuds de GPU, sont disponibles avant le début de la charge de travail afin d'éviter les blocages.
  • Le placement exclusif permet de s'assurer qu'un seul JobSet a un accès exclusif à la topologie du réseau (par exemple, une tranche de TPU) pour maximiser les performances d'interconnexion.
  • La récupération en cas d'échec vous permet de redémarrer des jobs répliqués spécifiques ou l'ensemble des jobs si un nœud de calcul échoue, en fonction de votre configuration.

Utiliser des jobs indexés pour le partitionnement des données

Lorsque vous utilisez JobSet, configurez ReplicatedJob pour utiliser le paramètre completionMode: Indexed. Ce paramètre injecte automatiquement une variable d'environnement JOB_COMPLETION_INDEX dans chaque pod. Votre code d'inférence peut utiliser cet index pour sélectionner de manière déterministe un fragment de données unique à traiter.

Par exemple, si vous disposez d'un bucket Cloud Storage contenant 100 000 images et que vous déployez un JobSet avec un parallélisme de 10, chacun des 10 pods lit son index (0 à 9) au démarrage. Le pod 0 peut alors calculer qu'il doit traiter les images 0 à 9 999, tandis que le pod 1 traite les images 10 000 à 19 999. Cette approche réduit le besoin d'un service de file d'attente de tâches distinct.

Utiliser le modèle side-car pour la saturation du serveur

Pour maximiser l'utilisation des accélérateurs, configurez les pods JobSet avec deux conteneurs à l'aide du modèle sidecar :

  • Serveur d'inférence : serveur optimisé (tel que vLLM) qui se concentre entièrement sur le calcul GPU ou TPU.
  • Pilote client : conteneur logique qui envoie de manière asynchrone un grand nombre de requêtes au serveur sur localhost.

Ce découplage garantit que le GPU ou la TPU reste occupé et n'est jamais inactif en attendant les E/S réseau ou le prétraitement des données. Sans cette approche, les modèles qui chargent les données de manière séquentielle peuvent entraîner l'attente de l'accélérateur pour la fin des opérations d'E/S, ce qui conduit à une sous-utilisation. Par exemple, au lieu d'attendre que les données soient traitées, le pilote client peut précharger les données et envoyer en continu des requêtes asynchrones au serveur d'inférence, ce qui garantit que la file d'attente des requêtes de l'accélérateur reste saturée.

Checklist

Catégorie Bonne pratique
Modèles architecturaux
Coût et débit
Messages et scaling
Orchestration