Roadmap / NumPy & Math Foundations

✓

Batched Attention Scores with einsum

mediumnumpyeinsumreshape

The problem

Inside a transformer, every position of the text looks at the other positions and decides how much each one matters. To do that, each position has two vectors of DD numbers: a query (what this position is looking for) and a key (what this position offers). The score between a query and a key is their dot product: large when the two vectors point the same way.

The real arrays have four axes, because many scores are computed at once:

letter axis meaning
BB batch several texts processed together
HH heads attention runs HH times side by side, each with its own queries and keys (explained in the Transformers section)
TT or SS positions the tokens of the text: TT query positions, SS key positions
DD head size the numbers in one query or key vector

This exercise is the shape work you will reuse when you build attention, without the attention itself.

Part 1: scaled dot products

For every text bb and head hh, the score between query position tt and key position ss is:

scoresb,h,t,s=1D∑d=1Dqb,h,t,d kb,h,s,d\mathrm{scores}_{b,h,t,s} = \frac{1}{\sqrt{D}} \sum_{d=1}^{D} q_{b,h,t,d}\, k_{b,h,s,d}

where:

  • qb,h,t,dq_{b,h,t,d} is number dd of the query vector at position tt (text bb, head hh);
  • kb,h,s,dk_{b,h,s,d} is number dd of the key vector at position ss;
  • the sum multiplies the two vectors number by number and adds up: their dot product;
  • dividing by D\sqrt{D} keeps the scores around the same size whatever the head size (more on this in the Transformers section).

Example. One text and one head (B=H=1B = H = 1), D=4D = 4, two query positions and one key position.

  • Queries: q0=(1,0,0,0)q_0 = (1, 0, 0, 0), q1=(0,2,0,0)q_1 = (0, 2, 0, 0). Key: k0=(1,1,0,0)k_0 = (1, 1, 0, 0).
  • Dot products: q0⋅k0=1⋅1=1q_0 \cdot k_0 = 1 \cdot 1 = 1 and q1⋅k0=2⋅1=2q_1 \cdot k_0 = 2 \cdot 1 = 2.
  • Divided by 4=2\sqrt{4} = 2: the scores are 0.50.5 and 1.01.0.

The result has shape (1,1,2,1)(1, 1, 2, 1): one score for each of the 2 query positions and the 1 key position.

np.einsum writes this in one line. You give each axis a letter, and letters that appear in the inputs but not in the output are summed over. Here the output keeps b,h,t,sb, h, t, s, and dd is summed.

Part 2: merging the heads

After attention, each head has produced a vector of DD numbers for every position. The next layer wants, for every position, one long vector with all the heads side by side: head 0's DD numbers, then head 1's, and so on. So shape (B,H,T,D)(B, H, T, D) must become (B,T,H⋅D)(B, T, H \cdot D).

Example. One text, H=2H = 2 heads, T=2T = 2 positions, D=2D = 2:

  • head 0 gives [0, 1] at position 0 and [2, 3] at position 1;
  • head 1 gives [4, 5] at position 0 and [6, 7] at position 1.

The right result puts each position's heads together: position 0 is [0, 1, 4, 5] and position 1 is [2, 3, 6, 7].

A plain x.reshape(1, 2, 4) has the right shape but returns [0, 1, 2, 3] and [4, 5, 6, 7]: it glues together the two positions of head 0, then the two positions of head 1. That is the wrong grouping.

reshape reads the numbers in memory order, last axis fastest, and never moves them. To group by position, first swap the head and position axes with transpose, so the order is (B,T,H,D)(B, T, H, D), and then reshape.

What to implement

  • attention_scores(q, k): q has shape (B,H,T,D)(B, H, T, D) and k has shape (B,H,S,D)(B, H, S, D). Return the (B,H,T,S)(B, H, T, S) array of scaled dot products.
  • merge_heads(x): x has shape (B,H,T,D)(B, H, T, D). Return shape (B,T,H⋅D)(B, T, H \cdot D), where out[b, t] is x[b, 0, t], then x[b, 1, t], and so on, side by side.

Requirements

  • No Python loops over batch or heads.
  • merge_heads must keep each position's heads together, as in the example. The tests catch a plain reshape.

Hints

  • For attention_scores, the einsum string has the form "....,....->....": four letters for q, four for k, four for the output. The two inputs share the letters of bb, hh and dd.
  • For merge_heads, transpose takes the new order of the axes: on a table, m.transpose(1, 0) swaps rows and columns.
solution.py
Python idle

Press Run (⌘/Ctrl + Enter) to test your code.