Back to Deep Learning
AI/ML · Deep Learning

Transformer

Every position looks at every other position and decides, from content alone, which ones matter to it. A convolution asserts in advance that neighbours are relevant; recurrence asserts that recency is. Attention asserts neither — it learns the routing. That is why it transfers across modalities, and also why it needs far more data than architectures whose prior is built in: it has to learn from scratch what the others assume.

SupervisedSelf-supervisedTransformerSequence modelingClassificationRegressionRankingAnomaly detection

Also known as: Self-attention network, Encoder-only Transformer, BERT-family

Intuition

What it actually does

Every position looks at every other position and decides, from content alone, which ones matter to it. A convolution asserts in advance that neighbours are relevant; recurrence asserts that recency is. Attention asserts neither — it learns the routing. That is why it transfers across modalities, and also why it needs far more data than architectures whose prior is built in: it has to learn from scratch what the others assume.

Assumptions it makes
  • Enough data exists to learn the routing that convolution and recurrence assume structurally
  • The relevant context fits inside the attention window, since nothing outside it is reachable
  • Positional information is supplied explicitly — attention itself is permutation-invariant
  • A pretrained checkpoint exists in a compatible domain, or the data budget is very large
Objective

Loss function

Compare every position against every other, normalize those scores into weights, and take a weighted average of the values. The division by root-d_k is not cosmetic: without it, dot products of high-dimensional vectors grow with dimension, the softmax saturates into a near one-hot, and gradients vanish. The task loss on top is ordinary — cross-entropy for tokens, MSE for regression.

queries, keys and values — three learned projections of the same input
all-pairs similarity: an n x n matrix of how much each position attends to each other
the scaling that keeps the softmax out of its saturated regime
the values, mixed according to the attention weights
Optimization

How it is minimized

AdamW with warmup and cosine decay, pre-norm residual blocks, gradient clipping

warmup steps — a linear ramp from zero
peak learning rate, reached at the end of warmup
the current optimizer step

Warmup is genuinely load-bearing rather than a tuning nicety. Adam's second-moment estimate is unreliable in the first few hundred steps, and combined with the residual stream that produces enormous early updates that destroy the attention patterns before they form. Ramping the learning rate from zero avoids it. Pre-norm placement — LayerNorm before the sublayer rather than after — is the other structural fix: it gives the residual stream a clean identity path, which is what made deep transformers trainable without exhaustive warmup tuning. AdamW rather than Adam because decoupled weight decay actually regularizes here, where L2-in-the-gradient does not.

Hyperparameters
  • d_model / n_heads

    Width and how many independent attention subspaces; head dim is d_model/n_heads (256–4096, 4–32 heads)

  • depth

    Number of blocks; capacity scales roughly with depth x width (6 to 96)

  • warmup steps

    Ramp length — too short and early updates destabilize training (1% to 10% of total steps)

  • context length

    Sequence window; attention cost is quadratic in this (512 to 128k)

  • dropout

    Applied to attention weights and residuals; often 0 at very large scale (0.0 to 0.1)

Convergence & cost

Non-convex but, with warmup and pre-norm, remarkably reliable — which is precisely why the architecture scaled. The characteristic failures are about data and memory rather than optimization: overfitting badly when pretraining is skipped, and running out of memory because attention is quadratic in sequence length. Attention-entropy collapse is the subtle one — heads degenerate into attending to a single position and stop contributing, which the loss does not reveal and only per-head inspection catches.

O(n²·d) time and O(n²) memory for attention, plus O(n·d²) for the feed-forward layers. The quadratic term dominates past a few thousand tokens and is the entire reason FlashAttention, sliding-window attention, and linear-attention variants exist.

Applications

How, where, and why it is used

Time-Series Forecasting

Viable
How

Patch the series into segments, embed each patch as a token, and attend across patches. Patching rather than one-token-per-timestep is what made transformers competitive here — it cuts sequence length by the patch factor and gives each token enough local context to be meaningful.

Where
  • Long-horizon multivariate forecasting with many correlated series
  • Forecasting with rich exogenous covariates that attention can weight per horizon
Why

Competitive rather than dominant, and worth being precise about: a well-tuned linear model beats many transformer forecasters on standard benchmarks, which was a genuinely embarrassing finding for the field. It earns its cost when series are numerous, histories are long, and cross-series structure exists to learn — otherwise the inductive bias it lacks is the bias you needed.

Featurization
  • Patch the series; per-timestep tokens make sequence length explode for no benefit
  • Instance-normalize per series to handle distribution shift between train and test windows
  • Causal masking is mandatory — bidirectional attention leaks the future
Evaluation

Rolling-origin backtesting scored with MASE against seasonal naive AND against a linear baseline, because the linear baseline frequently wins.

Pitfalls
  • Omitting the causal mask leaks future values and produces spectacular, meaningless validation scores
  • Quadratic memory makes long lookbacks infeasible without patching or windowed attention

Anomaly Detection

Viable
How

Train on normal sequences and score by prediction or reconstruction error, or use masked reconstruction — mask random positions and measure how badly they are recovered.

Where
  • Multivariate telemetry monitoring where cross-channel relationships define normal
  • Log-sequence anomaly detection over structured event streams
Why

The advantage over an LSTM detector is that attention can relate distant events directly, so a failure whose precursor was hours earlier is reachable. The cost is data hunger and the same capacity trap every reconstruction detector has — a large enough model reconstructs anomalies too.

Featurization
  • Train exclusively on confirmed-normal windows
  • Attention weights double as an explanation of which positions drove the score
Evaluation

Precision@k and time-to-detection against confirmed incidents, holding out whole episodes rather than points.

Pitfalls
  • Needs far more normal data than a classical detector before it is competitive
  • Excess capacity reconstructs anomalies and the detector goes blind

Optimization

Adapted
How

Not an optimizer. It appears as a learned dynamics model whose rollouts a planner searches over, and increasingly as the policy network inside decision-transformer formulations that treat trajectories as sequences.

Where
  • World models in model-based RL, where attention over trajectory history conditions the prediction
  • Sequence-to-sequence formulations of routing and scheduling
Why

Offers no constraint guarantees and no optimality argument, so it belongs inside the loop rather than replacing the solver. Its real contribution is conditioning on long context — a schedule that depends on events far back is expressible here and not in a Markov state.

Featurization
  • Encode the action as part of the token, or the dynamics model is not conditioned on what you did
  • Train on trajectories under a diverse action distribution or it knows only one regime
Evaluation

Multi-step rollout error under held-out action sequences — a planner exploits exactly the horizon where the model is weakest.

Pitfalls
  • One-step accuracy is a poor proxy for rollout accuracy, and only the latter matters to a planner
  • No feasibility guarantee, so a hard-constrained problem still needs a classical solver
Also applied in

Natural Language

Primary fit
How

Pretrain on unlabelled text with a masked or autoregressive objective, then fine-tune a small head on the downstream task. For encoder models the pretrained representation is the product; the head is almost an afterthought.

Where
  • Named-entity recognition, classification and extraction on a pretrained encoder
  • Semantic search and reranking from contextual embeddings
  • Text-to-SQL and structured extraction via sequence-to-sequence decoding
Why

The architecture the field converged on, and the reason is transfer: attention plus scale turned a single self-supervised objective into general linguistic capability, so a few thousand labelled examples now suffice where a from-scratch model needed hundreds of thousands. The economics of NLP inverted because of this.

Featurization
  • Use the pretrained tokenizer exactly — a mismatched vocabulary silently destroys the transfer
  • Bucket by sequence length to limit padding waste under quadratic attention
Evaluation

Task F1 or exact match on a held-out split, with explicit contamination checks against the pretraining corpus.

Pitfalls
  • Fine-tuning on a small set can catastrophically forget the pretrained representation — use a low learning rate
  • Benchmark contamination inflates reported scores when the eval set appeared in pretraining data

Computer Vision

Viable
How

Vision Transformer: split the image into fixed patches, embed each as a token, and attend across them with a positional encoding supplying the spatial information convolution gets structurally.

Where
  • Large-scale image classification where pretraining data is abundant
  • Multimodal models where a shared architecture across vision and text is the point
Why

Overtakes CNNs at very large scale, where enough data exists to *learn* the spatial prior rather than assume it — and loses below that threshold, because a learned prior needs far more evidence than a built-in one. The honest rule is that dataset size, not architecture fashion, decides between them.

Featurization
  • Patch size trades sequence length against spatial detail directly
  • Heavy augmentation and regularization are required — ViTs overfit more readily than CNNs
Evaluation

Top-k accuracy sliced by lighting, pose and demographic, plus robustness under corruption.

Pitfalls
  • Underperforms a CNN on modest datasets, sometimes badly
  • Quadratic attention over patches makes high-resolution input expensive without windowing

Recommendation & Ranking

Viable
How

Session-based recommendation: treat the interaction history as a token sequence and attend over it to predict the next item.

Where
  • Sequential recommendation where order and recency both matter
  • Reranking a candidate set with cross-attention between query and items
Why

Captures long-range session structure that a bag-of-history model discards. In practice gradient boosting still wins the final ranking stage on tabular features, so this typically supplies the retrieval or the sequence representation rather than the final score.

Featurization
  • Cap history length; attention cost is quadratic in it
  • Include dwell time and gaps as features — the sequence alone is not the whole signal
Evaluation

NDCG@k offline with online A/B as the decision, since offline ranking gains routinely fail to transfer.

Pitfalls
  • The catalogue is the output vocabulary, so the softmax needs sampling or candidate generation
  • Logged sequences reflect what the previous ranker chose to show
Deployment

What it costs to run

Training

Pretraining is GPU-months at scale and is why almost nobody does it. Fine-tuning a pretrained encoder is minutes to hours on one GPU, which is the path essentially everyone takes.

Inference

Tens of milliseconds for an encoder on GPU. Cost is dominated by sequence length through the quadratic attention term, so batching by similar length matters more than batch size.

Retraining

Fine-tune when the task distribution shifts; re-pretrain essentially never. Vocabulary drift is usually handled by fine-tuning rather than starting over.

Drift & monitoring
  • Track out-of-vocabulary and unknown-token rates — a rising rate signals the domain moving away from the tokenizer
  • Monitor per-slice task metrics; aggregate scores hide subgroup degradation entirely
  • Watch input length distribution, since a shift changes both latency and cost non-linearly
Production gotchas
  • Tokenizer and model must be versioned together — a vocabulary mismatch produces plausible-looking nonsense
  • Padding and attention masks must be correct, or padded positions silently contribute to the output
  • Batching by uniform length rather than uniform count is what actually controls serving cost
Trade-offs

Pros and cons, in context

Strengths
  • Fully parallel across sequence positions

    The structural reason it displaced recurrence: training parallelizes over the sequence axis, which made pretraining at scale economically possible. Irrelevant at inference for autoregressive decoding, which is sequential again.

  • Direct paths between distant positions

    Any two positions are one attention hop apart, so long-range dependency does not degrade with distance the way it does through recurrence.

  • Transfers across modalities and tasks

    The same architecture serves text, vision, audio and time series, because it assumes almost nothing about input structure. That generality is exactly what costs it data efficiency.

  • Attention weights offer a partial window into the model

    Genuinely useful for debugging which positions drove a prediction — though attention is not a faithful explanation, and treating it as one is a known error.

Limitations
  • Quadratic time and memory in sequence length

    The defining constraint. It is why long-context work is an active research area and why patching, windowing and FlashAttention exist. Irrelevant at 512 tokens, decisive at 100k.

  • Very data-hungry without pretraining

    It must learn the spatial or temporal prior that CNNs and RNNs have built in. On a modest dataset from scratch it will lose to both, and that is the expected outcome rather than a bug.

  • Permutation-invariant without positional encoding

    Attention alone cannot tell order. The positional scheme is a real design decision — absolute, relative and rotary encodings extrapolate to unseen lengths very differently.

  • Expensive and opaque relative to the alternatives

    Both training and serving cost more than a CNN or GBM of comparable task performance. On tabular data that trade is simply not worth making.

Implementation

Intuitive, idiomatic, optimized

The same algorithm in three languages, each progressing through Kent Beck's three stages. Every optimization names the lever it pulls and the trade-off it accepts — cost profiles are illustrative, not measured benchmarks.

1. Intuitive

The math, transcribed literally. No dependencies.

"""Scaled dot-product attention - the equation, transcribed.

Three loops: for each query position, score it against every key position,
softmax those scores, then take the weighted average of the values.
"""

import math


def softmax(scores):
    # Subtract the max before exponentiating - otherwise exp() overflows on
    # large logits and the whole row becomes NaN.
    largest = max(scores)
    exps = [math.exp(s - largest) for s in scores]
    total = sum(exps)
    return [e / total for e in exps]


def attention(queries, keys, values, causal=False):
    """queries[n][d_k], keys[m][d_k], values[m][d_v] -> output[n][d_v]."""
    n = len(queries)
    m = len(keys)
    d_k = len(queries[0])
    scale = 1.0 / math.sqrt(d_k)

    output = []
    for i in range(n):
        # Score this query against every key: the all-pairs comparison.
        scores = []
        for j in range(m):
            dot = sum(queries[i][k] * keys[j][k] for k in range(d_k))
            # Causal masking: a position may not attend to the future.
            # Without this a sequence model trivially cheats.
            if causal and j > i:
                scores.append(float("-inf"))
            else:
                # Scaling by sqrt(d_k) keeps the softmax out of saturation,
                # where gradients vanish.
                scores.append(dot * scale)

        weights = softmax(scores)

        # Weighted average of the value vectors.
        mixed = [0.0] * len(values[0])
        for j in range(m):
            for v in range(len(mixed)):
                mixed[v] += weights[j] * values[j][v]
        output.append(mixed)

    return output

O(n·m·d) in pure Python with the all-pairs matrix built row by row.

2. IdiomaticPyTorch

How this language actually wants it written.

"""Transformer block - typed, pre-norm, with masking handled explicitly."""

from dataclasses import dataclass

import torch
from torch import Tensor, nn


@dataclass(frozen=True)
class TransformerConfig:
    d_model: int = 512
    n_heads: int = 8
    d_ff: int = 2048
    dropout: float = 0.1

    def __post_init__(self) -> None:
        if self.d_model % self.n_heads != 0:
            raise ValueError(
                f"d_model {self.d_model} must divide evenly by n_heads {self.n_heads}"
            )


class MultiHeadAttention(nn.Module):
    """Heads are a RESHAPE, not separate modules.

    One fused qkv projection then a view into (batch, heads, seq, head_dim) -
    running n_heads separate Linear layers would be the same arithmetic in
    n_heads times as many kernel launches.
    """

    def __init__(self, config: TransformerConfig) -> None:
        super().__init__()
        self.n_heads = config.n_heads
        self.head_dim = config.d_model // config.n_heads

        self.qkv = nn.Linear(config.d_model, 3 * config.d_model, bias=False)
        self.out = nn.Linear(config.d_model, config.d_model)
        self.dropout = nn.Dropout(config.dropout)

    def forward(self, x: Tensor, mask: Tensor | None = None) -> Tensor:
        batch, seq, _ = x.shape

        qkv = self.qkv(x).reshape(batch, seq, 3, self.n_heads, self.head_dim)
        q, k, v = qkv.permute(2, 0, 3, 1, 4)     # (batch, heads, seq, head_dim)

        scores = (q @ k.transpose(-2, -1)) / (self.head_dim ** 0.5)

        if mask is not None:
            # -inf, not a large negative number: after softmax this is exactly
            # zero, whereas -1e9 leaves a small residue that accumulates over
            # long sequences.
            scores = scores.masked_fill(mask == 0, float("-inf"))

        weights = self.dropout(torch.softmax(scores, dim=-1))
        mixed = (weights @ v).transpose(1, 2).reshape(batch, seq, -1)
        return self.out(mixed)


class TransformerBlock(nn.Module):
    """PRE-norm: LayerNorm before each sublayer, not after.

    Post-norm (the original paper) needs careful warmup tuning to train deep.
    Pre-norm leaves the residual stream a clean identity path, which is what
    made deep transformers trainable in practice.
    """

    def __init__(self, config: TransformerConfig) -> None:
        super().__init__()
        self.norm1 = nn.LayerNorm(config.d_model)
        self.attention = MultiHeadAttention(config)
        self.norm2 = nn.LayerNorm(config.d_model)
        self.feedforward = nn.Sequential(
            nn.Linear(config.d_model, config.d_ff),
            nn.GELU(),
            nn.Linear(config.d_ff, config.d_model),
            nn.Dropout(config.dropout),
        )

    def forward(self, x: Tensor, mask: Tensor | None = None) -> Tensor:
        x = x + self.attention(self.norm1(x), mask)
        return x + self.feedforward(self.norm2(x))


def causal_mask(seq_len: int, device: str = "cpu") -> Tensor:
    """Lower-triangular mask. Omitting this in a sequence model lets every
    position read the answer, which validates spectacularly and means nothing."""
    return torch.tril(torch.ones(seq_len, seq_len, dtype=torch.bool, device=device))

What changed: Heads become a reshape of one fused qkv projection rather than separate modules — identical arithmetic in a fraction of the kernel launches. Normalization moves to pre-norm, which is what actually made deep transformers trainable, and masking uses true -inf rather than a large negative constant, whose residue accumulates over long sequences.

Explicit type hints on every public signatureDataclasses or NamedTuples over ad-hoc dictsRaise specific exceptions, never bare exceptGuard clauses over nested conditionals

O(n²·d) attention, O(n·d²) feed-forward, on fused kernels.

3. OptimizedPyTorch (SDPA)

Faster, and the justification for each change.

"""Attention - FlashAttention, KV caching, length bucketing."""

import torch
from torch import Tensor, nn


class FastAttention(nn.Module):
    def __init__(self, d_model: int, n_heads: int) -> None:
        super().__init__()
        self.n_heads = n_heads
        self.head_dim = d_model // n_heads
        self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
        self.out = nn.Linear(d_model, d_model)

    def forward(self, x: Tensor, is_causal: bool = True) -> Tensor:
        batch, seq, _ = x.shape
        qkv = self.qkv(x).reshape(batch, seq, 3, self.n_heads, self.head_dim)
        q, k, v = qkv.permute(2, 0, 3, 1, 4)

        # FlashAttention: never materializes the n x n score matrix at all.
        # It tiles the computation so scores stay in SRAM, which turns
        # attention from O(n^2) MEMORY to O(n) memory. That is the change that
        # made long context feasible - it is a memory-traffic win, not an
        # arithmetic one.
        mixed = torch.nn.functional.scaled_dot_product_attention(
            q, k, v, is_causal=is_causal
        )

        return self.out(mixed.transpose(1, 2).reshape(batch, seq, -1))


class CachedAttention(nn.Module):
    """Incremental decoding with a KV cache.

    Autoregressive generation recomputes attention over the entire prefix for
    every new token, which is O(n^2) work across a sequence for something that
    only needs O(n). Caching past keys and values makes each step O(n) instead
    - the single most important inference optimization for generation.
    """

    def __init__(self, d_model: int, n_heads: int, max_seq: int, max_batch: int) -> None:
        super().__init__()
        self.n_heads = n_heads
        self.head_dim = d_model // n_heads
        self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
        self.out = nn.Linear(d_model, d_model)

        # Pre-allocated to full size. Growing the cache per token would
        # reallocate and copy on every single step of generation.
        self.register_buffer(
            "k_cache",
            torch.zeros(max_batch, n_heads, max_seq, self.head_dim),
            persistent=False,
        )
        self.register_buffer(
            "v_cache",
            torch.zeros(max_batch, n_heads, max_seq, self.head_dim),
            persistent=False,
        )

    def forward(self, x: Tensor, position: int) -> Tensor:
        batch, seq, _ = x.shape
        qkv = self.qkv(x).reshape(batch, seq, 3, self.n_heads, self.head_dim)
        q, k, v = qkv.permute(2, 0, 3, 1, 4)

        # Write this step's keys/values into the cache, then attend over the
        # whole prefix. No recomputation of anything already seen.
        self.k_cache[:batch, :, position : position + seq] = k
        self.v_cache[:batch, :, position : position + seq] = v

        keys = self.k_cache[:batch, :, : position + seq]
        values = self.v_cache[:batch, :, : position + seq]

        mixed = torch.nn.functional.scaled_dot_product_attention(
            q, keys, values, is_causal=False   # prefix is all valid history
        )
        return self.out(mixed.transpose(1, 2).reshape(batch, seq, -1))


def bucket_by_length(sequences: list[Tensor], bucket_size: int = 64) -> list[list[Tensor]]:
    """Group sequences of similar length before batching.

    Attention is quadratic in the PADDED length, so one long sequence in a
    batch makes every short one pay its cost. Bucketing is usually a larger
    throughput win than any kernel-level optimization.
    """
    ordered = sorted(sequences, key=len)
    return [ordered[i : i + bucket_size] for i in range(0, len(ordered), bucket_size)]

What changed: Three changes, each targeting a different bottleneck. FlashAttention never materializes the n×n score matrix, converting attention from quadratic *memory* to linear — the change that made long context feasible. KV caching turns autoregressive decoding from O(n²) total work into O(n). And length bucketing addresses the fact that attention cost is quadratic in the *padded* length, so one long sequence taxes every short one in its batch.

  • Fuse adjacent operations so intermediates are never materialized

    FlashAttention tiles the score computation so it stays in SRAM and the n x n matrix is never written to memory — attention is memory-bandwidth bound, so this is where the win comes from.

    Trade-off: The fused kernel supports a fixed set of mask patterns; a custom or arbitrary mask falls back to the materializing path and loses the benefit entirely.

  • Pre-allocate output arrays and use in-place operations

    The KV cache is allocated at full size once, so incremental decoding writes into it rather than reallocating and copying the whole prefix every token.

    Trade-off: Memory is reserved for max_seq x max_batch whether used or not, which at long context is a large fixed cost even for short generations.

  • Batch work to amortize interpreter overhead

    Length bucketing keeps padded length close to true length, and since attention is quadratic in padded length this is often the single largest throughput gain available.

    Trade-off: Bucketing reorders the data, so any downstream code depending on input order must re-sort — and small buckets underfill the device.

O(n) attention memory; O(n) per decode step with caching. Illustrative, not a measured benchmark.