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}