| 1 | # Paged Attention |
| 2 | |
| 3 | !!! warning |
| 4 | This is a historical document based on the [original paper for vLLM](https://arxiv.org/abs/2309.06180). |
| 5 | It no longer describes the code used in vLLM today. |
| 6 | |
| 7 | Currently, vLLM utilizes its own implementation of a multi-head query |
| 8 | attention kernel (`csrc/attention/attention_kernels.cu`). |
| 9 | This kernel is designed to be compatible with |
| 10 | vLLM's paged KV caches, where the key and value cache are stored in |
| 11 | separate blocks (note that this block concept differs from the GPU |
| 12 | thread block. So in a later document, I will refer to vLLM paged |
| 13 | attention block as "block", while refer to GPU thread block as |
| 14 | "thread block"). |
| 15 | |
| 16 | To achieve high performance, this kernel relies on a specially |
| 17 | designed memory layout and access method, specifically when threads |
| 18 | read data from global memory to shared memory. The purpose of this |
| 19 | document is to provide a high-level explanation of the kernel |
| 20 | implementation step by step, aiding those who wish to learn about the |
| 21 | vLLM multi-head query attention kernel. After going through this |
| 22 | document, users will likely have a better understanding and feel easier |
| 23 | to follow the actual implementation. |
| 24 | |
| 25 | Please note that this document may not cover all details, such as how |
| 26 | to calculate the correct index for the corresponding data or the dot |
| 27 | multiplication implementation. However, after reading this document |
| 28 | and becoming familiar with the high-level logic flow, it should be |
| 29 | easier for you to read the actual code and understand the details. |
| 30 | |
| 31 | ## Inputs |
| 32 | |
| 33 | The kernel function takes a list of arguments for the current thread |
| 34 | to perform its assigned work. The three most important arguments are |
| 35 | the input pointers `q`, `k_cache`, and `v_cache`, which point |
| 36 | to query, key, and value data on global memory that need to be read |
| 37 | and processed. The output pointer `out` points to global memory |
| 38 | where the result should be written. These four pointers actually |
| 39 | refer to multidimensional arrays, but each thread only accesses the |
| 40 | portion of data assigned to it. I have omitted all other runtime |
| 41 | parameters here for simplicity. |
| 42 | |
| 43 | ```cpp |
| 44 | template<typename scalar_t, int HEAD_SIZE, int BLOCK_SIZE, int NUM_THREADS, int PARTITION_SIZE = 0> |
| 45 | __device__ void paged_attention_kernel( |
| 46 | ... // Other side args. |
| 47 | const scalar_t* __restrict__ out, // [num_seqs, num_heads, max_num_partitions, head_size] |
| 48 | const scalar_t* __restrict__ q, // [num_seqs, num_heads, head_size] |
| 49 | const scalar_t* __restrict__ k_cache, // [num_blocks, num_kv_heads, head_size/x, block_size, x] |
| 50 | const scalar_t* __restrict__ v_cache, // [num_blocks, num_kv_heads, head_size, block_size] |
| 51 | ... // Other side args. |
| 52 | ) |
| 53 | ``` |
| 54 | |
| 55 | There is also a list of template arguments above the function |
| 56 | signature that are determined during compilation time. `scalar_t` |
| 57 | represents the data type of the query, key, and value data elements, |
| 58 | such as FP16. `HEAD_SIZE` indicates the number of elements in each |
| 59 | head. `BLOCK_SIZE` refers to the number of tokens in each block. |
| 60 | `NUM_THREADS` denotes the number of threads in each thread block. |
| 61 | `PARTITION_SIZE` represents the number of tensor parallel GPUs (For |
| 62 | simplicity, we assume this is 0 and tensor parallel is disabled). |
| 63 | |
| 64 | With these arguments, we need to perform a sequence of preparations. |
| 65 | This includes calculating the current head index, block index, and |
| 66 | other necessary variables. However, for now, we can ignore these |
| 67 | preparations and proceed directly to the actual calculations. It will |
| 68 | be easier to understand them once we grasp the entire flow. |
| 69 | |
| 70 | ## Concepts |
| 71 | |
| 72 | Just before we dive into the calculation flow, I want to describe a |
| 73 | few concepts that are needed for later sections. However, you may |
| 74 | skip this section and return later if you encounter any confusing |
| 75 | terminologies. |
| 76 | |
| 77 | - **Sequence**: A sequence represents a client request. For example, |
| 78 | the data pointed to by `q` has a shape of |
| 79 | `[num_seqs, num_heads, head_size]`. That represents there are total |
| 80 | `num_seqs` of query sequence data are pointed by `q`. Since this |
| 81 | kernel is a single query attention kernel, each sequence only has one |
| 82 | query token. Hence, the `num_seqs` equals the total number of tokens |
| 83 | that are processed in the batch. |
| 84 | - **Context**: The context consists of the generated tokens from the |
| 85 | sequence. For instance, `["What", "is", "your"]` are the context |
| 86 | tokens, and the input query token is `"name"`. The model might |
| 87 | generate the token `"?"`. |
| 88 | - **Vec**: The vec is a list of elements that are fetched and |
| 89 | calculated together. For query and key data, the vec size |
| 90 | (`VEC_SIZE`) is determined so that each thread group can fetch and |
| 91 | calculate 16 bytes of data at a time. For value data, the vec size |
| 92 | (`V_VEC_SIZE`) is determined so that each thread can fetch and |
| 93 | calculate 16 bytes of data at a time. For example, if the |
| 94 | `scalar_t` is FP16 (2 bytes) and `THREAD_GROUP_SIZE` is 2, the |
| 95 | `VEC_SIZE` will be 4, while the `V_VEC_SIZE` will be 8. |
| 96 | - **Thread group**: The thread group is a small group of |
| 97 | threads(`THREAD_GROUP_SIZE`) that fetches and calculates one |
| 98 | query token and one key token at a time. Each thread handles only a |
| 99 | portion of the token data. The total number of elements processed by |
| 100 | one thread group is referred as `x`. For example, if the thread |
| 101 | group contains 2 threads and the head size is 8, then thread 0 |
| 102 | handles the query and key elements at index 0, 2, 4, 6, while thread |
| 103 | 1 handles the elements at index 1, 3, 5, 7. |
| 104 | - **Block**: The key and value cache data in vLLM are split into |
| 105 | blocks. Each block stores data for a fixed number(`BLOCK_SIZE`) |
| 106 | of tokens at one head. Each block may contain only a portion of the |
| 107 | whole context tokens. For example, if the block size is 16 and the |
| 108 | head size is 128, then for one head, one block can store 16 * 128 = |
| 109 | 2048 elements. |
| 110 | - **Warp**: A warp is a group of 32 threads(`WARP_SIZE`) that |
| 111 | execute simultaneously on a stream multiprocessor (SM). In this |
| 112 | kernel, each warp processes the calculation between one query token |
| 113 | and key tokens of one entire block at a time (it may process multiple |
| 114 | blocks in multiple iterations). For example, if there are 4 warps and |
| 115 | 6 blocks for one context, the assignment would be like warp 0 handles |
| 116 | the 0th, 4th blocks, warp 1 handles the 1st, 5th blocks, warp 2 |
| 117 | handles the 2nd block and warp 3 handles the 3rd block. |
| 118 | - **Thread block**: A thread block is a group of |
| 119 | threads(`NUM_THREADS`) that can access the same shared memory. |
| 120 | Each thread block contains multiple warps(`NUM_WARPS`), and in |
| 121 | this kernel, each thread block processes the calculation between one |
| 122 | query token and key tokens of a whole context. |
| 123 | - **Grid**: A grid is a collection of thread blocks and defines the |
| 124 | shape of the collection. In this kernel, the shape is |
| 125 | `(num_heads, num_seqs, max_num_partitions)`. Therefore, each thread |
| 126 | block only handles the calculation for one head, one sequence, and |
| 127 | one partition. |
| 128 | |
| 129 | ## Query |
| 130 | |
| 131 | This section will introduce how query data is stored in memory and |
| 132 | fetched by each thread. As mentioned above, each thread group fetches |
| 133 | one query token data, while each thread itself only handles a part of |
| 134 | one query token data. Within each warp, every thread group will fetch |
| 135 | the same query token data, but will multiply it with different key |
| 136 | token data. |
| 137 | |
| 138 | ```cpp |
| 139 | const scalar_t* q_ptr = q + seq_idx * q_stride + head_idx * HEAD_SIZE; |
| 140 | ``` |
| 141 | |
| 142 |  |
| 143 | |
| 144 | Each thread defines its own `q_ptr` which points to the assigned |
| 145 | query token data on global memory. For example, if `VEC_SIZE` is 4 |
| 146 | and `HEAD_SIZE` is 128, the `q_ptr` points to data that contains |
| 147 | total of 128 elements divided into 128 / 4 = 32 vecs. |
| 148 | |
| 149 |  |
| 150 | |
| 151 | ```cpp |
| 152 | __shared__ Q_vec q_vecs[THREAD_GROUP_SIZE][NUM_VECS_PER_THREAD]; |
| 153 | ``` |
| 154 | |
| 155 | Next, we need to read the global memory data pointed to by `q_ptr` |
| 156 | into shared memory as `q_vecs`. It is important to note that each |
| 157 | vecs is assigned to a different row. For example, if the |
| 158 | `THREAD_GROUP_SIZE` is 2, thread 0 will handle the 0th row vecs, |
| 159 | while thread 1 handles the 1st row vecs. By reading the query data in |
| 160 | this way, neighboring threads like thread 0 and thread 1 can read |
| 161 | neighbor memory, achieving the memory coalescing to improve |
| 162 | performance. |
| 163 | |
| 164 | ## Key |
| 165 | |
| 166 | Similar to the "Query" section, this section introduces memory layout |
| 167 | and assignment for keys. While each thread group only handle one |
| 168 | query token one kernel run, it may handle multiple key tokens across |
| 169 | multiple iterations. Meanwhile, each warp will process multiple blocks |
| 170 | of key tokens in multiple iterations, ensuring that all context |
| 171 | tokens are processed by the entire thread group after the kernel run. |
| 172 | In this context, "handle" refers to performing the dot multiplication |
| 173 | between query data and key data. |
| 174 | |
| 175 | ```cpp |
| 176 | const scalar_t* k_ptr = k_cache + physical_block_number * kv_block_stride |
| 177 | + kv_head_idx * kv_head_stride |
| 178 | + physical_block_offset * x; |
| 179 | ``` |
| 180 | |
| 181 | Unlike `q_ptr`, `k_ptr` in each thread will point to different |
| 182 | key token at different iterations. As shown above, that `k_ptr` |
| 183 | points to key token data based on `k_cache` at assigned block, |
| 184 | assigned head and assigned token. |
| 185 | |
| 186 |  |
| 187 | |
| 188 | The diagram above illustrates the memory layout for key data. It |
| 189 | assumes that the `BLOCK_SIZE` is 16, `HEAD_SIZE` is 128, `x` is |
| 190 | 8, `THREAD_GROUP_SIZE` is 2, and there are a total of 4 warps. Each |
| 191 | rectangle represents all the elements for one key token at one head, |
| 192 | which will be processed by one thread group. The left half shows the |
| 193 | total 16 blocks of key token data for warp 0, while the right half |
| 194 | represents the remaining key token data for other warps or |
| 195 | iterations. Inside each rectangle, there are a total 32 vecs (128 |
| 196 | elements for one token) that will be processed by 2 threads (one |
| 197 | thread group) separately. |
| 198 | |
| 199 |  |
| 200 | |
| 201 | ```cpp |
| 202 | K_vec k_vecs[NUM_VECS_PER_THREAD] |
| 203 | ``` |
| 204 | |
| 205 | Next, we need to read the key token data from `k_ptr` and store |
| 206 | them on register memory as `k_vecs`. We use register memory for |
| 207 | `k_vecs` because it will only be accessed by one thread once, |
| 208 | whereas `q_vecs` will be accessed by multiple threads multiple |
| 209 | times. Each `k_vecs` will contain multiple vectors for later |
| 210 | calculation. Each vec will be set at each inner iteration. The |
| 211 | assignment of vecs allows neighboring threads in a warp to read |
| 212 | neighboring memory together, which again promotes the memory |
| 213 | coalescing. For instance, thread 0 will read vec 0, while thread 1 |
| 214 | will read vec 1. In the next inner loop, thread 0 will read vec 2, |
| 215 | while thread 1 will read vec 3, and so on. |
| 216 | |
| 217 | You may still be a little confused about the overall flow. Don't |
| 218 | worry, please keep reading the next "QK" section. It will illustrate |
| 219 | the query and key calculation flow in a clearer and higher-level |
| 220 | manner. |
| 221 | |
| 222 | ## QK |
| 223 | |
| 224 | As shown the pseudocode below, before the entire for loop block, we |
| 225 | fetch the query data for one token and store it in `q_vecs`. Then, |
| 226 | in the outer for loop, we iterate through different `k_ptrs` that |
| 227 | point to different tokens and prepare the `k_vecs` in the inner for |
| 228 | loop. Finally, we perform the dot multiplication between the |
| 229 | `q_vecs` and each `k_vecs`. |
| 230 | |
| 231 | ```cpp |
| 232 | q_vecs = ... |
| 233 | for ... { |
| 234 | k_ptr = ... |
| 235 | for ... { |
| 236 | k_vecs[i] = ... |
| 237 | } |
| 238 | ... |
| 239 | float qk = scale * Qk_dot<scalar_t, THREAD_GROUP_SIZE>::dot(q_vecs[thread_group_offset], k_vecs); |
| 240 | } |
| 241 | ``` |
| 242 | |
| 243 | As mentioned before, for each thread, it only fetches part of the |
| 244 | query and key token data at a time. However, there will be a cross |
| 245 | thread group reduction happen in the `Qk_dot<>::dot` . So `qk` |
| 246 | returned here is not just between part of the query and key token dot |
| 247 | multiplication, but actually a full result between entire query and |
| 248 | key token data. |
| 249 | |
| 250 | For example, if the value of `HEAD_SIZE` is 128 and |
| 251 | `THREAD_GROUP_SIZE` is 2, each thread's `k_vecs` will contain |
| 252 | total 64 elements. However, the returned `qk` is actually the |
| 253 | result of dot multiplication between 128 query elements and 128 key |
| 254 | elements. If you want to learn more about the details of the dot |
| 255 | multiplication and reduction, you may refer to the implementation of |
| 256 | `Qk_dot<>::dot`. However, for the sake of simplicity, I will not |
| 257 | cover it in this document. |
| 258 | |
| 259 | ## Softmax |
| 260 | |
| 261 | Next, we need to calculate the normalized softmax for all `qk`s, |
| 262 | as shown above, where each $x$ represents a `qk`. To do this, |
| 263 | we must obtain the reduced value of `qk_max`($m(x)$) and |
| 264 | the `exp_sum`($\ell(x)$) of all `qk`s. The reduction |
| 265 | should be performed across the entire thread block, encompassing |
| 266 | results between the query token and all context key tokens. |
| 267 | |
| 268 | $$ |
| 269 | \begin{gather*} |
| 270 | m(x):=\max _i \quad x_i \\ \quad f(x):=\left[\begin{array}{lll}e^{x_1-m(x)} & \ldots & e^{x_B-m(x)}\end{array}\right]\\ \quad \ell(x):=\sum_i f(x)_i \\ |
| 271 | \quad \operatorname{softmax}(x):=\frac{f(x)}{\ell(x)} |
| 272 | \end{gather*} |
| 273 | $$ |
| 274 | |
| 275 | ### `qk_max` and `logits` |
| 276 | |
| 277 | Just right after we get the `qk` result, we can set the temporary |
| 278 | `logits` result with `qk` (In the end, the `logits` should |
| 279 | store the normalized softmax result). Also we can compare and collect |
| 280 | the `qk_max` for all `qk`s that are calculated by current |
| 281 | thread group. |
| 282 | |
| 283 | ```cpp |
| 284 | if (thread_group_offset == 0) { |
| 285 | const bool mask = token_idx >= context_len; |
| 286 | logits[token_idx - start_token_idx] = mask ? 0.f : qk; |
| 287 | qk_max = mask ? qk_max : fmaxf(qk_max, qk); |
| 288 | } |
| 289 | ``` |
| 290 | |
| 291 | Please note that the `logits` here is on shared memory, so each |
| 292 | thread group will set the fields for its own assigned context tokens. |
| 293 | Overall, the size of logits should be number of context tokens. |
| 294 | |
| 295 | ```cpp |
| 296 | for (int mask = WARP_SIZE / 2; mask >= THREAD_GROUP_SIZE; mask /= 2) { |
| 297 | qk_max = fmaxf(qk_max, VLLM_SHFL_XOR_SYNC(qk_max, mask)); |
| 298 | } |
| 299 | |
| 300 | if (lane == 0) { |
| 301 | red_smem[warp_idx] = qk_max; |
| 302 | } |
| 303 | ``` |
| 304 | |
| 305 | Then we need to get the reduced `qk_max` across each warp. The main |
| 306 | idea is to make threads in warp to communicate with each other and |
| 307 | get the final max `qk` . |
| 308 | |
| 309 | ```cpp |
| 310 | for (int mask = NUM_WARPS / 2; mask >= 1; mask /= 2) { |
| 311 | qk_max = fmaxf(qk_max, VLLM_SHFL_XOR_SYNC(qk_max, mask)); |
| 312 | } |
| 313 | qk_max = VLLM_SHFL_SYNC(qk_max, 0); |
| 314 | ``` |
| 315 | |
| 316 | Finally, we can get the reduced `qk_max` from whole thread block by |
| 317 | compare the `qk_max` from all warps in this thread block. Then we |
| 318 | need to broadcast the final result to each thread. |
| 319 | |
| 320 | ### `exp_sum` |
| 321 | |
| 322 | Similar to `qk_max`, we need to get the reduced sum value from the |
| 323 | entire thread block too. |
| 324 | |
| 325 | ```cpp |
| 326 | for (int i = thread_idx; i < num_tokens; i += NUM_THREADS) { |
| 327 | float val = __expf(logits[i] - qk_max); |
| 328 | logits[i] = val; |
| 329 | exp_sum += val; |
| 330 | } |
| 331 | ... |
| 332 | exp_sum = block_sum<NUM_WARPS>(&red_smem[NUM_WARPS], exp_sum); |
| 333 | ``` |
| 334 | |
| 335 | Firstly, sum all exp values from each thread group, and meanwhile, |
| 336 | convert each entry of `logits` from `qk` to `exp(qk - qk_max)`. |
| 337 | Please note, the `qk_max` here is already the max `qk` across the |
| 338 | whole thread block. And then we can do reduction for `exp_sum` |
| 339 | across whole thread block just like the `qk_max`. |
| 340 | |
| 341 | ```cpp |
| 342 | const float inv_sum = __fdividef(1.f, exp_sum + 1e-6f); |
| 343 | for (int i = thread_idx; i < num_tokens; i += NUM_THREADS) { |
| 344 | logits[i] *= inv_sum; |
| 345 | } |
| 346 | ``` |
| 347 | |
| 348 | Finally, with the reduced `qk_max` and `exp_sum`, we can obtain |
| 349 | the final normalized softmax result as `logits`. This `logits` |
| 350 | variable will be used for dot multiplication with the value data in |
| 351 | later steps. Now, it should store the normalized softmax result of |
| 352 | `qk` for all assigned context tokens. |
| 353 | |
| 354 | ## Value |
| 355 | |
| 356 |  |
| 357 | |
| 358 |  |
| 359 | |
| 360 |  |
| 361 | |
| 362 | Now we need to retrieve the value data and perform dot multiplication |
| 363 | with `logits`. Unlike query and key, there is no thread group |
| 364 | concept for value data. As shown in diagram, different from key token |
| 365 | memory layout, elements from the same column correspond to the same |
| 366 | value token. For one block of value data, there are `HEAD_SIZE` of |
| 367 | rows and `BLOCK_SIZE` of columns that are split into multiple |
| 368 | `v_vecs`. |
| 369 | |
| 370 | Each thread always fetches `V_VEC_SIZE` elements from the same |
| 371 | `V_VEC_SIZE` of tokens at a time. As a result, a single thread |
| 372 | retrieves multiple `v_vec`s from different rows and the same |
| 373 | columns through multiple inner iterations. For each `v_vec`, it |
| 374 | needs to be dot multiplied with the corresponding `logits_vec`, |
| 375 | which is also `V_VEC_SIZE` elements from `logits`. Overall, with |
| 376 | multiple inner iterations, each warp will process one block of value |
| 377 | tokens. And with multiple outer iterations, the whole context value |
| 378 | tokens are processed |
| 379 | |
| 380 | ```cpp |
| 381 | float accs[NUM_ROWS_PER_THREAD]; |
| 382 | for ... { // Iteration over different blocks. |
| 383 | logits_vec = ... |
| 384 | for ... { // Iteration over different rows. |
| 385 | v_vec = ... |
| 386 | ... |
| 387 | accs[i] += dot(logits_vec, v_vec); |
| 388 | } |
| 389 | } |
| 390 | ``` |
| 391 | |
| 392 | As shown in the above pseudocode, in the outer loop, similar to |
| 393 | `k_ptr`, `logits_vec` iterates over different blocks and reads |
| 394 | `V_VEC_SIZE` elements from `logits`. In the inner loop, each |
| 395 | thread reads `V_VEC_SIZE` elements from the same tokens as a |
| 396 | `v_vec` and performs dot multiplication. It is important to note |
| 397 | that in each inner iteration, the thread fetches different head |
| 398 | position elements for the same tokens. The dot result is then |
| 399 | accumulated in `accs`. Therefore, each entry of `accs` is mapped |
| 400 | to a head position assigned to the current thread. |
| 401 | |
| 402 | For example, if `BLOCK_SIZE` is 16 and `V_VEC_SIZE` is 8, each |
| 403 | thread fetches 8 value elements for 8 tokens at a time. Each element |
| 404 | is from different tokens at the same head position. If `HEAD_SIZE` |
| 405 | is 128 and `WARP_SIZE` is 32, for each inner loop, a warp needs to |
| 406 | fetch `WARP_SIZE * V_VEC_SIZE = 256` elements. This means there are |
| 407 | a total of 128 * 16 / 256 = 8 inner iterations for a warp to handle |
| 408 | a whole block of value tokens. And each `accs` in each thread |
| 409 | contains 8 elements that accumulated at 8 different head positions. |
| 410 | For the thread 0, the `accs` variable will have 8 elements, which |
| 411 | are 0th, 32nd … 224th elements of a value head that are accumulated |
| 412 | from all assigned 8 tokens. |
| 413 | |
| 414 | ## LV |
| 415 | |
| 416 | Now, we need to perform reduction for `accs` within each warp. This |
| 417 | process allows each thread to accumulate the `accs` for the |
| 418 | assigned head positions of all tokens in one block. |
| 419 | |
| 420 | ```cpp |
| 421 | for (int i = 0; i < NUM_ROWS_PER_THREAD; i++) { |
| 422 | float acc = accs[i]; |
| 423 | for (int mask = NUM_V_VECS_PER_ROW / 2; mask >= 1; mask /= 2) { |
| 424 | acc += VLLM_SHFL_XOR_SYNC(acc, mask); |
| 425 | } |
| 426 | accs[i] = acc; |
| 427 | } |
| 428 | ``` |
| 429 | |
| 430 | Next, we perform reduction for `accs` across all warps, allowing |
| 431 | each thread to have the accumulation of `accs` for the assigned |
| 432 | head positions of all context tokens. Please note that each `accs` |
| 433 | in every thread only stores the accumulation for a portion of |
| 434 | elements of the entire head for all context tokens. However, overall, |
| 435 | all results for output have been calculated but are just stored in |
| 436 | different thread register memory. |
| 437 | |
| 438 | ??? code |
| 439 | |
| 440 | ```cpp |
| 441 | float* out_smem = reinterpret_cast<float*>(shared_mem); |
| 442 | for (int i = NUM_WARPS; i > 1; i /= 2) { |
| 443 | // Upper warps write to shared memory. |
| 444 | ... |
| 445 | float* dst = &out_smem[(warp_idx - mid) * HEAD_SIZE]; |
| 446 | for (int i = 0; i < NUM_ROWS_PER_THREAD; i++) { |
| 447 | ... |
| 448 | dst[row_idx] = accs[i]; |
| 449 | } |
| 450 | |
| 451 | // Lower warps update the output. |
| 452 | const float* src = &out_smem[warp_idx * HEAD_SIZE]; |
| 453 | for (int i = 0; i < NUM_ROWS_PER_THREAD; i++) { |
| 454 | ... |
| 455 | accs[i] += src[row_idx]; |
| 456 | } |
| 457 | |
| 458 | // Write out the accs. |
| 459 | } |
| 460 | ``` |
| 461 | |
| 462 | ## Output |
| 463 | |
| 464 | Now we can write all of calculated result from local register memory |
| 465 | to final output global memory. |
| 466 | |
| 467 | ```cpp |
| 468 | scalar_t* out_ptr = out + seq_idx * num_heads * max_num_partitions * HEAD_SIZE |
| 469 | + head_idx * max_num_partitions * HEAD_SIZE |
| 470 | + partition_idx * HEAD_SIZE; |
| 471 | ``` |
| 472 | |
| 473 | First, we need to define the `out_ptr` variable, which points to |
| 474 | the start address of the assigned sequence and assigned head. |
| 475 | |
| 476 | ```cpp |
| 477 | for (int i = 0; i < NUM_ROWS_PER_THREAD; i++) { |
| 478 | const int row_idx = lane / NUM_V_VECS_PER_ROW + i * NUM_ROWS_PER_ITER; |
| 479 | if (row_idx < HEAD_SIZE && lane % NUM_V_VECS_PER_ROW == 0) { |
| 480 | from_float(*(out_ptr + row_idx), accs[i]); |
| 481 | } |
| 482 | } |
| 483 | ``` |
| 484 | |
| 485 | Finally, we need to iterate over different assigned head positions |
| 486 | and write out the corresponding accumulated result based on the |
| 487 | `out_ptr`. |
| 488 | |
| 489 | ## Citation |
| 490 | |
| 491 | ```bibtex |
| 492 | @inproceedings{kwon2023efficient, |
| 493 | title={Efficient Memory Management for Large Language Model Serving with PagedAttention}, |
| 494 | author={Woosuk Kwon and Zhuohan Li and Siyuan Zhuang and Ying Sheng and Lianmin Zheng and Cody Hao Yu and Joseph E. Gonzalez and Hao Zhang and Ion Stoica}, |
| 495 | booktitle={Proceedings of the ACM SIGOPS 29th Symposium on Operating Systems Principles}, |
| 496 | year={2023} |
| 497 | } |
| 498 | ``` |