snapkitty
machine-learning
c
snapkitty-mlc / src /sk_model.c
SNAPKITTYWEST's picture
Upload folder using huggingface_hub
8b41169 verified
Raw History Blame Contribute Delete
13.3 kB
/* sk_model.c β€” SnapKitty MLC: computation graph, autograd, training loop
*
* Authors: Ahmad Ali Parr, Jessica L. Williams (SNAPKITTYWEST)
* Source: SNAPKITTYAGENT9NOVA/MLC
* License: BSL-1.1 / AGPL-3.0 / MPL-2.0
*/
#include "../include/sk_defs.h"
#include "../include/sk_arena.h"
#include "../include/sk_random.h"
#include "../include/sk_matrix.h"
#include "../include/sk_model.h"
/* ── Internal helpers ─────────────────────────────────────────────── */
static sk_model_var* _mv_unary_impl(
sk_arena* arena, sk_model* model,
sk_model_var* input, u32 rows, u32 cols,
u32 flags, sk_mv_op op
) {
if (input->flags & SK_MV_REQUIRES_GRAD) flags |= SK_MV_REQUIRES_GRAD;
sk_model_var* out = sk_mv_create(arena, model, rows, cols, flags);
out->op = op;
out->inputs[0] = input;
return out;
}
static sk_model_var* _mv_binary_impl(
sk_arena* arena, sk_model* model,
sk_model_var* a, sk_model_var* b,
u32 rows, u32 cols, u32 flags, sk_mv_op op
) {
if ((a->flags & SK_MV_REQUIRES_GRAD) || (b->flags & SK_MV_REQUIRES_GRAD))
flags |= SK_MV_REQUIRES_GRAD;
sk_model_var* out = sk_mv_create(arena, model, rows, cols, flags);
out->op = op;
out->inputs[0] = a;
out->inputs[1] = b;
return out;
}
/* ── Variable constructors ────────────────────────────────────────── */
sk_model_var* sk_mv_create(
sk_arena* arena, sk_model* model, u32 rows, u32 cols, u32 flags
) {
sk_model_var* out = SK_PUSH_STRUCT(arena, sk_model_var);
out->index = model->num_vars++;
out->flags = flags;
out->op = SK_OP_CREATE;
out->val = sk_mat_create(arena, rows, cols);
if (flags & SK_MV_REQUIRES_GRAD)
out->grad = sk_mat_create(arena, rows, cols);
if (flags & SK_MV_INPUT) model->input = out;
if (flags & SK_MV_OUTPUT) model->output = out;
if (flags & SK_MV_DESIRED_OUTPUT) model->desired_output = out;
if (flags & SK_MV_COST) model->cost = out;
return out;
}
sk_model_var* sk_mv_relu(
sk_arena* arena, sk_model* model, sk_model_var* input, u32 flags
) {
return _mv_unary_impl(arena, model, input,
input->val->rows, input->val->cols, flags, SK_OP_RELU);
}
sk_model_var* sk_mv_softmax(
sk_arena* arena, sk_model* model, sk_model_var* input, u32 flags
) {
return _mv_unary_impl(arena, model, input,
input->val->rows, input->val->cols, flags, SK_OP_SOFTMAX);
}
sk_model_var* sk_mv_add(
sk_arena* arena, sk_model* model,
sk_model_var* a, sk_model_var* b, u32 flags
) {
if (a->val->rows != b->val->rows || a->val->cols != b->val->cols) return NULL;
return _mv_binary_impl(arena, model, a, b,
a->val->rows, a->val->cols, flags, SK_OP_ADD);
}
sk_model_var* sk_mv_sub(
sk_arena* arena, sk_model* model,
sk_model_var* a, sk_model_var* b, u32 flags
) {
if (a->val->rows != b->val->rows || a->val->cols != b->val->cols) return NULL;
return _mv_binary_impl(arena, model, a, b,
a->val->rows, a->val->cols, flags, SK_OP_SUB);
}
sk_model_var* sk_mv_matmul(
sk_arena* arena, sk_model* model,
sk_model_var* a, sk_model_var* b, u32 flags
) {
if (a->val->cols != b->val->rows) return NULL;
return _mv_binary_impl(arena, model, a, b,
a->val->rows, b->val->cols, flags, SK_OP_MATMUL);
}
sk_model_var* sk_mv_cross_entropy(
sk_arena* arena, sk_model* model,
sk_model_var* p, sk_model_var* q, u32 flags
) {
if (p->val->rows != q->val->rows || p->val->cols != q->val->cols) return NULL;
return _mv_binary_impl(arena, model, p, q,
p->val->rows, p->val->cols, flags, SK_OP_CROSS_ENTROPY);
}
/* ── Program: topological sort (iterative DFS) ───────────────────── */
sk_model_prog sk_model_prog_create(
sk_arena* arena, sk_model* model, sk_model_var* out_var
) {
sk_arena_temp scratch = sk_arena_scratch_get(&arena, 1);
b8* visited = SK_PUSH_ARRAY(scratch.arena, b8, model->num_vars);
sk_model_var** stack = SK_PUSH_ARRAY(scratch.arena, sk_model_var*, model->num_vars);
sk_model_var** out = SK_PUSH_ARRAY(scratch.arena, sk_model_var*, model->num_vars);
u32 stack_size = 0, out_size = 0;
stack[stack_size++] = out_var;
while (stack_size > 0) {
sk_model_var* cur = stack[--stack_size];
if (cur->index >= model->num_vars) continue;
if (visited[cur->index]) {
if (out_size < model->num_vars) out[out_size++] = cur;
continue;
}
visited[cur->index] = true;
if (stack_size < model->num_vars) stack[stack_size++] = cur;
u32 n = SK_MV_NUM_INPUTS(cur->op);
for (u32 i = 0; i < n; i++) {
sk_model_var* inp = cur->inputs[i];
if (!inp || inp->index >= model->num_vars || visited[inp->index]) continue;
/* remove duplicates in stack */
for (u32 j = 0; j < stack_size; j++) {
if (stack[j] == inp) {
for (u32 k = j; k < stack_size - 1; k++) stack[k] = stack[k+1];
stack_size--;
break;
}
}
if (stack_size < model->num_vars) stack[stack_size++] = inp;
}
}
sk_model_prog prog = {
.size = out_size,
.vars = SK_PUSH_ARRAY_NZ(arena, sk_model_var*, out_size)
};
memcpy(prog.vars, out, sizeof(sk_model_var*) * out_size);
sk_arena_scratch_release(scratch);
return prog;
}
/* ── Forward pass ─────────────────────────────────────────────────── */
void sk_model_prog_compute(sk_model_prog* prog) {
for (u32 i = 0; i < prog->size; i++) {
sk_model_var* cur = prog->vars[i];
sk_model_var* a = cur->inputs[0];
sk_model_var* b = cur->inputs[1];
switch (cur->op) {
case SK_OP_NULL:
case SK_OP_CREATE: break;
case _SK_OP_UNARY_START: break;
case SK_OP_RELU: sk_mat_relu(cur->val, a->val); break;
case SK_OP_SOFTMAX: sk_mat_softmax(cur->val, a->val); break;
case _SK_OP_BINARY_START: break;
case SK_OP_ADD: sk_mat_add(cur->val, a->val, b->val); break;
case SK_OP_SUB: sk_mat_sub(cur->val, a->val, b->val); break;
case SK_OP_MATMUL: sk_mat_mul(cur->val, a->val, b->val, 1, 0, 0); break;
case SK_OP_CROSS_ENTROPY: sk_mat_cross_entropy(cur->val, a->val, b->val); break;
}
}
}
/* ── Backward pass (reverse-mode autodiff) ───────────────────────── */
void sk_model_prog_compute_grads(sk_model_prog* prog) {
/* Zero non-parameter grads */
for (u32 i = 0; i < prog->size; i++) {
sk_model_var* cur = prog->vars[i];
if ((cur->flags & SK_MV_REQUIRES_GRAD) && !(cur->flags & SK_MV_PARAMETER))
sk_mat_clear(cur->grad);
}
/* Seed loss gradient = 1 */
sk_mat_fill(prog->vars[prog->size - 1]->grad, 1.0f);
for (i64 i = (i64)prog->size - 1; i >= 0; i--) {
sk_model_var* cur = prog->vars[i];
if (!(cur->flags & SK_MV_REQUIRES_GRAD)) continue;
sk_model_var* a = cur->inputs[0];
sk_model_var* b = cur->inputs[1];
u32 n = SK_MV_NUM_INPUTS(cur->op);
if (n == 1 && !(a->flags & SK_MV_REQUIRES_GRAD)) continue;
if (n == 2 &&
!(a->flags & SK_MV_REQUIRES_GRAD) &&
!(b->flags & SK_MV_REQUIRES_GRAD)) continue;
switch (cur->op) {
case SK_OP_NULL:
case SK_OP_CREATE: break;
case _SK_OP_UNARY_START: break;
case SK_OP_RELU:
sk_mat_relu_add_grad(a->grad, a->val, cur->grad); break;
case SK_OP_SOFTMAX:
sk_mat_softmax_add_grad(a->grad, cur->val, cur->grad); break;
case _SK_OP_BINARY_START: break;
case SK_OP_ADD:
if (a->flags & SK_MV_REQUIRES_GRAD) sk_mat_add(a->grad, a->grad, cur->grad);
if (b->flags & SK_MV_REQUIRES_GRAD) sk_mat_add(b->grad, b->grad, cur->grad);
break;
case SK_OP_SUB:
if (a->flags & SK_MV_REQUIRES_GRAD) sk_mat_add(a->grad, a->grad, cur->grad);
if (b->flags & SK_MV_REQUIRES_GRAD) sk_mat_sub(b->grad, b->grad, cur->grad);
break;
case SK_OP_MATMUL:
if (a->flags & SK_MV_REQUIRES_GRAD) sk_mat_mul(a->grad, cur->grad, b->val, 0, 0, 1);
if (b->flags & SK_MV_REQUIRES_GRAD) sk_mat_mul(b->grad, a->val, cur->grad, 0, 1, 0);
break;
case SK_OP_CROSS_ENTROPY:
sk_mat_cross_entropy_add_grad(
a->grad, b->grad, a->val, b->val, cur->grad); break;
}
}
}
/* ── Model lifecycle ──────────────────────────────────────────────── */
sk_model* sk_model_create(sk_arena* arena) {
return SK_PUSH_STRUCT(arena, sk_model);
}
void sk_model_compile(sk_arena* arena, sk_model* model) {
if (model->output)
model->forward_prog = sk_model_prog_create(arena, model, model->output);
if (model->cost)
model->cost_prog = sk_model_prog_create(arena, model, model->cost);
}
void sk_model_feedforward(sk_model* model) {
sk_model_prog_compute(&model->forward_prog);
}
/* ── Training: mini-batch SGD ─────────────────────────────────────── */
void sk_model_train(sk_model* model, const sk_training_desc* desc) {
u32 num_examples = desc->train_images->rows;
u32 input_size = desc->train_images->cols;
u32 output_size = desc->train_labels->cols;
u32 num_tests = desc->test_images->rows;
u32 num_batches = num_examples / desc->batch_size;
sk_arena_temp scratch = sk_arena_scratch_get(NULL, 0);
u32* order = SK_PUSH_ARRAY_NZ(scratch.arena, u32, num_examples);
for (u32 i = 0; i < num_examples; i++) order[i] = i;
for (u32 epoch = 0; epoch < desc->epochs; epoch++) {
/* Fisher-Yates shuffle */
for (u32 i = 0; i < num_examples; i++) {
u32 a = sk_prng_rand() % num_examples;
u32 b2 = sk_prng_rand() % num_examples;
u32 tmp = order[b2]; order[b2] = order[a]; order[a] = tmp;
}
for (u32 batch = 0; batch < num_batches; batch++) {
/* Zero parameter grads */
for (u32 i = 0; i < model->cost_prog.size; i++) {
sk_model_var* cur = model->cost_prog.vars[i];
if (cur->flags & SK_MV_PARAMETER) sk_mat_clear(cur->grad);
}
f32 avg_cost = 0.0f;
for (u32 i = 0; i < desc->batch_size; i++) {
u32 idx = order[batch * desc->batch_size + i];
memcpy(model->input->val->data,
desc->train_images->data + idx * input_size,
sizeof(f32) * input_size);
memcpy(model->desired_output->val->data,
desc->train_labels->data + idx * output_size,
sizeof(f32) * output_size);
sk_model_prog_compute(&model->cost_prog);
sk_model_prog_compute_grads(&model->cost_prog);
avg_cost += sk_mat_sum(model->cost->val);
}
avg_cost /= (f32)desc->batch_size;
/* SGD update */
for (u32 i = 0; i < model->cost_prog.size; i++) {
sk_model_var* cur = model->cost_prog.vars[i];
if (!(cur->flags & SK_MV_PARAMETER)) continue;
sk_mat_scale(cur->grad, desc->learning_rate / desc->batch_size);
sk_mat_sub(cur->val, cur->val, cur->grad);
}
printf("Epoch %2d/%2d Batch %4d/%4d Cost: %.4f\r",
epoch+1, desc->epochs, batch+1, num_batches, avg_cost);
fflush(stdout);
}
printf("\n");
/* Epoch evaluation */
u32 num_correct = 0;
f32 avg_cost = 0.0f;
for (u32 i = 0; i < num_tests; i++) {
memcpy(model->input->val->data,
desc->test_images->data + i * input_size,
sizeof(f32) * input_size);
memcpy(model->desired_output->val->data,
desc->test_labels->data + i * output_size,
sizeof(f32) * output_size);
sk_model_prog_compute(&model->cost_prog);
avg_cost += sk_mat_sum(model->cost->val);
num_correct += (sk_mat_argmax(model->output->val) ==
sk_mat_argmax(model->desired_output->val));
}
avg_cost /= (f32)num_tests;
printf("Test Accuracy: %5u/%5u (%.1f%%) Cost: %.4f\n",
num_correct, num_tests,
(f32)num_correct / num_tests * 100.0f, avg_cost);
}
sk_arena_scratch_release(scratch);
}