Placeholder post3 min read
Attention, from the softmax up
Scaled dot-product attention one piece at a time, with a temperature dial you can turn to watch a query make up its mind.
Every transformer layer asks the same question once per token: of everything I have seen so far, what should I copy from? Attention is the answer, and it fits in one line. This placeholder post walks through that line slowly, so the typography, the math and the margin notes have something real to hold.
The one line
Given queries , keys and values , a single head computes
where is the causal mask: on and below the diagonal, above it.1 Row of the softmax is a probability distribution over the positions , and the output for token is the matching weighted average of value vectors.
The is there for variance. If the entries of and are independent with zero mean and unit variance, then
so without the scale the logits grow with width and the softmax saturates into a one-hot vector, which has almost no gradient.
Temperature, by hand
Dividing by is a fixed temperature. Turning it by hand shows what it does. Below is one query, the token was, scoring the seven tokens before it. Low temperature sharpens the distribution toward the highest score; high temperature flattens it toward uniform.
The entropy readout gives the same picture as a number. At it falls to zero bits (one token gets everything); as it approaches bits, the uniform distribution over seven visible tokens.
In code
The whole head is a handful of lines. This version is written for reading, not speed; fused kernels compute the same thing without ever materialising the matrix.2
import math
import torch
def attention(q, k, v, causal=True):
# q, k: (batch, heads, n, d) v: (batch, heads, n, d_v)
d = q.shape[-1]
scores = q @ k.transpose(-2, -1) / math.sqrt(d)
if causal:
n = scores.shape[-1]
mask = torch.ones(n, n, dtype=torch.bool, device=q.device).triu(1)
scores = scores.masked_fill(mask, float("-inf"))
weights = scores.softmax(dim=-1)
return weights @ v, weightsA quick check that it matches the library implementation:
q, k, v = (torch.randn(2, 4, 128, 64) for _ in range(3))
ours, _ = attention(q, k, v)
ref = torch.nn.functional.scaled_dot_product_attention(q, k, v, is_causal=True)
print((ours - ref).abs().max()) # ~1e-6Where to go from here
| Variant | What changes | Memory in |
|---|---|---|
| Full softmax attention | nothing | |
| Flash attention | tiling, online softmax | |
| Sliding window | if | |
| Linear attention | kernel feature map |
Each row of that table deserves its own post. For now this one exists to show how the site sets an argument: a sentence, an equation, a thing to touch, a margin note, some code.
Notes
Cite this post
@article{sahu2026attention,
author = {Surya Kant Sahu},
title = {Attention, from the softmax up},
year = {2026},
url = {https://ojus1.github.io/posts/attention-from-the-softmax-up/}
}