Attention (spec layer) #
This file defines the standard scaled dot-product attention primitive and a simple multi-head wrapper.
Attention(Q,K,V) = softmax(Q Kᵀ / √d) V
TorchLean goal here is to mirror the math you see in deep learning libraries (especially PyTorch),
but keep everything as pure functions on TorchLean.Tensor so the same definitions can be reused
for:
- proofs (e.g. reasoning about shapes and gradients),
- reference implementations (runtime extraction),
- verification backends (e.g. interval semantics).
Shapes and conventions #
We model the "single batch element" case. Batched attention is obtained by prepending [B]
and mapping over it.
Core shapes:
In many transformer blocks dV = d, and this file uses that common choice for simplicity.
The optional Boolean mask has shape (nQ × nK). Blocked entries receive zero numerator before
row normalization. Over real scalars with a nonzero denominator, their attention weight is zero.
This is the finite-scalar encoding of the PyTorch pattern
scores.masked_fill(~mask, -torch.inf).
Rows with no allowed entries return zero weights directly, avoiding division by zero. This
guarantees zero weights even for exceptional scores; the final multiplication by V still obeys
the scalar backend's arithmetic. For IEEE scalars, 0 * ∞ can produce NaN. On a partially allowed
row, exceptional allowed scores can also make the denominator and blocked weights NaN.
PyTorch analogy:
scaledDotProductAttentioncorresponds totorch.nn.functional.scaled_dot_product_attention(no dropout), with Boolean masks interpreted as true-∞masks.MultiHeadAttention.forwardcorresponds to the core computation insidenn.MultiheadAttention/ transformer blocks, ignoring biases and dropout.
Scaled Dot-Product Attention #
We separate out the single-head primitive (scaledDotProductAttention) because:
- it is the core mathematical object, reused in multi-head attention,
- it is a good target for proofs and for "spec vs runtime" comparisons.
Boolean masks #
TorchLean uses the same boolean mask convention as PyTorch SDPA:
truemeans a key/value position is allowed to be attended to,falsemeans it is blocked (its softmax numerator is exactly zero).
If an entire row is false, every output weight in that row is zero.
PyTorch reference: torch.nn.functional.scaled_dot_product_attention uses the same convention for
boolean attn_mask entries: True entries are included, and False entries are blocked.
A (nQ × nK) mask where every position is allowed (true).
Instances For
A (nQ × nK) mask where every position is blocked (false).
Instances For
Causal (lower-triangular) self-attention mask of shape (n, n).
mask[i,j] = true iff j ≤ i, i.e. each query position can attend to itself and past positions.
Instances For
Future-only (upper-triangular) self-attention mask of shape (n, n).
This is the (strict) complement of causalMask: mask[i,j] = true iff i < j.
Instances For
Bundled inputs and mask needed for scaled dot-product attention.
- Q : TorchLean.Tensor α [nQ, dModel]
- K : TorchLean.Tensor α [nK, dModel]
- V : TorchLean.Tensor α [nK, dModel]
Instances For
Denominator used by scaled dot-product attention.
Standard attention requires a positive feature dimension and divides scores by sqrt(dModel).
TorchLean's tensor shapes also admit zero dimensions. In that degenerate case the result has no
feature coordinates, so choosing denominator 1 gives the unique empty-feature result without
introducing a division by zero. The scalar context constructs the root from the count: binary
backends can recover a finite root even when casting the dimension itself would overflow.
Instances For
Exact hard masking #
TorchLean encodes the usual "true -∞ before softmax" behavior without requiring the
tensor scalar
type itself to contain infinities. Instead of replacing blocked logits by a finite sentinel, we form
stable softmax numerators directly. If rowMax is the greatest allowed score in the row, then
numerator_j = if mask_j then exp(score_j - rowMax) else 0.
This models the zero numerator contributed by exp(-∞). The zero-mass conclusion used in causal
proofs requires the scalar division laws and a nonzero denominator. A row with no allowed positions
returns zeros directly.
Maximum allowed score in one hard-masked row, or none when every entry is blocked.
Instances For
Hard-masked softmax on one vector.
mask[j] = false makes the j-th numerator exactly zero before normalization. This is the
ordinary finite-scalar encoding of softmax with true -∞ masked logits.
The maximum and denominator are computed only over allowed entries. Subtracting the allowed-row
maximum gives the usual numerically stable softmax formula. If every mask entry is false, the result
is the zero vector, matching PyTorch SDPA and the LibTorch CUDA attention bridge
(NN/Backend/LibTorch.lean).
Instances For
Hard-masked softmax along the innermost axis of an arbitrary tensor.
Instances For
VJP/JVP helper for a softmax-like row-normalization when the forward weights are already known.
For ordinary softmax, weights = softmax(scores). For hard-masked softmax over real scalars with
weights = 0 at blocked entries, the same formula gives zero gradient through blocked logits.
Exceptional floating-point operands need not satisfy that zero-product conclusion:
dScores = weights ⊙ (dWeights - Σⱼ dWeightsⱼ * weightsⱼ).
Attention uses center := true: subtract the upstream coordinate at a maximal weight before
taking the weighted sum. For normalized real weights this leaves the derivative unchanged; for
an all-zero row both formulas return zero. In floating point it retains a small dominant-logit
gradient after its probability rounds to one. The default retains the uncentered algebraic
formula for arbitrary supplied weights, whose sum need not be one.
Instances For
Reconstruct a complete finite scaled contraction without rounding its coefficients.
Instances For
Round a finite contraction only after reconstructing every original coefficient.
Instances For
Scaled sums of matrix products used by attention's forward and differential rules.
One product serves the forward or VJP; two products retain the JVP's joint contraction. Within
and between products, checked arithmetic follows the original left-fold schedule. scaleFirst
keeps the VJP's input-scaling convention. Range loss or an absorbed nonzero term replays all
original finite coefficient trees, including the scale, before rounding. Missing finite input
encodings retain the ordinary result. Unsupported carriers use the original expression directly.
This is a reference scalar repair, not a numerical contract for an expanded IR or foreign kernel.
Instances For
Scaled contractions over a shared batch prefix, without materializing an unscaled product.
Instances For
A scaled matrix product with broadcast batch prefixes and one retained contraction boundary.
Instances For
The two product-rule terms share one replay boundary, so large opposite tangents can cancel.
Instances For
Cotangents of a scaled, broadcast matrix product, retaining its fixed scale in each product.
Instances For
Replay one softmax row from retained finite cotangent coefficients.
Instances For
Replay one softmax row from its original gradient contractions and saved weights.
Instances For
Multiply a retained contraction by one coordinate's factor before encoding its result.
Instances For
Retain both pointwise factors through the transposed value-gradient contraction.
Instances For
Conservative range guard for an ordinary product whose coefficients must survive another map. Only flags escape this check; the caller keeps the original value-producing schedule.
Instances For
Retain the original gradient contractions through the centered softmax pullback.
The ordinary tensor calculation is unchanged unless an output coefficient loses range. Replaying the contraction and pullback together can recover a finite result even when the intervening gradient tensor cannot represent it. This uses the same coefficient replay as ordinary softmax.
Instances For
Row normalization followed by a differentiable multiplier and a value contraction. The forward schedule is deliberately the ordinary softmax, pointwise product, then matrix product. The compound boundary matters for derivatives, where the unweighted cotangent can overflow.
Instances For
Mix value rows from saved weights, keeping pointwise multiplication before the contraction.
Instances For
Forward value of the normalized matrix product, without changing its association.
Instances For
Cotangents of normalized scores, values and their differentiable multiplier. Finite coefficient replay keeps the original output cotangent and values through both the multiplier and centered softmax. Unsupported scalar carriers use the ordinary local formula.
Instances For
Product-rule tangent of the normalized matrix product, using its saved row weights.
Instances For
Scaled dot-product attention (forward).
Given:
we compute:
- scores
S = Q Kᵀwith shape(nQ × nK) - scaled scores
S' = S / √d - (optional) mask: for each
(i,j), ifmask[i,j] = false, its softmax numerator is exactly zero (the finite-scalar encoding of true-∞masking) - attention weights
Aby row normalization over the last axis - output
Out = A Vwith shape(nQ × d)
Mask convention:
mask[i,j] = true means "this key position is allowed", and false means "mask it out".
Over real scalars, each unmasked attention row sums to 1; a masked row with at least one allowed
key has the same normalization. These equalities are not promised for arbitrary numeric contexts.
A fully blocked row returns all-zero weights directly, avoiding a 0/0 result.
The ordinary matrix product and scaling order is retained. Finite-coefficient backends replay the complete scaled contraction after range loss or an absorbed nonzero term. This prevents an unscaled score overflow from poisoning otherwise representable scaled scores, without pre-scaling tiny queries to zero. Scalar conversion and native execution remain separate trust obligations.
PyTorch analogy: torch.softmax(scores.masked_fill(~mask, -torch.inf), dim=-1) row-wise, then a
final matrix multiply by V.
Instances For
Backward/VJP for scaled dot-product attention.
Returns (dQ, dK, dV) given an upstream gradient dOut.
We recompute the forward intermediates locally so this spec stays self-contained and does not rely on a global tape.
Over real scalars, masked calls use the VJP for hard masking: blocked logits have zero forward
weight and therefore zero gradient. Finite-coefficient replay retains dOut and V through the
centered pullback: their matrix product can overflow even when the score cotangent is finite.
Nonfinite original operands retain the ordinary backend arithmetic.
Instances For
Forward-mode JVP for scaled dot-product attention.
This differentiates the pure attention equation
Out = softmax(mask(Q Kᵀ / sqrt(d))) V
in the direction (dQ,dK,dV). Over real scalars, hard-masked blocked logits have zero
forward weight,
so their tangent contribution is zero in softmaxBackwardFromWeightsSpec. Upstream centering
preserves that zero-row convention. The row-wise softmax Jacobian is symmetric, so the same
centered formula serves as both VJP and JVP once the forward weights are known.
When a tangent contraction overflows before normalization, finite-coefficient replay carries both original product terms through the pullback. This also preserves zero tangents for a singleton softmax without rounding an unrepresentable intermediate tangent to infinity first.
Instances For
Multi-head attention parameters (projection matrices).
PyTorch analogy: this corresponds to the four linear maps used in attention blocks:
Wq,Wk,WvprojectdModel -> (numHeads * headDim)Woprojects(numHeads * headDim) -> dModel
This spec keeps them as explicit matrices, without bias terms, so the parameterization remains visible in statements about the forward and derivative maps.
- queryWeight : TorchLean.Tensor α [dModel, numHeads * headDim]
Query projection from
dModelto all attention heads. - keyWeight : TorchLean.Tensor α [dModel, numHeads * headDim]
Key projection from
dModelto all attention heads. - valueWeight : TorchLean.Tensor α [dModel, numHeads * headDim]
Value projection from
dModelto all attention heads. - outputWeight : TorchLean.Tensor α [numHeads * headDim, dModel]
Projection from the concatenated heads back to
dModel.
Instances For
Split (n, dModel) into (numHeads, n, headDim).
We store heads as the outermost axis so that "per-head computation" is just a Tensor.dim over
Fin numHeads.
The feature coordinate is interpreted as (head, coordinate-within-head): first reshape to
(n, numHeads, headDim), then swap the token and head axes. Reshaping directly to
(numHeads, n, headDim) would preserve the wrong row-major coordinate order.
Instances For
Combine a tensor-of-heads back into a single (n, numHeads*headDim) tensor.
Implementation detail:
Tensor.swapAdjacentAxesexchanges the head and token axes.reshapeSpecflattens the final two axes into(n, numHeads * headDim).
Instances For
Multi-head attention forward pass (self-attention when mask is square).
High-level structure:
- project
xintoQ,K,V - split the projection dimension into heads
- run scaled dot-product attention per head (sharing the same mask)
- combine heads back and project with
Wo
Instances For
Parameter gradients for multi-head attention: one per projection matrix.
The record lives beside the backward pass that produces it. All four fields are named after the
corresponding field of MultiHeadAttention, so { queryWeight, keyWeight, valueWeight, outputWeight } works with the anonymous constructor at every call site.
PyTorch analogue: the .grad of nn.MultiheadAttention.in_proj_weight split into its three blocks,
plus out_proj.weight.grad.
- queryWeight : TorchLean.Tensor α [dModel, numHeads * headDim]
Gradient of the query projection matrix.
- keyWeight : TorchLean.Tensor α [dModel, numHeads * headDim]
Gradient of the key projection matrix.
- valueWeight : TorchLean.Tensor α [dModel, numHeads * headDim]
Gradient of the value projection matrix.
- outputWeight : TorchLean.Tensor α [numHeads * headDim, dModel]
Gradient of the output projection matrix.
Instances For
Everything multiHeadAttentionBackward sends backwards: the four parameter gradients and the
gradient with respect to the attended sequence.
- parameters : MultiHeadAttentionParameterGradients numHeads dModel headDim α
Gradients for the four projection matrices.
- input : TorchLean.Tensor α [n, dModel]
Gradient with respect to the input sequence
x.
Instances For
Multi-head attention backward pass.
Returns gradients for input x and all projection matrices (Wq,Wk,Wv,Wo).
The forward intermediates are recomputed locally instead of relying on a global tape.
Instances For
Forward-mode JVP for multi-head attention.
The rule follows the same computational graph as MultiHeadAttention.forward:
- project tangents through
Q/K/V, - split primal and tangent projections into heads,
- apply
scaledDotProductAttentionJvphead-wise, - combine head tangents, then differentiate the final output projection.
Attention forward-mode AD is explicit at the specification layer rather than hidden behind a runtime-only implementation.
Instances For
Self-attention on a single sequence.
This uses the same input x for Q/K/V, runs scaled dot-product attention, then applies the output
projection Wo.
PyTorch analogue: the core of nn.MultiheadAttention / TransformerEncoderLayer (ignoring the
batch axis).