Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 10 additions & 14 deletions crates/model/tests/attn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,14 +237,6 @@ fn hw_attn_exact_input_stages() {
v,
)
};
let maxdiff = |a: &Tensor2, b: &Tensor2| {
a.data
.iter()
.zip(&b.data)
.map(|(x, y)| (x - y).abs())
.fold(0.0f32, f32::max)
};

for blk in [0usize, 1usize] {
let g = gguf::Gguf::open(model_path::model_path()).unwrap();
let p = attn::MlaParams::read(&g, blk).unwrap();
Expand All @@ -262,9 +254,11 @@ fn hw_attn_exact_input_stages() {
attn::q_nope2_absorbed(derived.wk_b_all_heads(blk).unwrap(), &q_exact, &p).unwrap();
let want = load2(&format!("q_nope2-{blk}"), 0);
assert_eq!([out.ne0, out.ne1], [want.ne0, want.ne1], "stage A shape");
assert!(
maxdiff(&out, &want) <= 1e-5,
"stage A: absorption arithmetic"
asserts::assert_close(
&out.data,
&want.data,
1e-5,
"stage A: absorption arithmetic",
);

// B: the attention proper — two legs.
Expand Down Expand Up @@ -366,9 +360,11 @@ fn hw_attn_exact_input_stages() {
let out = model::ops::matmul_q(&g, wo, &kqv_2d).unwrap();
let want = load2(&format!("kqv_out-{blk}"), 0);
assert_eq!([out.ne0, out.ne1], [want.ne0, want.ne1], "stage C shape");
assert!(
maxdiff(&out, &want) <= 1e-4,
"stage C: wv_b + output projection"
asserts::assert_close(
&out.data,
&want.data,
1e-4,
"stage C: wv_b + output projection",
);
}
}
Expand Down
9 changes: 9 additions & 0 deletions crates/model/tests/common/asserts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ pub fn assert_close(got: &[f32], want: &[f32], tol: f32, what: &str) {
let mut worst = 0.0f32;
let mut at = 0usize;
for (i, (&g, &w)) in got.iter().zip(want).enumerate() {
// Reject non-finite operands before comparing errors.
assert!(
g.is_finite(),
"{what}: non-finite got at index {i} (reference {w}); gate is {tol:e}"
);
assert!(
w.is_finite(),
"{what}: non-finite reference at index {i} (got {g}); gate is {tol:e}"
);
let d = (g - w).abs();
if d > worst {
worst = d;
Expand Down
16 changes: 7 additions & 9 deletions crates/model/tests/derived.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
//! `hw_` prefix: needs the box and the model file. Like `tests/kv.rs`, this
//! compares our paths against each other — `tests/attn.rs` and
//! `tests/forward.rs` are what tie them to ik.
#[path = "common/asserts.rs"]
mod asserts;
#[path = "common/manifest.rs"]
mod manifest;
#[path = "common/model_path.rs"]
Expand Down Expand Up @@ -119,15 +121,11 @@ fn hw_derived_wblocks_bit_identical() {
let from_reference = q_nope2_absorbed(&reference_flat, &q, &p).unwrap();
let from_derived = q_nope2_absorbed(d.wk_b_all_heads(blk).unwrap(), &q, &p).unwrap();
assert_eq!(from_derived.data.len(), from_reference.data.len(), "shape");
let worst = from_derived
.data
.iter()
.zip(&from_reference.data)
.map(|(&a, &b)| (a - b).abs())
.fold(0.0f32, f32::max);
assert_eq!(
worst, 0.0,
"q_nope2-{blk}: cached-wblocks vs in-place-wblocks max|diff| {worst:e}"
asserts::assert_close(
&from_derived.data,
&from_reference.data,
0.0,
&format!("q_nope2-{blk}: cached-wblocks vs in-place-wblocks"),
);
eprintln!("q_nope2-{blk:<4} cached == in-place max|diff| = 0 exact");
}
Expand Down
4 changes: 4 additions & 0 deletions crates/model/tests/forward.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ fn max_abs_diff(got: &[f32], want: &[f32]) -> (f32, usize) {
let mut worst = 0.0f32;
let mut at = 0usize;
for (i, (&g, &w)) in got.iter().zip(want).enumerate() {
assert!(
g.is_finite() && w.is_finite(),
"non-finite comparison operand at index {i}: got {g}, reference {w}"
);
let d = (g - w).abs();
if d > worst {
worst = d;
Expand Down
4 changes: 4 additions & 0 deletions crates/model/tests/head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ fn assert_close_rel(got: &[f32], want: &[f32], what: &str) {
let mut at = 0usize;
let mut pair = (0.0f32, 0.0f32);
for (i, (&g, &w)) in got.iter().zip(want).enumerate() {
assert!(
g.is_finite() && w.is_finite(),
"non-finite comparison operand at index {i}: got {g}, reference {w}"
);
let d = (g - w).abs();
worst_abs = worst_abs.max(d);
max_abs_ref = max_abs_ref.max(w.abs());
Expand Down
13 changes: 10 additions & 3 deletions crates/model/tests/kv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,14 @@ fn max_abs_diff(got: &[f32], want: &[f32]) -> f32 {
);
got.iter()
.zip(want)
.map(|(&g, &w)| (g - w).abs())
.enumerate()
.map(|(i, (&g, &w))| {
assert!(
g.is_finite() && w.is_finite(),
"non-finite comparison operand at index {i}: got {g}, reference {w}"
);
(g - w).abs()
})
.fold(0.0f32, f32::max)
}

Expand All @@ -43,8 +50,8 @@ fn max_abs_diff(got: &[f32], want: &[f32]) -> f32 {
/// only its batch, so any difference here means the shim is not the identity it says
/// it is. The same run is `Derived`'s end-to-end claim (`tests/derived.rs`, layer 3):
/// `forward` builds its own `Derived`, `step` is handed one, and block `b`'s absorption
/// must get block `b`'s blocks either way. Bits, not differences: `NaN - NaN` folds to
/// a clean zero through `f32::max`.
/// must get block `b`'s blocks either way. Bits, not differences; a non-finite
/// side in either vector is refused before the fold.
#[test]
#[ignore = "hw: needs the box and the model file"]
fn hw_kv_one_shot_equals_uncached() {
Expand Down
17 changes: 17 additions & 0 deletions crates/model/tests/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,23 @@ fn hw_rms_norm_matches_ggml() {
asserts::assert_close(&got.data, &want, 0.0, "rms_norm -> attn_norm-0");
}

// The shared numeric gate rejects a non-finite output without fixtures.
#[test]
fn the_nan_output_cell_cannot_pass_the_gate() {
let got = std::panic::catch_unwind(|| {
asserts::assert_close(&[f32::NAN], &[1.0], 0.0, "rms_norm mutant -> attn_norm-0")
})
.is_err();
let reference = std::panic::catch_unwind(|| {
asserts::assert_close(&[1.0], &[f32::NAN], 0.0, "rms_norm mutant -> attn_norm-0")
})
.is_err();
assert!(
got && reference,
"a NaN must fail the gate in each operand position"
);
}

#[test]
#[ignore = "hw: needs the box, the model file and $BLOOMERY_DATA/ref"]
fn hw_matmul_q_matches_ggml() {
Expand Down
14 changes: 3 additions & 11 deletions crates/model/tests/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
//! hold anything specific — the oracle is used only as the shared prompt, the same
//! one every other gate runs, so the bit-identity claim is about the prompt that
//! matters.
#[path = "common/asserts.rs"]
mod asserts;
#[path = "common/manifest.rs"]
mod manifest;
#[path = "common/model_path.rs"]
Expand Down Expand Up @@ -117,17 +119,7 @@ fn hw_profile_gate() {
off.len(),
on.len()
);
let worst = off
.iter()
.zip(&on)
.map(|(&a, &b)| (a - b).abs())
.fold(0.0f32, f32::max);
assert_eq!(
worst,
0.0,
"profiling changed the logits: max|diff| {worst:e} over {} values",
off.len()
);
asserts::assert_close(&on, &off, 0.0, "profiling changed the logits");
eprintln!("logits, off vs profiled level 2 max|diff| = 0 exact");

// Assertions 2 and 3 need the in-process accumulators. The children were separate
Expand Down
Loading