+ New

utensil

Public
← utensil / examples / mobilenetv1_cifar10.c
#include "utensil.h"

#include "cifar10_loader.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

// MobileNetV1 (Howard et al.), scaled down for CIFAR-10 (32x32, 10 classes)
// instead of the ImageNet table (224x224, 1000 classes, channels up to 1024) --
// that table doesn't fit a 32x32 input and would be wildly over-parameterized
// for a 10-class problem. Keeps the architecture's defining idea (depthwise
// separable conv: a per-channel spatial conv + a 1x1 pointwise conv, each
// followed by BN+ReLU6, replacing one dense conv) and its downsampling
// pattern (stride-2 in a depthwise conv, never in the pointwise conv), just
// right-sized: stem -> 6 depthwise-separable blocks (channels 32-256, three
// stride-2 steps: 32x32 -> 16x16 -> 8x8 -> 4x4) -> GAP -> linear head. No
// data augmentation -- a comparison scaffold, not a from-scratch SOTA run.

// gradient tensors are borrowed from the optimizer's own grads[]; wired once
// in main() after ut_adam_alloc, then read directly during backward.
typedef struct {
  ut_tensor *dw_w, *dw_b, *dw_bn_w, *dw_bn_b, *pw_w, *pw_b, *pw_bn_w, *pw_bn_b;
} block_grads_t;

typedef struct {
  ut_dwconv2d dw;      // depthwise: one 3x3 filter per input channel
  ut_batchnorm2d dw_bn;
  ut_conv2d pw;        // pointwise: 1x1 conv, mixes channels
  ut_batchnorm2d pw_bn;
  block_grads_t g;
} block_t;

typedef struct {
  ut_dwconv2d_cache dw_c;
  ut_batchnorm2d_cache dw_bn_c;
  ut_conv2d_cache pw_c;
  ut_batchnorm2d_cache pw_bn_c;
  ut_tensor *dw_relu_in, *pw_relu_in;  // kept for backward's two ReLU6es
} block_cache_t;

typedef struct {
  ut_conv2d stem_conv;
  ut_batchnorm2d stem_bn;
  block_t blocks[6];
  ut_linear fc;
  ut_tensor *stem_cw, *stem_cb, *stem_bw, *stem_bb, *fc_w, *fc_b;  // borrowed, like block_grads_t
} mobilenet_t;

typedef struct {
  ut_conv2d_cache stem_c;
  ut_batchnorm2d_cache stem_bn_c;
  ut_tensor* stem_relu_in;
  block_cache_t block_c[6];
  ut_tensor* pooled;  // kept for the fc layer's backward
  int pool_h, pool_w;
} mobilenet_cache_t;

static block_t block_alloc(int in_c, int out_c, int stride, ut_dev dev) {
  block_t b = {0};
  b.dw = ut_dwconv2d_alloc(in_c, 3, 3, stride, 1, true, dev);
  b.dw_bn = ut_batchnorm2d_alloc(in_c, dev);
  b.pw = ut_conv2d_alloc(in_c, out_c, 1, 1, 1, 0, true, dev);
  b.pw_bn = ut_batchnorm2d_alloc(out_c, dev);
  return b;
}

static mobilenet_t mobilenet_alloc(ut_dev dev) {
  mobilenet_t m = {0};
  m.stem_conv = ut_conv2d_alloc(3, 32, 3, 3, 1, 1, true, dev);
  m.stem_bn = ut_batchnorm2d_alloc(32, dev);
  m.blocks[0] = block_alloc(32, 64, 1, dev);   // 32x32
  m.blocks[1] = block_alloc(64, 128, 2, dev);  // 32x32 -> 16x16
  m.blocks[2] = block_alloc(128, 128, 1, dev); // 16x16
  m.blocks[3] = block_alloc(128, 256, 2, dev); // 16x16 -> 8x8
  m.blocks[4] = block_alloc(256, 256, 1, dev); // 8x8
  m.blocks[5] = block_alloc(256, 256, 2, dev); // 8x8 -> 4x4
  m.fc = ut_linear_alloc(256, 10, true, dev);
  return m;
}

// appends this block's param tensors to out[] (for ut_adam_alloc) and returns
// the count -- always 8, no conditional (unlike a residual block's shortcut)
static int block_params(block_t* b, ut_tensor** out) {
  int n = 0;
  out[n++] = b->dw.weight, out[n++] = b->dw.bias;
  out[n++] = b->dw_bn.weight, out[n++] = b->dw_bn.bias;
  out[n++] = b->pw.weight, out[n++] = b->pw.bias;
  out[n++] = b->pw_bn.weight, out[n++] = b->pw_bn.bias;
  return n;
}

// mirrors block_params' exact order to wire each gradient pointer to its param
static void block_wire_grads(block_t* b, ut_tensor** g) {
  int n = 0;
  b->g.dw_w = g[n++], b->g.dw_b = g[n++];
  b->g.dw_bn_w = g[n++], b->g.dw_bn_b = g[n++];
  b->g.pw_w = g[n++], b->g.pw_b = g[n++];
  b->g.pw_bn_w = g[n++], b->g.pw_bn_b = g[n++];
}

static ut_tensor* block_forward(block_t* b, ut_tensor* x, bool training, block_cache_t* c) {
  ut_tensor* d1 = ut_dwconv2d_forward(&b->dw, x, c ? &c->dw_c : NULL);
  ut_tensor* d2 = ut_batchnorm2d_forward(&b->dw_bn, d1, training, c ? &c->dw_bn_c : NULL);
  ut_free(d1);
  ut_tensor* d3 = ut_relu6(d2);
  if (c) c->dw_relu_in = d2; else ut_free(d2);

  ut_tensor* p1 = ut_conv2d_forward(&b->pw, d3, c ? &c->pw_c : NULL);
  ut_free(d3);
  ut_tensor* p2 = ut_batchnorm2d_forward(&b->pw_bn, p1, training, c ? &c->pw_bn_c : NULL);
  ut_free(p1);
  ut_tensor* out = ut_relu6(p2);
  if (c) c->pw_relu_in = p2; else ut_free(p2);
  return out;
}

static ut_tensor* block_backward(block_t* b, block_cache_t* c, ut_tensor* dout) {
  ut_tensor* dp2 = ut_relu6_backward(dout, c->pw_relu_in);
  ut_tensor* dp1 = ut_batchnorm2d_backward(&b->pw_bn, &c->pw_bn_c, dp2, b->g.pw_bn_w, b->g.pw_bn_b);
  ut_free(dp2);
  ut_tensor* dd3 = ut_conv2d_backward(&b->pw, &c->pw_c, dp1, b->g.pw_w, b->g.pw_b);
  ut_free(dp1);
  ut_tensor* dd2 = ut_relu6_backward(dd3, c->dw_relu_in);
  ut_free(dd3);
  ut_tensor* dd1 = ut_batchnorm2d_backward(&b->dw_bn, &c->dw_bn_c, dd2, b->g.dw_bn_w, b->g.dw_bn_b);
  ut_free(dd2);
  ut_tensor* dx = ut_dwconv2d_backward(&b->dw, &c->dw_c, dd1, b->g.dw_w, b->g.dw_b);
  ut_free(dd1);
  return dx;
}

static void block_cache_free(block_cache_t* c) {
  ut_dwconv2d_cache_free(&c->dw_c);
  ut_batchnorm2d_cache_free(&c->dw_bn_c);
  ut_conv2d_cache_free(&c->pw_c);
  ut_batchnorm2d_cache_free(&c->pw_bn_c);
  ut_free_all(c->dw_relu_in, c->pw_relu_in);
}

static void block_free(block_t* b) {
  ut_dwconv2d_free(&b->dw);
  ut_batchnorm2d_free(&b->dw_bn);
  ut_conv2d_free(&b->pw);
  ut_batchnorm2d_free(&b->pw_bn);
}

static ut_tensor* mobilenet_forward(mobilenet_t* m, ut_tensor* x, bool training,
                                    mobilenet_cache_t* c) {
  ut_tensor* s = ut_conv2d_forward(&m->stem_conv, x, c ? &c->stem_c : NULL);
  ut_tensor* sbn = ut_batchnorm2d_forward(&m->stem_bn, s, training, c ? &c->stem_bn_c : NULL);
  ut_free(s);
  ut_tensor* h = ut_relu6(sbn);
  if (c) c->stem_relu_in = sbn; else ut_free(sbn);

  for (int i = 0; i < 6; i++) {
    ut_tensor* next = block_forward(&m->blocks[i], h, training, c ? &c->block_c[i] : NULL);
    ut_free(h);
    h = next;
  }

  int ph = h->shape.shape[2], pw = h->shape.shape[3];
  ut_tensor* pooled = ut_global_avgpool2d(h);
  ut_free(h);
  if (c) c->pooled = pooled, c->pool_h = ph, c->pool_w = pw;
  ut_tensor* logits = ut_linear_forward(&m->fc, pooled);
  if (!c) ut_free(pooled);
  return logits;
}

static ut_tensor* mobilenet_backward(mobilenet_t* m, mobilenet_cache_t* c, ut_tensor* dlogits) {
  ut_tensor* dpooled = ut_linear_backward(&m->fc, c->pooled, dlogits, m->fc_w, m->fc_b);
  ut_tensor* dh = ut_global_avgpool2d_backward(dpooled, c->pool_h, c->pool_w);
  ut_free(dpooled);

  for (int i = 5; i >= 0; i--) {
    ut_tensor* dprev = block_backward(&m->blocks[i], &c->block_c[i], dh);
    ut_free(dh);
    dh = dprev;
  }

  ut_tensor* dsbn = ut_relu6_backward(dh, c->stem_relu_in);
  ut_free(dh);
  ut_tensor* ds = ut_batchnorm2d_backward(&m->stem_bn, &c->stem_bn_c, dsbn, m->stem_bw, m->stem_bb);
  ut_free(dsbn);
  ut_tensor* dx = ut_conv2d_backward(&m->stem_conv, &c->stem_c, ds, m->stem_cw, m->stem_cb);
  ut_free(ds);
  return dx;
}

static void mobilenet_cache_free(mobilenet_cache_t* c) {
  ut_conv2d_cache_free(&c->stem_c);
  ut_batchnorm2d_cache_free(&c->stem_bn_c);
  ut_free(c->stem_relu_in);
  for (int i = 0; i < 6; i++) block_cache_free(&c->block_c[i]);
  ut_free(c->pooled);
}

static void mobilenet_free(mobilenet_t* m) {
  ut_conv2d_free(&m->stem_conv);
  ut_batchnorm2d_free(&m->stem_bn);
  for (int i = 0; i < 6; i++) block_free(&m->blocks[i]);
  ut_linear_free(&m->fc);
}

int main(void) {
  srand(42);

  ut_dev dev = UT_METAL;  // flip to UT_CPU to compare against a (CPU-only) PyTorch script

  printf("Loading CIFAR-10…\n");
  const char* train_files[5] = {"cifar/data_batch_1.bin", "cifar/data_batch_2.bin",
                                "cifar/data_batch_3.bin", "cifar/data_batch_4.bin",
                                "cifar/data_batch_5.bin"};
  const char* test_files[1] = {"cifar/test_batch.bin"};
  cifar10_t train = cifar10_load(train_files, 5);
  cifar10_t test = cifar10_load(test_files, 1);
  printf("train: %d  test: %d\n\n", train.n, test.n);

  mobilenet_t m = mobilenet_alloc(dev);

  ut_tensor* params[64];
  int np = 0;
  params[np++] = m.stem_conv.weight, params[np++] = m.stem_conv.bias;
  params[np++] = m.stem_bn.weight, params[np++] = m.stem_bn.bias;
  int block_off[6];
  for (int i = 0; i < 6; i++) {
    block_off[i] = np;
    np += block_params(&m.blocks[i], params + np);
  }
  int fc_off = np;
  params[np++] = m.fc.weight, params[np++] = m.fc.bias;

  ut_adam opt = ut_adam_alloc(params, np, 1e-3f, 0.9f, 0.999f, 1e-8f, 1e-4f);

  // wire each layer's gradient pointers to the optimizer's own grads[], in the
  // same order the params[] above was built
  m.stem_cw = opt.grads[0], m.stem_cb = opt.grads[1];
  m.stem_bw = opt.grads[2], m.stem_bb = opt.grads[3];
  for (int i = 0; i < 6; i++) block_wire_grads(&m.blocks[i], opt.grads + block_off[i]);
  m.fc_w = opt.grads[fc_off], m.fc_b = opt.grads[fc_off + 1];

  int B = 64, epochs = 20, batches = train.n / B;
  ut_tensor* grad_logits = ut_alloc(2, (int[]){B, 10}, dev);
  int* idx = malloc((size_t)train.n * sizeof(int));
  for (int i = 0; i < train.n; i++) idx[i] = i;

  for (int ep = 0; ep < epochs; ep++) {
    cifar10_shuffle(idx, train.n);
    clock_t t0 = clock();
    float loss_sum = 0;
    int correct = 0;

    for (int bi = 0; bi < batches; bi++) {
      float bx[B * CIFAR10_PIX];
      int bl[B];
      for (int i = 0; i < B; i++) {
        int ii = idx[bi * B + i];
        memcpy(bx + i * CIFAR10_PIX, train.imgs + ii * CIFAR10_PIX, CIFAR10_PIX * sizeof(float));
        bl[i] = train.labels[ii];
      }
      ut_tensor* x = ut_from_data(4, (int[]){B, 3, 32, 32}, bx, dev);

      mobilenet_cache_t c;
      ut_tensor* logits = mobilenet_forward(&m, x, true, &c);
      float loss = ut_cross_entropy(logits, bl, grad_logits);
      ut_sync_cpu(logits);
      for (int i = 0; i < B; i++)
        if (cifar10_argmax(logits, i) == bl[i]) correct++;

      ut_tensor* dx = mobilenet_backward(&m, &c, grad_logits);
      ut_adam_step(&opt, 5.0f);
      loss_sum += loss;

      mobilenet_cache_free(&c);
      ut_free_all(x, logits, dx);
    }

    float secs = (float)(clock() - t0) / (float)CLOCKS_PER_SEC;

    int test_ok = 0;
    for (int i = 0; i < test.n; i += B) {
      int nb = (i + B <= test.n) ? B : (test.n - i);
      ut_tensor* tx = ut_from_data(4, (int[]){nb, 3, 32, 32}, test.imgs + i * CIFAR10_PIX, dev);
      ut_tensor* tl = mobilenet_forward(&m, tx, false, NULL);
      ut_sync_cpu(tl);
      for (int j = 0; j < nb; j++)
        if (cifar10_argmax(tl, j) == test.labels[i + j]) test_ok++;
      ut_free_all(tx, tl);
    }

    printf("epoch %2d  loss %7.4f  train %5.1f%%  test %5.1f%%  %6.1fs\n", ep + 1,
           loss_sum / (float)batches, 100.f * (float)correct / (float)(batches * B),
           100.f * (float)test_ok / (float)test.n, secs);
  }

  free(idx);
  ut_free(grad_logits);
  ut_adam_free(&opt);
  mobilenet_free(&m);
  cifar10_free(&train);
  cifar10_free(&test);
  return 0;
}