Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ add_library(nir_c_runtime
src/runtime.c
src/fixed_event.c
src/train.c
src/stdp_graph.c
src/rng.c
src/encode.c
src/data.c
Expand Down
33 changes: 27 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,36 @@ target-specific kernels remain future optimization work.
- [x] C-side training math (`train.h`): named surrogate gradients
(fast_sigmoid/atan, no silent default) with Threshold/LI/Affine/Linear
backward ops, softmax cross-entropy, SGD/Adam optimizers over raw
weight buffers, and a local STDP rule. Dataset loading, RNG/encoding,
checkpoints, and a graph-level backprop-through-time tape remain
future work (see the training epic).
weight buffers, and a local STDP rule with a graph-bound adapter
(`stdp_graph.h`) that trains a loaded Affine/Linear node's weights
from recorded pre/post spikes.
- [x] C-side training IO: seeded RNG (`rng.h`), rate/latency/delta
spike encoders (`encode.h`), IDX MNIST dataset loading (`data.h`),
and versioned checkpoint save/load with weight accessors and
summary export (`checkpoint.h`), all covered by C tests.

## Out of scope (for now)

- Full training pipelines. The loss/optimizer/STDP primitives above are
supported; data loading, spike encoding, checkpointing, and graph-level
backprop-through-time are not built yet.
- Full training pipelines. The training math and training IO above are
supported; still missing are the graph-level backprop-through-time tape
([#39](https://github.com/Capsize-Games/nir-c-runtime/issues/39)),
backward ops for spatial/linear
([#40](https://github.com/Capsize-Games/nir-c-runtime/issues/40)) and
spiking-neuron
([#41](https://github.com/Capsize-Games/nir-c-runtime/issues/41)) nodes,
mini-batch training
([#42](https://github.com/Capsize-Games/nir-c-runtime/issues/42)),
optimizer-state checkpoint resume
([#43](https://github.com/Capsize-Games/nir-c-runtime/issues/43)), and an
end-to-end C training driver with a worked example
([#44](https://github.com/Capsize-Games/nir-c-runtime/issues/44)).
Weight initializers
([#45](https://github.com/Capsize-Games/nir-c-runtime/issues/45)),
learning-rate schedules
([#46](https://github.com/Capsize-Games/nir-c-runtime/issues/46)), and
additional loss functions
([#47](https://github.com/Capsize-Games/nir-c-runtime/issues/47)) are filed
as follow-ups.
- General Curlee/JOE graph execution. The narrow, allocation-free event ABI is
integrated as the reference path for
[curlee#316](https://github.com/w4ffl35/curlee/issues/316); loading arbitrary
Expand Down
69 changes: 69 additions & 0 deletions include/nir_c_runtime/stdp_graph.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* Graph-bound STDP: apply the train.h local-learning rule to a
* loaded graph's weights (#48).
*
* `nir_stdp_t` keeps its own weight matrix in [in][out] pre-major
* layout, while graph Affine/Linear nodes store [out][in]
* out-major weights -- so manual wiring through
* `nir_graph_get_weights`/`nir_graph_set_weights` alone would
* silently transpose the matrix. This adapter is the documented,
* tested path instead: it binds to one named node, loads that
* node's current weights (transposed) into an owned STDP instance,
* and writes every step's update back through
* `nir_graph_set_weights` (biases are left unchanged).
*
* Only Affine and Linear nodes bind. Conv2d/Conv1d carry 3D/4D
* kernels with no agreed flattening into the STDP rule's 2D
* pre/post matrix, so they fail with a named error until that
* mapping is decided.
*
* Intended on-chip local-learning flow: run the graph with
* `nir_runtime_run`, record each step's input spikes to the bound
* node (pre) and output spikes from the node or its downstream
* neuron (post), pass those vectors to `nir_stdp_graph_step`, and
* persist the trained weights with `nir_checkpoint_save`. The
* binding owns the STDP-side copy: bind snapshots the graph's
* weights, and each step overwrites the graph's weights with the
* update. The graph must outlive the binding.
*/
#ifndef NIR_C_RUNTIME_STDP_GRAPH_H
#define NIR_C_RUNTIME_STDP_GRAPH_H

#include <stddef.h>

#include "nir_c_runtime/graph.h"
#include "nir_c_runtime/train.h"

typedef struct {
nir_stdp_t stdp;
nir_graph_t *graph; /* borrowed; must outlive the binding */
char *node; /* bound node name, malloc'd */
float *staging; /* [out][in] write-back buffer */
size_t in_features;
size_t out_features;
} nir_stdp_graph_t;

/* Bind `node` (Affine or Linear) in `graph`, snapshotting its
* current weights. Hyperparameters follow `nir_stdp_init` (taus
* finite and > 0, w_min <= w_max); the dimensions come from the
* node. Returns 0 and sets *error on unknown/unweighted/
* non-2D nodes and on invalid hyperparameters. */
int nir_stdp_graph_bind(
nir_stdp_graph_t *b, nir_graph_t *graph, const char *node,
float tau_pre, float tau_post, float a_plus, float a_minus,
float w_min, float w_max, char **error
);

/* One STDP step from this step's pre/post spike vectors (lengths
* in_features/out_features), written back to the graph through
* `nir_graph_set_weights`. Returns 0 on NULL or non-finite
* spikes. */
int nir_stdp_graph_step(
nir_stdp_graph_t *b, const float *pre_spikes,
const float *post_spikes
);

/* Free the STDP state, node name, and staging buffer, and zero
* `b` out. Safe on an already-freed or zeroed binding. */
void nir_stdp_graph_free(nir_stdp_graph_t *b);

#endif /* NIR_C_RUNTIME_STDP_GRAPH_H */
137 changes: 137 additions & 0 deletions src/stdp_graph.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#include "nir_c_runtime/stdp_graph.h"

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

#include "nir_c_runtime/checkpoint.h"

static void set_error(char **error, const char *message) {
if (error == NULL) return;
*error = NULL;
if (message == NULL) return;
size_t n = strlen(message) + 1;
char *copy = (char *)malloc(n);
if (copy != NULL) memcpy(copy, message, n);
*error = copy;
}

/* Out-major [o][i] -> pre-major [i][o]. */
static void to_premajor(
const float *src, float *dst, size_t in, size_t out
) {
for (size_t i = 0; i < in; i++) {
for (size_t o = 0; o < out; o++) {
dst[i * out + o] = src[o * in + i];
}
}
}

/* Pre-major [i][o] -> out-major [o][i]. */
static void to_outmajor(
const float *src, float *dst, size_t in, size_t out
) {
for (size_t o = 0; o < out; o++) {
for (size_t i = 0; i < in; i++) {
dst[o * in + i] = src[i * out + o];
}
}
}

int nir_stdp_graph_bind(
nir_stdp_graph_t *b, nir_graph_t *graph, const char *node,
float tau_pre, float tau_post, float a_plus, float a_minus,
float w_min, float w_max, char **error
) {
if (error != NULL) *error = NULL;
if (b == NULL || graph == NULL || node == NULL) {
set_error(error, "STDP bind needs a binding, graph, and node");
return 0;
}
memset(b, 0, sizeof(*b));
size_t index = nir_graph_find(graph, node);
if (index == (size_t)-1) {
set_error(error, "STDP bind names an unknown node");
return 0;
}
const nir_node_t *found = &graph->nodes[index];
size_t in = 0;
size_t out = 0;
if (found->kind == NIR_NODE_AFFINE) {
in = found->params.affine.in_features;
out = found->params.affine.out_features;
} else if (found->kind == NIR_NODE_LINEAR) {
in = found->params.linear.in_features;
out = found->params.linear.out_features;
} else {
set_error(error, "STDP bind needs an Affine or Linear node");
return 0;
}
if (!nir_stdp_init(
&b->stdp, in, out, tau_pre, tau_post, a_plus, a_minus,
w_min, w_max, error
)) {
memset(b, 0, sizeof(*b));
return 0;
}
b->node = (char *)malloc(strlen(node) + 1);
b->staging = (float *)calloc(in * out, sizeof(float));
if (b->node == NULL || b->staging == NULL) {
nir_stdp_graph_free(b);
set_error(error, "out of memory");
return 0;
}
strcpy(b->node, node);
const float *weight = NULL;
const float *bias = NULL;
size_t wn = 0;
size_t bn = 0;
if (!nir_graph_get_weights(
graph, node, &weight, &wn, &bias, &bn
) ||
wn != in * out) {
nir_stdp_graph_free(b);
set_error(error, "STDP bind cannot read the node weights");
return 0;
}
/* Staging doubles as the pre-major load buffer; set_weight
* copies it into the STDP instance (and rejects non-finite
* graph weights). */
to_premajor(weight, b->staging, in, out);
if (!nir_stdp_set_weight(&b->stdp, b->staging)) {
nir_stdp_graph_free(b);
set_error(error, "STDP bind found non-finite node weights");
return 0;
}
b->graph = graph;
b->in_features = in;
b->out_features = out;
return 1;
}

int nir_stdp_graph_step(
nir_stdp_graph_t *b, const float *pre_spikes,
const float *post_spikes
) {
if (b == NULL || b->graph == NULL || b->node == NULL ||
b->staging == NULL) {
return 0;
}
if (!nir_stdp_step(&b->stdp, pre_spikes, post_spikes)) {
return 0;
}
to_outmajor(
b->stdp.weight, b->staging, b->in_features, b->out_features
);
return nir_graph_set_weights(
b->graph, b->node, b->staging, b->in_features * b->out_features,
NULL, 0, NULL
);
}

void nir_stdp_graph_free(nir_stdp_graph_t *b) {
if (b == NULL) return;
nir_stdp_free(&b->stdp);
free(b->node);
free(b->staging);
memset(b, 0, sizeof(*b));
}
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ nir_add_test(test_workspace)
nir_add_test(test_tensor)
nir_add_test(test_error_format)
nir_add_test(test_train)
nir_add_test(test_stdp_graph)
nir_add_test(test_rng)
nir_add_test(test_encode)
nir_add_test(test_data)
Expand Down
Loading
Loading