/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ /* * test_metal.c — Tests for the Metal GPU compute backend * * Tests lifecycle, buffers, MPS GEMM/GEMV, and custom compute kernels. */ #include "lib/test.h" #include "metal/fm_metal.h" #include #include #include #include /* ================================================================ * Phase 1: Infrastructure + MPS GEMM * ================================================================ */ static void test_init_and_available(void) { int ret = fm_init(); ASSERT_INT_EQ(0, ret); ASSERT_TRUE(fm_available()); } static void test_double_init_is_safe(void) { int ret = fm_init(); ASSERT_INT_EQ(0, ret); /* Second init should succeed (no-op) */ ret = fm_init(); ASSERT_INT_EQ(0, ret); ASSERT_TRUE(fm_available()); } static void test_buffer_alloc(void) { fm_buffer *buf = fm_buffer_alloc(1024); ASSERT_NOT_NULL(buf); ASSERT_NOT_NULL(fm_buffer_data(buf)); ASSERT_INT_EQ(1024, (int)fm_buffer_size(buf)); fm_buffer_free(buf); } static void test_buffer_from_ptr(void) { float data[4] = {1.0f, 2.0f, 3.0f, 4.0f}; fm_buffer *buf = fm_buffer_from_ptr(data, sizeof(data)); ASSERT_NOT_NULL(buf); ASSERT_INT_EQ(sizeof(data), (int)fm_buffer_size(buf)); fm_buffer_free(buf); } static void test_buffer_alloc_write_read(void) { fm_buffer *buf = fm_buffer_alloc(4 * sizeof(float)); ASSERT_NOT_NULL(buf); float *ptr = (float*)fm_buffer_data(buf); ptr[0] = 1.0f; ptr[1] = 2.0f; ptr[2] = 3.0f; ptr[3] = 4.0f; /* Read back */ ASSERT_FLT_NEAR(1.0f, ptr[0], 1e-6f); ASSERT_FLT_NEAR(2.0f, ptr[1], 1e-6f); ASSERT_FLT_NEAR(3.0f, ptr[2], 1e-6f); ASSERT_FLT_NEAR(4.0f, ptr[3], 1e-6f); fm_buffer_free(buf); } static void test_sgemm_2x2(void) { /* A = [[1,2],[3,4]], B = [[5,6],[7,8]] => C = [[19,22],[43,50]] */ float a[4] = {1, 2, 3, 4}; float b[4] = {5, 6, 7, 8}; float c[4] = {0, 0, 0, 0}; fm_buffer *ba = fm_buffer_from_ptr(a, sizeof(a)); fm_buffer *bb = fm_buffer_from_ptr(b, sizeof(b)); fm_buffer *bc = fm_buffer_from_ptr(c, sizeof(c)); fm_sgemm(ba, bb, bc, 2, 2, 2, 1.0f, 0.0f); /* Read result from Metal buffer */ float *result = (float*)fm_buffer_data(bc); ASSERT_FLT_NEAR(19.0f, result[0], 0.01f); ASSERT_FLT_NEAR(22.0f, result[1], 0.01f); ASSERT_FLT_NEAR(43.0f, result[2], 0.01f); ASSERT_FLT_NEAR(50.0f, result[3], 0.01f); fm_buffer_free(ba); fm_buffer_free(bb); fm_buffer_free(bc); } static void test_sgemm_alpha_beta(void) { /* C = 2*A@B + 3*C with A=B=I(2), C=[[1,0],[0,1]] => C = [[5,0],[0,5]] */ float a[4] = {1, 0, 0, 1}; float b[4] = {1, 0, 0, 1}; float c[4] = {1, 0, 0, 1}; fm_buffer *ba = fm_buffer_from_ptr(a, sizeof(a)); fm_buffer *bb = fm_buffer_from_ptr(b, sizeof(b)); fm_buffer *bc = fm_buffer_from_ptr(c, sizeof(c)); fm_sgemm(ba, bb, bc, 2, 2, 2, 2.0f, 3.0f); float *result = (float*)fm_buffer_data(bc); ASSERT_FLT_NEAR(5.0f, result[0], 0.01f); ASSERT_FLT_NEAR(0.0f, result[1], 0.01f); ASSERT_FLT_NEAR(0.0f, result[2], 0.01f); ASSERT_FLT_NEAR(5.0f, result[3], 0.01f); fm_buffer_free(ba); fm_buffer_free(bb); fm_buffer_free(bc); } static void test_sgemm_768x768(void) { /* Test larger matrix matches CPU result */ int M = 768, N = 768, K = 768; size_t sz = M * N * sizeof(float); float *a = (float*)malloc(sz); float *b = (float*)malloc(sz); float *c_cpu = (float*)calloc(M * N, sizeof(float)); float *c_gpu = (float*)calloc(M * N, sizeof(float)); /* Fill with deterministic values */ for (int i = 0; i < M * K; i++) a[i] = (float)(i % 17) * 0.01f; for (int i = 0; i < K * N; i++) b[i] = (float)(i % 13) * 0.01f; /* CPU reference (naive) */ for (int m = 0; m < M; m++) { for (int n = 0; n < N; n++) { float sum = 0; for (int k = 0; k < K; k++) { sum += a[m * K + k] * b[k * N + n]; } c_cpu[m * N + n] = sum; } } /* GPU */ fm_buffer *ba = fm_buffer_from_ptr(a, M * K * sizeof(float)); fm_buffer *bb = fm_buffer_from_ptr(b, K * N * sizeof(float)); fm_buffer *bc = fm_buffer_from_ptr(c_gpu, M * N * sizeof(float)); fm_sgemm(ba, bb, bc, M, N, K, 1.0f, 0.0f); /* Copy back if buffer was non-zero-copy */ float *result = (float*)fm_buffer_data(bc); /* Compare with relaxed tolerance for large matmul */ int mismatches = 0; float max_diff = 0.0f; for (int i = 0; i < M * N; i++) { float diff = fabsf(result[i] - c_cpu[i]); if (diff > max_diff) max_diff = diff; /* Relative tolerance for large values */ float tol = fabsf(c_cpu[i]) * 1e-4f + 1e-4f; if (diff > tol) mismatches++; } if (mismatches > 0) { printf("\n 768x768 GEMM: %d mismatches, max_diff=%.6f\n", mismatches, max_diff); } ASSERT_TRUE(mismatches == 0); fm_buffer_free(ba); fm_buffer_free(bb); fm_buffer_free(bc); free(a); free(b); free(c_cpu); free(c_gpu); } static void test_sgemv_basic(void) { /* A = [[1,2],[3,4]], x = [1,1] => y = [3, 7] */ float a[4] = {1, 2, 3, 4}; float x[2] = {1, 1}; float y[2] = {0, 0}; fm_buffer *ba = fm_buffer_from_ptr(a, sizeof(a)); fm_buffer *bx = fm_buffer_from_ptr(x, sizeof(x)); fm_buffer *by = fm_buffer_from_ptr(y, sizeof(y)); fm_sgemv(ba, bx, by, 2, 2, 1.0f, 0.0f); float *result = (float*)fm_buffer_data(by); ASSERT_FLT_NEAR(3.0f, result[0], 0.01f); ASSERT_FLT_NEAR(7.0f, result[1], 0.01f); fm_buffer_free(ba); fm_buffer_free(bx); fm_buffer_free(by); } /* ================================================================ * Phase 2: Custom compute kernels * ================================================================ */ static void test_silu_gpu(void) { int n = 8; float x[8] = {-2, -1, -0.5f, 0, 0.5f, 1, 2, 3}; float out[8] = {0}; float expected[8]; for (int i = 0; i < n; i++) expected[i] = x[i] / (1.0f + expf(-x[i])); fm_buffer *bx = fm_buffer_from_ptr(x, sizeof(x)); fm_buffer *bo = fm_buffer_from_ptr(out, sizeof(out)); fm_silu(bx, bo, n); float *result = (float*)fm_buffer_data(bo); for (int i = 0; i < n; i++) { ASSERT_FLT_NEAR(expected[i], result[i], 0.01f); } fm_buffer_free(bx); fm_buffer_free(bo); } static void test_elementwise_add(void) { float a[4] = {1, 2, 3, 4}; float b[4] = {5, 6, 7, 8}; float out[4] = {0}; fm_buffer *ba = fm_buffer_from_ptr(a, sizeof(a)); fm_buffer *bb = fm_buffer_from_ptr(b, sizeof(b)); fm_buffer *bo = fm_buffer_from_ptr(out, sizeof(out)); fm_add(ba, bb, bo, 4); float *result = (float*)fm_buffer_data(bo); ASSERT_FLT_NEAR(6.0f, result[0], 1e-6f); ASSERT_FLT_NEAR(8.0f, result[1], 1e-6f); ASSERT_FLT_NEAR(10.0f, result[2], 1e-6f); ASSERT_FLT_NEAR(12.0f, result[3], 1e-6f); fm_buffer_free(ba); fm_buffer_free(bb); fm_buffer_free(bo); } static void test_elementwise_mul(void) { float a[4] = {1, 2, 3, 4}; float b[4] = {5, 6, 7, 8}; float out[4] = {0}; fm_buffer *ba = fm_buffer_from_ptr(a, sizeof(a)); fm_buffer *bb = fm_buffer_from_ptr(b, sizeof(b)); fm_buffer *bo = fm_buffer_from_ptr(out, sizeof(out)); fm_mul(ba, bb, bo, 4); float *result = (float*)fm_buffer_data(bo); ASSERT_FLT_NEAR(5.0f, result[0], 1e-6f); ASSERT_FLT_NEAR(12.0f, result[1], 1e-6f); ASSERT_FLT_NEAR(21.0f, result[2], 1e-6f); ASSERT_FLT_NEAR(32.0f, result[3], 1e-6f); fm_buffer_free(ba); fm_buffer_free(bb); fm_buffer_free(bo); } static void test_softmax_gpu(void) { /* 2 rows x 4 cols */ float x[8] = {1, 2, 3, 4, 1, 1, 1, 1}; float out[8] = {0}; fm_buffer *bx = fm_buffer_from_ptr(x, sizeof(x)); fm_buffer *bo = fm_buffer_from_ptr(out, sizeof(out)); fm_softmax(bx, bo, 2, 4); float *result = (float*)fm_buffer_data(bo); /* CPU reference for row 0: softmax([1,2,3,4]) */ float cpu_out[8]; for (int r = 0; r < 2; r++) { float mx = x[r*4]; for (int c = 1; c < 4; c++) if (x[r*4+c] > mx) mx = x[r*4+c]; float sum = 0; for (int c = 0; c < 4; c++) { cpu_out[r*4+c] = expf(x[r*4+c] - mx); sum += cpu_out[r*4+c]; } for (int c = 0; c < 4; c++) cpu_out[r*4+c] /= sum; } for (int i = 0; i < 8; i++) { ASSERT_FLT_NEAR(cpu_out[i], result[i], 0.001f); } /* Row 1 should be uniform: [0.25, 0.25, 0.25, 0.25] */ ASSERT_FLT_NEAR(0.25f, result[4], 0.001f); ASSERT_FLT_NEAR(0.25f, result[5], 0.001f); fm_buffer_free(bx); fm_buffer_free(bo); } static void test_rms_norm_gpu(void) { float x[4] = {1, 2, 3, 4}; float w[4] = {1, 1, 1, 1}; float out[4] = {0}; /* CPU reference */ float sum_sq = 0; for (int i = 0; i < 4; i++) sum_sq += x[i] * x[i]; float inv_rms = 1.0f / sqrtf(sum_sq / 4.0f + 1e-5f); float expected[4]; for (int i = 0; i < 4; i++) expected[i] = x[i] * inv_rms * w[i]; fm_buffer *bx = fm_buffer_from_ptr(x, sizeof(x)); fm_buffer *bw = fm_buffer_from_ptr(w, sizeof(w)); fm_buffer *bo = fm_buffer_from_ptr(out, sizeof(out)); fm_rms_norm(bx, bw, bo, 4, 1e-5f); float *result = (float*)fm_buffer_data(bo); for (int i = 0; i < 4; i++) { ASSERT_FLT_NEAR(expected[i], result[i], 0.001f); } fm_buffer_free(bx); fm_buffer_free(bw); fm_buffer_free(bo); } static void test_wkv7_one_gpu(void) { /* Test WKV-7 with 2 heads, head_size=4. * Compare GPU output against CPU reference. */ int n_head = 2, head_size = 4; int C = n_head * head_size; /* 8 */ int state_size = n_head * head_size * head_size; /* 32 */ float r[8], w[8], k[8], v[8], nkk[8], kka[8]; float state_cpu[32], state_gpu[32]; float y_cpu[8], y_gpu[8]; /* Fill with deterministic values */ for (int i = 0; i < C; i++) { r[i] = 0.1f * (i + 1); w[i] = 0.9f - 0.02f * i; k[i] = 0.05f * (i % 5 + 1); v[i] = 0.1f * (C - i); nkk[i] = -0.03f * (i + 1); kka[i] = 0.02f * (i + 2); } for (int i = 0; i < state_size; i++) { state_cpu[i] = 0.01f * (i % 7 + 1); state_gpu[i] = state_cpu[i]; } /* CPU reference */ for (int h = 0; h < n_head; h++) { int off = h * head_size; for (int i = 0; i < head_size; i++) { int sbase = (h * head_size + i) * head_size; float sa = 0; for (int j = 0; j < head_size; j++) sa += nkk[off + j] * state_cpu[sbase + j]; float vv = v[off + i]; float yy = 0; for (int j = 0; j < head_size; j++) { state_cpu[sbase + j] = state_cpu[sbase + j] * w[off + j] + k[off + j] * vv + sa * kka[off + j]; yy += state_cpu[sbase + j] * r[off + j]; } y_cpu[off + i] = yy; } } /* GPU */ fm_buffer *br = fm_buffer_from_ptr(r, sizeof(r)); fm_buffer *bw = fm_buffer_from_ptr(w, sizeof(w)); fm_buffer *bk = fm_buffer_from_ptr(k, sizeof(k)); fm_buffer *bv = fm_buffer_from_ptr(v, sizeof(v)); fm_buffer *bnkk = fm_buffer_from_ptr(nkk, sizeof(nkk)); fm_buffer *bkka = fm_buffer_from_ptr(kka, sizeof(kka)); fm_buffer *bst = fm_buffer_from_ptr(state_gpu, sizeof(state_gpu)); fm_buffer *by = fm_buffer_from_ptr(y_gpu, sizeof(y_gpu)); fm_wkv7_one(br, bw, bk, bv, bnkk, bkka, bst, by, n_head, head_size); /* Compare output */ float *gy = (float*)fm_buffer_data(by); for (int i = 0; i < C; i++) { ASSERT_FLT_NEAR(y_cpu[i], gy[i], 0.01f); } /* Compare state */ float *gs = (float*)fm_buffer_data(bst); for (int i = 0; i < state_size; i++) { ASSERT_FLT_NEAR(state_cpu[i], gs[i], 0.01f); } fm_buffer_free(br); fm_buffer_free(bw); fm_buffer_free(bk); fm_buffer_free(bv); fm_buffer_free(bnkk); fm_buffer_free(bkka); fm_buffer_free(bst); fm_buffer_free(by); } static void test_matvec_q8_gpu(void) { /* 4 rows x 8 cols INT8 matvec */ int rows = 4, cols = 8; int8_t x_q[8] = {10, -20, 30, -40, 50, -60, 70, -80}; float x_scale = 0.01f; int8_t w_data[32]; float w_scales[4] = {0.1f, 0.2f, 0.05f, 0.15f}; for (int i = 0; i < 32; i++) w_data[i] = (int8_t)((i * 7 - 50) % 127); float out_cpu[4] = {0}, out_gpu[4] = {0}; /* CPU reference */ for (int r = 0; r < rows; r++) { int32_t dot = 0; for (int c = 0; c < cols; c++) { dot += (int32_t)x_q[c] * (int32_t)w_data[r * cols + c]; } out_cpu[r] = (float)dot * w_scales[r] * x_scale; } fm_buffer *bxq = fm_buffer_from_ptr(x_q, sizeof(x_q)); fm_buffer *bw = fm_buffer_from_ptr(w_data, sizeof(w_data)); fm_buffer *bws = fm_buffer_from_ptr(w_scales, sizeof(w_scales)); fm_buffer *bo = fm_buffer_from_ptr(out_gpu, sizeof(out_gpu)); fm_matvec_q8(bxq, bw, bws, bo, x_scale, rows, cols); float *result = (float*)fm_buffer_data(bo); for (int r = 0; r < rows; r++) { ASSERT_FLT_NEAR(out_cpu[r], result[r], 0.01f); } fm_buffer_free(bxq); fm_buffer_free(bw); fm_buffer_free(bws); fm_buffer_free(bo); } /* ================================================================ * Phase 4: Command batching * ================================================================ */ static void test_batch_basic(void) { ASSERT_FALSE(fm_batch_active()); int ret = fm_batch_begin(); ASSERT_INT_EQ(0, ret); ASSERT_TRUE(fm_batch_active()); /* Do a couple ops within the batch */ float a[4] = {1, 2, 3, 4}; float b[4] = {5, 6, 7, 8}; float out1[4] = {0}; float out2[4] = {0}; fm_buffer *ba = fm_buffer_from_ptr(a, sizeof(a)); fm_buffer *bb = fm_buffer_from_ptr(b, sizeof(b)); fm_buffer *bo1 = fm_buffer_from_ptr(out1, sizeof(out1)); fm_buffer *bo2 = fm_buffer_from_ptr(out2, sizeof(out2)); fm_add(ba, bb, bo1, 4); fm_mul(ba, bb, bo2, 4); /* Results shouldn't be ready until batch_end */ fm_batch_end(); ASSERT_FALSE(fm_batch_active()); float *r1 = (float*)fm_buffer_data(bo1); float *r2 = (float*)fm_buffer_data(bo2); ASSERT_FLT_NEAR(6.0f, r1[0], 1e-6f); ASSERT_FLT_NEAR(5.0f, r2[0], 1e-6f); ASSERT_FLT_NEAR(32.0f, r2[3], 1e-6f); fm_buffer_free(ba); fm_buffer_free(bb); fm_buffer_free(bo1); fm_buffer_free(bo2); } /* ================================================================ * main * ================================================================ */ int main(void) { printf("\n Metal GPU Backend Tests\n"); printf(" =======================\n\n"); /* Initialize Metal once for all tests */ if (fm_init() != 0) { printf(" Metal not available — skipping all tests.\n\n"); return 0; } /* Phase 1: Infrastructure */ RUN_TEST(init_and_available); RUN_TEST(double_init_is_safe); RUN_TEST(buffer_alloc); RUN_TEST(buffer_from_ptr); RUN_TEST(buffer_alloc_write_read); /* Phase 1: MPS GEMM/GEMV */ RUN_TEST(sgemm_2x2); RUN_TEST(sgemm_alpha_beta); RUN_TEST(sgemm_768x768); RUN_TEST(sgemv_basic); /* Phase 2: Custom kernels */ RUN_TEST(silu_gpu); RUN_TEST(elementwise_add); RUN_TEST(elementwise_mul); RUN_TEST(softmax_gpu); RUN_TEST(rms_norm_gpu); RUN_TEST(wkv7_one_gpu); /* Phase 3: INT8 quantized */ RUN_TEST(matvec_q8_gpu); /* Phase 4: Batching */ RUN_TEST(batch_basic); fm_free(); TEST_REPORT(); return __ft_failed > 0 ? 1 : 0; }