Skip to content

Hybrid numba - #775

Open
maedoc wants to merge 117 commits into
masterfrom
hybrid-numba
Open

Hybrid numba#775
maedoc wants to merge 117 commits into
masterfrom
hybrid-numba

Conversation

@maedoc

@maedoc maedoc commented Apr 9, 2026

Copy link
Copy Markdown
Member

In anticipation of merging #771 this adds a numba backend to accelerate hybrid simulations.

This was referenced Apr 27, 2026
maedoc and others added 16 commits April 30, 2026 13:19
* added numba dfun for model
* signature for elementwise if/else, njit for helper funcs
* tuple unpack svars
* test: fix and test numba kionex
* fix: notebook fixes
* fix: pickle load error
* fix(kionex): shape error in notebook
* fix(jansen-rit notebook): numpy 2.4 scalar assignment compat

In numpy 2.4+, assigning a non-scalar ndarray to a scalar index
raises ValueError instead of a deprecation warning. The expression
phi_n_scaling = (jrm.a * jrm.A * ...) yields a (1,) array, so
sigma[3] = phi_n_scaling now fails. Fix by adding .item() to
extract the scalar value.
* added numba dfun for model
* signature for elementwise if/else, njit for helper funcs
* feat(kionex): extend numba backend and implement kionex
* fix(kionex): fix dfun call
…n 2 pickled gpickle files

pandas.read_pickle defaults to ASCII encoding which fails on binary numpy
data pickled with Python 2. The gpickle format is just a pickled networkx
graph, so use stdlib pickle.load with encoding='latin1' instead.
- Rebase hybrid-numba onto origin/master (picks up KIonEx numba backend
  commits: Add numba dfun for model KIonEx + Enable numba backend)
- Extend NbHybridBackend._check_compatibility to accept KIonEx alongside
  MontbrioPazoRoxin
- Add sin/cos/exp/log shorthands to nb-hybrid-sim.py.mako template header
  (required by KIonEx dfun_helpers which reference exp/log by name)
- Extend nb-hybrid-sim.py.mako to emit per-subnet dfun_constants,
  dfun_helpers, and dfun_intermediates when the model provides them
  (KIonEx-style models with ionic current helpers)
- New test: tvb/tests/library/simulator/hybrid/test_mpr_kionex.py
  Pure-Python Simulator.run() smoke test: MPR + KIonEx two-subnet hybrid
  (output shape, NaN-free at safe ICs, bidirectional inter-projections)
- New test class: TestNbHybridMprKIonEx in test_nb_hybrid.py
  NbHybridBackend compiled kernel: acceptance, shape correctness, and
  Python-vs-Numba numerical consistency for MPR + KIonEx
Replace log(K_o/K_i) and log(Na_o/Na_i) with log(K_o)-log(K_i) and
log(Na_o)-log(Na_i) in all four representation sites:

  - dfun_helpers strings (used by nb-hybrid-sim codegen)
  - _numpy_dfun inner helper lambdas
  - standalone @njit I_K_form / I_Na_form (two duplicate copies)

In float32 the ratio K_o/K_i can underflow to zero before the log is
taken, yielding -inf.  Computing log(a)-log(b) avoids the intermediate
ratio and keeps both operands in the normal float32 range as long as each
concentration is individually representable.

Note: log(Cl_o0/Cl_i0) is left as-is because both values are compile-time
constants that are always positive and finite.

Also document in TestNbHybridMprKIonEx.test_output_shapes that NaN-freedom
for KIonEx in float32 is a separate open item (K_o can go negative under
extreme states), while the Python float64 path is already checked in
test_mpr_kionex.py.
Add Sigmoidal and SigmoidalJansenRit coupling functions to the numba
hybrid backend, and support the JansenRit model as a second model target
alongside MontbrioPazoRoxin and KIonEx.

Backend changes (nb_hybrid.py):
- _cfun_type(): recognise Sigmoidal -> 'sigmoidal', SigmoidalJR -> 'sigmoidal_jr'
- _cfun_params(): return float32[5] array (replaces (cfun_a, cfun_b) 2-tuple)
  Layout: [a, sigma, midpoint, cmin, cmax] for Sigmoidal;
          [a, e0, r, v0, 0] for SigmoidalJansenRit
- _check_compatibility(): accept JansenRit alongside MPR and KIonEx
- _run_compiled(): pass cfun_params array (single arg per projection)

Template changes (nb-hybrid-sim.py.mako):
- cfun_a/cfun_b scalar args -> cfun_params float32[5] array throughout
- Pre-cfun hook inside CSR inner loop for sigmoidal_jr (pre-synaptic
  sigmoid on source state values before weighted-sum accumulation)
- Post-cfun dispatch: sigmoidal branch added; linear/scaling use cfun_params[0/1]
- Both mono_src (n_modes==1) and general paths updated

Model changes (jansen_rit.py):
- Add coupling_terms, parameter_names, dfun_helpers (sigm_jr helper),
  dfun_intermediates, state_variable_dfuns for numba codegen
- coupling enters y4 equation only as Coupling_Term
- 13 scalar parameters baked at codegen time

Tests (test_nb_hybrid.py):
- TestNbHybridSigmoidalCfun: 6 tests (Sigmoidal + SigmoidalJansenRit,
  shape + finite + matches-python)
- TestNbHybridJansenRit: 4 tests (acceptance, shape, finite, NB vs Python)
- TestNbHybridMultiMode: 3 tests (MPR with n_modes=2, shape + matches-python)
- Total: 260 tests passing (was 28)
Replace exec()-into-dict with _build_as_module(): renders generated source
to a real .py file under $TMPDIR/tvb_nb_hybrid_cache/, registers it in
sys.modules, then imports it. This gives Numba a real co_filename so that
cache=True on @nb.njit writes .nbi/.nbc native-code files next to the .py.

On second process startup with the same network topology the SHA-256 key
matches the existing .py, Numba loads the native cache directly (~50 ms vs
~5 s cold JIT). The in-process _COMPILED_FN_CACHE dict is retained as an
additional first-level fast path.

Changes:
- _build_as_module() helper with atomic os.replace write
- _build() delegates to _build_as_module instead of exec()
- NbHybridBackend.clear_cache() classmethod (in-process + disk)
- NbHybridBackend.get_cache_dir() staticmethod
- Template: cache=True added to all @nb.njit decorators (inline + plain)
- TestNbHybridDiskCache: 3 tests (dir created, in-process hit, clear)
Mark as done: Sigmoidal cfun, SigmoidalJansenRit cfun, JansenRit model,
n_modes>1 test, disk-persistent JIT cache. Update test count to 44 (263
total across backend + hybrid suites). Update §6 supported configuration
table and §7/§8 to reflect 2026-04-10 work.
… strip CSR zeros

Model codegen attributes added (coupling_terms, parameter_names,
dfun_intermediates, state_variable_dfuns):
- ReducedWongWang (wong_wang.py): 1 svar S, 8 params, 2 intermediates
  (x_ww, H_ww); S clipped to [0,1] via existing boundaries
- Epileptor (epileptor.py): 6 svars, 16 params, 4 ternary intermediates
  (f1, zterm, h, f2); modification=False only
- WilsonCowan (wilson_cowan.py): 2 svars E/I, 22 params, 4 intermediates
  (x_e, x_i, s_e, s_i); shifted sigmoid (shift_sigmoid=True only)
- Generic2dOscillator (oscillator.py): already added in prior session

Backend (nb_hybrid.py):
- _check_compatibility: accept RWW, Epileptor, WilsonCowan (7 models total)
- Per-model constraint checks: rejects Epileptor(modification=True) and
  WilsonCowan(shift_sigmoid=False) with NotImplementedError
- _build_projection_info: strip structural epsilon zeros via
  eliminate_zeros() on a copy of p.weights before extracting
  .data/.indices/.indptr; idelays aligned with stripped structure

Tests (test_nb_hybrid.py):
- TestNbHybridReducedWongWang: 4 tests (accepted, shape, finite, py-match)
- TestNbHybridEpileptor: 4 tests
- TestNbHybridWilsonCowan: 4 tests (uses source_cvar=[0] for E-only coupling)
- test_rejects_unsupported_model: updated to use SupHopf (WilsonCowan now accepted)
- 283 tests passing total (was 52)
…/8.9/8.10)

§8.8 Resumable runs / snapshot API:
- CompiledNetworkFn.run(return_snapshot=True) -> (outputs, snapshot)
  snapshot = {'states': [...], 'buffers': {...}}; captured from in-place
  Numba writes, zero kernel changes required
- CompiledNetworkFn.resume(snapshot, nstep) restores state+buffers and
  continues; numerically identical to a single longer run
- _run_compiled gains _initial_buffers=None parameter

§8.9 Testing gaps:
- TestNbHybridModeMap (4 tests): non-identity mode_map on 2-mode MPR nets;
  verifies accepted, shape, finite, and that mixing actually differs from identity
- TestNbHybridLargeNScaling (2 tests): N=100 no-error smoke test; N=50
  speedup regression (cached Numba vs Python loop with generous bound)

§8.10 Code quality:
- __all__ added to nb_hybrid.py (NbHybridBackend, CompiledNetworkFn,
  NetworkAnalysis, SubnetworkInfo, ProjectionInfo)
- backend/__init__.py: lazy __getattr__ for NbHybridBackend + CompiledNetworkFn
  (direct import caused circular dep via integrators -> equations -> backend)
- Plan doc: §8.3 marked DONE, §6 table extended, Phase C2 added to §7

Tests: 74 passing (was 68)
…§8.4/8.10)

§8.10 Code quality:
- compile(debug_nojit=True) / TVB_HYBRID_NO_JIT=1 env var: replaces @nb.njit
  decorators with no-ops for fast debugging; hash naturally differs (rendered
  source changes) so no cache collisions
- run_network() also accepts debug_nojit kwarg
- Type annotations added: _run_compiled, _analyse, _check_compatibility,
  _build_projection_info
- Module docstring references nb_hybrid_plan.md
- TestNbHybridDebugNojit: 2 tests (runs without error, matches JIT output)

§8.4 Lazy stimulus infrastructure (stub):
- _STIM_LAZY_THRESHOLD_MB = 64 (overridable via TVB_HYBRID_LAZY_STIM_MB)
- _stim_estimate_mb(sn_info, nstep): projected stim array size in MiB
- _compute_stimulus_lazy stub: windowed (step_start, step_end) computation
  with TODO explaining template change needed (network_chunk must use t_local
  indexing instead of global t-1 before lazy path can be wired to run_network)
- TestStimulusMemoryEstimate: 2 tests (small <64MB, large >64MB)

Plan doc (nb_hybrid_plan.md):
- Status line updated; §6 table extended (8 new rows); Phase D + E added
- §8.2 checkboxes all ticked; §8.6 FHN note clarified; §8.7 marked DONE
- §8.8 marked DONE; §8.9 table fully checked; §8.10 checkboxes all ticked

Tests: 78 passing (was 74)
nb_hybrid_next.md: plan for bulk model codegen (17 scalar models via
Ralph loop), monitor support (M1 Python dispatch, M2 in-kernel subsample,
M3 projection monitors), combined-mode dfun generation for
ReducedSetFitzHughNagumo/HindmarshRose (unrolled matrix ops at codegen time).

ralph_add_model_codegen.sh: bash script that iterates over 17 model files,
calling opencode run with zai/glm-5.1 to add coupling_terms, parameter_names,
dfun_intermediates, state_variable_dfuns. Includes validation (attr existence,
key matching, coupling_term usage, parameter attribute check) and 3-retry
with revert on failure.

nb_hybrid_plan.md: updated status line, reclassified ReducedSetFHN from
'permanently deferred' to 'deferred to next phase (combined-mode)', updated
file summary table.
…emplate (Phase F)

Phase F — Bulk Model Codegen (Ralph Loop):
- 15 scalar models via AI-assisted Ralph loop: SupHopf, Kuramoto,
  Epileptor2D, Hopfield, LarterBreakspear, EpileptorRestingState,
  EpileptorCodim3, EpileptorCodim3SlowMod, ZetterbergJansen,
  ReducedWongWangExcInh, CoombesByrne, CoombesByrne2D,
  GastSchmidtKnosche_SD, GastSchmidtKnosche_SF, DumontGutkin
- Each model gets coupling_terms, parameter_names, dfun_intermediates,
  state_variable_dfuns codegen attributes
- 2 Zerlaut models via custom nb-zerlaut-dfun.py.mako template:
  ZerlautAdaptationFirstOrder (5 svars, erfc transfer function pipeline)
  ZerlautAdaptationSecondOrder (8 svars, numerical-derivative covariance)

Combined-mode dfun (nb_hybrid_next.md §3 / G4):
- ReducedSetFitzHughNagumo (4 svars × 3 modes): dfun_mode='combined'
- ReducedSetHindmarshRose (6 svars × 3 modes): dfun_mode='combined'
- Template gains is_combined branch in nb-hybrid-sim.py.mako
- Inter-mode matrix products (Aik, Bik, Cik) unrolled at codegen time
- derived_matrix_names/ops model attributes for combined-mode metadata

Gaps G1–G3:
- G1: Multiplicative noise raises NotImplementedError (was silent wrong result)
- G2: chunk_size > min_horizon raises ValueError (was silent wrong result)
- G3: Monitor M1 Python-side dispatch: monitors= kwarg on run_network()
  supports TemporalAverage, Raw, SubSample, GlobalAverage, AfferentCoupling

Test coverage: 78 → 145 tests (+67), all passing across 28 test classes
Models supported: 8 → 27 (19 new via codegen)
_ralph_models parametrized smoke tests (accepted + shape + finite)
Zerlaut dedicated tests (shape + finite + python-match)
Monitor dispatch tests (TestNbHybridMonitors)
maedoc added 20 commits May 20, 2026 09:31
…rs, model dfun corrections, test improvements, notebook fixes

Phase 1-4 fixes from hybrid-numba code review (OVERALL_REVIEW.md):
- cvar_utils: bare int handling, shared helper extraction
- projection_utils: connectivity extraction for intra projections
- inter_projection: target cvar validation, target symmetry check
- stimulus: resolve_target_cvar instead of resolve_cvar_names
- infinite_theta: alpha default 10.0→1.0, docstring equations
- jansen_rit: ZetterbergJansen keke→kiki in derivative[9]
- larter_breakspear/zerlaut: local_coupling deprecation docs
- nb_hybrid_sweep_cpu: NotImplementedError guard for model-param sweeps
- nb_hybrid_cuda_sweep: cuda.local.array(76)→MAX_NODES=1024
- nb_hybrid_cuda_sweep_backend: copy BOLD states from GPU
- nb-hybrid-sweep-cuda.py.mako: per-source-subnet horizon
- test fixes: CSR diagonal, CUDA fixture, eval() sort, unseeded RNG
- test_subnetwork: test hybrid Stim instead of StimuliRegion
- test_base: fix JansenRit source_cvar indices
- test_classic_tvb_coupling_equiv: update stale docs/names
- test_toy_synchronization: non-trivial ICs, Kuramoto coupling, sim length
- test_model_codegen_parity: new file, 12 model dfun parity tests
- test_stimulus_utils: new file, 7 stimulus utility tests
- notebooks: axis fix, separate integrators, title restore, warnings
…ove unused dfe_plus, add local_coupling deprecation note
… noise floor corruption, add local_coupling deprecation note
…omission in EpileptorRestingState state_variable_dfuns
…Mod state_variable_dfuns and parameter_names
…at dfun_intermediates only encode shift_sigmoid=True branch
…_dfuns in DecoBalancedExcInh to include M_i scaling
…, EpileptorRestingState modification=True, Hopfield dynamic=True, and WilsonCowan shift_sigmoid=False
…field, stefanescu_jirsa

- simulator.py: fix 3 _run_numba bugs (time dup, concat fail, merged monitor IndexError); replace assert with explicit if/raise for dt validation
- base_projection.py: fix lengths=None crash by defaulting to zero-lengths CSR; replace assert with if/raise for nnz check
- hopfield.py: fix state_variable_dfuns theta expression (x→theta, taux→tauT); add tauT to parameter_names
- stefanescu_jirsa.py: fix FHN/HR codegen alpha expressions (c_alpha_{m}*mu→c_xi_{m}) to match Python dfun
NetworkSet no longer accepts 'stimuli' kwarg (moved to Subnetwork.stimuli).
Update 28 NetworkSet(stimuli=[...]) calls to assign sn.stimuli = [...] instead.
Comment thread tvb_library/tvb/simulator/backend/nb_hybrid_cuda_sweep_backend.py Fixed
maedoc added 9 commits June 8, 2026 16:52
…dation

- Two scenarios: CRBL-only (open-loop, 27 nodes) and WW+CRBL (closed-loop, 126 nodes)
- 10s simulations at dt=1ms (~10s wall-clock total, python backend)
- Per-subnet recorders capture all 4 CRBL populations (GrC, GoC, MLI, PC)
- Welch PSD analysis with frequency band overlays (delta/theta/alpha/beta/gamma)
- Carrier frequency comparison table across populations and scenarios
- Scientific background on construct validation (Esaghei 2022, Lorenzi 2023/2025)
- Inline figures in notebook via %pylab inline
- CerebellarMF model registered in models/__init__.py
…F-TVB

Critical fixes validated against monolithic reference simulation:

Model parameter defaults (cerebellar_mf.py):
- weight_noise: 10.5 → 4e-3 (was 2625x too high)
- external_input_ex_ex: 0.0 → 3.15e-4 (was missing entirely)
- tau_OU: 5.0 → 3.5 (1.43x off)
- state_variable_range: match Lorenzi 2023 ICs [0.5e3, 5e3, 15e3, 38e3]

Anatomical routing fractions (new parameters):
- frac_mossy=0.57, frac_parallel=0.43, mf_to_grc=0.97, mf_to_goc=0.03,
  pf_to_goc=0.14, pf_to_mli=0.55, pf_to_pc=0.31
- These decompose the monolithic's single cvar coupling into the hybrid's
  explicit mossy/parallel pathways in the dfun

Demo script updates:
- Column-normalize SC weights (matching monolithic init)
- Add P4 (GrC→mossy) IntraProjection alongside P5 (GrC→parallel)
- Use nperseg=1024 for Welch PSD (avoids resolution artifacts)
- Open-loop firing rates now match monolithic within ~10%:
  GrC 0.17 vs 0.18, GoC 1.54 vs 1.73, MLI 4.72 vs 5.26, PC 11.85 vs 13.22
…reproduction notebook

- Add use_legacy_goc_e_e flag to CerebellarMF for reproducing published
  cMF-TVB results (GoC E_i bug in parallel_crbl.py:583)
- Fix production parameter defaults: alpha_mli=5.0, external_input=0.0,
  frac_mossy/parallel=1.0, kHz-scale initial conditions
- Add external_input_in_ex parameter for GoC drive (matches monolithic)
- Add add_noise_mli_pc flag for CRBL-only anatomical routing
- Update Numba Mako template (nb-cerebellar-dfun) for legacy mode support
- Fix noise variable name collision in nb-hybrid-sim.py.mako: rename
  stochastic noise array param from 'noise' to '_nsig_arr' to avoid
  shadowing CerebellarMF's 'noise' state variable
- Update tests: legacy+fixed GoC parity, production defaults
- Add reproduction notebook: UC-1 (CRBL-only) + UC-2 (WW+CRBL) with
  both Python and Numba backends, reference comparison (39.1 Hz exact)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants