454 lines
14 KiB
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;
|
|
}
|