diff --git a/pynn_genn/connectors.py b/pynn_genn/connectors.py index d8b5561f..06b3e34e 100644 --- a/pynn_genn/connectors.py +++ b/pynn_genn/connectors.py @@ -140,7 +140,7 @@ def __init__(self, safe=True, callback=None): class AllToAllConnector(GeNNConnectorMixin, AllToAllPyNN): __doc__ = AllToAllPyNN.__doc__ - def __init__(self, allow_self_connections=True, safe=True, callback=None,): + def __init__(self, allow_self_connections=True, safe=True, callback=None): GeNNConnectorMixin.__init__(self, use_sparse=False) AllToAllPyNN.__init__( self, allow_self_connections=allow_self_connections, @@ -172,7 +172,7 @@ class FixedTotalNumberConnector(GeNNConnectorMixin, FixTotalPyNN): __doc__ = FixTotalPyNN.__doc__ def __init__(self, n, allow_self_connections=True, with_replacement=True, - rng=None, safe=True, callback=None,): + rng=None, safe=True, callback=None): GeNNConnectorMixin.__init__(self) FixTotalPyNN.__init__(self, n, allow_self_connections, with_replacement, rng, safe=safe, callback=callback) diff --git a/pynn_genn/projections.py b/pynn_genn/projections.py index d22f21fe..2cd58e61 100644 --- a/pynn_genn/projections.py +++ b/pynn_genn/projections.py @@ -3,6 +3,8 @@ except NameError: # Python 3 xrange = range +import warnings + from collections import defaultdict, namedtuple, Iterable from itertools import product, repeat import logging @@ -16,16 +18,48 @@ from pyNN import common from pyNN.connectors import AllToAllConnector, FromListConnector, \ - FromFileConnector + FromFileConnector, OneToOneConnector from pyNN.core import ezip from pyNN.space import Space from pyNN.parameters import LazyArray +from pygenn import genn_wrapper + from . import simulator from .standardmodels.synapses import StaticSynapse from .model import sanitize_label from .contexts import ContextMixin -from .random import NativeRNG +from .random import NativeRNG, RandomDistribution + + +class RetrieveProceduralWeightsException(Exception): + def __str__(self): + return ("Downloading weights from device when using procedural " + "connectivity is not supported") + +class RetrieveProceduralConnectivityException(Exception): + def __str__(self): + return ("Downloading procedural connectivity from device is not supported") + +class PositiveNumThreadsException(Exception): + def __str__(self): + return ("The parameter num_threads_per_spike has to be greater than 0") + +class OnlyStaticWeightsProceduralException(Exception): + def __str__(self): + return ("Only static projections can be generated as procedural.") + +class TuneThreadsPerSpikeWarning(Warning): + def __str__(self): + return ("Performance of network may vary if num_threads_per_spike is not " + "appropriately selected") + +class OneToOneThreadsPerSpikeWarning(Warning): + def __str__(self): + return ("A OneToOneConnector can only really use a single thread per spike. " + "Setting Projection parameter num_threads_per_spike to 1") + +DEFAULT_NUM_THREADS_PER_SPIKE = 8 # Tuple type used to store details of GeNN sub-projections SubProjection = namedtuple("SubProjection", @@ -121,7 +155,8 @@ class Projection(common.Projection, ContextMixin): def __init__(self, presynaptic_population, postsynaptic_population, connector, synapse_type=None, source=None, - receptor_type=None, space=Space(), label=None): + receptor_type=None, space=Space(), label=None, + use_procedural=False, num_threads_per_spike=None): # Make a deep copy of synapse type # so projection can independently change parameters synapse_type = deepcopy(synapse_type) @@ -137,6 +172,14 @@ def __init__(self, presynaptic_population, postsynaptic_population, self._sub_projections = [] self.use_sparse = connector.use_sparse + + # later on we can actually asses if it + # is possible to use procedural synapses or not + self.use_procedural = use_procedural + if not num_threads_per_spike is None and num_threads_per_spike <= 0: + raise PositiveNumThreadsException() + self.num_threads_per_spike = num_threads_per_spike + # Generate name stem for sub-projections created from this projection # **NOTE** superclass will always populate label PROPERTY # with something moderately useful i.e. at least unique @@ -177,7 +220,13 @@ def _get_attributes_as_arrays(self, names, multiple_synapses="sum"): for sub_pop in self._sub_projections: # Loop through names and pull variables for n in names: - if n != "presynaptic_index" and n != "postsynaptic_index" and n in sub_pop.syn_pop.vars: + # grabbing weights when using procedurally generated projections + # is not possible + if n == "g" and self.use_procedural: + raise RetrieveProceduralWeightsException() + + if (n != "presynaptic_index" and n != "postsynaptic_index" and + n in sub_pop.syn_pop.vars): genn_model.pull_var_from_device(sub_pop.genn_label, n) # If projection is sparse @@ -194,6 +243,10 @@ def _get_attributes_as_arrays(self, names, multiple_synapses="sum"): # if we were able to initialize connectivity on device # we need to get it before examining variables if self._connector.connectivity_init_possible: + # grabbing connectivity when using procedurally generated + # projections is not possible + if self.use_procedural: + raise RetrieveProceduralConnectivityException() sub.syn_pop.pull_connectivity_from_device() # Get connection indices in @@ -215,6 +268,11 @@ def _get_attributes_as_arrays(self, names, multiple_synapses="sum"): else: # Loop through variables for n in names[0]: + if n == "g" and self.use_procedural: + # grabbing weights when using procedurally generated projections + # is not possible + raise RetrieveProceduralWeightsException() + # Create empty array to hold variable var = np.empty((self.pre.size, self.post.size)) @@ -247,6 +305,11 @@ def _get_attributes_as_list(self, names): for sub_pop in self._sub_projections: # Loop through names and pull variables for n in names: + if n == "g" and self.use_procedural: + # grabbing weights when using procedurally generated projections + # is not possible + raise RetrieveProceduralWeightsException() + if n != "presynaptic_index" and n != "postsynaptic_index": genn_model.pull_var_from_device(sub_pop.genn_label, n) @@ -338,17 +401,53 @@ def _get_sub_pops(self, pop, neuron_slice, conn_inds, conn_mask): else: return [(pop, neuron_slice, conn_mask)] + def can_use_procedural(self, params): + # do a series of checks to see if the projection can be computed + # on the fly by the GPU + + # select the apropriate matrix type given the connector type + if isinstance(self._connector, AllToAllConnector): + mtx_type = "DENSE_PROCEDURALG" + else: + mtx_type = "PROCEDURAL_PROCEDURALG" + + # did the user ask for a procedural projection? + if not self.use_procedural: + return False, mtx_type + + # did the user set the projection as plastic? not valid! + if not isinstance(self.synapse_type, StaticSynapse): + # todo: should this be a warning? + raise OnlyStaticWeightsProceduralException() + # return False + + weights_ok = False + if 'g' in params: + g = params['g'] + # if weights were not expanded and are either homogeneous (constant) + # or to be generated on device + if (isinstance(g, LazyArray) and + (g.is_homogeneous or + (isinstance(g.base_value, RandomDistribution) and + isinstance(g.base_value.rng, NativeRNG)))): + weights_ok = True + # if weights were expanded but are homegeneous + # todo: not sure if this case can happen? + elif not np.allclose(g, g[0]): + weights_ok = True + + # can we generate the connectivity on device or + # is easily assumed as in the All-to-All + connect_ok = (self._connector.connectivity_init_possible or + isinstance(self._connector, AllToAllConnector)) + + return (weights_ok and connect_ok), mtx_type def _create_native_projection(self): """Create GeNN projections (aka synaptic populatiosn) This function is supposed to be called by the simulator """ - if self.use_sparse: - matrix_type = "SPARSE_INDIVIDUALG" - else: - matrix_type = "DENSE_INDIVIDUALG" - # Set prefix based on receptor type # **NOTE** this is used to translate the right set of # neuron parameters into postsynaptic model parameters @@ -376,7 +475,6 @@ def _create_native_projection(self): conn_params=params): self._connector.connect(self) - # Convert pre and postsynaptic indices to numpy arrays pre_indices = np.asarray(pre_indices, dtype=np.uint32) post_indices = np.asarray(post_indices, dtype=np.uint32) @@ -390,6 +488,14 @@ def _create_native_projection(self): for c in self._connector.on_device_init_params: params[c] = self._connector.on_device_init_params[c] + use_procedural, mtx_type = self.can_use_procedural(params) + if use_procedural: + matrix_type = mtx_type + elif self.use_sparse: + matrix_type = "SPARSE_INDIVIDUALG" + else: + matrix_type = "DENSE_INDIVIDUALG" + # Extract delays # If the delays were not expanded on host, check if homogeneous and # evaluate through the LazyArray method @@ -424,13 +530,46 @@ def _create_native_projection(self): # prevented PyNN from expanding indices if self._connector.connectivity_init_possible: self._on_device_init_native_projection( - matrix_type, prefix, params, delay_steps) + matrix_type, prefix, params, delay_steps, use_procedural) else: self._on_host_init_native_projection( - pre_indices, post_indices, matrix_type, prefix, params, delay_steps) + pre_indices, post_indices, matrix_type, prefix, params, + delay_steps, use_procedural) + + def _setup_procedural(self, synaptic_population): + if isinstance(self._connector, AllToAllConnector): + return + + # if we can use a procedural connection set the apropriate span type + synaptic_population.pop.set_span_type( + genn_wrapper.SynapseGroup.SpanType_PRESYNAPTIC) + + # if we want and can use a procedural connection, check for + # the number of threads selected by teh user + n_thr = self.num_threads_per_spike + # if it wasn't set + if self.num_threads_per_spike is None: + # and it's a 1-to-1 just set it to 1 + if isinstance(self._connector, OneToOneConnector): + n_thr = 1 + # if it is another connector, just set it to the default + # and warn the user that they may be able to do better + else: + n_thr = DEFAULT_NUM_THREADS_PER_SPIKE + warnings.warn(TuneThreadsPerSpikeWarning()) + # if it was set and it's a 1-to-1 and the number of threads is not 1 + # warn the user about this and set it to 1 + elif (isinstance(self._connector, OneToOneConnector) and + self.num_threads_per_spike != 1) : + n_thr = 1 + warnings.warn(OneToOneThreadsPerSpikeWarning()) + + # finally pass the value to GeNN + synaptic_population.pop.set_num_threads_per_spike(n_thr) - def _on_device_init_native_projection(self, matrix_type, prefix, params, delay_steps): + def _on_device_init_native_projection( + self, matrix_type, prefix, params, delay_steps, use_procedural): """ Create an on-device connectivity initializer based projection, this removes the need to compute things on host so we can use less memory and faster network initialization @@ -438,6 +577,7 @@ def _on_device_init_native_projection(self, matrix_type, prefix, params, delay_s :param prefix: for the connection type (excitatory or inhibitory) :param params: connection parameters, required synapse_type.build_genn_wum :param delay_steps: delay used by connections + :param use_procedural: whether this projection can be generated on the fly :return: """ # Build GeNN postsynaptic model @@ -467,12 +607,15 @@ def _on_device_init_native_projection(self, matrix_type, prefix, params, delay_s wum_model, wum_params, wum_init, wum_pre_init, wum_post_init, psm_model, psm_params, psm_ini, conn_init) + if use_procedural: + self._setup_procedural(syn_pop) + self._sub_projections.append( - SubProjection(genn_label, self.pre, self.post, - slice(0, self.pre.size), slice(0, self.post.size), syn_pop, wum_params)) + SubProjection(genn_label, self.pre, self.post, slice(0, self.pre.size), + slice(0, self.post.size), syn_pop, wum_params)) - def _on_host_init_native_projection(self, pre_indices, post_indices, - matrix_type, prefix, params, delay_steps): + def _on_host_init_native_projection(self, pre_indices, post_indices, matrix_type, + prefix, params, delay_steps, use_procedural): """If the projection HAS to be generated on host (i.e. using a FromListConnector) then go through the standard connectivity path :param pre_indices: indices for the pre-synaptic population neurons @@ -481,6 +624,7 @@ def _on_host_init_native_projection(self, pre_indices, post_indices, :param prefix: for the connection type (excitatory or inhibitory) :param params: connection parameters, required synapse_type.build_genn_wum :param delay_steps: delay used by connections + :param use_procedural: whether this projection can be generated on the fly :return: """ num_synapses = len(pre_indices) @@ -555,6 +699,9 @@ def _on_host_init_native_projection(self, pre_indices, post_indices, if self.use_sparse: syn_pop.set_sparse_connections(conn_pre_inds, conn_post_inds) + if use_procedural: + self._setup_procedural(syn_pop) + self._sub_projections.append( SubProjection(genn_label, pre_pop, post_pop, pre_slice, post_slice, syn_pop, wum_params)) diff --git a/setup.py b/setup.py index 93431440..d6a58dab 100644 --- a/setup.py +++ b/setup.py @@ -37,4 +37,4 @@ install_requires=["pynn>=0.9, <0.9.3", "pygenn >= 0.4.1", "lazyarray>=0.3, < 0.4", "sentinel", "neo>=0.6, <0.7", "numpy>=1.10.0,!=1.16.*", "six"], zip_safe=False, # Partly for performance reasons -) \ No newline at end of file +) diff --git a/test/system/test_genn.py b/test/system/test_genn.py index 4ceb1dac..1818e36f 100644 --- a/test/system/test_genn.py +++ b/test/system/test_genn.py @@ -313,13 +313,137 @@ def conn_init_fix_post(): abs_diff = numpy.abs(n - numpy.mean(n_cols)) epsilon = 0.01 - assert abs_diff <= epsilon + assert_less_equal(abs_diff, epsilon) scale = dist_params['high'] - dist_params['low'] s, p = stats.kstest((comp_var - dist_params['low']) / scale, 'uniform') min_p = 0.05 assert_greater(p, min_p) +def conn_proc_o2o(): + import numpy as np + import pynn_genn as sim + import copy + timestep = 1. + sim.setup(timestep) + + n_neurons = 100 + params = copy.copy(sim.IF_curr_exp.default_parameters) + pre = sim.Population(n_neurons, sim.SpikeSourceArray, + {'spike_times': [[1 + i] for i in range(n_neurons)]}, + label='pre') + params['tau_syn_E'] = 5. + post = sim.Population(n_neurons, sim.IF_curr_exp, params, + label='post') + post.record('spikes') + + conn = sim.OneToOneConnector() + syn = sim.StaticSynapse(weight=5, delay=1) + proj = sim.Projection(pre, post, conn, synapse_type=syn, + use_procedural=bool(1), num_threads_per_spike=1) + + sim.run(2 * n_neurons) + data = post.get_data() + spikes = np.asarray(data.segments[0].spiketrains) + sim.end() + + all_at_appr_time = 0 + sum_spikes = 0 + for i, times in enumerate(spikes): + sum_spikes += len(times) + if int(times[0]) == (i + 9): + all_at_appr_time += 1 + + assert_equal(sum_spikes, n_neurons) + assert_equal(all_at_appr_time, n_neurons) + +def conn_proc_a2a(): + import numpy as np + import pynn_genn as sim + import copy + timestep = 1. + sim.setup(timestep) + + n_neurons = 100 + params = copy.copy(sim.IF_curr_exp.default_parameters) + pre = sim.Population(n_neurons, sim.SpikeSourceArray, + {'spike_times': [[1] for _ in range(n_neurons)]}, + label='pre') + params['tau_syn_E'] = 5. + post = sim.Population(n_neurons, sim.IF_curr_exp, params, + label='post') + post.record('spikes') + + conn = sim.AllToAllConnector() + syn = sim.StaticSynapse(weight=5. / n_neurons, delay=1) # rand_dist) + proj = sim.Projection(pre, post, conn, synapse_type=syn, + use_procedural=bool(1)) + + sim.run(2 * n_neurons) + data = post.get_data() + spikes = np.asarray(data.segments[0].spiketrains) + + sim.end() + + all_at_appr_time = 0 + sum_spikes = 0 + for i, times in enumerate(spikes): + sum_spikes += len(times) + if int(times[0]) == 9: + all_at_appr_time += 1 + + assert_equal(sum_spikes, n_neurons) + assert_equal(all_at_appr_time, n_neurons) + +def conn_proc_fix_post(): + import numpy as np + import pynn_genn as sim + import copy + from pynn_genn.random import NativeRNG, NumpyRNG, RandomDistribution + + np_rng = NumpyRNG() + rng = NativeRNG(np_rng) + + timestep = 1. + sim.setup(timestep) + + n_pre = 100 + n_post = 50000 + params = copy.copy(sim.IF_curr_exp.default_parameters) + times = [[1] for _ in range(n_pre)] + pre = sim.Population(n_pre, sim.SpikeSourceArray, + {'spike_times': times}, + label='pre') + post = sim.Population(n_post, sim.IF_curr_exp, params, + label='post') + post.record('spikes') + + n = 2 + dist_params = {'low': 4.99, 'high': 5.01} + dist = 'uniform' + rand_dist = RandomDistribution(dist, rng=rng, **dist_params) + conn = sim.FixedNumberPostConnector(n, with_replacement=True, rng=rng) + syn = sim.StaticSynapse(weight=rand_dist, delay=1) # rand_dist) + # needed to use 1 thread per spike to get correct results, + # this is because the number of connections? + proj = sim.Projection(pre, post, conn, synapse_type=syn, + use_procedural=bool(1), num_threads_per_spike=1) + + sim.run(100) + data = post.get_data() + spikes = np.asarray(data.segments[0].spiketrains) + sim.end() + + all_at_appr_time = 0 + sum_spikes = 0 + for i, times in enumerate(spikes): + sum_spikes += (1 if len(times) else 0) + if len(times) == 1 and times[0] == 9: + all_at_appr_time += 1 + + assert_less_equal(np.abs(sum_spikes - (n_pre * n)), 2) + assert_less_equal(np.abs(all_at_appr_time - (n_pre * n)), 2) + if __name__ == '__main__': test_scenarios() @@ -329,3 +453,8 @@ def conn_init_fix_post(): conn_init_fix_prob() conn_init_fix_total() conn_init_fix_post() + # todo: these tests are not super good, need to think a better way + conn_proc_o2o() + conn_proc_a2a() + conn_proc_fix_post() +