Homework · Runs in your browser
Softmax, causal attention weights and KV-cache size
Build three small pieces of the lesson in plain Python: softmax, the causal attention weights for a short sequence, and the KV-cache size formula. Edit starter.py, then run the tests. In the browser runner, press Run tests. In Colab or on your own machine, put starter.py and test_starter.py in one folder and run pytest. The same files work everywhere, and nothing needs installing except pytest.
Hints:
- Softmax: subtract the largest score before exponentiating. The answer does not change and large numbers do not overflow.
- Scores: dot(query i, key j) divided by the square root of d_k, where d_k is the vector length.
- Causal mask: query i may only use keys 0 to i. Run softmax over just those scores and put 0.0 in the rest of the row.
- KV-cache: 2 x layers x kv_heads x head_dim x seq_len x batch x bytes per value. The lesson’s worked example (32, 8, 128, 2 bytes) is 131,072 bytes per token.
Running the tests needs JavaScript. You can still read the starter code below and run it on your own machine with pytest.
Tab inserts 4 spaces. To move on with the keyboard, press Esc, then Tab. Shift+Tab always moves back.
Show a solution
Try it yourself first. This site is static, so the solution is sent to your browser with the page; it is hidden here, not secret.
import math
def softmax(scores):
"""Turn a list of scores into probabilities that sum to 1."""
top = max(scores)
exps = [math.exp(s - top) for s in scores]
total = sum(exps)
return [e / total for e in exps]
def causal_attention_weights(Q, K):
"""Attention weights for a sequence of n tokens."""
n = len(Q)
d_k = len(Q[0])
rows = []
for i in range(n):
visible = [
sum(q * k for q, k in zip(Q[i], K[j])) / math.sqrt(d_k)
for j in range(i + 1)
]
weights = softmax(visible)
rows.append(weights + [0.0] * (n - i - 1))
return rows
def kv_cache_bytes(layers, kv_heads, head_dim, seq_len, batch, bytes_per_value):
"""Bytes of KV-cache: a Key and a Value for every layer, head, token and request."""
return 2 * layers * kv_heads * head_dim * seq_len * batch * bytes_per_value
Show the tests (test_starter.py)
import math
from starter import causal_attention_weights, kv_cache_bytes, softmax
def test_softmax_sums_to_one_and_keeps_order():
p = softmax([1.0, 2.0, 3.0])
assert math.isclose(sum(p), 1.0), "softmax output must sum to 1"
assert p[0] < p[1] < p[2], "a higher score must get a higher probability"
def test_softmax_does_not_overflow_on_large_scores():
p = softmax([1000.0, 1000.0])
assert math.isclose(p[0], 0.5) and math.isclose(p[1], 0.5), (
"equal scores give equal probability; subtract the max before exp"
)
def test_future_tokens_get_zero_weight():
Q = [[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]
w = causal_attention_weights(Q, Q)
assert w[0] == [1.0, 0.0, 0.0], "token 0 can only see itself"
assert w[1][2] == 0.0, "token 1 must not see token 2 (causal mask)"
def test_each_row_sums_to_one():
Q = [[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]
for row in causal_attention_weights(Q, Q):
assert math.isclose(sum(row), 1.0), "each row of weights must sum to 1"
def test_scores_are_scaled_by_sqrt_d_k():
# Token 1 has raw scores [0, 2]; d_k = 4, so scaled they are [0, 1].
Q = [[0.0, 0.0, 0.0, 0.0], [1.0, 1.0, 0.0, 0.0]]
K = [[0.0, 0.0, 0.0, 0.0], [1.0, 1.0, 0.0, 0.0]]
w = causal_attention_weights(Q, K)
expected = 1.0 / (1.0 + math.e)
assert math.isclose(w[1][0], expected), "divide the dot products by sqrt(d_k)"
def test_kv_cache_matches_lesson_example():
assert kv_cache_bytes(32, 8, 128, 1, 1, 2) == 131072, (
"lesson example is 131,072 bytes per token"
)
assert kv_cache_bytes(32, 8, 128, 8192, 10, 2) == 10 * 8192 * 131072, (
"the cache grows with sequence length and with concurrent requests"
)