Computing Context Vectors with Attention Weights
Attention scores are dot products between input vectors and can be computed with nested loops or with inputs @ inputs.T.
Why Attention Starts With a Grid of Comparisons
Attention weights tell us how strongly each input token should influence a chosen query token. On their own, though, they are only numbers that sum to 1.0. The step that turns them into something a model can use is the context vector: a single vector that blends the whole input sequence according to those weights. This article walks through the full pipeline, from raw comparisons between input vectors, to normalized weights, to the blended context vector, and it checks at each stage what the output should look like.
Self-attention begins by measuring how strongly each input token relates to every other input token. For a sequence of six input vectors, that means comparing every vector against every other vector, which produces a grid with six rows and six columns. The first version of this computation fills that grid one cell at a time with nested loops. The same grid can later be produced with a single matrix multiplication, and then the raw grid is turned into weights whose rows behave like proportions.
Read the stages as a pipeline with a checkpoint at each stage. Each stage produces a tensor, and each tensor has an expected shape and expected row behavior. Knowing what each stage should produce is what lets you find the exact place where a result goes wrong.
Computing Scores With Nested Loops
The loop-based computation starts by allocating an empty six by six tensor. Two nested loops visit every pair of input vectors, and each cell receives the dot product of the two vectors that index it. The outer loop picks a row i and the inner loop picks a column j, so the cell at position (i, j) holds the dot product of input i with input j. Every cell gets exactly one dot product, so the grid has thirty six entries.
What do you think happens?
Before reading on: what do you expect about the cell at row i, column j compared with the cell at row j, column i?
Reveal answer
They are equal. The dot product of input i with input j is the same value as the dot product of input j with input i, so the score grid is symmetric across the diagonal.
The diagonal cells pair each vector with itself. The off-diagonal cells pair different tokens, and each pair appears twice, once above the diagonal and once below it.
Compare that prediction with the source output. The scores are symmetric across the diagonal, so the cell at row 0, column 1 and the cell at row 1, column 0 both hold 0.9544. The diagonal, where each vector is paired with itself, is worth noticing as well, because it is the one place where a vector meets its own copy.
torch.Size([6, 6])- torch.empty allocates the grid before any values are filled in
- The outer loop chooses the row i and the inner loop chooses the column j
- Each cell holds the dot product of the two inputs that index it
Replacing Loops With One Matrix Multiplication
The loop version is correct, but for loops are generally slow. The same grid of scores can be produced with a single matrix multiplication, written inputs @ inputs.T. The input matrix multiplied by its own transpose contains, in each cell, the dot product of the corresponding row vectors. The transpose turns the columns of the second copy into rows, so the product lines up every vector against every other vector in one operation.
torch.Size([6, 6])- inputs.T flips the rows and columns of the second copy
- The product places the dot product of every pair of rows into one grid
The source notes that the matrix version produces the same results as the loop version, and the values can be checked visually. This is the verification habit to carry forward: when you change how a computation is written, compare the new output against the old one, element by element or as a whole grid.
Normalizing Scores With Softmax
The second stage normalizes the score grid so that the values in each row behave like proportions. The result of this step is called the attention weight tensor, and attention weights are the normalized version of the attention scores. Each raw score is passed through the softmax function, which makes each value positive and divides it by the total of its row, so that the row sums to 1.
In this expression s_{ij} is the raw score at row i and column j, and a_{ij} is the resulting attention weight. The denominator sums over the columns k of the same row i, which is exactly why each row ends up summing to 1.
The dim argument decides which axis the normalization runs across. In PyTorch, dim=-1 tells softmax to work along the last dimension. For a two-dimensional score tensor shaped as rows and columns, this means each row is normalized across its columns. Changing dim changes which direction is normalized, and that changes what the rows mean. This is why the choice of dim is part of the correctness of the result, not a cosmetic setting.
tensor([1., 1., 1., 1., 1., 1.])- dim=-1 normalizes along the columns, one row at a time
- Summing along dim=-1 gives one total per row, and each total should be 1
The source output shows why the normalized weights are not symmetric. In the score grid, the cells at row 0, column 1 and row 1, column 0 both hold 0.9544. In the weight grid, the same two positions hold 0.2006 and 0.1385, which differ. Each row is normalized by its own total, so each row changes by a different amount, and the mirror symmetry of the scores is lost in the weights. Scores are symmetric, but the normalized weights generally are not.
Checking That Each Row Sums to One
After normalization, the expected property is simple: every row of the weight tensor should sum to 1. Checking this is the fastest way to catch a normalization step that was applied along the wrong axis. The check can be done for a single row, by adding the values of one row, or for the whole tensor, by summing along the last dimension and confirming that every total is 1.
Building a Context Vector as a Weighted Sum
A context vector is the weighted sum of all input vectors, where each input is multiplied by its attention weight for one chosen query. Read this definition as a recipe with three ingredients. You need the full list of input vectors, the attention weight attached to each of them for one chosen query, and a running total that the weighted vectors are added into. Nothing in the recipe depends on the order of the inputs except that each weight must stay paired with its own input vector.
In this notation z^{2} is the context vector for the second token, x^{i} is the i-th input vector, and a^{2}_{i} is the attention weight that the second token assigns to input i. The sum runs over all T input tokens, so every input contributes to the result, even the ones that receive a very small weight.
Tracing the Accumulation Loop
The implementation follows the formula directly. It starts from a zero vector with the same shape as the query, then walks through the inputs one at a time. On each pass it scales the current input vector by its matching weight and adds the result into the running vector. Because the loop uses enumerate, the index i selects the right weight for the input x_i at the same position.
tensor([0.4419, 0.6515, 0.5683])- torch.zeros starts the running vector empty, with the same shape as the query
attn_weights[query_idx] selects the row of weights for the chosen query- enumerate pairs each index i with its own input x_i and its own weight
Predicting the shape of the context vector
Six input vectors, each of dimension 3, and a chosen query. Predict the shape of the context vector and the number of multiplications the loop performs.
- 1Identify the query shape
Each context vector has the same shape as the query, which is one input vector of dimension 3.
- 2Count the passes
The loop visits every input once, so there are six passes, one per input token.
- 3Count the scalings
Each pass multiplies one input vector by one scalar weight, giving six scale-and-add operations.
- 4Check the running total
The zero start means the first addition carries no leftover values, and after six passes the running vector holds the full sum.
The context vector has shape 3, and the loop performs six scale-and-add operations, one for each input token.
The zero start matters. The running vector has to begin empty so that the first addition does not carry leftover values from anywhere else. After the loop finishes, the running vector holds the complete sum, and printing it shows the context vector. The source's printed output for its query is three values, which the trace above is designed to let you reproduce by reasoning about the pass-by-pass totals.
Mistakes That Corrupt a Context Vector
Errors in this computation can be located by checking the query index, the normalization of the weights, and the pairing of each weight with its input. The following mistakes are the most common ways the pipeline fails.
Common mistake Normalizing along the wrong axis
What it looks likeattn_weights = torch.softmax(attn_scores, dim=0), followed by a row-sum check that returns column-like totalsWhy it failsWith dim=0 each column is normalized instead of each row, so the rows no longer represent proportions for a single query.
Do this insteadUse dim=-1 so each row of the score grid is normalized across its columns.
Common mistake Starting the running total from leftover values
What it looks likecontext_vec = torch.empty(query.shape) before the loop, with no zeroingWhy it failsThe first addition then adds to whatever values happened to be in memory, so the sum is contaminated before the loop begins.
Do this insteadStart from torch.zeros with the same shape as the query.
Common mistake Pairing a weight with the wrong input
What it looks likeUsing attn_weights[0][i] while looping over the inputs of query 1
Why it failsThe weight for one query is paired with input vectors it was not computed for, so each contribution is scaled incorrectly.
Do this insteadSelect the row for the chosen query, attn_weights[query_idx], and pair each weight with its input through enumerate.
Common mistake Assuming the score grid is the final answer, when it has not yet been normalized into weights
Computing Every Context Vector at Once
Computing one context vector answers a single question: how should the second token's representation be blended from the whole sequence? A model needs an answer for every token. The source handles this by repeating the same three steps for each query rather than inventing a new method. Attention weights are first computed for every input token as a query, producing a full table with one row per query. Each row of that table is then used in the same weighted sum to produce that query's context vector.
The source notes that the change needed to compute all context vectors, instead of only one, is a modification to the code rather than to the underlying steps. The three steps remain the same: take the query's row of weights, scale each input by its matching weight, and add the results into a running total. A matrix formulation organizes these repeated passes into one pass, but each output row is still a weighted sum of all input vectors for its query.
Practice: Diagnosing a Column-Normalized Weight Tensor
A teammate builds the weight tensor and then checks its rows by summing along dim=0. Every column total comes out to 1, but the row totals do not. Using the pipeline, name the stage that introduced the error, state what each row of the correct weight tensor should sum to, and explain why the context vector for token 1 would be wrong if you used the column-normalized weights.
Hints
- Ask which axis the softmax normalized before you look at the checks.
- Each row of the weight tensor corresponds to one query token.
- The context vector for a query uses that query's row of weights.
Summary of the Context Vector Pipeline
- 1Attention scores are dot products between every pair of input vectors. They can be computed with nested loops or with a single matrix multiplication, inputs @ inputs.T, and the two versions should agree.
- 2Softmax with dim=-1 normalizes each row of the score grid so that each row of the attention weights sums to 1. The dim argument decides the normalization axis, and a failed row-sum check usually points to it.
- 3A context vector is the weighted sum of all input vectors for one query. It starts from a zero vector shaped like the query and adds each weighted input at its matching index.
- 4Computing every context vector repeats the same three steps for every query, using one row of the attention weight table per query.
At a glance
- Attention scores are dot products between every pair of input vectors and can be computed with nested loops or with inputs @ inputs.T.
- Softmax along dim=-1 turns each row of the score grid into attention weights whose rows sum to 1.
- A context vector is the weighted sum of all input vectors for one query, built by an accumulation loop that starts from a zero vector.
- When a result is wrong, check the row sums first, since a failed check usually points to the normalization axis.