Qu

scaled_dot_product_attention

Builtin index · Statistics & ML

result = scaled_dot_product_attention(q, k, v, [mask=])

Input: q, k, v, mask · Output: result

What it is

Core attention computation: an (Nq, D) query matrix q, an (Nk, D) key matrix k, and an (Nk, Dv) value matrix v (k and v share the same row count Nk). Scores are q * kᵀ scaled by 1/sqrt(D) before the softmax, so the softmax does not saturate as D grows; an optional mask matrix (same shape as the score matrix, (Nq, Nk)) adds large negative values to positions that should not be attended to before the softmax.

Overloads

One call shape — this function is not overloaded.

CodeInputOutputDescription
result = scaled_dot_product_attention(q, k, v, [mask=])
q matrixCore attention computation: an (Nq, D) query matrix q, an (Nk, D) key matrix k, and an (Nk, Dv) value matrix v (k and v share the same row count Nk).
k matrixCore attention computation: an (Nq, D) query matrix q, an (Nk, D) key matrix k, and an (Nk, Dv) value matrix v (k and v share the same row count Nk).
v matrixCore attention computation: an (Nq, D) query matrix q, an (Nk, D) key matrix k, and an (Nk, Dv) value matrix v (k and v share the same row count Nk).
mask matrix, optionalan optional mask matrix (same shape as the score matrix, (Nq, Nk)) adds large negative values to positions that should not be attended to before the softmax.
result matrix
Returns an (Nq, Dv) matrix.

Examples

From the documentation, and run by the build -- the output and the figure below are what the engine actually produced. Simplest first.

x = [1.0, 2.0, 3.0; 10.0, 20.0, 30.0]
normed = layer_norm(x, [1.0, 1.0, 1.0], [0.0, 0.0, 0.0])
print("layer_norm (scale-invariant per row): {normed}")

q = [1.0, 0.5; -0.5, 1.0; 0.2, -0.3]
k = [1.0, 0.0; 0.0, 1.0; 0.5, 0.5]
v = [1.0, 2.0; 3.0, 4.0; 5.0, 6.0]
attn = scaled_dot_product_attention(q, k, v)
print("attention output: {round(attn, 3)}")

eye2 = [1.0, 0.0; 0.0, 1.0]
head = {wq = eye2, wk = eye2, wv = eye2, wo = eye2}
mh = multi_head_attention(q, (head,), 1)
plain = scaled_dot_product_attention(q, q, q)
print("multi_head_attention with 1 identity head matches plain self-attention: {sum(abs(mh - plain)) < 1e-9}")

weights = {heads = (head,), ln1_gamma = [1.0, 1.0], ln1_beta = [0.0, 0.0],
           w1 = [0.2, -0.1; 0.1, 0.3], b1 = [0.0, 0.0],
           w2 = [0.1, -0.2; 0.3, 0.1], b2 = [0.0, 0.0],
           ln2_gamma = [1.0, 1.0], ln2_beta = [0.0, 0.0]}
block_out = transformer_block(q, weights)
print("transformer_block: {round(block_out, 3)}")
output
layer_norm (scale-invariant per row): [-1.224736, 0, 1.224736; -1.224745, 0, 1.224745]
attention output: [2.872, 3.872; 3.25, 4.25; 2.872, 3.872]
multi_head_attention with 1 identity head matches plain self-attention: true
transformer_block: [1, -1; -1, 1; 1, -1]

from statistics-ml

How it works

scaled_dot_product_attention(q, k, v, [mask]) — softmax(Q K^T / sqrt(d_k)) V (Vaswani et al. 2017, eq. 1). q/k/v are (seq_len_q, d_k)/(seq_len_k, d_k)/ (seq_len_k, d_v) matrices (or Tensors wrapping them); an optional 0/1 mask of the raw scores' own (seq_len_q, seq_len_k) shape (1 = attend, 0 = block) is added as a large negative bias BEFORE the softmax — the standard "additive mask" convention, not zeroing probabilities after the fact (which would leave blocked entries still skewing the row's normalization). K^T uses tt (this session's new tracked transpose, so gradient flows back into k too); the two matmuls (Q@K^T, probs@V) reuse tb's "*", and so Matrix::matmul's own existing rayon parallelism above its size threshold — the actual CPU-parallel win for this op. True head-level parallelism (see multi_head_attention) can't extend down into this call too: every tracked op here mutates self.tape behind one shared &mut self.

From the note above this builtin in the interpreter, where the reasoning was written down as it was built.