Foundry/rwkvx/tests/test_rfa_train.c

454 lines
14 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/*
* test_rfa_train.c — Unit tests for RFA LoRA training
*
* Tests backward pass correctness via finite-difference gradient verification,
* LoRA adapter creation, parameter collection, data loading, and config defaults.
*/
#include "lib/test.h"
#include "rwkvx/rfa_train.h"
#include "rwkvx/rfa.h"
#include "lora/lora.h"
#include "tensor/tensor.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
/* ================================================================
* Helper: random float in [-1, 1]
* ================================================================ */
static float randf(void) {
return 2.0f * ((float)rand() / RAND_MAX) - 1.0f;
}
static void fill_random(float *buf, int n) {
for (int i = 0; i < n; i++)
buf[i] = randf() * 0.5f;
}
/* ================================================================
* Test 1: RMSNorm backward (finite difference)
* ================================================================ */
static void rms_norm_fwd(const float *x, const float *w, float *out,
int n, float eps) {
float sum_sq = 0.0f;
for (int i = 0; i < n; i++)
sum_sq += x[i] * x[i];
float scale = 1.0f / sqrtf(sum_sq / n + eps);
for (int i = 0; i < n; i++)
out[i] = x[i] * scale * w[i];
}
static void test_rms_norm_backward(void) {
int c = 16;
float eps = 1e-6f;
float h = 1e-4f;
float *x = (float*)malloc(c * sizeof(float));
float *w = (float*)malloc(c * sizeof(float));
float *d_out = (float*)malloc(c * sizeof(float));
float *d_x = (float*)calloc(c, sizeof(float));
float *out1 = (float*)malloc(c * sizeof(float));
float *out2 = (float*)malloc(c * sizeof(float));
srand(42);
fill_random(x, c);
for (int i = 0; i < c; i++) w[i] = 0.5f + randf() * 0.5f;
fill_random(d_out, c);
/* Analytical gradient */
rfa_rms_norm_backward(d_out, x, w, d_x, c, eps);
/* Finite difference */
float max_err = 0.0f;
for (int i = 0; i < c; i++) {
float orig = x[i];
x[i] = orig + h;
rms_norm_fwd(x, w, out1, c, eps);
float f_plus = 0.0f;
for (int j = 0; j < c; j++) f_plus += out1[j] * d_out[j];
x[i] = orig - h;
rms_norm_fwd(x, w, out2, c, eps);
float f_minus = 0.0f;
for (int j = 0; j < c; j++) f_minus += out2[j] * d_out[j];
float fd_grad = (f_plus - f_minus) / (2.0f * h);
float err = fabsf(d_x[i] - fd_grad);
if (err > max_err) max_err = err;
x[i] = orig;
}
/* Tolerance: relative to gradient magnitude */
ASSERT_TRUE(max_err < 0.01f);
free(x); free(w); free(d_out); free(d_x);
free(out1); free(out2);
}
/* ================================================================
* Test 2: SwiGLU backward (finite difference)
* ================================================================ */
static float fast_silu(float x) {
return x / (1.0f + expf(-x));
}
/* Minimal SwiGLU forward for FD testing (no LoRA) */
static void swiglu_fwd(const float *x, const float *gate_w, const float *up_w,
const float *down_w, float *out, int c, int inter) {
float *gate = (float*)calloc(inter, sizeof(float));
float *up_v = (float*)calloc(inter, sizeof(float));
float *act = (float*)calloc(inter, sizeof(float));
/* gate = gate_w^T @ x (gate_w stored as [c, inter] for t_matvec convention) */
for (int j = 0; j < inter; j++) {
float s = 0.0f;
for (int i = 0; i < c; i++) s += x[i] * gate_w[i * inter + j];
gate[j] = s;
}
for (int j = 0; j < inter; j++) {
float s = 0.0f;
for (int i = 0; i < c; i++) s += x[i] * up_w[i * inter + j];
up_v[j] = s;
}
for (int i = 0; i < inter; i++)
act[i] = fast_silu(gate[i]) * up_v[i];
memset(out, 0, c * sizeof(float));
for (int i = 0; i < c; i++) {
float s = 0.0f;
for (int j = 0; j < inter; j++) s += act[j] * down_w[j * c + i];
out[i] = s;
}
free(gate); free(up_v); free(act);
}
static void test_swiglu_backward(void) {
int c = 8;
int inter = 12;
float h = 1e-4f;
float *x = (float*)malloc(c * sizeof(float));
float *d_out = (float*)malloc(c * sizeof(float));
float *d_x = (float*)calloc(c, sizeof(float));
float *out1 = (float*)malloc(c * sizeof(float));
float *out2 = (float*)malloc(c * sizeof(float));
/* Create synthetic weights in transposed layout [c, inter] / [inter, c] */
float *gate_w = (float*)malloc(c * inter * sizeof(float));
float *up_w = (float*)malloc(c * inter * sizeof(float));
float *down_w = (float*)malloc(inter * c * sizeof(float));
srand(123);
fill_random(x, c);
fill_random(d_out, c);
fill_random(gate_w, c * inter);
fill_random(up_w, c * inter);
fill_random(down_w, inter * c);
/* Set up rfa_qwen_weights struct for backward */
rfa_qwen_weights w;
memset(&w, 0, sizeof(w));
w.gate_proj = gate_w;
w.up_proj = up_w;
w.down_proj = down_w;
w.inter_size = inter;
/* NULL LoRA layers */
fl_layer *lora[7] = {NULL};
/* Analytical gradient */
rfa_swiglu_backward(d_out, x, &w, lora, d_x, c, inter);
/* Finite difference */
float max_err = 0.0f;
for (int i = 0; i < c; i++) {
float orig = x[i];
x[i] = orig + h;
swiglu_fwd(x, gate_w, up_w, down_w, out1, c, inter);
float f_plus = 0.0f;
for (int j = 0; j < c; j++) f_plus += out1[j] * d_out[j];
x[i] = orig - h;
swiglu_fwd(x, gate_w, up_w, down_w, out2, c, inter);
float f_minus = 0.0f;
for (int j = 0; j < c; j++) f_minus += out2[j] * d_out[j];
float fd_grad = (f_plus - f_minus) / (2.0f * h);
float err = fabsf(d_x[i] - fd_grad);
if (err > max_err) max_err = err;
x[i] = orig;
}
ASSERT_TRUE(max_err < 0.05f);
free(x); free(d_out); free(d_x);
free(out1); free(out2);
free(gate_w); free(up_w); free(down_w);
}
/* ================================================================
* Test 3: GQA attention backward (finite difference)
* ================================================================ */
static void gqa_fwd(const float *q, const float *k_cache, const float *v_cache,
int cache_len, float *y, float *weights_out,
int n_q, int n_kv, int hd) {
int kv_dim = n_kv * hd;
int group_size = n_q / n_kv;
float scale = 1.0f / sqrtf((float)hd);
memset(y, 0, n_q * hd * sizeof(float));
for (int qh = 0; qh < n_q; qh++) {
int kvh = qh / group_size;
const float *q_h = &q[qh * hd];
float *scores = (float*)malloc(cache_len * sizeof(float));
float max_s = -1e30f;
for (int t = 0; t < cache_len; t++) {
const float *kt = &k_cache[t * kv_dim + kvh * hd];
float dot = 0.0f;
for (int j = 0; j < hd; j++) dot += q_h[j] * kt[j];
scores[t] = dot * scale;
if (scores[t] > max_s) max_s = scores[t];
}
float sum_e = 0.0f;
for (int t = 0; t < cache_len; t++) {
scores[t] = expf(scores[t] - max_s);
sum_e += scores[t];
}
for (int t = 0; t < cache_len; t++) scores[t] /= sum_e;
if (weights_out)
memcpy(&weights_out[qh * cache_len], scores, cache_len * sizeof(float));
float *y_h = &y[qh * hd];
for (int t = 0; t < cache_len; t++) {
const float *vt = &v_cache[t * kv_dim + kvh * hd];
for (int j = 0; j < hd; j++)
y_h[j] += scores[t] * vt[j];
}
free(scores);
}
}
static void test_gqa_attention_backward(void) {
/* Small synthetic GQA: 4 Q heads, 2 KV heads, head_dim=4, cache=3 */
int n_q = 4, n_kv = 2, hd = 4, cl = 3;
int c = n_q * hd; /* 16 */
int kv_dim = n_kv * hd; /* 8 */
float h_eps = 1e-4f;
float *q = (float*)malloc(c * sizeof(float));
float *k_cache = (float*)malloc(cl * kv_dim * sizeof(float));
float *v_cache = (float*)malloc(cl * kv_dim * sizeof(float));
float *d_y = (float*)malloc(c * sizeof(float));
float *y = (float*)malloc(c * sizeof(float));
float *y2 = (float*)malloc(c * sizeof(float));
float *weights = (float*)malloc(n_q * cl * sizeof(float));
float *d_q = (float*)calloc(c, sizeof(float));
float *d_k = (float*)calloc(kv_dim, sizeof(float));
float *d_v = (float*)calloc(kv_dim, sizeof(float));
srand(777);
fill_random(q, c);
fill_random(k_cache, cl * kv_dim);
fill_random(v_cache, cl * kv_dim);
fill_random(d_y, c);
/* Forward + save weights */
gqa_fwd(q, k_cache, v_cache, cl, y, weights, n_q, n_kv, hd);
/* Analytical gradient */
rfa_gqa_attention_backward(d_y, q, k_cache, v_cache, weights, cl,
d_q, d_k, d_v, n_q, n_kv, hd);
/* Finite difference for d_q */
float max_err_q = 0.0f;
for (int i = 0; i < c; i++) {
float orig = q[i];
q[i] = orig + h_eps;
gqa_fwd(q, k_cache, v_cache, cl, y2, NULL, n_q, n_kv, hd);
float f_plus = 0.0f;
for (int j = 0; j < c; j++) f_plus += y2[j] * d_y[j];
q[i] = orig - h_eps;
gqa_fwd(q, k_cache, v_cache, cl, y2, NULL, n_q, n_kv, hd);
float f_minus = 0.0f;
for (int j = 0; j < c; j++) f_minus += y2[j] * d_y[j];
float fd = (f_plus - f_minus) / (2.0f * h_eps);
float err = fabsf(d_q[i] - fd);
if (err > max_err_q) max_err_q = err;
q[i] = orig;
}
ASSERT_TRUE(max_err_q < 0.05f);
free(q); free(k_cache); free(v_cache); free(d_y);
free(y); free(y2); free(weights);
free(d_q); free(d_k); free(d_v);
}
/* ================================================================
* Test 4: Config defaults
* ================================================================ */
static void test_config_defaults(void) {
rfa_train_config cfg = rfa_train_config_defaults();
ASSERT_INT_EQ(cfg.lora_rank, 32);
ASSERT_FLT_NEAR(cfg.lora_alpha, 64.0f, 0.01f);
ASSERT_FLT_NEAR(cfg.lr, 2e-5f, 1e-7f);
ASSERT_INT_EQ(cfg.epochs, 3);
ASSERT_INT_EQ(cfg.max_seq_len, 256);
ASSERT_INT_EQ(cfg.warmup_steps, 100);
ASSERT_INT_EQ(cfg.save_every, 1000);
ASSERT_INT_EQ(cfg.log_every, 10);
ASSERT_INT_EQ(cfg.max_cache, 2048);
}
/* ================================================================
* Test 5: Example free
* ================================================================ */
static void test_example_free(void) {
rfa_train_example ex;
ex.seq_len = 5;
ex.input_ids = (uint32_t*)malloc(5 * sizeof(uint32_t));
ex.target_ids = (uint32_t*)malloc(5 * sizeof(uint32_t));
rfa_train_example_free(&ex);
/* No crash = pass */
ASSERT_TRUE(1);
}
static void test_examples_batch_free(void) {
int n = 3;
rfa_train_example *exs = (rfa_train_example*)calloc(n, sizeof(rfa_train_example));
for (int i = 0; i < n; i++) {
exs[i].seq_len = 4;
exs[i].input_ids = (uint32_t*)malloc(4 * sizeof(uint32_t));
exs[i].target_ids = (uint32_t*)malloc(4 * sizeof(uint32_t));
}
rfa_train_examples_free(exs, n);
ASSERT_TRUE(1);
}
/* ================================================================
* Test 6: Binary data loader (write then read)
* ================================================================ */
static void test_load_pretokenized(void) {
const char *path = "/tmp/test_rfa_pretok.bin";
/* Write a small binary file */
FILE *f = fopen(path, "wb");
ASSERT_NOT_NULL(f);
uint32_t n_ex = 2;
fwrite(&n_ex, sizeof(uint32_t), 1, f);
/* Example 1: [10, 20, 30, 40] */
uint32_t len1 = 4;
uint32_t toks1[] = {10, 20, 30, 40};
fwrite(&len1, sizeof(uint32_t), 1, f);
fwrite(toks1, sizeof(uint32_t), 4, f);
/* Example 2: [5, 15, 25] */
uint32_t len2 = 3;
uint32_t toks2[] = {5, 15, 25};
fwrite(&len2, sizeof(uint32_t), 1, f);
fwrite(toks2, sizeof(uint32_t), 3, f);
fclose(f);
/* Load */
int n_loaded;
rfa_train_example *exs = rfa_load_pretokenized(path, 512, &n_loaded);
ASSERT_NOT_NULL(exs);
ASSERT_INT_EQ(n_loaded, 2);
/* Example 1: input=[10,20,30], target=[20,30,40] */
ASSERT_INT_EQ(exs[0].seq_len, 3);
ASSERT_INT_EQ(exs[0].input_ids[0], 10);
ASSERT_INT_EQ(exs[0].input_ids[2], 30);
ASSERT_INT_EQ(exs[0].target_ids[0], 20);
ASSERT_INT_EQ(exs[0].target_ids[2], 40);
/* Example 2: input=[5,15], target=[15,25] */
ASSERT_INT_EQ(exs[1].seq_len, 2);
ASSERT_INT_EQ(exs[1].input_ids[0], 5);
ASSERT_INT_EQ(exs[1].target_ids[1], 25);
rfa_train_examples_free(exs, n_loaded);
}
/* ================================================================
* Test 7: LoRA adapter creation (verify dimensions)
* ================================================================ */
static void test_create_qwen_lora(void) {
/* We can't load a real model, but we can test the function signature
* and parameter count logic by creating a mock scenario.
* For now, test with NULL model returns NULL. */
fl_adapter *adapter = rfa_create_qwen_lora(NULL, 32, 64.0f);
ASSERT_NULL(adapter);
/* Also test collect_params with NULL */
int n;
tm_tensor **params = rfa_collect_params(NULL, &n);
ASSERT_NULL(params);
ASSERT_TRUE(1);
}
/* ================================================================
* Test 8: Null input handling
* ================================================================ */
static void test_null_inputs(void) {
/* Load with NULL path */
int n;
rfa_train_example *exs = rfa_load_pretokenized(NULL, 256, &n);
ASSERT_NULL(exs);
/* Free NULL */
rfa_train_example_free(NULL);
rfa_train_examples_free(NULL, 0);
ASSERT_TRUE(1);
}
/* ================================================================
* Main
* ================================================================ */
int main(void) {
printf("\nRFA Training Tests\n");
printf("==================\n\n");
RUN_TEST(rms_norm_backward);
RUN_TEST(swiglu_backward);
RUN_TEST(gqa_attention_backward);
RUN_TEST(config_defaults);
RUN_TEST(example_free);
RUN_TEST(examples_batch_free);
RUN_TEST(load_pretokenized);
RUN_TEST(create_qwen_lora);
RUN_TEST(null_inputs);
TEST_REPORT();
return __ft_failed > 0 ? 1 : 0;
}