
Pourquoi DeepSeek-V3 est-il censé être rapide et bon marché à grande échelle, mais trop lent et coûteux à exploiter localement ? Pourquoi certains modèles d'IA sont-ils lents à répondre mais rapides une fois qu'ils sont lancés ?
Les fournisseurs d'inférence IA évoquent souvent un compromis fondamental entre le débit et la latence : pour un modèle donné, vous pouvez soit le servir à haut débit et haute latence, soit à faible débit et faible latence. En fait, certains modèles sont si naturellement inefficaces en termes de GPU qu'ils doivent, dans la pratique, être servis à haute latence pour avoir un débit exploitable (par exemple, DeepSeek-V3).
Ce compromis provient de la taille du lot que le fournisseur d'inférence choisit pour le modèle : il ne s'agit pas de regrouper les inférences au sein d'une requête individuelle, mais de regrouper les inférences entre des dizaines ou des centaines de requêtes utilisateur simultanées. C'est une caractéristique particulière des LLM basés sur des transformateurs que le calcul d'un lot de complétions en même temps est presque aussi rapide que le calcul d'une seule complétion. Pourquoi ?
Qu'est-ce que l'inférence par lots ?
Les GPU sont efficaces pour effectuer de grandes multiplications matricielles (GEMM, ou « general matrix multiplications »). Supposons que vous ayez un seul jeton que vous souhaitez faire passer dans un modèle (c'est-à-dire en le multipliant par tous ses poids - les autres détails de l'architecture ne sont pas pertinents). Vous l'exprimez sous la forme d'un vecteur qui correspond à la dimension (ou taille cachée) du modèle (c'est-à-dire 1 x la largeur de ses grandes matrices de poids) et vous le multipliez. Cela correspond à 1 GEMM. Mais si vous souhaitez faire passer dix jetons dans un lot, cela ne correspond toujours qu'à un seul GEMM, car vous pouvez empiler les jetons dans une seule matrice (10 x la dimension du modèle). C'est beaucoup plus rapide que d'effectuer dix GEMM légèrement plus petits. Ainsi, la mise en œuvre d'un serveur d'inférence pourrait ressembler à ceci :
- Une requête arrive avec une instruction générative
- Cette instruction générative est préremplie (passée par l'attention - nous verrons plus tard comment cela peut également être traité par lots), formant un cache KV et une matrice de la taille d'un jeton (1 x la taille du modèle) qui deviendra finalement le jeton prédit
- Cette matrice de la taille d'un jeton est placée dans une file d'attente
- Un serveur GPU extrait des lots (par exemple de 128) de cette file d'attente, les empile dans une matrice de 128 x la taille du modèle et les multiplie par les poids du modèle feed-forward
- Le résultat final est ensuite divisé en 128 jetons distincts
- Celui correspondant à la requête d'origine est renvoyé à l'utilisateur.
- En supposant que ce jeton ne soit pas un jeton de fin de séquence, revenez à l'étape 2 pour continuer à générer le jeton suivant dans la réponse.
Notez que c'est le serveur qui décide de la taille du lot à extraire. Il s'agit d'un compromis entre le débit et la latence. Si vous ne faites pas de traitement par lots et que vous traitez les jetons un par un, aucun utilisateur n'attend dans une file d'attente (étape 3 ci-dessus), donc la latence est faible (en supposant que vous disposiez de suffisamment de GPU). Cependant, si vous effectuez beaucoup de traitements par lots, la latence est élevée car les utilisateurs attendent que la taille du lot soit remplie, mais le débit est beaucoup plus élevé car les GPU sont utilisés plus efficacement.
Pourquoi les GPU sont-ils plus rapides pour multiplier une seule fois de grandes matrices que pour multiplier plusieurs fois de petites matrices ? Il y a deux raisons à cela. Premièrement, l'émission de chaque commande au GPU entraîne une certaine surcharge, et une seule commande permet de lancer une grande multiplication. Deuxièmement, chaque nouvelle commande GPU implique de récupérer des poids dans la mémoire, ce qui peut être coûteux pour les poids importants. Si vous exécutez de nombreux petits GEMM, vous pouvez finir par passer la plupart de votre temps à transférer des poids vers et depuis la mémoire au lieu de les calculer.
Pourquoi certains modèles sont-ils optimisés pour des tailles de lots élevées ?
En général, un serveur d'inférence dispose d'une « fenêtre de collecte » où les demandes des utilisateurs sont reçues et mises en file d'attente. Les serveurs de chat visent généralement 5 à 10 ms, mais les backends à très haut débit peuvent aller jusqu'à 200 ms. Si une nouvelle requête arrive au début de la fenêtre, elle peut attendre toute la durée de la fenêtre avant d'être traitée. Lorsque la fenêtre se ferme, toutes les requêtes en file d'attente sont regroupées en lots (c'est-à-dire que toutes les matrices de taille 1xmodèle sont concaténées en une seule matrice de taille 128xmodèle) et ce lot est envoyé via le pipeline. L'exécution d'un lot comme celui-ci est parfois appelée « tick ».
Comme l'explique le paragraphe ci-dessus, vous pouvez exécuter n'importe quel modèle avec n'importe quelle taille de lot. Le processus de traitement par lots n'a rien en soi qui exclurait certains types de modèles. Cependant, il est possible de construire un modèle si peu efficace en termes de GPU qu'il nécessite effectivement un traitement par lots pour être pratique.
Pourquoi le mélange d'experts nécessite des tailles de lots plus importantes
Prenons par exemple un modèle de mélange d'experts (comme DeepSeek-V3 ou, supposément, le GPT-4 original). Vous pouvez obtenir un modèle puissant en l'entraînant à disposer de centaines et de centaines d'« experts » : des blocs distincts de poids feed-forward, à partir desquels une couche de routage sélectionne un sous-ensemble qui est utilisé sur chaque jeton. Mais un modèle comme celui-ci est vraiment inefficace en termes de GPU. Nous pouvons comprendre pourquoi : les GPU veulent effectuer un petit nombre de multiplications matricielles vraiment importantes, mais si vous avez beaucoup d'experts, vous êtes obligé d'effectuer de nombreuses petites multiplications. À moins que vous ne fassiez votre inférence par lots, cela se traduira par un faible débit.
Réfléchissons à la façon dont une « fenêtre de collecte » de 5 ms et 200 ms fonctionnerait pour un grand modèle de mélange d'experts. Supposons que vous récupériez dix requêtes utilisateur dans cette fenêtre de 5 ms. Si vous avez de nombreux experts, certains d'entre eux pourraient finir par ne traiter qu'un ou deux jetons (c'est-à-dire que la taille du lot pour chaque expert sera bien inférieure à l'ensemble total des requêtes que vous avez récupérées dans votre fenêtre). Si, en revanche, vous attendez 200 ms et récupérez 4 000 requêtes utilisateur, vous avez beaucoup plus de chances de saturer tous vos experts. Au prix d'une certaine latence, vous vous assurez que vos GEMM sont volumineux et que vos GPU sont constamment utilisés à leur capacité maximale.
Pourquoi les pipelines volumineux nécessitent des tailles de lots élevées pour éviter les bulles de pipeline
Pour les modèles volumineux, il peut être difficile de maintenir les GPU actifs. Les modèles volumineux comportent généralement de nombreuses couches de transformateurs, c'est-à-dire des centaines de matrices de poids qui composent le réseau feed-forward. La seule façon d'obtenir une inférence rapide dans ce cas est de mettre ces couches en pipeline en demandant à un GPU de traiter les dix premières couches, à un autre de traiter les dix suivantes, et ainsi de suite. Sinon, vous ne pourrez tout simplement pas faire tenir tous les poids dans la mémoire d'un seul GPU, vous passerez donc beaucoup de temps à échanger les poids dans et hors de la mémoire, ce qui finira par être très lent. Pendant l'inférence, chaque jeton (généralement dans un « micro-lot » de quelques dizaines de jetons chacun) passe séquentiellement par ce pipeline de GPU.
L'efficacité de votre pipeline dépend du nombre de couches dont vous disposez et de la taille de votre fenêtre de collecte. Lorsque vous traitez les jetons dans une fenêtre pendant un « tick », vous obtenez des GPU inactifs au début (car les GPU des couches suivantes n'ont encore rien à traiter) et d'autres GPU inactifs à la fin (lorsqu'il n'y a plus de jetons dans la file d'attente, les GPU des premières couches doivent attendre le prochain « tick »). Ces périodes d'inactivité sont parfois appelées « warmup » (échauffement) et « drain » (vidange). Si vous avez de nombreuses petites fenêtres, vous passerez plus de temps GPU en échauffement et en vidange que si vous avez moins de grandes fenêtres. En choisissant la taille de votre fenêtre, vous faites donc un compromis direct entre le débit et la latence.
Si vous avez une multitude de couches et que votre fenêtre de collecte est très courte, vous pouvez parfois vous retrouver avec moins de jetons à traiter que de couches. C'est ce qu'on appelle une « bulle de pipeline » : en effet, la phase de « vidange » commence plus tôt que d'habitude. Vous ne pouvez pas éliminer l'échauffement et la vidange (pour les raisons évoquées ci-dessous, l'inférence doit fonctionner par « ticks » séquentiels), mais vous pouvez éliminer les bulles de pipeline en rendant votre fenêtre de collecte suffisamment longue. Les bulles de pipeline peuvent être absolument brutales pour le débit du modèle, c'est pourquoi les fournisseurs d'inférence définissent toujours leurs fenêtres suffisamment larges pour les éviter. Cela ajoute une latence notable pour les modèles comportant de nombreuses couches.
Ne pouvez-vous pas simplement garder la file d'attente pleine ?
Pourquoi les fournisseurs d'inférence ne pourraient-ils pas éliminer complètement l'échauffement et la vidange en gardant la file d'attente du GPU pleine de jetons ? En d'autres termes, ne pourriez-vous pas supprimer complètement les ticks et simplement maintenir le flux des micro-lots de jetons ? Bien sûr, l'inférence de chaque utilisateur doit être séquentielle (car vous ne pouvez pas commencer à générer le jeton suivant tant que le jeton actuel n'est pas terminé), mais les grands fournisseurs d'inférence devraient avoir suffisamment de trafic simultané pour maintenir la file d'attente pleine de demandes d'utilisateurs distinctes.
J'avoue que j'ai du mal à comprendre pourquoi cela ne serait pas possible en théorie. D'après ce que je peux en juger, l'obstacle pratique réside dans la manière dont l'étape d'attention est traitée par lots : si vous voulez regrouper les GEMM d'attention, ils doivent tous avoir la même forme (c'est-à-dire le même nombre de jetons antérieurs dans la séquence). Vous devez donc exécuter des groupes de même forme en même temps, au lieu de pouvoir simplement maintenir une seule file d'attente. Il existe au moins quelques recherches publiques à ce sujet, mais je ne serais pas surpris qu'il existe des astuces plus ingénieuses pour y parvenir que je ne connais pas.
Autre idée : si vous avez besoin de ticks pour l'étape d'attention, pourquoi ne pas simplement avoir un système d'inférence d'attention basé sur les ticks et un système continu plus efficace pour le FFN ? Si je comprends bien, la raison est la surcharge mémoire :
- comme la sortie d'attention est nécessaire pour le FFN, vous auriez besoin d'un emplacement en mémoire pour la stocker en attendant son tour dans la file d'attente du FFN, ce qui deviendrait rapidement trop coûteux.
- Les piles d'inférence modernes sont capables de combiner l'étape d'attention et l'étape FFN en quelques GEMM de grande taille dans une seule « opération ». Si vous effectuez ces opérations sur différents GPU, vous devez exécuter différentes opérations et transférer les poids vers et depuis la mémoire.
Résumé
- Les GPU sont plus efficaces sur les GEMM de grande taille. Par conséquent, empiler de nombreux jetons dans une seule multiplication matricielle permet d'obtenir un débit de jetons bien supérieur à celui obtenu en les traitant un par un.
- Pendant le décodage, l'attention ne peut être regroupée que pour les jetons d'une même étape, ce qui oblige les planificateurs à fonctionner par « ticks » courts. Le nombre de jetons que vous regroupez dans un seul « tick » (c'est-à-dire le temps que vous attendez pour collecter les jetons) correspond à la taille de votre lot.
- Il s'agit de jetons provenant de différents utilisateurs. Vous ne pouvez pas regrouper les jetons d'un même utilisateur, car vous avez besoin des jetons précédents pour générer le suivant. Le regroupement nécessite donc un volume élevé de trafic provenant de différents utilisateurs.
- Les lots plus importants augmentent la latence, car les jetons des utilisateurs peuvent attendre jusqu'à 200 ms avant que le lot soit suffisamment rempli pour être exécuté, mais ils augmentent le débit en permettant des GEMM plus importants (et donc plus efficaces) dans l'étape de feed-forward.
- Les modèles comportant de nombreuses couches (par exemple, les pipelines longs) nécessitent des lots plus importants pour éviter les bulles de pipeline (en s'assurant que chaque tick contient plus de lots que d'étapes de pipeline).
- Les modèles de type « mixture-of-experts » doivent être servis avec une latence élevée pour être efficaces : chaque expert ne voit que les jetons qui lui sont acheminés, vous avez donc besoin de lots globaux plus importants pour que chaque expert reste occupé.
- Les fournisseurs d'inférence choisissent une taille de lot/fenêtre qui élimine les bulles de pipeline et sature les experts. Les lots de grande taille vous permettent d'obtenir un débit plus élevé au prix d'une latence plus importante, car les jetons attendent de remplir le tick.
- Certains modèles (comme ceux de DeepSeek) qui sont des mélanges d'experts à plusieurs couches nécessitent donc des lots de grande taille et une latence élevée, sinon le débit chute brutalement. C'est pourquoi on dit souvent qu'il n'est pas facile d'utiliser DeepSeek à des fins personnelles : en effet, avec un seul utilisateur exécutant une seule inférence à la fois, son efficacité/son débit est très faible.
- Le fait que les modèles d'OpenAI et d'Anthropic soient rapides à répondre suggère que :
- leurs modèles ont une architecture plus efficace (non MoE, moins de couches), ou
- OpenAI/Anthropic ont recours à des astuces très intelligentes pour servir l'inférence, ou
- ils paient le prix fort pour beaucoup plus de GPU qu'ils n'en ont strictement besoin.
Source : "Why DeepSeek is cheap at scale but expensive to run locally"
Et vous ?


Voir aussi :




Vous avez lu gratuitement 1 039 articles depuis plus d'un an.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.