code wiki / (root) / nx_attention_test.nx

nx_attention_test.nx source

↩ module page · 155 lines · 6769 B

1// nx_attention_test.nx -- attention kernel composes the stack. 2 3import "nx_syscalls.nx" 4import "nx_tier.nx" 5import "nx_tensor.nx" 6import "nx_attention.nx" 7import "nx_numeric_oracle.nx" 8 9func main() -> nx_int { 10 let err: *i64 = (sys_mmap(8)) as *i64 11 let idx: *i64 = (sys_mmap(64)) as *i64 12 13 // ===== Build a 2-token query against a 3-token key/value cache = 14 // 15 // n_tokens = 2 (the queries we want to attend FROM) 16 // n_kv = 3 (the past tokens we attend TO) 17 // head_dim = 4 18 19 let sh_q: *i64 = (sys_mmap(16)) as *i64 20 sh_q[0] = 2; sh_q[1] = 4 21 let q: *NxTensor = nx_t_alloc(NX_DT_I64, sh_q, 2, err) 22 nx_t_fill_zero(q) 23 let pq: *i64 = q.storage as *i64 24 // Q[0] = [1, 0, 0, 0] 25 pq[0] = 1 26 // Q[1] = [0, 1, 0, 0] 27 pq[5] = 1 28 29 let sh_k: *i64 = (sys_mmap(16)) as *i64 30 sh_k[0] = 3; sh_k[1] = 4 31 let k: *NxTensor = nx_t_alloc(NX_DT_I64, sh_k, 2, err) 32 nx_t_fill_zero(k) 33 let pk: *i64 = k.storage as *i64 34 // K[0] = [4, 0, 0, 0] -> Q[0] @ K[0]^T = 4 35 pk[0] = 4 36 // K[1] = [0, 8, 0, 0] -> Q[1] @ K[1]^T = 8 37 pk[5] = 8 38 // K[2] = [2, 2, 0, 0] -> Q[0] @ K[2]^T = 2; Q[1] @ K[2]^T = 2 39 pk[8] = 2; pk[9] = 2 40 41 let sh_scores: *i64 = (sys_mmap(16)) as *i64 42 sh_scores[0] = 2; sh_scores[1] = 3 43 let scores: *NxTensor = nx_t_alloc(NX_DT_I64, sh_scores, 2, err) 44 45 // ===== Step 1: score matrix Q @ K^T ========================== 46 if nx_attn_score_matrix(q, k, scores) != NX_ATTN_OK { return 1 } 47 let ps: *i64 = scores.storage as *i64 48 if ps[0 * 3 + 0] != 4 { return 2 } // Q[0].K[0] 49 if ps[0 * 3 + 1] != 0 { return 3 } // Q[0].K[1] 50 if ps[0 * 3 + 2] != 2 { return 4 } // Q[0].K[2] 51 if ps[1 * 3 + 0] != 0 { return 5 } // Q[1].K[0] 52 if ps[1 * 3 + 1] != 8 { return 6 } // Q[1].K[1] 53 if ps[1 * 3 + 2] != 2 { return 7 } // Q[1].K[2] 54 55 // ===== Step 2: scale (use Q10 = 512 = 0.5) =================== 56 nx_attn_scale_q10(scores, 512) 57 if ps[0 * 3 + 0] != 2 { return 10 } // 4 * 512 / 1024 = 2 58 if ps[1 * 3 + 1] != 4 { return 11 } // 8 * 512 / 1024 = 4 59 60 // ===== Step 3: softmax per row (Q10 probabilities) ========== 61 nx_attn_softmax_row_q10(scores) 62 // Each row should sum to ~Q10 (1024) within rounding tolerance 63 var row0_sum: nx_int = ps[0] + ps[1] + ps[2] 64 var row1_sum: nx_int = ps[3] + ps[4] + ps[5] 65 // Allow small rounding error (the per-row rounding pads by n_cols) 66 if row0_sum < 1020 { return 20 } 67 if row0_sum > 1028 { return 21 } 68 if row1_sum < 1020 { return 22 } 69 if row1_sum > 1028 { return 23 } 70 71 // The cell with the highest score gets the highest probability -- but only down to the 72 // softmax's Q10 RESOLUTION. Logit gaps of 1-2 Q10 (score 2 vs 0, 1 vs 0, 4 vs 1 = 73 // ~0.001-0.004) sit BELOW resolution: the 16-entry exp LUT distinguishes them (1024 vs 74 // 1023) yet the Q10 normalize (* Q10 / sum, integer floor) rounds both to equal (~341). 75 // So near-tied positions assert MONOTONICITY (>=, via `<`); the one resolvable gap below 76 // (ps[4] score 4 vs ps[3] score 0 -> 341 vs 340) keeps the STRICT check. A real inversion 77 // (higher score -> strictly lower prob) still fails either form. 78 // Row 0: scores [2, 0, 1] -> positions 0 and 2 (>0) tie with position 1 at this resolution. 79 if ps[0] < ps[1] { return 24 } 80 if ps[2] < ps[1] { return 25 } 81 // Row 1: scores [0, 4, 1] -> Q[1].K[1] (highest) dominates; STRICT vs the 0, >= vs the 1. 82 if ps[4] <= ps[3] { return 26 } 83 if ps[4] < ps[5] { return 27 } 84 85 // ===== Step 4: apply to V ==================================== 86 // 87 // V[0] = [10, 20, 30, 40] 88 // V[1] = [50, 60, 70, 80] 89 // V[2] = [90, 100, 110, 120] 90 let v: *NxTensor = nx_t_alloc(NX_DT_I64, sh_k, 2, err) 91 let pv: *i64 = v.storage as *i64 92 pv[0] = 10; pv[1] = 20; pv[2] = 30; pv[3] = 40 93 pv[4] = 50; pv[5] = 60; pv[6] = 70; pv[7] = 80 94 pv[8] = 90; pv[9] = 100; pv[10] = 110; pv[11] = 120 95 96 let sh_out: *i64 = (sys_mmap(16)) as *i64 97 sh_out[0] = 2; sh_out[1] = 4 98 let out: *NxTensor = nx_t_alloc(NX_DT_I64, sh_out, 2, err) 99 if nx_attn_apply_to_v(scores, v, out) != NX_ATTN_OK { return 30 } 100 let po: *i64 = out.storage as *i64 101 102 // Row 0: weights ~= [w0, w1, w2] with w0+w2 ~= 1024-w1 dominating 103 // (since K[0] and K[2] tied at score 2) 104 // Output is roughly an average of V[0] and V[2] weighted by w0, w2. 105 // V[0][0]=10, V[2][0]=90 -> expected out[0][0] is somewhere 106 // between 10 and 90, closer to the midpoint (~50). 107 if po[0 * 4 + 0] < 20 { return 40 } 108 if po[0 * 4 + 0] > 80 { return 41 } 109 110 // Row 1: weights mostly on K[1] (largest score), so output should 111 // be close to V[1] = [50, 60, 70, 80]. 112 if po[1 * 4 + 0] < 30 { return 42 } 113 if po[1 * 4 + 0] > 60 { return 43 } // dominated by V[1] 114 if po[1 * 4 + 3] < 50 { return 44 } 115 if po[1 * 4 + 3] > 90 { return 45 } 116 117 // ===== Forward pass: same answer as step-by-step ============== 118 // 119 // Re-create scores tensor since the step-by-step path consumed 120 // it in place; rebuild from fresh Q/K/V. 121 let q2: *NxTensor = nx_t_alloc(NX_DT_I64, sh_q, 2, err) 122 let pq2: *i64 = q2.storage as *i64 123 pq2[0] = 1; pq2[5] = 1 124 let k2: *NxTensor = nx_t_alloc(NX_DT_I64, sh_k, 2, err) 125 let pk2: *i64 = k2.storage as *i64 126 pk2[0] = 4; pk2[5] = 8; pk2[8] = 2; pk2[9] = 2 127 let v2: *NxTensor = nx_t_alloc(NX_DT_I64, sh_k, 2, err) 128 let pv2: *i64 = v2.storage as *i64 129 pv2[0] = 10; pv2[1] = 20; pv2[2] = 30; pv2[3] = 40 130 pv2[4] = 50; pv2[5] = 60; pv2[6] = 70; pv2[7] = 80 131 pv2[8] = 90; pv2[9] = 100; pv2[10] = 110; pv2[11] = 120 132 133 let out_fw: *NxTensor = nx_t_alloc(NX_DT_I64, sh_out, 2, err) 134 if nx_attn_forward(q2, k2, v2, out_fw, 512) != NX_ATTN_OK { return 50 } 135 136 // Verify via oracle: step-by-step output == forward output (bit-exact) 137 let witness: *i64 = (sys_mmap(NX_NO_WITNESS_FIELDS * 8)) as *i64 138 let v_fw: nx_int = nx_no_check_bit_exact_i64(out, out_fw, witness) 139 if v_fw != NX_NO_VERDICT_EQUAL { return 60 + v_fw } 140 141 // ===== exp(x) sanity ======================================== 142 if _attn_exp_q10(0) != 1024 { return 70 } // exp(0) = 1 143 let e_neg2: nx_int = _attn_exp_q10(0 - 2048) // exp(-2.0) 144 if e_neg2 < 130 { return 71 } 145 if e_neg2 > 145 { return 72 } 146 if _attn_exp_q10(0 - 10240) != 0 { return 73 } // far below -> 0 147 if _attn_exp_q10(99999) != 1024 { return 74 } // x>0 clamped to 1 148 149 // ===== Sealed verdict band coverage ========================= 150 if nx_attn_verdict_is_valid(NX_ATTN_OK) != 1 { return 80 } 151 if nx_attn_verdict_is_valid(NX_ATTN_N_VERDICTS) != 0 { return 81 } 152 if nx_attn_verdict_is_valid(0 - 1) != 0 { return 82 } 153 154 return 0 155}