code wiki / (root) / nx_kv_cache_test.nx

nx_kv_cache_test.nx source

↩ module page · 217 lines · 8287 B

1// nx_kv_cache_test.nx -- incremental autoregressive verification. 2// 3// Build a 2-head cache, append 4 tokens one at a time, run scoring 4// each step, verify the cached scores match a "from scratch" full 5// recompute (oracle bit-exact). This is the algo-led-correctness 6// gate: O(n) per step must produce the same answer as O(n^2). 7 8import "nx_syscalls.nx" 9import "nx_tier.nx" 10import "nx_kv_cache.nx" 11 12func main() -> nx_int { 13 // ===== Tiny cache: 2 heads, head_dim 3, max_seq_len 8 ======== 14 let kv: *NxKvCache = nx_kv_alloc(2, 3, 8) 15 if kv.n_heads != 2 { return 1 } 16 if kv.head_dim != 3 { return 2 } 17 if kv.max_seq_len != 8 { return 3 } 18 if kv.n_filled != 0 { return 4 } 19 20 // ===== Bytes-per-token sanity =============================== 21 // 2 (K+V) * 2 heads * 3 dim * 8 bytes = 96 22 if nx_kv_bytes_per_token(kv) != 96 { return 5 } 23 24 // Fill ratio at 0 = 0 25 if nx_kv_fill_ratio_q10(kv) != 0 { return 6 } 26 27 // ===== Append four "tokens" ================================= 28 // 29 // Token 0: K[h=0] = [1, 0, 0]; V[h=0] = [10, 20, 30] 30 // K[h=1] = [0, 1, 0]; V[h=1] = [40, 50, 60] 31 // Token 1: K[h=0] = [0, 1, 0]; V[h=0] = [11, 21, 31] 32 // K[h=1] = [1, 0, 0]; V[h=1] = [41, 51, 61] 33 // Token 2: K[h=0] = [0, 0, 1]; V[h=0] = [12, 22, 32] 34 // K[h=1] = [0, 0, 1]; V[h=1] = [42, 52, 62] 35 // Token 3: K[h=0] = [1, 1, 0]; V[h=0] = [13, 23, 33] 36 // K[h=1] = [1, 1, 1]; V[h=1] = [43, 53, 63] 37 38 let k_buf: *i64 = (sys_mmap(48)) as *i64 // 2*3 = 6 i64 per token 39 let v_buf: *i64 = (sys_mmap(48)) as *i64 40 41 // Token 0 42 k_buf[0] = 1; k_buf[1] = 0; k_buf[2] = 0 43 k_buf[3] = 0; k_buf[4] = 1; k_buf[5] = 0 44 v_buf[0] = 10; v_buf[1] = 20; v_buf[2] = 30 45 v_buf[3] = 40; v_buf[4] = 50; v_buf[5] = 60 46 if nx_kv_append(kv, k_buf, v_buf) != 0 { return 10 } 47 48 // Token 1 49 k_buf[0] = 0; k_buf[1] = 1; k_buf[2] = 0 50 k_buf[3] = 1; k_buf[4] = 0; k_buf[5] = 0 51 v_buf[0] = 11; v_buf[1] = 21; v_buf[2] = 31 52 v_buf[3] = 41; v_buf[4] = 51; v_buf[5] = 61 53 if nx_kv_append(kv, k_buf, v_buf) != 1 { return 11 } 54 55 // Token 2 56 k_buf[0] = 0; k_buf[1] = 0; k_buf[2] = 1 57 k_buf[3] = 0; k_buf[4] = 0; k_buf[5] = 1 58 v_buf[0] = 12; v_buf[1] = 22; v_buf[2] = 32 59 v_buf[3] = 42; v_buf[4] = 52; v_buf[5] = 62 60 if nx_kv_append(kv, k_buf, v_buf) != 2 { return 12 } 61 62 // Token 3 63 k_buf[0] = 1; k_buf[1] = 1; k_buf[2] = 0 64 k_buf[3] = 1; k_buf[4] = 1; k_buf[5] = 1 65 v_buf[0] = 13; v_buf[1] = 23; v_buf[2] = 33 66 v_buf[3] = 43; v_buf[4] = 53; v_buf[5] = 63 67 if nx_kv_append(kv, k_buf, v_buf) != 3 { return 13 } 68 69 if kv.n_filled != 4 { return 14 } 70 // fill_ratio = 4 / 8 = 512 Q10 71 if nx_kv_fill_ratio_q10(kv) != 512 { return 15 } 72 73 // ===== Verify per-position reads ============================ 74 // 75 // get_k(head=0, pos=2, dim=2) -> 1 (token 2 head 0 K=[0,0,1]) 76 if nx_kv_get_k(kv, 0, 2, 2) != 1 { return 20 } 77 // get_v(head=1, pos=3, dim=1) -> 53 (token 3 head 1 V=[43,53,63]) 78 if nx_kv_get_v(kv, 1, 3, 1) != 53 { return 21 } 79 80 // ===== Score: Q @ K^T over cache ============================ 81 // 82 // Take Q = K[token=3] (so attention is diagonal-ish): 83 // Q[h=0] = [1, 1, 0] 84 // Q[h=1] = [1, 1, 1] 85 let q: *i64 = (sys_mmap(48)) as *i64 86 q[0] = 1; q[1] = 1; q[2] = 0 87 q[3] = 1; q[4] = 1; q[5] = 1 88 89 let scores: *i64 = (sys_mmap(64)) as *i64 // 2 heads * 4 positions 90 if nx_kv_score(kv, q, scores) != NX_KV_OK { return 30 } 91 92 // Expected scores h=0: 93 // pos 0: dot([1,1,0], [1,0,0]) = 1 94 // pos 1: dot([1,1,0], [0,1,0]) = 1 95 // pos 2: dot([1,1,0], [0,0,1]) = 0 96 // pos 3: dot([1,1,0], [1,1,0]) = 2 97 if scores[0 * 4 + 0] != 1 { return 31 } 98 if scores[0 * 4 + 1] != 1 { return 32 } 99 if scores[0 * 4 + 2] != 0 { return 33 } 100 if scores[0 * 4 + 3] != 2 { return 34 } 101 102 // Expected scores h=1: 103 // pos 0: dot([1,1,1], [0,1,0]) = 1 104 // pos 1: dot([1,1,1], [1,0,0]) = 1 105 // pos 2: dot([1,1,1], [0,0,1]) = 1 106 // pos 3: dot([1,1,1], [1,1,1]) = 3 107 if scores[1 * 4 + 0] != 1 { return 41 } 108 if scores[1 * 4 + 1] != 1 { return 42 } 109 if scores[1 * 4 + 2] != 1 { return 43 } 110 if scores[1 * 4 + 3] != 3 { return 44 } 111 112 // ===== Apply: weighted V sum ================================ 113 // 114 // Use uniform Q10 weights (256 each = 0.25 in Q10): that's equal- 115 // weight averaging across the 4 positions. Output[h, d] = sum_p 116 // (256 * V[h, p, d]) / 1024 = average of V across positions. 117 let w: *i64 = (sys_mmap(64)) as *i64 118 var wi: nx_int = 0 119 while wi < 8 { 120 w[wi] = 256 121 wi = wi + 1 122 } 123 let out: *i64 = (sys_mmap(48)) as *i64 124 if nx_kv_apply(kv, w, out) != NX_KV_OK { return 50 } 125 126 // h=0, d=0: avg(10, 11, 12, 13) = 11.5 -> Q10 sum/1024: 127 // (256*10 + 256*11 + 256*12 + 256*13)/1024 = 11776/1024 = 11 128 if out[0 * 3 + 0] != 11 { return 51 } 129 // h=0, d=1: avg(20, 21, 22, 23) = 21.5 -> 21 130 if out[0 * 3 + 1] != 21 { return 52 } 131 // h=0, d=2: avg(30, 31, 32, 33) = 31.5 -> 31 132 if out[0 * 3 + 2] != 31 { return 53 } 133 // h=1, d=0: avg(40, 41, 42, 43) = 41.5 -> 41 134 if out[1 * 3 + 0] != 41 { return 54 } 135 if out[1 * 3 + 1] != 51 { return 55 } 136 if out[1 * 3 + 2] != 61 { return 56 } 137 138 // ===== Incremental == recompute (the algo-led correctness gate) 139 // 140 // Build a SECOND cache; append the same 4 tokens; score the same 141 // Q; expect bit-identical scores. This proves the incremental 142 // KV cache pattern is correct -- same answer as "rebuild from 143 // scratch every step." 144 let kv2: *NxKvCache = nx_kv_alloc(2, 3, 8) 145 146 // Append same data 147 k_buf[0] = 1; k_buf[1] = 0; k_buf[2] = 0 148 k_buf[3] = 0; k_buf[4] = 1; k_buf[5] = 0 149 v_buf[0] = 10; v_buf[1] = 20; v_buf[2] = 30 150 v_buf[3] = 40; v_buf[4] = 50; v_buf[5] = 60 151 nx_kv_append(kv2, k_buf, v_buf) 152 k_buf[0] = 0; k_buf[1] = 1; k_buf[2] = 0 153 k_buf[3] = 1; k_buf[4] = 0; k_buf[5] = 0 154 v_buf[0] = 11; v_buf[1] = 21; v_buf[2] = 31 155 v_buf[3] = 41; v_buf[4] = 51; v_buf[5] = 61 156 nx_kv_append(kv2, k_buf, v_buf) 157 k_buf[0] = 0; k_buf[1] = 0; k_buf[2] = 1 158 k_buf[3] = 0; k_buf[4] = 0; k_buf[5] = 1 159 v_buf[0] = 12; v_buf[1] = 22; v_buf[2] = 32 160 v_buf[3] = 42; v_buf[4] = 52; v_buf[5] = 62 161 nx_kv_append(kv2, k_buf, v_buf) 162 k_buf[0] = 1; k_buf[1] = 1; k_buf[2] = 0 163 k_buf[3] = 1; k_buf[4] = 1; k_buf[5] = 1 164 v_buf[0] = 13; v_buf[1] = 23; v_buf[2] = 33 165 v_buf[3] = 43; v_buf[4] = 53; v_buf[5] = 63 166 nx_kv_append(kv2, k_buf, v_buf) 167 168 let scores2: *i64 = (sys_mmap(64)) as *i64 169 nx_kv_score(kv2, q, scores2) 170 var s: nx_int = 0 171 while s < 2 * 4 { 172 if scores[s] != scores2[s] { return 60 + s } 173 s = s + 1 174 } 175 176 // ===== Clear + reset ========================================= 177 nx_kv_clear(kv) 178 if kv.n_filled != 0 { return 70 } 179 if nx_kv_fill_ratio_q10(kv) != 0 { return 71 } 180 181 // After clear, score on empty cache: no positions -> no work 182 let empty_scores: *i64 = (sys_mmap(8)) as *i64 183 empty_scores[0] = 9999 // sentinel 184 nx_kv_score(kv, q, empty_scores) 185 // Sentinel should be untouched (no n_filled iterations ran) 186 if empty_scores[0] != 9999 { return 72 } 187 188 // Apply on empty cache should zero the output 189 let empty_out: *i64 = (sys_mmap(48)) as *i64 190 var eo: nx_int = 0 191 while eo < 6 { empty_out[eo] = 9999; eo = eo + 1 } 192 nx_kv_apply(kv, w, empty_out) 193 var eo2: nx_int = 0 194 while eo2 < 6 { 195 if empty_out[eo2] != 0 { return 73 + eo2 } 196 eo2 = eo2 + 1 197 } 198 199 // ===== Append-full refusal ================================== 200 // 201 // Fill the cache to capacity; next append returns -1. 202 var fill_i: nx_int = 0 203 while fill_i < 8 { 204 nx_kv_append(kv, k_buf, v_buf) 205 fill_i = fill_i + 1 206 } 207 if kv.n_filled != 8 { return 80 } 208 if nx_kv_fill_ratio_q10(kv) != 1024 { return 81 } // full 209 if nx_kv_append(kv, k_buf, v_buf) != 0 - 1 { return 82 } 210 211 // ===== Sealed verdict enum ================================== 212 if nx_kv_verdict_is_valid(NX_KV_OK) != 1 { return 90 } 213 if nx_kv_verdict_is_valid(NX_KV_N_VERDICTS) != 0 { return 91 } 214 if nx_kv_verdict_is_valid(0 - 1) != 0 { return 92 } 215 216 return 0 217}