Les couches d'un transformer,
vue par un informaticien

Complément technique à « Comprendre les IA génératives ». Ce que fait réellement une couche, en termes d'algorithme et de structures de données.

L'analogie du dîner mondain — chaque token dialoguant avec les autres — cache quelque chose de très simple algorithmiquement. Le point de départ, c'est la structure de données.

La structure : un tableau qui ne change jamais de forme

Tout tient dans une matrice X de dimensions [n_tokens, d_model] — disons 30 lignes (les tokens) sur 4096 colonnes. Une ligne par token, 4096 nombres flottants par ligne.

Et voici l'essentiel : chaque couche est une fonction de X vers X, de signature identique. Pas de transformation de format, pas de structure intermédiaire. Une centaine de fonctions à la queue leu leu, toutes de même type. Une couche ressemble à ceci :

def couche(X, W):                       # X : [n, 4096]
    X = X + attention(normalise(X), W)  # note le +
    X = X + mlp(normalise(X), W)
    return X                            # [n, 4096]

Le + n'est pas un détail. Les couches ne s'enchaînent pas comme g(f(x)) : elles accumulent. X est un bus partagé — les interprétabilistes disent residual stream — que chaque sous-couche lit intégralement, puis auquel elle ajoute sa contribution. Deux cents sous-couches qui écrivent sur un tableau blanc commun, plutôt qu'un pipeline de filtres. C'est ce qui permet à la couche 80 de lire directement ce qu'a écrit la couche 3.

L'attention : un dict à correspondance floue

C'est là que le dîner mondain devient concret. Pour un informaticien, la meilleure description est : une table associative dont la comparaison de clés est un produit scalaire au lieu d'une égalité de hash, et qui renvoie une moyenne pondérée de toutes les valeurs au lieu d'une seule.

def attention(X, W):
    Q = X @ W.q          # [n, 4096] @ [4096, 4096] -> [n, 4096]   "ce que je cherche"
    K = X @ W.k          #                                          "ce que j'annonce"
    V = X @ W.v          #                                          "ce que je fournis"

    S = Q @ K.T / sqrt(d_head)   # [n, n] : similarité requête/clé
    S = S + masque_causal        # -inf au-dessus de la diagonale
    A = softmax(S, axis=-1)      # chaque ligne somme à 1

    return (A @ V) @ W.o         # moyenne pondérée, puis reprojection

Six multiplications de matrices et un softmax. C'est tout.

Le multi-têtes n'ajoute rien de conceptuel : on découpe les 4096 colonnes en 32 tranches de 128, on fait tourner l'algorithme ci-dessus indépendamment sur chaque tranche, on reconcatène. Trente-deux recherches associatives en parallèle, sur trente-deux sous-espaces différents.

Le MLP : là où sont réellement les connaissances

def mlp(X, W):
    H = gelu(X @ W.up)    # [n, 4096] -> [n, 16384]
    return H @ W.down     # [n, 16384] -> [n, 4096]

Aucune interaction entre tokens ici : chaque ligne est traitée séparément, comme un map. On dilate d'un facteur 4, on applique une non-linéarité, on recomprime.

Répartition des paramètres, pour un modèle de 7 milliards à 32 couches : environ 67 M par couche pour l'attention (4 matrices d×d), 134 M pour le MLP. Le MLP pèse les deux tiers du modèle. Grossièrement : l'attention déplace l'information entre positions, le MLP est la mémoire associative qui contient les faits et les règles. « Paris est la capitale de la France » vit dans des matrices de MLP, pas dans l'attention.

Le point qui déroute quand on vient du code impératif

Il n'y a aucune structure de contrôle. Pas de if, pas de boucle sur les données, pas de branche. Le graphe d'exécution est rigoureusement identique pour « bonjour » et pour une démonstration de topologie — mêmes opérations, mêmes dimensions, même nombre d'instructions. Seules les valeurs diffèrent.

Ce qui tient lieu de conditionnelle, c'est la saturation du softmax : quand un score domine largement, la distribution s'approche d'un vecteur one-hot et le A @ V devient de facto une sélection. Un if émulé par une fonction continue qui se rapproche d'une marche. C'est aussi pourquoi tout est dérivable, donc entraînable par descente de gradient — un vrai branchement ne le serait pas.

Coûts

BlocComplexitéDomine quand
AttentionO(n² · d)contexte long
MLPO(n · d²)contexte court

Le de l'attention explique tout : le prix des grands contextes, et l'intérêt des architectures hybrides qui remplacent une partie des couches par des mécanismes en O(n).

Corollaire pratique, si vous faites tourner du local : le cache KV. Comme le masque causal garantit que les K et V d'un token ne dépendent jamais de ce qui le suit, on les conserve d'un passage à l'autre. À chaque nouveau token généré on ne recalcule que sa propre ligne, pas les 30 précédentes. C'est ce cache qui occupe la mémoire vidéo en plus des poids, et qui croît linéairement avec le contexte — la vraie raison pour laquelle un contexte de 100 000 tokens sature une carte bien avant que les poids ne posent problème.