Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Procedural synapses #54

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pynn_genn/connectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
181 changes: 164 additions & 17 deletions pynn_genn/projections.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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))

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -424,20 +530,54 @@ 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
:param matrix_type: Whether we are using a dense or sparse matrix
: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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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))
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
)
Loading