Foundry/cuda/tests/test_cuda.c

481 lines
13 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/*
* test_cuda.c — Unit tests for CUDA GPU backend
*
* Tests run on the GPU server with CUDA available.
* If CUDA is unavailable, tests gracefully skip.
*
* Build: make USE_CUDA=1 test-cuda
*/
#include "lib/test.h"
#include "cuda/fc_cuda.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
/* ================================================================
* Lifecycle tests
* ================================================================ */
static void test_init_shutdown(void) {
int ret = fc_init();
if (ret != 0) {
printf(" [SKIP] CUDA not available\n");
return;
}
ASSERT_TRUE(fc_available());
fc_free();
ASSERT_FALSE(fc_available());
/* Re-init should work (idempotent pattern) */
ret = fc_init();
ASSERT_TRUE(ret == 0);
ASSERT_TRUE(fc_available());
fc_free();
}
static void test_double_init(void) {
int ret = fc_init();
if (ret != 0) { printf(" [SKIP] CUDA not available\n"); return; }
/* Second init should be idempotent */
int ret2 = fc_init();
ASSERT_TRUE(ret2 == 0);
ASSERT_TRUE(fc_available());
fc_free();
}
/* ================================================================
* Buffer tests
* ================================================================ */
static void test_buffer_roundtrip(void) {
if (fc_init() != 0) { printf(" [SKIP]\n"); return; }
float data[] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f };
size_t bytes = sizeof(data);
fc_buffer *buf = fc_buffer_from_ptr(data, bytes);
ASSERT_NOT_NULL(buf);
ASSERT_TRUE(fc_buffer_size(buf) == bytes);
/* Read back */
float result[5] = {0};
fc_buffer_to_cpu(buf, result);
for (int i = 0; i < 5; i++) {
ASSERT_TRUE(fabsf(result[i] - data[i]) < 1e-6f);
}
fc_buffer_free(buf);
fc_free();
}
static void test_buffer_alloc_and_write(void) {
if (fc_init() != 0) { printf(" [SKIP]\n"); return; }
fc_buffer *buf = fc_buffer_alloc(1024 * sizeof(float));
ASSERT_NOT_NULL(buf);
ASSERT_TRUE(fc_buffer_size(buf) == 1024 * sizeof(float));
/* Write data to it */
float *data = (float*)malloc(1024 * sizeof(float));
for (int i = 0; i < 1024; i++) data[i] = (float)i;
fc_buffer_from_cpu(buf, data);
/* Read back */
float *result = (float*)malloc(1024 * sizeof(float));
fc_buffer_to_cpu(buf, result);
for (int i = 0; i < 1024; i++) {
ASSERT_TRUE(fabsf(result[i] - (float)i) < 1e-6f);
}
free(data);
free(result);
fc_buffer_free(buf);
fc_free();
}
static void test_buffer_large(void) {
if (fc_init() != 0) { printf(" [SKIP]\n"); return; }
/* 1 MB buffer */
size_t n = 256 * 1024;
float *data = (float*)malloc(n * sizeof(float));
for (size_t i = 0; i < n; i++) data[i] = (float)(i % 1000) * 0.001f;
fc_buffer *buf = fc_buffer_from_ptr(data, n * sizeof(float));
ASSERT_NOT_NULL(buf);
float *result = (float*)malloc(n * sizeof(float));
fc_buffer_to_cpu(buf, result);
/* Spot check */
ASSERT_TRUE(fabsf(result[0] - data[0]) < 1e-5f);
ASSERT_TRUE(fabsf(result[1000] - data[1000]) < 1e-5f);
ASSERT_TRUE(fabsf(result[n - 1] - data[n - 1]) < 1e-5f);
free(data);
free(result);
fc_buffer_free(buf);
fc_free();
}
/* ================================================================
* cuBLAS SGEMM tests
* ================================================================ */
static void test_sgemm_identity(void) {
if (fc_init() != 0) { printf(" [SKIP]\n"); return; }
/* C[2,2] = A[2,3] @ B[3,2] */
float A[] = { 1, 2, 3,
4, 5, 6 };
float B[] = { 7, 8,
9, 10,
11, 12 };
float C[4] = {0};
/* Expected: C = [[58, 64], [139, 154]] */
float expected[] = { 58, 64, 139, 154 };
fc_buffer *ba = fc_buffer_from_ptr(A, sizeof(A));
fc_buffer *bb = fc_buffer_from_ptr(B, sizeof(B));
fc_buffer *bc = fc_buffer_alloc(sizeof(C));
ASSERT_NOT_NULL(ba);
ASSERT_NOT_NULL(bb);
ASSERT_NOT_NULL(bc);
fc_sgemm(ba, bb, bc, 2, 2, 3, 1.0f, 0.0f);
fc_sync();
fc_buffer_to_cpu(bc, C);
for (int i = 0; i < 4; i++) {
ASSERT_TRUE(fabsf(C[i] - expected[i]) < 1e-3f);
}
fc_buffer_free(ba);
fc_buffer_free(bb);
fc_buffer_free(bc);
fc_free();
}
static void test_sgemm_larger(void) {
if (fc_init() != 0) { printf(" [SKIP]\n"); return; }
/* 64x64 matmul */
int M = 64, K = 64, N = 64;
float *A = (float*)malloc(M * K * sizeof(float));
float *B = (float*)malloc(K * N * sizeof(float));
float *C_gpu = (float*)calloc(M * N, sizeof(float));
float *C_cpu = (float*)calloc(M * N, sizeof(float));
for (int i = 0; i < M * K; i++) A[i] = (float)(i % 7) * 0.1f;
for (int i = 0; i < K * N; i++) B[i] = (float)(i % 11) * 0.1f;
/* CPU reference */
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 */
fc_buffer *ba = fc_buffer_from_ptr(A, M * K * sizeof(float));
fc_buffer *bb = fc_buffer_from_ptr(B, K * N * sizeof(float));
fc_buffer *bc = fc_buffer_alloc(M * N * sizeof(float));
fc_sgemm(ba, bb, bc, M, N, K, 1.0f, 0.0f);
fc_sync();
fc_buffer_to_cpu(bc, C_gpu);
/* Compare */
float max_err = 0;
for (int i = 0; i < M * N; i++) {
float err = fabsf(C_gpu[i] - C_cpu[i]);
if (err > max_err) max_err = err;
}
ASSERT_TRUE(max_err < 0.01f);
free(A); free(B); free(C_gpu); free(C_cpu);
fc_buffer_free(ba);
fc_buffer_free(bb);
fc_buffer_free(bc);
fc_free();
}
/* ================================================================
* cuBLAS SGEMV tests
* ================================================================ */
static void test_sgemv(void) {
if (fc_init() != 0) { printf(" [SKIP]\n"); return; }
/* y[3] = A[3,4] @ x[4] */
float A[] = { 1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12 };
float x[] = { 1, 2, 3, 4 };
float y[3] = {0};
/* Expected: [30, 70, 110] */
float expected[] = { 30.0f, 70.0f, 110.0f };
fc_buffer *ba = fc_buffer_from_ptr(A, sizeof(A));
fc_buffer *bx = fc_buffer_from_ptr(x, sizeof(x));
fc_buffer *by = fc_buffer_alloc(sizeof(y));
fc_sgemv(ba, bx, by, 3, 4, 1.0f, 0.0f);
fc_sync();
fc_buffer_to_cpu(by, y);
for (int i = 0; i < 3; i++) {
ASSERT_TRUE(fabsf(y[i] - expected[i]) < 1e-3f);
}
fc_buffer_free(ba);
fc_buffer_free(bx);
fc_buffer_free(by);
fc_free();
}
/* ================================================================
* Custom kernel tests
* ================================================================ */
static void test_add(void) {
if (fc_init() != 0) { printf(" [SKIP]\n"); return; }
int n = 1024;
float *a = (float*)malloc(n * sizeof(float));
float *b = (float*)malloc(n * sizeof(float));
float *out = (float*)malloc(n * sizeof(float));
for (int i = 0; i < n; i++) {
a[i] = (float)i;
b[i] = (float)(n - i);
}
fc_buffer *ba = fc_buffer_from_ptr(a, n * sizeof(float));
fc_buffer *bb = fc_buffer_from_ptr(b, n * sizeof(float));
fc_buffer *bo = fc_buffer_alloc(n * sizeof(float));
fc_add(ba, bb, bo, n);
fc_sync();
fc_buffer_to_cpu(bo, out);
for (int i = 0; i < n; i++) {
ASSERT_TRUE(fabsf(out[i] - (float)n) < 1e-5f);
}
free(a); free(b); free(out);
fc_buffer_free(ba);
fc_buffer_free(bb);
fc_buffer_free(bo);
fc_free();
}
static void test_silu(void) {
if (fc_init() != 0) { printf(" [SKIP]\n"); return; }
int n = 512;
float *x = (float*)malloc(n * sizeof(float));
float *out_gpu = (float*)malloc(n * sizeof(float));
for (int i = 0; i < n; i++) {
x[i] = (float)(i - n / 2) * 0.01f;
}
fc_buffer *bx = fc_buffer_from_ptr(x, n * sizeof(float));
fc_buffer *bo = fc_buffer_alloc(n * sizeof(float));
fc_silu(bx, bo, n);
fc_sync();
fc_buffer_to_cpu(bo, out_gpu);
/* Compare with CPU reference */
float max_err = 0;
for (int i = 0; i < n; i++) {
float v = x[i];
float expected = v / (1.0f + expf(-v));
float err = fabsf(out_gpu[i] - expected);
if (err > max_err) max_err = err;
}
ASSERT_TRUE(max_err < 1e-5f);
free(x); free(out_gpu);
fc_buffer_free(bx);
fc_buffer_free(bo);
fc_free();
}
static void test_rms_norm(void) {
if (fc_init() != 0) { printf(" [SKIP]\n"); return; }
int n = 768; /* typical hidden dim */
float *x = (float*)malloc(n * sizeof(float));
float *w = (float*)malloc(n * sizeof(float));
float *out_gpu = (float*)malloc(n * sizeof(float));
for (int i = 0; i < n; i++) {
x[i] = (float)(i % 17) * 0.1f - 0.8f;
w[i] = 1.0f + (float)(i % 5) * 0.01f;
}
fc_buffer *bx = fc_buffer_from_ptr(x, n * sizeof(float));
fc_buffer *bw = fc_buffer_from_ptr(w, n * sizeof(float));
fc_buffer *bo = fc_buffer_alloc(n * sizeof(float));
float eps = 1e-5f;
fc_rms_norm(bx, bw, bo, n, eps);
fc_sync();
fc_buffer_to_cpu(bo, out_gpu);
/* CPU reference */
float sum_sq = 0;
for (int i = 0; i < n; i++) sum_sq += x[i] * x[i];
float rms = sqrtf(sum_sq / n + eps);
float max_err = 0;
for (int i = 0; i < n; i++) {
float expected = (x[i] / rms) * w[i];
float err = fabsf(out_gpu[i] - expected);
if (err > max_err) max_err = err;
}
ASSERT_TRUE(max_err < 1e-4f);
free(x); free(w); free(out_gpu);
fc_buffer_free(bx);
fc_buffer_free(bw);
fc_buffer_free(bo);
fc_free();
}
static void test_softmax(void) {
if (fc_init() != 0) { printf(" [SKIP]\n"); return; }
int rows = 4, cols = 256;
float *x = (float*)malloc(rows * cols * sizeof(float));
float *out_gpu = (float*)malloc(rows * cols * sizeof(float));
for (int i = 0; i < rows * cols; i++) {
x[i] = (float)(i % 31) * 0.1f - 1.5f;
}
fc_buffer *bx = fc_buffer_from_ptr(x, rows * cols * sizeof(float));
fc_buffer *bo = fc_buffer_alloc(rows * cols * sizeof(float));
fc_softmax(bx, bo, rows, cols);
fc_sync();
fc_buffer_to_cpu(bo, out_gpu);
/* Check each row sums to 1 and all values are in [0, 1] */
for (int r = 0; r < rows; r++) {
float sum = 0;
for (int c = 0; c < cols; c++) {
float v = out_gpu[r * cols + c];
ASSERT_TRUE(v >= 0.0f && v <= 1.0f);
sum += v;
}
ASSERT_TRUE(fabsf(sum - 1.0f) < 1e-4f);
}
free(x); free(out_gpu);
fc_buffer_free(bx);
fc_buffer_free(bo);
fc_free();
}
static void test_matvec_q8(void) {
if (fc_init() != 0) { printf(" [SKIP]\n"); return; }
int rows = 64, cols = 128;
/* Create quantized weight matrix (int8) and scales */
signed char *w = (signed char*)malloc(rows * cols);
float *w_scales = (float*)malloc(rows * sizeof(float));
for (int i = 0; i < rows; i++) {
w_scales[i] = 0.01f * (1.0f + (float)(i % 3));
for (int j = 0; j < cols; j++) {
w[i * cols + j] = (signed char)((i + j) % 127);
}
}
/* Create quantized input vector */
signed char *x_q = (signed char*)malloc(cols);
float x_scale = 0.02f;
for (int j = 0; j < cols; j++) {
x_q[j] = (signed char)(j % 64 - 32);
}
float *out_gpu = (float*)malloc(rows * sizeof(float));
fc_buffer *bxq = fc_buffer_from_ptr(x_q, cols);
fc_buffer *bw = fc_buffer_from_ptr(w, rows * cols);
fc_buffer *bws = fc_buffer_from_ptr(w_scales, rows * sizeof(float));
fc_buffer *bo = fc_buffer_alloc(rows * sizeof(float));
fc_matvec_q8(bxq, bw, bws, bo, x_scale, rows, cols);
fc_sync();
fc_buffer_to_cpu(bo, out_gpu);
/* CPU reference */
float max_err = 0;
for (int i = 0; i < rows; i++) {
int dot = 0;
for (int j = 0; j < cols; j++) {
dot += (int)x_q[j] * (int)w[i * cols + j];
}
float expected = x_scale * w_scales[i] * (float)dot;
float err = fabsf(out_gpu[i] - expected);
if (err > max_err) max_err = err;
}
ASSERT_TRUE(max_err < 0.01f);
free(w); free(w_scales); free(x_q); free(out_gpu);
fc_buffer_free(bxq);
fc_buffer_free(bw);
fc_buffer_free(bws);
fc_buffer_free(bo);
fc_free();
}
/* ================================================================
* Main
* ================================================================ */
int main(void) {
printf("CUDA Backend Tests\n");
printf("==================\n");
/* Lifecycle */
RUN_TEST(init_shutdown);
RUN_TEST(double_init);
/* Buffers */
RUN_TEST(buffer_roundtrip);
RUN_TEST(buffer_alloc_and_write);
RUN_TEST(buffer_large);
/* cuBLAS */
RUN_TEST(sgemm_identity);
RUN_TEST(sgemm_larger);
RUN_TEST(sgemv);
/* Custom kernels */
RUN_TEST(add);
RUN_TEST(silu);
RUN_TEST(rms_norm);
RUN_TEST(softmax);
RUN_TEST(matvec_q8);
TEST_REPORT();
return __ft_failed > 0 ? 1 : 0;
}