1. Préfill et décodage
Le problème : Un prompt de 8 000 tokens arrive d’un coup ; la réponse sort ensuite token par token. Si le moteur traite le prompt au rythme de la génération — un token à la fois — l’utilisateur attend le premier mot pendant des secondes entières.
L’idée : Deux phases, deux régimes : le préfill voit tous les tokens du prompt en même temps (travail massivement parallélisable) ; le décodage n’ajoute qu’un token par pas (travail intrinsèquement séquentiel). Même mécanisme, profils d’exécution opposés.
Pourquoi / à quel prix : Séparer les deux permet d’optimiser chacun — latence du premier token d’un côté, débit de génération de l’autre. Le prix : deux chemins de code pour un seul mécanisme, qui doivent produire exactement les mêmes nombres.
Vérification de compréhension
Nommez l’entrée, l’état transformé, la sortie et une hypothèse nécessaire. Comparez ensuite votre chaîne à l’explication ci-dessus.
2. Récurrence naïve
Le problème : La mémoire récurrente semble condamner le préfill : S₅ exige S₄, qui exige S₃… Exécuter 8 000 mises à jour l’une après l’autre laisse un GPU — conçu pour des matrices entières — presque vide à chaque pas.
L’idée : Le diagnostic précis : la DÉPENDANCE est séquentielle (chaque S_t dépend de S_{t−1}), mais la majorité du CALCUL par token — produits q·k locaux, écritures k vᵀ — ne l’est pas. La récurrence naïve sérialise tout parce qu’elle ne sépare pas les deux.
Pourquoi / à quel prix : Ce constat ouvre la porte du chunking : ne sérialiser que ce qui doit l’être. Le prix d’en rester à la version naïve se mesure directement : des unités matricielles facturées à l’heure qui exécutent des produits vecteur-matrice.
Vérification de compréhension
Nommez l’entrée, l’état transformé, la sortie et une hypothèse nécessaire. Comparez ensuite votre chaîne à l’explication ci-dessus.
3. Découper en chunks
Le problème : Comment donner au GPU des blocs matriciels pleins sans violer l’ordre ? Il faut un découpage où l’intérieur d’un bloc se calcule en parallèle et où le passé lointain arrive compressé — sans double compte ni oubli.
L’idée : Un chunk de C tokens calcule d’un coup ses interactions internes autorisées (matrice C×C triangulaire) et lit le passé antérieur via l’état entrant : O = M·V + K·S_entrant. Sur la trace, le chunk 1 produit S₄, le chunk 2 le consomme — et o₅..o₈ sont exactement ceux de la récurrence.
Pourquoi / à quel prix : Exactitude algébrique, parallélisme retrouvé. Le prix : une mémoire temporaire en C² pour le triangle, et une complexité de code réelle — deux termes à additionner, donc deux occasions de se tromper : ce sont précisément les deux bugs de la trace.
Vérification de compréhension
Nommez l’entrée, l’état transformé, la sortie et une hypothèse nécessaire. Comparez ensuite votre chaîne à l’explication ci-dessus.
4. Triangle causal
Le problème : À l’intérieur d’un chunk, tous les tokens se calculent ensemble — y compris t5 avec t7, qui est son futur. Sans garde-fou, le préfill apprendrait des dépendances que le décodage ne pourra jamais reproduire.
L’idée : Une matrice triangulaire inférieure matérialise la règle « i ne lit que j ≤ i » : les cases au-dessus de la diagonale sont interdites par construction. Piège de lecture : un 0 sous la diagonale est un produit scalaire nul (autorisé) ; un « . » au-dessus est la causalité.
Pourquoi / à quel prix : Le triangle rend la contrainte vérifiable d’un coup d’œil et gratuite à appliquer. Le prix d’un masque faux est vicieux : la perplexité s’améliore « trop bien » en préfill et rien ne casse — jusqu’au décodage, qui ne peut pas tricher.
Vérification de compréhension
Nommez l’entrée, l’état transformé, la sortie et une hypothèse nécessaire. Comparez ensuite votre chaîne à l’explication ci-dessus.
5. État entrant et sortant
Le problème : Le chunk 2 ne doit revoir aucun token du chunk 1 — sinon le parallélisme s’effondre — mais o₅ dépend de v₁ et v₃. Comment transmettre « tout le passé utile » sans transmettre le passé ?
L’idée : Par l’état : le chunk 1 émet S₄ = K₁ᵀV₁, un résumé de taille fixe ; le chunk 2 le lit par q_tᵀS₄ et ajoute ses termes locaux. Sur la trace : o₅ = [3,4] (hérité) + [0,1] (local) = [3,5]. Les frontières transportent l’ordre et la causalité, pas les tokens.
Pourquoi / à quel prix : Un seul objet à passer entre blocs, de taille fixe. Le prix : le chunk 2 ne peut plus décomposer [3,4] entre v₁ et v₃ — la compression de la session 13 s’applique aux frontières. Et oublier le terme entrant (bug 1) ampute tout le prompt.
Vérification de compréhension
Nommez l’entrée, l’état transformé, la sortie et une hypothèse nécessaire. Comparez ensuite votre chaîne à l’explication ci-dessus.
6. Taille du chunk
Le problème : C = 1 redonne la récurrence lente ; C = longueur totale fait exploser le triangle en C². Entre les deux, qui décide ? Le même code peut tourner plusieurs fois plus lentement avec un C mal choisi pour le GPU.
L’idée : C arbitre deux coûts opposés : transitions séquentielles en n/C contre mémoire temporaire en C². Doubler C divise les transitions par 2 et multiplie le triangle par 4 — l’optimum se trouve là où le triangle sature juste la mémoire rapide (SRAM).
Pourquoi / à quel prix : Un C bien choisi sature le matériel. Le prix : le bon C n’est pas transférable d’un GPU à l’autre — c’est un paramètre d’exécution à re-mesurer, pas une constante du modèle. Et il ne change jamais les résultats, seulement leur coût.
Vérification de compréhension
Nommez l’entrée, l’état transformé, la sortie et une hypothèse nécessaire. Comparez ensuite votre chaîne à l’explication ci-dessus.
Développement depuis la source de cours
Chapitre 10 — Découpage en blocs et préremplissage parallèle
10.1 Pourquoi utiliser des blocs
But : combiner une règle de mémoire récurrente avec le calcul rapide d’un GPU. Traiter les jetons un par un est simple à décrire, mais utilise mal les nombreuses unités de calcul parallèles pendant la lecture du prompt, appelée préremplissage.
Au lieu de transporter les courses article par article, place plusieurs articles dans une caisse et déplace la caisse.
Étape par étape
-
Découper N jetons en blocs, par exemple 64 ou 128 dans une implémentation.
-
À l’intérieur d’un bloc, organiser de nombreuses lectures et mises à jour sous forme de grandes opérations matricielles.
-
Transmettre l’état final S d’un bloc au suivant.
-
Préserver l’ordre causal : le jeton t ne doit pas utiliser les jetons futurs.
Exemple détaillé : 12 jetons divisés en blocs de 4 donnent les blocs 1–4, 5–8 et 9–12. Le deuxième bloc reçoit l’état résumant les jetons 1–4. Ses quatre jetons peuvent effectuer une grande partie de leurs calculs ensemble, puis produire un état pour le troisième bloc.
Pourquoi c’est important : Le découpage ne change pas l’objectif d’apprentissage. Il réorganise des opérations équivalentes, ou soigneusement dérivées, afin que le matériel les exécute efficacement.
Vérification rapide : pourquoi ne pas choisir automatiquement un bloc gigantesque ? Réponse : les grands blocs augmentent le travail et le stockage temporaires ; la meilleure taille dépend du matériel et des noyaux de calcul.
10.2 Structure triangulaire causale
But : comprendre le masque triangulaire inférieur utilisé dans un bloc. Une matrice triangulaire inférieure contient des zéros au-dessus de sa diagonale principale.
C’est une règle scolaire : chaque élève peut lire seulement les lignes précédentes, jamais les réponses écrites plus tard.
Étape par étape
-
Pour quatre positions, les liens autorisés forment [[1,0,0,0],[1,1,0,0],[1,1,1,0],[1,1,1,1]].
-
La ligne 3 peut utiliser les positions 1, 2 et 3.
-
La ligne 1 peut utiliser uniquement la position 1.
Exemple détaillé : Le triangle empêche la prédiction du prochain jeton de tricher pendant l’entraînement et le préremplissage.
Pourquoi c’est important : La causalité est une condition de correction, pas seulement un détail d’optimisation.
Vérification rapide : dans un bloc causal, le jeton 2 peut-il utiliser le jeton 4 ? Réponse : non.
Cas guidé complet
Pour 8 tokens en chunks de 4, le premier calcule un triangle 4×4 puis transmet S₄. Le second reçoit S₄, calcule son triangle local et produit S₈. Aucun token du premier bloc ne peut lire le second.
Méthode de lecture: écrire les données, annoncer la forme de chaque objet, effectuer une seule transformation, puis interpréter le résultat avant de continuer.