| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| #include <hip/hip_runtime.h> |
| #include <atomic> |
| #include <vector> |
| #include <cassert> |
| #include <cstdio> |
| #include <cstdlib> |
| #include <cstring> |
|
|
| |
| |
| |
| constexpr size_t WARP_SIZE = 64; |
| constexpr size_t CACHE_LINE_BYTES = 128; |
|
|
| |
| |
| |
| |
| constexpr size_t KV_BLOCK_BYTES = 256; |
| constexpr size_t TOKENS_PER_BLOCK = 16; |
|
|
| |
| constexpr size_t PROD_KV_BLOCK_BYTES = 32768; |
| constexpr size_t PROD_TOKENS_PER_BLOCK = 16; |
|
|
| |
| |
| |
|
|
| struct BlockTableEntry { |
| uint64_t physical_base; |
| std::atomic<uint16_t> refcount; |
|
|
| BlockTableEntry() : physical_base(0), refcount(0) {} |
| BlockTableEntry(uint64_t base, uint16_t rc) : physical_base(base), refcount(rc) {} |
| }; |
|
|
| |
| class BlockAllocator { |
| private: |
| std::vector<uint64_t> free_list; |
| std::atomic<size_t> free_idx{0}; |
| size_t total_blocks; |
|
|
| public: |
| BlockAllocator(size_t num_blocks, uint64_t base_address = 0x10000000) |
| : total_blocks(num_blocks) { |
| free_list.resize(num_blocks); |
| |
| for (size_t i = 0; i < num_blocks; ++i) { |
| free_list[i] = base_address + i * KV_BLOCK_BYTES; |
| } |
| free_idx.store(num_blocks, std::memory_order_relaxed); |
| } |
|
|
| |
| uint64_t allocate() { |
| size_t idx = free_idx.fetch_sub(1, std::memory_order_acquire); |
| if (idx == 0) { |
| free_idx.store(0, std::memory_order_relaxed); |
| return 0; |
| } |
| return free_list[idx - 1]; |
| } |
|
|
| |
| void deallocate(uint64_t block_base) { |
| size_t idx = free_idx.fetch_add(1, std::memory_order_release); |
| if (idx < free_list.size()) { |
| free_list[idx] = block_base; |
| } |
| } |
|
|
| size_t free_count() const { |
| return free_idx.load(std::memory_order_relaxed); |
| } |
| }; |
|
|
| |
| |
| |
|
|
| class PagedAttentionManager { |
| private: |
| BlockAllocator allocator; |
| std::vector<uint64_t> block_table_ptrs; |
| std::vector<uint32_t> seq_lengths; |
| size_t max_batch_size; |
| size_t max_blocks_per_seq; |
|
|
| public: |
| PagedAttentionManager(size_t max_batch, size_t max_blocks, size_t total_physical_blocks) |
| : allocator(total_physical_blocks), |
| max_batch_size(max_batch), |
| max_blocks_per_seq(max_blocks) { |
| block_table_ptrs.resize(max_batch, 0); |
| seq_lengths.resize(max_batch, 0); |
| } |
|
|
| |
| int allocate_block(size_t seq_id, size_t block_index) { |
| assert(seq_id < max_batch_size); |
| assert(block_index < max_blocks_per_seq); |
|
|
| uint64_t block_base = allocator.allocate(); |
| if (block_base == 0) return -1; |
|
|
| |
| |
| printf("[Allocator] Block allocated: seq=%zu block=%zu -> 0x%lx (free=%zu)\n", |
| seq_id, block_index, block_base, allocator.free_count()); |
| return 0; |
| } |
|
|
| |
| void release_block(size_t seq_id, size_t block_index, uint16_t old_refcount) { |
| if (old_refcount <= 1) { |
| |
| printf("[Allocator] Block freed: seq=%zu block=%zu\n", seq_id, block_index); |
| |
| } else { |
| printf("[Allocator] Block refcount decremented: seq=%zu block=%zu refcount=%u\n", |
| seq_id, block_index, old_refcount - 1); |
| } |
| } |
|
|
| |
| void share_block(size_t src_seq, size_t src_block, |
| size_t dst_seq, size_t dst_block) { |
| printf("[Allocator] Block shared: seq%zu:block%zu -> seq%zu:block%zu\n", |
| src_seq, src_block, dst_seq, dst_block); |
| |
| } |
|
|
| |
| void set_seq_length(size_t seq_id, uint32_t len) { |
| if (seq_id < max_batch_size) { |
| seq_lengths[seq_id] = len; |
| } |
| } |
|
|
| uint32_t get_seq_length(size_t seq_id) const { |
| return (seq_id < max_batch_size) ? seq_lengths[seq_id] : 0; |
| } |
|
|
| size_t get_free_blocks() const { |
| return allocator.free_count(); |
| } |
| }; |
|
|
| |
| |
| |
|
|
| __device__ __forceinline__ uint64_t resolve_kv_address( |
| const BlockTableEntry* block_table, |
| uint32_t token_pos, |
| uint32_t tokens_per_block, |
| uint32_t bytes_per_token |
| ) { |
| |
| uint32_t block_idx = token_pos / tokens_per_block; |
| uint32_t offset_in_block = (token_pos % tokens_per_block) * bytes_per_token; |
|
|
| |
| BlockTableEntry entry = block_table[block_idx]; |
|
|
| |
| if (entry.physical_base & 0x1ULL) { |
| uint64_t cpu_base = entry.physical_base & ~0x1ULL; |
| return cpu_base + offset_in_block; |
| } |
|
|
| return entry.physical_base + offset_in_block; |
| } |
|
|
| |
| |
| |
|
|
| __global__ void paged_attention_kernel( |
| const float* __restrict__ Q, |
| const float* __restrict__ K_cache, |
| const float* __restrict__ V_cache, |
| const BlockTableEntry* __restrict__ block_tables, |
| const uint32_t* __restrict__ seq_lens, |
| float* __restrict__ output, |
| int batch_size, |
| int max_seq_len, |
| int num_heads, |
| int head_dim, |
| uint32_t tokens_per_block, |
| uint32_t bytes_per_token |
| ) { |
| const int tid = threadIdx.x; |
| const int batch_idx = blockIdx.y; |
| const int token_pos = blockIdx.x * blockDim.x + tid; |
|
|
| if (batch_idx >= batch_size || token_pos >= seq_lens[batch_idx]) return; |
|
|
| |
| const BlockTableEntry* block_table = &block_tables[batch_idx * 64]; |
|
|
| |
| float acc[1] = {0.0f}; |
|
|
| for (int kv_pos = 0; kv_pos <= token_pos; ++kv_pos) { |
| |
| uint64_t k_addr = resolve_kv_address( |
| block_table, kv_pos, tokens_per_block, bytes_per_token |
| ); |
|
|
| |
| float k_val = *reinterpret_cast<const float*>(k_addr); |
| float q_val = Q[batch_idx * max_seq_len * num_heads * head_dim + |
| token_pos * num_heads * head_dim + |
| tid % num_heads * head_dim]; |
|
|
| |
| acc[0] += q_val * k_val / sqrtf((float)head_dim); |
| } |
|
|
| |
| output[batch_idx * max_seq_len + token_pos] = acc[0]; |
| } |
|
|
| |
| |
| |
|
|
| struct BenchmarkResult { |
| double fragmentation_ratio; |
| double memory_utilization; |
| size_t blocks_allocated; |
| size_t blocks_used; |
| size_t contiguous_blocks_baseline; |
| }; |
|
|
| BenchmarkResult measure_fragmentation( |
| const std::vector<size_t>& seq_lens, |
| size_t max_batch, |
| size_t tokens_per_block |
| ) { |
| BenchmarkResult result; |
|
|
| size_t total_allocated = 0; |
| size_t total_used = 0; |
|
|
| for (size_t len : seq_lens) { |
| size_t blocks_needed = (len + tokens_per_block - 1) / tokens_per_block; |
| total_allocated += blocks_needed; |
| total_used += blocks_needed; |
| } |
|
|
| |
| size_t max_seq_len = 0; |
| for (size_t len : seq_lens) { |
| if (len > max_seq_len) max_seq_len = len; |
| } |
| size_t contiguous_blocks = max_batch * ((max_seq_len + tokens_per_block - 1) / tokens_per_block); |
|
|
| result.blocks_allocated = total_allocated; |
| result.blocks_used = total_used; |
| result.fragmentation_ratio = 1.0 - (double)total_used / total_allocated; |
| result.memory_utilization = (double)total_used / contiguous_blocks; |
| result.contiguous_blocks_baseline = contiguous_blocks; |
|
|
| return result; |
| } |
|
|
| |
| |
| |
|
|
| void run_paged_attention_test() { |
| printf("=== PagedAttention KV Cache Manager ===\n\n"); |
|
|
| |
| constexpr size_t MAX_BATCH = 8; |
| constexpr size_t MAX_BLOCKS_PER_SEQ = 64; |
| constexpr size_t TOTAL_PHYSICAL_BLOCKS = 512; |
|
|
| PagedAttentionManager manager(MAX_BATCH, MAX_BLOCKS_PER_SEQ, TOTAL_PHYSICAL_BLOCKS); |
|
|
| |
| manager.allocate_block(0, 0); |
| manager.allocate_block(0, 1); |
| manager.allocate_block(0, 2); |
|
|
| |
| manager.allocate_block(1, 0); |
| manager.allocate_block(1, 1); |
|
|
| |
| manager.share_block(0, 0, 1, 0); |
|
|
| |
| manager.release_block(0, 0, 2); |
|
|
| printf("\nFree blocks remaining: %zu\n\n", manager.get_free_blocks()); |
|
|
| |
| printf("=== Fragmentation Analysis ===\n\n"); |
|
|
| |
| std::vector<size_t> sharegpt_lens = { |
| 16, 16, 16, 16, 16, |
| 128, 128, 128, |
| 1024, 1024 |
| }; |
|
|
| BenchmarkResult paged = measure_fragmentation(sharegpt_lens, MAX_BATCH, TOKENS_PER_BLOCK); |
|
|
| printf("PagedAttention:\n"); |
| printf(" Blocks allocated: %zu\n", paged.blocks_allocated); |
| printf(" Blocks used: %zu\n", paged.blocks_used); |
| printf(" Fragmentation: %.1f%%\n", paged.fragmentation_ratio * 100); |
| printf(" Memory utilization: %.1f%%\n\n", paged.memory_utilization * 100); |
|
|
| printf("Contiguous baseline:\n"); |
| printf(" Blocks allocated: %zu\n", paged.contiguous_blocks_baseline); |
| printf(" Fragmentation: %.1f%%\n", (1.0 - (double)paged.blocks_used / paged.contiguous_blocks_baseline) * 100); |
| printf(" Memory savings: %.1f%%\n\n", |
| (1.0 - (double)paged.blocks_allocated / paged.contiguous_blocks_baseline) * 100); |
|
|
| |
| printf("=== Address Translation Demo ===\n\n"); |
| printf("Schema: root_table(1, 100) -> block_table_entry(100, 0, 0x10000000, 2)\n"); |
| printf(" block_table_entry(100, 1, 0x20000000, 1)\n"); |
| printf(" block_table_entry(100, 2, 0x40000000, 1)\n\n"); |
|
|
| |
| struct { |
| size_t seq_id; |
| size_t token_pos; |
| size_t block_idx; |
| size_t offset; |
| uint64_t expected_addr; |
| const char* note; |
| } test_tokens[] = { |
| {1, 0, 0, 0, 0x10000000, "Block 0, offset 0"}, |
| {1, 15, 0, 15, 0x1000000F, "Block 0, offset 15"}, |
| {1, 16, 1, 0, 0x20000000, "Block 1, offset 0"}, |
| {1, 31, 1, 15, 0x2000000F, "Block 1, offset 15"}, |
| {1, 32, 2, 0, 0x40000000, "Block 2, offset 0 (would be swapped)"}, |
| {2, 0, 0, 0, 0x10000000, "Shares block 0 with seq1 (refcount=2)"}, |
| }; |
|
|
| for (const auto& t : test_tokens) { |
| printf(" virtual_token(%zu, %zu, %zu, %zu) -> 0x%lx %s\n", |
| t.seq_id, t.token_pos, t.block_idx, t.offset, |
| t.expected_addr, t.note); |
| } |
|
|
| printf("\n=== Done ===\n"); |
| } |
|
|
| |
| |
| |
|
|
| int main() { |
| run_paged_attention_test(); |
| return 0; |
| } |
|
|