Continuing the discussion from Delay for 'summed' variables in synapses:
Dear all,
I am a new Brian2 (version 2.5.0.1) user, currently working on a model using delayed summed variables.
Indeed, at each time step t of the simulation, to compute the summed variable, the Synapses object needs to access the value of a specific variable of the input NeuronGroup object at time t-d_ij, with d_ij the delay between the input neuron i and the synapse between neurons i & j.
I implemented a NumPy version, using a buffer to keep values in memory for several time steps, and using .run_regularly both for the update of this buffer, and for fetching the correct delayed variables for the synapses. My implementation was inspired by this former solution proposed by @mstimberg : Example of implementing a buffer for a delayed variable in Brian2 · GitHub
Below are the most relevant parts of it (simplified to keep only the elements related to the issue):
- The part where the input NeuronGroup is defined, with a function to update the buffer of variable v at each time step:
G = NeuronGroup(nb_neurons, '''dv/dt = 0.4/ms : 1
buffer_pointer : integer (shared)
''', method='euler', threshold='v > 4.0', reset='v = 0.0')
buffer_size = 1+ max(delays)
G.variables.add_array('v_buffer', size=(buffer_size, len(G)))
update_code = '''buffer_pointer = (buffer_pointer + 1) % buffer_size
return_ = update_v_buffer(v, v_buffer, buffer_pointer)'''
buffer_updater = G.run_regularly(update_code, codeobj_class=NumpyCodeObject)
@implementation('numpy', discard_units=True)
@check_units(v=1, v_buffer=1, buffer_pointer=1, result = 1) # the units
def update_v_buffer(v, v_buffer, buffer_pointer):
'''
A buffer updated at each time step keeps v(t) for all input neurons of G for a temporal time window defined by the highest delay available
:param v:
:param v_buffer:
:param buffer_pointer:
:return:
'''
v_buffer[buffer_pointer, :] = v
return 0.0
- The part where the Synapses object is defined, with a function to fetch the delayed values of variable v at each time step:
S = Synapses(G,H, '''
w : 1
v_delayed : 1
delay_step : integer
I_post = v_delayed*w : 1 (summed)
''', method='euler')
S.connect()
S.delay_step = delays
S.variables.add_reference('v_buffer_from_synapse', G, varname='v_buffer', index=None)
S.variables.add_reference('buffer_pointer_from_synapse', G, varname='buffer_pointer', index=None)
update_code_syn ='''
v_delayed = get_v_delayed(v_buffer_from_synapse, buffer_pointer_from_synapse, buffer_size, i, delay_step)
'''
get_new_v_delayed = S.run_regularly(update_code_syn, codeobj_class=NumpyCodeObject)
@implementation('numpy', discard_units=True)
@check_units(v_buffer_from_synapse=1, buffer_pointer_from_synapse=1, buffer_size=1, i=1, delay_step=1, result=1)
def get_v_delayed(v_buffer_from_synapse, buffer_pointer_from_synapse, buffer_size, i, delay_step):
return v_buffer_from_synapse[(buffer_pointer_from_synapse - delay_step) % buffer_size, i]
This works as expected, but I was wondering if anyone had tips to make it faster.
Indeed, it is time-consuming, as it can be seen thanks to profiling:
Profiling summary
=================
synapses_run_regularly 92.75 1.0 91.06 %
synapses_summed_variable_I_post 7.85 1.0 7.70 %
neurongroup_run_regularly 0.59 1.0 0.58 %
neurongroup_stateupdater 0.25 1.0 0.24 %
neurongroup_thresholder 0.15 1.0 0.15 %
The update of the buffer (neurongroup_run_regularly) is ok, but the time spent to fetch the delayed variables for the summed variables (synapses_run_regularly) is quite high (more than 90% of the overall simulation time).
One puzzling fact about synapses_run_regularly is that it is obviously time-consuming, even when the number of synapses is reduced : I launched the simulation with only autapses ( S.connect(j=‘i’)), thus with as many synapses than neurons in the input NeuronGroup, but the computation time was still far higher than the one of neurongroup_run_regularly.
synapses_run_regularly is currently my bottleneck, thus I would like to improve it as much as possible, to use this model with a higher number of neurons.
The question of this topic: How could this bottleneck be reduced, as efficiently as possible (from the software side) ?
I already thought & tested a few things, but I got no success so far (and I am open to new ideas!):
-
using the @network_operation() variant proposed in the shared topic instead of .run_regularly : I got equivalent / slightly worse results
-
Try to use a cython implementation instead of the NumPy version: it did not work, as the 2D array (the buffer) appears to not be accepted as an input…
-
Try a ‘cpp’ implementation: the possible problem is that I will use the model in a global simulation where @network_operation() will be used, for interaction with an environment. Thus, it appears that, for the sake of the simulation, the standalone mode is not usable. Is a ‘cpp’ implementation only usable when the device is set to ‘cpp_standalone’, or is it possible to use a cpp implementation just for synapses_run_regularly, while staying in runtime mode?
-
Try to use Brian2GeNN : I would rather not use it, because of the unsupported features (that are included in my simulation) :
Unsupported features in Brian2GeNN — Brian2GeNN 1.5 documentation -
Try to use frameworks to boost NumPy code, like Numba:
It appears there was a project to make Numba compatible with Brian several years ago (GitHub - brian-team/brian2numba: Numba code generation backend for Brian 2), but in my current Brian2 version NumbaCodeObject is not defined, thus it is not usable in .run_regularly() .
Another possibility would be to directly import Numba in a script that is also using Brian2, like below (but I am not sure that it is a good idea, as it might lead to issues / conflicts ? ) :
from brian2 import *
from numba import njit
@njit
@implementation('numpy', discard_units=True)
@check_units(v_buffer_from_synapse=1, buffer_pointer_from_synapse=1, buffer_size=1, i=1, delay_step=1, result=1)
def get_v_delayed(v_buffer_from_synapse, buffer_pointer_from_synapse, buffer_size, i, delay_step):
return v_buffer_from_synapse[(buffer_pointer_from_synapse - delay_step) % buffer_size, i]
Anyway, I get the following error :
Traceback (most recent call last):
File "/home/.../.py", line 128, in Simulation
def get_v_delayed(v_buffer_from_synapse, buffer_pointer_from_synapse, buffer_size, i, delay_step):
File "/home/.../.local/lib/python3.8/site-packages/numba/core/decorators.py", line 258, in njit
return jit(*args, **kws)
File "/home/.../.local/lib/python3.8/site-packages/numba/core/decorators.py", line 179, in jit
return wrapper(pyfunc)
File "/home/.../.local/lib/python3.8/site-packages/numba/core/decorators.py", line 198, in wrapper
raise TypeError(
TypeError: The decorated object is not a function (got type <class 'brian2.core.functions.Function'>).
Process finished with exit code 1
Making the decorators in another order before the function produces other errors:
@implementation('numpy', discard_units=True)
@check_units(v_buffer_from_synapse=1, buffer_pointer_from_synapse=1, buffer_size=1, i=1, delay_step=1, result=1)
@njit
def get_v_delayed(v_buffer_from_synapse, buffer_pointer_from_synapse, buffer_size, i, delay_step):
File "/home/.../.py", line 128, in Simulation
def get_v_delayed(v_buffer_from_synapse, buffer_pointer_from_synapse, buffer_size, i, delay_step):
File "/home/.../.local/lib/python3.8/site-packages/brian2/core/functions.py", line 559, in do_user_implementation
function.implementations.add_numpy_implementation(wrapped_func=func,
File "/home/.../.local/lib/python3.8/site-packages/brian2/core/functions.py", line 376, in add_numpy_implementation
new_globals = dict(orig_func.__globals__)
AttributeError: 'CPUDispatcher' object has no attribute '__globals__'
Process finished with exit code 1
Is there a way to use decorators from both Brian2 & Numba on the same function? Or is it impossible / too prone to errors?
Thank you very much for your attention!
Here is the link of my implementation of the delayed summed variable, in case it can be of any use : GitHub - Gaerdil/Brian2_DelayedSummedVariable
p.s : This topic is about advice to improve an implementation allowing a feature that was requested in the linked topic. I am not sure if it entirely belongs to the “Support” category, but I don’t know to which other category it could belong to…