Better strategy for attention matrix multiplications when generating tokens #218
Merged
Conversation
added 2 commits
February 21, 2025 16:38
to do the attention matrix multiplications in the TG case.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The
K*QandV*softmax(K*Q)matrix multiplications have the shapewhere$K$ is the head size, $N_t$ is the number of tokens in the cache, $N_b$ is the number of tokens in the current batch, $N_k$ is the $N_h$ is the total number of heads. In $N_h$ consecutive matrix multiplications, each being of shape
KorVnumber of heads, andllama.cppthis tensor multiplication has been traditionally performed asThe issue with this is that for token generation (TG) we have$N_b = 1$ , so we are dealing with $N_h$ matrix-vector multiplications, which are notoriously memory bound, and hence limit performance for large cache size (long contexts). To add insult to injury, the stride between consecutive rows in the left matrix is not just the row size $R$ , but rather $N_k R$ , so fetching data from memory is associated with big jumps and sub-optimal cache use, which is not exactly ideal in a memory bound situation.
When$N_h > N_k$ (GQA, in that case $N_h$ is divisible by $N_k$ ), PR #207 changed the multiplication strategy to perform $N_k$ matrix multiplications, each with shape $\left(K x N_t\right) \times \left(K x N_h/N_k\right)$ , thus turning many matrix-vector multiplications into fewer matrix-matrix multiplications. This leads to non negligible performance gains for long contexts.
But when$N_h = N_k$ (e.g., DeepSeek attention architecture), the above does not work. What we could do instead is to perform $N_t x N_h$ dot products, where the inner loop is over $N_h$ and the outer loop is over $N_t$ . When multi-threaded, each thread performs $N_t/M x N_h$ dot products (where $M$ is the number of threads). The advantage of doing this is that memory is accessed consecutively, resulting in better throughput and cache utilization. This is being done with this PR.
To access performance impact, I use DeepSeek-Lite quantized with
IQ1_S. This minimizes the model size, thus allowing to achieve higher tokens per second and hence the size of the KV cache has a stronger impact. Calculations are on a Ryzen-7950X (Zen4), Ryzen-5975WX (AVX2) and M2-Max CPU (NEON). Calculations are without FA so the change in tensor multiplication strategy is invoked. As performance is also influenced by cache size and quantization type (if the cache is quantized), we examinefp16, Q8_0, Q8_KVand, on Zen4,bf16for the K-cache (without FA the V cache cannot be quantized).AVX2
NEON (M2-Max CPU)
Zen4 (Ryzen-7950X)