code wiki / (root) / nx_kvcache.nx

nx_kvcache.nx source

↩ module page · 273 lines · 9562 B

1// nx_kvcache.nx -- sovereign PAGED KV-CACHE (census OPPORTUNITY axis 2// "KV-cache / paged attention", momentum 3; vLLM-class, Kwon 2023). 3// 4// The contiguous NxF32KVCache allocates max_seq_len rows per sequence up 5// front: fragmentation, no sharing, one sequence per buffer. Paging fixes 6// all three: the cache is a POOL of fixed-size BLOCKS (BS tokens); a 7// sequence is a PAGE TABLE of block ids; blocks allocate on demand and 8// refcount for sharing. 9// 10// KEY STRUCTURAL FACT exploited here: decode is APPEND-ONLY -- past rows 11// are never mutated -- so full blocks are IMMUTABLE and can be shared 12// forever; only the PARTIAL TAIL block ever needs copy-on-append. fork() 13// is therefore O(page-table) + refcounts: prefill a prompt ONCE, fork N 14// sequences that share the prompt blocks (the self-consistency / best-of-N 15// serving pattern), each fork copies at most ONE partial block on its 16// first append. 17// 18// Pool layout (f32 bits in i64 slots, engine convention): 19// K_pool/V_pool: [n_blocks][n_layers][BS][kv_dim] 20// row t of seq -> block pt[t / BS], offset t % BS. 21// Fail-fast: pool exhaustion returns an error id (-1) -- NO silent 22// fallback to contiguous. 23// 24// genealogy_id: kwon_2023_pagedattention 25// lineage_id: substrate_paged_kv_v1 26 27import "nx_syscalls.nx" 28import "nx_tier.nx" 29 30const NX_PKV_BS: nx_int = 16 // tokens per block 31 32const NX_PKV_OK: nx_int = 0 33const NX_PKV_ERR_NULL: nx_int = 1 34const NX_PKV_ERR_BAD_DIM: nx_int = 2 35const NX_PKV_ERR_EXHAUST: nx_int = 3 36const NX_PKV_ERR_BOUNDS: nx_int = 4 37 38// ===== Pool ========================================================= 39 40struct NxPagedPool { 41 n_blocks: nx_int, 42 n_layers: nx_int, 43 kv_dim: nx_int, 44 K_pool: *i64, // [n_blocks][n_layers][BS][kv_dim] 45 V_pool: *i64, 46 refcnt: *i64, // per block; 0 = free 47 free_st: *i64, // stack of free block ids 48 n_free: i64 49} 50 51const NX_PKV_POOL_BYTES: nx_int = 64 52 53func nx_pkv_pool_new(n_blocks: nx_int, n_layers: nx_int, 54 kv_dim: nx_int) -> *NxPagedPool { 55 if n_blocks <= 0 { return 0 as *NxPagedPool } 56 if n_layers <= 0 { return 0 as *NxPagedPool } 57 if kv_dim <= 0 { return 0 as *NxPagedPool } 58 let p: *NxPagedPool = sys_mmap(NX_PKV_POOL_BYTES) as *NxPagedPool 59 p.n_blocks = n_blocks 60 p.n_layers = n_layers 61 p.kv_dim = kv_dim 62 let blk_elems: i64 = n_layers * NX_PKV_BS * kv_dim 63 p.K_pool = sys_mmap(n_blocks * blk_elems * 8) as *i64 64 p.V_pool = sys_mmap(n_blocks * blk_elems * 8) as *i64 65 p.refcnt = sys_mmap(n_blocks * 8) as *i64 66 p.free_st = sys_mmap(n_blocks * 8) as *i64 67 // free stack: push descending so ids allocate 0,1,2,... (readable KATs) 68 var i: nx_int = 0 69 while i < n_blocks { 70 p.free_st[i] = (n_blocks - 1 - i) as i64 71 i = i + 1 72 } 73 p.n_free = n_blocks as i64 74 return p 75} 76 77func nx_pkv_alloc_block(p: *NxPagedPool) -> i64 { 78 if p.n_free <= 0 { return 0 - 1 } // fail-fast: exhausted 79 p.n_free = p.n_free - 1 80 let b: i64 = p.free_st[p.n_free] 81 p.refcnt[b] = 1 82 return b 83} 84 85func nx_pkv_ref_block(p: *NxPagedPool, b: i64) -> i64 { 86 p.refcnt[b] = p.refcnt[b] + 1 87 return 0 88} 89 90func nx_pkv_unref_block(p: *NxPagedPool, b: i64) -> i64 { 91 p.refcnt[b] = p.refcnt[b] - 1 92 if p.refcnt[b] <= 0 { 93 p.refcnt[b] = 0 94 p.free_st[p.n_free] = b 95 p.n_free = p.n_free + 1 96 } 97 return 0 98} 99 100// element index of (block b, layer L, row r, dim 0) 101func _pkv_base(p: *NxPagedPool, b: i64, L: nx_int, r: nx_int) -> i64 { 102 return ((b * p.n_layers + L) * NX_PKV_BS + r) * p.kv_dim 103} 104 105// ===== Sequence (page table) ======================================== 106 107struct NxPagedSeq { 108 pool: *NxPagedPool, 109 pt: *i64, // page table: block ids 110 max_pt: nx_int, 111 n_pt: nx_int, 112 seq_len: nx_int 113} 114 115const NX_PKV_SEQ_BYTES: nx_int = 48 116 117func nx_pkv_seq_new(p: *NxPagedPool, max_tokens: nx_int) -> *NxPagedSeq { 118 if p == (0 as *NxPagedPool) { return 0 as *NxPagedSeq } 119 let s: *NxPagedSeq = sys_mmap(NX_PKV_SEQ_BYTES) as *NxPagedSeq 120 let mp: nx_int = (max_tokens + NX_PKV_BS - 1) / NX_PKV_BS + 1 121 s.pool = p 122 s.pt = sys_mmap(mp * 8) as *i64 123 s.max_pt = mp 124 s.n_pt = 0 125 s.seq_len = 0 126 return s 127} 128 129// fork: share every block (refcount++), same seq_len. O(page table). 130func nx_pkv_seq_fork(src: *NxPagedSeq) -> *NxPagedSeq { 131 if src == (0 as *NxPagedSeq) { return 0 as *NxPagedSeq } 132 let p: *NxPagedPool = src.pool 133 let s: *NxPagedSeq = sys_mmap(NX_PKV_SEQ_BYTES) as *NxPagedSeq 134 s.pool = p 135 s.pt = sys_mmap(src.max_pt * 8) as *i64 136 s.max_pt = src.max_pt 137 s.n_pt = src.n_pt 138 s.seq_len = src.seq_len 139 var i: nx_int = 0 140 while i < src.n_pt { 141 s.pt[i] = src.pt[i] 142 nx_pkv_ref_block(p, src.pt[i]) 143 i = i + 1 144 } 145 return s 146} 147 148func nx_pkv_seq_free(s: *NxPagedSeq) -> i64 { 149 if s == (0 as *NxPagedSeq) { return 0 } 150 var i: nx_int = 0 151 while i < s.n_pt { nx_pkv_unref_block(s.pool, s.pt[i]); i = i + 1 } 152 s.n_pt = 0 153 s.seq_len = 0 154 return 0 155} 156 157// Rewind (speculative-decode truncation): drop rows > new_len; unref any 158// now-unused whole blocks. The (possibly shared) tail keeps its data -- 159// rows past seq_len are semantically dead and copy-on-append protects 160// sharers. 161func nx_pkv_seq_truncate(s: *NxPagedSeq, new_len: nx_int) -> nx_int { 162 if new_len > s.seq_len { return NX_PKV_ERR_BOUNDS } 163 let need_pt: nx_int = (new_len + NX_PKV_BS - 1) / NX_PKV_BS 164 while s.n_pt > need_pt { 165 s.n_pt = s.n_pt - 1 166 nx_pkv_unref_block(s.pool, s.pt[s.n_pt]) 167 } 168 s.seq_len = new_len 169 return NX_PKV_OK 170} 171 172// ===== Capacity + copy-on-append ==================================== 173// Called ONCE per multi-token step (stack level, before per-layer 174// appends). Ensures blocks exist for rows [seq_len, seq_len+n_new) and 175// PRIVATIZES the partial tail block if shared (copy rows < tail_off for 176// ALL layers into a fresh block -- the only copy sharing ever costs). 177 178func nx_pkv_ensure_append(s: *NxPagedSeq, n_new: nx_int) -> nx_int { 179 if s == (0 as *NxPagedSeq) { return NX_PKV_ERR_NULL } 180 if n_new <= 0 { return NX_PKV_ERR_BAD_DIM } 181 let p: *NxPagedPool = s.pool 182 183 // 1) privatize the partial tail if shared. 184 let tail_off: nx_int = s.seq_len - (s.seq_len / NX_PKV_BS) * NX_PKV_BS 185 if tail_off > 0 { 186 let tb: i64 = s.pt[s.n_pt - 1] 187 if p.refcnt[tb] > 1 { 188 let nb: i64 = nx_pkv_alloc_block(p) 189 if nb < 0 { return NX_PKV_ERR_EXHAUST } 190 var L: nx_int = 0 191 while L < p.n_layers { 192 var r: nx_int = 0 193 while r < tail_off { 194 let src_base: i64 = _pkv_base(p, tb, L, r) 195 let dst_base: i64 = _pkv_base(p, nb, L, r) 196 var d: nx_int = 0 197 while d < p.kv_dim { 198 p.K_pool[dst_base + d] = p.K_pool[src_base + d] 199 p.V_pool[dst_base + d] = p.V_pool[src_base + d] 200 d = d + 1 201 } 202 r = r + 1 203 } 204 L = L + 1 205 } 206 nx_pkv_unref_block(p, tb) 207 s.pt[s.n_pt - 1] = nb 208 } 209 } 210 211 // 2) allocate blocks to cover seq_len + n_new rows. 212 let need_pt: nx_int = (s.seq_len + n_new + NX_PKV_BS - 1) / NX_PKV_BS 213 if need_pt > s.max_pt { return NX_PKV_ERR_BOUNDS } 214 while s.n_pt < need_pt { 215 let b2: i64 = nx_pkv_alloc_block(p) 216 if b2 < 0 { return NX_PKV_ERR_EXHAUST } 217 s.pt[s.n_pt] = b2 218 s.n_pt = s.n_pt + 1 219 } 220 return NX_PKV_OK 221} 222 223// Per-layer append (mirrors nx_f32_kv_cache_append_layer): write rows 224// [seq_len, seq_len+n) of layer L. Blocks must be pre-ensured. Does NOT 225// advance seq_len (the stack advances once after all layers). 226 227func nx_pkv_append_layer(s: *NxPagedSeq, L: nx_int, 228 K_new: *i64, V_new: *i64, n_new: nx_int) -> nx_int { 229 if s == (0 as *NxPagedSeq) { return NX_PKV_ERR_NULL } 230 let p: *NxPagedPool = s.pool 231 var t: nx_int = 0 232 while t < n_new { 233 let row: nx_int = s.seq_len + t 234 let b: i64 = s.pt[row / NX_PKV_BS] 235 let off: nx_int = row - (row / NX_PKV_BS) * NX_PKV_BS 236 let base: i64 = _pkv_base(p, b, L, off) 237 var d: nx_int = 0 238 while d < p.kv_dim { 239 p.K_pool[base + d] = K_new[t * p.kv_dim + d] 240 p.V_pool[base + d] = V_new[t * p.kv_dim + d] 241 d = d + 1 242 } 243 t = t + 1 244 } 245 return NX_PKV_OK 246} 247 248func nx_pkv_advance(s: *NxPagedSeq, n_new: nx_int) -> nx_int { 249 s.seq_len = s.seq_len + n_new 250 return NX_PKV_OK 251} 252 253// Gather one kv-head stripe of rows [0, total_k) into dst[total_k][head_dim] 254// (the paged twin of the contiguous per-head gather in attn_with_cache). 255func nx_pkv_gather_head(s: *NxPagedSeq, L: nx_int, kv_head: nx_int, 256 head_dim: nx_int, total_k: nx_int, 257 dstK: *i64, dstV: *i64) -> nx_int { 258 let p: *NxPagedPool = s.pool 259 var t: nx_int = 0 260 while t < total_k { 261 let b: i64 = s.pt[t / NX_PKV_BS] 262 let off: nx_int = t - (t / NX_PKV_BS) * NX_PKV_BS 263 let base: i64 = _pkv_base(p, b, L, off) + kv_head * head_dim 264 var d: nx_int = 0 265 while d < head_dim { 266 dstK[t * head_dim + d] = p.K_pool[base + d] 267 dstV[t * head_dim + d] = p.V_pool[base + d] 268 d = d + 1 269 } 270 t = t + 1 271 } 272 return NX_PKV_OK 273}