Aller au contenu principal

Module 7 — Le décodeur : GPT et la génération

Le décodeur du Transformer produit du texte un jeton à la fois, en s'appuyant sur ce qu'il a déjà écrit. C'est l'architecture de GPT, de LLaMA, de Mistral, de la quasi-totalité des grands modèles de langue actuels. Ce module explique la contrainte de causalité qui les définit, montre comment cette contrainte s'implémente en une ligne, puis passe en revue les décisions de décodage qui gouvernent le style et la variété des sorties.

Une seule différence par rapport à l'encodeur

Un décodeur pur ressemble à un encodeur, à un détail près mais capital : la couche d'auto-attention utilise un masque causal. Une position ne peut attendre que les positions précédentes ou elle-même, jamais celles qui suivent.

Sans cette contrainte, prédire le mot en position tt serait trivial : il suffirait de lire le mot en position tt dans l'entrée. Le modèle n'apprendrait rien. Le masque causal force chaque position à deviner ce qui vient, à partir de ce qui précède, ce qui est exactement la tâche d'un modèle de langue autorégressif.

Le masque prend la forme d'une matrice triangulaire supérieure de -\infty ajoutée aux scores d'attention avant le softmax. Après softmax, les positions futures reçoivent un poids de zéro :

import torch

def masque_causal(L: int) -> torch.Tensor:
"""Matrice (L, L) avec 1 en dessous et sur la diagonale, 0 au-dessus."""
return torch.tril(torch.ones(L, L, dtype=torch.bool))

m = masque_causal(5)
print(m.int())
# tensor([[1, 0, 0, 0, 0],
# [1, 1, 0, 0, 0],
# [1, 1, 1, 0, 0],
# [1, 1, 1, 1, 0],
# [1, 1, 1, 1, 1]])

Ce masque est réutilisable pour tout le lot ; on le construit une fois pour la longueur maximale et on le passe à toutes les couches.

Ne jamais oublier le masque causal

Un modèle entraîné sans masque causal semble apprendre — la perte descend — mais il ne fait qu'apprendre à lire le futur. Au moment de la génération, où le futur n'existe pas, les prédictions sont incohérentes. Cette erreur silencieuse a piégé plusieurs implémentations pédagogiques ; elle se détecte en vérifiant que la matrice d'attention finale a bien la structure triangulaire.

Prédire le jeton suivant, encore et encore

L'objectif d'entraînement d'un décodeur est la prédiction du jeton suivant. Pour chaque position tt d'une séquence, le modèle prédit une distribution sur le vocabulaire, et l'entropie croisée est calculée contre le jeton effectivement présent en position t+1t+1. Toutes les positions contribuent à la perte simultanément, ce qui rend l'entraînement massivement parallèle : une phrase de longueur LL produit L1L - 1 signaux de gradient en un seul passage.

C'est une différence importante avec BERT : BERT calcule la perte uniquement sur les 15 % de jetons masqués, alors qu'un décodeur la calcule sur presque tous les jetons. À taille de données égale, un décodeur reçoit donc environ sept fois plus de signal, ce qui explique en partie pourquoi les grands modèles de langue actuels sont autorégressifs.

Le décodage : générer un mot à la fois

Une fois le modèle entraîné, on lui donne un début de séquence (le prompt) et on lui demande de compléter, jeton par jeton. À chaque étape, on fait passer la séquence courante dans le modèle, on récupère la distribution de sortie sur la dernière position, on choisit un jeton selon cette distribution, on l'ajoute à la séquence, on recommence. La procédure s'arrête sur un jeton spécial de fin ou à une longueur maximale.

La question centrale est : comment choisir ce jeton à chaque pas ? Les cinq stratégies suivantes se retrouvent dans toutes les bibliothèques.

  • Glouton (argmax) : on prend systématiquement le jeton le plus probable. La sortie est déterministe, souvent répétitive, adaptée aux tâches où l'on veut la « meilleure » réponse unique.
  • Recherche par faisceau (beam search) : on maintient les kk meilleures séquences partielles et on avance en parallèle. Utile pour la traduction, où la qualité globale prime sur la variété.
  • Échantillonnage à température : on tire le jeton dans la distribution après avoir divisé les logits par une température TT. À T=0.7T = 0.7, la distribution est piquée mais bruitée ; à T=1.2T = 1.2, elle est plus plate et la sortie devient plus créative.
  • Top-k : on ne conserve que les kk jetons les plus probables (typiquement k=40k = 40) et on échantillonne parmi eux, ce qui coupe la longue traîne de jetons improbables.
  • Top-p ou nucleus : on garde le plus petit ensemble de jetons dont la probabilité cumulée dépasse pp (typiquement p=0.9p = 0.9), puis on échantillonne. C'est plus adaptatif que top-k : le seuil s'ajuste à la forme de la distribution.

Voici l'exemple minimal en PyTorch, sans dépendance externe :

import torch
import torch.nn.functional as F

def echantillonner(logits: torch.Tensor, T: float = 1.0, top_p: float = 0.9):
logits = logits / T
probas = F.softmax(logits, dim=-1)
probas_triees, indices = torch.sort(probas, descending=True)
cumul = torch.cumsum(probas_triees, dim=-1)
a_garder = cumul <= top_p
a_garder[..., 0] = True # toujours garder au moins un jeton
probas_triees = probas_triees * a_garder
probas_triees = probas_triees / probas_triees.sum(dim=-1, keepdim=True)
tire = torch.multinomial(probas_triees, num_samples=1)
return indices.gather(-1, tire)

En pratique, T = 0.7 et top_p = 0.9 sont un point de départ raisonnable pour un modèle de chat ; T = 0 (équivalent au glouton) convient à des tâches déterministes comme l'extraction d'information.

Le cache clé-valeur, indispensable en production

Une génération naïve refait passer toute la séquence dans le modèle à chaque nouveau jeton, ce qui multiplie inutilement le calcul par la longueur. Le cache clé-valeur évite ce gaspillage : les KK et VV calculés pour les jetons précédents sont conservés et réutilisés au pas suivant, où l'on n'a plus qu'à calculer QQ, KK, VV pour le seul nouveau jeton.

Le gain est colossal. Sans cache, générer NN jetons coûte O(N3)O(N^3) opérations (chaque nouvelle position refait le calcul quadratique) ; avec cache, cela tombe à O(N2)O(N^2). C'est ce qui rend possible la génération de longues réponses en temps interactif sur les modèles à 7 ou 70 milliards de paramètres.

Le cache change les compromis mémoire

Le cache occupe une mémoire proportionnelle à la longueur et au nombre de couches, souvent plusieurs gigaoctets sur les gros modèles. Sur GPU contraint, on peut le quantifier (int8) ou utiliser des variantes comme l'attention à multiples requêtes (MQA) ou groupée (GQA) — LLaMA-2 utilise GQA pour cette raison — qui partagent les KK et VV entre plusieurs têtes de QQ.

Sur le fil rouge

Le décodeur de notre Transformer maison, au module 10, applique un masque causal sur son auto-attention et intègre en plus une attention croisée vers l'encodeur (module 8). Il génère la date ISO cible jeton par jeton, en s'arrêtant à un jeton de fin [EOS]. Le décodage glouton suffira à la démonstration, l'échantillonnage n'apportant rien à une tâche qui a une seule bonne réponse par entrée.

En résumé

  • Le décodeur diffère de l'encodeur par une seule chose : un masque causal qui empêche chaque position de regarder le futur, condition nécessaire à la prédiction du jeton suivant.
  • L'entraînement calcule la perte sur presque tous les jetons de la séquence, ce qui donne au décodeur bien plus de signal par phrase qu'à un BERT masqué à 15 %.
  • Les stratégies de décodage — glouton, faisceau, température, top-k, top-p — pilotent le compromis entre déterminisme et créativité de la sortie.
  • Le cache clé-valeur réduit la génération de O(N3)O(N^3) à O(N2)O(N^2) et rend le service en temps réel possible ; il consomme en revanche une mémoire proportionnelle à la longueur.

Module suivant : rassembler encodeur et décodeur dans une même architecture, avec l'attention croisée qui les relie, pour donner T5 et la traduction moderne.