Measuring Attention's Geometry in JAX/Flax NNX
Part 6 of 6Attention Is a Kernel
The explainer derives the territory maps of the two attention laws and then checks the theorems on trained transformers. This is the measurement side: the telemetry that exports a trained head’s real query and key vectors, the offline replays that recompute winners under rescaled queries, the hull linear program, and the census. The models and training harness are scripts/yat_attention.py, the same script the kernel-attention contest ran; the analysis is scripts/export_attention_geometry_viz.py; figures are rendered from the bundles by scripts/render_attention_geometry_gifs.py. Bundles: kgl_blog-attngeom-qk and kgl_blog-attngeom-goat.
Exporting the geometry
Everything in the explainer’s measured half rides on one small addition to the attention module: alongside the maps, the telemetry can now capture the actual projected vectors each layer compared, on the first sequence of the batch:
if telemetry is not None and "q" in telemetry:
# the geometry export: the actual trained query/key vectors this
# layer compared, on the first sequence of the batch. (h,T,dh)
telemetry["q"].append(np.asarray(q[0], dtype=np.float16))
telemetry["k"].append(np.asarray(k[0], dtype=np.float16))
After training, a GEOMETRY=1 run does one forward pass over the fixed validation window and writes the stack to the bundle:
g = {"q": [], "k": [], "attn": [], "score_max": [], "mass": []}
model(x_fix, telemetry=g)
np.savez_compressed(
os.path.join(RESULTS_DIR, f"geom_{variant}_s{seed}.npz"),
tokens=np.asarray(x_fix[0]),
q=np.stack(g["q"]), # (L, h, T, dh) f16
k=np.stack(g["k"]), # (L, h, T, dh) f16
attn=np.stack(g["attn"])) # (L, h, T, T) f16
One sanity check makes the export trustworthy: recomputing the kernel weights offline from the exported q/k and the learned per-head scalars reproduces the model’s own attention maps to a maximum absolute error below 4e-4 across all 24 heads, which is float16 doing float16 things. The vectors on disk are the geometry the model actually used.
Two laws, two maps
The toy animations compute both owner maps from the raw formulas at every frame, six bodies, one of them moving. The whole computation is a dozen lines of numpy, and it is the same math the explainer’s draggable panels run:
dots = q @ keys.T # (G*G, K)
d2 = ((q[:, None, :] - keys[None, :, :]) ** 2).sum(-1)
kappa = (dots + B) ** 2 / (d2 + EPS)
soft_owner = dots.argmax(-1) # softmax winner = argmax q.k
ker_owner = kappa.argmax(-1)

The hull theorem gets its own animation, because its signature is a discontinuity you can watch for. The orange body walks a straight line from the center of the ring to well outside and back; each frame recomputes its territory share under both laws:

One ray, both readings
The scaling story is a one-dimensional process, so it animates as one: a query rides a ray out of the origin, and both weight rows are recomputed from the two formulas at every position:

The scale test on the trained model
The explainer’s strongest claim, that no rescaling of any query can change a softmax winner, is checked on the real trained vectors by brute replay. For each row, at each scale , the score is rebuilt from the raw law; for the kernel that means reassembling the distance from the same dot products, with the head’s own learned scalars:
if model == "softmax":
def score(t):
return t * dots # argmax unaffected by /sqrt(dh)
else:
b, eps = b_l[l][h], e_l[l][h]
def score(t):
d2 = (t * t) * qq[:, None] + kk2[None, :] - 2 * t * dots
return (t * dots + b) ** 2 / (np.maximum(d2, 0.0) + eps)
| queries scaled by | softmax winners changed | yat kernel | no Q/K tier |
|---|---|---|---|
| 0.5x | 0 | 12.1% | 19.0% |
| 2x | 0 | 15.1% | 16.7% |
| 4x | 0 | 23.3% | 24.2% |
The zeros are exact: 0 of 15,240 winner comparisons across all layers, heads, rows, and scales. The animation sweeps the same computation continuously on one head of each model:

The census, through training
Ownership on the real window comes straight off the checkpointed maps: the winner of row is its argmax, and occupancy is how many distinct keys ever win:
win = attn.argmax(axis=-1).astype(np.int16) # (L,H,T)
occ = len(set(win[l, h][first:].tolist())) / T # distinct winners

Averaged over all 24 heads, the softmax model’s occupancy climbs from 27 to 72 distinct winners out of 128 through training; the kernel’s from 31 to 54. Held against the entropy telemetry (0.49 versus 0.72 on this window), that is the inversion the explainer dwells on: the sharper-rowed model crowns more distinct winners, and row concentration and map concentration come apart as measurements.
The hull, by linear program
Whether a key sits inside the convex hull of its 127 siblings is a feasibility question, so it goes to a linear program: does a convex combination of the others reproduce this key exactly?
from scipy.optimize import linprog
others = np.delete(K, j, axis=0) # (T-1, dh)
A_eq = np.vstack([others.T, np.ones(T - 1)])
b_eq = np.concatenate([K[j], [1.0]])
r = linprog(np.zeros(T - 1), A_eq=A_eq, b_eq=b_eq,
bounds=(0, None), method="highs")
inside = (r.status == 0)
Result: 0 interior keys of 3,072 in the softmax model, 0 of 3,072 in the kernel model. In 48 dimensions a generic cloud of 128 points is all hull, so the plane’s absolute disenfranchisement becomes, in the real network, the statistical statement the census already measured.
The far sky, on its schedule
The last animation is the explainer’s limit made visible, with its counter computed at every frame: the kernel’s owner map as the view zooms from radius 2 to 120, against the softmax map that provably cannot depend on the zoom at all. The antipodal-agreement statistic, the fraction of directions whose owner matches their opposite’s, climbs from 0 to 100 percent on the schedule the correction sets:

Every number is from seed-0 runs of scripts/yat_attention.py on Kaggle with TELEMETRY=1 GEOMETRY=1 (bundles kgl_blog-attngeom-qk, kgl_blog-attngeom-goat), analyzed locally by scripts/export_attention_geometry_viz.py; the toy animations compute the two laws’ formulas directly, with the explainer’s panel scalars. Quality numbers for these architectures, with seeds, live in the kernel-attention companion.
Cite as
Bouhsine, T. (). Measuring Attention's Geometry in JAX/Flax NNX. Records of the !mmortal Data Scientist. https://tahabouhsine.com/blog/the-geometry-of-attention-jax-flax-nnx/
BibTeX
@misc{bouhsine2026thegeometryofattentionjaxflaxnnx,
author = {Bouhsine, Taha},
title = {Measuring Attention's Geometry in JAX/Flax NNX},
year = {2026},
month = {jul},
howpublished = {\url{https://tahabouhsine.com/blog/the-geometry-of-attention-jax-flax-nnx/}},
note = {Blog post, Records of the !mmortal Data Scientist}
}