moe(wip): decode-path debugging tools — decode STILL broken

Honest checkpoint. Builds green. The KV-cache decode path (decode_step/
generate) is NOT correct: top-1 diverges from the verified host-attention
forward and is non-deterministic run-to-run; generation is garbage. Two
attempted fixes are included but did NOT solve it: gemm::matmul_dense
(cuBLAS without the m==1 GEMV shortcut) and zeroing the dequant output.

What IS verified and correct (unchanged): the host-attention forward
(gptoss-logits forward path -> top-1 " Paris"), the MXFP4 GPU dequant
kernel (mxfp4-check == numpy), and the sink-attention kernel in isolation
(sink-attn-check == CPU ref, max_diff 0.0017). So the decode bug is in how
decode_step composes these (KV layout / per-step state), not in the kernels
themselves. Root cause still OPEN; see docs/MOE_PROGRESS.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 22:29:55 +08:00
parent 6fdfb1b9d9
commit fcd7fa62b7

View File

@@ -0,0 +1,57 @@
//! Unit-test decode_attention_sink (GPU) against a CPU reference on small random
//! inputs. Isolates the sink/window attention kernel from the rest of gpt-oss.
use half::bf16;
use xserv_kernels::decode_attention_sink;
use xserv_tensor::{Device, Tensor};
fn main() {
let (n_heads, n_kv, head_dim, kv_len) = (8usize, 2usize, 4usize, 3usize);
let scale = (head_dim as f32).powf(-0.5);
let n_rep = n_heads / n_kv;
// deterministic pseudo-random fill
let mut seed = 12345u64;
let mut rnd = || { seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1); ((seed >> 33) as f32 / (1u64 << 31) as f32) - 1.0 };
let q: Vec<f32> = (0..n_heads * head_dim).map(|_| rnd()).collect(); // [H,1,D]
let k: Vec<f32> = (0..n_kv * kv_len * head_dim).map(|_| rnd()).collect(); // [Hkv,kv,D]
let v: Vec<f32> = (0..n_kv * kv_len * head_dim).map(|_| rnd()).collect();
let sinks: Vec<f32> = (0..n_heads).map(|_| rnd()).collect();
let qb: Vec<bf16> = q.iter().map(|&x| bf16::from_f32(x)).collect();
let kb: Vec<bf16> = k.iter().map(|&x| bf16::from_f32(x)).collect();
let vb: Vec<bf16> = v.iter().map(|&x| bf16::from_f32(x)).collect();
let qt = Tensor::from_slice(&qb, &[1, n_heads, 1, head_dim]).to_device(Device::Cuda(0));
let kt = Tensor::from_slice(&kb, &[1, n_kv, kv_len, head_dim]).to_device(Device::Cuda(0));
let vt = Tensor::from_slice(&vb, &[1, n_kv, kv_len, head_dim]).to_device(Device::Cuda(0));
let st = Tensor::from_slice(&sinks, &[n_heads]).to_device(Device::Cuda(0)); // f32
let out = decode_attention_sink(&qt, &kt, &vt, &st, scale, 0); // [1,H,1,D]
let outh = out.to_device(Device::Cpu);
let og = outh.as_slice::<bf16>();
// CPU reference: for each q head, softmax over [s_0..s_{kv-1}, sink], drop sink, weight V.
let mut max_diff = 0f32;
for h in 0..n_heads {
let kv = h / n_rep;
let mut s = vec![0f32; kv_len];
let mut m = sinks[h];
for j in 0..kv_len {
let mut dot = 0f32;
for d in 0..head_dim { dot += q[h * head_dim + d] * k[(kv * kv_len + j) * head_dim + d]; }
s[j] = dot * scale;
if s[j] > m { m = s[j]; }
}
let mut denom = (sinks[h] - m).exp();
for j in 0..kv_len { denom += (s[j] - m).exp(); }
for d in 0..head_dim {
let mut acc = 0f32;
for j in 0..kv_len { acc += (s[j] - m).exp() / denom * v[(kv * kv_len + j) * head_dim + d]; }
let got = og[h * head_dim + d].to_f32();
max_diff = max_diff.max((got - acc).abs());
}
}
println!("decode_attention_sink vs CPU ref: max_abs_diff = {max_diff:.5}");
println!("{}", if max_diff < 0.05 { "SINK_KERNEL_OK" } else { "SINK_KERNEL_MISMATCH" });
}