|
| 1 | +#pragma once |
| 2 | + |
| 3 | +// BlockPool — physical block allocation pool for the paged tier refactor. |
| 4 | +// |
| 5 | +// Design adapted from vLLM (Apache 2.0, vllm/v1/core/block_pool.py + |
| 6 | +// vllm/v1/worker/block_table.py). Code is independent (C++ vs Python), |
| 7 | +// architecture is theirs. See docs/memory-tier/CREDITS.md when this |
| 8 | +// lands as a complete subsystem. |
| 9 | +// |
| 10 | +// A "physical block" is a fixed-size chunk of KV cache storage capable |
| 11 | +// of holding `block_size` tokens worth of K/V for one layer. Blocks are |
| 12 | +// allocated from one of two pools — GPU (hot tier) or CPU (warm tier) — |
| 13 | +// distinguished by their ID range: |
| 14 | +// |
| 15 | +// [0, total_gpu_blocks) GPU blocks |
| 16 | +// [total_gpu_blocks, total_gpu_blocks + total_cpu_blocks) CPU blocks |
| 17 | +// |
| 18 | +// Cold tier (NVMe) is handled outside this pool — KvtcStore writes by |
| 19 | +// (layer, position) and doesn't need a physical block ID. |
| 20 | +// |
| 21 | +// Allocation is LIFO from a free stack; cache-line reuse on freshly- |
| 22 | +// freed blocks helps decode locality. Watermark gating reserves a small |
| 23 | +// fraction of free blocks against admission spikes so the scheduler |
| 24 | +// never starves itself by accepting one more request than it can |
| 25 | +// retire. |
| 26 | +// |
| 27 | +// PHASE 1: This is a pure data structure with no live wiring. The |
| 28 | +// existing position-keyed warm_pos_to_slot_ / cold_positions_ paths |
| 29 | +// in mt-tiered.cpp continue to work. PHASE 2 wires this in behind a |
| 30 | +// feature flag; PHASE 3 makes it the only path. |
| 31 | +// |
| 32 | +// Single-threaded — caller must serialize. mt:: is single-seq today |
| 33 | +// and the scheduler runs single-threaded, so no internal locking. |
| 34 | + |
| 35 | +#include <cstddef> |
| 36 | +#include <cstdint> |
| 37 | +#include <vector> |
| 38 | + |
| 39 | +namespace mt { |
| 40 | + |
| 41 | +// Sentinel for "no block." Chosen as ~0u so it sticks out in logs and |
| 42 | +// any accidental use as an array index will explode loudly. |
| 43 | +static constexpr uint32_t kInvalidBlockId = ~0u; |
| 44 | + |
| 45 | +class BlockPool { |
| 46 | +public: |
| 47 | + // Initialize the pool with `n_gpu` GPU blocks and `n_cpu` CPU blocks. |
| 48 | + // `watermark` is the fraction (0.0..1.0) of each pool kept in reserve |
| 49 | + // before has_free_*_blocks() returns true for new admissions. 0.0 |
| 50 | + // disables the watermark; vLLM defaults to ~0.05 (5%). |
| 51 | + void init(uint32_t n_gpu, uint32_t n_cpu, float watermark); |
| 52 | + |
| 53 | + // Allocate one physical block from the GPU pool. Returns |
| 54 | + // kInvalidBlockId when the pool is exhausted (caller should evict |
| 55 | + // first). Watermark is NOT enforced here — callers that care about |
| 56 | + // admission control should consult has_free_gpu_blocks() up-front. |
| 57 | + // alloc_*() always allocates if anything is free. |
| 58 | + uint32_t alloc_gpu(); |
| 59 | + uint32_t alloc_cpu(); |
| 60 | + |
| 61 | + // Return a block to its pool. Idempotent on already-free blocks |
| 62 | + // (logs a warning); double-free does NOT corrupt the pool. Asserts |
| 63 | + // on out-of-range IDs. |
| 64 | + void free_block(uint32_t block_id); |
| 65 | + |
| 66 | + // True if `block_id` is a GPU block (vs CPU). Inferred from the ID |
| 67 | + // range — no per-block bool stored. |
| 68 | + bool is_gpu(uint32_t block_id) const; |
| 69 | + |
| 70 | + // Pool size queries. n_free_*() is the actual count of free blocks |
| 71 | + // (ignores watermark); has_free_*_blocks(N) tests whether N blocks |
| 72 | + // can be allocated WITHOUT crossing the watermark reserve. |
| 73 | + size_t n_free_gpu() const; |
| 74 | + size_t n_free_cpu() const; |
| 75 | + bool has_free_gpu_blocks(uint32_t n) const; |
| 76 | + bool has_free_cpu_blocks(uint32_t n) const; |
| 77 | + |
| 78 | + uint32_t total_gpu_blocks() const { return total_gpu_blocks_; } |
| 79 | + uint32_t total_cpu_blocks() const { return total_cpu_blocks_; } |
| 80 | + |
| 81 | + // Reset to all-free state. Used by whole-cache wipes (mt::clear()). |
| 82 | + // Block IDs are NOT renumbered; existing block-table entries that |
| 83 | + // pointed into the freed range become dangling and the caller must |
| 84 | + // also clear those before next use. |
| 85 | + void reset(); |
| 86 | + |
| 87 | +private: |
| 88 | + // Free stacks (LIFO). gpu_free_ holds IDs in [0, total_gpu_blocks_), |
| 89 | + // cpu_free_ holds IDs in [total_gpu_blocks_, total_gpu_blocks_ + |
| 90 | + // total_cpu_blocks_). Stack push/pop is the eviction order under |
| 91 | + // pure LRU; smarter eviction policies layer above. |
| 92 | + std::vector<uint32_t> gpu_free_; |
| 93 | + std::vector<uint32_t> cpu_free_; |
| 94 | + |
| 95 | + uint32_t total_gpu_blocks_ = 0; |
| 96 | + uint32_t total_cpu_blocks_ = 0; |
| 97 | + uint32_t watermark_gpu_ = 0; // reserve count, NOT a fraction |
| 98 | + uint32_t watermark_cpu_ = 0; |
| 99 | +}; |
| 100 | + |
| 101 | +} // namespace mt |
0 commit comments