LLM Engine: KV Cache
From the previous posts, it should be clear(-ish) that to compute attention for a given token, we need to have performed calculations on all previous tokens. Concretely we need to compute their keys () and values (). Computing these values depends only on the token and the model weights, therefore they can be computed once and just referred to in the future. This is what a KV-cache is/does: As we process a token, we generate its and and store them in the cache that we can just read when we need them again.
In terms of implementation, this is a somewhat simple thing to do, the requirements are (a) values need to be stored by head, and position; (b) when read we need to retrieve all the positions stored (this is the "we need and for all previous tokens"); and (c) we need values to be stored in a way that is read-efficient as we write once but read many times.
Let's start by describing the shape, the relevant tensors are: , , and . First, we define a few values for the dimensions:
- : Heads
- : Key-Value heads.
- : Head dimensions.
- : Hidden dimension, essentially the embedding dimensions.
Then how we compute each of them (and their dimensions):
Now, these vectors are the tensors for all heads/kv-heads, so each of them is of this form:
Given this, going forward we'll use , , and to denote the specific tensor for a head/kv-head.
Next, for how they are used, for each token at position we do (note that for simplicity we will use to denote the appropriate kv-head too, see the previous post to see how we get that ;)):
Which is equivalent to:
From this it is clear that, for each token we compute its and we also need the values from every token up to there, so we just store it:
And the retrieve method should give us a shape that we can use in the vectorized version of the computation, so:
Cool, now to the part: the usage of .
Which is a bit of a mouthful, but can be rewritten as:
So, this is a little bit funnier. We have the option of writing the double loop or transpose and use our matvec kernel. Initially I did implement the double loop, but later updated to the other alternative (which is what we'll use here). It's clear that we need all of the old , so we do the same as we did for but we store transposed:
Interface has the same shape, although less efficient to write for a row-aligned matrix (which is what we use), not terribly worrying as we write very few times compared to how many times we read.
And that is actually all there is to it, exactly what you would expect of something that has cache in the name, with the added interesting bit that shape matters.
Next up, we'll talk about floating-point numbers.
The PRs are that are relevant for the implementation of the KV cache are:
