276 lines
9.1 KiB
C
276 lines
9.1 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/*
|
|
* Property tests for the CPU vector kernels (src/accel/simd.c), derived from the
|
|
* behaviour contract only. Each property holds under release (-ffast-math) and debug flags.
|
|
*
|
|
* - n <= 0: reductions return +0.0f bits, nothing is written.
|
|
* - Element-wise ops write exactly n elements with correctly rounded values.
|
|
* - In-place calls give the same bits as out-of-place calls.
|
|
* - dot/vsum over small integers are exact for every tail length.
|
|
* - Q4_0/Q8_0 element mapping via one-hot vectors (exact).
|
|
* - rms_norm accumulates in double (a case float accumulation gets visibly wrong).
|
|
* - silu*up against a double reference.
|
|
*/
|
|
|
|
#include "lib/test.h"
|
|
#include "../src/accel/simd.h"
|
|
#include "../src/formats/gguf_dequant.h"
|
|
#include <math.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#define PROP_MAX_N 300
|
|
#define PROP_GUARD 8
|
|
#define PROP_GUARD_VALUE 12345.0f
|
|
|
|
static uint32_t fbits(float f) {
|
|
volatile union { float f; uint32_t u; } x;
|
|
x.f = f;
|
|
return x.u;
|
|
}
|
|
|
|
static uint64_t g_rng = 0x2545F4914F6CDD1Dull;
|
|
static uint32_t rnd(void) {
|
|
g_rng ^= g_rng << 13;
|
|
g_rng ^= g_rng >> 7;
|
|
g_rng ^= g_rng << 17;
|
|
return (uint32_t)(g_rng >> 16);
|
|
}
|
|
|
|
/* Uniform-ish float in [-4, 4) with a few random mantissa bits. */
|
|
static float rnd_float(void) {
|
|
return ((float)(rnd() % 65536u) - 32768.0f) / 8192.0f;
|
|
}
|
|
|
|
static void fill_guard(float *buf, int len) {
|
|
for (int i = 0; i < len; i++)
|
|
buf[i] = PROP_GUARD_VALUE;
|
|
}
|
|
|
|
static int guard_intact(const float *buf, int from, int to) {
|
|
for (int i = from; i < to; i++) {
|
|
if (fbits(buf[i]) != fbits(PROP_GUARD_VALUE))
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static void test_nonpositive_n(void) {
|
|
float a[16], b[16], out[16];
|
|
for (int i = 0; i < 16; i++) {
|
|
a[i] = 1.5f;
|
|
b[i] = 2.0f;
|
|
}
|
|
const int sizes[] = {-8, -1, 0};
|
|
for (int s = 0; s < 3; s++) {
|
|
int n = sizes[s];
|
|
ASSERT_INT_EQ(fbits(pn_dot_f32(a, b, n)), 0);
|
|
ASSERT_INT_EQ(fbits(pn_vsum_f32(a, n)), 0);
|
|
fill_guard(out, 16);
|
|
pn_vadd_f32(a, b, out, n);
|
|
pn_vmul_f32(a, b, out, n);
|
|
pn_vscale_f32(a, 3.0f, out, n);
|
|
pn_vsilu_mul_f32(a, b, out, n);
|
|
pn_vrms_norm_f32(a, b, out, n, 1e-5f);
|
|
ASSERT_TRUE(guard_intact(out, 0, 16));
|
|
}
|
|
}
|
|
|
|
static void test_elementwise_extent_and_values(void) {
|
|
float a[64], b[64], out[64 + PROP_GUARD];
|
|
for (int n = 1; n <= 64; n++) {
|
|
for (int i = 0; i < n; i++) {
|
|
a[i] = rnd_float();
|
|
b[i] = rnd_float();
|
|
}
|
|
volatile float scale = rnd_float();
|
|
|
|
fill_guard(out, n + PROP_GUARD);
|
|
pn_vadd_f32(a, b, out, n);
|
|
for (int i = 0; i < n; i++) {
|
|
volatile float want = a[i] + b[i];
|
|
ASSERT_INT_EQ(fbits(out[i]), fbits(want));
|
|
}
|
|
ASSERT_TRUE(guard_intact(out, n, n + PROP_GUARD));
|
|
|
|
fill_guard(out, n + PROP_GUARD);
|
|
pn_vmul_f32(a, b, out, n);
|
|
for (int i = 0; i < n; i++) {
|
|
volatile float want = a[i] * b[i];
|
|
ASSERT_INT_EQ(fbits(out[i]), fbits(want));
|
|
}
|
|
ASSERT_TRUE(guard_intact(out, n, n + PROP_GUARD));
|
|
|
|
fill_guard(out, n + PROP_GUARD);
|
|
pn_vscale_f32(a, scale, out, n);
|
|
for (int i = 0; i < n; i++) {
|
|
volatile float want = a[i] * scale;
|
|
ASSERT_INT_EQ(fbits(out[i]), fbits(want));
|
|
}
|
|
ASSERT_TRUE(guard_intact(out, n, n + PROP_GUARD));
|
|
|
|
fill_guard(out, n + PROP_GUARD);
|
|
pn_vsilu_mul_f32(a, b, out, n);
|
|
ASSERT_TRUE(guard_intact(out, n, n + PROP_GUARD));
|
|
|
|
fill_guard(out, n + PROP_GUARD);
|
|
pn_vrms_norm_f32(a, b, out, n, 1e-5f);
|
|
ASSERT_TRUE(guard_intact(out, n, n + PROP_GUARD));
|
|
}
|
|
}
|
|
|
|
/* silu is excluded: on x86 the in-place alias fallback divides exactly while the
|
|
* vectorized body uses a reciprocal estimate, so its bits legitimately differ. */
|
|
static void test_in_place_matches_out_of_place(void) {
|
|
float a[64], b[64], ref[64], work[64];
|
|
for (int n = 1; n <= 64; n++) {
|
|
for (int i = 0; i < n; i++) {
|
|
a[i] = rnd_float();
|
|
b[i] = rnd_float();
|
|
}
|
|
|
|
pn_vadd_f32(a, b, ref, n);
|
|
memcpy(work, a, sizeof(float) * (size_t)n);
|
|
pn_vadd_f32(work, b, work, n);
|
|
ASSERT_INT_EQ(memcmp(work, ref, sizeof(float) * (size_t)n), 0);
|
|
memcpy(work, b, sizeof(float) * (size_t)n);
|
|
pn_vadd_f32(a, work, work, n);
|
|
ASSERT_INT_EQ(memcmp(work, ref, sizeof(float) * (size_t)n), 0);
|
|
|
|
pn_vmul_f32(a, b, ref, n);
|
|
memcpy(work, a, sizeof(float) * (size_t)n);
|
|
pn_vmul_f32(work, b, work, n);
|
|
ASSERT_INT_EQ(memcmp(work, ref, sizeof(float) * (size_t)n), 0);
|
|
|
|
pn_vscale_f32(a, 0.75f, ref, n);
|
|
memcpy(work, a, sizeof(float) * (size_t)n);
|
|
pn_vscale_f32(work, 0.75f, work, n);
|
|
ASSERT_INT_EQ(memcmp(work, ref, sizeof(float) * (size_t)n), 0);
|
|
|
|
pn_vrms_norm_f32(a, b, ref, n, 1e-6f);
|
|
memcpy(work, a, sizeof(float) * (size_t)n);
|
|
pn_vrms_norm_f32(work, b, work, n, 1e-6f);
|
|
ASSERT_INT_EQ(memcmp(work, ref, sizeof(float) * (size_t)n), 0);
|
|
|
|
pn_vrms_norm_f32(a, a, ref, n, 1e-6f);
|
|
memcpy(work, a, sizeof(float) * (size_t)n);
|
|
pn_vrms_norm_f32(work, work, work, n, 1e-6f);
|
|
ASSERT_INT_EQ(memcmp(work, ref, sizeof(float) * (size_t)n), 0);
|
|
}
|
|
}
|
|
|
|
/* Small integers keep every partial sum exact, so any summation order gives the same
|
|
* answer and a skipped, doubled or misplaced element shows up as a wrong integer. */
|
|
static void test_integer_reductions_exact(void) {
|
|
float a[PROP_MAX_N], b[PROP_MAX_N];
|
|
for (int n = 1; n <= PROP_MAX_N; n++) {
|
|
long dot_want = 0, sum_want = 0;
|
|
for (int i = 0; i < n; i++) {
|
|
int ai = (int)(rnd() % 17u) - 8;
|
|
int bi = (int)(rnd() % 17u) - 8;
|
|
a[i] = (float)ai;
|
|
b[i] = (float)bi;
|
|
dot_want += (long)ai * bi;
|
|
sum_want += ai;
|
|
}
|
|
ASSERT_INT_EQ((long)pn_dot_f32(a, b, n), dot_want);
|
|
ASSERT_INT_EQ((long)pn_vsum_f32(a, n), sum_want);
|
|
}
|
|
}
|
|
|
|
static void test_q4_0_element_mapping(void) {
|
|
pn_block_q4_0 blk;
|
|
float vec[32];
|
|
for (int trial = 0; trial < 64; trial++) {
|
|
blk.d = 0x3C00; /* 1.0 */
|
|
for (int j = 0; j < 16; j++)
|
|
blk.qs[j] = (uint8_t)rnd();
|
|
for (int e = 0; e < 32; e++) {
|
|
memset(vec, 0, sizeof(vec));
|
|
vec[e] = 1.0f;
|
|
int code = e < 16 ? (blk.qs[e] & 0x0F) : (blk.qs[e - 16] >> 4);
|
|
ASSERT_INT_EQ((long)pn_dot_q4_0_f32(&blk, vec), code - 8);
|
|
}
|
|
blk.d = 0x4000; /* 2.0 */
|
|
long want = 0;
|
|
for (int e = 0; e < 32; e++) {
|
|
vec[e] = 1.0f;
|
|
int code = e < 16 ? (blk.qs[e] & 0x0F) : (blk.qs[e - 16] >> 4);
|
|
want += 2 * (code - 8);
|
|
}
|
|
ASSERT_INT_EQ((long)pn_dot_q4_0_f32(&blk, vec), want);
|
|
}
|
|
}
|
|
|
|
static void test_q8_0_element_mapping(void) {
|
|
pn_block_q8_0 blk;
|
|
float vec[32];
|
|
for (int trial = 0; trial < 64; trial++) {
|
|
blk.d = 0x4000; /* 2.0 */
|
|
for (int j = 0; j < 32; j++)
|
|
blk.qs[j] = (int8_t)(rnd() & 0xFF);
|
|
long want = 0;
|
|
for (int e = 0; e < 32; e++) {
|
|
memset(vec, 0, sizeof(vec));
|
|
vec[e] = 1.0f;
|
|
ASSERT_INT_EQ((long)pn_dot_q8_0_f32(&blk, vec), 2L * blk.qs[e]);
|
|
want += 2L * blk.qs[e];
|
|
}
|
|
for (int e = 0; e < 32; e++)
|
|
vec[e] = 1.0f;
|
|
ASSERT_INT_EQ((long)pn_dot_q8_0_f32(&blk, vec), want);
|
|
}
|
|
}
|
|
|
|
/* One big element (square 2^24) then many ones: a float sum loses most of the ones,
|
|
* a double sum is exact. With x = w = 1 the output is r itself. */
|
|
static void test_rms_double_accumulation(void) {
|
|
enum { RMS_N = 4097 };
|
|
static float x[RMS_N], w[RMS_N], out[RMS_N];
|
|
x[0] = 4096.0f;
|
|
w[0] = 1.0f;
|
|
for (int i = 1; i < RMS_N; i++) {
|
|
x[i] = 1.0f;
|
|
w[i] = 1.0f;
|
|
}
|
|
double ss = 16777216.0 + (double)(RMS_N - 1);
|
|
float r_want = (float)(1.0 / sqrt(ss / (double)RMS_N));
|
|
pn_vrms_norm_f32(x, w, out, RMS_N, 0.0f);
|
|
for (int i = 1; i < RMS_N; i++)
|
|
ASSERT_INT_EQ(fbits(out[i]), fbits(r_want));
|
|
}
|
|
|
|
static void test_silu_mul_reference(void) {
|
|
float gate[64], up[64], out[64];
|
|
for (int i = 0; i < 64; i++) {
|
|
gate[i] = rnd_float();
|
|
up[i] = rnd_float();
|
|
}
|
|
for (int n = 1; n <= 64; n++) {
|
|
pn_vsilu_mul_f32(gate, up, out, n);
|
|
for (int i = 0; i < n; i++) {
|
|
double g = gate[i];
|
|
double want = g / (1.0 + exp(-g)) * up[i];
|
|
ASSERT_TRUE(fabs(out[i] - want) <= 1e-5 * (fabs(want) + 1e-3));
|
|
}
|
|
}
|
|
}
|
|
|
|
int main(void) {
|
|
printf("\n SIMD kernel property tests\n\n");
|
|
|
|
RUN_TEST(nonpositive_n);
|
|
RUN_TEST(elementwise_extent_and_values);
|
|
RUN_TEST(in_place_matches_out_of_place);
|
|
RUN_TEST(integer_reductions_exact);
|
|
RUN_TEST(q4_0_element_mapping);
|
|
RUN_TEST(q8_0_element_mapping);
|
|
RUN_TEST(rms_double_accumulation);
|
|
RUN_TEST(silu_mul_reference);
|
|
|
|
TEST_REPORT();
|
|
return __ft_failed > 0 ? 1 : 0;
|
|
}
|