ml-models / Language models / Attention core

Attention

Attention lets every position in a sequence read from every other position, weighting each by how well it matches. It is three matrix products and a softmax. This page works one tiny example by hand (queries, keys, values, scores, weights, outputs), splits it into 2 heads, applies padding and causal masks, and then reads the same ideas out of PyTorch v2.15.0: nn.MultiheadAttention, F.scaled_dot_product_attention and FlexAttention.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. Queries, keys and values
  5. Scaled dot-product attention
  6. Multi-head attention
  7. Masks: padding, causal, boolean and additive
  8. Cross-attention
  9. Grouped-query attention
  10. nn.MultiheadAttention in PyTorch
  11. F.scaled_dot_product_attention and its backends
  12. CausalBias and FlexAttention
  13. Worked example
  14. Implementation
  15. Tradeoffs
  16. Check yourself
  17. Connections
  18. Read first
  19. Read next
  20. References

Problem

A language model has to decide what "it" refers to in "The cat sat because it was tired", and a translation model has to decide which source word to look at while producing each target word. The answer depends on the whole sentence, and the distance between the words varies. Earlier designs squeezed a sentence through a recurrent state one step at a time (see the recurrent networks that came before), so information from the start had to survive every step. Convolutions see only a fixed window.

What we want is a layer where each position can look directly at any other position, decide how much each matters for the current job, and take a weighted blend of what it finds, with the weights computed from the content rather than fixed by position. That layer is attention.

Intuition

Think of a library lookup. Each position writes a query ("what am I looking for?"), a key ("what do I contain, for the purpose of being found?") and a value ("what will I hand over if you pick me?"). To compute the output at position i, take its query, compare it with every key by dot product (a big dot product means a good match, see vectors and dot products), turn the scores into weights that sum to 1 with a softmax, and return the weighted average of the values. A position whose key matches strongly contributes most of its value.

Nothing here is fixed in advance: the queries, keys and values are produced from the input by learned matrices, so training decides what counts as a match. Multi-head attention runs several such lookups in parallel, each in its own smaller subspace, because one blended average can express only one kind of relationship at a time (who is the subject, which noun does this pronoun point to, what came just before).

Attention weights of two heads on the same four tokens Two four-by-four grids of attention weights, one per head, for the tokens the, cat, sat, <pad>. Each row is a query position and sums to 1. In head 0 the token "the" gives 0.51 to itself, while in head 1 it gives 0.22 to itself and 0.45 to "sat". The padding column still receives weight because no mask has been applied yet. head 0 weights, no mask (rows: queries, columns: keys) the the cat cat sat sat <pad> <pad> 0.51 0.12 0.25 0.12 0.12 0.51 0.25 0.12 0.31 0.31 0.31 0.07 0.25 0.25 0.25 0.25 head 1 weights, no mask the the cat cat sat sat <pad> <pad> 0.22 0.22 0.45 0.11 0.33 0.33 0.17 0.17 0.25 0.25 0.25 0.25 0.25 0.25 0.25 0.25 darker = larger weight; every row sums to 1

The same four tokens, two heads, two different patterns. Both heads still put weight on the last column, which is padding. Masks (below) remove that.

Mechanics

Queries, keys and values

Let X be the input, shape [L, E]: L positions, each an E-dimensional vector (the output of an embedding layer, or of the previous layer). Three learned matrices turn it into queries, keys and values:

Q = X WQT   K = X WKT   V = X WVT    shapes: [L, E] @ [E, E] → [L, E]

The transpose is there because PyTorch stores a linear layer's weight as [out, in] and computes x @ weight.T. For self-attention all three come from the same X. For cross-attention the queries come from one sequence and the keys and values from another (below). Queries and keys must share a width (they get dotted); values may differ.

Scaled dot-product attention

Attention(Q, K, V) = softmax( Q KT / √dk + M ) V    shapes: [L, dk] @ [dk, S] → [L, S]; [L, S] @ [S, dv] → [L, dv]

  1. Scores. Q KT is an [L, S] matrix: entry (i, j) is the dot product of query i with key j.
  2. Scale. Divide by √dk. A dot product of two dk-vectors with independent unit-variance entries has variance dk, so without scaling the scores grow with the width, the softmax saturates toward a one-hot row, and its gradient (which is tiny where the softmax is saturated) stalls training. Dividing by √dk brings the variance back to 1.
  3. Mask. Add M: 0 where attention is allowed and −∞ where it is not, so the softmax gives those positions exactly zero weight.
  4. Softmax along the key axis (each row). Row i becomes a probability distribution over the S positions.
  5. Mix. Multiply by V: output row i is the weights-weighted average of the value rows.

The score matrix is L × S, so memory and compute grow with the square of the sequence length; that cost drives most of the engineering at the end of this page.

Multi-head attention

With E = 4 and h = 2 heads, each head works in dk = E / h = 2 dimensions. The layer projects once to full width and then reshapes so that each head sees its own slice of columns:

[N, L, E] → view [N, L, h, E/h] → transpose [N, h, L, E/h]   attend per head   → [N, L, h·E/h] → @ WOT → [N, L, E]

The head split costs nothing: it is a view of the same memory. The heads attend independently (the head dimension acts as a batch dimension), their outputs are concatenated back to width E, and a final linear layer WO mixes the heads. Total compute is about the same as one head at full width, but the model gets h different weight patterns. In F.multi_head_attention_forward the code spells the split out as q.view(tgt_len, bsz * num_heads, head_dim).transpose(0, 1) and, on the way out, attn_output.permute(2, 0, 1, 3).reshape(bsz * tgt_len, embed_dim), followed by the output projection.

Splitting a projection into heads and merging it back A four-column projection output is split into 2 groups of 2 columns, one per head. Each head runs its own softmax(QKt) V and produces a width-2 output; the outputs are concatenated back to width 4 and multiplied by the output projection. Q, K or V: [L, E] = [4, 4] 0 1 2 3 head 0: columns 0..1 softmax(QKᵀ/√2) V head 1: columns 2..3 softmax(QKᵀ/√2) V concat [L, E] WO out_proj

Green and amber are the two heads' column slices. Heads never see each other's columns until the concatenation, and the output projection is the only place they interact.

Masks: padding, causal, boolean and additive

The mask M in the formula says which key positions a query may use. Two situations need it. Padding: sequences in a batch have different lengths and are padded to the longest; padded positions must be ignored. Causal: when a model predicts the next token, position i must not see positions after i, or training would simply copy the answer. A causal mask is a lower-triangular pattern; a padding mask blocks whole columns.

A mask can be given two ways. An additive (float) mask is added to the scores: 0 means allowed, −∞ means blocked (finite negative values act as a soft penalty, which is how relative-position biases such as ALiBi work). A boolean mask is converted to the additive form before use. The trap is the direction of True, which differs between the two main APIs:

APIArgumentTrue means
F.scaled_dot_product_attentionattn_mask (bool)this position takes part in attention (allowed)
nn.MultiheadAttentionattn_mask (bool)this position is not allowed to be attended to (masked out)
nn.MultiheadAttentionkey_padding_mask (bool), shape [N, S]this key is padding and is ignored (masked out)
either, float maskattn_mask (float)no meaning: the value is added to the score

PyTorch's own docstring warns about exactly this and tells you to invert a boolean mask (~mask) when moving from MultiheadAttention to scaled_dot_product_attention. Inside MultiheadAttention, _canonical_mask performs the conversion (torch.zeros_like(mask, dtype=...).masked_fill_(mask, float("-inf"))), so a boolean True becomes −∞. A key_padding_mask of shape [N, S] is reshaped to [N·h, 1, S] and added to attn_mask, so the two combine by addition.

Two details matter in practice. If a whole row is masked (a padded query position that is also not allowed to see anything), softmax has no allowed entry and the result is NaN; implementations avoid this by keeping at least one allowed entry per row or by zeroing such rows afterwards. And is_causal=True is a hint that lets fused kernels skip building the mask; for a non-square [L, S] mask it means the upper-left aligned triangle, which differs from the lower-right alignment you want when a cache supplies S > L keys (see CausalBias below).

Padding, causal and combined additive masks Three four-by-four additive masks over the tokens the, cat, sat, <pad>. The padding mask blocks the last column with minus infinity. The causal mask blocks every column to the right of the diagonal. The combined mask is their sum and blocks both. Allowed cells hold 0. padding (last key is pad) the cat sat <pad> 0 0 0 −∞ 0 0 0 −∞ 0 0 0 −∞ 0 0 0 −∞ + causal (lower triangle) the cat sat <pad> 0 −∞ −∞ −∞ 0 0 −∞ −∞ 0 0 0 −∞ 0 0 0 0 = combined M the cat sat <pad> 0 −∞ −∞ −∞ 0 0 −∞ −∞ 0 0 0 −∞ 0 0 0 −∞ green 0 = allowed grey −∞ = blocked rows: queries, columns: keys added to QKᵀ/√d before the softmax

Masks are added to the scores, so they stack by addition: −∞ plus anything is −∞, and two 0s stay 0.

Cross-attention

When Q comes from one sequence (length L) and K, V from another (length S), the score matrix is [L, S] and the same formula applies. An encoder–decoder translation model uses this in every decoder layer: queries are the target tokens produced so far, keys and values are the encoder's output for the source sentence, so each target word picks the source words it needs. The projection code reflects it: in _in_projection_packed the "encoder–decoder" branch (keys and values are the same tensor but the query is not) splits the packed weight into WQ and a stacked WKV and projects the source once. A padding mask in this setting is over the source (S) axis.

Grouped-query attention

In ordinary multi-head attention every head has its own K and V. In grouped-query attention (GQA) there are Hq query heads but only Hkv key/value heads, and each K/V head serves a group of Hq / Hkv query heads. Hkv = Hq is ordinary multi-head attention; Hkv = 1 is multi-query attention. The saving is memory during generation: the keys and values of all past tokens are cached, and the cache is proportional to Hkv. For a model shaped like the example in PyTorch's docstring (32 query heads, 8 key/value heads, head width 128), with 32 layers and 2-byte values, the cache costs 131,072 bytes per token against 524,288 with 32 key/value heads, a 4× saving. F.scaled_dot_product_attention(..., enable_gqa=True) accepts K and V with fewer heads; its reference code does key.repeat_interleave(query.size(-3) // key.size(-3), -3), so with 8 query heads over 2 K/V heads, query heads 0 to 7 use K/V heads 0, 0, 0, 0, 1, 1, 1, 1, and each K/V head is shared by four query heads. It requires Hq divisible by Hkv.

nn.MultiheadAttention in PyTorch

The module holds one packed parameter in_proj_weight of shape [3E, E] (rows 0..E−1 are WQ, the next E are WK, the last E are WV), an in_proj_bias of length 3E, and an out_proj linear layer [E, E]. Packing lets self-attention compute Q, K and V with one matrix multiplication and then split. If the key and value widths kdim and vdim differ from E, it stores three separate matrices instead (q_proj_weight, k_proj_weight, v_proj_weight) and in_proj_weight is None. All of them start from Xavier-uniform initialisation with zero biases. The parameter count is 4E² + 4E; for E = 512 that is 1,050,624.

Two code paths follow from the arguments. With need_weights=True (the default) the forward pass computes the scaled scores with baddbmm, a softmax, dropout and a bmm explicitly, because it must return the weights (averaged over heads unless average_attn_weights=False). With need_weights=False it calls F.scaled_dot_product_attention and returns None for the weights, which allows fused kernels. Separately, when the module is in eval mode, without gradients, with self-attention on batched batch_first=True input, an even number of heads and a few other conditions, forward takes a fast path to a fused inference kernel (and supports nested tensors for padding). The code records the first failing condition in why_not_fast_path; every condition must hold, so for example training mode or requires_grad inputs always use the ordinary path.

F.scaled_dot_product_attention and its backends

F.scaled_dot_product_attention(query, key, value, attn_mask=None, dropout_p=0.0, is_causal=False, scale=None, enable_gqa=False) is the primitive. Its docstring gives the mathematical reference (the "Efficient implementation equivalent to the following" block) and the function dispatches to one of several kernels that compute the same thing:

SDPBackendWhat it is
FLASH_ATTENTIONFlashAttention-2: tiles Q, K, V and fuses the softmax with the matmuls so the L × S matrix is never written to GPU memory.
EFFICIENT_ATTENTIONMemory-efficient attention (xformers lineage); more flexible about masks and shapes than flash.
CUDNN_ATTENTIONNVIDIA cuDNN's fused attention kernel.
MATHThe plain C++ implementation matching the reference code; works on every device and supports float64.

By default PyTorch picks the best backend whose input constraints are met (dtype, head width, mask type, device). To force or exclude backends, use the context manager with sdpa_kernel(SDPBackend.FLASH_ATTENTION): ... (or a list, with set_priority=True to order them); it restores the previous state on exit. The fused kernels give slightly different floating-point results from each other, and dropout_p is applied whenever it is above 0, so a module should pass dropout_p=self.p if self.training else 0.0.

CausalBias and FlexAttention

torch.nn.attention.bias provides causal_upper_left(L, S) and causal_lower_right(L, S), mask objects that fused kernels recognise without materialising the matrix. For L = 3 and S = 4 the allowed pattern is [1000, 1100, 1110] for upper-left and [1100, 1110, 1111] for lower-right, where the lower-right form is what you want when the query is the last L positions of a sequence whose cache has S entries. The two coincide when L = S.

FlexAttention (torch.nn.attention.flex_attention, a prototype feature) generalises the idea. Instead of a mask tensor you write small Python functions and a compiler fuses them into the attention kernel:

  • score_mod(score, b, h, q_idx, kv_idx) returns a modified score for one (query, key) pair, enough for relative-position biases, soft-capping or ALiBi.
  • mask_mod(b, h, q_idx, kv_idx) returns a boolean, True meaning the pair is allowed (the SDPA direction). create_block_mask(mask_mod, B, H, Q_LEN, KV_LEN) evaluates it per block and produces a BlockMask, so blocks that are entirely masked (causal upper triangle, sliding-window outside, other documents in a packed batch) are skipped, not computed and discarded. and_masks and or_masks combine them.

The call is flex_attention(query, key, value, score_mod=None, block_mask=None, scale=None, enable_gqa=False) on 4-D tensors [B, H, L, E], and it needs torch.compile to be fast.

ComponentShape/ParamsMeaningSource
nn.MultiheadAttention embed_dim, num_heads, dropout=0.0, bias=True, add_bias_kv=False, add_zero_attn=False, kdim=None, vdim=None, batch_first=False The reference multi-head layer from the original paper. Requires embed_dim % num_heads == 0; head width is embed_dim // num_heads. activation.py, __init__
in_proj_weight [3·E, E] (or separate q/k/v_proj_weight when kdim/vdim differ) Packed Q, K, V projection; bias in_proj_bias has length 3·E. _reset_parameters
out_proj [E, E] Linear layer applied to the concatenated heads (WO). __init__
forward query [N, L, E], key and value [N, S, E] with batch_first=True; returns (output [N, L, E], weights [N, L, S] or None) Decides fast path or general path; handles key_padding_mask, attn_mask, need_weights, is_causal. forward, fast-path checks
merge_masks attn_mask [L, S], key_padding_mask [N, S] → [N, h, L, S] Combines the two masks for the fast path. merge_masks
F.multi_head_attention_forward all projection weights, masks, need_weights, average_attn_weights The functional body: projection, head split, mask merge, attention, merge, output projection. functional.py, head split, attention
_in_projection_packed w [3E, E] → three [.., E] tensors Packed projection with separate branches for self-attention and encoder–decoder attention. functional.py
_canonical_mask bool or float mask → float mask Turns True into −∞ and keeps float masks as they are. functional.py
F.scaled_dot_product_attention q [N, .., Hq, L, E], k [N, .., H, S, E], v [N, .., H, S, Ev] The primitive; docstring holds the reference implementation, mask semantics and GQA constraints. functional.py
sdpa_kernel, SDPBackend backend or list, set_priority Context manager that enables only the listed backends. __init__.py, backends
causal_upper_left, causal_lower_right (L, S) → CausalBias Causal mask objects that kernels recognise without a dense tensor. CausalVariant, bias.py
flex_attention q [B, Hq, L, E], k [B, Hkv, S, E], v [B, Hkv, S, Ev]; score_mod, block_mask Attention with user-defined score modification and block-sparse masking, compiled into one kernel. flex_attention.py
create_block_mask, BlockMask (mask_mod, B, H, Q_LEN, KV_LEN, BLOCK_SIZE) Evaluates a mask function per block; empty blocks are skipped by the kernel. create_block_mask, BlockMask

Worked example

Single head first. Four tokens the, cat, sat, down with query and key width 2. For the third token ("sat") the query is [2, 0]; its scores against the four keys ([0, 1], [2, 0], [1, 1], [0, 2]) are the dot products [0, 4, 2, 0], divided by √2 to give [0, 2.83, 1.41, 0]; the softmax of that row is [0.04, 0.73, 0.18, 0.04]; and with values [1, 0], [0, 1], [1, 1], [0, 0] the output is [0.22, 0.91].

Now with two heads. Take E = 4, h = 2, and the input rows X below. The packed weight Win has 12 rows (3E); its first four rows are the identity (so Q = X), rows 5 to 8 give K, and rows 9 to 12 give V. The output projection is the identity so the merge is visible.

tokenXQ = X WQTKV
the[1, 0, 1, 0][1, 0, 1, 0][2, 0, 1, 1][1, 0, 1, 1]
cat[0, 1, 0, 1][0, 1, 0, 1][0, 2, 1, 1][0, 1, 1, 1]
sat[1, 1, 0, 0][1, 1, 0, 0][1, 1, 2, 0][1, 1, 0, 1]
<pad>[0, 0, 0, 0][0, 0, 0, 0][0, 0, 0, 0][0, 0, 0, 0]

Splitting each row into two halves gives head 0 columns 0 to 1 and head 1 columns 2 to 3. For the query "sat" the head-0 scores against the four head-0 keys are [1.41, 1.41, 1.41, 0] (already divided by √2), the weights [0.31, 0.31, 0.31, 0.07], and the head-0 output [0.62, 0.62]. Head 1 gives weights [0.25, 0.25, 0.25, 0.25] and output [0.50, 0.75]. Concatenated: [0.62, 0.62, 0.50, 0.75]; the identity output projection leaves it unchanged.

Masks change the answer. Without a mask the padding token (zeros) still receives weight 0.07 in head 0. Row "sat" of the final output, for the three cases:

maskhead-0 weights for "sat"merged output for "sat"
none[0.31, 0.31, 0.31, 0.07][0.62, 0.62, 0.50, 0.75]
padding[0.33, 0.33, 0.33, 0][0.67, 0.67, 0.67, 1]
padding + causal[0.33, 0.33, 0.33, 0][0.67, 0.67, 0.67, 1]

The causal mask changes nothing for the last real token ("sat" already sees everything before it) but changes the earlier ones: "the" puts all of its weight on itself ([1, 0, 0, 0]), and "cat" splits between "the" and itself ([0.20, 0.80, 0, 0]). Averaged over heads, as MultiheadAttention returns the weights by default, the "cat" row is [0.35, 0.65, 0, 0]. All numbers come from multihead_attention in ml_models_docs.py (stdlib only).

Implementation

import math
import torch
import torch.nn as nn
import torch.nn.functional as F

N, L, E, H = 1, 4, 4, 2
x = torch.randn(N, L, E)

# the reference formula, one head
def attention(q, k, v, mask=None):                    # q: [.., L, d], k, v: [.., S, d]
    scores = q @ k.transpose(-2, -1) / math.sqrt(q.size(-1))   # [.., L, S]
    if mask is not None:
        scores = scores + mask                        # additive: 0 allowed, -inf blocked
    return torch.softmax(scores, dim=-1) @ v

# the module: packed projection, heads, output projection
mha = nn.MultiheadAttention(E, H, batch_first=True)
mha.in_proj_weight.shape                              # [12, 4]: W_Q, W_K, W_V stacked
out, weights = mha(x, x, x)                           # out [1, 4, 4], weights [1, 4, 4] (mean over heads)
out, per_head = mha(x, x, x, average_attn_weights=False)   # weights [1, 2, 4, 4]

# masks, with the two opposite boolean conventions
padding = torch.tensor([[False, False, False, True]])         # MHA: True = ignore this key
causal_blocked = torch.triu(torch.ones(L, L, dtype=torch.bool), diagonal=1)   # MHA: True = not allowed
out, _ = mha(x, x, x, key_padding_mask=padding, attn_mask=causal_blocked)

q = k = v = x.view(N, L, H, E // H).transpose(1, 2)           # [N, H, L, E/H]
allowed = ~causal_blocked                                     # SDPA: True = allowed
y = F.scaled_dot_product_attention(q, k, v, attn_mask=allowed)             # [N, H, L, E/H]
y = F.scaled_dot_product_attention(q, k, v, is_causal=True)                # same causal mask, no tensor
y = y.transpose(1, 2).reshape(N, L, E)                                     # merge heads

# grouped-query attention: 8 query heads share 2 key/value heads
q = torch.randn(1, 8, L, 16)
k = v = torch.randn(1, 2, L, 16)
F.scaled_dot_product_attention(q, k, v, enable_gqa=True)

# choose a backend explicitly
from torch.nn.attention import SDPBackend, sdpa_kernel
with sdpa_kernel(SDPBackend.MATH):
    F.scaled_dot_product_attention(q, k, v, enable_gqa=True)

The same computation in plain Python, with the projection, head split and mask spelled out:

import math

def softmax(row):
    top = max(row)
    exps = [math.exp(s - top) for s in row]
    return [e / sum(exps) for e in exps]

def attend(Q, K, V, bias=None):                       # Q [L, d], K [S, d], V [S, dv]
    out = []
    for i, q in enumerate(Q):
        scores = [sum(a * b for a, b in zip(q, k)) / math.sqrt(len(q))
                  + (bias[i][j] if bias else 0.0) for j, k in enumerate(K)]
        w = softmax(scores)
        out.append([sum(w[j] * V[j][c] for j in range(len(V))) for c in range(len(V[0]))])
    return out

def multihead(X, W_in, W_out, heads, bias=None):      # W_in [3E, E], W_out [E, E]
    E = len(X[0])
    proj = lambda W: [[sum(a * b for a, b in zip(x, row)) for row in W] for x in X]
    Q, K, V = proj(W_in[:E]), proj(W_in[E:2 * E]), proj(W_in[2 * E:])
    d = E // heads
    cols = lambda M, h: [row[h * d:(h + 1) * d] for row in M]
    merged = [sum(rows, []) for rows in zip(*[attend(cols(Q, h), cols(K, h), cols(V, h), bias)
                                              for h in range(heads)])]
    return [[sum(a * b for a, b in zip(m, row)) for row in W_out] for m in merged]

Tradeoffs

  • Quadratic cost. The score matrix has L × S entries per head. Doubling the context quadruples the work and, in a naive implementation, the memory for the scores. Fused kernels (flash, memory-efficient) remove the memory term by never storing the matrix, but the arithmetic is still quadratic. Sliding-window masks, block-sparse patterns (FlexAttention) and linear-time alternatives such as state-space models exist to bend this.
  • No built-in order. Attention treats its input as a set: shuffle the rows and the outputs shuffle the same way. Order enters only through positional information added to the embeddings, or through a causal mask.
  • Heads versus width. More heads give more distinct patterns but each has fewer dimensions, so very narrow heads (below about 32 to 64) get less expressive. GQA and multi-query trade a little quality for much less cache memory at inference.
  • Generation cache. At inference, keys and values of earlier tokens do not change, so they are cached instead of recomputed; the cache grows linearly with context and dominates memory for long sequences. The cache is why decoding speed is memory-bound.
  • Pitfalls. Reversing the boolean mask direction between MultiheadAttention and SDPA silently attends to padding and masks the real tokens. Fully masked rows produce NaN. dropout_p in SDPA applies even in eval mode unless you pass 0. need_weights=True forces the slower explicit path, so request weights only when you need them. is_causal=True with a non-square mask is upper-left aligned. The head count must divide the embedding width.
  • When not to use it. For short, fixed-size inputs (small images, tabular rows) a convolution or plain linear layer is cheaper and often as good; attention pays off when long-range, content-dependent mixing matters.

Check yourself

Why divide the scores by √dk and not by dk?

The dot product of two independent unit-variance vectors of width dk has variance dk, so its standard deviation is √dk. Dividing by the standard deviation restores unit scale; dividing by dk would shrink the scores too much and flatten the softmax.

You pass the boolean mask tril(ones(L, L)) (True on and below the diagonal) as attn_mask to nn.MultiheadAttention. What happens?

It silently builds the wrong mask: in MultiheadAttention, True means "not allowed", so the lower triangle (each token and its past) is blocked and the first row is left with only the future. Use triu(..., diagonal=1) there; tril is the right mask for scaled_dot_product_attention, where True means allowed.

How many parameters does nn.MultiheadAttention(embed_dim=512, num_heads=8) have, and does the number of heads change it?

1,050,624: 3·512² for the packed input projection, 512² for the output projection, plus 3·512 + 512 biases. The head count does not change it; it only changes how the same E columns are grouped.

A decoder layer has 12 target tokens and the encoder produced 20 source tokens. What shape is the cross-attention weight matrix per head, and which sequence does a padding mask cover?

[12, 20] (L × S). The padding mask has one entry per source position (shape [N, 20]) because the weights are normalised over source positions.

Connections

References