diff --git a/tests/test-dt3.cpp b/tests/test-dt3.cpp index 5ef357ec7..d116e3129 100644 --- a/tests/test-dt3.cpp +++ b/tests/test-dt3.cpp @@ -479,6 +479,89 @@ static int test_vec_dot(const ggml_type_traits_cpu * qfns_cpu) { return num_failed; } +// the scalar reference implementation, always compiled into ggml-cpu; on +// architectures without a native kernel the dispatched vec_dot IS this +// function and the parity test below passes trivially +extern "C" void ggml_vec_dot_dt3_q8_0_generic(int n, float * s, size_t bs, const void * vx, size_t bx, const void * vy, size_t by, int nrc); + +// the dispatched (possibly vectorized) vec_dot must match the generic scalar +// implementation exactly — the actual function is called, not a re-derivation +// of it. Blocks exercise all three regions, the 79/80 and 119/120 boundaries, +// non-trivial qh bytes (would expose reading their padding 5th digit), and +// scales of both and mixed signs. +static int test_vec_dot_arch_parity(const ggml_type_traits_cpu * qfns_cpu) { + int num_failed = 0; + + const auto * vdot_traits = ggml_get_type_traits_cpu(qfns_cpu->vec_dot_type); + + const int nblocks = 3; + const int n = nblocks*QK_DT3; + + const float scale_cases[][2] = { + { 1.0f, 0.25f }, + { 0.5f, -0.125f }, // negative second plane + {-2.0f, 0.75f }, // negative first plane + {-0.75f, -0.0625f }, // both negative + { 0.0f, 1.0f }, // dead first plane + }; + const int n_scale_cases = (int)(sizeof(scale_cases)/sizeof(scale_cases[0])); + + // pattern 0: fully random trits + // pattern 1: zero everywhere except elements 120..127 (qh-only) + // pattern 2: single +1/-1 walking over the region boundaries + const int boundary_pos[] = { 0, 79, 80, 119, 120, 127 }; + + for (int rep = 0; rep < 96; ++rep) { + std::vector xq(nblocks*DT3_BLOCK_SIZE); + + for (int i = 0; i < nblocks; ++i) { + int8_t t1[QK_DT3] = {0}; + int8_t t2[QK_DT3] = {0}; + + const int pattern = rep % 3; + if (pattern == 0) { + for (int j = 0; j < QK_DT3; ++j) { + t1[j] = rng_trit(); + t2[j] = rng_trit(); + } + } else if (pattern == 1) { + for (int j = 120; j < QK_DT3; ++j) { + t1[j] = rng_trit(); + t2[j] = rng_trit(); + } + } else { + const int pos = boundary_pos[rep/3 % 6]; + t1[pos] = (rep & 1) ? 1 : -1; + t2[QK_DT3 - 1 - pos] = (rep & 1) ? -1 : 1; + } + + const float * sc = scale_cases[(rep + i) % n_scale_cases]; + ref_pack_block(t1, sc[0], t2, sc[1], &xq[i*DT3_BLOCK_SIZE]); + } + + std::vector y(n); + for (int j = 0; j < n; ++j) { + // reach the full q8_0 range, both signs + y[j] = 127.0f*sinf(0.7f*(float)(j + 13*rep)) + 0.5f*cosf((float)j); + } + std::vector yq(ggml_row_size(qfns_cpu->vec_dot_type, n)); + vdot_traits->from_float(y.data(), yq.data(), n); + + float res_arch = INFINITY; + float res_generic = -INFINITY; + qfns_cpu->vec_dot(n, &res_arch, 0, xq.data(), 0, yq.data(), 0, 1); + ggml_vec_dot_dt3_q8_0_generic(n, &res_generic, 0, xq.data(), 0, yq.data(), 0, 1); + + if (memcmp(&res_arch, &res_generic, sizeof(float)) != 0) { + printf("FAILED: vec_dot arch parity rep %d: arch %.9g != generic %.9g\n", + rep, res_arch, res_generic); + num_failed++; + } + } + + return num_failed; +} + // --dequant IN.bin OUT.f32 : dequantize raw DT3 blocks, for parity checks // against external packers (ternaria's Rust pack_dt3) static int run_dequant_file(const char * in_path, const char * out_path) { @@ -541,6 +624,7 @@ int main(int argc, char * argv[]) { num_failed += test_roundtrip(qfns); num_failed += test_quantize_pack_parity(qfns_cpu); num_failed += test_vec_dot(qfns_cpu); + num_failed += test_vec_dot_arch_parity(qfns_cpu); printf("%d tests failed\n", num_failed);