Cet article existe aussi en anglais : lire la version anglaise.
Les deux temps d'une réponse
Cette page reprend le texte de la vidéo, avec ses images, puis donne en fin d'article les conditions exactes de chaque mesure.
Ces dernières semaines, je vous ai montré comment Herbert, mon moteur d'inférence pour IA locales, fonctionne sur différents types de matériel : processeurs, cartes graphiques, puces Apple.
Mais de quoi parle-t-on exactement, quand on dit qu'un moteur d'IA tourne à 40 ou 55 tokens par seconde ? Il y a en fait deux vitesses, qui ne dépendent pas de la même chose. Et au final, ce qui compte vraiment, c'est le temps qu'il faut attendre pour obtenir une réponse.
En développant Herbert, je me suis donné les moyens de mesurer précisément chaque étape. Je m'en sers ici pour approfondir le sujet, sans une seule formule.
Un exemple simple
Je donne à un modèle d'IA, qui tourne avec Herbert sur un processeur grand public, le début d'une phrase : « My cat is purple. But my friends tell me there is no such thing as a ». Il la continue, et je le laisse écrire 100 tokens. Sur ma machine, entre l'envoi de la phrase et le dernier token écrit, il s'écoule 7,2 secondes.

Pendant ce temps, il se passe deux choses très différentes. D'abord, le modèle lit la phrase : c'est ce qu'on appelle le prefill. Ici, ça prend 8 centièmes de seconde. Ensuite, il écrit la suite, un token après l'autre : c'est le décodage (decode). Cette phase dure 7,1 secondes. Pourquoi l'un est-il presque instantané et l'autre si long ? Pour le comprendre, il faut regarder ce qui se passe dans le moteur.
Un modèle d'IA, qu'est-ce que c'est ?
Un modèle d'IA, c'est d'abord un fichier, parfois découpé en plusieurs. Dedans, il n'y a que des nombres, des milliards de nombres : ce qu'on appelle des paramètres. Ces paramètres sont rangés en couches, que le calcul traverse l'une après l'autre.

Ils ont été réglés pendant l'entraînement, une fois pour toutes. Quand on pose une question, ils ne changent plus : on s'en sert seulement pour faire les calculs.
Et ce calcul ne fait qu'une chose : à partir du texte qu'on lui donne, il calcule le morceau de mot qui devrait venir ensuite. C'est ce qu'on appelle un token, ou un jeton en français. On ajoute ce token au texte, et on recommence. Une réponse, c'est juste cette boucle, un token après l'autre.

Herbert, vLLM, MLX ou llama.cpp : le programme qui fait ce calcul, on l'appelle un moteur d'inférence.
Deux opérations
Tout ce qui suit repose sur deux opérations très simples. La première prend deux listes de nombres de même longueur, les multiplie case par case et additionne les résultats. Ça donne un seul nombre : le produit scalaire.
Il permet de savoir simplement si deux listes vont dans le même sens. Ces listes, en mathématiques, on les appelle des vecteurs. Quand ils vont dans le même sens, le produit scalaire est positif ; quand ils vont dans des sens contraires, il est négatif. Et plus ils sont alignés, plus leurs nombres sont grands, plus il s'éloigne de zéro.

La deuxième prend une série de scores et les transforme en pourcentages dont la somme fait 100 %. Le plus grand score prend la plus grosse part. C'est le softmax.

Un modèle de langage utilise bien d'autres opérations, mais ces deux-là suffisent pour comprendre l'essentiel de ce qui se passe.
Le modèle qu'on ouvre
Pour regarder à l'intérieur, je prends un petit modèle : Qwen2.5, en version 1,5 milliard de paramètres, dans sa version de base. J'ai fait ce choix parce que toutes ses couches fonctionnent de la même façon, contrairement à certains modèles plus récents, et je tenais à garder mes exemples simples.
Tout ce qu'on voit ici à l'intérieur de ce modèle, ce sont ses vrais nombres, sur de vraies phrases. Les temps et les choix du modèle sortent d'Herbert. Les valeurs internes, elles, sont extraites avec Transformers, la bibliothèque de référence.
Des tokens et des nombres
Reprenons notre phrase. Le texte est d'abord découpé en tokens : ici, dix-sept, un par mot, plus le point. Ce modèle en connaît plus de 150 000 différents.

Chaque token est remplacé par une ligne d'un grand tableau appris. Chaque ligne, un vecteur, est composée de 1 536 nombres : c'est la table d'embedding. À partir de là, le modèle ne voit plus jamais de mots, seulement ces listes de nombres, une par token.

Chacune de ces listes traverse 28 couches. Aucune couche ne remplace la liste : chacune calcule des modifications et les ajoute pour la couche suivante. Couche après couche, la liste se transforme, selon ce que le modèle a appris.
Dans chaque couche, il y a deux étapes, toujours dans le même ordre. La première fait communiquer les tokens entre eux : c'est l'attention, le seul endroit du modèle où un token reçoit de l'information des autres. La seconde travaille sur chaque token tout seul, sans regarder les autres : le MLP.

L'attention
L'attention n'est pas faite une seule fois, mais plusieurs fois en parallèle, par ce qu'on appelle des têtes. Chaque tête regarde la phrase à sa façon et va chercher autre chose. Dans ce modèle, il y en a 12 par couche.

Suivons-en une seule. Arrivé au dernier token, « a », le modèle fabrique trois listes à partir de lui, chacune par des produits scalaires avec des tableaux de paramètres appris : une requête, Q, une clé, K, une valeur, V. Dans ce modèle, 128 nombres chacune. Chaque token précédent a aussi sa clé et sa valeur.
Le moteur fait ensuite le produit scalaire de la requête de « a » avec la clé de chaque token jusqu'à lui. Ça donne un score par token. Pour schématiser, « a » demande à chacun des tokens qui le précèdent : à quel point es-tu utile pour moi ?
Avec un softmax, ces scores deviennent des pourcentages. Voici les vrais, dans la couche 19, pour une de ses têtes : 91 % sur « purple », 5 % sur « My » et presque rien sur le reste.

J'ai pris un exemple totalement improbable : un chat violet. Ce n'est pas dans sa mémoire que le modèle peut trouver la couleur de mon chat, c'est dans ma phrase. Et justement, au moment d'écrire la suite, cette tête va chercher « purple », treize tokens plus tôt. Personne ne lui a dit de le faire : c'est ce que ses paramètres ont appris.
Ce qui revient à « a », c'est la somme des valeurs de ces tokens, chacune pesée par son pourcentage : 91 % de la valeur de « purple », 5 % de celle de « My », et ainsi de suite.

Les 12 têtes font ce calcul en même temps, chacune avec sa propre requête. Mais elles se partagent les clés et les valeurs : deux jeux pour les douze. Si le chat est vert, je retrouve les mêmes têtes sur la couleur. S'il a cinq pattes, ce sont d'autres têtes qui réagissent. Et si j'éteins celle de la couche 19, le modèle écrit quand même « purple » : elle n'est donc pas indispensable, l'information passe aussi ailleurs dans le réseau.
On voit aussi où le modèle va chercher « purple » : jusqu'à la couche 10, l'attention moyenne sur ce mot reste sous 5 %. Puis elle monte, jusqu'à 29 % à la couche 17.

En moyenne, les têtes donnent 43 % de leur attention au tout premier token, « My ». Les chercheurs appellent ça un puits d'attention : dans de nombreux modèles, le premier token attire une grande part de l'attention, même quand il ne porte rien de particulièrement utile. Il sert un peu de point de repos.
Pour que l'ordre compte, la position de chaque token est inscrite dans sa requête et sa clé, en faisant tourner leurs nombres d'un angle qui dépend de la position.
Le KV cache
Une fois calculées, la clé et la valeur d'un token ne changent plus jamais. Alors le moteur les garde, dans un tableau qui gagne une ligne à chaque token : c'est le KV cache, K pour les clés, V pour les valeurs.
Quand le modèle écrit un nouveau token, il ne calcule que la requête, la clé et la valeur de ce token-là. Il ajoute sa ligne au tableau et compare sa requête à toutes les clés déjà gardées. Sans ce tableau, il faudrait tout recalculer depuis le début, à chaque token écrit.

Sa taille se calcule. Dans ce modèle : 14 336 nombres par token, soit environ 29 kilo-octets. Pour 4 096 tokens, environ 117 mégaoctets, encore peu à côté des 3,1 gigaoctets de paramètres. Mais à 32 000 tokens, c'est près d'un gigaoctet de plus à relire, à chaque token écrit. Plus la conversation est longue, plus chaque nouveau token coûte cher. On vous a déjà conseillé de garder vos conversations avec une IA courtes ? En voilà une des raisons.
Le token seul : le MLP
La deuxième étape de chaque couche travaille sur chaque token seul. Ici, elle contient presque 9 000 paires de directions apprises. Pour chaque paire, le modèle fait deux produits scalaires avec la liste du token : le premier sert de vanne, il décide combien du second laisser passer. Ça donne une valeur par paire.
Pour « a », dans la couche 19, la plupart sont petites, en positif comme en négatif : la moitié ne dépasse pas 0,08. Près de 300 dépassent 0,5, 70 dépassent 1, et la plus forte vaut 3,3.

Chacune de ces valeurs règle une correction apprise : les fortes pèsent beaucoup, les petites presque rien. Le tout est ramené à 1 536 nombres et ajouté à la liste du token. C'est là que se trouve l'essentiel des paramètres de chaque couche : 41 millions sur 47.
Choisir le token suivant
Après la 28e et dernière couche, la liste du dernier token est comparée, par produit scalaire, à chacun des tokens connus : 151 936 scores. Je les montre en pourcentages, mais pour choisir, pas besoin. Ici, je prends toujours le plus grand score : c'est l'argmax. Dans la plupart des assistants, on ne choisit pas comme ça. Une partie du moteur, le sampler, tire le token au hasard en favorisant les plus probables. Là, les pourcentages servent vraiment.

Pour notre chat violet : premier token, « purple », 92,5 %, loin devant « blue », moins de 1 %. Puis « cat », 90,5 %. Puis le point, 68,8 %. Un token à la fois.
Je ne sais pas pour vous, mais pour moi, c'est une dinguerie : personne ne lui a dit que mon chat était violet, sauf ma phrase, mon prompt. Et c'est là qu'il est allé le chercher.
Cette dernière étape pèse 233 millions de paramètres sur 1,5 milliard. Dans beaucoup de modèles, c'est un deuxième grand tableau, de la même forme que celui du début, mais appris pour un autre usage : non plus transformer un token en liste, mais donner un score à chaque token. Ce petit modèle, lui, se sert du même tableau dans les deux sens, pour économiser. Et pour écrire un seul token, il faut le parcourir en entier : 15 % de tout ce que le modèle doit relire.

Premier temps : lire
Le premier temps, le prefill, c'est lire le texte qu'on donne au modèle. Tous les tokens du texte traversent les couches ensemble. Dans chaque couche, chaque paramètre lu en mémoire sert à tous les tokens d'un coup : pour un document de 3 000 tokens, chaque paramètre sert 3 000 fois.

Dans l'attention, chaque token est comparé à tous ceux d'avant. Ça fait un triangle de scores qui grossit très vite avec la longueur. Et le KV cache se remplit.
Ici, ce qui compte, c'est la quantité de calcul. Plus le processeur calcule vite, plus la lecture va vite.

Deuxième temps : écrire
Le deuxième, le décodage, c'est écrire la suite. Là, un seul token à la fois, parce que le suivant dépend de celui-ci. Chaque paramètre lu en mémoire ne sert plus qu'une seule fois.
Pour chaque token écrit, il faut donc faire venir de la mémoire tous les paramètres du modèle, 3,1 gigaoctets, plus le KV cache. Et on recommence pour le token suivant.

Le processeur de mon PC écrit 14 tokens par seconde. 14 fois 3,1 gigaoctets, ça fait environ 44 gigaoctets lus par seconde. Mesurée seule, sa mémoire en donne au mieux 46 à 48. On est tout près du maximum : le calcul, lui, attend.

1 % ou 22 % ?
Ce qui compte, c'est le total : lire plus écrire. J'ai fait l'essai sur le processeur de mon PC, un Ryzen 9 9900X, avec deux textes.
Notre phrase courte, le chat violet : 17 tokens lus en 8 centièmes de seconde, 100 tokens écrits en 7,1 secondes, 7,2 secondes en tout. Lire ne pèse que 1 % du temps.
Un long texte, un passage d'une nouvelle d'Edgar Allan Poe, The Gold-Bug : 3 139 tokens lus en 2,1 secondes, 100 tokens écrits en 7,5 secondes, 9,6 secondes en tout. Lire représente 22 % du temps.


Même avec un long texte, c'est l'écriture qui fait l'essentiel de l'attente, parce que chaque token écrit relit tout le modèle.
La preuve que les deux temps ne dépendent pas de la même chose : sans les instructions AVX-512, qui calculent sur 512 bits d'un coup, la lecture de la nouvelle est près de 6 fois plus lente. L'écriture ne bouge pas.

Même chose quand j'optimise mon moteur : la lecture gagne 12 %, l'écriture ne bouge toujours pas.

Sur ce processeur, avec une seule conversation, lire, c'est du calcul. Écrire, c'est de la mémoire et elle est déjà presque à son maximum.
Et quand le modèle réfléchit ?
Beaucoup de modèles récents réfléchissent avant de répondre. Concrètement, ils écrivent d'abord un brouillon, que vous ne voyez pas toujours, puis la vraie réponse. Ce brouillon, le think, ce sont des tokens écrits un par un, exactement comme la réponse, à la vitesse de l'écriture.
J'ai posé une question à un autre petit modèle, Qwen3.5, en version 2 milliards de paramètres, qui, lui, sait réfléchir : « Pourquoi le ciel est-il bleu en pleine journée, mais rouge orangé au coucher du soleil ? ». Sans réflexion, il répond en 7,5 secondes, cinq fois sur cinq. Mais sa réponse est fausse : il dit que le bleu est absorbé par l'air. C'est un tout petit modèle, pas de miracle ici.
Réflexion activée, sur plusieurs essais, il écrit d'abord entre 3 600 et 5 100 tokens de brouillon. La réponse complète arrive au bout de 5 à 8 minutes. Et cette fois, toutes les réponses sont justes. Ici, une bonne réponse a donc coûté 40 à 60 fois plus d'attente, parce que chaque token de réflexion se paie au prix de l'écriture.

Le modèle qu'on a ouvert plus haut, lui, ne réfléchit pas : c'est un modèle de base, il complète le texte, c'est tout. Les assistants que vous utilisez ont été entraînés une deuxième fois, pour répondre à une question et s'arrêter à la fin de leur réponse. C'est ce qu'on appelle le post-entraînement, et le modèle obtenu, la version Instruct. Mais dessous, c'est la même boucle : un token après l'autre, au même prix.
Ce qu'il faut retenir
Une réponse, c'est donc deux temps. Sur ma machine, lire, c'est du calcul : tous les tokens passent ensemble, et un processeur qui calcule plus vite lit plus vite. Écrire, c'est de la mémoire : un token à la fois, et pour chaque token, tout le modèle à relire.

Alors, la prochaine fois qu'on vous annonce un moteur à tant de tokens par seconde, posez-vous deux questions : combien de temps lui faut-il pour lire ce que je lui donne ? Et combien de temps pour écrire sa réponse ? Au bout du compte, ce n'est pas le nombre de tokens par seconde qui compte, c'est le temps qu'on passe à attendre.
Dans une prochaine vidéo, je parlerai des modèles qui, pour écrire, ne relisent qu'une partie de leurs paramètres : les modèles à experts.
Les conditions de mesure
Tout ce qui précède vaut pour ces conditions-là : un processeur, une seule conversation à la fois, deux petits modèles, une question pour la réflexion.
La machine. AMD Ryzen 9 9900X (12 cœurs, AVX-512 et AVX512-BF16), 12 fils. Débit mémoire mesuré seul, par une lecture en flux émise par Herbert : 46,0 Go/s sur 1 fil, 48,1 Go/s au mieux sur 2 fils, 45,0 Go/s sur 12 fils.
Le moteur. Herbert, version JIT pour processeur, en bf16 (aucune quantification). Les temps du chat violet et de The Gold-Bug sont mesurés avant optimisation ; la colonne « après » de l'optimisation est la première étape du même moteur. Sans AVX-512, le moteur passe par ses noyaux AVX2 et perd aussi les instructions bf16 dédiées. Chaque temps est une médiane de 5 passages, contexte vidé entre deux passages, 100 tokens écrits.
Le modèle ouvert. Qwen2.5-1.5B, version de base (pas Instruct), poids d'origine en bf16 : 28 couches, 1 536 nombres par token, 12 têtes de requête et 2 têtes de clé et valeur de 128 nombres, MLP de 8 960, vocabulaire de 151 936 tokens, même tableau pour l'entrée et la sortie. Le choix des tokens est l'argmax (décodage glouton).
Les valeurs internes. Attention, KV cache, MLP et embedding sont extraits avec Transformers 5.14.1 (PyTorch 2.13), en bf16 sur le même processeur, attention en mode eager. Le modèle y prédit « purple » à 90,3 %, contre 92,5 % dans Herbert : les deux calculs n'arrondissent pas exactement aux mêmes endroits. Les numéros de couche et de tête sont comptés à partir de 1 dans le texte (la couche 19 est l'indice 18).
Les têtes. Les variantes « green » et « five legs » reprennent la même phrase avec un seul mot changé. Pour l'ablation, la sortie d'une tête est mise à zéro avant la projection de sortie de l'attention, dans toutes les positions. Tête 2 de la couche 19 éteinte : « purple » passe de 90,3 % à 92,5 %. Les 10 têtes qui regardent le plus la couleur éteintes ensemble : 83,0 %. Dix têtes tirées au hasard : 93,2 %. Ces observations montrent une redondance, elles ne disent pas à elles seules quelle tête « porte » l'information.
Les textes. Le passage de The Gold-Bug est l'explication du cryptogramme par Legrand, coupée après « about four miles to the northward of the island. » (3 139 tokens).
La réflexion. Qwen3.5-2B en bf16 dans Herbert (première étape d'optimisation), contexte de 16 384 tokens. Sans réflexion : décodage glouton, une chauffe puis 5 répétitions, 85 tokens écrits, fin à 7,49 s les cinq fois. Avec réflexion : l'échantillonnage recommandé par la carte du modèle (température 1,0, top_p 0,95, top_k 20, min_p 0, presence_penalty 1,5), graines 1 à 5, plafond de 8 192 tokens. Réponse complète en 320,6 à 464,6 s, à environ 11,2 tokens/s. Une limite à dire : les graines 2 et 3 ont donné exactement le même texte, les graines 4 et 5 aussi. Il n'y a donc que trois tirages différents sur les cinq ; le traitement de la graine dans le serveur est à corriger.
Les relevés. Les scripts de mesure, tels qu'ils ont tourné, avec les sorties brutes : emplacement du dépôt à venir.