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
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ add_library(nir_c_runtime
src/runtime.c
src/fixed_event.c
src/train.c
src/rng.c
src/encode.c
src/data.c
src/checkpoint.c
)
target_include_directories(nir_c_runtime PUBLIC include)
target_link_libraries(nir_c_runtime PUBLIC m)
Expand Down
123 changes: 123 additions & 0 deletions include/nir_c_runtime/checkpoint.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/* Torch-free checkpoint save/load plus the reverse bridge (#29, #30).
*
* Checkpoints are version-stamped JSON (mirroring the
* `spikeforge-nir-graph` v1 envelope discipline):
*
* {"format": "spikeforge-c-checkpoint", "version": 1,
* "topology": "conv_net", "seed": 7, "step": 100,
* "nodes": [{"name": "fc", "kind": "Affine",
* "shape": [4, 8], "weight": [...], "bias": [...]}]}
*
* Weights are flat row-major float arrays with an explicit shape;
* floats print with %.9g, which round-trips float32 exactly. Unknown
* format versions fail with a named error, as do missing nodes,
* kind/shape mismatches, and non-finite weights -- never a silent
* reinterpretation.
*
* The JSON writer here is new code (the existing json module stays
* parse-only, owned by another worker). The weight setter mutates
* the graph's public param buffers in place, which is also the
* mutation path a future C optimizer would use.
*
* `nir_graph_export_summary` is the C half of the reverse bridge:
* it re-emits a loaded graph as `graph_summary`-shaped JSON with
* current (possibly C-trained) weights, which `nir_graph_load`
* accepts and `tools/patch_nir_weights.py` can merge into a
* Python-side summary.
*/
#ifndef NIR_C_RUNTIME_CHECKPOINT_H
#define NIR_C_RUNTIME_CHECKPOINT_H

#include <stddef.h>
#include <stdint.h>

#include "nir_c_runtime/graph.h"

#define NIR_CHECKPOINT_FORMAT "spikeforge-c-checkpoint"
#define NIR_CHECKPOINT_VERSION 1

/* Growing JSON text builder. */
typedef struct nir_json_writer nir_json_writer_t;

nir_json_writer_t *nir_json_writer_new(void);
void nir_json_writer_free(nir_json_writer_t *writer);

/* The finished document; valid until the next call or free. NULL
* after any failed call. */
const char *nir_json_writer_data(const nir_json_writer_t *writer);
size_t nir_json_writer_length(const nir_json_writer_t *writer);

int nir_json_begin_object(nir_json_writer_t *writer);
int nir_json_begin_array(nir_json_writer_t *writer);
/* Close the innermost container. Fails when unbalanced. */
int nir_json_end(nir_json_writer_t *writer);
int nir_json_key(nir_json_writer_t *writer, const char *key);
int nir_json_string(nir_json_writer_t *writer, const char *value);
int nir_json_float(nir_json_writer_t *writer, float value);
int nir_json_double(nir_json_writer_t *writer, double value);
int nir_json_int(nir_json_writer_t *writer, long long value);
int nir_json_bool(nir_json_writer_t *writer, int value);
int nir_json_null(nir_json_writer_t *writer);

typedef struct {
char *topology; /* malloc'd, NULL when the file omits it */
uint64_t seed;
unsigned long step;
} nir_checkpoint_meta_t;

void nir_checkpoint_meta_free(nir_checkpoint_meta_t *meta);

/* Serialize `graph`'s weights plus `meta` (NULL for weights only)
* to a malloc'd JSON document. */
char *nir_checkpoint_dumps(
const nir_graph_t *graph, const nir_checkpoint_meta_t *meta,
char **error
);

/* Write the same document to `path`. */
int nir_checkpoint_save(
const char *path, const nir_graph_t *graph,
const nir_checkpoint_meta_t *meta, char **error
);

/* Load weights from a checkpoint document into an already-loaded
* graph: every weighted node must be present with matching kind
* and shape. Fills `meta` (NULL to skip; caller frees with
* `nir_checkpoint_meta_free`). */
int nir_checkpoint_loads(
const char *text, nir_graph_t *graph, nir_checkpoint_meta_t *meta,
char **error
);

/* Load weights from the checkpoint file at `path`. */
int nir_checkpoint_load(
const char *path, nir_graph_t *graph, nir_checkpoint_meta_t *meta,
char **error
);

/* Point `*weight`/`*bias` at node `name`'s buffers with element
* counts (`*bias` NULL, `*bias_count` 0 when the node has no bias).
* Returns 0 for unknown nodes and nodes without weights. */
int nir_graph_get_weights(
const nir_graph_t *graph, const char *name, const float **weight,
size_t *weight_count, const float **bias, size_t *bias_count
);

/* Copy `weight_count`/`bias_count` floats into node `name`'s
* buffers. Weight counts must match exactly; pass `bias` NULL to
* leave the bias unchanged (nodes without a bias reject bias
* data). Returns 0 and sets *error on any mismatch. */
int nir_graph_set_weights(
nir_graph_t *graph, const char *name, const float *weight,
size_t weight_count, const float *bias, size_t bias_count,
char **error
);

/* Re-emit `graph` as `graph_summary`-shaped JSON (nodes with kind
* params, edges with delayed flags) carrying the current weights.
* Returns a malloc'd document, or NULL with *error set. */
char *nir_graph_export_summary(
const nir_graph_t *graph, char **error
);

#endif /* NIR_C_RUNTIME_CHECKPOINT_H */
47 changes: 47 additions & 0 deletions include/nir_c_runtime/data.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* Torch-free dataset loading (#27).
*
* Minimal IDX-format MNIST reader (no new dependencies): the magic
* numbers, big-endian headers, and row-major u8 pixels of the
* original MNIST files. Normalization matches spikeforge's torch
* transform exactly (`datasets.transform`: Grayscale, Resize,
* ToTensor, Normalize((0,), (1,))) -- ToTensor scales bytes by
* 1/255 and the Normalize is an identity -- so C decodes each
* pixel as v / 255.0f. Multi-byte samples land in `nir_tensor_t`
* frames (batch-1, one tensor per sample); batching stays the
* caller's loop until the tensor ABI grows a batch axis.
*/
#ifndef NIR_C_RUNTIME_DATA_H
#define NIR_C_RUNTIME_DATA_H

#include <stddef.h>
#include <stdint.h>

#include "nir_c_runtime/tensor.h"

/* Decode `n` IDX pixels into normalized floats (v / 255). */
void nir_data_normalize(const uint8_t *pixels, float *out, size_t n);

/* Read an IDX image file (magic 2051) into a malloc'd row-major
* buffer of `*count * *rows * *cols` bytes. Dimensions are capped
* (count <= 2^24, rows/cols <= 1024) and the file must hold exactly
* the declared payload. Returns 0 and sets *error on failure. */
int nir_idx_read_images(
const char *path, uint8_t **pixels, size_t *count, size_t *rows,
size_t *cols, char **error
);

/* Read an IDX label file (magic 2049) into a malloc'd buffer of
* `*count` bytes. Returns 0 and sets *error on failure. */
int nir_idx_read_labels(
const char *path, uint8_t **labels, size_t *count, char **error
);

/* Normalize sample `index` of an image buffer into a fresh (C=1, H,
* W) tensor. Returns a tensor with data == NULL on bad sizes or
* allocation failure. */
nir_tensor_t nir_data_frame(
const uint8_t *pixels, size_t count, size_t rows, size_t cols,
size_t index
);

#endif /* NIR_C_RUNTIME_DATA_H */
87 changes: 87 additions & 0 deletions include/nir_c_runtime/encode.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* Spike encoders ported from spikeforge (#28).
*
* Mirrors `SpikeEncoder` (spikeforge/encoding/spike_encoder.py) on top
* of snnTorch's `spikegen` semantics:
*
* - rate: clamp01(gain * x) Bernoulli sampling, i.e. `spikegen.rate`
* with gain=1 (`rate_conv`: clamp then `torch.bernoulli`). A spike
* fires iff a uniform draw u satisfies u < p. The draws come from
* `nir_rng_t` (splitmix64), NOT torch's MT19937, so bit equality
* with torch holds only given the same uniform stream; the rule
* above is the documented equivalence contract (draw k = t * n +
* i, consumed t-major, i-minor, one `nir_rng_next_f32` per draw).
* - latency: first-spike-time code, `spikegen.latency` with the
* encoder's tau/threshold/clip/normalize/linear flags. Linear and
* log branches both implemented; spike times round half-to-even
* (`rintf`), matching `torch.round`.
* - delta: `spikegen.delta` over a linspace(0, 1, steps) ramp times
* each pixel (padding=False, off_spike=False), threshold given as
* a fraction (`SpikeEncoder` divides its percent by 100).
* - random: the noise baseline, `rate_conv(rand * scale)` with two
* RNG draws per element (magnitude, then threshold).
* - repeat: the legacy raw path (`_repeat_pixels`): normalised
* pixels repeated across steps, no spikes.
*
* Outputs are time-major: out[t * n + i] is pixel i at step t,
* matching the encoder's [T, B=1, N] layout flattened per sample.
*/
#ifndef NIR_C_RUNTIME_ENCODE_H
#define NIR_C_RUNTIME_ENCODE_H

#include <stddef.h>
#include <stdint.h>

#include "nir_c_runtime/rng.h"

#define NIR_LATENCY_EPSILON 1e-7f

typedef struct {
size_t num_steps;
float tau;
float threshold;
int clip;
int normalize;
int linear;
size_t first_spike_time;
float epsilon;
} nir_latency_params_t;

/* Bernoulli rate code of `x[0..n)` over `num_steps`. Consumes one RNG
* draw per (t, i). Returns 0 and sets *error on bad sizes. */
int nir_encode_rate(
const float *x, size_t n, size_t num_steps, float gain,
nir_rng_t *rng, uint8_t *out, char **error
);

/* First-spike-time latency code of `x[0..n)`. Every input must lie
* in [0, 1] (the torch side raises otherwise). Returns 0 and sets
* *error on out-of-range input, bad parameters, or a spike time
* that overflows `num_steps` (mirroring spikegen's no-bypass
* raise). */
int nir_encode_latency(
const float *x, size_t n, const nir_latency_params_t *params,
uint8_t *out, char **error
);

/* Delta code of `x[0..n)` over `num_steps` with a fractional
* change threshold. Returns 0 and sets *error on bad sizes. */
int nir_encode_delta(
const float *x, size_t n, size_t num_steps, float threshold,
uint8_t *out, char **error
);

/* Noise baseline: rate-coded uniform noise, input ignored.
* Returns 0 and sets *error on bad sizes. */
int nir_encode_random(
size_t n, size_t num_steps, float scale, nir_rng_t *rng,
uint8_t *out, char **error
);

/* Repeat `x[0..n)` across `num_steps` into `out[t * n + i]`.
* Returns 0 and sets *error on bad sizes. */
int nir_encode_repeat(
const float *x, size_t n, size_t num_steps, float *out,
char **error
);

#endif /* NIR_C_RUNTIME_ENCODE_H */
30 changes: 30 additions & 0 deletions include/nir_c_runtime/rng.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* Deterministic seeded RNG for training-side C code (#28).
*
* splitmix64 (Steele, Lea, O'Neill 2014): portable C11, no platform
* entropy dependency, so embedded targets keep working. The stream
* for a seed is fixed by the algorithm below -- identical across
* runs and platforms -- which is what makes seeded spike encoding
* reproducible. This is a sampler, not a CSPRNG.
*/
#ifndef NIR_C_RUNTIME_RNG_H
#define NIR_C_RUNTIME_RNG_H

#include <stdint.h>

typedef struct {
uint64_t state;
} nir_rng_t;

/* Prime the generator with `seed`. */
void nir_rng_seed(nir_rng_t *rng, uint64_t seed);

/* Return the next 64 bits of the stream. */
uint64_t nir_rng_next_u64(nir_rng_t *rng);

/* Return a uniform double in [0, 1) from the top 53 bits of one draw. */
double nir_rng_next_f64(nir_rng_t *rng);

/* Return a uniform float in [0, 1) from the top 24 bits of one draw. */
float nir_rng_next_f32(nir_rng_t *rng);

#endif /* NIR_C_RUNTIME_RNG_H */
Loading
Loading