Skip to content

Performance

The hot path is the reservoir harvest — the per-timestep state update. For a dense reservoir this is dominated by the matrix–vector product W @ x, which NumPy already runs on optimized BLAS, so the default is fast. Three optional accelerators help in different regimes, and all keep the public API and the results unchanged (each has a transparent NumPy fallback):

Accelerator How to enable Best for
Numba JIT pip install "esnfed[fast]" (auto) small/medium reservoirs
Sparse reservoir EchoStateNetwork(..., sparse=True) large, low-density reservoirs
float32 EchoStateNetwork(..., dtype=np.float32) large reservoirs (memory-bound)

What actually helps where

Honest measurements (one CPU, 3000 timesteps, density 0.1, harvest time in ms — lower is better), reproducible with python experiments/exp9_performance.py (needs esnfed[fast]). Use them as a guide to the regimes, not exact figures:

Reservoir N NumPy f64 Numba f64 NumPy f32 Sparse
50 50 4 (12×) 31 42
200 81 21 (3.8×) 41 62
800 411 340 322 261 (1.6×)
2000 3422 3982 (slower) 1776 (1.9×) 2808

The takeaways:

  • Numba is a big win for small reservoirs (12× at N=50, 3.8× at N=200), where Python per-step overhead dominates. For very large reservoirs the BLAS matvec dominates and Numba does not help (and can be slower), so the library auto-enables Numba only up to N = 1000; override with use_numba=True/False.
  • Sparse reservoirs win for large, low-density N — the matvec becomes O(edges) instead of O(N²).
  • float32 wins for large reservoirs (memory-bound), ~1.9× at N = 2000.

Numerical stability with float32

Only the reservoir and states are stored in float32 (the large O(T·N²) part); the small, ill-conditioned ridge solve is always done in float64, so float32 stays accurate.

Recommendations

import numpy as np
from esnfed import EchoStateNetwork, topologies

# Small/medium reservoir: just install esnfed[fast] — Numba kicks in automatically.
esn = EchoStateNetwork(1, 1, topologies.random_reservoir(200, rng=0))

# Large reservoir: go sparse and/or float32.
W = topologies.random_reservoir(2000, density=0.05, rng=0)
esn = EchoStateNetwork(1, 1, W, sparse=True, dtype=np.float32)

A few more notes:

  • The pure-NumPy default is unchanged — none of these are required, and the library still installs with only NumPy + NetworkX.
  • In the browser (live demo), Numba isn't available under Pyodide; the NumPy fallback is used automatically.
  • Numba pays a one-off JIT compilation cost (~1 s) on the first harvest.