2026/07/09 46.7k words

Transformer System #numinens#transformer#attention#self-attention#scaled-dot-product-attention#mask#softmax#qkv

M3.1 — Scaled Dot-Product Attention and Masks: The Score Path Inside One Attention Call

M3.1 — Scaled Dot-Product Attention and Masks: The Score Path Inside One Attention Call

0x00 From Recurrent Alignment To A Batched Score Path

M1.1 traced the long pressure that made attention useful: recurrent encoder-decoder systems had to expose a source sequence to a decoder without forcing every target decision through one fixed summary. M3.1 begins later and asks a narrower implementation question. Once a caller has produced query, key, and value tensors, what exact operation turns them into contextualized value rows?

The active input route is not M2.1 through M3.5. It is:

M2.1 versioned sequence package
    input_ids, valid lengths, segment boundaries, logical positions

M2.2 initial model state
    X0: [B, S, d_model]

M3.1 local attention primitive
    projected Q/K/V + visibility/bias objects -> contextual value rows

M3.1 deliberately studies the local primitive before its caller. It accepts already projected and head-shaped Q/K/V tensors. M3.2 later opens the caller and derives the projections, head split, cross-attention stream ownership, merge, and output projection. This is a bottom-up reading order, not a claim that Q/K/V appear without a producer.

How to read this page. The visible text follows one attention call from start to finish: where the query, key and value tensors come from and what shapes they have, how their scores are formed and scaled, how a mask removes illegal keys before softmax, how softmax turns each row of scores into probabilities, what dropout does to those probabilities, and how they mix the value rows. A three-token example worked by hand (0x09A) and a short reference implementation (0x09B) close that path. Everything else is still here, folded into labelled blocks: derivations, longer worked examples, code, alternative designs and their history, failure cases, framework and runtime details, and the checks behind the article. Each closed block says what it holds and when you would need it. Open a block when you need it; nothing has been removed.

Historical pressure: replace one fixed source summary with an addressable set

The chronology here is selective. It explains the interface inherited by the Transformer; it does not claim that one paper invented every component called attention.

Bahdanau, Cho, and Bengio identified the fixed-length encoder summary as a possible bottleneck and let each decoder step soft-search the encoder annotations. For one available decoder-side query state, the model computed one compatibility score per source annotation, normalized those scores, and formed a weighted context:

The important change was cardinality. The decoder no longer received only one source vector; it received an addressable set and constructed a different soft read for each target step. The paper describes this as an automatic soft search. Whether a learned row corresponds to a human alignment is an empirical claim, not part of the mathematical contract.

Luong, Pham, and Manning made several choices explicit: global attention can score the full source, local attention can restrict the candidate window, and dot, general, or concatenation-based score functions impose different representation and parameter contracts. The query still came from a recurrent decoder state, and source annotations still formed the memory. Attention was a retrieval operation attached to a recurrent sequence model.

The Transformer retained the normalized read but changed its role in the architecture. Learned projections create Q, K, and V roles; many query rows can be evaluated as matrix operations; the dot product is scaled before softmax; and explicit masks define which key rows each query may read. Self-attention uses states from one sequence on both sides of the read, while cross-attention keeps target queries and source key/value rows distinct.

What changed, and what did not

The recurrent and Transformer formulations share a read skeleton but not an execution graph:

ContractRecurrent alignmentTransformer attention
query ownerone available decoder stateone or many projected query rows
addressable memoryencoder annotationsprojected key rows from the same or another stream
score familyoften additive, dot, general, or concatscaled dot product in the canonical primitive
legal candidatesglobal source or a selected local windowexplicit boolean/additive visibility over every query-key pair
normalizationone distribution over source positionsone distribution over the key axis for every batch/head/query row
payload readweighted source contextweighted projected value rows
execution schedulenormally one recurrent target step at a timeall legal query rows can be evaluated together during training/prefill

The final row-wise contract is therefore still recognizable:

compatibility:
    how strongly this query matches each key

visibility:
    which query-key edges exist for this call

normalization:
    one probability row over legal keys

value mixing:
    use that row to retrieve paired value payloads

What the Transformer adds is a particular tensorization and score convention, not a different meaning for weighted retrieval. M3.1 derives each part rather than treating attention as an indivisible metaphor.

Owner question and local object

Owner question: How do projected Q/K/V tensors become masked attention probabilities and contextual value rows?

The complete local object is:

query, key, value
    -> scores = query @ key.T / sqrt(d_k)
    -> optional finite position_bias
    -> visibility mask before softmax
    -> p_attn = softmax(scores, dim=-1)
    -> optional dropout on p_attn
    -> output = p_attn @ value
Further reading Which Transformer Series articles supply this article's code path, mask cases, history and block context; open it to find the original handwritten sources. (about 60 words)

That path is the part A10 implements as attention(query, key, value, mask=None, dropout=None). A11 supplies the source-local causal, padding, training/inference, and packing cases that are reconciled at the score boundary. A1 supplies the generic read and historical lineage; A2 locates the primitive inside the larger Transformer block.

Scaled dot-product attention source code path from the handwritten Transformer series

How this was checked What this article takes from neighbouring articles and leaves to them, and the labels that mark how each claim is supported; open it to see where a related topic is covered. (about 340 words)

Owner contract: consumes, owns, and defers

BoundaryM3.1 contract
consumes from M2.1valid lengths, segment/sample boundaries, and logical position metadata from a versioned sequence package
consumes from M2.2model-width state rows; the M3.2 caller later projects them into Q/K/V
accepts from M3.3-M3.5optional finite position-bias or transformed Q/K objects without deriving those geometry methods here
ownslocal score construction, scaling, score-bias addition, visibility application, key-axis softmax, attention dropout, value mixing, local backward flow, and eager/fused semantic parity
defers to M3.2projection storage, head layout, cross-attention stream ownership, concatenation, and output projection
defers to M3.9construction of training batches, shifted supervision, and loss masks; M3.1 only consumes the visibility object at the score boundary
defers to M4.1/M4.3whole-model cost accounting and IO-aware exact-kernel derivation
defers to M5.1, M5.2, and M5.3persistent request/cache lifecycle in M5.1, multi-request admission and scheduling in M5.2, and physical paging/allocation in M5.3

This boundary resolves an apparent mask-ownership conflict. M3.1 does own the attention visibility taxonomy needed to define its score rows, including causal, padding, packed, prefix, window, and cache-shaped examples. It does not own the tokenizer/collator policy that produced lengths and segment IDs, nor the training loop that decides which labels contribute to loss.

Claim status and reading rules

LabelMeaning in this article
Source-localA1, A2, A10, or A11 directly supplies the explanation, code path, or figure.
Externally verifiedA primary paper supports the historical or algorithmic claim.
Official version-scopedA framework/API claim is tied to the cited PyTorch or Transformers version.
DerivationThe article proves an equation from stated assumptions; it is not attributed to a source merely because the final formula is familiar.
Rewrite interpretationThe owner boundary, ledger, or debugging rule is a synthesis used to make implementations auditable.
Source-reportedA paper’s observation or benchmark remains attached to that paper and setup.
UncertainEvidence is insufficient for a general claim; the article must preserve the limitation instead of filling it with intuition.

The scale, bias, mask, softmax axis, dropout mode, and value multiplication are separate contracts. Same-shape errors survive when those objects are collapsed into one opaque attention label. The remaining sections therefore reconstruct the path one tensor transition at a time.

0x01 The code spine

This is the local object for the whole article:

def attention(query, key, value, mask=None, dropout=None, position_bias=None):
    # query: [B, h, q_len, d_head]
    # key:   [B, h, k_len, d_head]
    # value: [B, h, k_len, d_value]

    d_k = query.size(-1)

    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
    # scores: [B, h, q_len, k_len]

    if position_bias is not None:
        scores = scores + position_bias

    if mask is not None:
        scores = scores.masked_fill(mask == 0, -float("inf"))

    p_attn = scores.softmax(dim=-1)
    # A production eager path may compute this softmax in float32.
    # p_attn: [B, h, q_len, k_len]

    if dropout is not None:
        p_attn = dropout(p_attn)

    out = torch.matmul(p_attn, value)
    # out: [B, h, q_len, d_value]

    return out, p_attn

The original Harvard code uses -1e9 in masked_fill. M3.1 uses -inf to state the semantics: a masked key should be outside the distribution, not merely less preferred.

Everything below is a line-by-line reconstruction of this function.


0x01A Immutable attention-call manifest

A real call fixes more than the code above shows: separate head counts for queries and for keys and values, memory layout, which mask value means “legal”, the precision of each stage, the dropout and random-number state, and which backend actually ran. This section lists those fields and code that rejects an incomplete call; open it when you need to reproduce or compare one specific call. The rest of the page does not depend on it.

Implementation and runtime contracts Every field that identifies one concrete call (head mapping, strides, mask polarity, finite bias, precision per stage, dropout state, requested and observed backend, data lineage), with a schema and validation code. (about 1,670 words, 7 code listings, 2 equations)

The code spine is readable because it suppresses decisions that a real call cannot suppress. The symbol h does not say whether K/V were stored with the same number of heads as Q. The name mask does not say whether True means legal or blocked. float16 inputs do not determine the accumulation dtype of a fused kernel. An API name such as scaled_dot_product_attention does not by itself identify the kernel that ran.

Before comparing formulas, implementations, or timings, freeze one call as an immutable manifest. “Immutable” means that these fields describe one concrete execution and cannot change halfway through a derivation or benchmark. It does not mean that every model and backend must choose the same values.

Logical axes and effective head ownership

Use distinct symbols even when a toy self-attention call makes several of them equal:

SymbolMeaning
Bbatch rows owned by this call
Hqquery heads that produce output rows
Hkvphysically represented key/value heads before any grouping adapter
Qquery-row count (q_len)
Kkey/value-row count (k_len)
Dkquery/key feature width used by the score dot product
Dvvalue feature width mixed into each output row

The immutable tensor ledger is:

Object and logical shapeImmutable relation
query
[B,Hq,Q,Dk]
owns output rows and the scale width
key_physical
[B,Hkv,K,Dk]
key row j addresses value row j
value_physical
[B,Hkv,K,Dv]
shares B, Hkv, and K with physical keys
head_map
[Hq] integers in [0,Hkv)
maps each query head to one KV head
key_effective
logically [B,Hq,K,Dk]
key_physical[:, head_map]; it may remain implicit
value_effective
logically [B,Hq,K,Dv]
value_physical[:, head_map]; it may remain implicit
scores
logically [B,Hq,Q,K]
one query-by-key row per batch and query head
visible
broadcastable to [B,Hq,Q,K]
semantic boolean relation; True means legal
position_bias
broadcastable to [B,Hq,Q,K]
finite score preference, not legality
probabilities
logically [B,Hq,Q,K]
pre-dropout softmax over K
dropped_probabilities
logically [B,Hq,Q,K]
training-time stochastic mixing weights
output
[B,Hq,Q,Dv]
one contextual value row per query row

For ordinary multi-head attention, Hq == Hkv and head_map[h] = h. The code spine’s shared h axis is exactly this case, or a call after K/V have already been expanded to the effective query-head axis. With grouped-query or multi-query attention, M3.2 owns physical projection/storage and the mapping. The M3.1 equation consumes that mapping. A backend may implement it by a view, an expand/repeat adapter, indexed grouping, or native grouped-head execution. Only the logical effective K/V objects are invariant.

This distinction matters for accounting. Repeating K/V into an eager reference tensor is a semantic oracle, not proof that a production kernel stores or moves the repeated tensor.

Storage orientation is not only a shape

The article’s canonical boundary is head-major: [B,H,sequence,feature]. The manifest must additionally record physical shape, stride, device, and dtype for Q, K, and V. Those fields answer different questions:

logical axis order:
    what each index means

physical strides:
    how the current tensor view walks storage

contiguity:
    whether the next operator can consume that view directly

materialization:
    whether an object receives its own storage at all

key.transpose(-2, -1) denotes a logical [B,H,Dk,K] view for the score matmul. It is not a claim that checkpoints or KV caches persist a transposed key. Likewise, scores and probabilities are logical objects even when an online fused kernel never writes their full [Q,K] matrices to HBM. M4.3 owns the schedule and IO proof; M3.1 freezes the values that schedule must preserve.

The manifest therefore distinguishes:

qkv_layout = "BHQD"
key_score_view = "transpose last two logical axes"
q_stride, k_stride, v_stride = observed runtime strides
q_contiguous, k_contiguous, v_contiguous = observed booleans
scores_materialized = true | false | backend_internal
probabilities_materialized = true | false | backend_internal

Two calls with equal shapes but different strides can dispatch differently or pay different copies. Two kernels can be semantically equivalent while only one materializes scores. Shape equality is necessary, not a complete execution identity.

Visibility and finite bias are separate objects

M3.1’s canonical semantic object is visible, a boolean relation where True means the query-key edge exists. It must broadcast to [B,Hq,Q,K]. A separate query_valid relation, normally broadcastable to [B,1,Q,1], states whether an output row will be consumed. Key visibility and query validity are not interchangeable.

position_bias is finite and changes preference among legal keys. Backend adapters may encode illegality as an additive tensor containing 0 and -inf, but that tensor is then a visibility encoding, not the finite position-bias object. The canonical composition is:

Here i indexes a query row and j indexes a key row. In implementation-oriented form:

scores = (query @ key_effective.transpose(-2, -1)) * (Dk ** -0.5)
biased_scores = scores if position_bias is None else scores + position_bias
masked_scores = biased_scores.masked_fill(~visible, -torch.inf)
probabilities = softmax(masked_scores, dim=-1)

Applying authoritative visibility after the finite preference makes the separation obvious: no later finite positive bias can revive an illegal edge. Loop 4 derives causal, padding, packed, prefix, window, and cache predicates in full. Loop 2 only freezes the interface every one of those designs must reach.

API adapters must name their polarity rather than passing an undocumented tensor called mask:

Boundary objectPolarity and adapter into the score path
M3.1 semantic visibleTrue means legal; use directly with masked_fill(~visible, -inf)
Harvard-style source maskTrue means legal; normalize with visible = mask.bool()
PyTorch functional SDPA boolean attn_maskTrue participates; pass semantic visible after broadcast checks
PyTorch MultiheadAttention binary masksTrue is blocked/ignored in the documented mask arguments; invert when converting to semantic visible
additive legality bias0 is legal and -inf is blocked; add as backend visibility encoding, not finite position bias

The PyTorch functional SDPA polarity and dropout behavior above are official version-scoped contracts, not universal conventions for every attention API. The all-masked-row policy is also a required manifest field: reject, sanitize_then_zero_invalid_query, or a named backend behavior that has been tested. Leaving it implicit is not a valid call specification.

Numerical state is a ledger, not one dtype label

At minimum, record:

FieldWhat it controls
q_dtype, k_dtype, v_dtypeoperand storage presented to the local call
score_accumulation_dtypedot-product accumulation, if the backend exposes or guarantees it
bias_dtypeconversion used before adding finite bias or additive visibility
softmax_accumulation_dtypeexponent/reduction state for normalization
probability_dtypedtype presented to value mixing if materialized
value_accumulation_dtypeaccumulation policy for probability-value multiplication
output_dtypedtype returned at the M3.1 boundary

An eager policy may explicitly compute softmax in float32 and cast its result back to the query dtype. The current version-scoped LLaMA eager path used later in this article does exactly that. A fused backend may preserve the same output contract with a different online reduction and without exposing intermediate dtypes. In that case the manifest records a documented backend guarantee or backend_internal; it must not guess float32 merely because the eager oracle uses it.

The same rule applies to arithmetic modes such as TF32, reduced-precision reductions, autocast, and deterministic-algorithm settings. They are execution state, not properties derivable from the attention equation.

Dropout mode includes RNG and returned-weight semantics

dropout_p alone does not identify the operation. Freeze this tuple:

training: true | false
dropout_p: probability in [0,1)
dropout_location: attention probabilities before value mixing
rng_provenance: generator/device plus reproducibility state
returned_weights: none | pre_dropout | post_dropout

For PyTorch’s functional SDPA, a nonzero dropout_p applies dropout even when the surrounding module is in evaluation mode; the caller must pass 0.0 for evaluation. Different kernels are not required to consume random numbers in the same order, so a common seed does not by itself prove bitwise parity. Semantic parity tests either disable dropout or compare the stated stochastic contract rather than one sampled mask.

Backend identity has requested and observed parts

backend = "SDPA" is incomplete because the functional entry point can dispatch among implementations. A reproducible call manifest records:

framework and exact version or source revision
model library and exact version or source revision
entry point
requested backend policy
observed selected backend
device type and architecture
autocast / deterministic / arithmetic-mode state
is_causal flag and external mask kind
dropout mode
return-attention-weights request
compile or graph-capture state

PyTorch documents automatic SDPA backend selection and provides sdpa_kernel to constrain eligible implementations. The manifest keeps requested_backend and selected_backend separate because eligibility is not proof of dispatch. If selection was not observed, record unobserved; do not relabel it as FlashAttention from timing intuition. Loop 8 will compare the runtime paths in detail.

Source-to-tensor lineage

Every local object must have one producer and one consumer contract. The lineage is:

Producer and objectM3.1 normalization and downstream use
M2.1 sequence package
valid lengths, segment/sample boundaries, logical positions
compile policy into semantic visible metadata or a backend-equivalent relation; consume at the score-boundary visibility adapter
M2.2 initial state
[B,S,d_model] state rows
no normalization inside M3.1; M3.2 owns the projection caller
M3.2 projection/head owner
physical Q/K/V plus query-to-KV head mapping
expose canonical axes and the effective head relation; consume in score and value paths
M3.3-M3.5 geometry owners, including M3.4 RoPE
transformed Q/K and/or finite position bias
verify axis, device, broadcast, and finite-bias contracts; consume before authoritative visibility
M3.9 training execution
batch visibility policy, training flag, dropout/RNG state
separate attention visibility from label/loss masks; consume in normalization and stochastic value mixing
M3.10 single-request execution
current query extent, effective key extent, cache-position relation
represent one concrete local call without mutating cache here; consume as the prefill/decode score row
framework/model adapter
API mask encoding and backend request
translate from the semantic manifest, record observed dispatch, and execute eager, SDPA, or model-integrated attention

The handwritten A10 code supplies the local Q/K/V score path. A11 supplies the mask creation and application lineage. The official PyTorch and version-pinned Transformers sources qualify API polarity, dropout, dtype, and dispatch claims. None of those sources replaces the manifest: the manifest states which subset of their choices applies to this call.

Concrete manifest schema and validation gate

One auditable call can be serialized as:

attention_call_v1:
  axes: {B, Hq, Hkv, Q, K, Dk, Dv}
  tensors:
    query:          {shape, stride, dtype, device, contiguous}
    key_physical:   {shape, stride, dtype, device, contiguous}
    value_physical: {shape, stride, dtype, device, contiguous}
    head_map:       {shape: [Hq], range: [0, Hkv)}
  visibility:
    semantic_polarity: true_means_legal
    shape: broadcastable_to_[B,Hq,Q,K]
    query_valid_shape: broadcastable_to_[B,1,Q,1]
    all_masked_row_policy: reject | sanitize_then_zero_invalid_query | tested_backend_contract
  bias:
    role: finite_preference_only
    shape: broadcastable_to_[B,Hq,Q,K]
  numerical:
    qkv_storage_dtype
    score_accumulation_dtype
    softmax_accumulation_dtype
    probability_dtype
    value_accumulation_dtype
    output_dtype
  dropout: {training, p, location, rng_provenance, returned_weights}
  backend:
    {framework_revision, model_revision, entrypoint,
     requested_backend, selected_backend, device_arch,
     autocast, deterministic, arithmetic_mode, is_causal,
     external_mask_kind, compile_state}
  materialization: {scores, probabilities}

Reject the manifest before execution if any invariant is missing:

assert query.shape == (B, Hq, Q, Dk)
assert key_physical.shape == (B, Hkv, K, Dk)
assert value_physical.shape == (B, Hkv, K, Dv)
assert query.device == key_physical.device == value_physical.device
assert query.dtype == key_physical.dtype
assert head_map.shape == (Hq,)
assert 0 <= head_map.min() and head_map.max() < Hkv
assert visible.dtype == torch.bool
assert torch.broadcast_shapes(visible.shape, (B, Hq, Q, K)) == (B, Hq, Q, K)
assert torch.broadcast_shapes(query_valid.shape, (B, 1, Q, 1)) == (B, 1, Q, 1)
if position_bias is not None:
    assert torch.broadcast_shapes(position_bias.shape, (B, Hq, Q, K)) == (B, Hq, Q, K)
    assert torch.isfinite(position_bias).all()
assert semantic_polarity == "true_means_legal"
assert 0.0 <= dropout_p < 1.0
assert dropout_location == "probabilities_before_value_mixing"
assert requested_backend is not None
assert selected_backend in observed_backend_or_unobserved

These assertions do not prove the attention implementation correct. They make the object being proved stable. Later loops can now derive scaling, masks, softmax, backward, and backend parity without silently changing axes, polarity, precision, or execution identity between sections.


0x02 The local inputs: query, key, value

Inside one attention call, Q/K/V are already tensors. M3.1 starts after projection:

# Already projected and head-shaped by the caller.
query = ...  # [B, h, q_len, d_head]
key   = ...  # [B, h, k_len, d_head]
value = ...  # [B, h, k_len, d_value]

The three names carry different roles.

ObjectRoleSequence axis
queryrows asking for informationq_len
keyrows being matched against the queriesk_len
valuepayload rows mixed after attention weights are knownk_len

The key and value sequence lengths must agree because each key row points to a value row:

key[j] and value[j] are a pair.

The query length can differ from the key/value length. This is normal:

self-attention:
    q_len = k_len

cross-attention:
    q_len = target length
    k_len = source length

one-token decode:
    q_len = 1
    k_len = cached past + current token

That last decode case is only a shape boundary here. M3.10 owns the request-local autoregressive transition and its logical cache-position/frontier requirements. M5.1 and M5.3 own persistent cache publication/lifecycle and physical allocation or mutation.

From hard lookup to differentiable retrieval

The Q/K/V names are easier to derive from a memory lookup than to memorize from the final equation. A hard table stores key-value pairs:

(k_1, v_1), (k_2, v_2), ..., (k_m, v_m)

Given query q, exact lookup returns the value paired with one matching key. That operation has two limitations for a learned sequence model: the address must match exactly, and choosing one entry is discontinuous. Attention replaces the discrete choice with learned compatibility scores:

then normalizes the scores into non-negative weights:

and reads a mixture:

This derivation explains why keys and values share a sequence axis but need not share a feature representation. The key answers “should this slot be read?”; the value answers “what should this slot contribute?” If a single vector were forced to serve both roles, the representation useful for matching would also have to be the representation written into the output.

In self-attention, one state row x_j produces both roles through independent learned projections:

The source row is the same; the learned coordinate systems are not. In cross-attention, x_i can be target-side state while the key/value source is encoder memory. The role contract survives even when the originating streams differ.

Q, K, and V projections separate the query, matching, and payload roles before attention

The source figure contains Chinese labels from the handwritten article. Read the three branches as learned role projections, not as three copies with fixed human meanings.

Worked detail Three readings of the query, key and value roles (soft database lookup, sequence alignment, value-space reconstruction) and where each one misleads; open it if the role names still feel arbitrary. (about 390 words, 3 code listings, 6 equations)

Three useful readings of the same Q/K/V contract

A10 approached Q/K/V from several directions. They are not competing definitions. Each highlights one invariant and becomes misleading when pushed too far.

Differentiable database reading

The database analogy makes the key-value pairing concrete:

query:
    a request represented in the learned matching space

key:
    an address or descriptor used to decide whether a slot is relevant

value:
    the payload retrieved if that slot receives weight

A10 database-style Q/K/V lookup in which one query scores candidate keys before retrieving their paired values

The source figure uses a shopping-query example and Chinese labels. Its useful claim is structural: candidate keys are scored first, and the values remain paired with those keys. Attention differs from a conventional database in at least four ways:

addresses are learned vectors rather than exact identifiers;
matching is dense similarity rather than equality;
the read normally mixes many values rather than returning one row;
the query, keys, values, and scoring projections train end to end.

The analogy therefore explains the read operation, not the storage engine. A Transformer attention layer does not sort scores, retrieve physical rows by ID, or necessarily materialize a top-k result. Dense attention evaluates every legal query-key pair and uses a differentiable weighted sum.

Sequence-to-sequence alignment reading

In encoder-decoder attention, the decoder state supplies a query while encoder states supply keys and values. Each target row obtains its own alignment over source positions:

The score row S[i,:] is target-indexed and source-normalized. This reading is historically important because it connects Bahdanau/Luong alignment to the Transformer primitive. It does not imply that every learned cross-attention head is a clean lexical word-alignment model. Values may represent syntax, position, discourse, or features that do not have one human label.

Contextual reconstruction reading

For self-attention, the output row is reconstructed from value rows:

This makes the operation a content-dependent linear combination in value space. The coefficients are nonlinear functions of Q and K, so the complete map from input states to output states is not linear. Conditional on a fixed probability matrix P, however, the value path is linear:

That distinction matters for both interpretation and optimization. A heatmap shows the coefficients used for the current input. It does not show the value directions being moved, the output projection applied later, or the complete nonlinear function learned by the block.

The three readings agree on the contract:

Q/K decide where information is read from.
V decides what information crosses the selected edges.
The output remains indexed by queries.

They do not assign immutable semantics to vector coordinates. Query, key, and value are roles in one call, not permanent object types.

From one query to all query rows

For one query row, dot-product scores are:

Stack the memory keys as rows of matrix K. The complete score row becomes:

Now stack all queries as rows of Q:

The matrix multiplication is therefore not an arbitrary optimization applied after defining attention. It is the batched form of every query comparing with every available key. Row i, column j remains the pair (q_i, k_j).


0x03 Score matrix: QK^T / sqrt(d_k)

Before the Transformer, attention also used additive (Bahdanau) and bilinear, or “general” (Luong), score functions. The folded blocks below compare them; the visible text keeps the score computation, its shapes, and why the Transformer chose the scaled dot product.

The first real computation is:

d_k = query.size(-1)
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)

Shape trace:

query:              [B, h, q_len, d_head]
key:                [B, h, k_len, d_head]
key.transpose:      [B, h, d_head, k_len]

scores:             [B, h, q_len, k_len]

The two middle sequence axes now have different meanings:

scores[..., i, j]
    = score for query row i against key row j

Rows are query positions. Columns are key/value positions.

This is one of the main objects later cost articles care about:

scores: [B, h, q_len, k_len]

When q_len = k_len = S, the score object is square: [B,h,S,S]. When cross-attention or decode changes the key axis, the object is not square. Treating all attention scores as [S,S] is a common source of bugs.

How this was checked How the score is split into compatibility, multiplier and finite bias for the rest of the article; open it if you want the exact notation behind later sections. (about 190 words, 2 equations)

Loop 3 contract: compatibility first, scaling second

Every attention family in this section must produce one scalar compatibility for every legal query-key pair. Freeze the unscaled compatibility as u and the multiplicative logit scale as alpha:

Here b is a finite preference such as position bias. Visibility is still the separate boolean relation frozen in Loop 2. This ordering prevents three different design choices from being hidden inside one word such as “score”:

Contract fieldFrozen meaning
compatibility familythe pair function f(q,k) before finite bias, visibility, or softmax
logit multiplier alphainverse temperature applied to the compatibility
temperature tautau = 1 / alpha when alpha > 0
canonical Transformer choicedot compatibility with alpha = Dk^(-1/2)
output contract[B,Hq,Q,K], irrespective of how the pair function is evaluated

Changing the family changes the model. Moving the same positive scalar between algebraically equivalent locations should not change the real-number model, although finite-precision execution can change. Loop 3 owns that distinction. It does not own the complete softmax algorithm in 0x07, position-dependent bias in M3.3, or context-length-dependent rescaling in M3.5.

Alternatives and history A table of score functions (dot, scaled dot, bilinear, additive, cosine) and why concatenation without a nonlinearity cannot rank keys; open it to compare the Transformer's choice with its predecessors. (about 230 words, 2 equations)

Alternative score functions and the Transformer choice

Scaled dot product is not the only differentiable compatibility function. The attention lineage used several choices:

Score familyMathematical and execution contract
dot productu = q^T k; equal Q/K widths; no score-local parameters; all pairs are Q @ K^T
scaled dot products = alpha q^T k, canonically alpha = Dk^(-1/2); same pair program plus a fixed inverse temperature
bilinear / Luong generalu = q^T W_a k; Q/K widths may differ; W_a owns the cross-coordinate metric
additive / Bahdanauu = v_a^T tanh(W_q q + W_k k + b_a); projected pair features and a nonlinear reduction
cosine / normalized QKu = q_hat^T k_hat, optionally s = gamma u; norms are removed and the effective temperature is explicit

A10 also lists the shorthand w^T[q;k] under concatenation similarity. Taken literally without a nonlinearity, it is not an interactive additive score:

For one query row, w_q^T q is constant over every key. Row-wise softmax is invariant to that constant, so the query-dependent term cancels. Bahdanau’s additive score and Luong’s concat score insert a learned nonlinear hidden layer; that hidden layer is what lets query and key jointly change the ranking. The A10 shorthand is retained as source history, not as the implementation contract.

Additive and multiplicative attention are legitimate design alternatives, not failed predecessors. The Transformer chose scaled dot product because projected queries and keys can be compared with a dense matrix multiplication, which is simple and accelerator-friendly, while the scale controls dimension-dependent logit growth. The choice couples an inductive assumption to a systems property: compatibility is represented by an inner product in a learned space, and all pairs can be evaluated by GEMM.

Alternatives and history Each score family derived from its assumption, with tensor shapes, parameter counts, one batched implementation and the limits of the Britz et al. comparison; open it when choosing or reimplementing a score function. (about 1,120 words, 5 code listings, 20 equations)

The alternatives also show what the projections are doing. A bilinear score can absorb W_a into one side. Separate W_Q and W_K let the Transformer learn two comparison spaces before the dot product. This does not make every score family equivalent, but it makes scaled dot product much more expressive than comparing raw input embeddings.

Deriving the score families from their interaction assumptions

The formulas look similar when reduced to a table, but they encode different pairwise interaction structures.

Dot product: coordinate agreement in a learned space

For q,k in R^d, the dot product is:

It rewards dimensions that have the same sign and large magnitude. Learned Q/K projections decide what those dimensions mean. There are no score-specific parameters after projection, and all query-key pairs become one GEMM:

This factorization constrains a single head’s unnormalized score matrix. If Q in R^{Q_len x d_k} and K in R^{K_len x d_k}, then:

Softmax is nonlinear and value/output paths add capacity, so this is not a rank bound on the whole attention module. It is a precise property of the raw logit matrix.

The geometric decomposition separates angle from magnitude:

For fixed query i, its norm multiplies the entire score row and therefore acts like a query-specific inverse temperature. A key norm changes one column relative to other keys. The cosine term carries angular interaction. Calling a dot product “only cosine similarity” drops two trainable magnitude channels.

Bilinear score: learn the interaction metric explicitly

Luong’s general score is:

If W_a is unconstrained, it can rotate, scale, and couple dimensions before the comparison. In a Transformer with independent Q/K projections, part of that freedom can be reparameterized into the projections:

This identity explains why a separate W_a is often unnecessary in the scaled-dot-product design. It does not prove every bilinear parameterization trains identically: initialization, sharing, dimensions, regularization, and factorization rank differ.

For unequal widths Dq and Dk, choose W_a in R^[Dq,Dk]. The batched tensor program is:

query:             [B,H,Q,Dq]
W_a:               [H,Dq,Dk]      # or shared [Dq,Dk]
query_metric:      [B,H,Q,Dk]
key:               [B,H,K,Dk]
score:              [B,H,Q,K]

query_metric = einsum("bhqd,hde->bhqe", query, W_a)
score        = query_metric @ key.transpose(-2, -1)

The score-local parameter count is H * Dq * Dk when every head owns a full metric. Association order matters for work and temporary storage even though (Q W_a)K^T = Q(W_a K^T) in exact arithmetic.

The following A10 figure rewrites projected dot-product attention as a learned QK circuit. It is the bridge between Luong’s explicit bilinear metric and the Transformer’s factorized W_Q W_K^T metric; the causal prefix shown in the figure is one visibility policy, not part of the bilinear identity.

Projected QK factorization and per-head scaling reused from A10 and the Transformer inner-workings primer

Visual provenance: reused from A10, where it is attributed to Ferrando et al., A Primer on the Inner Workings of Transformer-based Language Models. The semantic job here is the factorization W_Q W_K^T, not an empirical claim.

Additive score: a learned nonlinear pair function

Bahdanau attention uses a small network:

For all pairs, projected query terms broadcast down the key axis and projected key terms broadcast across the query axis:

q_term: [B,Q,1,D_a]
k_term: [B,1,K,D_a]
pair:   [B,Q,K,D_a]
score:  [B,Q,K]

The nonlinear pair tensor gives additive attention a different interaction family, but it also introduces a potentially large [B,Q,K,D_a] intermediate. This is why “similar theoretical complexity” does not mean identical hardware behavior. Dot-product attention maps its central pair operation directly onto matrix multiplication.

With heads and unequal input widths, the full tensor trace is:

query:             [B,H,Q,Dq]
key:               [B,H,K,Dk]
W_q:               [H,Da,Dq]
W_k:               [H,Da,Dk]
v_a, b_a:          [H,Da]

q_term:            [B,H,Q,1,Da]
k_term:            [B,H,1,K,Da]
pair_hidden:       [B,H,Q,K,Da]
score:              [B,H,Q,K]

For independent per-head parameters, the score-local parameter count is:

where the final 2Da accounts for b_a and v_a. An implementation can tile or fuse the pair operation instead of storing the complete five-dimensional tensor, but it cannot silently replace the nonlinear pair function with GEMM and still claim the same score family.

Cosine score: remove vector norm from compatibility

Cosine similarity with an explicit zero-norm policy is:

It bounds raw similarity to [-1,1] when norms are nonzero. A temperature or learned scale is usually needed to give softmax an appropriate logit range. Normalizing Q/K also changes the gradient: radial changes no longer affect the score in the same way as angular changes.

The bounded interval is not automatically sharp enough. If one of K logits is +1 and all others are -1, then without an extra multiplier the largest possible probability is:

At K=128, this is only about 0.055. QKNorm is one concrete design point: it L2-normalizes Q and K and replaces the fixed Dk^(-1/2) multiplier with a learned scale. That is a changed model parameterization, not a different placement of the canonical fixed scale.

One batched implementation oracle for all four families

The following pseudocode makes the common output axis and the family-specific temporary state explicit:

def compatibility(query, key, family, params, eps=1e-6):
    # query: [B,H,Q,Dq], key: [B,H,K,Dk]
    if family == "dot":
        assert query.shape[-1] == key.shape[-1]
        return query @ key.transpose(-2, -1)

    if family == "bilinear":
        # W: [H,Dq,Dk]
        q_metric = einsum("bhqd,hde->bhqe", query, params.W)
        return q_metric @ key.transpose(-2, -1)

    if family == "additive":
        # Wq: [H,Da,Dq], Wk: [H,Da,Dk], v/b: [H,Da]
        q_term = einsum("bhqd,had->bhqa", query, params.Wq)
        k_term = einsum("bhkd,had->bhka", key, params.Wk)
        pair = tanh(q_term[:, :, :, None, :]
                    + k_term[:, :, None, :, :]
                    + params.b[None, :, None, None, :])
        return einsum("bhqka,ha->bhqk", pair, params.v)

    if family == "cosine":
        assert query.shape[-1] == key.shape[-1]
        q_hat = query / norm(query, dim=-1, keepdim=True).clamp_min(eps)
        k_hat = key / norm(key, dim=-1, keepdim=True).clamp_min(eps)
        return q_hat @ k_hat.transpose(-2, -1)

    raise ValueError(f"unknown score family: {family}")

Only the dot-family result can be handed directly to the canonical SDPA primitive. Bilinear may be reducible to transformed Q or K before SDPA. Additive requires a different score kernel or an explicit pair program. Cosine requires normalization and, usually, an explicit scale. Backend availability is therefore part of the design choice, not an afterthought.

What the historical comparison does and does not establish

A10 reused the following table from Britz et al.’s large NMT architecture study. It is useful evidence that additive and multiplicative families can be competitive under a matched recurrent NMT experiment. It is not a universal ranking of Transformer attention mechanisms, and the Mul rows are not the canonical scaled Transformer primitive.

Source-reported additive and multiplicative attention comparison reused from A10

Visual provenance: A10 attributes this table to Britz et al., Massive Exploration of Neural Machine Translation Architectures. The table is source-reported, task-specific evidence. State width, recurrent architecture, training recipe, and measurement uncertainty prevent a general winner claim.

The implementation choice is therefore not just “which formula has fewer symbols.” It chooses:

the pairwise function class;
where trainable parameters live;
which pair tensors are materialized;
which operation accelerators execute efficiently;
how logit magnitude is controlled.

For M3.1, scaled dot product remains the reference because it is the model contract used by the canonical Transformer path. The alternatives are retained to explain the design space, not treated as interchangeable aliases.


0x04 Why the scale exists

The original Transformer defines scaled dot-product attention as:

The division by sqrt(d_k) is not decoration.

If the query and key components are roughly independent with unit variance, then a dot product sums d_k products. The variance of the unscaled dot product grows with d_k, and the standard deviation grows like:

Large-magnitude logits push softmax toward saturation:

one score dominates
other probabilities approach zero
gradients through softmax shrink

Scaling by sqrt(d_k) keeps the logit scale less sensitive to the head dimension. In per-head attention, the scale is the per-head width:

d_k = query.size(-1)  # d_head, not d_model

Using d_model here when query is already split into heads returns a tensor with the same shape and the wrong temperature.

Variance derivation, assumptions, and temperature interpretation

First freeze the notation. Frameworks often call alpha a scale, while statistical mechanics and sampling discussions usually name its reciprocal the temperature:

The canonical Transformer chooses:

This article uses multiplier for alpha and temperature for tau. Otherwise phrases such as “increase the scale” are ambiguous: increasing alpha sharpens logits, while increasing tau flattens them.

Let one unscaled logit be:

Assume for the initialization argument that components are independent across r, that q_r and k_r have mean zero, and that both have variance one. Then:

Also:

The variance of the sum is therefore:

and its standard deviation is sqrt(d_k). Dividing produces:

Derivation The variance result for general means and covariances, and why dependent Q and K need more than second moments; open it if you doubt the unit-variance assumption. (about 230 words, 6 equations)

The unit-variance case is only the shortest corollary. Suppose instead that Q and K are independent random vectors with means mu_q, mu_k and covariance matrices Sigma_q, Sigma_k. Then:

and:

For zero means this reduces to tr(Sigma_q Sigma_k). If coordinates are isotropic with variances sigma_q^2 and sigma_k^2, it becomes:

so the canonical multiplier leaves variance sigma_q^2 sigma_k^2, not necessarily one. The fixed 1/sqrt(d_k) term removes dimension-driven growth; it does not normalize arbitrary learned Q/K statistics.

If Q and K are statistically dependent, second moments alone generally do not determine the variance of their product sum. One needs cross moments or a stronger distributional assumption. This matters in self-attention because Q and K can originate from the same residual state. The Transformer footnote is an initialization-scale argument under independence, not a theorem about every trained logit distribution.

The assumptions are an initialization-scale explanation, not a claim that trained Q/K components remain independent standard-normal variables. Learned projections, normalization, position mechanisms, and data create correlations. The fixed scale still removes the most direct dimensional growth from summing d_k products.

From logit spread to a small softmax Jacobian

The original Transformer paper motivates scaling by the small gradients of softmax when unscaled dot products become large. The local statement can be made exact. For one row p = softmax(s), the Jacobian is:

If one logit exceeds all others by gaps that tend to infinity, p approaches a one-hot vector. Every entry of diag(p)-pp^T then approaches zero. Thus an upstream gradient reaching the probability row is strongly attenuated through the score path. This is a local Jacobian statement, not a promise that every parameter gradient in the complete residual network vanishes.

For two logits with gap Delta, the winning probability is logistic:

and its local derivative with respect to the scaled gap is:

Increasing the multiplier alpha sends a fixed nonzero gap toward a hard decision and sends this derivative toward zero. This is the precise bridge from dimension-dependent logit variance to softmax saturation. 0x07 later owns the complete row-wise softmax and backward contracts.

Softmax can be written with temperature tau:

Larger tau produces a flatter distribution; smaller tau produces a sharper one. The Transformer uses tau=sqrt(d_k). Accidentally scaling by sqrt(d_model) when d_model=h*d_k makes the temperature larger by sqrt(h), flattening attention relative to the intended per-head equation.

For d_model=4096 and h=32:

d_head = 128
correct divisor: sqrt(128)  ~= 11.31
wrong divisor:   sqrt(4096) = 64
temperature ratio:          ~= 5.66

The error is numerically large and shape-invisible.

How this was checked Expected results of a numerical sweep over head widths 8, 32 and 128 that confirms the variance formula; open it to see how a missing or wrong scale shows up. (about 90 words)

An executable reference sweep in the Loop 3 evidence package samples independent standard-normal Q/K components. Its expected ledger is:

Width and quantityExpected behavior
raw variance at d_k = 8, 32, 128grows approximately as 8, 32, 128
scaled variance with alpha = d_k^(-1/2)stays approximately 1
standard deviation ratio, raw 128 vs raw 8approximately sqrt(128/8) = 4
wrong d_model divisor for 32 headsmultiplier is too small by sqrt(32) ~= 5.66

Sampling error is expected; the analytic formula is the contract. The sweep is a diagnostic that catches an omitted scale, the wrong dimension, or accidental double scaling.

Implementation and runtime contracts Where the scale may be applied (after the product, on Q, on K, or split as d_k^(-1/4) on each side) and how PyTorch's scale argument avoids scaling twice; open it when porting a checkpoint or calling a fused kernel. (about 220 words, 3 code listings)

Scale placement and fused APIs

Let alpha > 0 be the effective logit multiplier. These four placements are algebraically equivalent in exact arithmetic:

scores_post = (q @ k.transpose(-2, -1)) * alpha

scores_q = (q * alpha) @ k.transpose(-2, -1)

scores_k = q @ (k * alpha).transpose(-2, -1)

root_alpha = math.sqrt(alpha)
scores_split = (q * root_alpha) @ (k * root_alpha).transpose(-2, -1)

For canonical attention, alpha = d_k^(-1/2). The symmetric multiplier on each operand is therefore sqrt(alpha) = d_k^(-1/4), a detail that is easy to get wrong. Scaling both Q and K by d_k^(-1/2) applies an effective multiplier of d_k^(-1) and changes the model.

The placements are not bitwise equivalent in finite precision. Pre-scaling Q or K rounds the operands before accumulation. Post-scaling rounds the dot-product accumulator. A fused kernel may fold the multiplier into a tiled score/softmax pipeline without ever materializing scores. Parity should therefore compare outputs and gradients under a declared tolerance, while the immutable contract compares the effective real-number multiplier.

PyTorch 2.13’s functional SDPA contract names this multiplier scale:

alpha = d_k ** -0.5

# Canonical default: the API derives alpha from query.size(-1).
out_default = F.scaled_dot_product_attention(q, k, v, scale=None)

# Explicit checkpoint or model contract.
out_explicit = F.scaled_dot_product_attention(q, k, v, scale=alpha)

The keyword is applied before softmax. It is a multiplier, not the denominator temperature. If Q is already pre-scaled by alpha, preserve that model with scale=1.0; leaving scale=None would apply the default multiplier again:

# Correct only when q_scaled already contains the complete alpha.
out = F.scaled_dot_product_attention(q_scaled, k, v, scale=1.0)

# Wrong: effective multiplier becomes alpha * d_k**(-0.5).
out_double_scaled = F.scaled_dot_product_attention(q_scaled, k, v)

The backend may differ while this semantic multiplier remains fixed. Conversely, changing from the canonical multiplier to a learned per-head gamma, a length-dependent multiplier, or a score soft-cap is a model change even if the same kernel can execute it. M3.5 owns context-extension scaling; M4 owns whether a requested placement/backend is actually faster.

How this was checked An eager-versus-fused comparison that catches shape-preserving scale errors (d_model instead of d_k, scaling twice, a multiplier read as a denominator); open it when testing a new backend. (about 50 words, 1 code listing)

Scale-placement validation gate

Before comparing eager and fused results, record and assert:

assert manifest.compatibility_family == "dot"
assert manifest.scale_semantics == "multiplicative_inverse_temperature"
assert manifest.effective_logit_multiplier == configured_alpha
assert manifest.scale_application_count == 1
assert manifest.scale_width == query.shape[-1]  # unless checkpoint overrides it

eager_scores = (q @ k.transpose(-2, -1)) * configured_alpha
eager_out = softmax(eager_scores + bias_or_mask, dim=-1) @ v

fused_out = F.scaled_dot_product_attention(
    q, k, v,
    attn_mask=bias_or_mask,
    dropout_p=0.0,
    scale=configured_alpha,
)

assert_close(eager_out, fused_out, atol=declared_atol, rtol=declared_rtol)

This gate catches the dangerous failures that preserve every tensor shape: using d_model instead of d_k, applying the multiplier twice, interpreting an inverse temperature as a denominator, or comparing two backends under different configured scales.


0x05 Optional finite bias: inherited score preferences

M3.3 opens the relative-position score tensor and shows that some mechanisms add finite score preferences before softmax:

scores = scores + position_bias

M3.1 only needs the slot:

if position_bias is not None:
    scores = scores + position_bias

Typical compatible shapes:

scores:        [B, h, q_len, k_len]

position_bias: [1, h, q_len, k_len]
position_bias: [B, h, q_len, k_len]
position_bias: [q_len, k_len]       # if intentionally shared

The semantic contract is:

position_bias:
    finite preference among keys that may be visible

attention_mask:
    visibility / illegality

A finite bias can encourage or discourage a key. It cannot make a future key legal. It cannot make a PAD key meaningful. That is why the mask remains authoritative.

How this was checked The masked softmax written as an equation over legal keys only, why a large finite negative constant only approximates it, and which earlier stage builds each mask input; open it for the exact definition of masking used throughout. (about 430 words, 8 equations)

Loop 4 contract: one support relation, several physical encodings

Freeze the pre-softmax row in three layers:

followed by a Boolean visibility predicate . The model-level probability is not merely “softmax plus a mask.” It is softmax on a restricted support:

This equation fixes four properties before selecting an API:

  1. each query row has its own legal key set;
  2. an illegal key is absent from both numerator and denominator;
  3. finite bias changes probability ratios within the legal set;
  4. visibility changes the support itself.

The familiar additive representation is one implementation of the same extended-real contract:

Using a large finite negative constant only approximates this support rule. If one illegal entry has original logit z_bad, receives additive penalty -C, and the legal row denominator is Z_legal, then it still receives

in exact arithmetic. Whether it rounds to zero depends on score range, accumulation dtype, and backend. -inf expresses illegality; -1e9 is a historical implementation sentinel whose behavior must be tested in the actual dtype.

Construction owner versus application owner

M3.1 does not invent document boundaries or decide which labels train. It consumes a structured batch contract and applies the resulting relation at the score boundary:

ObjectConstructed byConsumed here asDeferred concern
token validity, lengths, padding sideM2.1legal key columns and query-valid rowstokenizer/collator policy
segment IDs or cumulative lengthsM2.1same-segment visibility or varlen metadatapack assignment and label boundaries
target shift and objective topologyM3.9causal, prefix, or other declared supportsupervised-row ownership
positional geometry and request-local cache frontierM3.3, M3.4, and M3.5 construct positional geometry; M3.10 supplies the request-local cache-position/frontier relationquery/key coordinates in the predicatepersistent cache publication, physical mutation, and rollback in M5.1 and M5.3
visible[b,h,i,j]M3.1exclusion before row normalizationoptimized IO schedule in M4.3

The boundary is deliberate. M2.1 can prove that token j belongs to document d; M3.9 can prove that output row i predicts a particular target; M3.1 proves whether score edge (i,j) participates in the denominator.

This article does not re-explain T5 buckets, Shaw relative vectors, ALiBi slopes, DeBERTa, TUPE, RoPE, or length extrapolation. Those belong to M3.3, M3.4, and M3.5.


0x06 Visibility mask before softmax

Masks come in many forms, so this section is long. The visible text keeps the rule (an illegal key is removed before softmax, so it is absent from both the numerator and the denominator of its row; the text calls this restricted-support softmax, and its equation is in the last folded block of 0x05), the causal and padding cases, how they combine, and why every used query needs at least one legal key. The folded blocks cover cached-decoding alignment, mask polarity in different APIs, unused query rows, packing, prefix and window designs, and how masks are built and handed to fused kernels.

The Harvard attention code applies the mask after score construction and before softmax:

if mask is not None:
    scores = scores.masked_fill(mask == 0, -1e9)

p_attn = scores.softmax(dim=-1)

Conceptually, use -inf:

scores = scores.masked_fill(mask == 0, -float("inf"))

The role of the mask is not to nudge a preference. It removes illegal key positions from the softmax distribution.

Causal mask source visual from the handwritten mask article

Masking is exclusion, not multiplication or post-softmax erasure

Three shape-compatible programs are often described as “apply the mask,” but only one directly implements restricted-support softmax.

# Correct hard exclusion before normalization.
masked_logits = logits.masked_fill(~visible, -torch.inf)
probabilities = torch.softmax(masked_logits, dim=-1)

# Wrong: an illegal score becomes zero, and exp(0) is positive.
multiplied_logits = logits * visible
probabilities = torch.softmax(multiplied_logits, dim=-1)

# Wrong unless followed by an explicit renormalization over legal entries.
probabilities = torch.softmax(logits, dim=-1)
probabilities = probabilities * visible

For the last program, if p = softmax(logits) and at least one legal entry exists, then

recovers the same exact distribution as pre-softmax exclusion. Merely zeroing entries does not: the remaining row sums to less than one, and the illegal logits already affected the original denominator. Pre-softmax exclusion is therefore both the simpler contract and the form fused attention APIs expect.

The A11 prose sometimes says “multiply by a mask” while its executable Harvard code uses masked_fill before softmax. Treat the code path and the restricted-support equation as authoritative; literal multiplication by a zero-one matrix is not equivalent.

Causal mask

In decoder self-attention, query row i should not read future key rows j > i.

visible(i, j) = j <= i

For a length-5 prefix:

query\key   0  1  2  3  4
0           1  0  0  0  0
1           1  1  0  0  0
2           1  1  1  0  0
3           1  1  1  1  0
4           1  1  1  1  1

That lower triangle is only the square, zero-offset special case. The general causal predicate compares absolute positions:

This form survives left padding, rectangular cross-shapes, chunked decoding, and static caches because legality is attached to coordinates rather than matrix corners.

Edge cases and failure behaviour Why a "lower-triangular" mask is ambiguous when there are fewer queries than keys, as in cached decoding, and how absolute positions and PyTorch's is_causal alignment settle it; open it before decoding with a cache. (about 210 words, 3 code listings)

Rectangular causal alignment is part of the contract

For Q=2, K=5, the phrase “lower triangular” is ambiguous. Upper-left alignment produces:

q\k  0 1 2 3 4
 0   1 0 0 0 0
 1   1 1 0 0 0

This is correct when the two query rows have absolute positions [0,1]. It is wrong for a decode chunk representing absolute positions [3,4], which needs lower-right alignment:

q_abs\k_abs  0 1 2 3 4
      3      1 1 1 1 0
      4      1 1 1 1 1

The second relation follows from coordinates, not from a special drawing:

q_pos = torch.arange(q_len, device=device) + past_len
k_pos = torch.arange(k_len, device=device)
visible = k_pos[None, :] <= q_pos[:, None]

PyTorch 2.13 documents that scaled_dot_product_attention(..., is_causal=True) uses an upper-left causal bias for non-square matrices. Its prototype CausalBias API distinguishes causal_upper_left from causal_lower_right. Consequently, is_causal=True is not a generic promise that an arbitrary cache offset will be inferred. A runtime must prove that the API’s alignment matches its query/key coordinates, or pass an explicit bias that does.

Call shapeQuery coordinatesKey coordinatesDense support
square prefill Q=K=T0..T-10..T-1ordinary lower triangle
one-token contiguous decode[past_len]0..past_lenone fully legal row
chunk decode Q=c, K=past+cpast..past+c-10..past+c-1lower-right triangle
static cache K=capacitycurrent absolute rowsslot positions plus validitycausal AND initialized-slot validity

The table separates semantic causality from physical extent. A cache tensor can contain allocated but uninitialized slots; no triangle can infer those slots are invalid from shape alone.

The mask prevents future-token leakage inside attention. Target shifting and loss alignment are separate training objects owned by M3.9.

Padding mask

Padding keys should not receive probability mass.

For source padding, a typical key mask is:

src_key_visible: [B, 1, 1, S]

The 1 axes broadcast across heads and query rows:

scores:          [B, h, q_len, S]
src_key_visible: [B, 1,     1, S]

Padding masks primarily hide key/value columns. If a query row itself is padding, downstream training loss or pooling code must also avoid treating that row as meaningful. That is not solved by letting the row attend to nothing.

A11’s padding illustration is useful precisely because it exposes this distinction:

A11 padding-mask illustration showing both padded key columns and padded query rows

Edge cases and failure behaviour Why masking padded query rows as well as padded key columns leaves rows with no legal key, and why key validity and query validity are separate tensors; open it when your batches pad queries. (about 130 words, 1 code listing, 2 equations)

Read the rightmost matrix critically. Masking PAD columns excludes meaningless K/V rows from every query denominator. Masking the PAD query rows as well creates rows containing only -inf; ordinary softmax does not turn those rows into a mathematically defined row of zeros. A real implementation therefore needs two objects:

key_valid[b,j]:
    may key/value row j contribute to any query?

query_valid[b,i]:
    will output row i be consumed by residual, pooling, or loss code?

Query validity and key validity are different tensors. In particular, query_valid is separate from the relation normalized by attention; it may govern output zeroing or downstream loss without deleting every key from that physical query row.

For a padded encoder batch, the attention relation can remain

while query_valid separately zeros or ignores outputs at synthetic query rows. Conflating these objects is how an apparently reasonable two-dimensional padding picture becomes an all-masked-row bug.

Combined target mask

A decoder target mask often combines padding visibility and causal visibility:

target_pad:      [B, 1, T]
causal:          [1, T, T]

combined:        [B, T, T]
score-ready:     [B, 1, T, T]

The exact rank differs by implementation. The invariant is that the final mask must broadcast to:

scores: [B, h, q_len, k_len]
Implementation and runtime contracts The opposite meanings of True in Harvard code, PyTorch SDPA, nn.MultiheadAttention and additive masks, adapters between them, and the mask shapes each API expects; open it before passing a mask to a library. (about 200 words, 3 code listings)

Boolean and additive conventions

The dangerous part is not the mask idea. It is API polarity.

ConventionAllowed positionBlocked positionOperation
Harvard-style boolean1 / True0 / Falsemasked_fill(mask == 0, -inf)
PyTorch SDPA boolean attn_maskTrue participatesFalse blockedblocked entries receive -inf bias
PyTorch MultiheadAttention binary attn_maskFalse allowedTrue not allowedAPI interprets True as blocked
PyTorch MultiheadAttention key_padding_maskFalse token keptTrue token ignoredpadding-side mask convention
Additive float mask0.0-inf or very negativeadded to scores

Same dtype, opposite polarity is a silent correctness bug.

Do not let an untyped tensor named mask cross the model/backend boundary. Convert from one canonical semantic object in a named adapter:

def visible_to_sdpa_bool(visible):
    # SDPA: True participates.
    return visible.bool()

def visible_to_mha_attn_mask(visible):
    # MultiheadAttention binary attn_mask: True is blocked.
    return ~visible.bool()

def key_valid_to_mha_padding_mask(key_valid):
    # MultiheadAttention key_padding_mask: True is ignored.
    return ~key_valid.bool()

def visible_to_additive_bias(visible, dtype):
    bias = torch.zeros(visible.shape, dtype=dtype, device=visible.device)
    return bias.masked_fill(~visible, -torch.inf)

PyTorch’s public contracts also differ in rank:

SDPA attn_mask:
    broadcastable to [B,...,Hq,Q,K]

MultiheadAttention attn_mask:
    [Q,K] or [B*H,Q,K]

MultiheadAttention key_padding_mask:
    [B,K]

If MultiheadAttention receives both attn_mask and key_padding_mask, its documentation requires their types to match. The module expands and merges them internally. That convenience does not erase the semantic distinction: attn_mask represents query-key edges, while key_padding_mask represents key-column validity.

Float masks add values rather than declare a Boolean support. They must match the API’s required dtype and device, and any finite entries are preferences, not authoritative exclusion. A release manifest should therefore record:

canonical polarity: True = visible
adapter/API: SDPA bool | MHA blocked bool | additive float | block metadata
materialized shape and broadcast axes
causal alignment: square | upper-left | lower-right | explicit positions
all-masked-row precondition or policy

All-masked rows

One more failure is easy to miss:

scores[i, :] = [-inf, -inf, -inf, ...]

Softmax over an all-masked row can produce NaN, because there is no legal key for that query. This can happen with malformed padding, empty cross-attention memory, or a bad combination of causal and padding masks.

The reason is structural, not an arbitrary library limitation. Restricted softmax would require

but the legal set is empty, so both the intended numerator and denominator vanish. There is no probability distribution over an empty support.

A11 combined padding and causal mask illustration with an all-masked padded query row

The A11 figure labels the final PAD query row as zeros after softmax. That is a useful desired output policy, but it does not follow from applying ordinary softmax to five -inf logits. The implementation must explicitly reject the row, give it a sentinel legal key, or mark it unused and zero its output. The figure must not be read as a numerical derivation.

The mask contract is not just “make illegal logits small.” It is:

Every query row that will be used must have at least one legal key.
Edge cases and failure behaviour Three policies for query rows whose outputs are never used (reject them, give them a sentinel key, or mark them and zero their outputs), a reference implementation, and why zeroing NaN afterwards is weaker; open it when batches contain padded queries. (about 230 words, 4 code listings)

Policies for structurally unused query rows

Some batches contain query rows whose outputs will never be consumed, such as PAD query positions. There are three defensible policies, but they must not be mixed implicitly.

Policy 1: reject all-masked rows.

This is the strongest reference behavior:

assert visible.any(dim=-1).all()

It catches malformed examples early and is appropriate when every query row is expected to represent a real token.

Policy 2: give every physical query a legal sentinel key.

A dedicated null/sentinel slot can make the row mathematically defined. The sentinel value and its downstream treatment become learned/model state; this is not equivalent to pretending the row did not exist.

Policy 3: mark unused queries separately and zero their outputs.

Here key visibility and query validity are different tensors:

key_visible = ...   # [B,1,Q,K], relation used by softmax
query_valid = ...   # [B,1,Q,1], whether output row is consumed

A safe reference implementation can temporarily make one key legal for unused queries, normalize, then zero the output:

def attention_with_unused_queries(q, k, v, visible, query_valid):
    visible = torch.broadcast_to(visible, (*q.shape[:-1], k.size(-2)))
    query_valid = torch.broadcast_to(query_valid, (*q.shape[:-1], 1))

    no_key = ~visible.any(dim=-1, keepdim=True)
    assert torch.all(~(no_key & query_valid))

    safe_visible = visible.clone()
    safe_visible[..., 0:1] |= no_key

    scores = (q @ k.transpose(-2, -1)) * (q.size(-1) ** -0.5)
    scores = scores.masked_fill(~safe_visible, -torch.inf)
    probabilities = torch.softmax(scores, dim=-1, dtype=torch.float32)
    output = probabilities.to(q.dtype) @ v

    output = output * query_valid.to(output.dtype)
    return output

The temporary key is an implementation device for unused rows only. It must never make a real query legal when its semantic key set is empty.

Zeroing NaN after an undefined softmax is weaker:

probabilities = torch.nan_to_num(softmax(all_negative_infinity_row))

That can hide a real visibility bug and gives backend-dependent gradients. Choosing a policy before normalization makes forward and backward behavior auditable.

Optimized kernels may define their own all-masked-row result. A backend parity test must include this case explicitly or exclude it by precondition; it should not discover the policy accidentally in production.

Code and tensors Small functions that build causal and padding visibility apart from the single line that applies it; open it when writing mask code or locating a mask bug. (about 90 words, 3 code listings)

Mask construction and mask application are different interfaces

The attention kernel should not have to infer why an entry is illegal. Upstream code constructs visibility from semantic objects; the score path only applies the resulting rule.

def make_causal_visibility(q_len, k_len, past_len=0, device=None):
    q_pos = torch.arange(q_len, device=device) + past_len
    k_pos = torch.arange(k_len, device=device)
    return k_pos[None, :] <= q_pos[:, None]       # [q_len, k_len]

def make_key_padding_visibility(attention_mask):
    # attention_mask: [B, k_len], 1 means a real token.
    return attention_mask[:, None, None, :].bool()  # [B,1,1,k_len]

def combine_visibility(causal, key_visible):
    return causal[None, None, :, :] & key_visible

Application then has one narrow contract:

visible = combine_visibility(causal, key_visible)  # broadcastable to scores
masked_scores = scores.masked_fill(~visible, -torch.inf)

Separating the interfaces makes failures easier to localize:

wrong legal positions:
    construction bug

right legal positions, wrong polarity or dtype:
    application/API bug

right mask, NaN row:
    no legal key or unstable backend path

The distinction also matters for cached decoding. With q_len=1, the causal row must use the absolute query position, not local row zero. Otherwise a current token at absolute position 127 would be allowed to read only key zero.

Worked detail Visibility written as predicates for packed samples (block-diagonal, with pair counts for lengths 3, 4, 5 and 4), encoder, decoder, cross-attention and cached decoding, plus prefix-LM and sliding-window designs; open it for masks beyond causal and padding. (about 680 words, 3 code listings, 20 equations)

Packed samples require block-diagonal visibility

Packing several independent examples into one physical token row removes PAD tokens, but it does not make the examples one sequence. A causal mask alone would let a token in the second example read tokens from the first example.

For packed segment IDs:

segment: [0,0,0, 1,1,1,1, 2,2, 3,3,3]
position:[0,1,2, 0,1,2,3, 0,1, 0,1,2]

legal visibility is the conjunction:

same_segment = segment_ids[:, None] == segment_ids[None, :]
causal_local = positions[:, None] >= positions[None, :]
packed_visible = same_segment & causal_local

Packing changes the physical schedule, not the independent-example semantics. For lengths l_1,...,l_n, padding every example to L=max(l_s) presents nL token slots and up to

query-key score pairs to a dense batched kernel. Independent variable-length execution needs only

dense pairs before causal triangular savings. Concatenating all tokens and running one unrestricted dense matrix would instead expose

including cross-document pairs that must not exist. Block-diagonal visibility recovers the independent semantics; a varlen kernel can also avoid materializing or computing the off-block region.

For A11’s lengths [3,4,5,4]:

padded dense pairs:       4 * 5^2 = 100
independent dense pairs:  3^2 + 4^2 + 5^2 + 4^2 = 66
unrestricted concat:     16^2 = 256   # wrong support and extra work

These are logical pair counts, not a claim that every backend materializes the score matrix. M4.3 owns the IO schedule and kernel-level traffic.

Block-diagonal causal visibility for several independent sequences packed into one row

The source visual marks each packed sample as its own causal triangle. This is the semantic requirement. A production kernel may represent the same structure with cumulative sequence lengths and avoid materializing the dense mask.

Mask algebra across encoder, decoder, cross-attention, and cache modes

Write visibility as predicates rather than pictures. Let query position be i, key position be j, batch row be b, and real[b,j] indicate a non-padding key.

The omitted head index is intentional only when every head shares topology. The full object is V[B,H,Q,K]; a physical [Q,K], [B,1,Q,K], or [B,1,1,K] tensor is valid only because its omitted axes are declared broadcast-invariant. Head-specific windows or sparsity require an H axis and must not be silently collapsed to one shared mask.

Encoder self-attention normally uses:

Every real query can read every real key. Query padding still needs a separate downstream contract because key masking only controls which values enter a row.

Decoder causal self-attention uses:

Cross-attention from target query i to source key j uses:

unless the application adds an explicit source-memory restriction. There is no generic target causal triangle on the source axis. The target row is already conditioned on legal target history before it queries source memory.

For cached decoding, use absolute positions. Let the current query positions be p_i and cached key positions be r_j:

If the implementation appends exactly one current key after a contiguous past, all provided keys satisfy the predicate and an explicit triangular mask may be redundant. That does not justify removing masks from every decode kernel. Chunked decode, speculative branches, static cache buffers, left padding, and mixed-length batches can expose keys whose validity cannot be inferred from tensor extent alone.

Prefix and window masks are visibility designs, not score biases

A prefix language model may allow all prefix rows to see one another while continuation rows are causal:

A causal sliding window of width w uses:

These masks remove edges from the attention graph. A finite position bias does not: a sufficiently strong content score can overcome a finite negative bias, while a masked edge must remain impossible. That is why ALiBi-like preferences, relative-position biases, prefix visibility, and sliding-window legality should not all be represented by an undocumented float tensor called mask.

Implementation and runtime contracts How a visibility relation is compiled, composed and encoded for backends (dense, additive, sequence lengths, block masks), where masks arise in training and inference pipelines, and the rules for packed batches with a test; open it when building a data or serving pipeline. (about 1,500 words, 16 code listings)

One predicate compiler, not one universal dense matrix

A transparent reference compiler can combine declared semantic inputs before choosing a physical encoding:

def compile_visibility(
    q_pos,                 # [B,Q] absolute or segment-local coordinates
    k_pos,                 # [B,K]
    key_valid,             # [B,K]
    *,
    causal=False,
    q_segment=None,        # [B,Q]
    k_segment=None,        # [B,K]
    prefix_length=None,    # scalar or [B]
    window=None,
):
    visible = key_valid[:, None, :]          # [B,1,K]

    if q_segment is not None or k_segment is not None:
        assert q_segment is not None and k_segment is not None
        visible = visible & (
            q_segment[:, :, None] == k_segment[:, None, :]
        )

    if prefix_length is not None:
        P = torch.as_tensor(prefix_length, device=q_pos.device)
        if P.ndim == 0:
            P = P.expand(q_pos.size(0))
        q_in_prefix = q_pos < P[:, None]
        k_in_prefix = k_pos < P[:, None]
        topology = (
            (q_in_prefix[:, :, None] & k_in_prefix[:, None, :])
            | (~q_in_prefix[:, :, None]
               & (k_pos[:, None, :] <= q_pos[:, :, None]))
        )
        visible = visible & topology
    elif causal:
        visible = visible & (
            k_pos[:, None, :] <= q_pos[:, :, None]
        )

    if window is not None:
        distance = q_pos[:, :, None] - k_pos[:, None, :]
        visible = visible & (distance >= 0) & (distance < window)

    return visible[:, None, :, :]            # [B,1,Q,K]

This is an eager semantic oracle, not a recommendation to materialize a dense mask at long sequence lengths. A production adapter may lower the same predicate to an is_causal flag, sequence lengths, cumulative offsets, CausalBias, or block metadata. The lowering is correct only if it preserves every (b,h,i,j) truth value.

The function also exposes incompatible requests. A prefix topology already defines its own causal region; blindly setting both prefix_length and a separate global causal flag can accidentally remove legal prefix-to-prefix edges. Semantic composition must happen once before backend lowering.

Boolean composition and additive composition must agree

With True = visible, boolean rules compose with logical AND:

visible = key_is_real & causal & same_packed_segment

With additive masks, each rule contributes 0 for legal and -inf for illegal:

additive = padding_bias + causal_bias + segment_bias
masked_scores = scores + additive

Because -inf + finite = -inf, either illegal rule remains authoritative. In finite dtypes and mixed precision, constructing several copies of torch.finfo(dtype).min and adding them can overflow to -inf; that is usually semantically fine for illegal entries but can complicate debugging and backend dispatch. Constructing one final boolean visibility relation and converting it once is often easier to audit.

The following truth table should hold before any optimized representation is accepted:

Real keyCausally legalSame packed sampleFinal visibility
1111
0110
1010
1100

This is the mask’s model semantics. Dense tensors, block metadata, sequence length arrays, and kernel flags are alternative physical encodings.

Dense support, compact metadata, and predicate kernels

The same visibility relation can cross the backend boundary in several forms:

EncodingPersistent objectSuitable patternMain proof obligation
dense Booleanup to B*H*Q*K bits/bytessmall or irregular callspolarity, broadcast, exact entries
dense additiveup to B*H*Q*K score-dtype valuesfinite bias plus hard maskdtype and -inf placement
causal flag/biasshape and alignment metadatacanonical trianglecoordinate alignment is exact
lengths / cu_seqlensO(number of sequences) offsetsindependent packed sequencesordering and boundaries match
block maskblock counts, indices, partial-block predicatestructured sparse patternsfull/partial/empty block classification
runtime predicatefunction of (b,h,q_idx,kv_idx)custom topologylogical indices include cache offsets

PyTorch FlexAttention makes the last two distinctions explicit. A mask_mod returns whether an edge exists and can be precompiled into BlockMask metadata; a score_mod changes a computed finite score. The separation is the same one established here between visibility and bias. It also has a systems consequence: fully illegal blocks can be skipped, fully legal blocks need no per-element mask test, and only boundary blocks require the predicate.

During cached decoding, a predicate written against local q_idx=0 repeats the same offset bug as a dense upper-left triangle. The official FlexAttention inference example adds the cache offset to the query index before comparing it with kv_idx. Logical coordinates remain part of correctness even when no dense tensor exists.

The mask lifecycle: data policy, model semantics, backend encoding

A11 did more than draw triangular matrices. It followed masks from batch construction through encoder and decoder call sites. That lifecycle is worth preserving because many mask failures happen before the attention function is called.

dataset / tokenizer policy
    -> token IDs, lengths, segment boundaries, PAD convention
    -> semantic visibility predicates
    -> model API masks and position metadata
    -> broadcast / packed representation
    -> backend-specific mask, sequence lengths, or causal flag
    -> score exclusion before row normalization

Each stage owns a different question:

StageQuestionTypical failure
collatorWhich rows are real tokens and which examples share a physical row?PAD/segment metadata lost
objective builderWhich target positions may be visible for this loss?teacher-forced future leakage
model wrapperWhich mask applies to encoder self-, decoder self-, or cross-attention?source and target masks swapped
head-layout codeHow does the mask broadcast over heads and query rows?wrong unsqueeze axis
kernel adapterDoes True mean legal or blocked, and is causal masking implicit?polarity inversion or double masking
loss/output pathWhich query rows contribute to the objective?padded query rows still scored

The attention function can verify shapes and legal rows. It cannot reconstruct document boundaries that were discarded by the collator or infer whether the training objective intended prefix-LM rather than causal-LM visibility.

Immutable visibility handoff

A reproducible call should be able to serialize the information from which the relation was compiled, even if the dense relation itself is never stored:

visibility_handoff:
    batch_layout: padded | packed | jagged | cache
    q_positions / k_positions: values or derivation rule
    key_valid: shape, polarity, padding side, initialized cache length
    query_valid: shape and downstream zero/ignore policy
    segment_boundaries: segment IDs or cumulative lengths
    topology: bidirectional | causal | prefix | window | custom
    prefix_length / window / cache_offset: explicit when applicable
    head_sharing: shared | per-head

backend_lowering:
    API and pinned version
    bool/additive/block/length representation
    polarity and broadcast axes
    non-square causal alignment
    all-masked-row precondition

The handoff is immutable for one forward call. Mutating past_len, sequence boundaries, or padding side after Q/K positions were derived creates a new visibility object even if the underlying storage address is reused.

Harvard names, PyTorch names, and semantic objects

A11’s comparison figure records an important source of confusion: libraries split the semantic mask into differently named tensors.

A11 map between Harvard source/target masks, semantic padding/sequence masks, and PyTorch attention and key-padding mask names

The figure contains Chinese labels, but its central split is readable:

attention mask:
    relation between query positions and key positions
    examples: causal triangle, memory restriction, block-diagonal packing

key padding mask:
    which key columns correspond to real tokens in each batch row

For torch.nn.Transformer, the common public objects are:

API objectLogical shapeApplies to
src_mask[S,S] or head/batch-expanded formencoder source-position relation
tgt_mask[T,T] or expanded formdecoder target relation, normally causal
memory_mask[T,S] or expanded formdecoder-query to encoder-memory relation
src_key_padding_mask[B,S]source key columns
tgt_key_padding_mask[B,T]target key columns
memory_key_padding_mask[B,S]encoder-memory key columns during cross-attention

These names are API contracts, not six independent mathematical ideas. At the score boundary they become a relation broadcastable to [B,H,Q,K]. A wrapper may combine them; a fused kernel may receive only cumulative lengths and an is_causal flag. Parity means all encodings exclude the same edges.

Teacher-forced training: future rows exist but remain illegal

During decoder training, the target batch commonly contains the whole shifted target sequence:

decoder input:  <bos> y_0 y_1 ... y_(T-2)
labels:          y_0  y_1 y_2 ... y_(T-1)

All input rows can be processed in parallel because row i is prevented from reading rows j > i. The target causal mask and the label shift solve different problems:

causal visibility:
    prevents hidden-state leakage inside the model

label shift:
    chooses which next token each output row predicts

loss padding mask:
    chooses which output rows contribute to the objective

Applying only two of the three can produce plausible loss curves with the wrong task. In particular, masking PAD keys does not automatically remove PAD query rows from cross-entropy.

The handwritten mask article followed the same objects through the encoder and both decoder attention calls:

A11 model data flow showing source padding visibility entering encoder and cross-attention while target visibility enters masked decoder self-attention

The operational call graph is:

memory = encoder(src_states, src_visibility)

target_states = decoder_self_attention(
    target_states,
    target_states,
    target_states,
    target_visibility,       # causal AND target-key-real
)

target_states = cross_attention(
    target_states,
    memory,
    memory,
    source_key_visibility,   # source columns, no target triangle
)

When S == T, swapping the last two masks may preserve every tensor shape. The semantics still fail. This is why call-site tests need unequal source and target lengths.

Prefill, one-token decode, and static cache buffers

At inference prefill, a decoder-only model processes a prompt block with a causal relation over prompt rows. After prefill, ordinary one-token decode may have:

Q = 1
K = past_length + 1

If the K/V tensor contains exactly the contiguous legal prefix, every supplied key is legal for the current query and the kernel can omit an explicit dense triangle. That optimization is a consequence of cache extent and absolute positions, not a change in causal semantics.

Explicit validity becomes necessary again when physical extent exceeds legal state:

static cache:
    allocated slots beyond current length must stay invisible

left-padded batch:
    physical columns before the real prefix may be PAD

chunked decode:
    several new queries need a local causal relation inside the chunk

speculative branch:
    candidate rows may have branch-specific ancestry

sliding window:
    old but physically retained keys may be outside the legal window

The backend may encode these cases through lengths, cache positions, block tables, or a custom mask. The model-level audit remains the predicate legal(query_position,key_position).

Packing is a joint attention, position, and loss contract

Padding a batch to its longest sequence can waste most token slots when length distributions are broad.

A11 padding example where shorter sequences allocate many PAD positions to match the longest row

Packing removes that waste by concatenating independent examples into fewer physical rows:

A11 sample-packing diagram concatenating several logical examples into one physical token row

But concatenation alone silently changes the training problem. A correct packed representation needs at least:

token IDs:
    the physical packed row

segment or cumulative-length metadata:
    which tokens belong to one logical example

position IDs:
    whether positions reset per example or follow another documented policy

attention visibility:
    block-diagonal causal edges, no cross-example reads

loss boundaries:
    labels must not predict the first token of the next packed document from
    the final token of the previous document unless that transition is intended

For example lengths [3,4,5,4], cumulative boundaries are:

cu_seqlens = [0,3,7,12,16]

A variable-length attention kernel can consume those boundaries and run the four causal examples without constructing a dense 16 x 16 block mask. The dense block-diagonal matrix and cu_seqlens are semantically equivalent only if query/key ordering and position policy agree.

The LongAlign figure retained by A11 shows that packing also has a scheduling dimension: filling token capacity does not guarantee balanced per-rank work or equal example weighting.

A11 LongAlign comparison of naïve padded batches and packed/sorted long-context batching

Three concerns should be measured separately:

padding efficiency:
    fraction of physical token slots carrying real tokens

compute balance:
    whether workers receive comparable sequence work

optimization weighting:
    whether long examples dominate because loss is averaged per token rather
    than per logical example

The source packing survey compares combinations of sorting, padding, fixed length packing, position IDs, and document isolation:

A11 source comparison of padding and packing strategies, position-ID support, cross-example isolation, truncation, and sorting requirements

The figure is a source-reported design comparison, not a universal ranking. The correct choice depends on whether the workload permits truncation, whether the kernel accepts variable lengths, how distributed batches are balanced, and how the loss weights examples. The non-negotiable correctness rule is simpler:

tokens from independent examples must not exchange attention or labels unless
the dataset explicitly defines that relation.

Packing conformance test

A minimal test compares packed and unpacked execution with dropout disabled:

def assert_packed_equivalence(model, examples, pack):
    separate = [model(x, causal=True) for x in examples]

    packed_ids, segment_ids, positions = pack(examples)
    packed_out = model(
        packed_ids,
        segment_ids=segment_ids,
        position_ids=positions,
    )

    cursor = 0
    for expected, example in zip(separate, examples):
        length = example.shape[-1]
        actual = packed_out[:, cursor:cursor + length]
        torch.testing.assert_close(actual, expected, rtol=1e-4, atol=1e-5)
        cursor += length

This catches cross-example leakage, position-reset mistakes, and wrong cumulative boundaries. A separate label test must confirm that loss transitions also stop at document boundaries.


0x07 Softmax over key positions

The visible text covers the softmax axis, why subtracting the row maximum is exact and what it does and does not protect against, how softmax turns score differences into probability ratios, and how adding legal keys dilutes a fixed score gap. Two symbols come from the folded blocks: is the set of legal keys in one query row, and is that row’s log-sum-exp. The folds hold the theory of softmax (log-sum-exp, Jacobian, temperature, entropy), alternative normalizers, exceptional rows, and the online form used by fused kernels.

After bias and mask:

p_attn = scores.softmax(dim=-1)

The dim=-1 is the key/value axis:

p_attn: [B, h, q_len, k_len]

For each batch item, head, and query row:

sum_j p_attn[b, head, i, j] = 1

That means each query row forms a distribution over visible key/value rows.

Wrong-axis softmax keeps the same rank but changes the operation:

scores.softmax(dim=-2)  # wrong for normal attention

That would normalize over query rows. Instead of each query choosing keys, each key would distribute mass across queries. The tensor shape survives; the row ownership does not.

How this was checked The exact domain assumed here (a non-empty set of legal keys with finite scores) and what is left to the mask and dropout steps; open it for the formal setting of this section. (about 190 words, 1 code listing, 4 equations)

Loop 4 froze the support relation. For one fixed [batch, head, query] row, let

be the legal key indices and let s_j denote the finite scaled score plus any finite position bias for j in J. Loop 5 owns the map

It does not choose J, move a key into or out of the support, define a dropout realization, or choose an IO-aware kernel schedule. Those boundaries matter because four objects that are often called “the softmax row” are not interchangeable:

ObjectContractOwner after this loop
semantic support Jwhich key positions may receive massfrozen by Loop 4
legal logits s_Jfinite compatibility values on that supportLoops 3-4
pre-dropout distribution pnonnegative, unit-sum routing mass over JLoop 5
post-dropout weightsstochastic rescaled coefficients; one realization need not sum to oneLoop 6

The strict reference domain is therefore:

support is non-empty;
every legal score is finite;
masked entries are absent from the reduction;
normalization is over the key axis;
probabilities are inspected before attention dropout.

NaN, legal +inf, legal -inf, and empty support are explicit policy boundaries later in this section. They are not silently repaired by changing the formula.

Derivation Softmax as the gradient of log-sum-exp, which explains why a constant shift changes nothing, and as a score-plus-entropy maximization; open it for the theory behind the visible identities. (about 290 words, 1 code listing, 16 equations)

One log-partition function generates the entire row geometry

Define the log-partition function on the legal support:

Differentiate it with respect to one legal score:

Thus softmax is not an unrelated activation placed after the score function:

This one identity will generate the translation invariance, probability ratios, Jacobian, entropy relation, and temperature derivatives below. It also fixes the difference between a probability and its normalizer:

A(s):       one scalar log-normalizer for the row
p_i:        one derivative of that scalar with respect to score i
sum_i p_i:  one, because all legal terms share the same denominator

Adding a common constant c gives

so its gradient is unchanged:

The logits are consequently identifiable only up to a common row offset. Two implementations can use different representatives of the same row and still produce exactly the same real-arithmetic probabilities.

Softmax is entropy-regularized competition

Softmax also solves a constrained optimization problem:

where

Introduce a Lagrange multiplier for sum_j p_j = 1. For an interior optimum,

Therefore p_j is proportional to exp(s_j), and normalization recovers softmax. The score term rewards concentrating on large logits; Shannon entropy rewards spreading mass. Temperature changes the strength of that competition. Removing or replacing softmax is therefore a model change, not merely deleting one expensive function call.

Stable softmax and why subtracting the row maximum is exact

The mathematical softmax is invariant to adding the same constant to every logit in one row:

Choose c = max(scores[row]). The largest shifted logit is zero, so no shifted exponential is greater than one:

row_max = masked_scores.max(dim=-1, keepdim=True).values
shifted = masked_scores - row_max
probabilities = shifted.exp()
probabilities = probabilities / probabilities.sum(dim=-1, keepdim=True)

This is not an approximation to softmax. It is the same distribution evaluated with a numerically safer representative of the logit row. Blanchard, Higham, and Higham analyze this distinction directly: formulas that are equivalent in real arithmetic can have different overflow, underflow, and rounding behavior in finite precision.

The stable row can be written as a compact state:

Then:

At least one shifted score is zero. If n = |J|, every shifted exponential lies in (0,1], and therefore:

These bounds explain exactly what subtract-max repairs. It prevents positive exponential overflow for finite legal logits and keeps the denominator in a known range. It does not guarantee that every small term remains representable. A large negative gap can still underflow:

ideal real arithmetic:
    exp(s_j - m) is positive for every finite legal score

finite precision:
    a sufficiently negative s_j - m may round exp(...) to zero

That zero is numerical truncation, not semantic sparsity. It can be harmless for a negligible forward contribution while still erasing the corresponding small gradient. The required accumulation dtype and error tolerance are therefore part of the implementation contract.

For an explicit eager path, keep the reduction dtype visible:

def stable_softmax_finite_row(scores, visible, work_dtype=torch.float32):
    legal = scores[visible]
    if legal.numel() == 0:
        raise ValueError("softmax support is empty")
    if not torch.isfinite(legal).all():
        raise ValueError("legal softmax scores must be finite")

    work = legal.to(work_dtype)
    row_max = work.max()
    numerators = torch.exp(work - row_max)
    denominator = numerators.sum()

    probabilities = torch.zeros_like(scores, dtype=work_dtype)
    probabilities[visible] = numerators / denominator
    logsumexp = row_max + denominator.log()
    return probabilities, logsumexp

PyTorch’s functional softmax exposes a dtype argument that casts before the operation, specifically allowing callers to choose a safer computation type. torch.logsumexp exposes the stabilized log-normalizer directly. Neither API removes the need to validate the axis, legal support, or exceptional inputs.

A deterministic NumPy reference on shifted logits [0,-10,-20,-40] makes the precision boundary concrete:

Work dtypeStable exponential numerators, approximatelyResulting observation
float16[1, 4.54e-5, 0, 0]gaps 20 and 40 underflow to exact stored zeros; adding 4.54e-5 to 1 also rounds the denominator to 1
float32[1, 4.54e-5, 2.06e-9, 4.25e-18]all four terms remain represented in this fixture
float64[1, 4.54e-5, 2.06e-9, 4.25e-18]reference retains the same terms with a more accurate sum

These are fixture observations, not universal hardware thresholds. Subnormal handling, reduction precision, flush-to-zero behavior, and fused-kernel policy can differ. The invariant is the diagnostic method: record the work dtype, test adversarial score gaps, and compare against a higher-precision reference.

Fused kernels perform an equivalent running-max/running-sum reduction in tiles. They do not need to materialize the full score matrix to preserve exact softmax semantics. M4.3 owns the online-state merge proof, pass/access accounting, value-numerator recurrence, and IO reason for fusing score, mask, normalization, and value accumulation.

Probability ratios reveal what softmax preserves

For two legal keys j and r in one query row:

Softmax discards a common row offset but preserves every pairwise logit difference as a probability ratio. This gives three useful consequences:

adding the same constant to a row changes nothing;
multiplying logits changes sharpness because it scales every difference;
masking a key with -infinity makes its ratio to a legal key zero.

The A10 visual below shows both the row-wise probability calculation and the query-by-key matrix whose final axis is normalized:

A10 softmax example converting one score row into attention probabilities and locating the key axis in the attention matrix

The numerical labels are illustrative. The semantic point is that softmax is applied independently to every [batch,head,query] row, never once over the whole matrix.

The ratio identity also proves three order properties for finite legal scores:

Softmax changes magnitudes but not score ordering. It is also dense on a finite legal support:

Exact zeros in the ideal distribution come from support exclusion or a different normalizer, not from an ordinary finite softmax score. Finite precision can round a tiny positive value to zero, but that numerical event must not be mistaken for a learned sparse-support decision.

Derivation The softmax Jacobian and how it couples every key in a row, saturation from a large gap (with ties and single-key rows), and how temperature changes entropy; open it before the backward pass or entropy diagnostics. (about 660 words, 1 code listing, 30 equations)

The softmax Jacobian and gradient coupling

For probability vector p = softmax(s), the derivative is:

In matrix form:

The off-diagonal terms are not zero. Raising one logit changes all probabilities because the row must continue to sum to one. Given upstream gradient g_p, the efficient vector-Jacobian product is:

This form appears later in the full backward trace. It also explains why inspecting each attention edge independently is incomplete: legal edges in the same row compete through normalization.

The same matrix is the Hessian of the log-partition function:

For any vector z over the legal keys,

So the Jacobian is symmetric positive semidefinite. Its constant direction is null:

This is the differential form of row-offset invariance. If all n legal probabilities are positive, the Jacobian has rank n-1: softmax maps an n-coordinate logit row onto the interior of an (n-1)-dimensional probability simplex. No gradient can identify or update a common score offset through this operation alone.

The covariance reading is also an implementation test. A computed Jacobian should be symmetric, have row and column sums near zero, and have no materially negative eigenvalue beyond the declared numerical tolerance. Those checks are stronger than testing only sum(p) = 1.

Saturation from a large logit gap

For two logits [0, delta]:

As delta grows, p_2 approaches one and the derivative approaches zero. Scaling by sqrt(d_k) is therefore connected directly to optimization: it reduces dimension-driven logit gaps before softmax. It does not guarantee high entropy after training; the model can still learn sharp distributions.

Two boundary cases sharpen that statement:

one legal key:
    p = [1] and J = [0] for every finite score;
    this is structural certainty, not a numerical failure

tied largest scores:
    lowering temperature does not choose one tied key;
    the limiting mass is shared uniformly across the maximal set

For example, softmax(alpha * [3,3,1]) approaches [0.5,0.5,0] as alpha -> infinity, not a one-hot vector. An implementation oracle must include ties and singleton support instead of testing only a unique maximum.

Entropy is descriptive, not an objective by default

The entropy of one attention row is:

Uniform attention over n legal keys has entropy log(n); a one-hot limit has entropy zero. Temperature controls this distribution:

Increasing tau flattens the row; decreasing it sharpens the row. But low entropy is not automatically “better focus,” and high entropy is not automatically “confusion.” A head may need a broad average, a special-token sink, a local copy, or a sparse retrieval depending on layer and task. Unless the training loss contains an explicit entropy term, entropy is a diagnostic of the learned routing distribution rather than a direct target.

The log-partition identity gives a more precise temperature path. Let alpha = 1 / tau >= 0 and

Differentiate one probability:

Large-score keys gain mass and below-mean keys lose mass. The expected score obeys

Using

we obtain

For nonnegative inverse temperature, sharpening cannot increase entropy in exact arithmetic. Equality holds when the legal scores are all equal or at alpha = 0. The endpoints are:

and, as alpha -> infinity, uniform mass over the set of maximal scores. This derivation is row-local. It does not establish that changing temperature improves a trained model, preserves calibration, or generalizes across context lengths.

A10 raised a separate length-generalization problem: even if the logits for existing keys do not change, adding more legal keys changes the softmax denominator.

Suppose one relevant key has logit a and n distractors all have logit b, with gap Delta = a-b. The relevant probability is:

For fixed Delta, increasing n lowers the relevant key’s probability. To hold probability at target p^*, the required gap is:

This is a clean derivation of mass dilution. It does not say every long context becomes uniform: real distractor logits differ, position mechanisms change scores, and trained heads can increase the relevant gap. It says that softmax competition depends on the number and scores of all legal keys, not only the best pair.

A10 discussed this issue using an earlier title of the linked preprint. The current v3 is titled Softmax is not Enough (for Sharp Size Generalisation). Its externally verified claim is narrower than “softmax always becomes uniform”: for sharp tasks such as maximum-key selection, a fixed learned softmax circuit can disperse as valid problem size grows, and the paper studies adaptive temperature as an ad-hoc mitigation. The algebra above is a toy equal-distractor case that makes one denominator pressure visible; it is not a reproduction of every theorem or experiment in that paper.

The same argument applies when m relevant keys share logit a:

This form distinguishes adding useful evidence from adding distractors.

For a fixed gap Delta = 4, the one-relevant-key fixture gives:

Equal-logit distractors nRelevant-key probability
10.9820
40.9317
160.7734
640.4604

If the target relevant probability is 0.9, the required gaps for those same support sizes are approximately 2.197, 3.584, 4.970, and 6.356. Quadrupling distractor count adds log(4) to the required gap. The executable oracle derives these values from the formulas rather than storing them as unexplained examples.

Worked detail Why rescaling logits with context length changes the model rather than extending it exactly, and row diagnostics that say more than one mean entropy; open it when studying long-context behaviour. (about 190 words, 2 code listings, 2 equations)

Length-dependent temperature is a model change

A10 discusses scaling logits as context length changes, schematically using a factor related to log(n)/log(m) for inference length n and training length m. The motivation follows from the gap equation: sharper logits can offset a larger legal support.

Write a general multiplier c(n):

Increasing c(n) lowers the effective temperature and amplifies every score difference. This may preserve concentration in some distributions, but it is not an algebraically exact context extension. It changes attention weights, can over-sharpen already decisive rows, interacts with position geometry, and may alter behavior on heads that intentionally average broadly.

The claim should therefore be scoped:

source-local motivation:
    longer legal support can increase entropy or dilute fixed score gaps

rewrite interpretation:
    length-dependent logit scaling is one possible compensation

not guaranteed:
    one scaling law preserves quality or entropy for every head and input

M3.5 owns the full context-extension comparison, because score scaling must be evaluated beside RoPE frequencies, interpolation, fine-tuning, retrieval tests, and serving constraints.

Better diagnostics than one mean entropy number

For one attention row, useful diagnostics include:

entropy H(p):
    overall uncertainty, depends on support size

normalized entropy H(p)/log(n_legal):
    concentration relative to the maximum for that legal support

effective support exp(H(p)):
    number of equally weighted keys with the same entropy

top-k mass:
    how much probability lies in the largest k entries

maximum probability and logit gap:
    sharpest edge and separation from competitors

mass on designated evidence spans:
    task-conditioned retrieval diagnostic, when labels exist

These measurements should be stratified by layer, head, query type, and context length. Averaging all rows can hide a mixture of one-hot copy heads and broad aggregation heads.

Alternatives and history Why vocabulary methods such as hierarchical softmax do not apply here, and how sigmoid gates, sparsemax and entmax change the routing; open it when considering a different normalizer. (about 340 words, 2 code listings, 2 equations)

Attention softmax is not vocabulary softmax

A10 also surveys log-softmax, hierarchical softmax, and adaptive softmax. Those methods primarily address output distributions over a large vocabulary. They are not direct replacements for attention normalization over key positions.

vocabulary softmax axis:
    candidate token IDs, often tens or hundreds of thousands

attention softmax axis:
    legal key/value rows for one query

Log-softmax is valuable when a downstream loss consumes log probabilities, but attention needs probabilities or an equivalent normalized value accumulator for P@V. Hierarchical/adaptive vocabulary methods exploit token-frequency and class structure; key positions in one attention row do not follow that fixed vocabulary tree.

This is a deliberate non-import from the handwritten source: the broader softmax background remains useful in A10, while M3.1 retains only methods whose axis and contract match attention.

Alternative row normalizers change the routing geometry

The design space is broader than ordinary softmax, but each alternative gives up at least one part of the contract just derived:

MappingRow couplingExact zeros from finite legal scores?Geometry and runtime consequence
softmaxone exponential denominatornointerior of simplex; smooth dense competition
independent sigmoid gatesno shared denominatornototal row mass is not fixed; keys do not compete globally
sparsemaxEuclidean projection onto simplexyespiecewise-linear sparse support and a different Jacobian
alpha-entmaxgeneralized entropy regularizationyes for alpha > 1interpolates from dense softmax toward sparse mappings
unnormalized or kernel attentionmethod-specificmethod-specificchanges normalization, associativity, and often the runtime graph

Sparsemax is defined as a simplex projection:

Unlike finite-logit softmax, the projection can land on the simplex boundary and assign exact zero mass. Martins and Astudillo derived its Jacobian and applied it to neural attention. Peters, Niculae, and Martins later introduced alpha-entmax sequence models, where softmax and sparsemax are members of a larger family and alpha > 1 can produce sparse alignments.

These alternatives are not drop-in numerical fixes for an unstable softmax:

switching normalizer changes forward probabilities;
switching normalizer changes gradients and learned optima;
exact sparsity does not automatically produce kernel speedup;
checkpoint weights trained with one map need not preserve behavior under another.

Loop 5 records this comparison so that “tiny floating-point probability” is not confused with “model-defined zero support.” M3.1 Loop 9 owns the full alternative-interaction comparison, including kernel/linear attention and the M4 runtime boundary.

Edge cases and failure behaviour What the normalizer should do with empty rows and with infinite or NaN scores, and why repairing NaN after softmax hides the cause; open it when a row returns NaN. (about 230 words, 1 code listing)

Exceptional rows need policy before arithmetic

Subtract-max softmax has a finite-input contract. Applying it blindly to extended or undefined values produces ambiguous or invalid operations:

Legal row presented to the normalizerWhat the formula asks forStrict M3.1 policy
non-empty, all finiteordinary stable softmaxcompute
empty supportdenominator is an empty sumreject a consumed query or route an explicit unused-row policy
all entries -inf-inf - (-inf)do not call ordinary softmax; this is empty effective support
one legal +infinf - inf after subtract-maxreject or invoke a separately specified extended-real policy
multiple legal +inflimit depends on how infinities are approachedreject or explicitly define mass sharing
any legal NaNcomparisons and exponentials are contaminatedreject and diagnose the upstream producer
finite legal row plus masked -inf storagemasked entries are outside Jcompute over legal entries only

A convenient extended-real convention could assign all mass to one +inf, or split mass uniformly across several +inf entries. That convention is not the ordinary real-valued softmax formula, and different frameworks need not infer it. The strict oracle used here rejects non-finite legal logits and allows -inf only as a physical encoding of an already-excluded entry.

Do not repair an undefined row after the fact:

# Too late: the reduction has already lost the semantic distinction.
probabilities = torch.nan_to_num(torch.softmax(scores, dim=-1))

This can turn empty support, an upstream NaN, and a legal +inf into the same zero-filled symptom. Validate support and finiteness before normalization so the failure retains its cause.

Masking as a probability limit

For a blocked key, add mask value -C and let C grow:

This is the mathematical reason for an additive negative-infinity mask. A large finite constant approximates the limit, but the result depends on dtype range and implementation. In float16, a literal -1e9 cannot be represented as a finite value and becomes -inf; other constants may remain finite and leave tiny nonzero mass.

The ideal mask also removes score gradients through blocked entries because their probability is zero. That statement is local to the attention-score path. The underlying token row can still receive gradients through query-side work, residual branches, embeddings, or another attention call.

Derivation Proof that naive, max-subtracted and tile-by-tile (online) softmax share one denominator, a reference online normalizer, and the row [1000, 999, 998] worked through; open it before reading about fused kernels. (about 370 words, 2 code listings, 12 equations, 1 figure)

Naive, safe, and online normalization are the same row contract

A10 retained a two-pass naive softmax algorithm: first accumulate the denominator, then divide each exponential.

A10 source algorithm for a naive two-pass softmax denominator and normalization

The direct algorithm is mathematically clear but numerically unsafe if logits are large. For a finite row, define its native denominator

Subtracting the final maximum m gives

The common e^m cancels from every probability:

Safe softmax therefore changes coordinates, not the ideal row distribution. Online softmax changes when those same coordinates are discovered. Suppose a prefix has state (m_old, ell_old) and an incoming tile has maximum m_tile. Choose one common reference:

Every old and incoming exponential must be expressed under that reference:

By induction, after the final tile:

Native, safe, scalar-online, and block-online normalization thus share one denominator in exact arithmetic. They are not necessarily bitwise identical in finite precision because reduction and merge order change rounding.

The smallest auditable online normalizer keeps an explicit validity branch so the empty-state identity never evaluates -inf - (-inf):

def online_normalizer(finite_scores):
    valid = False
    running_max = -float("inf")
    running_sum = 0.0

    for score in finite_scores:
        if not math.isfinite(score):
            raise ValueError("legal score must be finite")
        if not valid:
            running_max = score
            running_sum = 1.0
            valid = True
            continue

        new_max = max(running_max, score)
        running_sum = (
            running_sum * math.exp(running_max - new_max)
            + math.exp(score - new_max)
        )
        running_max = new_max

    if not valid:
        raise ValueError("softmax support is empty")
    return running_max, running_sum

For the adversarial row [1000, 999, 998], direct exponentiation overflows in ordinary floating-point arithmetic. Subtract-max and online normalization both reduce it to the finite gaps [0,-1,-2] and produce approximately:

[0.66524096, 0.24472847, 0.09003057]

The A10 image is sourced from Milakov and Gimelshein’s online-normalizer paper. It captures the naive two-scan denominator, but not the safe maximum pass or the online recurrence. The existing M4.3 visual below supplies that missing algorithm comparison:

Native safe and online softmax schedules sharing one global row denominator while the online recurrence rescales prior state under a new running maximum

M3.1 owns the distribution identity, strict finite-row domain, and parity oracle. M4.3 owns scan/access counts, merge trees, online value numerators, FlashAttention scheduling, HBM traffic, and backward reconstruction. Carrying the same recurrence here is a semantic handoff, not a second kernel chapter.

How this was checked The softmax properties the article's test code checks (shift, gradient, curvature, temperature, support size, precision, exceptional inputs); open it when writing your own softmax tests. (about 40 words, 1 code listing)

Loop 5 executable gate

The publication oracle exercises properties that a shape-only example misses:

translation:
    softmax(s + c) == softmax(s)

partition gradient:
    finite_difference(grad(logsumexp)) == softmax(s)

curvature:
    J == diag(p) - p p^T
    J is symmetric positive semidefinite
    J @ ones == 0

temperature:
    entropy is non-increasing with nonnegative inverse temperature
    tied maxima share limiting mass

support:
    singleton -> p=[1], J=[0]
    empty support -> explicit error

finite precision:
    native overflow is observable
    subtract-max avoids positive overflow
    low precision may underflow tiny legal probabilities to zero

exceptional input:
    legal NaN, +inf, and -inf are rejected before reduction

The oracle also reconciles scalar-online and block-online states against the same stable reference. It does not benchmark a GPU kernel; that experiment belongs to M4.3.


0x08 Dropout on attention probabilities

The Harvard code applies dropout to p_attn:

if dropout is not None:
    p_attn = dropout(p_attn)

So the local order is:

scores
  -> mask
    -> softmax
      -> dropout on p_attn
        -> p_attn @ value

This is not dropout on the value tensor and not dropout on raw scores.

Modern functional APIs can have their own sharp edges. PyTorch’s scaled_dot_product_attention applies dropout according to the dropout_p argument, so callers must pass 0.0 during evaluation if they use the functional form directly.

The conceptual point for M3.1 is simple:

attention dropout changes which probability links are active during training.
it does not change which keys are legal.

Legality is still the mask.

How this was checked The objects of one dropout draw (keep mask, rescaling factor, consumed weights) with their shapes, and where the placement on probabilities comes from; open it for the setup used by the derivations below. (about 320 words, 4 equations)

Loop 6 contract: one realized stochastic local operation

Loop 5 ended with a deterministic pre-dropout probability tensor. Loop 6 adds one random object, uses it to mix values, and then differentiates that exact realization. For one batch/head/query row, write:

The consumed routing weights and head output are:

The distinction among these objects is part of the training contract:

ObjectShapeDeterministic after inputs?Semantic role
legal support[B,H,Q,K] predicateyeswhich query-key edges may exist
P[B,H,Q,K]yesnormalized pre-dropout routing distribution
M[B,H,Q,K] booleanno during trainingsampled kept/dropped edges
Z=M/(1-rho)[B,H,Q,K]no during traininginverted-dropout multiplier
Pdrop=P*Z[B,H,Q,K]no during trainingweights actually consumed by V
V[B,H,K,Dv]yes after the forward inputs are fixedpayload rows
O=Pdrop@V[B,H,Q,Dv]no during trainingcontextualized per-head output
dO[B,H,Q,Dv]supplied by later computationupstream backward signal

This loop owns the dense semantic equations, their tensor shapes, one fixed-RNG gradient trace, and the state required to reproduce that trace. It does not own how a tiled kernel visits query/key blocks, where those blocks live, or which gradient tile has exclusive hardware ownership. Those schedules remain in M4.3.

Historically, general dropout randomly removes units or connections during training to reduce co-adaptation. Srivastava et al. provide that general contract. The Annotated Transformer makes the attention-specific placement explicit in code: normalize scores, drop entries of p_attn, then multiply by value. That placement is a model choice, not an algebraic consequence of softmax.

Inverted attention dropout is unbiased only in expectation

Let pre-dropout probability be p_ij. With dropout probability rho, draw independent mask m_ij ~ Bernoulli(1-rho) and use inverted scaling:

Then:

The expected value mix is unchanged:

But one realized row generally does not sum to one:

Derivation Variances of the dropped probabilities, the row sum and the output, the chance that a whole row drops, and a Monte Carlo check of these formulas; open it to quantify dropout noise. (about 250 words, 1 code listing, 12 equations)

Its element-wise variance is:

The row sum is a random variable:

Assuming independent Bernoulli draws across legal edges:

This variance depends on concentration. A diffuse row has small sum_j p_ij^2; a nearly one-hot row inherits almost the full rho/(1-rho) variance of one Bernoulli multiplier. If a row has n_i legal keys, its probability of losing every legal route is:

That event is legal under ordinary element-wise attention dropout. It produces a zero head-output row for this attention call; it is not the same condition as an all-masked softmax row, which Loop 4 rejects before normalization.

The output is also an unbiased random estimator:

Its covariance under independent edge draws is:

The covariance is value-dependent. Attention dropout therefore injects noise in the payload directions selected by the routing distribution; it is not just noise in a scalar probability diagnostic.

The Loop 6 oracle checks these equations with 200,000 fixed-seed draws for:

P = [0.1, 0.2, 0.3, 0.4]
rho = 0.2

analytic E[row sum]       = 1.000000
empirical E[row sum]      = 1.000564
analytic Var(row sum)     = 0.075000
empirical Var(row sum)    = 0.075137

analytic E[output]        = [0.800000, 0.975000]
empirical E[output]       = [0.798995, 0.975084]
Pr(all four routes drop)  = 0.001600

These are deterministic fixture results, not a benchmark or a claim that independence is the only useful dropout structure.

This resolves an apparent contradiction in debugging:

before dropout:
    each legal probability row sums to one

after dropout during training:
    individual rows need not sum to one

in expectation:
    the original probability and value mixture are preserved

Frameworks also differ in which object they expose as “attention weights.” Some return pre-dropout probabilities for interpretation; the Harvard helper stores self.attn before or around the dropout path depending on the wrapper; fused kernels may not return the matrix at all. Tests must name the object they expect instead of asserting row sums on an undocumented tensor.

Derivation Why one realized output after dropout is a rescaled rather than a convex combination of values, with numbers from one fixed draw; open it if outputs after dropout look shifted. (about 120 words, 4 equations)

One realization is not a convex combination

Before dropout, P_i V lies in the convex hull of the visible values. After dropout, every coefficient remains nonnegative, but the coefficient sum is R_i, not necessarily one. A realized output is therefore a nonnegative, rescaled combination, not generally a convex combination.

This changes a useful translation identity. Add one constant vector c to every visible value row. Before dropout:

For one post-dropout realization:

Only in expectation does the shift return to c. In the fixed oracle trace, the two realized row sums are 1.004632 and 0.990504; adding c=[2,-1.5] consequently adds [2.009265,-1.506949] and [1.981008,-1.485756], respectively.

Alternatives and history Other dropout placements (renormalizing survivors, dropping keys before softmax, DropAttention, DropKey, DropHead) and why each is a different estimator, with a worked example; open it before changing the dropout scheme. (about 300 words)

Alternative dropout placements are different estimators

“Apply dropout to attention” does not identify one operation. The dropped object, placement relative to softmax, sharing pattern, schedule, and fallback policy all change the training distribution.

DesignRandom objectRow normalized in one realization?Key consequenceOwner here
standard attention-weight dropoutindependent entries of post-softmax PnoPdrop@V is unbiased after inverted scalingderived fully
drop then renormalize survivorspost-softmax entries, followed by division by surviving massyes unless every route dropschanges expectation and needs an all-drop fallbackcomparison only
pre-softmax key/edge deletionsupport or logits before normalizationyes if support stays nonemptyrecomputes competition over surviving keyscomparison only
DropAttentionelements, columns, or contiguous regions of the attention matrixmethod-dependentintroduces structured correlation across dropped routespaper boundary
DropHeadone multiplier for an entire headno at head-combination boundaryregularizes head co-adaptation rather than individual routesM3.2 boundary
value/output/residual dropoutvalue features or later sublayer outputnot a routing-row questionperturbs payload channels or residual writesM3.7 boundary

DropAttention studies element-, column-, and contiguous-region structures over attention weights. DropKey moves stochastic deletion before attention normalization for vision Transformers. Scheduled DropHead drops whole heads and changes the rate across training. Their reported quality results belong to their tasks and settings; the reusable point here is that they randomize different semantic objects.

Renormalization is especially easy to mislabel as an implementation detail. Enumerating every keep mask for P=[0.1,0.2,0.7] and rho=0.5 gives:

RuleExpected realized weights
inverted dropout, no renormalization[0.1000, 0.2000, 0.7000]
renormalize survivors, all-drop becomes zero[0.1948, 0.2611, 0.4191]
renormalize survivors, all-drop falls back to original P[0.2073, 0.2861, 0.5066]

Both renormalized variants bias the expected routing weights in this unequal row. Preserving row sum one and preserving the original expectation are not the same contract.

Implementation and runtime contracts The random-number state backward needs to replay the same mask, why evaluation must pass dropout_p=0.0 to PyTorch's functional call, and how precision and backend affect the score path; open it when training runs are not reproducible. (about 350 words, 3 code listings, 1 figure)

RNG state is part of the training input

For gradient derivation, M is held fixed. Backward differentiates the sampled program that ran in forward; it does not differentiate the Bernoulli sampling decision and must not draw a fresh mask. Reproducible execution therefore needs more than a nominal dropout probability:

global/model seed
device-specific generator identity
generator seed and counter/offset before the operation
logical batch, head, query, and key coordinates
draw-to-coordinate mapping
training/evaluation mode and dropout probability

A saved dense mask is sufficient but can be quadratic. A fused implementation may instead save compact generator state and replay each logical draw. Saving a seed alone is insufficient if a changed tile order, vector width, or counter advance maps that seed to different (batch,head,query,key) coordinates.

Forward and backward attention-dropout contract showing pre-dropout probabilities, coordinate-stable inverted-dropout multipliers, exact replay, value mixing, and the gradient path

The visual is reused from M4.3 because the semantic objects are identical. M3.1 owns why replay is required. M4.3 owns how tiled forward/backward regenerate those objects without materializing the global probability or mask matrices.

PyTorch’s functional.dropout takes an explicit training flag. Its functional scaled_dot_product_attention has a sharper contract: it applies dropout whenever dropout_p > 0, so a module wrapper must pass 0.0 explicitly during evaluation.

Precision, dtype, and backend are implementation contracts

The mathematical score path does not specify its accumulation dtype. In a mixed-precision model, Q/K/V may be float16 or bfloat16, while an eager implementation computes softmax in float32 and casts the probabilities back:

attn_weights = torch.softmax(scores, dim=-1, dtype=torch.float32)
attn_weights = attn_weights.to(query.dtype)

Transformers v5.13.1 uses this pattern in its eager LLaMA attention path. The point is not that every backend must copy those two lines. Fused SDPA or FlashAttention kernels can choose their own accumulation and normalization strategy without materializing scores or p_attn as full tensors.

The local audit is:

Q/K/V dtype:
    checkpoint and kernel contract

position bias and additive mask:
    compatible device and floating dtype

softmax accumulation:
    eager or fused backend policy

returned attention/output dtype:
    caller and residual-stream contract

An unintended promotion from a float32 bias can increase memory traffic; an unsafe downcast can lose the intended mask magnitude; and a CPU mask added to CUDA scores fails before the attention equation is evaluated. Numerically close eager and fused outputs also need not be bitwise identical because accumulation and reduction orders can differ.


0x09 Weighted value sum

The final line consumes whichever routing weights the mode produced:

consumed = p_drop if training else p
out = torch.matmul(consumed, value)

Shape trace:

P or Pdrop: [B, h, q_len, k_len]
value:      [B, h, k_len, d_value]

out:        [B, h, q_len, d_value]

The output sequence length follows q_len, not k_len.

For each query row i, evaluation uses:

while one training realization uses:

The attention probabilities decide how much of each value row enters the output. The score matrix itself is not the contextualized state. p_attn is also not the output state. The contextualized per-head output is p_attn @ value.

The function often returns both:

return out, p_attn

Returning p_attn is useful for debugging, visualization, or interpretability. The next sublayer consumes out, not the probability matrix.

Value-mixing geometry: convex combinations before dropout

For one head and one query row, legal softmax probabilities satisfy:

Therefore:

lies in the convex hull of the visible value rows. This yields several precise forward properties:

If all visible values are identical, the output equals that value.
If one probability approaches one, the output approaches one value row.
If probabilities are uniform, the output is the arithmetic mean of values.
The output cannot leave the value convex hull before later projections,
residual addition, normalization, or MLP transformation.

The final Transformer sublayer is not confined to that convex hull. W_O mixes head channels, and the residual path adds the previous state. Attention dropout also makes one realized post-dropout row a rescaled subset rather than a normalized convex combination. The convex-hull statement applies to the pre-dropout probability/value operation.

Worked detail A two-query numerical trace with one fixed dropout mask that separates three reasons for a zero weight (masked edge, dropped edge, dropped edge with a nonzero probability); open it to see dropout act on real numbers. (about 130 words, 3 code listings)

Fixed two-query training trace

The expectation equations do not reveal what one backward pass consumes. The Loop 6 fixture therefore freezes a rectangular call with two query rows, three key/value rows, value width two, dropout probability rho=0.25, and one realized keep mask:

P =
  [[0.753474, 0.246526, 0.000000],
   [0.257122, 0.191056, 0.551822]]

M =
  [[1, 0, 0],
   [0, 1, 1]]

Z = M / 0.75 =
  [[1.333333, 0.000000, 0.000000],
   [0.000000, 1.333333, 1.333333]]

Pdrop = P * Z =
  [[1.004632, 0.000000, 0.000000],
   [0.000000, 0.254741, 0.735763]]

row sums(Pdrop) = [1.004632, 0.990504]

With:

V = [[ 1.0, -1.0],
     [ 2.0,  0.5],
     [-0.5,  3.0]]

the value matmul produces:

O = Pdrop @ V =
  [[1.004632, -1.004632],
   [0.141601,  2.334659]]

This trace distinguishes three zeros that must not be conflated. P[0,2] is zero because the edge is illegal. Pdrop[0,1] is zero because a legal edge was dropped in this realization. Pdrop[1,0] is another dropped legal edge, even though its pre-dropout probability is nonzero. Only the first zero is a model support statement.

This distinction is useful when interpreting attention as retrieval. The head does not copy an existing value unless its routing is effectively one-hot. It normally interpolates among learned value directions.

QK and OV are separate functional circuits

For one head with projected states:

and, after the corresponding block of W_O:

The product W_QW_K^T controls content-dependent routing and is often called a QK circuit. The product W_VW_O^(h) controls what is read from a selected token and written back to the residual stream, an OV circuit. The decomposition is helpful because two heads can have similar attention maps and different value writes, or different attention maps whose writes cancel or align after W_O.

It also explains why an attention map is not the full head function:

P answers where the head reads.
W_V and W_O answer what the head writes.
The residual stream answers what that write changes.

Permutation equivariance without position-dependent structure

Let R be a permutation matrix that reorders sequence rows. Ignore position-dependent objects and permute Q, K, and V consistently:

Scores become:

Row-wise softmax commutes with that joint row/column permutation:

The output is:

So attention without positional geometry or an order-dependent mask is permutation equivariant: reorder the input rows and the output rows reorder the same way. It does not know that one token was “first” merely from tensor order.

Absolute/relative position, RoPE, causal masks, windows, and segment rules break or refine this symmetry by making pair relations depend on coordinates. This is why position handling cannot be treated as decoration added after the attention mechanism has already learned order.

Derivation Masks read as a directed communication graph, how information travels over several layers, and what one result on rank collapse under masking does and does not show; open it when comparing mask topologies. (about 230 words, 3 code listings)

Attention masks define a directed communication graph

For one layer, create directed edge j -> i when query row i may read key row j. The visibility mask is then an adjacency relation:

full encoder attention:
    complete directed graph among real tokens

causal attention:
    edges from current/past keys into each query

local window:
    bounded-neighborhood graph

packed attention:
    disconnected block-diagonal components

cross-attention:
    bipartite source-to-target read graph

After multiple layers, information can travel along graph paths even when one layer has no direct edge between two positions. This is the representational cost hidden by sparse edge counts: fewer edges can require more layers or special global routes to connect distant tokens.

A11 also retained a theoretical source on masked attention and token-rank collapse:

A11 source theorem sketch relating a quasi-strongly-connected attention-mask graph and bounded projections to exponential token rank collapse under stated assumptions

The figure contains Chinese exposition and summarizes one paper’s assumptions and bound. It should be read narrowly: under its stated conditions, repeated masked mixing on a sufficiently connected graph can drive token representations toward a lower-rank/common subspace, with graph radius and attention concentration affecting the bound. It is not proof that every trained Transformer inevitably becomes rank one.

Real blocks include residual connections, normalization, learned value maps, MLPs, finite depth, and data-dependent attention. A11’s useful contribution is the design question it raises:

mask topology affects more than FLOPs;
it changes information paths and repeated mixing dynamics.

This creates a three-way tradeoff for local/sparse masks:

fewer edges reduce pairwise work;
longer graph paths can delay global communication;
different connectivity/mixing can change smoothing and rank dynamics.

Those properties require model-level experiments and assumptions. M3.1 keeps the graph interpretation because it follows directly from visibility, while avoiding a universal convergence claim.


0x09A Worked three-token causal trace

The entire path is small enough to calculate by hand. Use one head, three token rows, d_k = 2, and:

The unmasked scaled score matrix is:

A causal mask keeps columns j <= i and replaces future columns with negative infinity:

Applying softmax across each row’s key axis gives approximately:

The blocked entries are exactly zero after softmax. Each visible row sums to one before dropout. The final value mixture is:

The second output row, for example, is not a score row. It is the value-space mixture:

Three-token scaled dot-product attention trace from query-key scores through causal masking, softmax, and value mixing

The corresponding executable trace is deliberately literal:

q = torch.tensor([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]])
k = q.clone()
v = torch.tensor([[1.0, 0.0], [0.0, 2.0], [1.0, 1.0]])

scores = q @ k.T / math.sqrt(q.size(-1))
causal = torch.tril(torch.ones(3, 3, dtype=torch.bool))
masked_scores = scores.masked_fill(~causal, -float("inf"))
p_attn = masked_scores.softmax(dim=-1)
out = p_attn @ v

This one example exposes four independent audit points: the transpose creates query-by-key scores, scaling changes their magnitude, masking changes which columns participate in normalization, and the final multiplication moves from key-space weights back into value-space vectors.

Per-head attention equation separating query, key matrix, value path, and output projection

The source figure writes one causal head row rather than a full batch. It makes the same ownership visible: the current row supplies the query, allowed prefix rows supply keys and values, and the output is a value-space mixture.

0x09B An auditable eager implementation

A reference implementation should expose semantic checks, not only return a tensor with the expected rank:

def eager_attention(
    q, k, v, visible=None, bias=None, dropout_p=0.0, training=False,
    dropout_multiplier=None,
):
    # q: [B,H,Q,D], k: [B,H,K,D], v: [B,H,K,Dv]
    assert q.ndim == k.ndim == v.ndim == 4
    assert q.shape[:2] == k.shape[:2] == v.shape[:2]
    assert q.shape[-1] == k.shape[-1]
    assert k.shape[-2] == v.shape[-2]

    scores = q @ k.transpose(-2, -1)
    scores = scores * (q.shape[-1] ** -0.5)

    if bias is not None:
        scores = scores + bias.to(device=scores.device, dtype=scores.dtype)

    if visible is not None:
        visible = visible.to(device=scores.device, dtype=torch.bool)
        visible = torch.broadcast_to(visible, scores.shape)
        assert visible.any(dim=-1).all(), "every used query needs a legal key"
        scores = scores.masked_fill(~visible, -torch.inf)

    pre_dropout = torch.softmax(scores, dim=-1, dtype=torch.float32)
    pre_dropout = pre_dropout.to(q.dtype)

    if dropout_multiplier is not None:
        # Test/oracle path: replay one already-sampled coordinate-wise Z.
        assert training and dropout_multiplier.shape == pre_dropout.shape
        consumed = pre_dropout * dropout_multiplier.to(pre_dropout.dtype)
    else:
        consumed = torch.dropout(pre_dropout, dropout_p, training)

    output = consumed @ v

    assert output.shape == (*q.shape[:-1], v.shape[-1])
    if not training or dropout_p == 0.0:
        row_sum = pre_dropout.float().sum(dim=-1)
        torch.testing.assert_close(row_sum, torch.ones_like(row_sum))
    return output, {
        "pre_dropout": pre_dropout,   # normalized distribution P
        "consumed": consumed,         # stochastic weights Pdrop
    }

The row-sum assertion is conditional because dropout on probabilities changes the realized row sum during training. Dropout is unbiased in expectation after its rescaling; one sampled mask does not remain a normalized probability row. That is another reason not to inspect post-dropout p_attn and assume it is the same object as pre-dropout routing probabilities.

The optional dropout_multiplier is an oracle hook, not a proposed public API. A production eager path normally asks the framework dropout operator to sample Z; a conformance test needs a way to replay one fixed realization across manual, autograd, and finite-difference calculations.

Derivation The backward pass of this function under a fixed dropout mask (gradients for V, the probabilities, the scores, Q and K), a finite-difference check, why masked and dropped edges differ, and the projection-weight gradients; open it when implementing or debugging backward. (about 680 words, 6 code listings, 22 equations)

Backward path: every role receives a different signal

Backward differentiates the exact sampled forward program. Hold Z fixed and write:

The value matmul sends gradients down two branches. Its value gradient is weighted by the consumed stochastic matrix:

The gradient of the consumed weights is:

The fixed dropout multiplier then gates the direct probability gradient:

Dropout does not create a differentiable path into the sampled Bernoulli decision. Conventional attention dropout treats Z as realized training input. If a method learns a drop rate or a discrete gate with another estimator, that is a different optimization contract.

Softmax couples every legal score in one query row. Define the row contraction:

Substituting the preceding branches gives a second form that is useful in fused backward algorithms:

The softmax gradient is then:

For S=alpha QK^T, with alpha=1/sqrt(d_k), the score matmul returns:

The scale appears once in both branches. Omitting it, or applying it again inside G_S, can preserve all shapes while changing every Q/K gradient.

Fixed-mask backward trace

For the forward fixture above, freeze:

G_O = [[ 0.7, -0.4],
       [-0.2,  0.9]]

The two equivalent row contractions and the resulting score gradient are:

D from sum(P * G_P) = [1.105096, 2.072873]
D from sum(G_O * O) = [1.105096, 2.072873]

G_S =
  [[ 0.272434, -0.272434,  0.000000],
   [-0.532981, -0.383297,  0.916279]]

The maximum central-finite-difference error over Q, K, and V is 2.84e-10 in float64 with epsilon 1e-6. Reconstructing P from saved row log-sum-exp differs by at most 2.78e-17, and replaying the same Z changes the manual gradients by at most 1.11e-16. Drawing the fixture’s different fresh mask instead produces a maximum gradient mismatch of 0.771809. That is not stochastic tolerance; it is differentiation of a different program.

Dropped, masked, and all-dropped routes have different gradients

The fixed trace exposes a subtle consequence of softmax coupling:

legal edge (query 0, key 1) is dropped:
    Z[0,1] = 0
    direct G_P[0,1] = 0
    score G_S[0,1] = -0.272434   # generally nonzero

illegal edge (query 0, key 2) is masked:
    P[0,2] = 0
    score G_S[0,2] = 0

The dropped edge receives no direct gradient through its own consumed route, but its score still affects the softmax denominator that allocated pre-dropout mass to kept edges. A masked edge is absent from that denominator and has zero ideal score gradient.

If every legal edge in one row drops, then Pdrop_i=0 and O_i=0. For that row, the contributions to G_V, G_P, D_i, G_S, G_Q, and G_K are all zero. This well-defined stochastic event is different from asking softmax to normalize an empty legal support. The oracle checks both distinctions and also verifies:

sum_j G_S[i,j] = 0 for each legal finite row
masked G_S entries = 0
all-dropped row attention gradients = 0

This exposes the training semantics that the forward mnemonic hides:

V learns what payload helps the downstream loss.
Q learns what each output row should request.
K learns how each memory row should advertise itself.

An additive mask with true -inf gives blocked entries zero probability; their score gradients are correspondingly zero under the ideal softmax semantics. This does not mean a padded token can never receive gradients elsewhere in the network. Residual paths, embeddings, losses, and query-side computation have their own masking contracts.

Projection gradients reconnect the local attention call to the model

M3.1 receives projected tensors, but training eventually updates the projection weights owned by M3.2. If:

then:

Input-state gradients accumulate all roles in self-attention because the same X feeds Q, K, and V:

In cross-attention, target states receive the Q branch while source memory receives the K and V branches. This is the backward counterpart of the forward stream contract:

self-attention:
    one state stream receives Q + K + V gradient contributions

cross-attention:
    target stream receives Q contribution
    source-memory stream receives K + V contributions

The split matters when freezing an encoder, reusing source memory, adding adapters to selected projections, or debugging a detached memory tensor.

0x09C Reference semantics versus production attention APIs

One mathematical attention call can appear as several code paths. The correct comparison is not line count; it is which semantic objects each path accepts, returns, saves for backward, or fuses.

Path 1: explicit eager attention

The eager_attention function above materializes scores and probabilities. Advantages:

every intermediate can be inspected;
custom finite bias and visibility are explicit;
attention probabilities can be returned;
gradient checks can target each stage.

Costs:

score and probability tensors may occupy O(B*H*Q*K) memory;
separate kernels move intermediates through memory;
Python/model code must handle mask polarity and all-masked rows.

This is the conformance oracle, not necessarily the production backend.

Path 2: functional scaled dot-product attention

PyTorch exposes the local operation directly:

def sdpa_attention(q, k, v, visible=None, dropout_p=0.0, training=False):
    additive = None
    if visible is not None:
        visible = torch.broadcast_to(visible, (*q.shape[:-2], q.size(-2), k.size(-2)))
        assert visible.any(dim=-1).all()
        additive = torch.zeros_like(visible, dtype=q.dtype)
        additive = additive.masked_fill(~visible, -torch.inf)

    return torch.nn.functional.scaled_dot_product_attention(
        q,
        k,
        v,
        attn_mask=additive,
        dropout_p=dropout_p if training else 0.0,
        is_causal=False,
    )

The explicit training branch is important: the functional API uses the provided dropout_p; it does not inspect a module’s .training flag for the caller. The example sets is_causal=False because visibility has already been encoded. Passing both a custom mask and an implicit causal contract requires checking the exact framework version and backend behavior rather than assuming they compose identically.

An implementation may dispatch to math, memory-efficient, or Flash-style backends. Dispatch can depend on device, dtype, dimensions, mask form, dropout, and whether gradients or attention weights are requested. Backend selection is not part of the checkpoint’s learned function, but it is part of reproducible execution.

Path 3: model-integrated attention

A decoder layer usually wraps the local operation with additional state:

q = q_proj(hidden_states)
k = k_proj(key_value_states)
v = v_proj(key_value_states)

q, k, v = split_heads(q, k, v)
q, k = apply_position_geometry(q, k, position_ids)
k, v = cache.update(k, v, cache_position)

out = scaled_dot_product_attention(q, k, v, ...)
out = merge_heads(out)
out = o_proj(out)

M3.1 owns only the middle score path, but a local test must still verify what arrives there:

Q/K already use the intended position transform;
K/V contain exactly the legal cached rows;
query and cache positions describe the same coordinate system;
mask dimensions refer to the post-cache key length;
dtype/device match the selected backend.

The A10 implementation diagram separates the same five local operations: score matmul, scale, optional mask, softmax, and value matmul.

A10 scaled-dot-product attention anatomy connecting the implementation stages to the final equation

Implementation and runtime contracts Why requesting attention weights can change the execution path, why an attention map alone is not an explanation, and what dense, recomputing and fused paths save for backward; open it before profiling or interpreting attention maps. (about 490 words, 2 code listings, 2 equations, 1 figure)

Requesting attention weights can change the execution path

An optimized kernel can produce O without writing the full P matrix to HBM. Asking for all attention weights may therefore:

force an eager or different backend;
materialize B*H*Q*K elements;
increase backward saved state;
change latency enough to invalidate a benchmark;
still return a tensor that is unsuitable as a causal explanation.

Debug runs should state whether weights are pre-dropout or post-dropout and whether they came from the same backend as normal execution. Production profiling should normally disable weight return unless the feature itself is a requirement.

PyTorch MultiheadAttention documents need_weights=False as the path that permits its optimized SDPA implementation. That is an API performance contract, not a mathematical statement that P does not exist. Conversely, a returned tensor named attn_weights does not by itself prove whether it is P, Pdrop, head-averaged P, or a tensor recomputed by another backend. The wrapper contract must say which object it returns.

An attention map is also only one part of the head function. Jain and Wallace showed counterexamples to treating learned attention weights as explanations; Wiegreffe and Pinter argued that stronger, task- and definition-specific tests can still make attention analytically useful. The bounded conclusion here is not that attention weights are meaningless. It is that a routing matrix alone omits V, W_O, residual state, downstream nonlinearities, and counterfactual behavior, so it must not be labeled a causal explanation without an explicit method and validation contract.

Training state saved by the local operation

Backward needs enough state to recover Q, K, V, the pre-dropout probability, and the exact sampled dropout multiplier. It does not require every one of those objects to be stored as a dense tensor. The saved-state choice is a time-memory-reproducibility contract:

PathTypical forward-visible stateSufficient backward stateBackward workMain failure
explicit eagermaterialized S, P, Pdrop, Oframework-dependent Q/K/V plus retained P and mask/multiplierdense matmuls and softmax backwardsilently retaining quadratic tensors beyond their useful lifetime
checkpoint/recomputeoften only O visibleQ/K/V, visibility/bias, scale, dropout replay state, normalization statistics or inputsrecreate scores/probabilities, then contract gradientsreplaying another mask or another model state
fused IO-awareno global S or P requiredQ/K/V, O, row log-normalizers, coordinate-stable RNG state, kernel metadatatile-local score/probability recomputationtile-order-dependent RNG or wrong gradient ownership

One dense reconstruction route saves the row log-partition L_i and recomputes:

on legal entries. The Loop 6 oracle reconstructs the fixture probabilities with maximum error 2.78e-17. It then replays the fixed coordinate-wise dropout multiplier and reproduces the analytic gradients within 1.11e-16. Those are semantic conformance checks, not expected low-precision GPU tolerances.

FlashAttention backward reconstruction showing saved row statistics, exact dropout replay, tile-local probability reconstruction, and gradient contractions

The visual is reused at a different level of abstraction. M3.1 derives the dense operation that must be preserved. M4.3 owns the FlashAttention tile schedule, HBM/SRAM traffic, recomputation order, and gradient-write ownership.

This creates a three-way distinction:

forward mathematical state:
    Q, K, V -> exact output O

debug-visible state:
    optional full scores or probabilities

autograd saved state:
    backend-specific tensors/statistics needed for backward

Conflating them leads to claims such as “attention requires storing the full matrix” or “FlashAttention has no attention probabilities.” The mathematical probabilities exist; whether they are materialized, returned, or recomputed is an execution choice.

How this was checked A table of dropout and backward mistakes with how each is detected, and the commands the article's test code runs for this part; open it when debugging attention during training. (about 260 words, 2 code listings)

Loop 6 failure lab

FailureShape-valid symptomBroken invariantDetection
assert post-dropout rows sum to oneintermittent training assertiononly pre-dropout P is normalizedcompare sum(P) and realized sum(Pdrop) separately
sample a fresh mask in backwardnoisy, irreproducible gradientsbackward differentiates a different programfixed-mask replay parity test
use Pdrop in the softmax Jacobianplausible but wrong Q/K gradientssoftmax produced P, then dropout produced Pdropfloat64 finite difference against fixed Z
omit Z from G_Pdropped routes receive direct gradientchain rule through Pdrop=P*Z is missinginspect a dropped legal edge
use P instead of Pdrop in G_Vvalue gradient ignores sampled routingforward consumed Pdrop@Vmanual/autograd V-gradient parity
treat dropped edge as masked edgeforce G_S=0 at a dropped legal scorelegal edge still participates in softmax denominatorinspect fixed edge (0,1)
treat all-dropped row as all-masked rowreject a valid dropout sample or create NaNssupport is nonempty even when all sampled routes dropseparate legal-count and keep-count assertions
omit alpha in Q/K backwardgradients have correct shapes and wrong scaleS=alpha QK^Tscale-ratio and finite-difference checks
leave functional SDPA dropout active in evaluationnondeterministic evaluation outputsfunctional API obeys dropout_p, not module moderepeated eval call and explicit dropout_p=0.0
request weights during benchmarklatency and memory regress unexpectedlyreturn contract changes dispatch/materializationrecord backend and need_weights in run manifest

Loop 6 executable gate

The evidence package turns this section into executable claims:

cd docs/numinens-evidence/m3-1-attention-score-path
python3 loop6_attention_oracle.py
python3 loop6_contract_audit.py
python3 -m unittest -v test_loop6_contract

Publication requires all of the following:

analytic and Monte Carlo dropout moments agree within declared tolerances;
fixed-mask manual Q/K/V gradients pass central finite differences;
the two D_i contractions agree;
masked, dropped, and all-dropped cases remain distinct;
same-mask replay agrees and fresh-mask replay demonstrably fails;
alternative renormalization is not mislabeled as unbiased inverted dropout;
source and visual provenance resolve locally;
Loops 1-5 section hashes remain frozen.

0x0A Self-attention, causal self-attention, and cross-attention axes

The same score path covers several attention calls.

ModeQuery sourceKey/value sourceScore shapeMask object
Encoder self-attentionencoder statesencoder states[B,h,S,S]source padding mask
Decoder causal self-attentiontarget/prefix statestarget/prefix states[B,h,T,T]causal + target padding mask
Cross-attentiondecoder statesencoder memory[B,h,T,S]source memory padding mask
One-token decode boundarycurrent token statecached history + current key/value[B,h,1,past+1]cache-aware visibility mask

The local math is identical. The axes change.

Cross-attention is the easiest shape trap. Its score object is target-by-source:

scores: [B, h, T, S]

Applying a target causal mask to cross-attention because the tensor happens to be rank four is a semantic error. Source keys are not future target tokens.


0x0A1 Full Source-to-Tensor and Eager Implementation Trace

The three-token example in 0x09A traced one call by hand. This section repeats the exercise for a cross-attention call in which two target rows read three source rows, one of them padding, and records every intermediate value; the stages and the matching reference code are folded below.

The earlier sections isolated each mathematical stage. This section asks a different implementation question:

Given one concrete caller, can we name the source row behind every tensor axis, reproduce every intermediate value, and explain every difference between the compact Harvard function and a defensive eager reference?

That question is stricter than checking the final shape. Many attention bugs preserve [B,H,Q,Dv]: K and V can be permuted differently, a source mask can be aligned to the query axis, softmax can reduce the wrong dimension, or a finite bias can revive an edge if support exclusion is applied in the wrong order. A full trace gives those errors nowhere to hide.

How this was checked How the trace keeps the source description, the equation and the program in agreement, and why it runs in evaluation mode; open it for the setup of the trace. (about 60 words, 1 code listing)

Loop 7 contract: reconcile source, equation, and program

Loop 7 freezes three descriptions of one operation:

source description:
    target rows query an encoder/source memory;
    source row j owns both key j and value j;
    source validity determines which memory rows are legal

equation description:
    S = alpha Q K^T
    S_bias = S + B
    S_mask = where(visible, S_bias, -inf)
    P = softmax_K(S_mask)
    O = (P * Z) V

program description:
    explicit tensors, views, broadcast adapters, work dtype,
    saved trace objects, assertions, and return semantics

The three must agree on row identity, not only dimensions. The fixture runs in evaluation mode, so its dropout multiplier is Z=1. Loop 6 already owns the stochastic training derivation and backward replay. Loop 7 records where that multiplier would enter the complete forward program without reopening its statistics.

The caller boundary is part of the trace, not part of the local owner

The original Transformer separates scaled dot-product attention from the multi-head wrapper:

Original Transformer Figure 2 separating the scaled dot-product primitive from Q/K/V projections, parallel heads, concatenation, and the output projection

Source: Attention Is All You Need, Figure 2. The left panel is M3.1’s local owner. The right panel is the M3.2 caller. Showing both is necessary for lineage; it does not transfer projection, head split, concatenation, or W_O ownership into this article.

Implementation and runtime contracts Which stages belong to the caller (projections, head merge) and which to this call, and a line-by-line comparison of the compact Harvard function with a safer reference; open it before reading the trace. (about 550 words, 1 code listing)

Use the following firewall when reading the executable fixture:

StageConcrete objectOwner in Transformer SystemLoop 7 use
data identitytarget/source token IDs, padding/segment metadataM2.1retain row provenance; do not derive tokenization
vector statetarget hidden rows and encoder/source memory rowsM2.2 and earlier blocksprovide deterministic caller inputs
learned adaptersW_Q, W_K, W_V, head split, head mapM3.2expose the exact Q/K/V values received by M3.1
local attentionscore, scale, finite bias, visibility, normalization, dropout, value mixM3.1derive and execute every intermediate
assembled resulthead merge, W_O, residual branchM3.2 and M3.7consume per-head O; remain outside this fixture

The trace therefore starts before the M3.1 API only to prove lineage. Its projection matrices use equation orientation [Dmodel,Dout], so row vectors compute X @ W. A PyTorch nn.Linear checkpoint stores its weight in [Dout,Din]; M3.2 owns that orientation conversion.

The Harvard source and the safe reference have different jobs

A10 and A11 reconstruct the compact function from the Annotated Transformer:

d_k = query.size(-1)
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
if mask is not None:
    scores = scores.masked_fill(mask == 0, -1e9)
p_attn = scores.softmax(dim=-1)
if dropout is not None:
    p_attn = dropout(p_attn)
return torch.matmul(p_attn, value), p_attn

That code is an excellent equation spine. It is not a complete execution manifest. A defensive oracle makes assumptions explicit:

Harvard linePreserved meaningSafe-reference addition
query.size(-1)Q/K score width owns 1/sqrt(Dk)assert Q and K widths agree; record alpha
key.transpose(-2,-1)exchange K’s row and feature axesrecord shape/stride and keep key/value row IDs paired
torch.matmulbatch/head axes are batch dimensionsreject unintended Q/K/V broadcasting before calling it
/ sqrt(d_k)scale every compatibility logit oncerecord configured multiplier; detect double scaling
masked_fill(mask == 0, -1e9)illegal keys leave the distributionadapt canonical True=legal; use -inf; reject empty used rows
softmax(dim=-1)normalize over source/key rowsexpose max, shifted logits, exponentials, and denominator in work dtype
dropout(p_attn)sample consumed routing weightsretain both pre-dropout P and consumed Pdrop; record replay state
p_attn @ valuemix value rows with key-axis weightsassert K rows equal V rows and preserve source row identity
return p_attnexpose the function’s current weight objectname whether returned weights are pre- or post-dropout

PyTorch’s torch.matmul broadcasts its leading batch dimensions. That behavior is useful, but it can hide a missing head or batch axis. The safe oracle requires Q/K/V leading axes to match exactly and permits broadcasting only for declared metadata such as visibility and finite bias. PyTorch’s broadcasting rules align trailing dimensions; they do not know that one axis means head and another means query.

The broad A11 reconstruction is useful because it shows the complete source path in one surface:

A11 source reconstruction from token and position vectors through Q/K/V projection, masking, probability weights, value mixing, head merge, and output projection

Source-local A11 figure. Read it critically: input/position construction is upstream, projections and multi-head merge belong to M3.2, and the multiplication symbol drawn between the score matrix and mask is schematic. Authoritative visibility is not score multiplication; it is support exclusion before softmax, as derived in Loop 4.

Worked detail The cross-attention call traced stage by stage (projection, scores, a large bias on the padded row, masking, stable softmax, value mixing), showing that the mask beats any finite bias; open it for a second complete numerical example. (about 480 words, 4 code listings, 30 equations)

Fixed fixture: two target rows read three source rows

The fixture is a one-batch, one-head cross-attention call:

B=1, H=1, Q=2, K=3, Dmodel=3, Dk=2, Dv=2
target token IDs: [31, 47]
source token IDs: [11, 22, PAD]
source row IDs:   [source:0, source:1, source:2]

Token IDs are provenance labels, not arithmetic inputs to M3.1. Upstream embedding/block computation supplies target states Xq and source memory Xm:

The caller’s deterministic projection matrices are:

Stage 1: project and expose the head axis

The M3.2 caller computes:

After adding batch and head axes:

Q: [1,1,2,2]
K: [1,1,3,2]
V: [1,1,3,2]

Key row j and value row j still name the same source row. That paired-row invariant is more important than the coincidence Dk=Dv=2 in this fixture. Permuting K without applying the same source-row permutation to V leaves every matrix multiplication shape-valid and changes the retrieved content.

Stage 2: transpose the key view and compute compatibility

K.transpose(-2,-1) is a logical [1,1,2,3] view. The batched product is:

With alpha=1/sqrt(2)=0.70710678:

The output row axis comes from Q and the column axis comes from K. This is why the result is target-by-source [2,3], not a square matrix inferred from tensor rank.

Stage 3: add finite preference, then enforce source visibility

Use a query-specific finite bias:

The padded source row deliberately receives +7. This turns operation order into an executable test: no finite preference may revive an illegal edge.

The semantic visibility object begins as a source-row vector:

source_visible: [True, True, False]      shape [B,K] = [1,3]
visible:        [True, True, False]      shape [B,1,1,K] = [1,1,1,3]
broadcast view: two identical query rows shape [B,H,Q,K] = [1,1,2,3]

After bias:

After authoritative support exclusion:

The large third-column bias remains observable in biased_scores for audit, but it cannot enter the denominator.

Stage 4: expose the stable-softmax state

The legal row maxima are:

Subtracting them and exponentiating legal entries gives:

with denominators:

Therefore:

The fixture uses float64 so the published values and executable oracle can agree tightly. A production eager call may use float32 for the softmax work state and cast probabilities back. The torch.softmax dtype parameter makes that policy explicit; fused kernels may preserve the same mathematical row through a different internal schedule.

Stage 5: consume probabilities and mix source values

Evaluation mode fixes:

The per-head output is:

For the first target row:

0.3650 * value(source:0) + 0.6350 * value(source:1)
= 0.3650 * [1, 2]       + 0.6350 * [2, -1]
= [1.6350, 0.0951]

value(source:2) contributes exactly zero. The reason is semantic exclusion, not a small score and not attention dropout.

Code and tensors Every intermediate with its shape and value, a traceable reference implementation, how source-row identity survives each step, how self-, cross- and cached attention differ only in their inputs, and stage-by-stage assertions; open it when instrumenting your own attention code. (about 690 words, 5 code listings)

Every intermediate has both values and execution metadata

The complete local ledger is:

ObjectShapeValue summaryStorage/materialization statement
q[1,1,2,2][[2,1],[1,2]]caller-produced head view or tensor
k[1,1,3,2]three source key rowscaller-produced; row-aligned with V
v[1,1,3,2]three source payload rowscaller-produced; row-aligned with K
k_t[1,1,2,3]same K values, axes exchangedtranspose view; no semantic row reorder
dot[1,1,2,3][[3,3.5,2.5],[3,2.5,3.5]]materialized by this eager oracle
scaled[1,1,2,3]dot / sqrt(2)one configured scale
bias_effective[1,1,2,3]query-specific, including illegal +7broadcast view when caller supplies fewer axes
visible_effective[1,1,2,3]third source column falsebroadcast view; semantic True=legal
masked_scores[1,1,2,3]third column -infauthoritative support encoding
row_max[1,1,2,1][2.6749,2.1213]reduction work state
exp_shifted[1,1,2,3]masked column exactly zeronormalization work state
denominator[1,1,2,1][1.5749,1.6354]strictly positive used-row state
pre_dropout[1,1,2,3]rows sum to onesemantic probability object
dropout_multiplier[1,1,2,3]all ones in this eval fixturetraining would require sampled replay state
consumed[1,1,2,3]equals P in evaluationweights actually passed to V mix
output[1,1,2,2]two target-indexed value mixturesM3.1 return to M3.2 caller

Full matrices belong in a tiny oracle trace, not in production telemetry. A runtime manifest normally records shape, dtype, device, stride, contiguity, materialization policy, and small checksums/statistics. Dumping attention matrices for real batches changes memory lifetime, may expose sensitive token relationships, and can force a different backend.

A traceable safe eager reference

The following function is intentionally more verbose than the code spine. It is a semantic oracle and debugging instrument, not the recommended production kernel:

def eager_attention_trace(
    q, k, v, *, visible, bias=None,
    dropout_multiplier=None, softmax_dtype=torch.float32,
):
    # Local contract: already projected/head-shaped tensors.
    assert q.ndim == k.ndim == v.ndim == 4
    assert q.shape[:2] == k.shape[:2] == v.shape[:2]
    assert q.shape[-1] == k.shape[-1]
    assert k.shape[-2] == v.shape[-2]
    assert q.device == k.device == v.device

    B, H, Q, Dk = q.shape
    K, Dv = k.shape[-2], v.shape[-1]
    expected_scores = (B, H, Q, K)
    trace = {"q": q, "k": k, "v": v}

    k_t = k.transpose(-2, -1)
    dot = torch.matmul(q, k_t)
    alpha = Dk ** -0.5
    scaled = dot * alpha

    if bias is None:
        bias_effective = torch.zeros_like(scaled)
    else:
        bias = bias.to(device=scaled.device, dtype=scaled.dtype)
        bias_effective = torch.broadcast_to(bias, expected_scores)
    biased = scaled + bias_effective

    visible = visible.to(device=scaled.device, dtype=torch.bool)
    visible_effective = torch.broadcast_to(visible, expected_scores)
    if not visible_effective.any(dim=-1).all():
        raise ValueError("every consumed query row needs at least one legal key")
    masked = biased.masked_fill(~visible_effective, -torch.inf)

    work = masked.to(softmax_dtype)
    row_max = work.max(dim=-1, keepdim=True).values
    shifted = work - row_max
    exp_shifted = torch.where(
        visible_effective, torch.exp(shifted), torch.zeros_like(shifted)
    )
    denominator = exp_shifted.sum(dim=-1, keepdim=True)
    pre_dropout_work = exp_shifted / denominator
    pre_dropout = pre_dropout_work.to(v.dtype)

    if dropout_multiplier is None:
        dropout_multiplier = torch.ones_like(pre_dropout)
    else:
        assert dropout_multiplier.shape == expected_scores
        dropout_multiplier = dropout_multiplier.to(
            device=pre_dropout.device, dtype=pre_dropout.dtype
        )
    consumed = pre_dropout * dropout_multiplier
    output = torch.matmul(consumed, v)

    trace.update({
        "k_t": k_t,
        "dot": dot,
        "alpha": alpha,
        "scaled": scaled,
        "bias_effective": bias_effective,
        "biased_scores": biased,
        "visible_effective": visible_effective,
        "masked_scores": masked,
        "row_max": row_max,
        "shifted": shifted,
        "exp_shifted": exp_shifted,
        "denominator": denominator,
        "pre_dropout": pre_dropout,
        "dropout_multiplier": dropout_multiplier,
        "consumed": consumed,
        "output": output,
    })
    assert output.shape == (B, H, Q, Dv)
    return output, trace

Two choices are deliberate.

First, Q/K/V do not broadcast. torch.matmul could broadcast leading axes, but a safe reference rejects that convenience because physical head mapping is an M3.2 decision. Visibility and finite bias may broadcast because their manifests explicitly declare that behavior.

Second, torch.broadcast_to returns an expanded view. A broadcast head/query axis can have stride zero: several logical coordinates name the same storage. The trace never mutates bias_effective or visible_effective in place. A later implementation that writes through an expanded view is not equivalent to reading broadcast metadata.

An end-to-end debugger should carry row labels beside tensor values:

query row labels:
    [target:0, target:1]

key/value row labels:
    [source:0, source:1, source:2]

score cell [i,j]:
    compatibility(target:i, source:j)

probability cell [i,j]:
    normalized legal routing weight from target:i to source:j

output row [i]:
    mixture of source value rows for target:i

Transpose does not reorder source identities; it changes how K participates in the matrix product. Broadcasting the source mask does not create new source rows; it applies one source-validity decision to several heads/query rows. Softmax does not move identities; it normalizes each fixed query row over the key axis. Value mixing removes the key axis by contracting each probability column with the correspondingly indexed value row.

This yields a useful conservation rule:

before P @ V:
    key axis j and value row j must denote the same source object

after P @ V:
    key axis is contracted away;
    query row identity and value feature identity remain

The rule catches a data-pipeline bug that pure tensor algebra cannot: if K is sorted, sharded, cached, or gathered under one index map and V under another, the calculation is numerically valid and semantically corrupt.

Self-attention, cross-attention, and cached self-attention are caller variants

The safe function does not branch on an attention_type string. The caller creates different source objects and coordinates:

CallQ lineageK/V lineageTypical shapeVisibility owner
full self-attentioncurrent stream, all query rowssame stream, same represented rows[B,H,S,S]bidirectional or causal/padding policy
rectangular cross-attentiontarget streamencoder/source memory[B,H,T,S]source validity; normally no target causal triangle
cached one-token self-attentioncurrent rowprior cached rows plus current row[B,H,1,past+1]cache coordinates and chronology
chunked cached self-attentionseveral current rowscache prefix plus current chunk[B,H,Qchunk,past+Qchunk]absolute query/key coordinates

The full numeric fixture is cross-attention because Q=2 and K=3 make axis ownership visible. A rectangular cached-self call can have the same score shape while representing a different graph. Shape does not determine whether the source is encoder memory or history from the same stream.

Reconciliation assertions

The fixture checks values at stage boundaries rather than trusting only the final output:

output, t = eager_attention_trace(...)

assert t["k_t"].shape == (1, 1, 2, 3)
assert t["dot"].shape == (1, 1, 2, 3)
assert t["visible_effective"].shape == t["dot"].shape
assert torch.isneginf(t["masked_scores"][..., 2]).all()
assert (t["pre_dropout"][..., 2] == 0).all()
torch.testing.assert_close(
    t["pre_dropout"].sum(dim=-1),
    torch.ones((1, 1, 2), dtype=t["pre_dropout"].dtype),
)
torch.testing.assert_close(output, t["consumed"] @ t["v"])

The evidence oracle additionally compares:

vectorized safe trace
    == scalar row-by-row reference
    == Harvard-style translated-mask reference

output row i
    == sum_j probability[i,j] * value[j]

masked source column
    == zero probability and zero value contribution

The Harvard comparison is restricted to its shared contract: one head-shaped Q/K/V call, no empty legal row, the same finite bias inserted before its mask step, dropout disabled, and 1=legal/0=blocked mask adaptation. Agreement does not prove that -1e9 is a universal mask policy or that implicit dtype/backend choices are safe.

How this was checked A table of shape-preserving mistakes this trace catches, and the commands the article's test code runs for it; open it when choosing debugging tests. (about 280 words, 2 code listings)

Loop 7 failure lab

FailureWhy the final shape can still passBroken trace invariantDetection
permute K without Vboth remain [B,H,K,D]key row j no longer addresses value row jrow-ID manifest plus scalar weighted-sum oracle
normalize over query axissoftmax returns the same rankeach source column competes across queriesassert row sums over K, not column sums over Q
align [B,Q,K] mask as if it were [B,1,Q,K]dimensions may accidentally agree when B=Hbatch identity is consumed as head identitycanonicalize named axes before broadcasting
add finite bias after maskinga large finite value can replace/revive exclusion under a faulty encodingsupport must dominate preferencefixture’s illegal +7 column remains zero probability
squeeze batch/head axes in a toyB=H=1 numeric result looks rightgeneral batched/head contract is lostkeep rank-four assertions in the oracle
transpose V like Kdimensions may pass when K=DvP contracts with the value feature axissource-row weighted-sum check
silently broadcast Q/K/V headstorch.matmul permits leading-axis broadcastphysical head ownership changesrequire identical Q/K/V leading axes
mutate a broadcast mask viewrepeated logical rows may alias storagemetadata adaptation changes another rowread-only expanded view and no in-place operations
return only one unnamed weight tensorcaller cannot tell P from Pdropinterpretation and backward state become ambiguousnamed pre_dropout and consumed fields
validate only final outputcompensating bugs can cancel in one fixtureno stage-level reconciliationfreeze intermediate shapes and values

Loop 7 executable gate

The deterministic evidence package runs the full fixture and freezes the source/visual contract:

cd docs/numinens-evidence/m3-1-attention-score-path
python3 loop7_source_to_tensor_oracle.py
python3 loop7_contract_audit.py
python3 -m unittest -v test_loop7_contract

Publication requires:

every projected row retains target/source provenance;
every intermediate shape and numeric value matches the fixture;
vectorized, scalar, and Harvard-style references agree;
the illegal high-bias source row remains excluded;
K/V row permutations fail the semantic oracle despite valid shapes;
only declared metadata broadcasts;
the M3.1/M3.2 ownership firewall remains explicit;
Loops 1-6 section hashes remain frozen.

0x0A2 SDPA, Framework, and Model Runtime Integration

This section follows the same call into PyTorch’s scaled_dot_product_attention, nn.MultiheadAttention and Hugging Face model code: mask polarity, causal alignment for cached calls, grouped-query heads, dropout in evaluation mode, backend selection, and tests that compare eager and fused paths. Open it when you move from the reference function to a framework call.

Implementation and runtime contracts Why framework paths that implement the same equation can still disagree on mask polarity, causal alignment, dropout mode, grouped heads, returned weights or backend. (about 120 words)

Loop 7 ended with a defensive eager function whose intermediate values can be inspected. Production models rarely call that function directly. They project and position Q/K/V, adapt a semantic visibility relation to a framework API, ask a dispatcher for an eligible implementation, and return only the objects the caller requested.

That indirection is useful, but it creates a new class of bugs. Two runtime paths may implement the same equation while disagreeing about boolean polarity, causal alignment, dropout mode, GQA head mapping, returned weights, or whether a requested backend actually ran.

Owner question. How does the exact eager operation from Loop 7 cross a framework and model-runtime boundary without allowing API defaults or backend dispatch to redefine its support, scale, stochastic state, or output meaning?

How this was checked The four runtime layers kept apart here (semantic operation, API adapter, dispatcher, model runtime) and the framework versions the claims are tied to. (about 260 words, 1 code listing)

Loop 8 contract: preserve semantics across four runtime layers

The word SDPA is overloaded. It can name the mathematical operation, the PyTorch functional API, or whichever kernel that API selects. Loop 8 keeps four objects separate:

LayerObjectWhat is fixed hereWhat remains elsewhere
semanticAttention(Q,K,V,visible,bias,scale,dropout_state)legal edges, finite bias, scale, probability axis, dropout state, value mixtureM2.1/M3.9 construct sequence policy
API adapterSDPA/MHA/model attention argumentspolarity, shape, causal alignment, training-conditioned dropout, GQA flag, return contractM3.2 owns projections and head assembly
dispatchereligible implementation set and observed executionrequested policy is recorded separately from observed executionM4.3 derives fused schedules and IO cost
model runtimelayer call, position state, cache state, output requestexact tensors and metadata handed into/out of the local callM3.3 owns position geometry; M3.10 owns request-local logical cache/frontier requirements; M5.1 owns persistent publication/lifecycle, M5.2 scheduling, and M5.3 physical mutation

The contract is:

same semantic manifest
    -> backend-specific adapter
    -> framework call
    -> local output O with declared tolerance

never:
framework default
    -> inferred model semantics

This section is version-scoped to the PyTorch 2.13 functional SDPA and MultiheadAttention documentation, plus the Hugging Face Transformers attention-interface and LLaMA implementation snapshots linked below. These are external implementation contracts, not timeless properties of every framework version. The manifest must record actual package versions and source revisions used by a run.

Implementation and runtime contracts PyTorch SDPA arguments, mask adapters, cache-aligned causal masks, grouped-query heads, dropout in evaluation, backend selection, nn.MultiheadAttention, Hugging Face integration and mask registries, a typed call object, and a parity test matrix. (about 1,830 words, 24 code listings, 18 equations, 3 figures)

One equation, one functional call, several hidden decisions

PyTorch’s functional entry point has the shape:

F.scaled_dot_product_attention(
    query,
    key,
    value,
    attn_mask=None,
    dropout_p=0.0,
    is_causal=False,
    scale=None,
    enable_gqa=False,
)

The version-scoped documentation gives the logical reference program:

where scale=None means:

and E=query.size(-1). Passing scale=alpha supplies the multiplier itself; it is not a denominator and it is not multiplied by the default a second time.

The documented tensor axes are more general than the four-axis fixture:

Q: [N, ..., Hq, L,  E]
K: [N, ..., H,  S,  E]
V: [N, ..., H,  S, Ev]
O: [N, ..., Hq, L, Ev]

N,... are batch-like axes, Hq is the query-head count, H is the key/value head count, L is query length, and S is key/value length. The score object is logically [N,...,Hq,L,S]. The API may broadcast an attention mask to that shape, but Loop 2’s strict contract still rejects accidental broadcasting of Q, K, or V batch axes. Framework permissiveness is not evidence that the model intended replication.

The returned tensor alone does not reveal:

which mask encoding was consumed;
whether causal alignment was upper-left or cache-aligned;
whether K/V heads were repeated logically;
whether dropout was active;
which implementation executed;
whether a quadratic probability tensor was materialized;
which numerical reduction order was used.

Those are runtime-manifest fields, not facts to reverse-engineer from O.

The visibility adapter is a polarity firewall

M3.1’s canonical relation remains:

For a dense additive encoding, lower it exactly once:

The adapter matrix is deliberately explicit:

InterfaceBoolean True meansFloat legal/blockedRequired conversion from M3.1 visible
functional SDPA attn_maskparticipate0 / -infboolean: pass visible; float: lower with the equation above
nn.MultiheadAttention.attn_maskblockedadditive floatboolean: pass ~visible after wrapper-specific reshaping
nn.MultiheadAttention.key_padding_maskignore that key rowadditive floatreduce only if legality is query-independent, then invert
Transformers SDPA 4D maskparticipate0 / -infuse the backend’s registered mask formatter or a validated 4D mask
eager Transformers attentionbackend-defined; current LLaMA path adds a float mask0 / -infprovide additive legality/bias in the expected shape

PyTorch explicitly documents the boolean polarity difference between functional SDPA and MultiheadAttention. This is not a naming inconvenience. The following two tensors encode the same support:

semantic / SDPA bool visible:      MHA bool blocked:
[[1, 0, 0],                       [[0, 1, 1],
 [1, 1, 0],                        [0, 0, 1],
 [1, 1, 1]]                        [0, 0, 0]]

A float 1/0 matrix is neither representation. Under additive semantics, 0 keeps a score and 1 rewards it. It masks nothing.

def lower_visibility_for_sdpa(visible, *, q, k):
    score_shape = (*q.shape[:-2], q.size(-2), k.size(-2))
    visible = torch.broadcast_to(visible, score_shape)
    if not visible.any(dim=-1).all():
        raise ValueError("all-masked query row")
    return visible  # SDPA bool polarity: True participates


def lower_visibility_for_mha(visible):
    return ~visible  # MHA bool polarity: True blocks

The real adapter must also validate dtype, device, batch/head expansion, and the all-masked-row policy frozen in Loop 4. ~mask is not a complete adapter.

Causal is a coordinate relation, not a triangular adjective

For a square prefill with query and key rows beginning at the same absolute position, causal visibility is:

For cached decoding, query row i can begin at absolute offset q_offset:

If L=2, S=5, and the two query rows are the final two rows of the key history, then q_offset=S-L=3. The desired lower-right relation is:

             key j ->  0 1 2 3 4
query abs 3 / row 0:  1 1 1 1 0
query abs 4 / row 1:  1 1 1 1 1

An upper-left triangle instead means:

             key j ->  0 1 2 3 4
query row 0:           1 0 0 0 0
query row 1:           1 1 0 0 0

Both are lower-triangular-looking relations. Only one uses the caller’s cache coordinates. PyTorch 2.13 documents is_causal=True as upper-left alignment for a non-square matrix. Its causal_lower_right(L,S) bias expresses the second relation. Therefore:

square prefill:
    is_causal=True may encode the complete support

rectangular cached call:
    do not infer cache alignment from is_causal=True
    provide a validated lower-right/cache-position relation or backend adapter

custom padding/packing/window constraints:
    compile the full semantic relation once
    do not also ask another layer to add an assumed causal triangle
Upper-left and lower-right causal visibility matrices for two query rows against five key rows, showing why cache coordinates must determine rectangular alignment

The safest runtime object carries coordinates, not only a boolean named is_causal:

@dataclass(frozen=True)
class AttentionCoordinates:
    q_positions: Tensor  # [B, L]
    k_positions: Tensor  # [B, S]
    q_segments: Tensor | None
    k_segments: Tensor | None


visible = (
    coordinates.k_positions[:, None, None, :]
    <= coordinates.q_positions[:, None, :, None]
)

Packing, windows, prefix regions, and padding compose with this relation before backend lowering. M3.9 owns the packed-example metadata; M3.10 owns the request-local logical cache-position/frontier transition. M5.1 owns persistent publication/lifecycle, and M5.3 owns physical cache mutation. M3.1 owns checking that the concrete relation arriving at the score boundary matches those coordinates.

GQA changes head mapping, not the local probability axis

Functional SDPA can receive more query heads than key/value heads:

Q: [B, Hq, L, E]
K: [B, Hkv, S, E]
V: [B, Hkv, S, Ev]

For equal-size groups, require:

With Hq=4 and Hkv=2:

query head:  0 1 2 3
KV head:     0 0 1 1

The documented enable_gqa=True reference repeats K and V over the head axis before the same score operation. That establishes semantic equivalence:

It does not require an optimized kernel to materialize repeated K/V rows. M3.2 owns why the checkpoint has Hq and Hkv, how heads are projected, and how outputs merge. M3.1 only records the head map consumed by the local call.

PyTorch 2.13 marks functional GQA support experimental and documents backend and shape restrictions. A runtime adapter should prefer capability checks over assuming that enable_gqa=True is accepted everywhere:

assert hq % hkv == 0
assert key.size(-3) == value.size(-3)

if backend_capabilities.native_gqa:
    out = F.scaled_dot_product_attention(
        q, k, v, attn_mask=mask, enable_gqa=True, **runtime_args
    )
else:
    # Semantic fallback; memory cost is explicit and benchmarked separately.
    group = hq // hkv
    k_expanded = k.repeat_interleave(group, dim=-3)
    v_expanded = v.repeat_interleave(group, dim=-3)
    out = F.scaled_dot_product_attention(
        q, k_expanded, v_expanded, attn_mask=mask, **runtime_args
    )

The fallback is a conformance path, not a promise about memory-efficient serving. M5.7 owns the cache-size consequences of MHA, GQA, and MQA, and M5.8 those of MLA.

Dropout mode must cross the module/function boundary explicitly

Functional SDPA always applies dropout according to the supplied dropout_p. It does not inspect the caller module’s .training flag. The adapter must set:

dropout_p = module.attention_dropout if module.training else 0.0

out = F.scaled_dot_product_attention(
    q,
    k,
    v,
    attn_mask=mask,
    dropout_p=dropout_p,
    is_causal=False,
    scale=module.scaling,
)

This is exactly the pattern in the current Transformers LLaMA attention call: the module computes projections, RoPE, and cache state; then passes a training-conditioned dropout value and explicit scaling into the selected attention interface.

Three stochastic states must be distinguished:

StateMeaningRequired evidence
training with attention dropoutsampled Z changes Pdrop and gradientsseed/RNG policy, dropout_p, training mode, replay contract
evaluationdropout_p=0.0 at the functional callrepeated-call equality within backend policy
backend comparison during trainingdifferent kernels may consume RNG differentlycompare distribution/gradients under a controlled test, not elementwise masks unless coordinate RNG is guaranteed

Loop 6 owns the dropout derivation and backward replay. Loop 8 owns carrying the mode and rate into the selected runtime function without silently changing that derivation.

Backend request, eligibility, selection, and observation are different facts

PyTorch documents automatic selection among SDPA implementations according to the inputs. A request such as FLASH_ATTENTION is a policy constraint, not a portable guarantee that every shape, dtype, device, mask, dropout, and gradient mode is eligible.

The runtime record needs four stages:

requested_backends:
    ordered policy supplied by the caller

eligible_backends:
    implementations that accept this concrete call

selected_backend:
    implementation chosen by the dispatcher, if observable

observed_evidence:
    strict-context success/warnings and profiler/operator trace

The preferred control surface is the scoped sdpa_kernel context manager:

from torch.nn.attention import SDPBackend, sdpa_kernel

with sdpa_kernel(SDPBackend.MATH):
    reference = F.scaled_dot_product_attention(q, k, v, **args)

with sdpa_kernel(SDPBackend.FLASH_ATTENTION):
    candidate = F.scaled_dot_product_attention(q, k, v, **args)

Forcing a single backend and receiving an eligibility error is useful evidence. Allowing several backends and seeing a numerically plausible output is not proof of which one ran. Record profiler/operator evidence when dispatch identity matters.

PyTorch 2.13 SDPA pathLogical contractNumerical/runtime qualification
math/C++same SDPA formula; supports float64documented to keep intermediates in float for half/bfloat16 inputs unless reduced-precision math reduction is enabled
FlashAttention pathsame logical output without globally materializing S/Pfused operation order can change low-order bits; eligibility is input/hardware dependent
memory-efficient pathsame logical output with another fused schedulebackward and reduction order can differ; eligibility is input/hardware dependent

The current PyTorch reproducibility note further distinguishes deterministic forward from backend-dependent backward behavior. Determinism is therefore a run policy, not an implication of setting one random seed:

framework version and build
accelerator and architecture
input dtype and accumulation policy
requested backend set/order
deterministic-algorithm flags
training/evaluation mode
dropout/RNG state
compile/export state

M4.3 owns why a fused implementation can reduce HBM traffic. Loop 8 owns only whether the runtime call preserved the M3.1 semantic operation and what execution evidence was recorded.

nn.MultiheadAttention is a caller and adapter, not an alias for functional SDPA

nn.MultiheadAttention owns projections, head layout, output projection, mask merging, and an optional weight-return contract. Functional SDPA starts after those learned projections. Treating the two APIs as interchangeable hides several differences:

QuestionFunctional SDPAnn.MultiheadAttention
input levelprojected Q/K/V with head axesembedding-level query/key/value; wrapper owns projection and merge
boolean maskTrue participatesTrue blocks for attn_mask and key_padding_mask
attention weightsreturns output onlyoptional weights; default may average heads
dropout modecaller passes effective ratemodule training state controls module dropout
optimized pathdispatcher chooses eligible SDPA implementationoptimized SDPA use depends on wrapper conditions; need_weights=False is documented for best performance

Requesting weights changes the output contract:

need_weights=False:
    output only; optimized SDPA path can remain eligible

need_weights=True, average_attn_weights=True:
    [B,L,S] head-averaged weights

need_weights=True, average_attn_weights=False:
    [B,H,L,S] per-head weights

That returned object is not automatically the same debug object as the pre-dropout P in Loop 6, and a backend that returns only O need not have materialized full probabilities in HBM. The caller must name the requested object and accept any backend change required to produce it.

Model integration is an object graph, not one attention line

The original A10 figure is useful precisely because it shows the complete source-to-output path that a framework later decomposes into modules and runtime adapters.

A10 source-to-output attention reconstruction showing input rows, Q K V projections, score matrix, softmax routing weights, and value mixing

Source-local visual from A10. The learned Q/K/V projections and final multi-head output belong to M3.2. Loop 8 uses the figure to locate the narrow runtime seam where projected, positioned, cache-complete tensors enter M3.1.

Current Transformers model code makes that seam explicit through an attention interface. A LLaMA-style call has this ownership trace:

hidden_states
    -> q_proj / k_proj / v_proj                 M3.2
    -> reshape and expose head axes             M3.2
    -> apply RoPE to Q and K                    M3.4
    -> derive request-local K/V frontier        M3.10
    -> publish/map persistent K/V rows          M5.1 and M5.3
    -> create backend-specific causal mask      M3.1 consumes policy
    -> resolve configured attention interface   framework runtime
    -> local score/dropout/value operation      M3.1
    -> merge heads and o_proj                   M3.2
    -> residual/block continuation              M3.7
Runtime attention layers separating model-owned projections positions and cache from semantic attention, API mask adaptation, backend dispatch, and output assembly

The model-level pseudocode should expose each handoff:

def model_attention_forward(
    module,
    hidden_states,
    *,
    position_embeddings,
    attention_policy,
    past_key_values,
    runtime,
):
    # M3.2: checkpoint-owned projections and head layout.
    q, k, v = module.project_and_split(hidden_states)

    # M3.3: checkpoint/config-owned positional geometry.
    q, k = module.apply_position_geometry(q, k, position_embeddings)

    # M3.10: request-local logical frontier; M5.1/M5.3: persistence/mapping.
    k, v, coordinates = past_key_values.update_and_describe(k, v)

    # M3.1 adapter: one semantic support relation, one backend encoding.
    visible = compile_visibility(attention_policy, coordinates)
    call = lower_runtime_call(
        q=q,
        k=k,
        v=v,
        visible=visible,
        finite_bias=runtime.finite_bias,
        scale=module.scaling,
        dropout_p=module.attention_dropout if module.training else 0.0,
        requested_backend=runtime.requested_backend,
        need_weights=runtime.need_weights,
    )

    local_output, optional_weights, execution = execute_attention(call)
    runtime.record(execution)

    # M3.2: concatenate/reshape query heads and apply W_O.
    return module.merge_and_project(local_output), optional_weights

This code does not prescribe one framework. It states the minimum information that must survive any framework adapter.

Attention and mask registries are one coupled extension point

Transformers exposes an AttentionInterface that can select eager, SDPA, FlashAttention, FlexAttention, or a registered custom function. Its AttentionMaskInterface separately formats masks for the active implementation. The documentation warns that registering a custom attention implementation without a matching mask formatter can skip mask creation and pass attention_mask=None.

That is a critical correctness contract:

AttentionInterface.register("my_sdpa", my_sdpa_forward)
AttentionMaskInterface.register("my_sdpa", sdpa_mask)

The two registry entries form one semantic adapter. Unit-testing only the attention function with a hand-built mask does not prove that model execution will construct and deliver that mask.

Backend-specific mask forms also differ:

SDPA:             dense boolean or additive mask broadcastable to [B,H,L,S]
eager LLaMA:      additive mask applied to materialized scores
FlashAttention:   often lengths / 2D padding metadata / causal flags
FlexAttention:    BlockMask or score-modification contract

The model should construct policy once, then call the formatter registered for the active backend. It should not independently invert or expand masks in both the model and backend adapter.

A runtime adapter needs a typed call object

Loose keyword arguments make it easy to combine individually valid choices into an invalid call. Freeze the call before execution:

@dataclass(frozen=True)
class AttentionRuntimeCall:
    q_shape: tuple[int, ...]
    k_shape: tuple[int, ...]
    v_shape: tuple[int, ...]
    q_positions: tuple[int, ...]
    k_positions: tuple[int, ...]
    semantic_visibility_hash: str
    mask_encoding: str
    mask_shape: tuple[int, ...] | None
    mask_polarity: str | None
    finite_bias_present: bool
    scale: float
    training: bool
    dropout_p: float
    enable_gqa: bool
    q_heads: int
    kv_heads: int
    need_weights: bool
    requested_backends: tuple[str, ...]
    framework_version: str
    device: str
    dtype: str

Validate cross-field invariants before invoking the framework:

assert q_shape[-1] == k_shape[-1]
assert k_shape[-2] == v_shape[-2]
assert len(q_positions) == q_shape[-2]
assert len(k_positions) == k_shape[-2]
assert dropout_p == (configured_dropout if training else 0.0)
assert q_heads % kv_heads == 0
assert not (mask_encoding == "explicit" and is_causal_flag)
assert semantic_visibility_hash == hash(reconstructed_visible)

The last assertion prevents a successful API call from becoming the only proof that the mask was correct.

Runtime parity is a matrix, not one allclose

The Loop 7 fixture tests one evaluation call. Runtime integration needs a bounded matrix that changes one contract at a time:

DimensionMinimum casesWhat it catches
attention modeself, rectangular cross, cached selfwrong source axis or causal alignment
supportnone, causal, padding, packed/custommask omission, polarity, composition
Q/K lengthssquare, L<S, L=1upper-left/lower-right and cache offset errors
head relationMHA Hq=Hkv, GQA Hq>Hkvwrong repeat/group mapping
modetrain and evalfunctional dropout left active in eval
returnoutput only, per-head weights if supportedbackend/materialization changes
dtypefloat64 math oracle, production low precisiontolerance and accumulation differences
backendeager, forced math, each eligible fused pathsemantic parity and dispatch evidence
autogradforward, Q/K/V gradientbackend-specific backward mismatch
executioneager, compiled/exported if shippedgraph-capture and adapter divergence

Use a float64 eager oracle for formula conformance. Production low-precision paths need declared absolute/relative tolerances and task-level checks. Exact bitwise equality across backend reduction orders is not a general requirement.

for case in runtime_cases:
    eager = eager_attention_trace(**case.semantic_inputs)
    candidate, execution = run_framework_path(case)

    assert execution.requested_backends == case.requested_backends
    assert execution.mask_hash == case.semantic_visibility_hash
    assert_close(candidate.output, eager.output, **case.tolerance)

    if case.check_gradients:
        assert_close(candidate.grad_q, eager.grad_q, **case.grad_tolerance)
        assert_close(candidate.grad_k, eager.grad_k, **case.grad_tolerance)
        assert_close(candidate.grad_v, eager.grad_v, **case.grad_tolerance)
How this was checked A table of runtime mistakes with their symptoms and detection, and the commands the article's test code runs for this section. (about 290 words, 2 code listings)

Loop 8 failure lab

FailureShape-valid symptomBroken runtime contractDetection
pass MHA blocked mask directly to SDPAmodel attends only to forbidden positionsboolean polarity invertedcompare semantic visibility hash and eager oracle
pass float 1/0 support as additive maskfuture/padded tokens retain probabilityadditive mask requires 0/-infillegal-edge probability check
set is_causal=True for L<S cached rowsnew token cannot read most historyupper-left alignment used instead of cache coordinatesrectangular lower-right fixture
combine explicit full mask with another causal helperlegal prefix/window edges disappearvisibility policy applied twicereconstruct final support from coordinates
leave dropout_p>0 in evalrepeated eval outputs varyfunctional SDPA ignores module moderepeated eval and runtime-call manifest
enable GQA with wrong head groupingplausible output with wrong KV source per headHq/Hkv map incorrectexplicit-repeat parity by head
assume requested Flash means observed Flashbenchmark labels fallback as fused runrequest and dispatch evidence conflatedstrict backend context plus profiler trace
request attention weights in baseline benchmarkmemory/latency jumps or backend changesreturn contract changedrecord need_weights and materialized shape
register custom attention without mask formattercausal/padding support silently vanishesattention and mask registries decoupledmodel-level illegal-edge test, not function-only test
compare low-precision paths with zero tolerancecorrect fused path fails conformancereduction order differsdtype/backend-specific tolerance
compare only outputs, not Q/K/V gradientstraining bug survives forward paritybackward contract untestedfixed-loss gradient matrix
let runtime mutate cache inside parity oracleeager and candidate see different K/V extentsstate transition contaminated local comparisonsnapshot cache and compare immutable call objects

Loop 8 executable gate

The evidence package tests the API-independent contracts even when PyTorch is not installed in the documentation environment:

cd docs/numinens-evidence/m3-1-attention-score-path
python3 loop8_runtime_oracle.py
python3 loop8_contract_audit.py
python3 -m unittest -v test_loop8_contract

Publication requires:

SDPA and MHA boolean polarities encode identical semantic visibility;
float 1/0 masks demonstrably fail additive exclusion;
upper-left and lower-right rectangular causal outputs demonstrably differ;
lower-right visibility agrees with explicit cache coordinates;
native/logical GQA agrees with explicit K/V head expansion;
evaluation resolves functional dropout to zero;
requested, eligible, selected, and observed backend fields stay distinct;
attention-function and mask-formatter registration remain coupled;
returned-weight requests are recorded as an execution-contract change;
source and visual provenance resolve locally;
Loops 1-7 section hashes remain frozen.

0x0AA Local arithmetic and materialization ledger

The shapes also determine the first-order work inside this attention primitive. Let:

Q: [B,h,q_len,d_head]
K: [B,h,k_len,d_head]
V: [B,h,k_len,d_value]

Then the two matrix products require:

MAC means one multiply-accumulate. Under the common convention that counts a multiply and an add as two FLOPs, multiply the MAC count by approximately two. Softmax, masking, exponentials, reductions, and dropout add non-matmul work that these expressions do not count.

If an implementation materializes both score and probability tensors, each has:

elements. For a concrete self-attention call:

B       = 2
h       = 8
q_len   = k_len = 1024
d_head  = d_value = 64
dtype   = fp16/bf16, 2 bytes per stored element

the ledger is:

score elements:
    2 * 8 * 1024 * 1024
    = 16,777,216 elements
    = 32 MiB

score + probability tensors, if both materialized:
    64 MiB

QK^T MACs:
    2 * 8 * 1024 * 1024 * 64
    = 1,073,741,824 MACs

P @ V MACs:
    1,073,741,824 MACs

This does not claim that a fused backend writes both quadratic tensors to HBM. M4.3 shows how IO-aware exact attention avoids globally materializing these score and probability objects; M4.4 continues into kernel and decode boundaries. The local M3.1 conclusion is narrower: q_len * k_len is already visible in the primitive, and shape-correct changes to either sequence axis change both the routing matrix and its first-order work.

0x0AB Alternative Interaction Graphs and the M4 Boundary

This section compares the dense score path with designs that change it: local and sparse masks, block sparsity, kernel (linear) attention, retention, low-rank length projection and hybrid stacks. For each it asks which query-key edges remain, how information reaches distant tokens across layers, and what state replaces the probability matrix; open it when you evaluate an efficient-attention method.

Alternatives and history Sparse, local, block-sparse, random and content-routed attention, kernel (linear) attention, retention, length projection and hybrid stacks, compared by the edges they keep, multi-layer reachability and the state they store. (about 3,190 words, 8 code listings, 54 equations, 3 figures)

The dense score path is not the only way to move information between sequence rows. It is, however, the reference needed to say precisely what an alternative preserves and what it replaces. A method can reduce a quadratic object by:

  1. preserving every legal pair but changing the evaluation schedule;
  2. removing pairwise edges from the model;
  3. projecting source rows into fewer landmarks;
  4. replacing pairwise softmax with an associative feature-state computation;
  5. replacing stored source rows with a recurrent state;
  6. composing several of those choices across heads or layers.

Those are different model and systems contracts. Calling all of them “efficient attention” hides whether the method changed semantics, reachability, training state, inference state, or only physical materialization.

Owner question. Which query-to-source interaction relation does an alternative define, how does information travel through a stack, what state replaces the dense probability matrix, and where must M3.1 stop before hardware scheduling and IO accounting begin?

One interaction method has three ledgers

For layer l, define a directed semantic graph:

The arrow follows information, not matrix indexing: query row i reads key and value row j, so source j can influence target i. Its legal neighborhood is:

For sparse exact-softmax attention, one head still computes:

That gives three separate ledgers:

LedgerQuestionObjectOwner
semantic graphwhich direct source-target interactions exist? and M3.1
stacked reachabilityafter several residual layers, which source can affect which target?relation composition M3.1
physical schedulewhich tiles, gathers, bytes, launches, workspaces, and reductions execute?backend plan and measured traceM4

An implementation can change the third ledger without changing the first two. FlashAttention is the canonical example: it evaluates the same legal dense softmax relation with an online tiled schedule. A sparse or recurrent model changes the first or second ledger before any kernel is chosen.

Reachability composes across layers, not inside one synchronous layer

Residual connections carry each row forward, so identity is always an available path even when it is omitted from an attention mask. Let mean that source row j at the model input can influence row i after layer l. Start with identity:

One more layer composes prior reachability with that layer’s attention edges or residual identity:

This matters for global tokens. If a global row reads every source and every row reads the global row, source j reaches the global output in one layer and another target i through that updated global state in the next layer. The two reads happen from the same layer input; they do not form an instantaneous two-edge path inside one attention call.

Stacked reachability comparison showing dense one-layer access, four local-window hops from source zero to target seven, disconnected independent chunks, and a two-layer symmetric global-token route

The Loop 9 fixture uses S=8 and causal window w=3. It proves:

dense causal graph:          36 legal edges
causal local graph:          21 legal edges
independent causal chunks:   20 legal edges
local source 0 -> target 7:  4 layers
chunk source 0 -> target 7:  disconnected
symmetric source 7 -> global 0 -> target 3: 2 layers

The global example is deliberately encoder-style and symmetric. A causal decoder cannot let an early global row read future tokens. It needs a chronology-preserving placement and update policy, which changes the graph.

Dense and local edge counts can be exact before they are asymptotic

For bidirectional dense attention of length S:

For causal dense attention, query i reads i+1 rows:

Ignoring projections, normalization, and elementwise work, one head performs approximately:

because every legal edge contributes to one query-key dot product and one probability-value weighted sum.

For a causal local window of width w, row i reads keys. When S>=w:

For the fixture, S=8,w=3 gives 24-3=21, not merely “about Sw.” The boundary term matters in small tests and disappears only in the large-S asymptotic statement .

The path-depth bound follows from the same geometry. One causal local layer can move information forward by at most w-1 positions. A distance therefore requires:

For j=0,i=7,w=3, the lower bound is four layers, and the executable graph search finds four. This proof does not transfer unchanged to dilation, shifted windows, alternating heads, recurrence, or global routes.

Locality has several non-equivalent designs

“Window attention” is not one graph:

DesignDirect neighborhoodReachability consequenceBoundary condition
overlapping causal windowprior w rowsgrows by at most w-1 positions per layerfirst rows have smaller neighborhoods
independent chunksrows in the same blockcross-chunk paths are absentchunk identity becomes semantic state
shifted chunks/windowsblock partition changes by layerlater layers can bridge prior boundariesshift schedule must be checkpointed
dilated windowsampled offsets with gapslarger span for a fixed edge countuncovered residues need other heads/layers
recurrent segment memorycurrent rows read prior segment stategraph crosses segment boundariesstate detach/update policy changes training

Independent chunks are especially easy to misdescribe. They have a low edge count but infinite path distance between chunks unless another layer supplies a bridge. A shifted partition, recurrent memory, or global route changes that fact; a faster block kernel does not.

The semantic reference remains straightforward:

def graph_softmax_attention(q, k, v, legal_sources):
    out = []
    for query_id in range(len(q)):
        source_ids = legal_sources(query_id)
        scores = (q[query_id] @ k[source_ids].T) / sqrt(q.shape[-1])
        probability = softmax(scores, dim="legal_source")
        out.append(probability @ v[source_ids])
    return stack(out)

This is an executable model definition, not a performant sparse kernel.

The book’s sparse-attention comparison becomes useful after treating every dark cell as a legal edge rather than an observed attention weight:

Eight sparse-attention visibility patterns comparing dense causal, local window, chunked, strided, dilated, learned routing, global-token, and hybrid graphs

Reconstructed from book Figure 6.13 and corrected against Sparse Transformer, Longformer, BigBird, and Routing Transformer. Dark cells denote legal query-key products, not measured probabilities.

PatternLeading legal-edge countWhat edge count does not prove
dense causalwhether a fused implementation materializes scores
causal local, fixed wacceptable path depth for the task
independent chunks, size bany cross-chunk connectivity
factorized strided/fixed in Sparse Transformerthe exact path under another factorization
dilated local for fixed sampled widthcoverage of skipped offsets
content-routedmethod-dependentstable or balanced route assignment
local plus fixed global rowscausal legality or one-layer transitive influence
local/random/global hybrid for fixed route countsdeterministic connectivity for one sampled graph

Sparse Transformer’s factorized patterns reduce quadratic attention to under the paper’s chosen factorization. Longformer combines sliding-window and task-motivated global attention, with linear scaling when window and global counts remain fixed. BigBird composes local, random, and global routes and proves properties for its specified constructions; those theorems are not a license to call any sparse mask universal or Turing complete. Routing Transformer instead makes the graph content-dependent using online k-means and reports attention complexity for its balanced clustering design.

Block sparsity adds a cover between semantic edges and executed work

Hardware rarely launches one query-key dot product per graph edge. A block-sparse adapter covers semantic edges with query/key tiles. For tile sizes Bq and Bk, define:

The covered candidate slots are:

and the semantic density of that cover is:

The Loop 9 local-window fixture uses Bq=Bk=2. Its 21 useful edges touch seven tiles, which expose 28 candidate slots: seven slots are padding or masked work and .

This is still not a hardware cost model. The backend may skip fully masked tiles, specialize boundary tiles, gather irregular blocks, pad dimensions, recompute metadata, or fall back to dense attention. BigBird’s implementation discussion similarly blockifies its local, random, and global routes because irregular scalar gathers alone do not guarantee an efficient accelerator schedule.

Static, random, and content-routed sparsity have different training state

The graph itself can be fixed, sampled, or learned:

Route policyState needed to reproduce a runGradient issueRuntime issue
fixed local/stridedwindow, dilation, layer/head patternno gradient through the fixed edge setregular blocks may map efficiently
sampled randomseed or explicit sampled adjacencysampled graph contributes variancegraph generation and gathers are work
hashedhash projections, rounds, buckets, duplicate policydiscrete bucket assignmentsorting, padding, and collision handling
online clusteringcentroids, normalization, balance/update ruleassignment is piecewise discreteroute construction can dominate small calls
learned top-krouter weights, tie-break, capacity/fallbackestimator or straight-through choice may be neededimbalance and dynamic shapes matter

Routing Transformer is not merely “local attention with better keys.” Queries and keys are assigned to shared centroids, same-cluster candidates are gathered, causal legality is restored inside those candidates, and centroids are updated during training. Reformer uses locality-sensitive hashing plus reversible layers; its hash rounds, sorting, duplicate correction, and bucket policy are part of its contract. M6 owns expert routing, but token-to-token sparse routing still needs analogous reproducibility state here.

Kernel attention replaces the pair matrix with feature summaries

Let a nonnegative feature map produce width m vectors:

Phi_Q: [S,m]
Phi_K: [S,m]
V:     [S,dv]

If similarity factors as:

then normalized attention is:

For bidirectional attention, define:

Associativity yields:

The pairwise reference and reassociated program are algebraically equal for the same finite feature map and arithmetic. Loop 9’s deterministic positive-feature fixture verifies them to 5.56e-17 maximum absolute error.

Ignoring feature-map construction, the leading summary work is:

plus denominator work O(Sm). Calling this O(S) assumes m and dv do not grow with S; hiding those widths makes the comparison incomplete.

A10 source comparison deriving feature-map linear attention by moving the key-feature and value reduction before query evaluation

The A10 figure is a useful algebraic map, but its O(N d'^2) label assumes a particular relation between feature and value widths. It is not a measured runtime claim.

Linear Transformer chooses a deterministic positive map such as and therefore defines a different kernel from softmax attention. Performer instead uses FAVOR+ positive orthogonal random features to approximate the softmax kernel. These alternatives must not be collapsed:

deterministic kernel replacement:
    exact for the chosen feature kernel
    not exact dense softmax

finite random-feature approximation:
    random estimator of a target kernel
    approximation quality depends on feature count and sampled projection

exact IO-aware softmax:
    exact dense softmax relation within numerical tolerance
    no feature approximation

Positivity is not cosmetic. It keeps similarity and the denominator compatible with normalized weighted averaging. Even then, a tiny denominator, growing summary magnitude, finite-precision drift, and feature-map overflow remain numerical contracts.

Causal kernel attention is a recurrent state and a parallel-prefix problem

For causal rows, update summaries before querying position t:

The state has m*dv + m values per head instead of one stored K/V row per token. Loop 9 compares the recurrent update with explicit causal pairwise kernel attention and obtains 5.56e-17 maximum absolute error.

state_kv = zeros(feature_width, value_width)
state_k = zeros(feature_width)

for t in range(sequence_length):
    kt = phi(key[t])
    qt = phi(query[t])
    state_kv += outer(kt, value[t])
    state_k += kt
    output[t] = (qt @ state_kv) / (qt @ state_k)

This recurrence describes one-token execution, not the only training schedule. The prefix operation is associative, so training can use a scan or chunkwise scheme that materializes different intermediates. Saved states, recomputation, chunk boundaries, and backward reductions must be specified separately.

Retention shows that recurrent and parallel forms can share one contract

RetNet removes row softmax and introduces distance decay. In a simplified real form, let:

Unrolling the recurrence gives:

and therefore:

The parallel score form is a causal bilinear interaction multiplied by a decay matrix. The recurrent state and parallel matrix implement the same simplified operator; they are not the same as normalized softmax attention. Loop 9’s gamma=0.8 fixture reconciles the two forms to 4.44e-16 maximum absolute error.

The RetNet paper defines parallel, recurrent, and chunkwise recurrent representations and adds multi-scale decay, gating, position treatment, and normalization. The equations above isolate only the core equivalence. A full RetNet implementation cannot be reconstructed from this simplified state.

Length projection is an adjacent branch, not kernel attention

Linformer reduces the source-length axis with learned projections. Conceptually:

The probability matrix is now [S,k], but each projected row can mix many original source rows. This is not a sparse subset of original edges and not a feature-map reassociation. In a causal decoder, a learned length projection must prove that projected source rows do not mix future information. That sequence-axis ownership is why low-rank projection remains a short adjacent branch here rather than being folded into the kernel state derivation.

Hybrid interaction graphs must be composed in layer order

Hybrids appear at two levels:

within one layer:
    local + random + global routes
    several head-specific graph families

across layers:
    local or recurrent early layers
    linear layers for most of the stack
    periodic full-softmax layers

The layer order is semantic configuration. A full-attention layer gives every row direct access to its legal inputs at that layer. It does not retroactively give earlier sparse layers access to information that had not yet crossed their graph, and information created after the full layer waits until another global route appears.

A10 source figure from The Devil in Linear Transformer showing diagonal local attention in early layers and normalized linear attention in later layers

TransNormer uses diagonal/local attention in early layers to address attention dilution and normalized linear attention later for global mixing. MiniMax-01 is a different hybrid: its report places one softmax-attention layer after every seven Lightning Attention layers. That 7:1 schedule is source-reported, not a universal optimum.

For L_lin linear layers and L_full dense causal layers, a symbolic leading ledger is:

where the factor 2 counts one feature-state update and one query-state read per linear layer, before feature construction, denominator work, projections, normalization, and runtime effects. If L_full>0 is fixed, the architecture still contains a quadratic term in S, even when the coefficient is much smaller than an all-softmax stack. The fixture’s eight-layer 7+1 schedule has one-eighth full layers and 8,256 causal pairs in its single full layer at S=128.

Paper-by-contract comparison

The purpose of this table is not to rank papers. It identifies the object each paper changes and the state an implementation must preserve.

MethodDirect interaction contractNormalization or approximationReproducible stateLeading sequence claimStatus
dense softmaxevery legal pairexact row softmaxQ/K/V, mask, bias, dropout pair workM3.1 reference
Sparse Transformerfixed factorized patternsexact softmax on selected positionsfactorization by layer/headprimary-paper verified
Longformersliding window plus task global rows; optional dilationexact softmax on selected positionswindow, dilation, global-row policylinear for fixed route countsprimary-paper verified
BigBirdlocal, random, and global sparse graphexact softmax on constructed graphrandom graph/seed, windows, globalslinear for fixed route countsprimary-paper verified
Routing Transformeronline-k-means content routes plus local heads/layersexact softmax within gathered candidatescentroids, assignment, balance, causal filter in paper designprimary-paper verified
ReformerLSH-selected candidatessoftmax over hashed buckets/roundshash projections, rounds, sorting, duplicate policy reportedprimary-paper boundary
Linear Transformerimplicit all-source feature summariesdeterministic positive kernel, not softmaxfeature map and recurrent summarieslinear in S for fixed widthsprimary-paper verified
Performerimplicit random-feature summariesFAVOR+ approximation to softmax kernelrandom projection/seed and feature countlinear space/time for fixed featuresprimary-paper verified
RetNetdecay-weighted bilinear historyno row softmax; parallel/recurrent/chunkwise formsdecay, complex/position state, normalization, recurrent statelinear sequence work; recurrent decodeprimary-paper verified
TransNormerearly diagonal/local plus later normalized linear attentionmixed local and linear operatorslayer schedule, normalization, feature mappaper-specific linear designprimary-paper verified
MiniMax-01seven Lightning layers then one softmax layerhybrid linear and softmax operatorsexact layer schedule and each operator stateretains a reduced quadratic componentsource-reported primary report
Linformerlearned projection from S rows to k landmarkssoftmax over projected rowslength projections and causal policylinear in S for fixed kadjacent primary-paper branch

The asymptotic column is conditional. Window, route, feature, landmark, head, and value widths can scale; graph construction and projection work can be nontrivial; and no O(S) label proves a lower wall-clock time at a particular shape.

Training and runtime integration are part of the architecture

An alternative interaction graph changes more than a forward equation:

Contract surfaceSparse graphKernel/recurrentHybrid
training manifestroute pattern/seed, per-head assignment, padding to buckets/blocksfeature map, projection seed, scan/chunk schedule, state dtypeexact layer operator sequence
backwardgradients only along selected edges; learned discrete routes need an estimator/update rulegradients through prefix summaries; saved versus recomputed stateoperator-specific backward by layer
checkpointrouting state and topology versionfeature projection, decay, normalization, state schemalayer schedule plus every component config
prefillbuild sparse metadata or parallel summariesscan/chunk state constructionexecute mixed operators in order
one-token decodegather legal sparse history or window cacheupdate fixed-size recurrent statesome layers append K/V, others update summaries
batchingroute and block shapes can diverge by requeststate shapes may be fixed but numerical age differsrequest state is heterogeneous by layer

M3.10 owns the one-request logical state transition. M5.1 owns the persistent request/cache lifecycle, M5.2 owns multi-request scheduling, and M5.3 owns physical allocation. Loop 9 only states which per-layer state those owners would have to carry.

The M3-to-M4 firewall: semantics, logical work, and physical evidence

Boundary diagram separating semantic legal edges and stacked reachability in M3.1 from block covers, tile schedules, HBM traffic, launch geometry, and profiler evidence in M4

M3.1 may derive logical pair counts because they follow from the model graph. It must not infer physical bytes or speed from them:

M3.1 freezes:
    legal edge relation and causal direction
    normalization/value-mixing function
    stacked reachability
    logical edge or summary-state dimensions
    required training and inference state

M4.1 integrates:
    whole-layer/model parameters, FLOPs, activations, and logical traffic

M4.3 proves for exact softmax:
    online state algebra
    tiled forward/backward schedule
    HBM/SRAM transfer bounds
    materialization and recomputation

M4.5 owns:
    decode-specific split work
    mask-program lowering
    backend admission and runtime evidence

Two opposite errors follow from crossing this firewall:

  1. N_edge < S^2, therefore the model must run faster. This ignores block cover density, route construction, sorting/gathers, padding, launch count, occupancy, and fallback.
  2. FlashAttention avoids an S x S HBM tensor, therefore it is sparse or linear attention. This confuses a physical exact schedule with a changed semantic graph or kernel.

The only publishable performance claim has an evidence coordinate:

model graph + operator parameters
    -> backend lowering and admitted schedule
    -> hardware, dtype, shape, phase, batch state
    -> measured latency/throughput/bytes

M4.1 integrates the local graph ledger into whole-layer and whole-model accounting. M4.3 owns the exact tiled-softmax schedule and IO derivation. M4.5 owns decode-phase backend admission, lowering, and measured dispatch evidence.

How this was checked A table of mistakes made when describing efficient-attention methods, and the conditions the article's test code checks for this section. (about 290 words, 2 code listings)

Loop 9 failure lab

FailureShape still looks valid becauseBroken contractDetection
call local attention “exact dense attention”every row still normalizes to onelegal edge set changedcompare visible and edge count
infer reachability from edge counttwo graphs can have equal countspath composition differsshortest-path/stacked-relation oracle
claim a global token transfers information in one layerall global edges appear in one masksynchronous layer reads pre-update statestwo-layer influence trace
use independent chunks as sliding windowsboth expose about Sb cellschunks disconnect boundariescross-chunk reachability test
equate semantic edges with block workblock tensor shapes are validboundary tiles contain masked slotsreport N_edge, N_slot, and rho_B
resample random routes without recording a seedoutput shape is unchangedgraph identity changesroute manifest hash
forget causal filtering after content routingcandidates have plausible similarityfuture keys leakabsolute-coordinate legality assertion
call deterministic linear attention a softmax approximationboth return [S,dv]chosen kernel differsrecord feature-map contract
omit kernel denominatoroutput shape and leading complexity remainnormalized weighted average is lostpairwise/reassociated oracle
initialize recurrent state after reading the querystate dimensions still matchoff-by-one causal semanticsexplicit prefix fixture
claim constant-memory training from constant-memory decoderecurrent step uses fixed statebackward needs saved or recomputed prefixestraining-state ledger
call a 7:1 hybrid linear in all termsmost layers are linearone full layer retains S^2 worksymbolic layer-sum ledger
transfer BigBird theory to any sparse maskmask is sparse and connected in a sampletheorem assumptions changedpaper-by-contract manifest
infer speed from asymptotic notationarithmetic count is lowerphysical schedule may be inefficientM4 profiler/traffic evidence

Loop 9 executable gate

The deterministic evidence package freezes the graph and state distinctions:

cd docs/numinens-evidence/m3-1-attention-score-path
python3 loop9_interaction_graph_oracle.py
python3 loop9_contract_audit.py
python3 -m unittest -v test_loop9_contract

Publication requires:

dense and local exact edge-count formulas match enumerated graphs;
local path depth and chunk disconnection are machine checked;
symmetric global routing takes two stacked layers;
semantic edges and block candidate slots remain separate;
pairwise and reassociated kernel attention agree for one feature map;
causal pairwise kernel attention and recurrent summaries agree;
parallel and recurrent retention agree for the simplified contract;
hybrid layer counts expose their remaining quadratic term;
every paper claim is source-qualified;
M4 receives schedules and physical evidence, not model semantics;
Loops 1-8 section hashes remain frozen.

0x0B What M3.1 does not own

BoundaryM3.1 importsM3.1 does not re-teach
M3.3optional finite position_bias slotT5, Shaw, ALiBi, DeBERTa, TUPE
M3.4Q/K may already have been rotated before scoringRoPE proof, rotate_half, cache-position geometry
M3.5long q_len / k_len may stress the score pathPI, NTK, YaRN, LongRoPE
M3.9masks arrive at the model boundarytarget shift, tgt_y, loss masks, label smoothing
M3.10decode may have q_len=1 and k_len=past+1loop, logical append, EOS, active rows, and request-local cache-frontier requirements; persistent cache lifecycle belongs to M5.1 and physical allocation/mutation to M5.3
M3.2Q/K/V are already shapedprojection matrices, head split, concat, output projection
M4.1local MAC and element counts from this articleintegrated layer/model totals, training state, HBM traffic, arithmetic intensity

M3.1 is deliberately narrow because the score path is already enough to debug many real failures.


0x0C Shape ledger

ObjectShapeNotes
query[B,h,q_len,d_head]query rows asking for information
key[B,h,k_len,d_head]matched rows
value[B,h,k_len,d_value]payload rows
key.transpose(-2,-1)[B,h,d_head,k_len]transpose only the last two axes
scores[B,h,q_len,k_len]one logit per query-key pair
position_bias[1,h,q_len,k_len] or broadcast-compatiblefinite preference, optional
source padding mask[B,1,1,k_len]hides key/value columns
causal mask[1,1,q_len,k_len] or compatiblehides future keys
combined target mask[B,1,q_len,k_len] or compatiblepadding + causal visibility
masked_scores[B,h,q_len,k_len]after finite bias and visibility mask
softmax accumulation[B,h,q_len,k_len]dtype is an eager/fused backend contract
p_attn[B,h,q_len,k_len]softmax over k_len
dropout mask on p_attn[B,h,q_len,k_len]training-time probability dropout
out = p_attn @ value[B,h,q_len,d_value]per-head contextual output
M3.2 merged output[B,q_len,d_model]after head concat and output projection
M4.1 score/probability object[B,H_q,S_q,S_k]cost-model handoff

Three common mode ledgers:

Modeq_lenk_lenScore shape
encoder self-attentionSS[B,h,S,S]
decoder prefix/training self-attentionTT[B,h,T,T]
cross-attentionTS[B,h,T,S]
one-token decode boundary1past + 1[B,h,1,past+1]

0x0CA Executable Oracle, Gradient Parity, and Reconciliation

This section builds one deliberately difficult test call and uses it to compare scalar and vectorized forward passes, hand-derived and numerical gradients, and differently encoded masks, including tests that must fail when the operation changes. Open it when you write tests for an attention implementation.

How this was checked One adversarial test call comparing scalar and vectorized forward passes, hand-derived and finite-difference gradients, blocked sources, mask adapters and deliberate failures, with the tolerances and a wider test matrix. (about 1,830 words, 10 code listings, 28 equations, 1 figure)

Loops 3 through 9 each froze one part of the attention contract: score families, visibility, stable normalization, dropout and backward, the complete eager trace, runtime adapters, and alternative interaction graphs. Independent fixtures are useful while deriving each mechanism, but they can still leave a gap between them. A final implementation can pass seven isolated examples and fail when finite bias, rectangular axes, fixed dropout state, mask polarity, and gradients meet in one call.

Owner question. Can one deterministic attention call reconcile scalar equations, vectorized eager code, manual backward, numerical derivatives, runtime mask adapters, and alternative-graph expectations without treating every numerical difference as either a backend bug or harmless noise?

Loop 10 adds that reconciliation layer. It does not claim that NumPy predicts the exact reduction order of a GPU kernel. It freezes the semantic baseline, the differentiable variables, the stochastic realization, and the expected verdict for each comparison before a production backend runs.

Four verdicts replace one ambiguous allclose

Two outputs with the same shape do not automatically belong in a parity test. The comparison first declares its relation:

VerdictCompared pathsRequired evidenceA difference means
exact/float64 parityscalar loops and vectorized eager evaluation of the same graphtiny absolute errorimplementation defect or oracle defect
tolerance paritysame semantic operator under different reduction/derivative evaluationpredeclared atol, rtol, dtype, backendinvestigate beyond tolerance
invariantone operator under a symmetry or limiting transformationproperty-specific equalityequation or metadata contract was violated
expected divergencepolarity, causal alignment, or legal interaction graph changedoutput delta above a minimum witnessthe test successfully distinguished operators
Reconciliation matrix separating exact parity, numerical-tolerance parity, algebraic invariants, and expected divergence when an attention contract changes

This prevents two opposite mistakes:

wrong:
    changed graph produces another output
    -> optimized path is numerically inaccurate

also wrong:
    same graph produces another output
    -> backend differences are always expected

correct:
    declare semantic identity first
    -> choose exact, tolerance, invariant, or divergence gate
    -> record the observed result

The immutable reconciliation fixture is intentionally adversarial

The fixture is one local head, not a full projection stack:

Q:          [2,3]
K:          [5,3]
V:          [5,2]
visible:    [2,5]
finite_bias:[2,5]
keep_mask:  [2,5]
upstream G: [2,2]
output O:   [2,2]

It has five properties that a square random fixture usually misses:

  1. q_len=2 and k_len=5, so query and source axes cannot be conflated.
  2. Each query has a different legal set, so visibility is genuinely row-wise.
  3. Finite bias contains large values on illegal edges; masking must dominate preference without allowing those values back into the denominator.
  4. Source row four is globally blocked but has value [1000,-1000]; even a small leak becomes visible.
  5. Dropout probability is 0.25, but the boolean keep matrix is frozen. Every compared differentiable path therefore uses the same realized multiplier:

on legal edges and zero elsewhere.

Freezing the keep matrix is not an inference-time convention. It is a test contract. Resampling dropout between a plus/minus finite-difference evaluation would estimate stochastic noise instead of a derivative of one function.

Scalar and vectorized forwards meet at every named object

For this fixture, the vector path computes:

The scalar path independently loops over query rows and legal source indices:

for i in range(q_len):
    legal = [j for j in range(k_len) if visible[i, j]]
    logits = [scale * dot(Q[i], K[j]) + bias[i, j] for j in legal]
    numerators = exp(logits - max(logits))
    denominator = sum(numerators)
    for j, numerator in zip(legal, numerators):
        P[i, j] = numerator / denominator
        O[i] += P[i, j] * multiplier[i, j] * V[j]

It does not call the vector softmax helper. That independence matters: two wrappers around the same faulty helper are not two oracles.

The generated report obtains:

max |P_scalar - P_vector| = 2.78e-17
max |O_scalar - O_vector| = 5.55e-17

Those are float64 arithmetic differences, not a universal tolerance for fp16/bf16 kernels.

Backward parity starts from one scalar objective

Let the test loss be the Frobenius inner product with an explicit upstream matrix G:

This is more general than hard-coding O.square().mean(): G makes the vector-Jacobian product explicit and keeps the derivative trace small.

Because O=CV, the value and consumed-probability gradients are:

The frozen dropout multiplier is elementwise:

For one row, softmax has Jacobian

Multiplying that Jacobian by the incoming row gradient gives the implementation form used by Loop 6 and the reconciliation oracle:

Visibility is applied again to the gradient, not assumed from approximate underflow:

Finally:

The finite-bias derivative is worth retaining. Bias can be broadcast or parameterized by another module; the local score-shaped gradient must be reduced back over exactly those broadcast axes by its owner.

Central differences test the implemented vector-Jacobian product

For each scalar coordinate x_r of Q, K, V, and finite bias, the dependency- free oracle evaluates:

Central difference has truncation and floating-point error. It is therefore a tolerance test, not a bitwise oracle. The fixture uses float64, avoids softmax ties and visibility changes, and keeps the dropout realization fixed. Those conditions matter because finite differences do not define a useful gradient through a discrete top-k route, a changed boolean mask, a resampled dropout draw, or a branch boundary.

The measured maximum absolute errors are:

VariableManual versus central difference
Q6.21e-11
K1.10e-10
V7.42e-11
finite bias7.28e-11

The gate is fixed at 2e-9, before observing a production backend. PyTorch’s gradcheck mechanics similarly compare analytical and numerical derivatives, while warning that the checking contract depends on function, precision, and numerical assumptions. For an actual PyTorch implementation, torch.autograd.gradcheck complements this repository oracle; it does not replace the semantic mask and runtime tests.

Blocked sources need output and gradient witnesses

An illegal probability being printed as 0.0 is useful but incomplete. Loop 10 perturbs the globally blocked value by [10^6,-10^6] and checks:

output delta after blocked-value perturbation = 0
blocked key-gradient max absolute value       = 0
blocked value-gradient max absolute value     = 0
blocked score/bias gradients                  = 0

This catches four distinct failures:

  • a finite sentinel that leaks after a large later bias;
  • masking after softmax normalization;
  • a backward path that ignores visibility;
  • a gather/scatter map that assigns the blocked source another row’s gradient.

For a source blocked only from some queries, its total K/V gradient need not be zero because legal queries can still use it. A zero-gradient assertion must be scoped to an edge or to a globally blocked source, as it is here.

Algebraic invariants do not trust a second implementation

Row-shift invariance. For any row scalar c_i:

Adding [1.75,-2.25] row-wise to finite bias changes Loop 10’s output by only 1.67e-16. A wrong reduction axis generally breaks this property.

Paired source permutation. Let Pi permute source rows. Without an untransformed order-dependent term:

The fixture transforms K, V, visibility, finite bias, and dropout multiplier together. Its output delta is 2.78e-17. Permuting K alone is not an invariant; it changes which value receives each score.

RoPE, relative bias, causal coordinates, packed-example identity, and cache positions are order-dependent state. A permutation test must transform those objects too or explicitly run in a regime where they are absent. M3.3 owns those geometry transformations.

Runtime adapters can differ in representation and still require parity

Loop 8 established opposite boolean conventions:

semantic / functional SDPA boolean:
    True = legal

MultiheadAttention boolean:
    True = blocked

The reconciliation oracle lowers one semantic visibility matrix both ways:

sdpa_visible = visible
mha_blocked = logical_not(visible)
recovered_for_local_oracle = logical_not(mha_blocked)

The two adapted outputs match exactly. Passing the semantic boolean directly to the blocked-mask API instead produces an output delta above 1000, amplified by the adversarial illegal value. That is expected divergence proving the polarity test has power, not an acceptable backend tolerance.

Production SDPA parity needs an environment manifest in addition to semantic inputs:

framework/version
device and compute capability
input and accumulation dtype
requested / eligible / selected / observed backend
determinism settings
training/eval and dropout state
forward and backward tolerances

PyTorch’s version-scoped reproducibility documentation records different determinism characteristics for SDPA backends. Therefore “same output within tolerance” and “bitwise reproducible backward” are different gates.

Expected divergence proves that the oracle distinguishes contracts

A parity suite that only compares equivalent paths can pass even if every fixture is too weak. Loop 10 includes three negative controls:

Changed contractObserved max output deltaCorrect verdict
invert boolean polarity1000.35expected divergence
remove legal edges with a narrower interaction graph0.244expected divergence
use upper-left instead of cache-aligned rectangular causality0.457expected divergence

The threshold is only 1e-3; the purpose is not to rank the changes by quality. It proves that the fixture can observe the semantic difference.

By contrast, Loop 9’s pairwise and reassociated programs for the same fixed positive feature map differ by only 5.56e-17. That is parity. Replacing the feature map, approximating softmax with finite random features, or changing the legal edge graph would move the comparison into approximation or expected divergence.

The reconciliation registry prevents independent loops from drifting

Loop 10 imports each deterministic oracle from Loops 3 through 9, rebuilds its report, canonicalizes JSON keys and separators, and compares it with the frozen expected report. The registry covers:

Frozen oracleReplayed contract
Loop 3score families, scale placement, variance and temperature
Loop 4causal/padding/packed/cache visibility and all-masked policy
Loop 5stable softmax, support, temperature, precision edge cases
Loop 6fixed-dropout forward/backward and recomputation
Loop 7source-to-tensor scalar/vector/Harvard eager trace
Loop 8polarity, rectangular causality, GQA, dropout mode, dispatch
Loop 9graph reachability, block cover, kernel recurrence, retention

All seven observed canonical hashes equal their frozen hashes. This does not prove the articles are universally correct. It proves that Loop 10 has not quietly redefined an earlier executable contract while composing them.

Tolerances are inputs to the experiment

A production comparison should use:

But choosing atol and rtol after seeing a failure turns the gate into a description of the current error. Record them with the environment manifest, then investigate failures by stage:

  1. verify shape, coordinates, legal support, bias, scale, and dropout draw;
  2. compare eager intermediate scores/probabilities when available;
  3. force a math backend before comparing fused backends;
  4. separate forward from Q/K/V/bias gradient errors;
  5. repeat determinism checks separately from closeness checks;
  6. widen tolerance only with a numerical argument and a documented risk.

The test matrix still needs more than this one fixture:

CaseRequired variationBug class
bidirectional self-attentionsquare plus key paddingpadding polarity and broadcast
causal trainingsquare lower-triangular supportfuture leakage
rectangular cross-attentionindependent query/source lengthssquare-axis assumptions
cached single queryabsolute query position and long sourcelocal-index causality
cached query chunkq_len>1, k_len>q_lenupper-left/lower-right confusion
packed examplesblock-diagonal causal supportcross-example leakage
GQA/MQAH_q != H_kvhead map and grouped broadcast
train/evalnonzero configured dropoutfunctional dropout left active
fp32/fp16/bf16backend-qualified toleranceoverflow, promotion, reduction order
gradientsQ/K/V/bias and blocked edgeswrong saved tensor or backward map

M3.1 owns this semantic matrix. M4.3 owns the fused schedule and IO proof; M4.5 owns backend admission and observed execution; M3.9 owns packed batch construction; M3.10 owns request-local logical cache coordination; M5.1 owns persistent publication/lifecycle, and M5.3 owns physical cache mutation. Their tests should consume the same manifest fields rather than inventing new mask or coordinate conventions.

Loop 10 executable gate

The dependency-light evidence lives beside the article:

cd docs/numinens-evidence/m3-1-attention-score-path
python3 loop10_reconciliation_oracle.py
python3 loop10_contract_audit.py
python3 -m unittest -v \
  test_loop1_contract test_loop2_contract test_loop3_contract \
  test_loop4_contract test_loop5_contract test_loop6_contract \
  test_loop7_contract test_loop8_contract test_loop9_contract \
  test_loop10_contract

The gate requires:

scalar/vector forward errors below the frozen float64 threshold;
Q/K/V/bias manual gradients within the frozen finite-difference threshold;
illegal probabilities and blocked-source gradients exactly zero;
row-shift, paired-permutation, and SDPA/MHA-adapter invariants preserved;
negative controls visibly divergent;
Loops 3-9 reports equal their frozen canonical reports;
Loops 1-9 article-section hashes unchanged.

0x0D Failure lab

This section lists how attention implementations go wrong, from malformed inputs that should be rejected before softmax to errors that keep every shape correct (wrong scale, inverted mask polarity, softmax over the wrong axis), each with a test that exposes it. Its last part records how the article’s sources, figures and links were verified. Open it when you are debugging.

How this was checked Why the failures below are sorted by the stage at which each one can be detected. (about 100 words)

The final local question is not merely what can go wrong? It is:

At which boundary can each failure be rejected, and what evidence distinguishes a malformed call from a same-shape semantic error?

An attention implementation can satisfy every Python shape operation and still compute the wrong interaction graph, the wrong probability distribution, or the wrong runtime mode. Conversely, a fused backend can differ numerically from the eager reference while remaining semantically valid. A useful failure lab therefore needs more than a list of symptoms. It needs a detection stage, a witness, and an owner.

Edge cases and failure behaviour Thirty-two numbered failures, from malformed calls that must be rejected before softmax to shape-preserving errors (wrong scale, inverted polarity, wrong axis, dropout left on in evaluation), each with its evidence, and an order for triage. (about 1,240 words, 3 code listings)

Five detection stages prevent one vague attention failed verdict

StageWhat it can proveRepresentative evidenceWhat it cannot prove alone
manifest validationranks, named axes, source counts, head divisibility, dtype/device compatibility, mode stateimmutable call object plus rejection codethat same-shaped axes carry the intended meaning
support validationevery consumed query has legal sources and boolean/additive encodings agreelegal-source counts, blocked-mass assertionsthat the chosen legal graph is the intended graph
numerical oraclestable normalization, finite output, gradient parity, blocked-route zeroesscalar reference, finite differences, invariantsthat a production backend actually executed
semantic negative controlwrong polarity, wrong scale, unpaired permutations, or changed graphs diverge when they shouldnon-zero witness against the frozen fixtureperformance or kernel identity
runtime evidencerequested/eligible/selected/observed backend, train/eval dropout state, fallback reasonprofiler and runtime recordarchitectural intent that was never placed in the call manifest

The order matters. Reject impossible objects before allocating a large score tensor. Then test support. Then compare numbers. Only after the semantic contract is stable should a runtime claim be attached.

Construction-time rejections: malformed objects must not reach softmax

The following cases are deterministic manifest failures. No tolerance and no backend benchmark is needed.

IDFailureRequired rejection
M3A-F001Wrong transpose or incompatible query/key width. query @ key cannot form the intended query-key matrix, or a coincidental rank-four result assigns the wrong meaning to its axes.require the contracted feature axis of Q and K to equal d_head, and require score shape [B,H_q,q_len,k_len]
M3A-F002Assuming scores are square. Cross-attention and decode normally have q_len != k_len.validate query and source axes independently; never infer one from the other
M3A-F003Visibility or finite-bias shape drift. Broadcastability can hide a missing batch, head, query, or source dimension.validate the declared broadcast class and the resulting logical score shape
M3A-F004All keys masked for a consumed query row. Stable softmax has a zero denominator and no probability distribution to return.reject or route through an explicit unused-row policy before normalization
M3A-F005Bias or mask contains a non-finite value on a visible edge. NaN and unintended +inf contaminate the row.require finite visible logits; reserve -inf only for physically encoded exclusion
M3A-F006Bias, mask, and scores live on incompatible devices or use an unapproved dtype conversion.resolve device and accumulation-dtype policy before score construction
M3A-F007Value length differs from key length. p_attn has a k_len axis, so V needs the same source-row count as K.require K.shape[-2] == V.shape[-2]
M3A-F008GQA replication is not integral. The local mapping requires H_q % H_kv == 0.reject before any repeat/interleave operation

A compact validator should fail closed:

def validate_attention_manifest(call):
    require_rank(call.Q, 4)
    require_rank(call.K, 4)
    require_rank(call.V, 4)

    B, H_q, q_len, d_head = call.Q.shape
    B_k, H_kv, k_len, d_key = call.K.shape
    B_v, H_v, v_len, d_value = call.V.shape

    require(B == B_k == B_v, "BATCH_MISMATCH")
    require(d_head == d_key, "QUERY_KEY_WIDTH_MISMATCH")
    require(H_kv == H_v and k_len == v_len, "KEY_VALUE_SOURCE_MISMATCH")
    require(H_q % H_kv == 0, "INVALID_GQA_HEAD_DIVISIBILITY")

    score_shape = (B, H_q, q_len, k_len)
    require_broadcast_class(call.visible, score_shape, "VISIBILITY_SHAPE_MISMATCH")
    require_broadcast_class(call.finite_bias, score_shape, "BIAS_SHAPE_MISMATCH")
    require_every_used_row_has_source(call.visible, "ALL_MASKED_USED_ROW")
    require_finite_on_visible_edges(call.finite_bias, call.visible,
                                    "NONFINITE_VISIBLE_BIAS")
    require_mode_dropout(call.mode, call.dropout_p,
                         "FUNCTIONAL_DROPOUT_ACTIVE_IN_EVAL")

This validator does not prove that True means visible, that the causal coordinates are correct, or that the scale uses d_head. Those are same-shape semantic properties.

Same-shape semantic failures need discriminating witnesses

IDFailureWhy shape checks passWitness
M3A-F009Using d_model instead of d_head.both are scalar divisorscompare against the frozen d_head^{-1/2} oracle; probability and output differences must exceed the negative-control threshold
M3A-F010Missing the scale.score shape is unchangedincreasing d_head changes logit variance and softmax saturation; Loop 3 freezes the intended temperature
M3A-F011Mask applied after softmax.both operations return score-shaped arrayslegal rows no longer sum to one because blocked logits influenced the denominator
M3A-F012Mask polarity inverted.visible and blocked tensors have identical shape and dtypethe Loop 10 adversarial fixture produces a large output divergence
M3A-F013Finite bias used as a mask.a large negative finite number is a valid floating-point scoreblocked mass is small but non-zero; preference is not structural illegality
M3A-F014Bias added after finite masking.every operation is broadcast-valida sufficiently positive later bias can revive an illegal edge
M3A-F015Padding mask hides query rows instead of key columns.square fixtures can conceal the axis swapperturb a PAD value row; legal output must remain invariant when that source column is blocked
M3A-F016Causal triangle reversed or cache coordinates anchored incorrectly.upper- and lower-triangular masks have the same shapecompare absolute-position legality on a rectangular cached-decode fixture
M3A-F017Softmax over the query axis.dim=-2 returns the same tensor shapekey-axis row sums differ from one and query rows no longer choose source keys
M3A-F018Source permutation is not applied jointly to K, V, support, bias, and dropout realization.every independently permuted tensor still has a legal shapea paired permutation is invariant; an unpaired key permutation must diverge
M3A-F019Source padding support is reused as target causality.equal source and target lengths can make masks shape-compatiblecross-attention source columns must not inherit future-target semantics
M3A-F020A changed interaction graph is treated as backend parity.dense and sparse methods can share output shapeclassify it as expected divergence, not a failed exact-backend comparison

The crucial testing rule is:

same shape + same dtype + finite output
    does not imply
same attention semantics

A negative control is useful only when it would fail under the defect it is meant to detect. The wrong-polarity, wrong-scale, unpaired-permutation, and rectangular-causal fixtures are therefore frozen alongside the positive reference.

Softmax, dropout, and value-mixing failures cross phase boundaries

  1. Dropout applied to scores or values instead of probabilities. This is a different estimator, not an implementation detail. The Harvard source and the Transformer contract apply dropout to p_attn before value mixing.

  2. Functional SDPA dropout left active during evaluation. PyTorch functional SDPA applies the supplied dropout_p; the caller must pass zero outside training. A module’s .eval() flag does not silently rewrite an arbitrary functional argument.

  3. Post-dropout attention rows assumed to sum to one in every sample. Inverted dropout preserves each probability link in expectation. A realized row can sum below one, above one, or even to zero.

  4. Returned p_attn mistaken for contextual state. The state output is p_attn @ value; its final axis is d_value, not k_len.

  5. Output row count mistaken for source row count. Attention emits one contextual vector per query, so output length is q_len.

  6. Globally blocked source changes output or receives K/V gradient. Perturbing that value row must not change output, and its key/value gradients must be exactly zero in the frozen float64 fixture.

  7. A resampled dropout mask is used for finite differences or backend parity. The compared programs no longer have the same realized stochastic input. Freeze RNG identity or the realized multiplier.

Precision and runtime failures require environment evidence

  1. Low-precision softmax assumed backend-independent. Eager and fused paths may use different reduction trees and accumulation policies. Use dtype- and backend-specific tolerances after semantic invariants pass.

  2. A requested kernel is reported as the observed kernel. Request, eligibility, selection, and observation are separate facts. A fallback is not evidence that the preferred backend ran.

  3. Requesting weights silently changes the execution path. Some module paths materialize weights or leave an optimized route when need_weights=True. The returned API object is part of the runtime contract.

  4. Decode score is modeled as [S,S]. One-token decode normally scores [B,H_q,1,past+1]. M3.10 owns the request-local logical cache frontier; M5.1 owns persistent publication and M5.3 physical mutation; M3.1 owns the local call coordinates.

  5. Materialized score assumptions leak into semantics. Fused exact attention can preserve the logical score path without storing the full pair matrix in HBM. M4.3 owns that schedule and IO proof.

Failure triage follows the first violated invariant

1. freeze the call manifest and environment
2. reject rank, axis, source-count, head-map, device, and mode mismatches
3. inspect legal-source counts and all-masked rows
4. run scalar/vector forward parity in float64
5. run blocked-mass, row-sum, permutation, and row-shift invariants
6. freeze dropout, then compare backward and finite differences
7. run expected-divergence controls
8. compare runtime adapter outputs under declared tolerances
9. attach profiler evidence only after semantic parity passes
How this was checked How the article's failure fixtures, sources, figures and internal links were verified, and what that verification does not prove. (about 590 words, 2 code listings, 1 figure)

The executable Loop 11 fixture contains both rejection cases and same-shape defects. Each must produce its declared error code or numerical witness. Merely raising some exception does not pass the test.

M3.1 Loop 11 closure matrix connecting malformed calls and same-shape defects to manifest, support, numerical, semantic, and runtime evidence gates

The publication gate is a chain: an object rejected at an earlier boundary never earns a later numerical or runtime claim.

Complete source closure is an inventory, not a larger bibliography

The article’s references play different evidential roles:

ClassAllowed useForbidden shortcut
immutable local Transformer articlessource-local explanation, historical organization, and retained author figurestreating source-local prose as independent external verification
primary papersmechanism, derivation, algorithm contract, and source-reported results within the paper’s setupconverting a paper result into a universal runtime guarantee
official framework documentationcurrent API semantics, argument conventions, and documented backend behaviorassuming a moving documentation URL describes every installed version
project or author engineering sourceimplementation orientation and source lineageusing it as sole support for an unrelated theoretical or benchmark claim
contextual analysisinterpretation and boundary-settingpresenting interpretation as a paper claim

Loop 11 extracts every https:// URL from the article, joins it against the frozen Loop 1-10 source manifests plus a supplemental audit for older citations, and requires exact set equality. An uncatalogued article URL fails. A stale manifest URL not used by the article is allowed as upstream research history, but it does not count as a published citation. Network reachability is deliberately a separate dated audit; reproducible offline closure concerns identity, classification, and claim scope.

The two immutable local source owners remain:

  • A10, Transformer Series Self-Attention, for self-attention lineage and retained figures;
  • A11, Transformer Series Mask, for mask construction, mask figures, and source-local implementation history.

Their hashes are recorded. Loop 11 does not edit them.

Visual provenance closes every published asset route

The article mixes three legitimate visual classes:

  1. repository-authored diagrams and executable-fixture reconstructions;
  2. diagrams reused from another Transformer System owner with an explicit local lineage;
  3. retained figures from the author’s original Transformer articles.

The visual manifest extracts every Markdown image and every <picture> source/fallback, checks that each route resolves under public/, hashes the file, parses SVG XML, records its semantic job from the alt text, and checks responsive pair closure. It does not claim that a provenance record creates new rights. Source-local inherited figures retain their original lineage; newly authored diagrams state that no paper artwork was copied.

A visual fails closure when any of these is true:

route unresolved
empty semantic alt text
malformed SVG
mobile source without its desktop fallback
desktop/mobile paths present but only one is published
asset lacks provenance class, semantic job, or reuse decision

Every /posts/<slug> link is resolved to src/content/posts/<slug>.md. The manifest records the destination title and the reason for the handoff. That catches an old slug even when a development server happens to return a generic page.

The boundary is deliberate:

  • M3.2 owns head projection, layout, concatenation, and output projection;
  • M3.3-M3.5 own positional geometry and context extension;
  • M3.9 owns training batch, position, attention, and loss-mask alignment;
  • M3.10 owns request-local logical cache coordination, positions/frontiers, and one-request transitions;
  • M5.1 owns persistent request/cache schema, publication, and lifecycle;
  • M5.2 owns multi-request admission, scheduling, and preemption;
  • M5.3 owns physical cache allocation, reference ownership, copy-on-write, and mutation;
  • M4.1 owns whole-model arithmetic and memory ledgers;
  • M4.3 owns exact tiled schedules and IO complexity;
  • M4.5 owns backend eligibility, fallback, profiling, and observed execution.

Loop 11 may verify that those links exist. It may not absorb their mechanisms into M3.1.

Loop 11 executable gate

The frozen closure passes only when all of the following hold:

- every declared invalid fixture is rejected by its expected code or witness
- wrong-axis, wrong-polarity, wrong-scale, and unpaired-permutation controls diverge
- blocked-source and legal permutation invariants still hold
- every external URL is present in the complete source inventory
- A10 and A11 remain hash-identical
- every local post route resolves to a Markdown owner
- every published image route resolves and every SVG parses
- responsive image pairs are complete
- Loops 1-10 section hashes remain unchanged
- the Loop 11 section hash matches the frozen contract

This is publication closure, not production exhaustiveness. It proves that the article’s declared examples, failures, sources, visuals, and handoffs agree with one another. It does not prove every accelerator, framework version, or model integration correct.


0x0E Handoff to M3.2 and M4.1

M3.1 ends with a function that assumes Q/K/V are already available:

out, p_attn = attention(query, key, value, mask=mask, dropout=dropout)

M3.2 opens the caller:

X: [B,S,d_model]
  -> W_Q, W_K, W_V projections
  -> [B,S,d_model]
  -> view [B,S,h,d_head]
  -> transpose [B,h,S,d_head]
  -> attention(...)
  -> transpose / contiguous / view
  -> output projection W_O

So M3.1 owns:

scores -> mask/bias -> softmax -> p_attn -> weighted values

M3.2 owns:

projection -> head layout -> attention call -> concat -> output projection

That split keeps the block stack readable. First understand how one attention call works. Then understand how the Transformer packs many heads around it.

M4.1 receives the same objects as a ledger:

Q:        [B, H_q, S_q, d_h]
K:        [B, H_kv, S_k, d_h]
V:        [B, H_kv, S_k, d_h]
scores:   [B, H_q, S_q, S_k]
p_attn:   [B, H_q, S_q, S_k]
out_head: [B, H_q, S_q, d_h]

M3.1 defines the tensors and derives their local element and arithmetic counts. M4.1 integrates those counts across layers and execution phases, then asks which objects are materialized, how many bytes reach HBM, and whether the schedule is compute- or bandwidth-bound.


0xFF References