spyx.optimize
High-level training loops for Spyx models. Three entry points, in increasing order of "how much is compiled":
fit(...)— a Python epoch loop driving a per-step@nnx.jit. The common case; returns aHistoryof per-epoch metrics. Usemake_train_step/make_eval_stepto roll your own loop with the same JIT'd pieces.compile_fit(...)— stages the whole dataset on-device andjax.lax.scans the loop over epochs × batches under a singlejax.jit, so a full run compiles to one XLA kernel with no per-step Python or re-tracing (the throughput pattern the Spyx paper relies on).Solverprotocol —compile_fitis optimiser-agnostic: pass an OptaxGradientTransformation(auto-wrapped bybackprop(...)for the surrogate-gradient path) or a customSolverimplementinginit/step, which is how the evolutionary trainers (ES / CMA-ES, seespyx.experimental.hybrid) drop into the same whole-loop-JIT machinery.
High-level training utilities for Spyx SNNs.
Issue #26 asked for a "quick train/eval loop" so users don't have to
re-derive the nnx.Optimizer + nnx.value_and_grad + per-epoch boiler-
plate every time they build a new model. This module provides that, with a
minimum of magic:
- :func:
train_step— JIT-compiled single-step update. - :func:
eval_step— JIT-compiled single-step accuracy/loss. - :func:
fit— end-to-end Python epoch loop that iterates an iterable data source (anything yielding(events, targets)tuples — Spyx loader, generator, or plain list).
The utilities deliberately don't hide the loss / metric / optimizer choices.
Pass your own via spyx.fn.integral_crossentropy / optax.lion etc.
SolverImpl
Bases: NamedTuple
The three pure functions a solver hands to :func:compile_fit.
init(key) -> state— build the optimiser state from the initial params.step(state, batch, key) -> (state, metric)— one update;metricis a scalar summarised intohistory(the loss / best-fitness).get_params(state) -> params— the current point estimate (themeanfor ask/tell strategies).
Source code in spyx/optimize.py
backprop(tx)
A plain gradient-descent :data:Solver — value_and_grad + an Optax update.
This is the surrogate-gradient path; passing an Optax GradientTransformation
straight to :func:compile_fit wraps it in this automatically.
Source code in spyx/optimize.py
compile_fit(model, solver, loss_fn, train_data, *, epochs, eval_data=None, metric_fn=None, key=None)
Compile the entire training loop to a single XLA dispatch.
Where :func:fit is a Python epoch loop driving a per-step JIT, this stages the
whole dataset on-device and jax.lax.scan\ s the loop over epochs × batches
under one jax.jit — a full run becomes a single compiled kernel with no
per-step Python or re-tracing (the throughput pattern the Spyx paper relies on).
The optimiser is a :data:Solver, so gradient descent and gradient-free
ask/tell (CMA-ES etc.) share the same compiled loop: pass an Optax
GradientTransformation (wrapped in :func:backprop), or a solver from
:mod:spyx.experimental.evolve.
:param model: the SNN to train (any Flax NNX module).
:param solver: a :data:Solver builder, or an Optax GradientTransformation
(e.g. optax.adam(3e-3)) which is treated as backprop(tx).
:param loss_fn: (model, *batch) -> scalar.
:param train_data: (X, Y, …) staged with a leading batch axis to scan over —
each leaf shaped [n_batches, batch, ...], already on device (jnp.stack
your loader's batches once).
:param epochs: number of passes over the staged batches.
:param eval_data: optional held-out data staged the same way; scored with
metric_fn once per epoch inside the compiled loop.
:param metric_fn: (model, *batch) -> scalar (e.g. accuracy); required with
eval_data.
:param key: PRNG key threaded through init / step (ES, dropout, …).
:return: (trained_model, history) where history holds stacked per-epoch
arrays: train_loss (the mean per-step metric) and eval_metric.
Source code in spyx/optimize.py
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | |
fit(model, tx, loss_fn, train_iter, *, epochs, eval_iter=None, eval_fn=None, on_epoch_end=None)
End-to-end training loop.
:param model: the Spyx / Flax NNX module to train.
:param tx: an Optax :class:GradientTransformation (e.g. optax.lion(3e-4)).
:param loss_fn: (model, *batch) -> loss. batch is whatever
train_iter yields.
:param train_iter: zero-arg callable returning a fresh iterable of
training batches each epoch. This matches the spyx.data.*_loader
convention where loader.train_epoch() is called per epoch.
:param epochs: number of training epochs.
:param eval_iter: optional zero-arg callable yielding evaluation batches.
:param eval_fn: optional (model, *batch) -> (accuracy, loss);
required if eval_iter is set.
:param on_epoch_end: optional callback (epoch, metrics_dict) -> None
for progress printing etc. Metrics dict carries keys
train_loss, plus eval_acc / eval_loss when evaluating.
:return: list of per-epoch metric dicts.
Source code in spyx/optimize.py
make_eval_step(metric_fn)
Build a JIT-compiled single-step evaluation callable.
:param metric_fn: closure taking (model, *metric_args) and returning
(accuracy_or_similar, loss).
Source code in spyx/optimize.py
make_train_step(loss_fn)
Build a JIT-compiled single-step updater.
The returned callable has signature (model, optimizer, *loss_args) ->
loss_value and mutates model / optimizer in place via NNX.
:param loss_fn: closure taking (model, *loss_args) and returning a
scalar loss. Typically wraps spyx.fn.integral_crossentropy().