A minimal autograd tensor library in C.
You can install bare-lm in two ways.
Requirements:
- A C compiler (
cc,gcc, orclang) make- OpenBLAS (headers + library)
Install OpenBLAS first:
brew install openblas
sudo apt update && sudo apt install -y libopenblas-dev
Then build and install:
git clone https://github.com/thevoxium/bare-lm.git
cd bare-lm
make OPENBLAS_PREFIX=/opt/homebrew/opt/openblas
make
sudo make install
By default this installs to /usr/local/include/bare.h and /usr/local/lib/libbare.{so,dylib}.
Each tagged release publishes prebuilt archives for Linux and macOS:
bare-<version>-linux.tar.gzbare-<version>-macos.tar.gz
After down and extracting a release tarball, install headers and library:
tar -xzf bare-<version>-<artifact>.tar.gz
cd bare-<version>-<artifact>
sudo cp include/bare.h /usr/local/include/
sudo cp lib/libbare.* /usr/local/lib/
cc file.c -lbare
If your system does not search /usr/local/lib by default, add -L/usr/local/lib and -I/usr/local/include.
#include "bare.h"
int main() {
Memory *mem = create_global_mem(1 << 28);
ParameterList *pl = create_param_list(mem);
int x_shape[] = {4, 2};
int y_shape[] = {4, 1};
Tensor *x = tensor_init(mem, x_shape, 2, PERM);
Tensor *y = tensor_init(mem, y_shape, 2, PERM);
float x_data[] = {0, 0, 0, 1, 1, 0, 1, 1};
float y_data[] = {0, 1, 1, 0};
for (int i = 0; i < 8; i++) x->data[i] = x_data[i];
for (int i = 0; i < 4; i++) y->data[i] = y_data[i];
Linear *l1 = create_linear(mem, pl, 2, 8);
Linear *l2 = create_linear(mem, pl, 8, 1);
load_checkpoint(pl, "xor.btw");
for (int epoch = 0; epoch < 500; epoch++) {
zero_grad(pl);
Tensor *h = linear_t(mem, l1, x);
h = relu_t(mem, h);
Tensor *o = linear_t(mem, l2, h);
o = sigmoid_t(mem, o);
Tensor *loss = mseloss_t(mem, o, y);
backward(mem, loss);
if (epoch % 100 == 0)
printf("epoch %3d loss=%.4f\n", epoch, loss->data[0]);
sgd_step(pl, 0.1f);
reset_temp_mem(mem);
}
save_checkpoint(pl, "xor.btw");
free_global_mem(mem);
}
Step 1: Create memory and a parameter list. A single Memory object holds two arenas (permanent and temporary). 1 << 28 = 256 MB per arena. A ParameterList tracks all trainable tensors.
Memory *mem = create_global_mem(1 << 28);
ParameterList *pl = create_param_list(mem);
Step 2: Allocate persistent tensors. Inputs x and y are plain PERM tensors (not trainable). Pass PERM so they survive reset_temp_mem.
Tensor *x = tensor_init(mem, x_shape, 2, PERM);
Tensor *y = tensor_init(mem, y_shape, 2, PERM);
Step 3: Create layers with auto-registration. create_linear allocates weights and bias as PERM tensors and automatically adds them to the parameter list.
Linear *l1 = create_linear(mem, pl, 2, 8);
Linear *l2 = create_linear(mem, pl, 8, 1);
// pl now contains l1->weights, l1->bias, l2->weights, l2->bias
Step 4: Forward pass. Every operation (linear_t, relu_t, sigmoid_t, mseloss_t) allocates its result from the temp arena. No malloc calls, no cleanup code.
Tensor *h = linear_t(mem, l1, x);
h = relu_t(mem, h);
Tensor *o = linear_t(mem, l2, h);
o = sigmoid_t(mem, o);
Tensor *loss = mseloss_t(mem, o, y);
Step 5: Backward pass. backward builds a topological sort of the computation graph (also temp-allocated) and propagates gradients. Intermediate tensor data is still valid at this point.
backward(mem, loss);
Step 6: Zero grads, update, reset. zero_grad clears all parameter gradients. sgd_step applies SGD to all parameters in one call. After gradients are consumed, reset_temp_mem zeroes the temp arena in O(1). All intermediate tensors from the forward pass are gone.
zero_grad(pl);
sgd_step(pl, 0.1f);
reset_temp_mem(mem);
Step 7: Cleanup. free_global_mem releases both arenas and the Memory struct.
free_global_mem(mem);
All allocations go through allocate_mem(mem, size, perm). There are no individual free calls.
| Flag | Lifetime | Used for |
|---|---|---|
PERM |
Until free_global_mem |
Weights, biases, persistent inputs/targets |
TEMP |
Until reset_temp_mem |
Operation results, graph traversal arrays, loss |
reset_temp_mem resets the temp arena pointer to 0. It is called after both forward and backward complete, because backward reads intermediate tensor data.
epoch N: forward (temp fills) β backward (reads temp) β update β reset_temp_mem
epoch N+1: forward (temp fills from 0) β ...
ParameterList is a dynamic array of trainable tensors. It is a typedef of Dt_array allocated in the PERM arena.
ParameterList *pl = create_param_list(mem);
// Manual registration
param_list_add(mem, pl, my_tensor);
// Or automatic β create_linear registers weights and bias for you
Linear *l = create_linear(mem, pl, 128, 64);
Once built, the parameter list drives the training loop:
zero_grad(pl); // zero all parameter gradients
backward(mem, loss); // backprop
sgd_step(pl, 0.01f); // update all parameters
| Function | Signature | Description |
|---|---|---|
create_global_mem |
(size_t size) β Memory* |
Allocate perm + temp arenas (e.g., 1<<28 = 256MB each) |
reset_temp_mem |
(Memory *mem) |
Reset temp arena to empty |
allocate_mem |
(Memory *mem, size_t size, uint8_t perm) β void* |
Arena allocation |
free_global_mem |
(Memory *mem) |
Free both arenas and Memory |
save_checkpoint |
(ParameterList *pl, const char *path) |
Save all parameters to binary file |
load_checkpoint |
(ParameterList *pl, const char *path) |
Load parameters from binary file |
| Function | Signature | Description |
|---|---|---|
create_param_list |
(Memory *mem) β ParameterList* |
Create an empty PERM parameter list |
param_list_add |
(Memory *mem, ParameterList *pl, Tensor *t) |
Add a tensor to the list |
zero_grad |
(ParameterList *pl) |
Zero gradients for all parameters |
sgd_step |
(ParameterList *pl, float lr) |
SGD update: data -= lr * grad |
clip_gradients |
(ParameterList *pl, float threshold) |
Clip gradients to [-threshold, threshold] |
adam_init |
(Memory*, ParameterList*, lr, beta1, beta2, eps, t) β Adam* |
Initialize Adam optimizer state |
adam_step |
(Adam*, ParameterList*) |
Adam update step |
adamw_init |
(Memory*, ParameterList*, lr, beta1, beta2, eps, weight_decay, t) β AdamW* |
Initialize AdamW optimizer state |
adamw_step |
(AdamW*, ParameterList*) |
AdamW update step |
| Function | Signature | Description |
|---|---|---|
tensor_init |
(Memory*, int *shape, int ndim, uint8_t perm) β Tensor* |
Zero-initialized tensor |
tensor_zeros |
(Memory*, int *shape, int ndim, uint8_t perm) β Tensor* |
Same as tensor_init |
tensor_ones |
(Memory*, int *shape, int ndim, uint8_t perm) β Tensor* |
All ones |
tensor_randn |
(Memory*, int *shape, int ndim, uint8_t perm) β Tensor* |
Random normal (Box-Muller) |
tensor_xavier |
(Memory*, int *shape, int ndim, uint8_t perm) β Tensor* |
Xavier initialization |
tensor_eye |
(Memory*, int n, int m, uint8_t perm) β Tensor* |
Identity matrix (nΓn or nΓm) |
tensor_full_like |
(Memory*, Tensor *a, float fill_value, uint8_t perm) β Tensor* |
Same shape as a, filled with fill_value |
tensor_zeros_like |
(Memory*, Tensor *a, uint8_t perm) β Tensor* |
Same shape as a, filled with zeros |
tensor_ones_like |
(Memory*, Tensor *a, uint8_t perm) β Tensor* |
Same shape as a, filled with ones |
print_t |
(Tensor*, uint8_t grad) |
Print tensor (grad=1 to include gradients) |
| Function | Signature | Description |
|---|---|---|
add_t |
(Memory*, Tensor *a, Tensor *b) β Tensor* |
Element-wise a + b |
sub_t |
(Memory*, Tensor *a, Tensor *b) β Tensor* |
Element-wise a - b |
mul_t |
(Memory*, Tensor *a, Tensor *b) β Tensor* |
Element-wise a * b |
divide_t |
(Memory*, Tensor *a, Tensor *b) β Tensor* |
Element-wise a / b |
neg_t |
(Memory*, Tensor *a) β Tensor* |
Element-wise -a |
pow_t |
(Memory*, Tensor *a, float exp) β Tensor* |
Element-wise pow(a, exp) |
scale_t |
(Memory*, Tensor *a, float v) β Tensor* |
Element-wise a * scalar |
| Function | Signature | Description |
|---|---|---|
exp_t |
(Memory*, Tensor *a) β Tensor* |
Element-wise exp |
log_t |
(Memory*, Tensor *a) β Tensor* |
Element-wise log |
| Function | Signature | Description |
|---|---|---|
sum_t |
(Memory*, Tensor *a, int dim) β Tensor* |
Sum along dimension |
mean_t |
(Memory*, Tensor *a, int dim) β Tensor* |
Mean along dimension |
max_t |
(Memory*, Tensor *a, int dim) β Tensor* |
Max along dimension |
| Function | Signature | Description |
|---|---|---|
relu_t |
(Memory*, Tensor *a) β Tensor* |
ReLU |
gelu_t |
(Memory*, Tensor *a) β Tensor* |
GELU (tanh approximation) |
sigmoid_t |
(Memory*, Tensor *a) β Tensor* |
Sigmoid |
tanh_t |
(Memory*, Tensor *a) β Tensor* |
Tanh |
softmax_t |
(Memory*, Tensor *a, int dim) β Tensor* |
Softmax along dimension |
| Function | Signature | Description |
|---|---|---|
mask_t |
(Memory*, Tensor *a, Tensor *b, float val) β Tensor* |
Where b==1, set to val; else keep a |
| Function | Signature | Description |
|---|---|---|
matmul_t |
(Memory*, Tensor *a, Tensor *b) β Tensor* |
Matrix multiply (2D) |
bmm_t |
(Memory*, Tensor *a, Tensor *b) β Tensor* |
Batch matrix multiply (3D: [B,T,D] x [B,D,T] β [B,T,T]) |
transpose_t |
(Memory*, Tensor *a) β Tensor* |
Transpose 2D |
dot_t |
(Memory*, Tensor *a, Tensor *b) β Tensor* |
Dot product (1D) |
| Function | Signature | Description |
|---|---|---|
embedding_t |
(Memory*, Tensor *vocab, Tensor *indices) β Tensor* |
Look up embeddings (vocab: [V,D], indices: [B,T]) |
| Function | Signature | Description |
|---|---|---|
reshape_t |
(Memory*, Tensor *a, int *shape, int ndim) β Tensor* |
Reshape (shares data) |
squeeze_t |
(Memory*, Tensor *a, int dim) β Tensor* |
Remove dim of size 1 |
unsqueeze_t |
(Memory*, Tensor *a, int dim) β Tensor* |
Insert dim of size 1 |
broadcast_t |
(Memory*, Tensor *a, int *shape, int tar_dim) β Tensor* |
Broadcast to target shape |
permute_t |
(Memory*, Tensor *a, int *dims, int total_dim) β Tensor* |
Permute dimensions |
concat_t |
(Memory*, Tensor *a, Tensor *b, int dim) β Tensor* |
Concatenate along dimension |
slice_t |
(Memory*, Tensor *a, int dim, int split_size) β Pair_T* |
Split tensor along dim, returns pair {F, S} |
| Function | Signature | Description |
|---|---|---|
mseloss_t |
(Memory*, Tensor *a, Tensor *b) β Tensor* |
Mean squared error |
crossentropyloss_t |
(Memory*, Tensor *logits, Tensor *targets) β Tensor* |
Cross-entropy (logits: [N,C], targets: [N]) |
| Function | Signature | Description |
|---|---|---|
create_linear |
(Memory*, ParameterList *pl, int d_in, int d_out) β Linear* |
Linear layer, auto-registers weights + bias |
linear_t |
(Memory*, Linear*, Tensor *x) β Tensor* |
Forward: x @ W^T + b |
create_layernorm |
(Memory*, ParameterList *pl, int normalized_shape, float eps) β LayerNorm* |
LayerNorm, auto-registers weight + bias |
layernorm_t |
(Memory*, LayerNorm*, Tensor *x) β Tensor* |
Layer normalization forward |
| Function | Signature | Description |
|---|---|---|
backward |
(Memory*, Tensor *root) |
Backpropagate from root |
All operations register backward passes and track parent tensors automatically.