← Commits · 3edc0440
3edc04404d6908f51e0e172bfefb0bbdd69601da
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..176f046
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,18 @@
+name: test
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ test:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v4
+ - run: make test
diff --git a/README.md b/README.md
index 6e75174..18eed1c 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,8 @@
# utensil
+[](https://github.com/zserge/utensil/actions/workflows/test.yml)
+[](LICENSE)
+
A single-header C99 tensor library for training and running small neural networks, with an optional Apple Metal GPU backend. Built for tinyML and embedded machine learning work, where the dependency footprint and code size of mainstream frameworks are hard to justify.
- **No dependencies beyond libc and, optionally, the platform's own GPU framework.** No BLAS, no autograd engine, no build system beyond a compiler.
diff --git a/test.c b/test.c
index 33ecafa..83af798 100644
--- a/test.c
+++ b/test.c
@@ -1,4 +1,5 @@
#include <assert.h>
+#include <stdio.h>
#include "utensil.h"
@@ -635,71 +636,120 @@ static void test_adam(void) {
ut_free(p);
}
+// UT_METAL only does anything on Apple platforms (see utensil.h); elsewhere
+// ut_alloc silently downgrades it to UT_CPU, which the *(UT_METAL) calls'
+// own dev-tracking assertions aren't written to expect, so they're skipped.
int main() {
test_shape();
test_lifetime();
test_reshape();
test_elementwise(UT_CPU);
+#ifdef __APPLE__
test_elementwise(UT_METAL);
+#endif
test_edge_activations(UT_CPU);
+#ifdef __APPLE__
test_edge_activations(UT_METAL);
+#endif
test_mse(UT_CPU);
+#ifdef __APPLE__
test_mse(UT_METAL);
+#endif
test_matmul_2d(UT_CPU);
+#ifdef __APPLE__
test_matmul_2d(UT_METAL);
+#endif
test_matmul_3d(UT_CPU);
+#ifdef __APPLE__
test_matmul_3d(UT_METAL);
+#endif
test_linear_forward();
test_linear_backward(UT_CPU);
+#ifdef __APPLE__
test_linear_backward(UT_METAL);
+#endif
test_softmax_lastdim(UT_CPU);
+#ifdef __APPLE__
test_softmax_lastdim(UT_METAL);
+#endif
test_softmax_firstdim(UT_CPU);
+#ifdef __APPLE__
test_softmax_firstdim(UT_METAL);
+#endif
test_layernorm_forward(UT_CPU);
+#ifdef __APPLE__
test_layernorm_forward(UT_METAL);
+#endif
test_layernorm_backward(UT_CPU);
+#ifdef __APPLE__
test_layernorm_backward(UT_METAL);
+#endif
test_batchnorm2d_forward(UT_CPU);
+#ifdef __APPLE__
test_batchnorm2d_forward(UT_METAL);
+#endif
test_batchnorm2d_backward(UT_CPU);
+#ifdef __APPLE__
test_batchnorm2d_backward(UT_METAL);
+#endif
test_global_avgpool2d(UT_CPU);
+#ifdef __APPLE__
test_global_avgpool2d(UT_METAL);
+#endif
test_maxpool2d(UT_CPU);
+#ifdef __APPLE__
test_maxpool2d(UT_METAL);
+#endif
test_avgpool2d(UT_CPU);
+#ifdef __APPLE__
test_avgpool2d(UT_METAL);
+#endif
test_im2col(UT_CPU);
+#ifdef __APPLE__
test_im2col(UT_METAL);
+#endif
test_col2im(UT_CPU);
+#ifdef __APPLE__
test_col2im(UT_METAL);
+#endif
test_conv1d_forward(UT_CPU);
+#ifdef __APPLE__
test_conv1d_forward(UT_METAL);
+#endif
test_conv1d_backward(UT_CPU);
+#ifdef __APPLE__
test_conv1d_backward(UT_METAL);
+#endif
test_conv2d_forward(UT_CPU);
+#ifdef __APPLE__
test_conv2d_forward(UT_METAL);
+#endif
test_conv2d_backward(UT_CPU);
+#ifdef __APPLE__
test_conv2d_backward(UT_METAL);
+#endif
test_dwconv2d_forward(UT_CPU);
+#ifdef __APPLE__
test_dwconv2d_forward(UT_METAL);
+#endif
test_dwconv2d_backward(UT_CPU);
+#ifdef __APPLE__
test_dwconv2d_backward(UT_METAL);
+#endif
test_sgd_momentum();
test_adam();
diff --git a/utensil.h b/utensil.h
index 2eae1e5..92b2542 100644
--- a/utensil.h
+++ b/utensil.h
@@ -1,6 +1,7 @@
#ifndef UTENSIL_H
#define UTENSIL_H
+#include <float.h>
#include <math.h>
#include <stdbool.h>
#include <stdlib.h>
@@ -714,19 +715,41 @@ void* ut_metal_ctx(void) {
return _g_metal;
}
-#if defined(__APPLE__) && defined(__GNUC__)
+#if defined(__GNUC__)
__attribute__((destructor)) static void _ut_metal_cleanup(void) {
if (_g_metal) {
_mtl_free(_g_metal);
_g_metal = NULL;
}
}
+#endif
-#else
+#else // !__APPLE__: no GPU backend, everything below always sees a NULL context
+
+typedef struct { int _unused; } _mtl_ctx_t;
void* ut_metal_ctx(void) { return NULL; }
-#endif
+static void _mtl_flush(_mtl_ctx_t* c) { (void)c; }
+static void* _mtl_buf_alloc(_mtl_ctx_t* c, size_t bytes) {
+ (void)c, (void)bytes;
+ return NULL;
+}
+static void _mtl_buf_free(void* b) { (void)b; }
+static void _mtl_buf_write(void* b, const float* src, size_t n) { (void)b, (void)src, (void)n; }
+static void _mtl_buf_read(void* b, float* dst, size_t n) { (void)b, (void)dst, (void)n; }
+static void _mtl_dispatch(_mtl_ctx_t* c, const char* kern, void** bufs, int nbufs,
+ const void* bytes, int blen, int n) {
+ (void)c, (void)kern, (void)bufs, (void)nbufs, (void)bytes, (void)blen, (void)n;
+}
+static void _mtl_dispatch_rows(_mtl_ctx_t* c, const char* kern, void** bufs, int nbufs,
+ const void* bytes, int blen, int rows, int tgsize) {
+ (void)c, (void)kern, (void)bufs, (void)nbufs, (void)bytes, (void)blen, (void)rows, (void)tgsize;
+}
+static void _mtl_matmul(_mtl_ctx_t* ctx, void* a, void* b, void* res, int m, int n, int k, bool ta,
+ bool tb) {
+ (void)ctx, (void)a, (void)b, (void)res, (void)m, (void)n, (void)k, (void)ta, (void)tb;
+}
-#endif
+#endif // __APPLE__
// =========================================================
// Allocation and lifetime management