LLM Engine (Side Quest): Heads down on attention
In a previous post, when explaining attention I did mention the expression "attention heads", and then quickly moved on without explaining what that meant or how it did affect the computation. In this post, I'll try to atone for that by explaining to the best of my current understanding (clearly here current means my understanding after being done with writing this and all the necessary reading).
We'll start just after the application of . We have embedded position into our query () and keys (), the important clarification to do here is that we treated these tensors as vectors, but in reality they are a collection of vectors that are packed as a single vector, so for :
Where is the number of heads and is the head dimension.
and (the value vector) have a different layout from (both are identical), but here each row will have the same dimension and there will be a different number of rows, the called "key-value heads" or for friends. The relationship between number of heads and the key-value heads is a model design decision and can define the following taxonomy:
- Multi-head Attention (MHA), same number of heads and key-value heads; the original transformer, good quality but more computationally expensive (this will be apparent in the next few paragraphs)
- Multi-query Attention (MQA), single key-value head is shared across all heads; this is cheaper to compute, but apparently has instability and quality issues.
- Grouped-query Attention (GQA), multiple key-value heads but they are each shared by a subset of the attention heads (e.g Llama 3 8B has 32 heads and 8 key-value heads)
Now that we have that out of the way, we'll enter into what is done for each attention head. First though, we need to determine which key-value head interacts with the current head. Let's say we are processing attention head (), we then will determine the key-value head :
We reached the part where in the previous post we said something to the effect of "we then apply our query to the keys of all previous tokens" and we also magically introduced a (master foreshadowing). Untangling all of the magic, we'll now say, we get all previously computed vectors corresponding to head from the cache (ok, not all of the magic, some mirrors left) and for computation convenience we'll pack them side by side to build a matrix (let's say current token is number ):
We then proceed to compute the scores that correspond to this head:
Then these scores need to be scaled. The explanation for the scaling and its value is that this product adds the components of all head dimensions. Here the assumption made is that each of these components is an independent variable, and if its mean is and variance is it follows that the variance of the sum is ( dimensions, components in the sum), so scaling by the standard deviation would normalize (standard deviation is ), so:
Now that scores are standardized, we use to turn them into probabilities:
And we use these to compute the attention for this head by applying it to the values of all tokens so far (we obtain in a way identical to )
Once this is done, we just have to project this into something we can add to the residual stream, and for this we use the weights. This is somewhat interesting too, as these weights are packed as a single matrix but in reality are matrices.
This is why we concatenate all of the heads attention results into a single vector of size , so we can do:
At this point, we can zoom out and continue moving forward in the block as described in the other post (add the projection to the residual stream, and move to the FFN part).
