Encoder, decoder, or both?
Choose an architecture by following the available information, the output you need, and the objective used to train it.
On this page Explore the sections +
Before you begin Prerequisites & learning goal +
Builds on: Lesson 6: attention blocks, residual addition, layer normalization, and position-wise FFNs.
Your goal: Trace all three transformer families and choose a sensible starting point for classification, generation, or conditional sequence generation.
We can assemble the same ingredients in several ways. The words encoder and decoder are easiest to understand as descriptions of information flow and purpose, rather than as mysterious types of neural computation.
Ask three questions. What information is available when a prediction must be made? Does the output describe an existing input, or does it extend a sequence? If it generates a sequence, is there a separate source representation it should read?
1. An encoder contextualizes an available input
Our transformer encoder embeds the source tokens, adds positions, and applies blocks with bidirectional self-attention. Each non-padding token can incorporate information from all real source positions. The encoder returns a memory tensor of shape (B, S, D).
Those memory vectors are internal representations, not automatically class labels, vocabulary distributions, or a single sentence embedding. A task head turns them into the output required by a training objective.
For token labeling, a shared linear layer can map each vector to label scores. For sentence classification, one can use a designated summary token or a padding-aware pooling operation, followed by a classifier. For retrieval, one can train representations with an appropriate similarity objective. Merely averaging an arbitrary encoder’s outputs does not guarantee a good semantic embedding space.
BERT is a familiar encoder-family example. Its pretraining includes masked-token prediction using bidirectional context. This is different from asking a causal language model to predict a token from only its prefix; both the allowed information and the training objective differ.
2. A decoder-only model extends a prefix
An autoregressive model factorizes a sequence probability as:
$$ p(y_1,\ldots,y_T)=\prod_{t=1}^{T}p(y_t\mid y_1,\ldots,y_{t-1}). $$Factor a sequence’s probability into next-token probabilities, each conditioned on the earlier tokens.
| Term | What it is and does | What it controls |
|---|---|---|
| \(y_1,\ldots,y_T,T\) | Output tokens in order and sequence length. | The complete sequence whose probability is evaluated. |
| \(p\) | A probability assigned by the model. | How much probability mass it gives a sequence or next token. |
| \(\prod_{t=1}^{T},t\) | Product symbol: multiply one conditional probability at each position t. | Combines next-token factors into a sequence probability. |
| Vertical conditioning bar | Read as given, not division. | Specifies which earlier tokens the model may use. |
| \(y_1,\ldots,y_{t-1}\) | Prefix before the token being predicted; the first step uses a start convention. | Prevents conditioning on future target tokens. |
Check: If a two-token sequence has first-token probability 0.5 and second-token conditional probability 0.2, its probability is 0.1. The product describes the training objective’s factorization, not a claim that all choices are independent.
A decoder-only transformer embeds the prefix, applies causal self-attention blocks, and maps the hidden states to vocabulary logits. It has no separate encoder memory and therefore no encoder–decoder cross-attention sublayer.
At training time, shifted targets let us evaluate many next-token losses in parallel. At generation time, we use the final prefix position’s logits to select the next symbol. Then that symbol becomes part of the next input prefix. Greedy choice, sampling, and beam search are different decoding policies applied to model outputs; they are not different attention mechanisms.
You can condition such a model by placing context or instructions in the prefix. That is why a separate encoder is not a strict requirement for tasks such as translation or summarization. The training formulation and the available checkpoint matter.
3. An encoder–decoder keeps source and target streams separate
For conditional generation, write:
$$ p(y\mid x)=\prod_t p(y_t\mid y_{Predict each target token using both the earlier target prefix and the available source.
| Term | What it is and does | What it controls |
|---|---|---|
| \(x,y\) | Source sequence and target sequence. These are whole streams in this equation, not scalar numbers. | What is provided and what must be predicted. |
| \(p(y\mid x)\) | Conditional probability of the target given the source. | The sequence-to-sequence prediction objective. |
| \(\prod_t,y_t\) | Multiply one probability factor for each target token. | Builds the full target likelihood from local next-token decisions. |
\(y_{| Shorthand for all target tokens before position t. | Autoregressive target context; no future target is included. | |
\(\mid y_{| Condition on that prefix and the source. | Adds source information to the decoder’s own prefix information. | |
Check: When translating, the first output token can use the entire supplied source but no previous target words. A later output can use that same source and the words already generated.
The encoder reads x once and creates source memory. The decoder builds target-side states from the prefix. Each decoder block can then retrieve source information through cross-attention.
Our decoder block has three sublayers: causal self-attention, cross-attention, and the FFN. Each has a pre-norm input and a residual update. For the cross-attention operation, target-side states supply Q, while encoder memory supplies K and V.
Source and target can differ in length, vocabulary, or even modality. An image encoder can supply visual features to a text decoder. An audio encoder can supply acoustic representations. The source representation must fit the cross-attention projections; it need not consist of the same kind of tokens as the target.
One operation at a time. Use the arrows or step buttons; nothing advances automatically.
4. Implement the decoder block
This extends the encoder-style block from lesson 6 with a second attention sublayer. Read the arguments to each call before reading the variable names. They tell you which stream is being queried.
class DecoderBlock:
def __init__(self, width: int = 32, heads: int = 4, inner: int = 64, seed: int = 20) -> None:
self.self_attn = MultiHead(width, heads, seed)
self.cross_attn = MultiHead(width, heads, seed + 1)
self.ffn = ffn_parameters(width, inner, seed + 2)
def __call__(self, y: FloatArray, memory: FloatArray, target_visible: BoolArray, source_visible: BoolArray) -> FloatArray:
normalized = layer_norm(y)
y = y + self.self_attn(normalized, normalized, target_visible)[0]
y = y + self.cross_attn(layer_norm(y), memory, source_visible)[0]
return y + feed_forward(layer_norm(y), *self.ffn)class DecoderBlock(nn.Module):
def __init__(self, width: int = 32, heads: int = 4, inner: int = 64, seed: int = 20) -> None:
super().__init__()
self.self_attn = MultiHead(width, heads, seed)
self.cross_attn = MultiHead(width, heads, seed + 1)
self.norm1, self.norm2, self.norm3 = [nn.LayerNorm(width) for _ in range(3)]
self.ffn = nn.Sequential(nn.Linear(width, inner), nn.ReLU(), nn.Linear(inner, width))
def forward(self, y: Tensor, memory: Tensor, target_visible: Tensor, source_visible: Tensor) -> Tensor:
normalized = self.norm1(y)
y = y + self.self_attn(normalized, normalized, target_visible)[0]
y = y + self.cross_attn(self.norm2(y), memory, source_visible)[0]
return y + self.ffn(self.norm3(y))Typed Python · Shapes are documented alongside the code. Open the full file for imports and dependencies, or step through a concrete example.
The memory tensor remains the encoder output throughout the decoder block. The target state y is updated after each sublayer. In our model, source memory has already passed through a final encoder normalization; the target stream uses a separate normalization before forming cross-attention queries.
Repeated decoder blocks would have their own parameters. “Stacking identical blocks” means identical structure, not necessarily shared weights. Our small model uses one block per side so its complete computation remains easy to inspect.
5. The same block structure can serve different tasks
The following forward demonstrations show an encoder-only classifier and a decoder-only next-token predictor. The latter reuses our two-sublayer block with a causal mask. Its class is called EncoderBlock because that is where we introduced it, but the mask changes the information it can access. The class name alone does not make it bidirectional.
def architecture_demo() -> tuple[FloatArray, FloatArray]:
source = np.array([[4, 7, 9], [6, 8, 0]])
prefix = np.array([[1, 4, 7], [1, 6, 8]])
width, vocabulary = 32, 12
rng = np.random.default_rng(8)
embedding = rng.normal(size=(vocabulary, width))
source_mask, target_mask = attention_masks(source, prefix)
x = embedding[source] + positions(source.shape[1], width)
y = embedding[prefix] + positions(prefix.shape[1], width)
# Encoder-only: pool only real source positions for classification.
encoded = layer_norm(EncoderBlock()(x, source_mask))
real = (source != 0)[..., None]
pooled = (encoded * real).sum(1) / real.sum(1)
class_logits = pooled @ rng.normal(size=(width, 3))
# Decoder-only: same two-sublayer structure, with CAUSAL visibility.
causal_hidden = layer_norm(EncoderBlock(seed=30)(y, target_mask))
next_token_logits = causal_hidden @ rng.normal(size=(width, vocabulary))
return class_logits, next_token_logits # (2, 3), (2, 3, 12)def architecture_demo() -> tuple[Tensor, Tensor]:
source = torch.tensor([[4, 7, 9], [6, 8, 0]])
prefix = torch.tensor([[1, 4, 7], [1, 6, 8]])
width, vocabulary = 32, 12
embedding = nn.Embedding(vocabulary, width)
source_mask, target_mask = attention_masks(source, prefix)
x = embedding(source) + positions(source.shape[1], width)
y = embedding(prefix) + positions(prefix.shape[1], width)
# Encoder-only: pool only real source positions for classification.
encoded = layer_norm(EncoderBlock()(x, source_mask))
real = (source != 0)[..., None]
pooled = (encoded * real).sum(1) / real.sum(1)
class_logits = nn.Linear(width, 3)(pooled)
# Decoder-only: same two-sublayer structure, with CAUSAL visibility.
causal_hidden = layer_norm(EncoderBlock(seed=30)(y, target_mask))
next_token_logits = nn.Linear(width, vocabulary)(causal_hidden)
return class_logits, next_token_logits # (2, 3), (2, 3, 12)Typed Python · Shapes are documented alongside the code. Open the full file for imports and dependencies, or step through a concrete example.
Run architecture_demo() from either full source file. The outputs have shapes (2, 3) for three sentence classes and (2, 3, 12) for twelve vocabulary scores at each prefix position. They are random, untrained logits; the example demonstrates architecture and shape, not task performance.
The PyTorch helper creates its modules inside the function for a one-off forward demonstration. A reusable trainable model stores its modules on an nn.Module instance, as our final TinyTransformer does. Recreating layers on every training call would reset their weights and prevent learning across calls.
6. Choosing a starting point
| What you need | A sensible starting point | Reason to consider it |
|---|---|---|
| Classify a complete document | Encoder + classifier | All document tokens are available for contextualization |
| Label each token | Encoder + token head | Each label can use surrounding input context |
| Build a retrieval representation | Encoder + pooling and suitable training | A fixed-size representation can be indexed and compared |
| Continue text or generate code | Decoder-only model | The next-token objective matches prefix continuation |
| Translate or summarize a separate source | Encoder–decoder, or a suitably trained decoder-only model | Both can express conditioning; compare the training setup and constraints |
| Generate text from image or audio features | Source encoder + decoder cross-attention is one option | Cross-attention provides an explicit interface between representations |
These are starting points, not exclusive capability boundaries. For example, a decoder-only model can classify through a prompted label or a learned head, and an encoder can participate in iterative or non-autoregressive generation systems. What matters is the actual computation, objective, and inference procedure.
A useful engineering distinction is reuse. An encoder–decoder can encode the source once, then repeatedly query that memory during generation. A decoder-only model can reuse prefix keys and values through caching. Neither design removes all costs associated with long context.
7. “Decoder” does not always mean causal text generation
Outside autoregressive language modeling, the same word is used for other transformations from internal representations to task outputs. DETR , for example, uses object queries to decode a set of detections. Its object-query decoder is not simply a left-to-right text generator with a vocabulary head.
When reading another paper, inspect the query source, the masks, and the output head. Do not infer causality from the word “decoder” alone. This is especially useful when moving between language and computer vision.
CHECK YOUR UNDERSTANDINGA model has no encoder, but its prompt contains a source document. Can it still generate a summary conditioned on that document?Think first. Open to check your reasoning.
Yes. A decoder-only formulation can place the source document in the prefix and generate the summary afterward. That differs structurally from cross-attending to separately encoded memory, but both can model source-conditioned output. Suitability depends on training, available models, and inference constraints.
Our choice for the final experiment
We will use an encoder–decoder for copying integer sequences. It gives us an explicit source/target separation and exercises all three kinds of attention from lesson 4. This is a pedagogical choice: copying could be formulated in other ways, but this version makes every component we have learned visible in one model.