Module 5. Modern Attention Mechanisms
Read the explanations and code in the existing notebooks, or download the files. Code does not run on this site.
Download course files (.zip) 2588.1 KBThe archive preserves the source folder structure, both languages, and supporting files.
Notebook 5. Attention by hand: masks, GQA, online softmax, and KV arithmetic
Main module notebook
Download .ipynb module_05_notebook_attention.ipynb · 58.5 KB
Show notebook text
Notebook 5. Attention by hand: masks, GQA, online softmax, and KV arithmetic
Module 5. Modern Attention Mechanisms — hands-on notebook
This notebook makes the attention calculation visible, step by step. The priorities are transparent tensor shapes, explicit invariants, and correct edge-case behavior. Real FlashAttention kernels additionally solve GPU tiling, vectorization, mixed precision, fused backward, and dispatch across mask layouts.
Colab contract: Runtime → Run all completes the mandatory path on CPU in a clean
Google Colab session. The notebook reads no adjacent files, requires no manual uploads,
and uses no API keys. The small frozen Module-5 reference payload is embedded with SHA-256.
Route:
-
a robust reference SDPA and an explicit fully-masked-row policy;
-
causal, sliding-window, and sink masks;
-
the exact GQA invariant and a negative wrong-duplication test;
-
online softmax and streaming tiled attention;
-
a KV calculator with unambiguous KiB/MiB/GiB units;
-
a learned sink as an extra logit with no value;
-
a deterministic comparison of direct and streaming arrays;
-
cached-prefix masks and MLA projection absorption.
# Install only missing dependencies; ordinary Colab already has NumPy.
import importlib.util
import subprocess
import sys
missing = [name for name in ("numpy",) if importlib.util.find_spec(name) is None]
if missing:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", *missing])
print('Environment ready.')0. Embedded Module-5 numerical contract
The reference numbers belong to the module rather than mutable lecture prose. The JSON payload below is embedded in the notebook and verified by SHA-256, so future lecture edits do not alter the computational route.
import gc
import hashlib
import json
import math
import tracemalloc
from typing import Literal
import numpy as np
_REFERENCE_JSON = '{\n "schema_version": 2,\n "context_tokens": 131072,\n "kv_rows": {\n "gpt_oss_gqa_all_full_equivalent": 73728,\n "gpt_oss_mha_hypothetical": 589824,\n "gpt_oss_mqa_hypothetical": 9216\n },\n "gpt_oss_hybrid": {\n "layers": 36,\n "full_layers": 18,\n "sliding_layers": 18,\n "window": 128,\n "per_layer_bytes_per_token": 2048,\n "full_bytes": 4831838208,\n "sliding_bytes": 4718592,\n "hybrid_total_bytes": 4836556800,\n "all_full_bytes": 9663676416\n },\n "online_softmax_trace": {\n "l1": 1.3678794411714423,\n "n1": 1.7357588823428847,\n "alpha": 0.36787944117144233,\n "l2": 1.5852997230319539,\n "n2": 3.966890002140263,\n "output": 2.502296533903012\n },\n "mla": {\n "bytes_per_token": 70272,\n "mha_equivalent_bytes_per_token": 3997696,\n "raw_element_ratio": 56.888888888888886\n },\n "score_matrix": {\n "n_4096_fp32_bytes": 67108864,\n "n_8192_fp32_bytes": 268435456,\n "tile_128_fp32_bytes": 65536\n }\n}\n'
_REFERENCE_SHA256 = '720d34f7da83628df36153a92218644fa6e643db4a345ba5b7b41d9c0206fc34'
assert hashlib.sha256(_REFERENCE_JSON.encode("utf-8")).hexdigest() == _REFERENCE_SHA256
REFERENCE = json.loads(_REFERENCE_JSON)
_CONFIG_JSON = '{"num_hidden_layers": 36, "num_experts": 128, "experts_per_token": 4, "vocab_size": 201088, "hidden_size": 2880, "intermediate_size": 2880, "swiglu_limit": 7.0, "head_dim": 64, "num_attention_heads": 64, "num_key_value_heads": 8, "sliding_window": 128, "initial_context_length": 4096, "rope_theta": 150000, "rope_scaling_factor": 32.0, "rope_ntk_alpha": 1, "rope_ntk_beta": 32}'
assert hashlib.sha256(_CONFIG_JSON.encode('utf-8')).hexdigest() == '063aaa98e012cac330d58d18471700f0b0eb90bb15c58fb227809d45090f3e65'
CONFIG = json.loads(_CONFIG_JSON)
assert (CONFIG['num_hidden_layers'], CONFIG['num_attention_heads'],
CONFIG['num_key_value_heads'], CONFIG['head_dim'], CONFIG['sliding_window']) == (36, 64, 8, 64, 128)
rng = np.random.default_rng(4)
np.set_printoptions(precision=5, suppress=True)
print('Module-5 reference metrics unpacked and verified', "· NumPy", np.__version__)1. Reference SDPA, masks, and empty rows
Shape contract:
Q:[..., T_q, d_k];K:[..., T_k, d_k];V:[..., T_k, d_v];- additive mask: broadcastable to
[..., T_q, T_k], with a finite bias for allowed positions and-inffor forbidden positions.
A row made entirely of -inf does not define a softmax distribution. The default policy
raises an error, which exposes a broken mask early. Padding rows may explicitly request
empty_row="zero"; their weights and output are then zero. A finite sink logit also gives
such a row support: if only the zero-valued sink is available, the output is zero.
EmptyRowPolicy = Literal["error", "zero"]
def make_mask(n: int, window: int | None = None, n_sink: int = 0) -> np.ndarray:
"""Return an additive causal mask of shape [n, n].
Allowed entries are 0 and forbidden entries are -inf. `window` keeps only the
latest `window` causal keys; `n_sink` preserves initial real positions as StreamingLLM-style anchors.
"""
if isinstance(n, bool) or not isinstance(n, int) or n <= 0:
raise ValueError("n must be positive")
if window is not None and (isinstance(window, bool) or not isinstance(window, int) or window <= 0):
raise ValueError("window must be positive")
if isinstance(n_sink, bool) or not isinstance(n_sink, int) or not 0 <= n_sink <= n:
raise ValueError("n_sink must satisfy 0 <= n_sink <= n")
if n_sink and window is None:
raise ValueError("n_sink is meaningful only together with a finite window")
query_pos = np.arange(n)[:, None]
key_pos = np.arange(n)[None, :]
allowed = key_pos <= query_pos
if window is not None:
allowed &= (query_pos - key_pos) < window
allowed |= (key_pos < n_sink) & (key_pos <= query_pos)
return np.where(allowed, 0.0, -np.inf)
def sdpa(
Q: np.ndarray,
K: np.ndarray,
V: np.ndarray,
mask: np.ndarray | None = None,
*,
sink: float | np.ndarray | None = None,
empty_row: EmptyRowPolicy = "error",
return_weights: bool = False,
) -> np.ndarray | tuple[np.ndarray, np.ndarray]:
"""Compute transparent scaled dot-product attention.
Shapes:
Q [..., T_q, d_k], K [..., T_k, d_k], V [..., T_k, d_v].
`mask` must broadcast to [..., T_q, T_k]. A finite entry is an additive
bias; -inf forbids the key. A per-head sink should be reshaped so it
broadcasts to [..., T_q, 1].
`empty_row="error"` rejects a row with no key and no sink. `"zero"` returns
zero weights/output for that row, which is useful for explicit padding rows.
"""
if empty_row not in {"error", "zero"}:
raise ValueError("empty_row must be 'error' or 'zero'")
Q, K, V = (np.asarray(x) for x in (Q, K, V))
if min(Q.ndim, K.ndim, V.ndim) < 2 or any(0 in x.shape for x in (Q, K, V)):
raise ValueError("Q, K, and V must each have at least two dimensions")
if Q.shape[-1] != K.shape[-1]:
raise ValueError("Q and K must have the same d_k")
if K.shape[-2] != V.shape[-2]:
raise ValueError("K and V must have the same T_k")
try:
np.broadcast_shapes(Q.shape[:-2], K.shape[:-2], V.shape[:-2])
except ValueError as exc:
raise ValueError("Q/K/V batch dimensions are not broadcast-compatible") from exc
if not all(np.isfinite(x).all() for x in (Q, K, V)):
raise ValueError("Q, K, and V must contain only finite values")
# Keep float32/float64 inputs in their precision; integer examples become float64.
dtype = np.result_type(Q.dtype, K.dtype, V.dtype, np.float32)
Q, K, V = (x.astype(dtype, copy=False) for x in (Q, K, V))
d_k = Q.shape[-1]
scale = np.asarray(1.0 / math.sqrt(d_k), dtype=dtype)
with np.errstate(over="ignore", invalid="ignore"):
scores = (Q @ np.swapaxes(K, -1, -2)) * scale
if not np.isfinite(scores).all():
raise FloatingPointError("Q @ K.T overflowed; rescale the inputs")
if mask is not None:
mask = np.asarray(mask, dtype=dtype)
if np.isnan(mask).any() or np.isposinf(mask).any():
raise ValueError("mask may contain finite biases and -inf, but not NaN/+inf")
try:
mask = np.broadcast_to(mask, scores.shape)
except ValueError as exc:
raise ValueError(f"mask shape {mask.shape} does not broadcast to {scores.shape}") from exc
finite_bias = np.isfinite(mask)
with np.errstate(over="ignore", invalid="ignore"):
scores = scores + mask
if np.any(finite_bias & ~np.isfinite(scores)):
raise FloatingPointError(
"adding the finite mask bias overflowed; reduce the bias magnitude"
)
has_token = np.isfinite(scores).any(axis=-1, keepdims=True)
token_max = np.max(scores, axis=-1, keepdims=True)
sink_logits = None
if sink is not None:
sink_logits = np.asarray(sink, dtype=dtype)
if not np.isfinite(sink_logits).all():
raise ValueError("sink logits must be finite")
try:
sink_logits = np.broadcast_to(sink_logits, token_max.shape)
except ValueError as exc:
raise ValueError(
f"sink shape {sink_logits.shape} does not broadcast to {token_max.shape}"
) from exc
row_max = np.maximum(token_max, sink_logits)
else:
if empty_row == "error" and not has_token.all():
bad = np.argwhere(~has_token.squeeze(-1))
raise ValueError('Attention contains fully masked rows' + f": {bad[:8].tolist()}")
# A zero-policy row gets a harmless max of 0; all its exponentials stay 0.
row_max = np.where(has_token, token_max, np.zeros_like(token_max))
token_exp = np.where(
np.isfinite(scores), np.exp(scores - row_max), np.zeros_like(scores)
)
denominator = token_exp.sum(axis=-1, keepdims=True)
if sink_logits is not None:
denominator += np.exp(sink_logits - row_max)
weights = np.divide(
token_exp,
denominator,
out=np.zeros_like(token_exp),
where=denominator > 0,
)
output = weights @ V
return (output, weights) if return_weights else output
n, d = 24, 16
M_causal = make_mask(n)
M_window = make_mask(n, window=8)
M_sink = make_mask(n, window=8, n_sink=2)
assert (M_causal == 0).sum() == n * (n + 1) // 2
assert np.isneginf(M_window[23, 10]) and M_window[23, 16] == 0
assert M_sink[23, 0] == 0 and M_sink[23, 1] == 0 and np.isneginf(M_sink[23, 2])
Q = rng.standard_normal((n, d))
K = rng.standard_normal((n, d))
V = rng.standard_normal((n, d))
Z, A = sdpa(Q, K, V, M_causal, return_weights=True)
assert Z.shape == (n, d) and A.shape == (n, n)
assert np.isfinite(Z).all() and np.allclose(A.sum(-1), 1.0)
# Deliberately remove all support from one query row.
M_empty = M_causal.copy()
M_empty[5, :] = -np.inf
try:
sdpa(Q, K, V, M_empty)
raise AssertionError("the default policy must reject a fully masked row")
except ValueError as exc:
assert str(exc)
Z_zero, A_zero = sdpa(Q, K, V, M_empty, empty_row="zero", return_weights=True)
assert np.all(A_zero[5] == 0) and np.all(Z_zero[5] == 0)
assert np.allclose(A_zero[np.arange(n) != 5].sum(-1), 1.0)
print('SDPA shapes, three masks, and the empty-row policy verified')2. The GQA invariant
GQA with H query heads and H_kv KV heads is exactly MHA in which every KV head is
repeated within its group H / H_kv times. We verify that equality and reproduce a common
bug separately: tile creates a different head order from repeat.
def gqa(
Q: np.ndarray,
K: np.ndarray,
V: np.ndarray,
n_heads: int,
n_kv_heads: int,
mask: np.ndarray | None = None,
) -> np.ndarray:
"""Educational grouped-query attention for 2D projected sequences.
Q [T, H*d_k], K [T, H_kv*d_k], V [T, H_kv*d_v].
The output has shape [T, H*d_v].
"""
Q, K, V = (np.asarray(x) for x in (Q, K, V))
if any(x.ndim != 2 or 0 in x.shape for x in (Q, K, V)):
raise ValueError("GQA expects 2D projected sequences")
if not (Q.shape[0] == K.shape[0] == V.shape[0]):
raise ValueError("Q, K, and V must have the same sequence length")
if any(isinstance(h, bool) or not isinstance(h, int) or h <= 0 for h in (n_heads, n_kv_heads)) or n_heads % n_kv_heads:
raise ValueError("n_heads must be a positive multiple of n_kv_heads")
if Q.shape[1] % n_heads:
raise ValueError("Q width must be divisible by n_heads")
if K.shape[1] % n_kv_heads or V.shape[1] % n_kv_heads:
raise ValueError("K/V widths must be divisible by n_kv_heads")
T = Q.shape[0]
d_k = Q.shape[1] // n_heads
if K.shape[1] // n_kv_heads != d_k:
raise ValueError("Q and K head dimensions must match")
d_v = V.shape[1] // n_kv_heads
q = Q.reshape(T, n_heads, d_k).transpose(1, 0, 2)
k = K.reshape(T, n_kv_heads, d_k).transpose(1, 0, 2)
v = V.reshape(T, n_kv_heads, d_v).transpose(1, 0, 2)
group_size = n_heads // n_kv_heads
# repeat preserves groups [kv0, kv0, ..., kv1, kv1, ...].
k = np.repeat(k, group_size, axis=0)
v = np.repeat(v, group_size, axis=0)
z = sdpa(q, k, v, mask)
return z.transpose(1, 0, 2).reshape(T, n_heads * d_v)
H, H_kv, d_k, T = 8, 2, 16, 32
Q_flat = rng.standard_normal((T, H * d_k))
K_flat = rng.standard_normal((T, H_kv * d_k))
V_flat = rng.standard_normal((T, H_kv * d_k))
mask = make_mask(T)
out_gqa = gqa(Q_flat, K_flat, V_flat, H, H_kv, mask)
k_grouped = K_flat.reshape(T, H_kv, d_k)
v_grouped = V_flat.reshape(T, H_kv, d_k)
K_mha = np.repeat(k_grouped, H // H_kv, axis=1).reshape(T, H * d_k)
V_mha = np.repeat(v_grouped, H // H_kv, axis=1).reshape(T, H * d_k)
out_mha = gqa(Q_flat, K_mha, V_mha, H, H, mask)
assert np.allclose(out_gqa, out_mha, atol=1e-12)
# tile interleaves groups [kv0, kv1, kv0, kv1, ...] and changes the mapping.
K_bad = np.tile(k_grouped, (1, H // H_kv, 1)).reshape(T, H * d_k)
V_bad = np.tile(v_grouped, (1, H // H_kv, 1)).reshape(T, H * d_k)
out_bad = gqa(Q_flat, K_bad, V_bad, H, H, mask)
assert not np.allclose(out_gqa, out_bad, atol=1e-3)
print('The GQA invariant and wrong-order negative test passed', f"· max|Δ|={np.max(np.abs(out_gqa - out_mha)):.2e}")3. Online softmax and streaming tiled attention
Online softmax retains three quantities for the tiles already processed: the maximum m,
the normalizer l, and the value numerator acc. When a new maximum arrives, the old
state is rescaled by exp(m_old - m_new). The mathematics is identical to ordinary
softmax without materializing the full probability matrix.
def online_softmax_dot(
logit_tiles: list[list[float]],
value_tiles: list[list[float]],
) -> tuple[float, list[tuple[float, float, float]]]:
"""Stream scalar values through online softmax and return the update trace."""
if len(logit_tiles) != len(value_tiles) or not logit_tiles:
raise ValueError("logit_tiles and value_tiles must have the same non-zero length")
m, normalizer, numerator = -np.inf, 0.0, 0.0
trace: list[tuple[float, float, float]] = []
for logits, values in zip(logit_tiles, value_tiles):
if len(logits) != len(values) or not logits:
raise ValueError("each logit tile must match a non-empty value tile")
if not np.isfinite(logits).all() or not np.isfinite(values).all():
raise ValueError("online-softmax inputs must be finite")
m_new = max(m, max(logits))
alpha = math.exp(m - m_new) if np.isfinite(m) else 0.0
weights = [math.exp(x - m_new) for x in logits]
normalizer = alpha * normalizer + sum(weights)
numerator = alpha * numerator + sum(w * v for w, v in zip(weights, values))
m = m_new
trace.append((m, normalizer, numerator))
return numerator / normalizer, trace
def tiled_attention(
Q: np.ndarray,
K: np.ndarray,
V: np.ndarray,
mask: np.ndarray | None = None,
*,
tile_size: int = 128,
sink: float | None = None,
empty_row: EmptyRowPolicy = "error",
causal: bool = False,
window: int | None = None,
query_start: int = 0,
) -> np.ndarray:
"""Streaming 2D attention that never materializes a T_q x T_k score matrix."""
Q, K, V = (np.asarray(x) for x in (Q, K, V))
if any(x.ndim != 2 or 0 in x.shape for x in (Q, K, V)):
raise ValueError("tiled_attention expects Q/K/V to be 2D")
if Q.shape[1] != K.shape[1] or K.shape[0] != V.shape[0]:
raise ValueError("incompatible Q/K/V shapes")
if isinstance(tile_size, bool) or not isinstance(tile_size, int) or tile_size <= 0:
raise ValueError("tile_size must be positive")
if empty_row not in {"error", "zero"}:
raise ValueError("empty_row must be 'error' or 'zero'")
if not all(np.isfinite(x).all() for x in (Q, K, V)):
raise ValueError("Q/K/V must be finite")
if sink is not None and not np.isfinite(sink):
raise ValueError("sink must be finite")
# Accumulate at least in float32. This mirrors the direct reference and avoids
# performing the dot product itself in float16 when learners try mixed precision.
dtype = np.result_type(Q.dtype, K.dtype, V.dtype, np.float32)
Q, K, V = (x.astype(dtype, copy=False) for x in (Q, K, V))
T_q, d_k = Q.shape
T_k, d_v = V.shape
if window is not None and (not causal or isinstance(window, bool) or not isinstance(window, int) or window <= 0):
raise ValueError("window requires causal=True and a positive integer")
if isinstance(query_start, bool) or not isinstance(query_start, int) or query_start < 0:
raise ValueError("query_start must be a nonnegative integer")
if mask is not None:
mask = np.asarray(mask, dtype=dtype)
if mask.shape != (T_q, T_k):
raise ValueError(f"mask must have shape {(T_q, T_k)}, got {mask.shape}")
if np.isnan(mask).any() or np.isposinf(mask).any():
raise ValueError("mask may not contain NaN/+inf")
scale = np.asarray(1.0 / math.sqrt(d_k), dtype=dtype)
out = np.zeros((T_q, d_v), dtype=dtype)
for qi in range(T_q):
# A sink starts with denominator mass 1 and zero value contribution.
m = float(sink) if sink is not None else -np.inf
normalizer = 1.0 if sink is not None else 0.0
acc = np.zeros(d_v, dtype=out.dtype)
for start in range(0, T_k, tile_size):
stop = min(start + tile_size, T_k)
with np.errstate(over="ignore", invalid="ignore"):
logits = (Q[qi] @ K[start:stop].T) * scale
if not np.isfinite(logits).all():
raise FloatingPointError("Q @ K.T overflowed; rescale the inputs")
mask_tile = 0.0 if mask is None else mask[qi, start:stop]
if causal:
distance = query_start + qi - np.arange(start, stop)
allowed = distance >= 0
if window is not None:
allowed &= distance < window
mask_tile = mask_tile + np.where(allowed, 0.0, -np.inf)
finite_bias = np.isfinite(mask_tile)
with np.errstate(over="ignore", invalid="ignore"):
logits = logits + mask_tile
if np.any(finite_bias & ~np.isfinite(logits)):
raise FloatingPointError(
"adding the finite mask bias overflowed; reduce the bias magnitude"
)
finite = np.isfinite(logits)
if not finite.any():
continue
tile_max = float(logits[finite].max())
m_new = max(m, tile_max)
alpha = math.exp(m - m_new) if np.isfinite(m) else 0.0
weights = np.where(finite, np.exp(logits - m_new), np.zeros_like(logits))
normalizer = alpha * normalizer + float(weights.sum())
acc = alpha * acc + weights @ V[start:stop]
m = m_new
if normalizer == 0.0:
if empty_row == "error":
raise ValueError('fully masked row in tiled_attention' + f": {qi}")
continue
out[qi] = acc / normalizer
return out
logits = [2.0, 1.0, 3.0, 0.5]
values = [1.0, 2.0, 3.0, 4.0]
output, trace = online_softmax_dot([logits[:2], logits[2:]], [values[:2], values[2:]])
(_, l1, n1), (_, l2, n2) = trace
ref_trace = REFERENCE["online_softmax_trace"]
for got, key in [(l1, "l1"), (n1, "n1"), (l2, "l2"), (n2, "n2"), (output, "output")]:
assert abs(got - ref_trace[key]) < 1e-12, (key, got, ref_trace[key])
direct_logits = np.asarray(logits)
direct_weights = np.exp(direct_logits - direct_logits.max())
direct_weights /= direct_weights.sum()
assert abs(output - direct_weights @ np.asarray(values)) < 1e-12
n, d = 40, 16
Q = rng.standard_normal((n, d))
K = rng.standard_normal((n, d))
V = rng.standard_normal((n, d))
for current_mask in (make_mask(n), make_mask(n, 8), make_mask(n, 8, 2)):
reference = sdpa(Q, K, V, current_mask)
streamed = tiled_attention(Q, K, V, current_mask, tile_size=7)
assert np.allclose(reference, streamed, atol=1e-12)
empty_mask = np.full((n, n), -np.inf)
zero_ref = sdpa(Q, K, V, empty_mask, empty_row="zero")
zero_stream = tiled_attention(Q, K, V, empty_mask, tile_size=7, empty_row="zero")
assert np.all(zero_ref == 0) and np.all(zero_stream == 0)
# Float16 inputs are intentionally accumulated in float32 in both references.
Q16, K16, V16 = (x.astype(np.float16) for x in (Q, K, V))
ref16 = sdpa(Q16, K16, V16, make_mask(n))
stream16 = tiled_attention(Q16, K16, V16, make_mask(n), tile_size=7)
assert ref16.dtype == np.float32 and stream16.dtype == np.float32
assert np.allclose(ref16, stream16, atol=2e-6)
# A very large but finite additive bias may overflow only after it is added to
# a finite score. Both references reject that state instead of treating +inf
# as a forbidden position or allowing NaN to propagate.
f32_max = np.finfo(np.float32).max
q_over = np.asarray([[math.sqrt(0.75 * f32_max)]], dtype=np.float32)
v_over = np.ones((1, 1), dtype=np.float32)
for sign in (1.0, -1.0):
k_over = sign * q_over
bias_over = np.asarray([[sign * 0.75 * f32_max]], dtype=np.float32)
for attention_fn in (sdpa, tiled_attention):
try:
attention_fn(q_over, k_over, v_over, bias_over)
raise AssertionError("finite mask-bias overflow must be rejected")
except FloatingPointError as exc:
assert "mask bias" in str(exc)
print('Online softmax and tiled attention match direct computation', f"· O={output:.6f}")4. The KV calculator and units
All arithmetic uses binary IEC units: KiB = 2**10, MiB = 2**20, and
GiB = 2**30. This removes ambiguity: GB strictly means 10**9 bytes and is not used
for these binary calculations.
KiB, MiB, GiB = 2**10, 2**20, 2**30
def kv_bytes_per_token(
layers: int,
kv_heads: int,
head_dim: int,
bytes_per_value: int = 2,
) -> int:
"""Return K+V cache bytes per generated token across all layers."""
values = (layers, kv_heads, head_dim, bytes_per_value)
if any(isinstance(x, bool) or not isinstance(x, int) or x <= 0 for x in values):
raise ValueError("all KV-cache dimensions must be positive integers")
return 2 * layers * kv_heads * head_dim * bytes_per_value
context = REFERENCE["context_tokens"]
rows = {
"gpt_oss_gqa_all_full_equivalent": kv_bytes_per_token(36, 8, 64),
"gpt_oss_mha_hypothetical": kv_bytes_per_token(36, 64, 64),
"gpt_oss_mqa_hypothetical": kv_bytes_per_token(36, 1, 64),
}
assert rows == REFERENCE["kv_rows"]
labels = {
"gpt_oss_gqa_all_full_equivalent": 'gpt-oss GQA all-full equivalent (36L, 8 KV, 64)',
"gpt_oss_mha_hypothetical": 'gpt-oss hypothetical MHA (64 KV)',
"gpt_oss_mqa_hypothetical": 'gpt-oss hypothetical MQA (1 KV)',
}
for key, byte_count in rows.items():
print(f"{labels[key]:42s} {byte_count / KiB:7.1f} KiB/token · {byte_count * context / GiB:7.2f} GiB @131072")
mla = (512 + 64) * 61 * 2
mha_equivalent = 2 * 61 * 128 * 128 * 2
assert mla == REFERENCE["mla"]["bytes_per_token"]
assert mha_equivalent == REFERENCE["mla"]["mha_equivalent_bytes_per_token"]
assert abs(mha_equivalent / mla - REFERENCE["mla"]["raw_element_ratio"]) < 1e-12
per_layer_token = 2 * 8 * 64 * 2
full = 18 * context * per_layer_token
sliding = 18 * 128 * per_layer_token
hybrid = REFERENCE["gpt_oss_hybrid"]
assert full == hybrid["full_bytes"]
assert sliding == hybrid["sliding_bytes"]
assert full + sliding == hybrid["hybrid_total_bytes"]
all_full = rows["gpt_oss_gqa_all_full_equivalent"] * context
assert all_full == hybrid["all_full_bytes"]
print(f"KV payload with window-capped local caches: {(full + sliding) / GiB:.6f} GiB vs all-full {all_full / GiB:.3f} GiB · ×{all_full / (full + sliding):.3f}")
llama70 = kv_bytes_per_token(80, 8, 128)
qwen7 = kv_bytes_per_token(28, 4, 128)
assert llama70 == 320 * KiB and llama70 * context == 40 * GiB
assert qwen7 == 56 * KiB and qwen7 * context == 7 * GiB
print(f"E1: Llama-3.3-70B = 320 KiB/token → 40 GiB; Qwen2.5-7B = 56 KiB/token → 7 GiB")
print('KV calculator: every frozen Module-5 number reproduced')5. Learned sink: an extra score slot with no value contribution
The sink participates in the softmax maximum and denominator but contributes no value
vector. Token weights may therefore sum to less than one. We verify the mass shares and
the exact equivalence to an extra column whose V is zero.
for sink_logit, percent_ref in [(0.0, 0.78), (2.0, 5.46), (5.0, 53.69)]:
share = math.exp(sink_logit) / (128 + math.exp(sink_logit)) * 100
assert abs(share - percent_ref) < 0.01
n, d = 20, 8
Q = rng.standard_normal((n, d))
K = rng.standard_normal((n, d))
V = rng.standard_normal((n, d))
mask = make_mask(n)
sink_logit = 1.3
out_sink, token_weights = sdpa(Q, K, V, mask, sink=sink_logit, return_weights=True)
scores = Q @ K.T / np.sqrt(d) + mask
extended_scores = np.concatenate([scores, np.full((n, 1), sink_logit)], axis=1)
row_max = extended_scores.max(axis=-1, keepdims=True)
extended_weights = np.exp(extended_scores - row_max)
extended_weights /= extended_weights.sum(axis=-1, keepdims=True)
out_column = extended_weights[:, :-1] @ V # the extra sink value is exactly zero
assert np.allclose(out_sink, out_column, atol=1e-12)
assert np.allclose(token_weights, extended_weights[:, :-1], atol=1e-12)
assert np.all(token_weights.sum(axis=-1) < 1.0)
# A common shift must include the sink. Shifting tokens alone changes the result.
shifted = sdpa(Q, K, V, mask + 2.0, sink=sink_logit + 2.0)
np.testing.assert_allclose(shifted, out_sink, atol=1e-12)
assert not np.allclose(sdpa(Q, K, V, mask + 2.0, sink=sink_logit), out_sink)
# With no visible token, all probability mass goes to the zero-valued sink.
all_masked = np.full((n, n), -np.inf)
sink_only, sink_only_weights = sdpa(
Q, K, V, all_masked, sink=sink_logit, return_weights=True
)
assert np.all(sink_only == 0) and np.all(sink_only_weights == 0)
streamed_sink_only = tiled_attention(Q, K, V, all_masked, sink=sink_logit)
assert np.all(streamed_sink_only == 0)
print('Sink mass, column equivalence, and the empty-row case passed', f"· max|Δ|={np.max(np.abs(out_sink - out_column)):.2e}")6. Working set: naive versus tiled
We count selected temporary arrays, not the total peak. Output adds T_q × d_v elements. The streaming path builds masks per tile; the direct path builds its full mask inside the measurement. The deterministic comparison is: naive attention stores
T_q × T_k matrices, while the streaming implementation keeps one tile and one value
accumulator. The tracemalloc readings below are diagnostic only: whether NumPy native
allocations are visible depends on the Python/NumPy build, so the measured ratio is not a
correctness assertion.
def naive_forward(Q: np.ndarray, K: np.ndarray, V: np.ndarray, mask: np.ndarray) -> np.ndarray:
"""Materialize scores/probabilities for a 2D memory baseline.
This path deliberately keeps the O(T_q*T_k) arrays, but still rejects a fully
masked row instead of silently producing NaN.
"""
Q, K, V, mask = map(np.asarray, (Q, K, V, mask))
if any(x.ndim != 2 for x in (Q, K, V)):
raise ValueError("naive_forward expects 2D Q/K/V")
if Q.shape[1] != K.shape[1] or K.shape[0] != V.shape[0]:
raise ValueError("incompatible Q/K/V shapes")
if mask.shape != (Q.shape[0], K.shape[0]):
raise ValueError("mask must have shape [T_q, T_k]")
dtype = np.result_type(Q.dtype, K.dtype, V.dtype, np.float32)
Q, K, V, mask = (x.astype(dtype, copy=False) for x in (Q, K, V, mask))
d_k = Q.shape[-1]
scale = np.asarray(1.0 / math.sqrt(d_k), dtype=dtype)
scores = (Q @ K.T) * scale + mask
if not np.isfinite(scores).any(axis=-1).all():
raise ValueError("naive_forward received a fully masked row")
row_max = scores.max(axis=-1, keepdims=True)
probabilities = np.where(
np.isfinite(scores), np.exp(scores - row_max), np.zeros_like(scores)
)
probabilities /= probabilities.sum(axis=-1, keepdims=True)
return probabilities @ V
def peak_python_mib(function, *args, **kwargs) -> float:
"""Measure Python-visible allocations; native NumPy visibility may vary."""
gc.collect()
tracemalloc.start()
try:
function(*args, **kwargs)
_, peak = tracemalloc.get_traced_memory()
finally:
tracemalloc.stop()
return peak / MiB
# First establish correctness independently of any memory measurement.
n_check, d_check = 256, 32
Qc = rng.standard_normal((n_check, d_check))
Kc = rng.standard_normal((n_check, d_check))
Vc = rng.standard_normal((n_check, d_check))
Mc = make_mask(n_check)
assert np.allclose(
naive_forward(Qc, Kc, Vc, Mc),
tiled_attention(Qc, Kc, Vc, Mc, tile_size=64),
atol=1e-12,
)
tile_size = 128
for n in (1024, 2048):
itemsize = np.dtype(np.float64).itemsize
naive_temporaries = 2 * n * n * itemsize # scores + probabilities
tiled_state = (2 * tile_size + 32) * itemsize # logits + weights + d_v accumulator
assert naive_temporaries / tiled_state > 100
Qm = rng.standard_normal((n, 32))
Km = rng.standard_normal((n, 32))
Vm = rng.standard_normal((n, 32))
observed_naive = peak_python_mib(lambda: naive_forward(Qm, Km, Vm, make_mask(n)))
observed_tiled = peak_python_mib(tiled_attention, Qm, Km, Vm, tile_size=tile_size, causal=True)
print(
f"n={n} · selected buffers (not total peak): dense={naive_temporaries / MiB:.1f} MiB, "
f"tiled≈{tiled_state / KiB:.1f} KiB · tracemalloc: "
f"{observed_naive:.1f} vs {observed_tiled:.1f} MiB"
)
print('Memory: selected dense O(n²) buffers versus O(tile + d_v) scratch state verified')7. Queries after a cached prefix
A query row number need not equal its token position. A query at position 5 can see keys 0–5. Compare full-sequence, single-token and chunked execution, then reproduce the missing-offset mask bug.
rng_cache = np.random.default_rng(505)
Qp = rng_cache.normal(size=(9, 6))
Kp = rng_cache.normal(size=(9, 6))
Vp = rng_cache.normal(size=(9, 4))
for window in (None, 3):
complete = sdpa(Qp, Kp, Vp, make_mask(9, window))
for prefix, count in ((0, 9), (5, 1), (4, 3)):
end = prefix + count
distance = np.arange(prefix, end)[:, None] - np.arange(end)[None, :]
allowed = distance >= 0
if window is not None:
allowed &= distance < window
mask = np.where(allowed, 0.0, -np.inf)
direct = sdpa(Qp[prefix:end], Kp[:end], Vp[:end], mask)
stream = tiled_attention(Qp[prefix:end], Kp[:end], Vp[:end],
causal=True, query_start=prefix, window=window, tile_size=2)
np.testing.assert_allclose(direct, complete[prefix:end], atol=1e-12)
np.testing.assert_allclose(stream, direct, atol=1e-12)
wrong = sdpa(Qp[5:6], Kp[:6], Vp[:6], np.where(np.tri(1, 6), 0.0, -np.inf))
assert not np.allclose(wrong, sdpa(Qp[5:6], Kp[:6], Vp[:6]))
# Empty tiles must be skipped without forming -inf - (-inf).
late_mask = np.full((1, 9), -np.inf)
late_mask[0, -1] = 0
np.testing.assert_allclose(tiled_attention(Qp[:1], Kp, Vp, late_mask, tile_size=2), Vp[-1:])
print('Cached prefix, chunked decode, window, empty tiles: PASS')8. MLA: moving a projection to the query side
C holds the shared latent; A and B expand keys and values. Compare explicit K/V reconstruction with latent-space computation, including a separate positional branch and an output projection. This small experiment tests MLA algebra rather than a complete model.
rng_mla = np.random.default_rng(506)
H, Tq, Tk, dc, dk, dv, dr = 2, 3, 7, 4, 6, 5, 2
C = rng_mla.normal(size=(Tk, dc))
A = rng_mla.normal(size=(H, dk, dc))
B = rng_mla.normal(size=(H, dv, dc))
q = rng_mla.normal(size=(H, Tq, dk))
qr = rng_mla.normal(size=(H, Tq, dr)) # already rotated query branch
kr = rng_mla.normal(size=(Tk, dr)) # shared, already rotated key branch
Wout = rng_mla.normal(size=(H, dv, 8))
explicit, absorbed = [], []
for h in range(H):
expanded_k, expanded_v = C @ A[h].T, C @ B[h].T
scores = (q[h] @ expanded_k.T + qr[h] @ kr.T) / np.sqrt(dk + dr)
latent_scores = ((q[h] @ A[h]) @ C.T + qr[h] @ kr.T) / np.sqrt(dk + dr)
np.testing.assert_allclose(scores, latent_scores, atol=1e-12)
p = np.exp(scores - scores.max(-1, keepdims=True))
p /= p.sum(-1, keepdims=True)
explicit.append((p @ expanded_v) @ Wout[h])
absorbed.append((p @ C) @ (B[h].T @ Wout[h]))
np.testing.assert_allclose(sum(explicit), sum(absorbed), atol=1e-12)
# A rectangular A makes R @ A and A @ R different *shapes*, not a commutator.
# The correct obstruction: A.T @ R(n-m).T @ q changes with key position n.
def R(angle):
return np.array([[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]])
A_small = np.array([[1., 2., 0.], [0., 1., 3.]])
q_small = np.array([1., .5])
c_small = np.array([.2, -.4, .7])
m, n = .3, 1.1
rotated_score = (R(m) @ q_small) @ (R(n) @ A_small @ c_small)
effective_q = A_small.T @ R(n-m).T @ q_small
np.testing.assert_allclose(rotated_score, effective_q @ c_small, atol=1e-12)
assert not np.allclose(effective_q, A_small.T @ R(.8-m).T @ q_small)
print('MLA key/value/output absorption and position dependence: PASS')PyTorch bridge · B04 — SDPA: transparent math and tensor API
This cell embeds the canonical NumPy→PyTorch bridge and runs its parity check. It is self-contained and does not import repository files.
Inputs are finite floating Q/K/V with nonempty compatible shapes [..., T_q, D], [..., T_k, D], and [..., T_k, D_v]. API comparisons share a dtype and device; the Boolean mask is on that device. This short bridge always returns zero for an empty row, whereas the main reference above offers an error/zero choice. The checks compare both explicit paths and the API, including empty rows and gradients.
import importlib.util
import subprocess
import sys
missing = []
if importlib.util.find_spec("numpy") is None:
missing.append("numpy>=1.26")
if importlib.util.find_spec("torch") is None:
missing.append("torch>=2.5")
if missing:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", *missing])
import numpy as np
import torch
import torch.nn.functional as F
import math
def numpy_sdpa_explicit(
q: np.ndarray,
k: np.ndarray,
v: np.ndarray,
*,
allowed_mask: np.ndarray | None = None,
) -> np.ndarray:
"""Minimal visible SDPA algorithm; ``True`` in the mask means allowed."""
output_dtype = np.result_type(q.dtype, k.dtype, v.dtype)
if output_dtype.kind not in "fc":
output_dtype = np.dtype(np.float64)
work_dtype = np.result_type(output_dtype, np.float32)
q, k, v = (x.astype(work_dtype, copy=False) for x in (q, k, v))
scale = 1.0 / math.sqrt(q.shape[-1])
scores = np.matmul(q, np.swapaxes(k, -1, -2)) * scale
if allowed_mask is None:
row_has_key = np.ones_like(scores[..., :1], dtype=bool)
else:
mask = np.broadcast_to(np.asarray(allowed_mask, dtype=bool), scores.shape)
row_has_key = mask.any(axis=-1, keepdims=True)
scores = np.where(mask, scores, -np.inf)
safe_scores = np.where(row_has_key, scores, 0.0)
shifted = safe_scores - safe_scores.max(axis=-1, keepdims=True)
exp = np.exp(shifted)
weights = exp / exp.sum(axis=-1, keepdims=True)
weights = np.where(row_has_key, weights, 0.0)
return np.matmul(weights, v).astype(output_dtype, copy=False)
def torch_sdpa_explicit(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
*,
allowed_mask: torch.Tensor | None = None,
) -> torch.Tensor:
"""Line-for-line tensor counterpart of :func:`numpy_sdpa_explicit`."""
output_dtype = q.dtype
work_dtype = torch.float32 if q.dtype in (torch.float16, torch.bfloat16) else q.dtype
q, k, v = (x.to(work_dtype) for x in (q, k, v))
scale = 1.0 / math.sqrt(q.shape[-1])
scores = q @ k.transpose(-2, -1) * scale
if allowed_mask is None:
row_has_key = torch.ones_like(scores[..., :1], dtype=torch.bool)
else:
mask = torch.broadcast_to(allowed_mask.to(torch.bool), scores.shape)
row_has_key = mask.any(dim=-1, keepdim=True)
scores = scores.masked_fill(~mask, -torch.inf)
safe_scores = torch.where(row_has_key, scores, torch.zeros_like(scores))
weights = torch.softmax(safe_scores, dim=-1)
weights = torch.where(row_has_key, weights, torch.zeros_like(weights))
return (weights @ v).to(output_dtype)
def torch_sdpa_optimized(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
*,
allowed_mask: torch.Tensor | None = None,
dropout_p: float = 0.0,
) -> torch.Tensor:
"""Public PyTorch SDPA API; the runtime may choose an optimized backend."""
return F.scaled_dot_product_attention(
q,
k,
v,
attn_mask=allowed_mask,
dropout_p=dropout_p,
)
rng = np.random.default_rng(4)
q_np = rng.standard_normal((1, 2, 4, 8))
k_np = rng.standard_normal((1, 2, 4, 8))
v_np = rng.standard_normal((1, 2, 4, 6))
mask_np = np.tril(np.ones((4, 4), dtype=bool))
np_out = numpy_sdpa_explicit(q_np, k_np, v_np, allowed_mask=mask_np)
q = torch.tensor(q_np, dtype=torch.float64, requires_grad=True)
k = torch.tensor(k_np, dtype=torch.float64, requires_grad=True)
v = torch.tensor(v_np, dtype=torch.float64, requires_grad=True)
mask = torch.tensor(mask_np)
explicit_out = torch_sdpa_explicit(q, k, v, allowed_mask=mask)
api_out = torch_sdpa_optimized(q, k, v, allowed_mask=mask, dropout_p=0.0)
torch.testing.assert_close(explicit_out, torch.from_numpy(np_out), rtol=1e-10, atol=1e-10)
torch.testing.assert_close(api_out, explicit_out, rtol=1e-10, atol=1e-10)
mask_np[0] = False
np_empty = numpy_sdpa_explicit(q_np, k_np, v_np, allowed_mask=mask_np)
t_empty = torch_sdpa_explicit(q, k, v, allowed_mask=torch.tensor(mask_np))
api_empty = torch_sdpa_optimized(q, k, v, allowed_mask=torch.tensor(mask_np), dropout_p=0.0)
np.testing.assert_array_equal(np_empty[..., 0, :], 0.0)
torch.testing.assert_close(t_empty[..., 0, :], torch.zeros_like(t_empty[..., 0, :]))
torch.testing.assert_close(api_empty, t_empty, rtol=1e-10, atol=1e-10)
grads_explicit = torch.autograd.grad(explicit_out.square().sum(), (q, k, v), retain_graph=True)
grads_api = torch.autograd.grad(api_out.square().sum(), (q, k, v), retain_graph=True)
for ge, ga in zip(grads_explicit, grads_api):
torch.testing.assert_close(ge, ga, rtol=1e-9, atol=1e-10)
explicit_out.sum().backward()
assert all(t.grad is not None and torch.isfinite(t.grad).all() for t in (q, k, v))
print("B04 explicit NumPy / explicit PyTorch / optimized API: PASS")PyTorch bridge · B05 — GQA: explicit repetition and enable_gqa
This cell embeds the canonical NumPy→PyTorch bridge and runs its parity check. It is self-contained and does not import repository files.
Heads occupy axis 1 here: Q [B,H_q,T_q,D], K [B,H_kv,T_k,D], and V [B,H_kv,T_k,D_v], with nonempty axes and H_q divisible by H_kv. Keep B04's value, dtype, device, and mask assumptions. The earlier reference flattened heads into [T,H*D]; reshape that interface before repeating anything along axis 1.
import importlib.util
import subprocess
import sys
missing = []
if importlib.util.find_spec("numpy") is None:
missing.append("numpy>=1.26")
if importlib.util.find_spec("torch") is None:
missing.append("torch>=2.5")
if missing:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", *missing])
import numpy as np
import torch
import torch.nn.functional as F
import math
def numpy_sdpa_explicit(
q: np.ndarray,
k: np.ndarray,
v: np.ndarray,
*,
allowed_mask: np.ndarray | None = None,
) -> np.ndarray:
"""Minimal visible SDPA algorithm; ``True`` in the mask means allowed."""
output_dtype = np.result_type(q.dtype, k.dtype, v.dtype)
if output_dtype.kind not in "fc":
output_dtype = np.dtype(np.float64)
work_dtype = np.result_type(output_dtype, np.float32)
q, k, v = (x.astype(work_dtype, copy=False) for x in (q, k, v))
scale = 1.0 / math.sqrt(q.shape[-1])
scores = np.matmul(q, np.swapaxes(k, -1, -2)) * scale
if allowed_mask is None:
row_has_key = np.ones_like(scores[..., :1], dtype=bool)
else:
mask = np.broadcast_to(np.asarray(allowed_mask, dtype=bool), scores.shape)
row_has_key = mask.any(axis=-1, keepdims=True)
scores = np.where(mask, scores, -np.inf)
safe_scores = np.where(row_has_key, scores, 0.0)
shifted = safe_scores - safe_scores.max(axis=-1, keepdims=True)
exp = np.exp(shifted)
weights = exp / exp.sum(axis=-1, keepdims=True)
weights = np.where(row_has_key, weights, 0.0)
return np.matmul(weights, v).astype(output_dtype, copy=False)
def torch_sdpa_explicit(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
*,
allowed_mask: torch.Tensor | None = None,
) -> torch.Tensor:
"""Line-for-line tensor counterpart of :func:`numpy_sdpa_explicit`."""
output_dtype = q.dtype
work_dtype = torch.float32 if q.dtype in (torch.float16, torch.bfloat16) else q.dtype
q, k, v = (x.to(work_dtype) for x in (q, k, v))
scale = 1.0 / math.sqrt(q.shape[-1])
scores = q @ k.transpose(-2, -1) * scale
if allowed_mask is None:
row_has_key = torch.ones_like(scores[..., :1], dtype=torch.bool)
else:
mask = torch.broadcast_to(allowed_mask.to(torch.bool), scores.shape)
row_has_key = mask.any(dim=-1, keepdim=True)
scores = scores.masked_fill(~mask, -torch.inf)
safe_scores = torch.where(row_has_key, scores, torch.zeros_like(scores))
weights = torch.softmax(safe_scores, dim=-1)
weights = torch.where(row_has_key, weights, torch.zeros_like(weights))
return (weights @ v).to(output_dtype)
def numpy_gqa_explicit(
q: np.ndarray,
k: np.ndarray,
v: np.ndarray,
*,
allowed_mask: np.ndarray | None = None,
) -> np.ndarray:
"""Visible GQA: repeat K/V heads, then apply the explicit SDPA reference."""
if q.shape[1] % k.shape[1]:
raise ValueError("Hq must be divisible by Hkv")
group = q.shape[1] // k.shape[1]
k_repeated = np.repeat(k, group, axis=1)
v_repeated = np.repeat(v, group, axis=1)
return numpy_sdpa_explicit(q, k_repeated, v_repeated, allowed_mask=allowed_mask)
def torch_gqa_explicit(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
*,
allowed_mask: torch.Tensor | None = None,
) -> torch.Tensor:
"""Visible tensor GQA: repeat K/V heads, then use explicit tensor SDPA."""
if q.shape[1] % k.shape[1]:
raise ValueError("Hq must be divisible by Hkv")
group = q.shape[1] // k.shape[1]
k_repeated = k.repeat_interleave(group, dim=1)
v_repeated = v.repeat_interleave(group, dim=1)
return torch_sdpa_explicit(q, k_repeated, v_repeated, allowed_mask=allowed_mask)
def torch_gqa_optimized(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
*,
allowed_mask: torch.Tensor | None = None,
dropout_p: float = 0.0,
) -> torch.Tensor:
"""Public SDPA GQA path; availability depends on PyTorch backend support."""
if q.shape[1] % k.shape[1]:
raise ValueError("Hq must be divisible by Hkv")
return F.scaled_dot_product_attention(
q,
k,
v,
attn_mask=allowed_mask,
dropout_p=dropout_p,
enable_gqa=True,
)
rng = np.random.default_rng(5)
q_np = rng.standard_normal((1, 4, 3, 8))
k_np = rng.standard_normal((1, 2, 3, 8))
v_np = rng.standard_normal((1, 2, 3, 7))
np_out = numpy_gqa_explicit(q_np, k_np, v_np)
q, k, v = (torch.tensor(a, dtype=torch.float64, requires_grad=True) for a in (q_np, k_np, v_np))
explicit = torch_gqa_explicit(q, k, v)
api = torch_gqa_optimized(q, k, v, dropout_p=0.0)
torch.testing.assert_close(explicit, torch.from_numpy(np_out), rtol=1e-10, atol=1e-10)
torch.testing.assert_close(api, explicit, rtol=1e-10, atol=1e-10)
for ge, ga in zip(torch.autograd.grad(explicit.square().sum(), (q,k,v)),
torch.autograd.grad(api.square().sum(), (q,k,v))):
torch.testing.assert_close(ge, ga, rtol=1e-9, atol=1e-10)
print("B05 explicit repetition / enable_gqa: PASS")Exercises
-
[auto] Add a boolean-mask API (
True = allowed) and compare it with the additive mask on selected inputs. Use the equation to explain why they express the same operation. -
[auto] Generalize
tiled_attentionto batches and multiple heads while preserving theempty_rowpolicy. -
[auto] Cross-check the NumPy reference against
torch.nn.functional.scaled_dot_product_attentionon causal/window masks and explain the boolean-mask convention difference. -
[project] On a Colab GPU, compare naive PyTorch with the SDPA/Flash backend: time, peak memory, and error against the float64 reference for
n = 1K…16K.
The mandatory path runs on CPU and needs no repository files. The code exposes the mechanism and its contracts; it is not presented as a deployment-ready attention kernel.
Module 5 project-exercise demo — Attention memory and the cost of selecting positions
Project demonstration
Download .ipynb module_05_project_demo.ipynb · 7.8 KB
Show notebook text
Module 5 project-exercise demo — Attention memory and the cost of selecting positions
Implementation demo, not a completed full-scale project. Two experiments: analytical array accounting and two-stage retrieval on synthetic data. The selector illustrates a principle; it is not a trained DSA indexer. All inputs are small, deterministic, offline, and CPU-friendly. The goal is to validate interfaces, metrics, invariants, and report structure before spending GPU or API budget.
from __future__ import annotations
import importlib.util, subprocess, sys
for package in ("numpy", "matplotlib"):
if importlib.util.find_spec(package) is None:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", package])
import math
import random
import time
from collections import Counter, defaultdict
import matplotlib.pyplot as plt
import numpy as np
SEED = 20260801
random.seed(SEED)
np.random.seed(SEED)
print(f"reproducible smoke tier; seed={SEED}")
DEMO_LANGUAGE = 'en'
Demo 1. Account for the arrays
Compare selected array sizes as sequence length grows. The direct path retains the output plus full score and probability matrices. The streaming estimate retains the output and the listed buffers for one tile. Neither expression includes Q/K/V, the backward pass, or runtime overhead. Identify the quadratic term and predict which curve grows faster before plotting it.
lengths = np.array([1024, 2048, 4096, 8192, 16384])
heads, width, tile, bytes_per = 16, 64, 128, 4
output = heads * lengths * width * bytes_per
dense = output + 2 * heads * lengths**2 * bytes_per
stream = output + heads * (tile**2 + tile * width + 2 * tile) * bytes_per
plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 12, "axes.spines.top": False, "axes.spines.right": False})
fig, ax = plt.subplots(figsize=(11, 5))
ax.loglog(lengths, dense / 2**20, "o-", color="#5E81AC", label="output + full scores/probabilities")
ax.loglog(lengths, stream / 2**20, "o-", color="#A3BE8C", label="output + selected tile buffers")
ax.set(xlabel="sequence length", ylabel="analytical MiB", title="Selected arrays, not measured GPU peak memory")
ax.legend(); ax.grid(alpha=.25); plt.show()
assert np.all(dense > stream)
print("Excludes Q/K/V, allocator overhead, backward, and external masks. Tile buffers depend on kernel scheduling.")At fixed tile size, tile buffers stay constant while the output grows linearly. The two square matrices dominate the direct estimate. This is array accounting, not a measured GPU peak: scheduling and additional buffers can change the complete working set.
Demo 2. Count the cost of a shortlist
Construct each query by adding noise to a known key. Use just eight coordinates to select 64 candidates, then score those candidates in the full 32-dimensional space. Measure shortlist recall, final retrieval, and the difference from the full attention output. These outcomes answer different questions: retrieving the best key does not necessarily preserve the complete weighted mixture.
rng = np.random.default_rng(SEED)
trials, length, dim, index_rank, topk = 200, 2048, 32, 8, 64
recall = hits = full_hits = 0
relative_errors, retained_mass = [], []
def softmax_row(x):
e = np.exp(x - x.max())
return e / e.sum()
for _ in range(trials):
keys = rng.normal(size=(length, dim))
values = rng.normal(size=(length, 6))
target = rng.integers(length)
query = keys[target] + 0.05 * rng.normal(size=dim)
index_scores = keys[:, :index_rank] @ query[:index_rank]
shortlist = np.argpartition(index_scores, -topk)[-topk:]
scores = keys @ query / np.sqrt(dim)
recall += int(target in shortlist)
hits += int(shortlist[np.argmax(scores[shortlist])] == target)
full_hits += int(np.argmax(scores) == target)
p_full, p_short = softmax_row(scores), softmax_row(scores[shortlist])
out_full, out_short = p_full @ values, p_short @ values[shortlist]
retained_mass.append(p_full[shortlist].sum())
relative_errors.append(np.linalg.norm(out_short-out_full) / max(np.linalg.norm(out_full), 1e-12))
score_ratio = (length * index_rank + topk * dim) / (length * dim)
assert score_ratio == .28125
assert 0 <= hits <= recall <= trials
print(f"shortlist recall={recall/trials:.3f}; final retrieval={hits/trials:.3f}; full retrieval={full_hits/trials:.3f}")
print(f"mean retained probability={np.mean(retained_mass):.3f}; mean relative output error={np.mean(relative_errors):.3f}")
print(f"score multiplications ratio={score_ratio:.5f}; final scoring alone={topk/length:.5f}")
print("The ratio excludes top-k, learned projections, softmax, value aggregation and data movement; it is not a speedup.")Read the metrics together. The two scoring stages use 8/32 + 64/2048 = 0.28125 of the full dot-product count; counting selected positions alone would give only 0.03125. Top-k, projections, value aggregation, and data movement are still excluded. Reduce the candidate count and measure how shortlist recall and output error change rather than assuming a particular result on this random sample.
Scaling to the full project
For GPU measurements, match inputs and masks, warm up, synchronize and reset peak counters. State whether projections, selection, main attention and cache are included. Retrieval accuracy and attention-output error are different metrics.
Minimum report
-
Data, model, and code revisions, plus the random seed.
-
The primary metric, plus quality, cost, latency, and memory where applicable.
-
A baseline, a matched budget, and at least one negative control.
-
Failed runs and the limits of the supported conclusions.