Naar de inhoud
NLEN
Illustratie: Voorwaartse vs achterwaartse pass: backpropagation

Voorwaartse versus achterwaartse pass: backpropagation in transformers

Door Ivo Donker — samengesteld met AI-ondersteuning (Claude & Gemini)
Wat je hiervoor moet weten

Dit artikel valt onder Module 5 (Onder de motorkap). Om de wiskundige stromen en geheugendynamiek volledig te doorgronden, helpt het als de basisstructuur van neurale netwerken al helder is. Raadpleeg vooraf hoe een transformer is opgebouwd en bekijk hoe matrixprojecties en gewichten zijn georganiseerd in het overzicht van parameters en gewichten.

Het trainen van een modern taalmodel berust fundamenteel op een herhalende cyclus van twee opeenvolgende fasen: de voorwaartse pass (forward pass) en de achterwaartse pass (backward pass). Hoewel een LLM tijdens normale inferentie uitsluitend de voorwaartse berekening doorloopt om het volgende token te voorspellen, vereist elk trainings- en fine-tuningproces de volledige lus. In de voorwaartse fase stromen getokeniseerde vectoren door tientallen transformatorlagen om tot een kansverdeling over het vocabulaire te komen. Vervolgens berekent een verliesfunctie (loss function) de discrepantie tussen de voorspelling en de werkelijke doeltekst. Direct daarna start de achterwaartse pass: via de kettingregel van de differentiaalrekening (backpropagation) reizen foutgradiënten terug door het netwerk om vast te stellen welke gewichten moeten worden aangepast.

In dit artikel ontleden we de wiskundige grondslagen, de computationele asymmetrie en de enorme geheugendruk die backpropagation uitoefent op moderne GPU-clusters. We volgen een concrete tensorstroom door een transformatorblok en analyseren waarom het opslaan van tussenliggende activaties de grootste flessenhals vormt bij grootschalige LLM-training.

De voorwaartse pass: van embedding naar logits

Tijdens de voorwaartse pass transformeert het model een discrete reeks invoertokens stap voor stap in een reeks continue representaties. Beschouw een batch tokens met dimensies [B, S], waarbij B staat voor de batchgrootte en S voor de sequentielengte. De eerste operatie is de embedding-lookup, waarbij elk token-ID wordt omgezet in een vector van verborgen dimensie D (de modeldimensie $d_{\text{model}}$), aangevuld met positionele informatie. Dit levert de initiële activatietensor $X_0 \in \mathbb{R}^{B \times S \times D}$ op.

Vanaf dit punt passeert de tensor achtereenvolgens $L$ identieke transformer-lagen. Binnen elke laag $l$ vinden twee opeenvolgende hoofdtransformaties plaats, elk omsloten door een residuale verbinding (skip connection) en een normalisatiestap (RMSNorm of LayerNorm):

Eerst ondergaat de genormaliseerde invoer een multi-head self-attention-berekening. De tensor projecteert via lineaire gewichtsmatrices $W_Q, W_K, W_V$ naar Queries, Keys en Values. Om de exacte matrixvermenigvuldigingen en de schalingsfactor van deze stap te begrijpen, kun je de wiskundige afleiding van self-attention bestuderen. De uitvoer van de attention-heads wordt samengevoegd, lineair geprojecteerd via $W_O$ en opgeteld bij de oorspronkelijke residustroom:

# Wiskundige stappen binnen de voorwaartse pass van laag l
X_norm1 = RMSNorm(X_{l-1})
X_attn  = MultiHeadAttention(Q=X_norm1 * W_Q, K=X_norm1 * W_K, V=X_norm1 * W_V) * W_O
X_mid   = X_{l-1} + X_attn

X_norm2 = RMSNorm(X_mid)
X_ffn   = FeedForward(X_norm2)
X_l     = X_mid + X_ffn

Na de attention-laag volgt het feed-forward netwerk (FFN of MLP). In moderne architecturen zoals Llama bestaat dit netwerk uit gepaarde lineaire projecties met een niet-lineaire gating-functie. Voor een diepere duik in waarom klassieke activaties hier niet meer worden toegepast, lees je de analyse over SwiGLU en GELU activatiefuncties. Na de laatste transformer-laag $L$ volgt een finale normalisatie en een lineaire projectie naar het vocabulaire via de embedding-matrix $W_{\text{vocab}}$, resulterend in logits $Z \in \mathbb{R}^{B \times S \times V}$.

De verliesfunctie en de start van de achterwaartse pass

De overgang van de voorwaartse naar de achterwaartse pass vindt plaats bij de loss function. Bij autoregressieve taalmodellen is dit vrijwel altijd de cross-entropy loss over het volgende token. Gegeven de logits $Z$ en het ware doeltarget $y \in \{1, \dots, V\}$, berekent de softmax-functie de voorspelde kansverdeling $\hat{p}$:

# Kansverdeling via Softmax voor token op positie t
p_{t, i} = exp(z_{t, i}) / sum_{j=1}^V exp(z_{t, j})

# Cross-entropy verlies voor positie t
L_t = -log(p_{t, y_t})

# Totale gemiddelde loss over de batch en sequentie
Loss = (1 / (B * S)) * sum_{b=1}^B sum_{t=1}^S L_{b, t}

De achterwaartse pass begint met het bepalen van de partiële afgeleide van de totale loss ten opzichte van de niet-genormaliseerde logits $z_{t, i}$. Een bekende wiskundige eigenschap van de combinatie van softmax en cross-entropy is dat deze afgeleide opmerkelijk eenvoudig reduceert:

dL / dz_{t, i} = p_{t, i} - 1   (als i gelijk is aan doeltarget y_t)
dL / dz_{t, i} = p_{t, i}       (als i NIET gelijk is aan target y_t)

Oftewel in vectorvorm:
dL / dZ = (P - Y_onehot) / (B * S)

Deze gradiënttensor $\frac{\partial \mathcal{L}}{\partial Z}$, met identieke dimensies aan de logits [B, S, V], vormt het startpunt van de achterwaartse golf. Vanaf hier reist de foutvector laag voor laag terug naar het begin van het netwerk.

Backpropagation via de kettingregel door het transformer-blok

Backpropagation is in essentie de systematische toepassing van de multivariabele kettingregel uit de calculus. Beschouw een generieke lineaire laag $Y = X \cdot W$, waarbij $X \in \mathbb{R}^{N \times D_{\text{in}}}$ de invoeractivatie is en $W \in \mathbb{R}^{D_{\text{in}} \times D_{\text{out}}}$ de gewichtsmatrix. Tijdens de achterwaartse pass ontvangt deze laag de stroomopwaartse gradiënt $\frac{\partial \mathcal{L}}{\partial Y}$. De laag moet nu twee afzonderlijke berekeningen uitvoeren:

Gradiënttype Wiskundige formulering Doel en bestemming
Gewichtsgradiënt ($\frac{\partial \mathcal{L}}{\partial W}$) $X^T \cdot \frac{\partial \mathcal{L}}{\partial Y}$ Wordt gebruikt door de optimizer (zoals AdamW) om gewicht $W$ bij te werken.
Activeringsgradiënt ($\frac{\partial \mathcal{L}}{\partial X}$) $\frac{\partial \mathcal{L}}{\partial Y} \cdot W^T$ Stroomt verder terug naar de voorgaande lagen in de computationele graaf.

Hier zien we direct het centrale mechanisme van deep learning: om de gewichtsgradiënt $\frac{\partial \mathcal{L}}{\partial W}$ te berekenen, heeft het systeem de oorspronkelijke invoeractivatie $X$ nodig die tijdens de voorwaartse pass is gegenereerd. Als $X$ niet bewaard is gebleven in het GPU-geheugen, kan de vermenigvuldiging $X^T \cdot \frac{\partial \mathcal{L}}{\partial Y}$ niet plaatsvinden.

Wanneer de gradiënt door een residuale verbinding $X_{l} = X_{l-1} + F(X_{l-1})$ stroomt, dicteert de optellingsregel dat de gradiënt zich splitst:

dL / dX_{l-1} = (dL / dX_l) + (dL / dF) * (dF / dX_{l-1})

De term $\frac{\partial \mathcal{L}}{\partial X_l}$ stroomt ongehinderd via de residustroom direct door naar eerdere lagen. Dit fenomeen voorkomt het beruchte probleem van verdwijnende gradiënten (vanishing gradients) in diepe architecturen met honderden lagen.

Het activatiegeheugen: de verborgen flessenhals

Bij het trainen van grote modellen is het VRAM-geheugengebruik niet alleen afhankelijk van het aantal parameters, maar vooral van de bewaarde tussenactivaties. Terwijl gewichten en optimizer-states een vaste omvang hebben, schaalt het activatiegeheugen lineair met de batchgrootte, lineair met het aantal lagen en kwadratisch (of lineair bij FlashAttention) met de sequentielengte.

Voor een standaard transformer-laag moeten de volgende tensoren worden opgeslagen tijdens de forward pass om de backward pass mogelijk te maken:

Bij een model met 70 miljard parameters en een contextlengte van 8192 tokens overtreft het geheugen dat nodig is voor deze tussenactivaties al snel het geheugen van de modelparameters zelf. Dit dwingt engineers om geavanceerde technieken toe te passen, zoals Activation Checkpointing (ook bekend als gradient checkpointing). Hierbij worden activaties tijdens de forward pass weggegooid en tijdens de backward pass lokaal opnieuw berekend, wat circa 30% extra rekentijd kost maar het activatiegeheugen met wel 70% tot 80% reduceert.

Computationele vergelijking: FLOPs en rekentijd

Er bestaat een vuistregel binnen LLM-engineering: de achterwaartse pass kost ruwweg tweemaal zoveel FLOPs (drijvendekomma-operaties) als de voorwaartse pass. Dit brengt de totale trainingskosten per token op circa $6N$ FLOPs, waarbij $N$ het aantal actieve modelparameters voorstelt ($2N$ voor forward, $4N$ voor backward).

Eigenschap Voorwaartse Pass (Inference & Training) Achterwaartse Pass (Uitsluitend Training)
Doel Logits en voorspellingen genereren. Gradiënten berekenen voor gewichten en inputs.
Rekenintensiteit (FLOPs) $\approx 2N$ operaties per token. $\approx 4N$ operaties per token (twee matrixproducten).
Geheugenvereiste Laag (tussenactivaties kunnen direct worden overschreven bij pure inferentie). Zeer hoog (vereist behoud van alle forward-activaties of herberekening).
Parallelle afhankelijkheid Strikte stroom van laag $1 \to L$. Strikte stroom van laag $L \to 1$.
Hardware-karakteristiek Compute-bound bij grote batches; memory-bandwidth bound bij single-token autoregressie. Vrijwel altijd compute-bound op tensor cores door grote matrixvermenigvuldigingen.

Waarom kost de backward pass precies tweemaal zoveel rekenkracht als de forward pass? Zoals eerder aangetoond bij de lineaire laag $Y = X \cdot W$: in de forward pass is er slechts één matrixvermenigvuldiging ($X \cdot W$). In de backward pass moeten er echter twee matrixvermenigvuldigingen worden uitgevoerd: één om de gradiënt naar de invoer te propageren ($\frac{\partial \mathcal{L}}{\partial Y} \cdot W^T$) en één om de gradiënt naar de parameters te bepalen ($X^T \cdot \frac{\partial \mathcal{L}}{\partial Y}$).

In productiesystemen zien we dat de kosten van deze rekenstappen direct doorwerken in de infrastructuurkeuzes; wie wil zien hoe deze computationele verdeling doorwerkt in serverkosten en doorvoersnelheden kan de analyse van batch versus realtime processing raadplegen.

Stapsgewijze gradient flow in een PyTorch-achtige implementatie

Om te visualiseren hoe een transformer-blok computationeel schakelt tussen de voorwaartse en achterwaartse stroom, volgt hieronder een modulaire weergave in Python-pseudocode:

import torch

class TransformerBlockBackprop:
    def __init__(self, d_model):
        # Initialisatie van projectiegewichten
        self.W_q = torch.randn(d_model, d_model, requires_grad=True)
        self.W_k = torch.randn(d_model, d_model, requires_grad=True)
        self.W_v = torch.randn(d_model, d_model, requires_grad=True)
        self.W_out = torch.randn(d_model, d_model, requires_grad=True)

    def forward(self, x):
        # Opslaan van invoeractivatie voor gebruik in backward pass
        self.saved_x = x
        
        # 1. Lineaire projecties
        self.q = torch.matmul(x, self.W_q)
        self.k = torch.matmul(x, self.W_k)
        self.v = torch.matmul(x, self.W_v)
        
        # 2. Scaled Dot-Product Attention
        d_k = self.q.shape[-1]
        scores = torch.matmul(self.q, self.k.transpose(-2, -1)) / (d_k ** 0.5)
        self.attn_weights = torch.softmax(scores, dim=-1)
        self.context = torch.matmul(self.attn_weights, self.v)
        
        # 3. Output projectie en residuale optelling
        out = torch.matmul(self.context, self.W_out)
        return x + out

    def backward(self, grad_output):
        # grad_output is dL/d(out), stroomopwaarts ontvangen
        
        # Gradiënt door de residuale verbinding splitst lineair
        grad_residual = grad_output.clone()
        
        # Gradiënten voor W_out en de context-tensor
        grad_W_out = torch.matmul(self.context.transpose(-2, -1), grad_output)
        grad_context = torch.matmul(grad_output, self.W_out.t())
        
        # Gradiënten door attention softmax en V-matrix
        grad_v = torch.matmul(self.attn_weights.transpose(-2, -1), grad_context)
        grad_attn_weights = torch.matmul(grad_context, self.v.transpose(-2, -1))
        
        # Softmax backward transformatie
        # (vereenvoudigde representatie van de Jacobiaan-vermenigvuldiging)
        s = self.attn_weights
        grad_scores = s * (grad_attn_weights - (grad_attn_weights * s).sum(dim=-1, keepdim=True))
        grad_scores = grad_scores / (self.q.shape[-1] ** 0.5)
        
        # Gradiënten voor Q en K projecties
        grad_q = torch.matmul(grad_scores, self.k)
        grad_k = torch.matmul(grad_scores.transpose(-2, -1), self.q)
        
        # Gewichtsgradiënten berekenen met opgeslagen invoeractivatie self.saved_x
        grad_W_q = torch.matmul(self.saved_x.transpose(-2, -1), grad_q)
        grad_W_k = torch.matmul(self.saved_x.transpose(-2, -1), grad_k)
        grad_W_v = torch.matmul(self.saved_x.transpose(-2, -1), grad_v)
        
        # Totale activeringsgradiënt naar vorige laag propageren
        grad_x = grad_residual + (
            torch.matmul(grad_q, self.W_q.t()) +
            torch.matmul(grad_k, self.W_k.t()) +
            torch.matmul(grad_v, self.W_v.t())
        )
        
        return grad_x, (grad_W_q, grad_W_k, grad_W_v, grad_W_out)

In bovenstaand codevoorbeeld is zichtbaar dat `self.saved_x` expliciet bewaard moet blijven tot de `backward`-functie wordt aangeroepen. Zodra het model miljoenen tokens per iteratie verwerkt over tientallen lagen, vormt dit de kernoorzaak van out-of-memory (OOM) fouten op trainingshardware.

Uitdagingen en numerieke instabiliteit bij backpropagation

Tijdens de backward pass bewegen gradiënten zich over honderden matrixoperaties. Dit brengt aanzienlijke technische en numerieke risico's met zich mee:

1. Gradiënt-explosie (Exploding Gradients): Wanneer matrixgewichten groter zijn dan 1 of bij herhaalde accumulatie in de residustroom, kunnen gradiënten exponentieel groeien naarmate ze terugkeren naar de eerste lagen. Dit leidt tot NaN-waarden (Not a Number) in de gewichten. De standaard remedie in transformer-training is Gradient Clipping, waarbij de globale norm van alle gradiëntvectoren wordt begrensd tot een vaste drempelwaarde (bijvoorbeeld 1.0):

if ||g|| > max_norm:
    g = g * (max_norm / ||g||)

2. Underflow bij mixed-precision training (FP16/BF16): Gradiënten zijn vaak extreem klein (bijvoorbeeld $10^{-6}$ of kleiner). In 16-bit floating point representaties (met name standaard IEEE FP16 met slechts 5 exponent-bits) resulteert dit snel in underflow naar nul. Hierdoor stopt het model met leren. BFloat16 lost dit grotendeels op door 8 exponent-bits te gebruiken (ten koste van precisie in de mantisse), terwijl FP16 geavanceerde Loss Scaling-algoritmen vereist die de loss met een factor vermenigvuldigen vóór backpropagation en de berekende gradiënten daarna weer terugschalen.

3. Asynchrone communicatie bij Distributed Data Parallel (DDP): Bij training over meerdere GPU's start de synchronisatie van gradiënten (AllReduce) asynchroon zodra een individuele laag zijn backward pass voltooit. Als de rekentijd van de backward pass niet perfect overlapt met de netwerkbandbreedte tussen GPU's (NVLink/Infiniband), ontstaat er aanzienlijke GPU-idle time.

Optimalisatiestrategieën voor de backward pass

Om de immense geheugendruk en rekenlast van de backward pass te beteugelen, heeft het AI-veld diverse doorbraken gerealiseerd:

Hierna verder met

Nu het mechanisme van forward en backward propagation helder is, kun je doorgroeien naar de technieken die deze berekeningen versnellen op hardwareniveau. Lees verder over GPU-geheugenoptimalisaties in FlashAttention ontleed: snellere berekening via GPU-geheugen of ontdek hoe parameter-efficiënt trainen werkt in LoRA en adapters uitgelegd.