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:
57
crates/xserv-model/src/bin/sink-attn-check.rs
Normal file
57
crates/xserv-model/src/bin/sink-attn-check.rs
Normal 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" });
|
||||
}
|
||||
Reference in New Issue
Block a user