Transformer and KV Cache Basics¶
Build Attention, then Decoder class¶
Since it has been so long since i read the paper "Attention is All You Need", i'm trying to write the code from scratch based on the incomplete impression, attempting to reconstruct the whole and detailed map by trial and error.
The basic idea of above words is just to make all dimensions of layers' input and output match, and finally, a forward computation can be done. To make it work, it definitely cost me much time.
Since i already completed the writing of Decoder before i write these words, so i will directly present the code here.
issues¶
As you can see, it looks very good. And if you create a main.py to give it an input and it will do perfectly as we expected.
But for serious work, there are still things to do:
-
where the model lives.
Since model typically runs on GPU, here's another perspective we should consider in coding.
Look at the_gen_maskmethod:Here, i created a brand new tensor, which won't follow the model
-
data types. Let's just do
model.to(device, torch.bfloat16)when moved to GPU
In this case now, it looks like we have done lot's of things, core components got.
KV Cache¶
I haven't implemented it before by hand.
Let's solve the problem by asking questions:
Conceptually¶
What does the KV cache store and how does it store data?¶
Write the attention formula manually then we will find the formula like this for every query \(q_n\):
And we will find that all \(k_i\) before n can be reused. Honestly, I kept wondering why we don't just store the product of K and V days ago and now I find that I missed softmax function in the middle. So, actually, the KV cache is two separate buffers for storing K and V respectively.
What are decoding and prefill phases?¶
For a decoder-only model, you can't let it start to output from empty tokens. So initially some tokens will be sent first, and based on that, model will start talking. Also, it's the stage for storing KV cache. So the process that first N tokens come up is called prefill, at which the KV cache is computed. Because it's the \(O(N^2)\) process, it's computation-bound, requiring lots of parallel computings like GEMM.
Then after k and v of previous tokens have been got. We will just send maybe one token a time and then use the previous cache, which requires much less computations but one can imagine that there will be so much data transferring between HBM and cache to and from. So it's memory bandwidth bound.
Note that for decoder-only model, we only need the last token's result to evaluate next token.
Engineering¶
-
KV_cache should be moved to GPU as well. Internally, we set k and v tensors to be buffers of pytorch, data of which will then follow with the model
Note that here we pre-allocate some memory for storing kv cache. -
The weights are kept in memory but KV cache will change constantly.
We setup methods for clearing content in kv cache along the composition, from Decoder to Attention and to kv_cache.
The way to clear kv_cache is simply reset the counter:
-
training shouldn't use kv cache. So how to design the class? And gradients are also not required.
Add additional property to judge whether to use KV cache for generation.
Then before KV calculation, ask it first. -
How to separate prefill and decode phases in code? Mask Design Actually, we don't need to separate them, just treating them the same way. And remember to add KV cache by the input length, which is enough.
For the mask, it's a point worth minding because it will no longer be previous square shape.
For multi-token decoding phase, you need to prevent a very small triangular at the right top corner of the score matrix.
-
How do we make sure that the
batchis always matched. Note that there's a dimension of data, very strange and mysterious that we haven't care about it.For a deployed model for inference, I think the
batchsize has been set up initially. And later maybe backends will combine different user requests together into? But will it actually accelerate?Yes, definitely, because if the batch is not filled, then many GPU computing units will just be idle, only memory working hard. So gathering batches helps a lot for throughput.
This is the final code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | |
Another problem is from "GLM 5.3 Flash", it told me i haven't implemented positional embedding, so the "past" K/V are identical because they are simply computed by \(W_k(x)\) and \(W_v(x)\).
AI claim¶
For learning purpose because i think concepts here are important as foundation work, no code is generated by AI, so here may be some design flaws or bugs.