Skip to content

Comments

Experimental cppyy#1769

Draft
Legend101Zz wants to merge 6 commits intobrian-team:masterfrom
Legend101Zz:experimental-cppyy
Draft

Experimental cppyy#1769
Legend101Zz wants to merge 6 commits intobrian-team:masterfrom
Legend101Zz:experimental-cppyy

Conversation

@Legend101Zz
Copy link
Contributor

Problem

The current Brian2 code generation pipeline suffers from a fundamental performance bottleneck. The issue is not tied to the specific tools we use, but rather to the Ahead-of-Time (AOT) compilation paradigm itself.

Regardless of whether we use Cython (our current approach) or manual C-extensions, the workflow remains slow and cumbersome:

  • Generate large C++ source files on disk
  • Invoke an external compiler (e.g., g++, clang) with significant overhead
  • Wait for compilation to complete (often 15–40 seconds, which disrupts interactivity)
  • Dynamically load the compiled result through a complex process

In other words, the bottleneck lies in the file-based, external-compiler, AOT workflow.


Proposed Solution: JIT Compilation with cppyy

This PR introduces cppyy as a new runtime code generation target, shifting from AOT to Just-in-Time (JIT) compilation.

With cppyy, C++ code is compiled in-memory using the Cling C++ interpreter, which eliminates:

  • File I/O overhead
  • External compiler process spawning
  • Long compilation waiting times
  • Complex dynamic loading procedures

Current Status

  • End-to-end JIT compilation pipeline
  • Basic neuron group simulations
  • State updates, thresholds, and resets
  • Template system for different operations
  • Integration with the device layer

Next Steps

Fix for dynamic arrays and spikequeue and synapses

@Legend101Zz
Copy link
Contributor Author

@mstimberg the initial PR I'll keep working on this branch itself and finally make the synapses too working on this , cheers :)

@review-notebook-app
Copy link

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@Legend101Zz
Copy link
Contributor Author

@mstimberg I added a introspection to do the stuff we were discussing yesterday , like to view the cppy code and codeobjects and even change it over the fly , I have added an attached jupyter sample for reference which you can use and play around with :)

Attaching a few screenshots of how it looks :

Screenshot 2026-02-14 at 16 01 02 Screenshot 2026-02-14 at 16 01 23

self.namespace[name] = value

# ── Dynamic arrays: store BOTH the data view AND the capsule ──
# The data view (_ptr_array_*) gives C++ direct pointer access
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I realised something while coding this , the way we coded dynamic arrays in runtime to be available for runtime mode , we'll have to change that for cppyy as RuntimeDevice currently creates its dynamic arrays through Cython wrappers. Those Cython wrappers own the underlying DynamicArray1D<T>*. If we want to go fully cppyy-native or have multiple backends only , we'll need to change how the RuntimeDevice stores dynamic arrays in cppyy codegen backend — either replacing the Cython wrappers with cppyy-managed objects, as that is what would be best ...

@Legend101Zz
Copy link
Contributor Author

here is a snippet I used to test things :

import time

import numpy as np

from brian2 import *

prefs.codegen.target = 'cppyy'
prefs.codegen.runtime.cppyy.enable_introspection = True # Enable introspection

# prefs.codegen.target = 'cython'
# prefs.codegen.runtime.cython.cache_dir = 'cythontmp/'
# prefs.codegen.runtime.cython.delete_source_files = False



# Hodgkin-Huxley neuron model

num_neurons = 100
duration = 500*ms

# Parameters
area = 20000*umetre**2
Cm = 1*ufarad*cm**-2 * area
gl = 5e-5*siemens*cm**-2 * area
El = -65*mV
EK = -90*mV
ENa = 50*mV
g_na = 100*msiemens*cm**-2 * area
g_kd = 30*msiemens*cm**-2 * area
VT = -63*mV

eqs = Equations('''
dv/dt = (gl*(El-v) - g_na*(m*m*m)*h*(v-ENa) - g_kd*(n*n*n*n)*(v-EK) + I)/Cm : volt
dm/dt = 0.32*(mV**-1)*4*mV/exprel((13.*mV-v+VT)/(4*mV))/ms*(1-m)-0.28*(mV**-1)*5*mV/exprel((v-VT-40.*mV)/(5*mV))/ms*m : 1
dn/dt = 0.032*(mV**-1)*5*mV/exprel((15.*mV-v+VT)/(5*mV))/ms*(1.-n)-.5*exp((10.*mV-v+VT)/(40.*mV))/ms*n : 1
dh/dt = 0.128*exp((17.*mV-v+VT)/(18.*mV))/ms*(1.-h)-4./(1+exp((40.*mV-v+VT)/(5.*mV)))/ms*h : 1
I : amp
''')

group = NeuronGroup(num_neurons, eqs,
                    threshold='v > -40*mV',
                    refractory='v > -40*mV',
                    method='exponential_euler')
group.v = El
group.I = '0.7*nA * i / num_neurons'


# SpikeMonitor: records spike times and indices (dynamic arrays)
spike_mon = SpikeMonitor(group)

# StateMonitor: records v for a few neurons every timestep (2D dynamic array)
state_mon = StateMonitor(group, 'v', record=[0, 25, 50, 75, 99])


print(f"Running {num_neurons} HH neurons for {duration}...")
t_start = time.perf_counter()
run(duration)
t_elapsed = time.perf_counter() - t_start
print(f"Done in {t_elapsed:.2f}s")

print(f"\nTotal spikes: {spike_mon.num_spikes}")
print(f"StateMonitor recorded {state_mon.t.shape[0]} timesteps "
      f"for {len(state_mon.record)} neurons")

# --- Now use the introspector ---
from brian2.codegen.runtime.cppyy_rt.introspector import get_introspector

intro = get_introspector()

# ---- 1. List all compiled code objects ----
print("=" * 60)
print("LIST OBJECTS")
print("=" * 60)
print(intro.list_objects())

# ---- 2. Inspect the state updater ----
print("\n" + "=" * 60)
print("INSPECT STATE UPDATER")
print("=" * 60)
# Using glob pattern — "stateupdater*" matches the full name
print(intro.inspect("*stateupdater*"))

# ---- 3. View just the params ----
print("\n" + "=" * 60)
print("PARAMS")
print("=" * 60)
print(intro.params("*stateupdater*"))

# ---- 4. View the namespace ----
print("\n" + "=" * 60)
print("NAMESPACE")
print("=" * 60)
print(intro.namespace("*stateupdater*"))

# ---- 5. View C++ globals ----
print("\n" + "=" * 60)
print("C++ GLOBALS")
print("=" * 60)
print(intro.cpp_globals())

# ---- 6. Evaluate a C++ expression ----
print("\n" + "=" * 60)
print("EVAL C++")
print("=" * 60)
print(f"M_PI = {intro.eval_cpp('M_PI')}")
print(f"sizeof(double) = {intro.eval_cpp('sizeof(double)', 'size_t')}")
print(f"_brian_mod(7, 3) = {intro.eval_cpp('_brian_mod(7, 3)', 'int32_t')}")

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.

1 participant