nx_gen_sdpa_verify.nx
buildroot/runtime/nx_gen_sdpa_verify.nx
about
nx_gen_sdpa_verify.nx -- SOVEREIGN scaled dot-product attention, verified vs the oracle.
scores[lk] = sum_d q[h][lq][d] * k[h][lk][d] / sqrt(head_dim)
out[lq][h][d] = sum_lk softmax(scores)[lk] * v[lk][h][d]
THE SCALE, derived not guessed: ggml_ext_attention_ext uses scale = 1/sqrt(d_head). It also
takes a kv_scale (1/128 here) which it applies to k and v before an f16 cast to avoid overflow
-- but that cancels EXACTLY: the flash path passes scale/kv_scale into the softmax and then
multiplies the result by 1/kv_scale, and the fallback path never applies kv_scale at all. So the
arithmetic to reproduce is plain SDPA with 1/sqrt(head_dim), and reading kv_scale as the softmax
scale would have been wrong by 11x while still producing a finite, plausible tensor.
Usage: nx_gen_sdpa_verify <model> <q> <k> <qkv> <out> <head_dim> <n_heads> <v_head_base> [rows]
⚠THREE DIFFERENT LAYOUTS MEET HERE, which is the whole difficulty:
q, k [head_dim, L, n_heads] -> (h*L + l)*head_dim + d (post-RoPE order)
v packed qkv [qkv_width, L] -> l*qkv_width + (base+h)*head_dim + d
out [n_heads*head_dim, L] -> l*(n_heads*head_dim) + h*head_dim + d
RoPE permuted q/k but v is still read straight from the fused projection, so v does NOT share
q's layout. Every index is written out explicitly rather than shared.
Softmax subtracts the row max before exponentiating -- without it, logits of a few hundred
overflow f32 exp and the whole row becomes NaN or zero.
license_tier: ORIGINAL
dependencies 9 imports · 0 importers
imports: nx_syscalls.nxnx_le.nxnx_f32.nxnx_f32_div.nxnx_f32_cvt.nxnx_f32_exp.nxnx_strconv.nxnx_genfix.nxnx_genver.nx
imported by: nobody (leaf or entry point)
call flow from main pre-order; caps 40 nodes / depth 6 declared; ↻ = already shown
structs
| none |
consts
| none |
functions
| 36 | func zs_strlen(s: *u8) -> i64 called by 1: main |
| 42 | func main(argc: i64, argv: *i64) -> i64 |