diff --git a/CHANGES.rst b/CHANGES.rst index 53d1a23e3..23dc4e291 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -31,6 +31,10 @@ Release history - Added a ``timers`` attribute to ``Simulator`` that tracks the wall time taken by various parts of the model, including build time and run time. (`#260 `__) +- Added the ``pop_type`` configuration option to the ``Connection`` config. + See `nengo_loihi.add_params + `__ + for details. (`#261 `__) **Changed** @@ -43,11 +47,22 @@ Release history - Added the ``add_to_container`` argument to ``DecodeNeurons.get_ensemble``, which makes it easier to add a decode neurons ensemble to a network. (`#260 `__) +- ``Convolution`` transforms with ``channels_last=True`` now work with outputs + up to 1024 neurons. + (`#261 `__) **Fixed** - We no longer create a spike generator if we are communicating through Snips. (`#260 `__) +- Fixed an issue in which ignored axons were still having an effect in + convolutional networks where not all input pixels are used in the output. + (`#261 `__) +- Fixed an issue that prevented population spikes to be sent to the chip when + ``precompute=True``. (`#261 `__) +- Fixed a bug preventing making sparse connections to an ensemble. + (`#245 `__, + `#246 `__) 0.10.0 (November 25, 2019) ========================== diff --git a/nengo_loihi/block.py b/nengo_loihi/block.py index 5201b5c56..969effc5b 100644 --- a/nengo_loihi/block.py +++ b/nengo_loihi/block.py @@ -282,12 +282,25 @@ def configure_relu(self, tau_ref=0.0, vth=1, dt=0.001): class Axon: """A group of axons targeting a specific Synapse object. + Parameters + ---------- + n_axons : int + The number of outgoing axons. + target : Synapse + Target synapses for these axons. + compartment_map : array_like (``n_compartments``,) + Indices indicating which target axon each compartment maps to. + If < 0, the corresponding compartment will not be used with these axons. + atoms : array_like (``n_compartments``,) + Atom (weight index) associated with each compartment. + Attributes ---------- compartment_atoms : list of length ``block.n_neurons`` - Atom (weight index) associated with each block compartment. + Atom (weight index) associated with each compartment. compartment_map : list of length ``block.n_neurons`` - Index of the axon in ``target`` targeted by each block compartment. + Indices indicating which target axon each compartment maps to. + If < 0, the corresponding compartment will not be used with these axons. n_axons : int The number of outgoing axons. target : Synapse @@ -302,33 +315,38 @@ class Spike: Parameters ---------- - axon_id : int + axon_idx : int The index of the axon within the targeted Synapse object. atom : int, optional (Default: 0) An index into the target Synapse weights. This allows spikes targeting a particular axon to use different weights. """ - __slots__ = ["axon_id", "atom"] + __slots__ = ["axon_idx", "atom"] - def __init__(self, axon_id, atom=0): - self.axon_id = axon_id + def __init__(self, axon_idx, atom=0): + self.axon_idx = axon_idx self.atom = atom def __repr__(self): - return "%s(axon_id=%d, atom=%d)" % ( + return "%s(axon_idx=%d, atom=%d)" % ( type(self).__name__, - self.axon_id, + self.axon_idx, self.atom, ) - def __init__(self, n_axons, label=None): + def __init__(self, n_axons, target, compartment_map, atoms=None, label=None): self.n_axons = n_axons + self.target = target + self.compartment_map = np.asarray(compartment_map, dtype=int) + self.compartment_atoms = ( + np.zeros(self.compartment_map.size, dtype=int) + if atoms is None + else np.asarray(atoms, dtype=int) + ) self.label = label - self.target = None - self.compartment_map = None - self.compartment_atoms = None + assert self.compartment_map.ndim == self.compartment_atoms.ndim == 1 def __str__(self): return "%s(%s)" % (type(self).__name__, self.label if self.label else "") @@ -346,43 +364,15 @@ def axon_slots(self): """The total number of axon_cfg slots used by all axons.""" return self.slots_per_axon * self.n_axons - def map_axon(self, compartment_idxs): - return ( - self.compartment_map[compartment_idxs] - if self.compartment_map is not None - else compartment_idxs - ) - - def map_atoms(self, compartment_idxs): - return ( - self.compartment_atoms[compartment_idxs] - if self.compartment_atoms is not None - else [0 for _ in compartment_idxs] - ) - def map_spikes(self, compartment_idxs): - axon_ids = self.map_axon(compartment_idxs) - atoms = self.map_atoms(compartment_idxs) + axon_ids = self.compartment_map[compartment_idxs] + atoms = self.compartment_atoms[compartment_idxs] + return [ self.Spike(axon_id, atom=atom) if axon_id >= 0 else None for axon_id, atom in zip(axon_ids, atoms) ] - def set_compartment_axon_map(self, target_axons, atoms=None): - """Set mapping from compartments to axons in target. - - Parameters - ---------- - target_axons : array_like (``n_compartments``,) - Indices indicating which target axon each compartment maps to. - If < 0, the corresponding compartment will not be used with these - axons. - atoms : array_like (``n_compartments``,) - Atoms to use for each compartment. Use only if ``pop_type != 0``. - """ - self.compartment_map = target_axons - self.compartment_atoms = atoms - class SynapseConfig(Config): INDEX_BITS_MAP = d(b"WzAsIDYsIDcsIDgsIDksIDEwLCAxMSwgMTJd", "list_int") @@ -569,21 +559,31 @@ def __str__(self): return "%s(%s)" % (type(self).__name__, self.label if self.label else "") def atom_bits(self): + """Number of bits needed to represent the atom for incoming spikes.""" max_populations = max(w.shape[0] for w in self.weights) return int(np.ceil(np.log2(max_populations))) def atom_bits_extra(self): - atom_bits = self.atom_bits() - assert atom_bits <= d(b"OQ==", int), "Too many atom bits" - return max(atom_bits - d(b"NQ==", int), 0) + """Number of extra bits needed for the atom for incoming pop16 spikes.""" + if self.pop_type == 16: + atom_bits = self.atom_bits() + assert atom_bits <= d(b"OQ==", int), "Too many atom bits" + return max(atom_bits - d(b"NQ==", int), 0) + else: + return 0 # meaningless if pop_type != 16 def axon_bits(self): + """Number of bits available to represent the target axon on incoming spikes.""" if self.pop_type == 16: return d(b"MTA=", int) - self.atom_bits_extra() else: return d(b"MTI=", int) def axon_compartment_base(self, axon_idx): + """Offset for compartment indices for a particular axon. + + A return value of ``None`` indicates the axon is unused. + """ if self.axon_compartment_bases is None: return 0 base = self.axon_compartment_bases[axon_idx] @@ -592,10 +592,12 @@ def axon_compartment_base(self, axon_idx): return base if base >= 0 else None def axon_populations(self, axon_idx): + """Number of populations (atom values) for a particular axon.""" weight_idx = self.axon_weight_idx(axon_idx) return self.weights[weight_idx].shape[0] def axon_weight_idx(self, axon_idx): + """Index of weights in weight array for a particular axon.""" return ( self.axon_to_weight_map[axon_idx] if self.axon_to_weight_map is not None @@ -603,20 +605,24 @@ def axon_weight_idx(self, axon_idx): ) def axon_weights_indices(self, axon_idx, atom=0): + """The weights and indices for a particular axon (and atom, if applicable).""" weight_idx = self.axon_weight_idx(axon_idx) w = self.weights[weight_idx] i = self.indices[weight_idx] return w[atom, :], i[atom, :] def bits(self): + """The total number of bits used by all weights in this Synapse.""" return sum(self.synapse_cfg.bits_per_axon(w.size) for w in self.weights) def format(self, **kwargs): + """Modify the SynapseConfig format of this Synapse.""" if self.synapse_cfg is None: self.synapse_cfg = SynapseConfig() self.synapse_cfg.set(**kwargs) def idx_bits(self): + """The number of index bits required for each weight entry.""" bits = int(np.ceil(np.log2(self.max_ind() + 1))) assert ( bits <= SynapseConfig.INDEX_BITS_MAP[-1] @@ -625,13 +631,19 @@ def idx_bits(self): return bits def idxs_per_synapse(self): + """The number of axon indices (slots) required for each incoming axon.""" return d(b"Mg==", int) if self.learning else d(b"MQ==", int) def max_abs_weight(self): + """The maximum absolute value of all the weights in this Synapse.""" return max(np.abs(w).max() if w.size > 0 else -np.inf for w in self.weights) def max_ind(self): - return max(i.max() if len(i) > 0 else -1 for i in self.indices) + """The maximum compartment index in weight memory. + + Does not include ``axon_compartment_base``. + """ + return max(i.max() if i.size > 0 else -1 for i in self.indices) def _set_weights_indices(self, weights, indices=None): weights = [np.array(w, copy=False, dtype=np.float32, ndmin=2) for w in weights] @@ -660,6 +672,7 @@ def _set_weights_indices(self, weights, indices=None): self.indices = indices def set_weights(self, weights): + """Set dense or sparse weights on this Synapse.""" if isinstance(weights, scipy.sparse.spmatrix): csr = weights.tocsr() weights_by_row, idxs_by_row = [], [] @@ -689,6 +702,7 @@ def set_weights(self, weights): def set_learning( self, learning_rate=1.0, tracing_tau=2, tracing_mag=1.0, wgt_exp=4 ): + """Set the learning parameters for this Synapse.""" assert tracing_tau == int(tracing_tau), "tracing_tau must be integer" self.learning = True @@ -707,6 +721,7 @@ def set_learning( def set_population_weights( self, weights, indices, axon_to_weight_map, compartment_bases, pop_type=None ): + """Set population weights on this Synapse.""" self._set_weights_indices(weights, indices) self.axon_to_weight_map = axon_to_weight_map self.axon_compartment_bases = compartment_bases diff --git a/nengo_loihi/builder/connection.py b/nengo_loihi/builder/connection.py index 1f279ca46..27633fd68 100644 --- a/nengo_loihi/builder/connection.py +++ b/nengo_loihi/builder/connection.py @@ -45,6 +45,21 @@ def _inherit_seed(dest_model, dest_obj, src_model, src_obj): dest_model.seeds[dest_obj] = src_model.seeds[src_obj] +def _inherit_config(dest_model, dest_obj, src_model, src_obj): + if src_model.config is None: + return + + assert dest_model.config is not None, "Destination model must have a config" + src_params = src_model.config[src_obj] # InstanceParams object for source + filled_params = [ + attr + for attr in src_params._clsparams.params + if src_params in src_params._clsparams.get_param(attr) + ] + for attr in filled_params: + setattr(dest_model.config[dest_obj], attr, getattr(src_params, attr)) + + @Builder.register(Connection) def build_connection(model, conn): pre_onchip = model.split.on_chip(base_obj(conn.pre)) @@ -101,6 +116,7 @@ def build_host_neurons_to_chip(model, conn): add_to_container=False, ) _inherit_seed(model, receive2post, model, conn) + _inherit_config(model, receive2post, model, conn) build_chip_connection(model, receive2post) logger.debug("Creating HostSendNode for %s", conn) @@ -182,6 +198,7 @@ def build_host_to_chip(model, conn): add_to_container=False, ) _inherit_seed(model, receive2post, model, conn) + _inherit_config(model, receive2post, model, conn) build_chip_connection(model, receive2post) logger.debug("Creating DecodeNeuron ensemble for %s", conn) @@ -518,7 +535,7 @@ def build_chip_connection(model, conn): # noqa: C901 loihi_weights = weights.T mid_obj = pre_obj - mid_axon_inds = None + mid_axon_inds = np.arange(mid_obj.n_neurons) post_tau = tau_s if needs_decode_neurons and not isinstance(conn.post_obj, Neurons): # --- add decode neurons @@ -580,9 +597,9 @@ def build_chip_connection(model, conn): # noqa: C901 target_axons[pre_slice] = np.arange(target_axons[pre_slice].size) pre_slice = slice(None) - dec_ax0 = Axon(n, label="decoders") - dec_ax0.target = dec_syn - dec_ax0.set_compartment_axon_map(target_axons) + dec_ax0 = Axon( + n, target=dec_syn, compartment_map=target_axons, label="decoders" + ) pre_obj.add_axon(dec_ax0) model.objs[conn]["decode_axon"] = dec_ax0 @@ -659,9 +676,12 @@ def build_chip_connection(model, conn): # noqa: C901 target_axons[pre_slice] = np.arange(target_axons[pre_slice].size) assert target_axons[pre_slice].size == n1 - ax = Axon(mid_obj.n_neurons, label="neuron_weights") - ax.target = syn - ax.set_compartment_axon_map(target_axons) + ax = Axon( + mid_obj.n_neurons, + target=syn, + compartment_map=target_axons, + label="neuron_weights", + ) mid_obj.add_axon(ax) post_obj.compartment.configure_filter(post_tau, dt=model.dt) @@ -684,8 +704,12 @@ def build_chip_connection(model, conn): # noqa: C901 post_obj.add_synapse(syn) model.objs[conn]["weights"] = syn - ax = Axon(n1, label="decoder_weights") - ax.target = syn + ax = Axon( + n1, + target=syn, + compartment_map=np.arange(mid_obj.n_neurons), + label="decoder_weights", + ) mid_obj.add_axon(ax) post_obj.compartment.configure_filter(post_tau, dt=model.dt) @@ -700,9 +724,12 @@ def build_chip_connection(model, conn): # noqa: C901 if target_encoders not in post_obj.named_synapses: build_decode_neuron_encoders(model, conn.post_obj, kind=target_encoders) - mid_ax = Axon(mid_obj.n_neurons, label="encoders") - mid_ax.target = post_obj.named_synapses[target_encoders] - mid_ax.set_compartment_axon_map(mid_axon_inds) + mid_ax = Axon( + mid_obj.n_neurons, + target=post_obj.named_synapses[target_encoders], + compartment_map=mid_axon_inds, + label="encoders", + ) mid_obj.add_axon(mid_ax) model.objs[conn]["mid_axon"] = mid_ax @@ -751,11 +778,11 @@ def build_conv2d_connection(model, conn): assert isinstance(conn.pre_obj, (Neurons, ChipReceiveNeurons)) assert isinstance(conn.transform, nengo_transforms.Convolution) - weights = conn.transform.sample(rng=rng) + kernel = conn.transform.sample(rng=rng) input_shape = conn.transform.input_shape # Account for nengo spike height of 1/dt - weights = weights / model.dt + kernel = kernel / model.dt if isinstance(conn.pre_obj, ChipReceiveNeurons): neuron_type = conn.pre_obj.neuron_type @@ -763,7 +790,7 @@ def build_conv2d_connection(model, conn): neuron_type = conn.pre_obj.ensemble.neuron_type if neuron_type is not None and hasattr(neuron_type, "amplitude"): - weights = weights * neuron_type.amplitude + kernel = kernel * neuron_type.amplitude # --- post assert isinstance(conn.post_obj, Neurons) @@ -771,7 +798,7 @@ def build_conv2d_connection(model, conn): gain = model.params[conn.post_obj.ensemble].gain if not np.all(gain == gain[0]): - # Cannot fold gains into weights, result would not be convolutional. + # Cannot fold gains into kernel, result would not be convolutional. # Therefore, Loihi does not support this if we want to share weights. raise ValidationError( "All neurons targeted by a Convolution connection must " @@ -779,11 +806,11 @@ def build_conv2d_connection(model, conn): "gain", obj=conn.post_obj.ensemble, ) - weights = weights * gain[0] + kernel = kernel * gain[0] - pop_type = 32 # TODO: pick this + pop_type = model.config[conn].pop_type new_transform = copy.copy(conn.transform) - type(new_transform).init.data[new_transform] = weights + type(new_transform).init.data[new_transform] = kernel weights, indices, axon_to_weight_map, offsets = conv2d_loihi_weights(new_transform) synapse = Synapse(np.prod(input_shape.spatial_shape), label="conv2d_weights") @@ -798,13 +825,17 @@ def build_conv2d_connection(model, conn): atoms = np.zeros(pre_obj.n_neurons, dtype=int) atoms[conn.pre_slice] = channel_idxs(input_shape) - ax = Axon(np.prod(input_shape.spatial_shape), label="conv2d_weights") - ax.target = synapse - ax.set_compartment_axon_map(target_axons, atoms=atoms) + ax = Axon( + np.prod(input_shape.spatial_shape), + target=synapse, + compartment_map=target_axons, + atoms=atoms, + label="conv2d_weights", + ) pre_obj.add_axon(ax) post_obj.compartment.configure_filter(tau_s, dt=model.dt) model.params[conn] = BuiltConnection( - eval_points=None, solver_info=None, transform=None, weights=weights + eval_points=None, solver_info=None, transform=None, weights=kernel ) diff --git a/nengo_loihi/config.py b/nengo_loihi/config.py index 98d79760f..59c18e552 100644 --- a/nengo_loihi/config.py +++ b/nengo_loihi/config.py @@ -11,6 +11,12 @@ def add_params(network): * ``on_chip``: Whether the ensemble should be simulated on a Loihi chip. Marking specific ensembles for simulation off of a Loihi chip can help with debugging. + `nengo.Connection` + * ``pop_type``: The axon format when using population spikes, which are only + used for convolutional connections. By default, we use ``pop_type`` 32. + Setting ``pop_type`` to 16 allows more axons to fit on one chip as long as + the ``Convolution`` transform has ``channels_last=True`` and ``n_filters`` + is a multiple of 4. Examples -------- @@ -29,6 +35,10 @@ def add_params(network): if "on_chip" not in ens_cfg._extra_params: ens_cfg.set_param("on_chip", Parameter("on_chip", default=None, optional=True)) + conn_cfg = config[nengo.Connection] + if "pop_type" not in conn_cfg._extra_params: + conn_cfg.set_param("pop_type", Parameter("pop_type", default=32, optional=True)) + def set_defaults(): """Modify Nengo's default parameters for better performance with Loihi. diff --git a/nengo_loihi/conv.py b/nengo_loihi/conv.py index 3d3e7dbd3..39741a157 100644 --- a/nengo_loihi/conv.py +++ b/nengo_loihi/conv.py @@ -143,14 +143,16 @@ def pixel_idxs(shape): def conv2d_loihi_weights(transform): - # TODO: It appears from that there is an upper limit on - # CxBase of 256 (bug), so I had to make extra sets of redundant weights - # with indices to work around this. If using pop32 axons then I could - # put the filters as the major index to avoid this that way. + assert transform.padding == "valid", "Only 'valid' padding currently implemented" + assert ( + transform.channels_last == transform.input_shape.channels_last + ), "Transforms that switch the channel position not yet implemented" - inp_shape = transform.input_shape - input_rows, input_cols = inp_shape.spatial_shape + input_rows, input_cols = transform.input_shape.spatial_shape + n_channels = transform.input_shape.n_channels output_rows, output_cols = transform.output_shape.spatial_shape + n_filters = transform.n_filters + n_compartments = output_rows * output_cols * n_filters # compute number of used input pixels ri_max = (output_rows - 1) * transform.strides[0] + 1 @@ -158,6 +160,7 @@ def conv2d_loihi_weights(transform): weights = [] indices = [] + # compartment offset (aka. compartment base) for each axon offsets = np.zeros(input_rows * input_cols, dtype=int) axon_to_weight_map = np.zeros(input_rows * input_cols, dtype=int) weights_map = {} @@ -180,7 +183,20 @@ def conv2d_loihi_weights(transform): offsets[ij] = -1 continue - weight_key = (tuple(wmask_i), tuple(wmask_j)) + assert ri[wmask_i][0] % transform.strides[0] == 0, "true if mode == 'valid'" + yi0 = ri[wmask_i][0] // transform.strides[0] + yj0 = rj[wmask_j][0] // transform.strides[1] + yij0 = yi0 * output_cols + yj0 + offset = yij0 * n_filters if transform.channels_last else yij0 + + # There is currently an upper limit on the axon compartment offset of 256. + # To work around this, we split the offset into two parts, and make extra sets + # of redundant weights with part of the offset in the indices, as needed. + axon_offset = offset % 256 + index_offset = offset - axon_offset + offsets[ij] = axon_offset + + weight_key = (tuple(wmask_i), tuple(wmask_j), index_offset) if weight_key not in weights_map: # tranpose kernel to (in_channels, rows, cols, out_channels) kernel = np.transpose(transform.init, (2, 0, 1, 3)) @@ -189,60 +205,41 @@ def conv2d_loihi_weights(transform): kernel = kernel[:, ::-1, ::-1, :] w = kernel[:, wmask_i[:, None] * wmask_j, :] - assert w.size == ( - inp_shape.n_channels - * wmask_i.sum() - * wmask_j.sum() - * transform.n_filters - ) - assert w.shape == ( - inp_shape.n_channels, - wmask_i.sum() * wmask_j.sum(), - transform.n_filters, - ) - - if transform.channels_last: - w = w.reshape(inp_shape.n_channels, -1) - inds = ( - np.zeros((inp_shape.n_channels, 1, 1, 1), dtype=int) - + ( - output_cols - * transform.n_filters - * np.arange(wmask_i.sum())[:, None, None] - ) - + transform.n_filters * np.arange(wmask_j.sum())[:, None] - + np.arange(transform.n_filters) - ).reshape(inp_shape.n_channels, -1) - else: - w = np.transpose(w, (0, 2, 1)).reshape(inp_shape.n_channels, -1) - inds = ( - np.zeros((inp_shape.n_channels, 1, 1, 1), dtype=int) - + ( - output_rows - * output_cols - * np.arange(transform.n_filters)[:, None, None] - ) - + output_cols * np.arange(wmask_i.sum())[:, None] - + np.arange(wmask_j.sum()) - ).reshape(inp_shape.n_channels, -1) + assert w.shape == (n_channels, wmask_i.sum() * wmask_j.sum(), n_filters) + + # --- determine indices + # channel inds are zero, since we use same indices for each channel + channel_inds = np.zeros(n_channels, dtype=int) + row_inds = np.arange(wmask_i.sum()) + col_inds = np.arange(wmask_j.sum()) + filter_inds = np.arange(n_filters) + + order = [channel_inds, row_inds, col_inds, filter_inds] + shape = [n_channels, output_rows, output_cols, n_filters] + if not transform.channels_last: + # move filters (aka. output channels) before rows/cols + w = np.transpose(w, (0, 2, 1)) + order = [order[i] for i in (0, 3, 1, 2)] + shape = [shape[i] for i in (0, 3, 1, 2)] + + n = len(shape) + strides = [np.prod(shape[i + 1 :]) for i in range(n)] + + # inds[i_0,...,i_{n-1}] = sum_{k=0}^{n-1} strides[k] * order[k][i_k] + strided_inds = [ + stride * ind.reshape([-1] + [1] * (n - 1 - k)) + for k, (ind, stride) in enumerate(zip(order, strides)) + ] + inds = sum([index_offset] + strided_inds) weights_map[weight_key] = len(weights) - weights.append(w) - indices.append(inds) + weights.append(w.reshape(n_channels, -1)) + indices.append(inds.reshape(n_channels, -1)) axon_to_weight_map[ij] = weights_map[weight_key] - assert ri[wmask_i][0] % transform.strides[0] == 0, "true if mode == 'valid'" - yi0 = ri[wmask_i][0] // transform.strides[0] - yj0 = rj[wmask_j][0] // transform.strides[1] - if transform.channels_last: - offsets[ij] = (yi0 * output_cols + yj0) * transform.n_filters - else: - offsets[ij] = yi0 * output_cols + yj0 - + # check that offset (compartment base) plus index points to a valid compartment inds = indices[axon_to_weight_map[ij]] - assert ( - offsets[ij] + inds < output_rows * output_cols * transform.n_filters - ).all() + assert (offsets[ij] + inds < n_compartments).all() return weights, indices, axon_to_weight_map, offsets diff --git a/nengo_loihi/emulator/interface.py b/nengo_loihi/emulator/interface.py index 8091f100d..95254effc 100644 --- a/nengo_loihi/emulator/interface.py +++ b/nengo_loihi/emulator/interface.py @@ -491,9 +491,7 @@ def inject_current(self, t, spike_inputs, all_axons, spiked): compartment_idxs = spike_input.spike_idxs(t - 1) for axon in spike_input.axons: spikes = axon.map_spikes(compartment_idxs) - self.spikes_in[axon.target].extend( - s for s in spikes if s is not None - ) + self.spikes_in[axon.target].extend(spikes) # --- axons pass spikes to synapses for axon, a_idx in all_axons.items(): @@ -506,12 +504,12 @@ def update_input(self, input): qb = input[:, s_slice] for spike in self.spikes_in[synapse]: - base = synapse.axon_compartment_base(spike.axon_id) + base = synapse.axon_compartment_base(spike.axon_idx) if base is None: continue weights, indices = synapse.axon_weights_indices( - spike.axon_id, atom=spike.atom + spike.axon_idx, atom=spike.atom ) qb[0, base + indices] += weights @@ -539,9 +537,9 @@ def update_traces(self, t, rng): trace_spikes = self.trace_spikes.get(synapse, None) if trace_spikes is not None: for spike in self.spikes_in[synapse]: - if spike.axon_id in trace_spikes: + if spike.axon_idx in trace_spikes: self.error("Synaptic trace spikes lost") - trace_spikes.add(spike.axon_id) + trace_spikes.add(spike.axon_idx) trace = self.traces.get(synapse, None) if trace is not None and t % synapse.train_epoch == 0: diff --git a/nengo_loihi/emulator/tests/test_interface.py b/nengo_loihi/emulator/tests/test_interface.py index 7369c69b7..c19583719 100644 --- a/nengo_loihi/emulator/tests/test_interface.py +++ b/nengo_loihi/emulator/tests/test_interface.py @@ -60,8 +60,7 @@ def test_uv_overflow(n_axons, plt, allclose, monkeypatch): synapse.set_weights(np.ones((n_axons, 1))) block.add_synapse(synapse) - axon = Axon(n_axons) - axon.target = synapse + axon = Axon(n_axons, target=synapse, compartment_map=np.arange(1)) input.add_axon(axon) probe_u = Probe(target=block, key="current") diff --git a/nengo_loihi/hardware/builder.py b/nengo_loihi/hardware/builder.py index 3b1431bb6..bcd5cdfc1 100644 --- a/nengo_loihi/hardware/builder.py +++ b/nengo_loihi/hardware/builder.py @@ -379,26 +379,15 @@ def build_input(nxsdk_core, core, spike_input, compartment_idxs): nxsdk_board.spike_inputs[spike_input] = loihi_input # add any pre-existing spikes to spikegen + nxsdk_spike_generator = nxsdk_board.global_spike_generator for t in spike_input.spike_times(): assert ( - nxsdk_board.global_spike_generator is not None + nxsdk_spike_generator is not None ), "Cannot add pre-existing spikes when using Snips (no spike generator)" spikes = spike_input.spike_idxs(t) - for spike in loihi_input.spikes_to_loihi(spikes): - assert ( - spike["atom"] == 0 - ), "Cannot send population spikes through spike generator" - d_func( - nxsdk_board.global_spike_generator, - b"YWRkU3Bpa2U=", - kwargs={ - b"dGltZQ==": t, - b"Y2hpcElk": spike["chip_id"], - b"Y29yZUlk": spike["core_id"], - b"YXhvbklk": spike["axon_id"], - }, - ) + loihi_spikes = loihi_input.spikes_to_loihi(spikes) + loihi_input.add_spikes_to_generator(t, loihi_spikes, nxsdk_spike_generator) def build_synapse(nxsdk_core, core, block, synapse, compartment_idxs): # noqa C901 @@ -415,9 +404,9 @@ def build_synapse(nxsdk_core, core, block, synapse, compartment_idxs): # noqa C synapse_map = {} # map weight_idx to (ptr, pop_size, len) total_synapse_ptr = int(core.synapse_entries[synapse][0]) for axon_idx, axon_id in enumerate(axon_ids): - assert axon_id <= 2 ** axon_bits + assert axon_id is None or axon_id <= 2 ** axon_bits - weight_idx = int(synapse.axon_weight_idx(axon_idx)) + weight_idx = synapse.axon_weight_idx(axon_idx) base = synapse.axon_compartment_base(axon_idx) if weight_idx not in synapse_map: @@ -451,14 +440,12 @@ def build_synapse(nxsdk_core, core, block, synapse, compartment_idxs): # noqa C synapse_ptr, n_atoms, n_compartments = synapse_map[weight_idx] assert n_atoms <= 2 ** atom_bits - if base is None: - # this is a dummy axon with no weights, so set n_compartments to 0 - synapse_ptr = 0 - n_compartments = 0 - base = 0 - else: - base = int(base) + if axon_id is None: + # This is a dummy axon with no base or no weights, so skip it + assert base is None or n_compartments == 0 + continue + base = int(base) assert base <= d(b"MjU2", int), "Currently limited by hardware" d_set( d_get(nxsdk_core, b"c3luYXBzZU1hcA==")[axon_id], @@ -539,7 +526,7 @@ def build_synapse(nxsdk_core, core, block, synapse, compartment_idxs): # noqa C def build_axons(nxsdk_core, core, block, axon, compartment_ids, pop_id_map): synapse = axon.target - tchip_idx, tcore_idx, tsyn_idxs = core.board.find_synapse(synapse) + tchip_idx, tcore_idx, taxon_ids = core.board.find_synapse(synapse) nxsdk_board = d_get(nxsdk_core, b"cGFyZW50", b"cGFyZW50") tchip_id = d_get(d_get(nxsdk_board, b"bjJDaGlwcw==")[tchip_idx], b"aWQ=") tcore_id = d_get( @@ -553,14 +540,14 @@ def build_axons(nxsdk_core, core, block, axon, compartment_ids, pop_id_map): spikes = axon.map_spikes(compartment_idxs) for compartment_id, spike in zip(compartment_ids, spikes): - if spike is None: - continue # this compartment does not route through these axons - - taxon_idx = int(spike.axon_id) - taxon_id = int(tsyn_idxs[taxon_idx]) + taxon_idx = spike.axon_idx + taxon_id = taxon_ids[taxon_idx] atom = int(spike.atom) n_atoms = synapse.axon_populations(taxon_idx) + if taxon_id is None: + continue # this connects to a dummy axon, so do not build + if synapse.pop_type == 0: # discrete assert atom == 0 assert n_atoms == 1 diff --git a/nengo_loihi/hardware/interface.py b/nengo_loihi/hardware/interface.py index e3da8c15c..2fb58ef5d 100644 --- a/nengo_loihi/hardware/interface.py +++ b/nengo_loihi/hardware/interface.py @@ -19,6 +19,7 @@ from nengo_loihi.hardware.allocators import OneToOne, RoundRobin from nengo_loihi.hardware.builder import build_board from nengo_loihi.nxsdk_obfuscation import d, d_func, d_get +from nengo_loihi.hardware.nxsdk_objects import LoihiSpikeInput from nengo_loihi.hardware.nxsdk_shim import assert_nxsdk, nxsdk, SnipPhase, SpikeProbe from nengo_loihi.hardware.validate import validate_board from nengo_loihi.validate import validate_model @@ -329,24 +330,12 @@ def chip2host(self, probes_receivers): self.sent_steps += increment def host2chip(self, loihi_spikes): + nxsdk_spike_generator = self.spike_generator tmax = -1 for t, spikes in loihi_spikes.items(): assert t >= tmax, "Spikes must be in order" tmax = t - - for spike in spikes: - assert spike["axon_type"] == 0, "Spikegen cannot send pop spikes" - assert spike["atom"] == 0, "Spikegen does not support atom" - d_func( - self.spike_generator, - b"YWRkU3Bpa2U=", - kwargs={ - b"dGltZQ==": t, - b"Y2hpcElk": spike["chip_id"], - b"Y29yZUlk": spike["core_id"], - b"YXhvbklk": spike["axon_id"], - }, - ) + LoihiSpikeInput.add_spikes_to_generator(t, spikes, nxsdk_spike_generator) class Snips: @@ -368,10 +357,9 @@ class Snips: write=d(b"d3JpdGVDaGFubmVs"), spike_shift=d(b"MTY="), spike_mask=d(b"MHgwMDAwRkZGRg=="), - axon_type_0=d(b"MA=="), do_axon_type_0=d(b"bnhfc2VuZF9kaXNjcmV0ZV9zcGlrZQ=="), - axon_type_1=d(b"MzI="), - do_axon_type_1=d(b"bnhfc2VuZF9wb3AzMl9zcGlrZQ=="), + do_axon_type_16=d(b"bnhfc2VuZF9wb3AxNl9zcGlrZQ=="), + do_axon_type_32=d(b"bnhfc2VuZF9wb3AzMl9zcGlrZQ=="), data=d(b"dXNlckRhdGE="), state=d(b"Y3hfc3RhdGU="), neuron=d(b"TkVVUk9OX1BUUg=="), @@ -749,9 +737,11 @@ def pack(cls, spikes): assert np.all(spikes["axon_type"] <= 32) assert np.all(spikes["atom"] < 1024) + axon_type = spikes["axon_type"] + axon_type[axon_type == 16] += spikes["atom_bits_extra"][axon_type == 16] return np.array( [ np.left_shift(spikes["core_id"], 16) + spikes["axon_id"], - np.left_shift(spikes["axon_type"], 16) + spikes["atom"], + np.left_shift(axon_type, 16) + spikes["atom"], ] ).T.ravel() diff --git a/nengo_loihi/hardware/nxsdk_objects.py b/nengo_loihi/hardware/nxsdk_objects.py index ffffdef3d..6d8c9bb01 100644 --- a/nengo_loihi/hardware/nxsdk_objects.py +++ b/nengo_loihi/hardware/nxsdk_objects.py @@ -103,7 +103,11 @@ def __init__(self, chip): self.stdp_pre_cfgs = [] self.synapse_cfg_idxs = {} # one synfmt per Synapse, for now + + # for each Synapse, provides a map from axon index to axon id self.synapse_axons = collections.OrderedDict() + + # for each Synapse, provides the indices occupied in the synapse weight table self.synapse_entries = collections.OrderedDict() self.learning_coreid = None @@ -169,15 +173,30 @@ def add_synapse(self, synapse): synapse_cfg_idx = self.get_synapse_cfg_idx(synapse.synapse_cfg) self.synapse_cfg_idxs[synapse] = synapse_cfg_idx - a0 = 0 + # determine starting ID for this synapse's axons + id0 = 0 if len(self.synapse_axons) > 0: last = next(reversed(self.synapse_axons)) - a0 = self.synapse_axons[last][-1] + 1 - idxs_per_synapse = synapse.idxs_per_synapse() - idxs = [a0 + idxs_per_synapse * i for i in range(synapse.n_axons)] - self.synapse_axons[synapse] = idxs - self.board.index_synapse(synapse, self.chip, self, idxs) + id0 = self.synapse_axons[last][-1] + 1 + # determine the ID for each synapse axon index + idxs_per_synapse = synapse.idxs_per_synapse() + i = id0 + ids = [] + for idx in range(synapse.n_axons): + base = synapse.axon_compartment_base(idx) + w, _ = synapse.axon_weights_indices(idx) + if base is None or w.size == 0: + # dummy axon, which we will not build + ids.append(None) + else: + ids.append(i) + i += idxs_per_synapse + + self.synapse_axons[synapse] = ids + self.board.index_synapse(synapse, self.chip, self, ids) + + # determine the indices in the synapse weight table that this synapse occupies s0 = 0 if len(self.synapse_entries) > 0: last = next(reversed(self.synapse_entries)) @@ -226,6 +245,8 @@ class LoihiSpikeInput: atom : np.int32 The population index (atom), used if this axon sends population spikes (i.e. axon_type != 0). + atom_bits_extra : np.int32 + The number of extra bits used for the atom (pop16 axons only). """ spike_dtype = np.dtype( @@ -236,9 +257,41 @@ class LoihiSpikeInput: ("core_id", np.int32), ("axon_id", np.int32), ("atom", np.int32), + ("atom_bits_extra", np.int32), ] ) + @classmethod + def add_spikes_to_generator(cls, t, spikes, basic_spike_generator): + methods = { + 0: getattr(basic_spike_generator, d(b"YWRkU3Bpa2U=")), + 16: getattr(basic_spike_generator, d(b"YWRkUG9wMTZTcGlrZQ==")), + 32: getattr(basic_spike_generator, d(b"YWRkUG9wMzJTcGlrZQ==")), + } + time = d(b"dGltZQ==") + chip_id = d(b"Y2hpcElk") + core_id = d(b"Y29yZUlk") + axon_id = d(b"YXhvbklk") + atom = d(b"c3JjQXRvbQ==") + atom_bits_extra = d(b"YXRvbUJpdHM=") + + for spike in spikes: + axon_type = int(spike["axon_type"]) + kwargs = { + time: t, + chip_id: spike["chip_id"], + core_id: spike["core_id"], + axon_id: spike["axon_id"], + } + if axon_type == 0: + assert spike["atom"] == 0, "Atom must be zero for discrete spikes" + else: + kwargs[atom] = spike["atom"] + if axon_type == 16: + kwargs[atom_bits_extra] = spike["atom_bits_extra"] + + methods[axon_type](**kwargs) + def __init__(self): self.axon_map = {} # maps spike_input idx to axon in self.axons @@ -258,22 +311,32 @@ def set_axons(self, board, nxsdk_board, spike_input): assert len(self.axon_map) == 0 input_idxs = np.arange(spike_input.n_neurons) for axon in spike_input.axons: - axon_type = axon.pop_type - assert axon_type in (0, 32), "Only discrete and pop32 supported" - tchip_idx, tcore_idx, tsyn_ids = board.find_synapse(axon.target) + synapse = axon.target + atom_bits_extra = synapse.atom_bits_extra() + tchip_idx, tcore_idx, taxon_ids = board.find_synapse(synapse) tchip = d_get(nxsdk_board, b"bjJDaGlwcw==")[tchip_idx] tcore = d_get(tchip, b"bjJDb3Jlcw==")[tcore_idx] spikes = axon.map_spikes(input_idxs) for input_idx, spike in zip(input_idxs, spikes): - if spike is not None: - taxon_idx = int(spike.axon_id) - taxon_id = int(tsyn_ids[taxon_idx]) - self.axon_map.setdefault(input_idx, []).append( - np.array( - (-1, axon_type, tchip.id, tcore.id, taxon_id, spike.atom), - dtype=self.spike_dtype, - ) + self.axon_map.setdefault(input_idx, []) + taxon_id = taxon_ids[spike.axon_idx] + if taxon_id is None: + continue # this goes to a dummy axon, so do not connect + + self.axon_map[input_idx].append( + np.array( + ( + -1, + axon.pop_type, + tchip.id, + tcore.id, + taxon_id, + spike.atom, + atom_bits_extra, + ), + dtype=self.spike_dtype, ) + ) def spikes_to_loihi(self, input_idxs): """Map spike input indices to axons targeting chip locations. diff --git a/nengo_loihi/hardware/snips/nengo_io.c.template b/nengo_loihi/hardware/snips/nengo_io.c.template index 7c95995d8..8f9028c1c 100644 --- a/nengo_loihi/hardware/snips/nengo_io.c.template +++ b/nengo_loihi/hardware/snips/nengo_io.c.template @@ -33,16 +33,18 @@ void nengo_io(runState *s) { {% for core in cores %} {{ obfs.core_class }} *core{{ core }} = NEURON_PTR((CoreId){.id = {{ core }}}); {% endfor %} - {{ obfs.id_class }} core_id; + int in_channel = {{ obfs.get_channel }}("nengo_io_h2c"); int out_channel = {{ obfs.get_channel }}("nengo_io_c2h"); - {{ obfs.int_type }} axon_type; - {{ obfs.int_type }} axon_id; - {{ obfs.int_type }} atom; {{ obfs.int_type }} n_spikes; // input spike count {{ obfs.int_type }} i_spike; // input spike position {{ obfs.int_type }} *spike; + {{ obfs.id_class }} core_id; + {{ obfs.int_type }} axon_type; + {{ obfs.int_type }} axon_id; + {{ obfs.int_type }} atom; + {{ obfs.int_type }} atom_bits; {{ obfs.int_type }} error_index; // index into error stored in shared data {{ obfs.int_type }} i_error = 0; // index of error block @@ -115,10 +117,13 @@ void nengo_io(runState *s) { printf("send spike core=%d, axon=%d, type=%d atom=%d\n", core_id.id, axon_id, axon_type, atom); #endif - if (axon_type == {{ obfs.axon_type_0 }}) { + if (axon_type == 0) { {{ obfs.do_axon_type_0 }}(s->{{ obfs.step }}, core_id, axon_id); - } else if (axon_type == {{ obfs.axon_type_1 }}) { - {{ obfs.do_axon_type_1 }}(s->{{ obfs.step }}, core_id, axon_id, atom, 0, 0, 0); + } else if (axon_type == 32) { + {{ obfs.do_axon_type_32 }}(s->{{ obfs.step }}, core_id, axon_id, atom, 0, 0, 0); + } else if (axon_type >= 16) { + atom_bits = axon_type - 16; + {{ obfs.do_axon_type_16 }}(s->{{ obfs.step }}, core_id, axon_id, atom, atom_bits); } else { printf("Got invalid axon_type: %d\n", axon_type); return; diff --git a/nengo_loihi/hardware/tests/test_allocators.py b/nengo_loihi/hardware/tests/test_allocators.py index 97c186feb..126e15d6e 100644 --- a/nengo_loihi/hardware/tests/test_allocators.py +++ b/nengo_loihi/hardware/tests/test_allocators.py @@ -71,24 +71,22 @@ def _basic_model(): block1.compartment.configure_lif() model.add_block(block1) - axon1 = Axon(1) - block0.add_axon(axon1) - synapse1 = Synapse(1) synapse1.set_weights([[1]]) - axon1.target = synapse1 block1.add_synapse(synapse1) - axon0 = Axon(1) - input = LoihiInput() - input.add_axon(axon0) - model.add_input(input) + axon1 = Axon(1, target=synapse1, compartment_map=np.arange(1)) + block0.add_axon(axon1) synapse0 = Synapse(1) synapse0.set_weights([[1]]) - axon0.target = synapse0 block0.add_synapse(synapse0) + axon0 = Axon(1, target=synapse0, compartment_map=np.arange(1)) + input = LoihiInput() + input.add_axon(axon0) + model.add_input(input) + discretize_model(model) return model diff --git a/nengo_loihi/hardware/tests/test_interface.py b/nengo_loihi/hardware/tests/test_interface.py index 3040a930c..d5ba529d6 100644 --- a/nengo_loihi/hardware/tests/test_interface.py +++ b/nengo_loihi/hardware/tests/test_interface.py @@ -78,15 +78,14 @@ def test_builder_poptype_errors(): block1.compartment.configure_lif() model.add_block(block1) - axon = Axon(1) - block0.add_axon(axon) - synapse = Synapse(1) synapse.set_weights([[1]]) synapse.pop_type = 8 - axon.target = synapse block1.add_synapse(synapse) + axon = Axon(1, target=synapse, compartment_map=[0]) + block0.add_axon(axon) + discretize_model(model) board = allocator(model) diff --git a/nengo_loihi/splitter.py b/nengo_loihi/splitter.py index a3e3c649a..482489f69 100644 --- a/nengo_loihi/splitter.py +++ b/nengo_loihi/splitter.py @@ -4,7 +4,6 @@ from nengo.exceptions import BuildError from nengo.connection import LearningRule -from nengo_loihi.compat import nengo_transforms from nengo_loihi.config import add_params from nengo_loihi.passthrough import base_obj, PassthroughSplit @@ -46,19 +45,7 @@ def __init__(self, network, hostchip, passthrough, strict): # Also see issue #214. has_learning = any(conn.learning_rule is not None for conn in self._conns) - # host->chip convolutional connections are also not supported with - # precompute=True because the BasicSpikeGenerator cannot send population - # spikes correctly, meaning we have to use Snips. - has_convolution = False - if nengo_transforms is not None: - has_convolution = any( - isinstance(conn.transform, nengo_transforms.Convolution) - and not self.hostchip.on_chip(base_obj(conn.pre)) - and self.hostchip.on_chip(base_obj(conn.post)) - for conn in self._conns - ) - - if not has_learning and not has_convolution: + if not has_learning: self._find_precomputable_objs() else: self._precomputable = False @@ -66,10 +53,6 @@ def __init__(self, network, hostchip, passthrough, strict): raise BuildError( "precompute=True not supported when using learning rules" ) - elif strict and has_convolution: - raise BuildError( - "precompute=True not supported when using convolutional connections" - ) if strict and not self._precomputable: raise BuildError("Cannot precompute input, as it is dependent on output") diff --git a/nengo_loihi/tests/test_block.py b/nengo_loihi/tests/test_block.py index 6b8fd6f6d..ead351206 100644 --- a/nengo_loihi/tests/test_block.py +++ b/nengo_loihi/tests/test_block.py @@ -40,11 +40,11 @@ def test_strings(): synapse = Synapse(2, label="mySynapse") assert str(synapse) == "Synapse(mySynapse)" - axon = Axon(2, label="myAxon") + axon = Axon(2, target=None, compartment_map=[], label="myAxon") assert str(axon) == "Axon(myAxon)" - spike = Axon.Spike(axon_id=7, atom=2) - assert str(spike) == "Spike(axon_id=7, atom=2)" + spike = Axon.Spike(axon_idx=7, atom=2) + assert str(spike) == "Spike(axon_idx=7, atom=2)" # TODO: Only targeting sim due to bug with negative cx_base on Loihi @@ -58,9 +58,6 @@ def test_negative_base(request, seed): input.add_spikes(1, list(range(n_axons))) model.add_input(input) - axon = Axon(n_axons) - input.add_axon(axon) - block = LoihiBlock(3) block.compartment.configure_relu() model.add_block(block) @@ -73,9 +70,11 @@ def test_negative_base(request, seed): synapse.set_population_weights( weights, indices, axon_to_weight_map, bases, pop_type=32 ) - axon.target = synapse block.add_synapse(synapse) + axon = Axon(n_axons, target=synapse, compartment_map=np.arange(3)) + input.add_axon(axon) + probe = Probe(target=block, key="voltage") block.add_probe(probe) diff --git a/nengo_loihi/tests/test_connection.py b/nengo_loihi/tests/test_connection.py index 0b9d9e08e..06ec4ab97 100644 --- a/nengo_loihi/tests/test_connection.py +++ b/nengo_loihi/tests/test_connection.py @@ -752,3 +752,38 @@ def test_input_synapses(Simulator, allclose, plt): # Only looking at t < 0.4 as there are weird effects at the end assert allclose(ref_filt[t < 0.4], sim_filt[t < 0.4], atol=1.5) + + +@pytest.mark.skipif(nengo_transforms is None, reason="Requires new nengo.transforms") +def test_sparse_transforms_empty_neurons(Simulator): + """Test that sparse transforms work properly, even if some neurons get no input""" + n_neurons = 3 + transform = nengo_transforms.Sparse( + shape=(n_neurons, n_neurons), indices=[(0, 0), (2, 2)], init=[1, 2] + ) + + with nengo.Network() as model: + x = nengo.Ensemble( + n_neurons, + 1, + max_rates=nengo.dists.Choice([200]), + intercepts=nengo.dists.Choice([-1]), + ) + y = nengo.Ensemble( + n_neurons, + 1, + max_rates=nengo.dists.Choice([100]), + intercepts=nengo.dists.Choice([0]), + ) + nengo.Connection(x.neurons, y.neurons, transform=transform) + + probe = nengo.Probe(y.neurons) + + with Simulator(model) as sim: + # Ensure the model builds and runs correctly as this used to raise a ValueError + assert sim + sim.run(0.1) + + # only the first and third neurons should get input, not the second + spikes = (sim.data[probe] > 0).sum(axis=0) + assert np.array_equal(spikes > 0, [1, 0, 1]) diff --git a/nengo_loihi/tests/test_conv.py b/nengo_loihi/tests/test_conv.py index 8de0d49f7..126c9799a 100644 --- a/nengo_loihi/tests/test_conv.py +++ b/nengo_loihi/tests/test_conv.py @@ -2,7 +2,7 @@ import pickle import nengo -from nengo.dists import Uniform +from nengo.dists import Choice, Uniform from nengo.exceptions import ValidationError from nengo_extras.matplotlib import tile, imshow from nengo_extras.vision import Gabor @@ -13,7 +13,7 @@ import nengo_loihi from nengo_loihi.block import Axon, LoihiBlock, Probe, Synapse from nengo_loihi.builder import Model -from nengo_loihi.compat import HAS_DL, nengo_dl, nengo_transforms +from nengo_loihi.compat import nengo_transforms from nengo_loihi import conv from nengo_loihi.discretize import discretize_model from nengo_loihi.emulator import EmulatorInterface @@ -97,19 +97,12 @@ def test_pop_tiny(pop_type, channels_last, nc, request, plt, seed, allclose): inp.compartment.configure_relu() inp.compartment.bias[:] = inp_biases.ravel() - inp_ax = Axon(nij, label="inp_ax") - # we always compute the pixel/channel idxs with channels_last=True # (not sure why?), and then set it to the correct value afterwards inp_shape = nengo_transforms.ChannelShape((ni, nj, nk), channels_last=True) - inp_ax.set_compartment_axon_map( - target_axons=conv.pixel_idxs(inp_shape), atoms=conv.channel_idxs(inp_shape) - ) inp_shape.shape = (ni, nj, nk) if channels_last else (nk, ni, nj) inp_shape.channels_last = channels_last - inp.add_axon(inp_ax) - model.add_block(inp) # conv block @@ -139,7 +132,18 @@ def test_pop_tiny(pop_type, channels_last, nc, request, plt, seed, allclose): out_probe = Probe(target=neurons, key="spiked") neurons.add_probe(out_probe) - inp_ax.target = synapse + # input axon + inp_shape_for_axon = nengo_transforms.ChannelShape((ni, nj, nk), channels_last=True) + inp_ax = Axon( + nij, + target=synapse, + compartment_map=conv.pixel_idxs(inp_shape_for_axon), + atoms=conv.channel_idxs(inp_shape_for_axon), + label="inp_ax", + ) + + inp.add_axon(inp_ax) + model.add_block(neurons) # simulation @@ -280,12 +284,6 @@ def conv_pm(x, kernel): inp.compartment.configure_relu() inp.compartment.bias[:] = inp_biases.ravel() - inp_ax = Axon(np.prod(inp_shape.spatial_shape), label="inp_ax") - inp_ax.set_compartment_axon_map( - target_axons=conv.pixel_idxs(inp_shape), atoms=conv.channel_idxs(inp_shape) - ) - inp.add_axon(inp_ax) - model.add_block(inp) # conv block @@ -308,7 +306,16 @@ def conv_pm(x, kernel): out_probe = Probe(target=neurons, key="spiked") neurons.add_probe(out_probe) - inp_ax.target = synapse + # input axon + inp_ax = Axon( + np.prod(inp_shape.spatial_shape), + target=synapse, + compartment_map=conv.pixel_idxs(inp_shape), + atoms=conv.channel_idxs(inp_shape), + label="inp_ax", + ) + inp.add_axon(inp_ax) + model.add_block(neurons) # simulation @@ -358,10 +365,6 @@ def conv_pm(x, kernel): @pytest.mark.parametrize("channels", [1, 2]) @pytest.mark.parametrize("channels_last", (True, False)) def test_conv_connection(channels, channels_last, Simulator, seed, rng, plt, allclose): - if channels_last: - plt.saveas = None - pytest.xfail("Blocked by CxBase cannot be > 256 bug") - # load data with open(os.path.join(test_dir, "mnist10.pkl"), "rb") as f: test10 = pickle.load(f) @@ -443,21 +446,13 @@ def test_conv_connection(channels, channels_last, Simulator, seed, rng, plt, all bp = nengo.Probe(b.neurons) - with nengo.Simulator(model, optimize=False) as sim: - sim.run(pres_time) - ref_out = sim.data[bp].mean(axis=0).reshape(output_shape.shape) - - # Currently, non-gpu TensorFlow does not support channels first in conv - use_nengo_dl = HAS_DL and channels_last - ndl_out = np.zeros_like(ref_out) - if use_nengo_dl: - with nengo_dl.Simulator(model) as sim_dl: - sim_dl.run(pres_time) - ndl_out = sim_dl.data[bp].mean(axis=0).reshape(output_shape.shape) + with nengo.Simulator(model, optimize=False) as sim_nengo: + sim_nengo.run(pres_time) + ref_out = sim_nengo.data[bp].mean(axis=0).reshape(output_shape.shape) - with nengo_loihi.Simulator(model, target="simreal") as sim_real: - sim_real.run(pres_time) - real_out = sim_real.data[bp].mean(axis=0).reshape(output_shape.shape) + with Simulator(model, target="simreal") as sim_emu: + sim_emu.run(pres_time) + emu_out = sim_emu.data[bp].mean(axis=0).reshape(output_shape.shape) hw_opts = dict(snip_max_spikes_per_step=800) with Simulator(model, hardware_options=hw_opts) as sim_loihi: @@ -466,11 +461,10 @@ def test_conv_connection(channels, channels_last, Simulator, seed, rng, plt, all if not output_shape.channels_last: ref_out = np.transpose(ref_out, (1, 2, 0)) - ndl_out = np.transpose(ndl_out, (1, 2, 0)) - real_out = np.transpose(real_out, (1, 2, 0)) + emu_out = np.transpose(emu_out, (1, 2, 0)) sim_out = np.transpose(sim_out, (1, 2, 0)) - out_max = max(ref_out.max(), sim_out.max()) + out_max = max(ref_out.max(), emu_out.max(), sim_out.max()) # --- plot results rows = 2 @@ -490,14 +484,12 @@ def test_conv_connection(channels, channels_last, Simulator, seed, rng, plt, all tile(np.transpose(ref_out, (2, 0, 1)), vmin=0, vmax=out_max, cols=8, ax=ax) ax = plt.subplot(rows, cols, 5) - tile(np.transpose(ndl_out, (2, 0, 1)), vmin=0, vmax=out_max, cols=8, ax=ax) + tile(np.transpose(emu_out, (2, 0, 1)), vmin=0, vmax=out_max, cols=8, ax=ax) ax = plt.subplot(rows, cols, 6) tile(np.transpose(sim_out, (2, 0, 1)), vmin=0, vmax=out_max, cols=8, ax=ax) - if use_nengo_dl: - assert allclose(ndl_out, ref_out, atol=1e-5, rtol=1e-5) - assert allclose(real_out, ref_out, atol=1, rtol=1e-3) + assert allclose(emu_out, ref_out, atol=1, rtol=1e-3) assert allclose(sim_out, ref_out, atol=10, rtol=1e-3) @@ -575,6 +567,158 @@ def test_conv_input(channels_last, Simulator, plt, allclose): assert allclose(p0, p1, rtol=0.15, atol=1) +@pytest.mark.skipif(nengo_transforms is None, reason="Requires new nengo.transforms") +@pytest.mark.parametrize("pop_type", [32, 16]) +@pytest.mark.parametrize("precompute", [False, True]) +def test_conv_deepnet(pop_type, precompute, Simulator, rng, seed, plt, allclose): + def conv_layer( + x, input_shape, array_init=None, label=None, conn_args=None, **conv_args + ): + conn_args = {} if conn_args is None else conn_args + + if array_init is not None: + assert all(a not in conv_args for a in ("init", "kernel_size", "n_filters")) + assert array_init.ndim == 4 + conv_args["init"] = array_init + conv_args["kernel_size"] = array_init.shape[:2] + assert array_init.shape[2] == input_shape.n_channels + conv_args["n_filters"] = array_init.shape[3] + + conv = nengo.Convolution(input_shape=input_shape, **conv_args) + + # add an ensemble to implement the activation function + layer = nengo.Ensemble(conv.output_shape.size, 1, label=label) + + # connect up the input object to the new layer + conn = nengo.Connection(x, layer.neurons, transform=conv) + + return layer, conv, conn + + channels_last = True + channels = 1 + n_filters0 = 1 + n_filters1 = 4 + n_filters2 = 4 + # load data + with open(os.path.join(test_dir, "mnist10.pkl"), "rb") as f: + test10 = pickle.load(f) + + test_x = test10[0][0].reshape(28, 28) # range (0, 1) + input_shape = nengo_transforms.ChannelShape( + (test_x.shape + (channels,)) if channels_last else ((channels,) + test_x.shape), + channels_last=channels_last, + ) + + filters0 = np.ones((1, 1, channels, n_filters0)) + + # use Gabor filters for first layer + filters1 = Gabor( + freq=Uniform(0.5, 1), sigma_x=Choice([0.9]), sigma_y=Choice([0.9]) + ).generate(n_filters1, (7, 7), rng=rng) + assert n_filters0 == 1 + filters1 = filters1[None, :, :, :] # single channel + filters1 = np.transpose(filters1, (2, 3, 0, 1)) # rows, cols, in_chan, out_chan + + # use random combinations of first-layer channels in 1x1 convolution + filters2 = rng.uniform(-0.2, 1, size=(n_filters1, n_filters2)).clip(0, None) + filters2 *= 2 / filters2.sum(axis=0, keepdims=True) # each filter sums to 2 + filters2 = filters2[None, None, :, :] # rows, cols, in_chan, out_chan + + tau_s = 0.001 + max_rate = 100 + amp = 1 / max_rate + + # use Loihi neuron type so Nengo sim mimics Loihi neuron effects + neuron_type = LoihiSpikingRectifiedLinear(amplitude=amp) + + pres_time = 0.2 + + with nengo.Network(seed=seed) as net: + nengo_loihi.add_params(net) + + net.config[nengo.Ensemble].neuron_type = neuron_type + net.config[nengo.Ensemble].max_rates = Choice([max_rate]) + net.config[nengo.Ensemble].intercepts = Choice([0]) + net.config[nengo.Connection].synapse = tau_s + + u = nengo.Node(test_x.ravel(), label="u") + + layer0, conv0, conn0 = conv_layer( + u, + input_shape=input_shape, + array_init=filters0, + strides=(1, 1), + label="layer0", + conn_args=dict(synapse=None), + ) + net.config[layer0].on_chip = False + + layer1, conv1, conn1 = conv_layer( + layer0.neurons, + input_shape=conv0.output_shape, + array_init=filters1, + strides=(2, 2), + label="layer1", + ) + net.config[conn1].pop_type = pop_type + + layer2, conv2, conn2 = conv_layer( + layer1.neurons, + input_shape=conv1.output_shape, + array_init=filters2, + strides=(1, 1), + label="layer2", + ) + net.config[conn2].pop_type = pop_type + + output_p = nengo.Probe(layer2.neurons) + output_shape = conv2.output_shape + + with nengo.Simulator(net, optimize=False) as sim_nengo: + sim_nengo.run(pres_time) + ref_out = (sim_nengo.data[output_p] > 0).sum(axis=0).reshape(output_shape.shape) + + hw_opts = dict(snip_max_spikes_per_step=800) + with Simulator(net, precompute=precompute, hardware_options=hw_opts) as sim_loihi: + block1 = sim_loihi.model.objs[layer1]["out"] + n_axons1 = sum(axon.axon_slots() for axon in block1.axons) + n_inputs1 = np.prod(conv1.output_shape.spatial_shape) + assert n_axons1 == (2 if pop_type == 32 else 1) * n_inputs1 + + sim_loihi.run(pres_time) + sim_out = (sim_loihi.data[output_p] > 0).sum(axis=0).reshape(output_shape.shape) + + out_max = ref_out.max() + ref_out = ref_out / out_max + sim_out = sim_out / out_max + + # --- plot results + rows = 2 + cols = 3 + + ax = plt.subplot(rows, cols, 1) + imshow(test_x, vmin=0, vmax=1, ax=ax) + + ax = plt.subplot(rows, cols, 2) + tile(np.transpose(filters1, (2, 3, 0, 1))[0], rows=2, cols=2, grid=True, ax=ax) + + ax = plt.subplot(rows, cols, 3) + assert filters2.shape[:2] == (1, 1) + filters12 = filters1.dot(filters2[0, 0]) + tile(np.transpose(filters12, (2, 3, 0, 1))[0], rows=2, cols=2, grid=True, ax=ax) + + ax = plt.subplot(rows, cols, 4) + plt.hist((ref_out.ravel(), sim_out.ravel()), bins=21) + + ax = plt.subplot(rows, cols, 5) + tile(np.transpose(ref_out, (2, 0, 1)), rows=2, cols=2, grid=True, ax=ax) + + ax = plt.subplot(rows, cols, 6) + tile(np.transpose(sim_out, (2, 0, 1)), rows=2, cols=2, grid=True, ax=ax) + + assert allclose(sim_out, ref_out, atol=0.15, rtol=1e-3) + + @pytest.mark.skipif(nengo_transforms is None, reason="Requires new nengo.transforms") def test_conv_split(Simulator, rng, plt, allclose): channels_last = False @@ -703,7 +847,8 @@ def test_conv_split(Simulator, rng, plt, allclose): assert allclose(loihi_out, nengo_out, atol=0.15 * out_max, rtol=0.15) -def test_conv_preslice(Simulator, plt): +@pytest.mark.parametrize("on_chip", [True, False]) +def test_conv_preslice(on_chip, Simulator, plt): conv2d = pytest.importorskip("nengo._vendor.npconv2d.conv2d") kernel = np.array([[-1, 2, -1], [-1, 2, -1], [-1, 2, -1]], dtype=float) @@ -726,14 +871,18 @@ def test_conv_preslice(Simulator, plt): input_gain = 149.0 neuron_type = nengo.SpikingRectifiedLinear() + loihi_neuron = LoihiSpikingRectifiedLinear() + layer0_neuron = loihi_neuron if on_chip else neuron_type - y_ref = LoihiSpikingRectifiedLinear().rates(image.ravel(), input_gain, 0) + y_ref = layer0_neuron.rates(image.ravel(), input_gain, 0) y_ref = conv2d.conv2d( y_ref.reshape((1, 5, 5, 1)), kernel.reshape((3, 3, 1, 1)), pad="VALID" ) - y_ref = LoihiSpikingRectifiedLinear().rates(y_ref.ravel(), 1.0, 0.0).reshape((3, 3)) + y_ref = loihi_neuron.rates(y_ref.ravel(), 1.0, 0.0).reshape((3, 3)) with nengo.Network() as net: + nengo_loihi.add_params(net) + u = nengo.Node(image2.ravel()) a = nengo.Ensemble( 50, @@ -742,6 +891,7 @@ def test_conv_preslice(Simulator, plt): gain=nengo.dists.Choice([input_gain]), bias=nengo.dists.Choice([0]), ) + net.config[a].on_chip = on_chip transform = nengo_transforms.Convolution( n_filters=1, input_shape=(5, 5, 1), init=kernel.reshape((3, 3, 1, 1)) @@ -759,8 +909,7 @@ def test_conv_preslice(Simulator, plt): nengo.Connection(a.neurons[::2], b.neurons, transform=transform) bp = nengo.Probe(b.neurons, synapse=nengo.Alpha(0.02)) - hw_opts = dict(snip_max_spikes_per_step=100) - with Simulator(net, hardware_options=hw_opts) as sim: + with Simulator(net) as sim: sim.run(0.3) y_ref = y_ref / input_gain @@ -930,6 +1079,101 @@ def test_conv_overlap_input(Simulator, plt): assert np.allclose(y1, y_ref[1:], atol=0.02, rtol=0.1) +@pytest.mark.target_loihi +@pytest.mark.parametrize("on_chip", [True, False]) +@pytest.mark.parametrize("precompute", [True, False]) +@pytest.mark.parametrize("pop_type", [16, 32]) +@pytest.mark.parametrize("channels_last", [True, False]) +def test_chip_population_axons( + on_chip, precompute, pop_type, channels_last, Simulator, rng +): + """Check that all types of population axons work as inputs or between cores. + + Also, on the chip, dummy axons were still having an effect. Check this is fixed. + """ + + def conv_layer(input=None, label=None, **kwargs): + conv = nengo.Convolution(**kwargs) + layer = nengo.Ensemble(conv.output_shape.size, 1, label=label) + conn = ( + nengo.Connection(input, layer.neurons, transform=conv) + if input is not None + else None + ) + return layer, conv, conn + + if pop_type == 16 and not channels_last: + pytest.skip("pop16 axons not compatible with single-compartment shifts") + + max_rate = 100 + amp = 1 / max_rate + + n_filters0 = 4 + n_filters1 = 4 + # 6 x 6 input will have one unused pixel at edge with 3 x 3 kernel and stride 2 + input_shape = (6, 6, 1) if channels_last else (1, 6, 6) + input_shape = nengo_transforms.ChannelShape( + input_shape, channels_last=channels_last + ) + X = rng.uniform(0.2, 1, size=input_shape.shape) + kernel0 = rng.uniform(0.2, 1, size=(1, 1, 1, n_filters0)) + kernel1 = rng.uniform(0.1, 0.5, size=(3, 3, n_filters0, n_filters1)) + + with nengo.Network(seed=0) as net: + nengo_loihi.add_params(net) + net.config[nengo.Ensemble].neuron_type = nengo.SpikingRectifiedLinear( + amplitude=amp + ) + net.config[nengo.Ensemble].max_rates = nengo.dists.Choice([max_rate]) + net.config[nengo.Ensemble].intercepts = nengo.dists.Choice([0]) + net.config[nengo.Connection].synapse = 0.005 + + inp = nengo.Node(X.ravel()) if not on_chip else None + + # first layer is off-chip to translate the inputs into spikes + layer0, conv0, _ = conv_layer( + input=inp, + n_filters=n_filters0, + input_shape=input_shape, + channels_last=channels_last, + kernel_size=(1, 1), + init=kernel0, + label="layer0", + ) + + net.config[layer0].on_chip = on_chip + if on_chip: + assert kernel0.shape[:2] == (1, 1) + w = kernel0[0, 0] + Y = X.dot(w) if channels_last else np.tensordot(w.T, X, axes=1) + layer0.gain = nengo.dists.Choice([0.0]) + layer0.bias = Y.ravel() * max_rate + + layer1, conv1, conn1 = conv_layer( + input=layer0.neurons, + n_filters=n_filters1, + input_shape=conv0.output_shape, + channels_last=channels_last, + kernel_size=(3, 3), + strides=(2, 2), + init=kernel1, + label="layer1", + ) + net.config[conn1].pop_type = pop_type + + probe = nengo.Probe(layer1.neurons) + + sim_time = 0.1 + with Simulator(net, target="sim") as emulator: + emulator.run(sim_time) + + with Simulator(net, target="loihi", precompute=precompute) as loihi: + loihi.run(sim_time) + + assert np.all(emulator.data[probe].sum(axis=0) > 0) + assert np.array_equal(loihi.data[probe], emulator.data[probe]) + + @pytest.mark.skipif(nengo_transforms is None, reason="Requires new nengo.transforms") def test_conv_gain(Simulator): with nengo.Network() as net: diff --git a/nengo_loihi/tests/test_simulator.py b/nengo_loihi/tests/test_simulator.py index 9884bf4c0..cf9e583a2 100644 --- a/nengo_loihi/tests/test_simulator.py +++ b/nengo_loihi/tests/test_simulator.py @@ -656,12 +656,6 @@ def test_population_input(request, allclose): model.add_input(input) spikes = [(input, ti, inds) for ti, inds in spike_times_inds] - input_axon = Axon(n_axons) - target_axons = np.zeros(n_inputs, dtype=int) - atoms = np.arange(n_inputs) - input_axon.set_compartment_axon_map(target_axons, atoms=atoms) - input.add_axon(input_axon) - block = LoihiBlock(n_compartments) block.compartment.configure_lif(tau_rc=0.0, tau_ref=0.0, dt=dt) block.compartment.configure_filter(0, dt=dt) @@ -676,7 +670,14 @@ def test_population_input(request, allclose): weights, indices, axon_to_weight_map, bases, pop_type=32 ) block.add_synapse(synapse) - input_axon.target = synapse + + input_axon = Axon( + n_axons, + target=synapse, + compartment_map=np.zeros(n_inputs), + atoms=np.arange(n_inputs), + ) + input.add_axon(input_axon) probe = Probe(target=block, key="voltage") block.add_probe(probe) diff --git a/nengo_loihi/tests/test_splitter.py b/nengo_loihi/tests/test_splitter.py index d106203f5..ac304aa6a 100644 --- a/nengo_loihi/tests/test_splitter.py +++ b/nengo_loihi/tests/test_splitter.py @@ -3,7 +3,6 @@ from nengo.exceptions import BuildError import numpy as np -from nengo_loihi.compat import nengo_transforms from nengo_loihi.config import add_params from nengo_loihi.splitter import Split @@ -126,23 +125,6 @@ def test_precompute_host_to_learning_rule_unsupported(): Split(net, precompute=True) -@pytest.mark.skipif(nengo_transforms is None, reason="Requires new nengo.transforms") -def test_precompute_with_convolution_unsupported(): - with nengo.Network() as net: - stim = nengo.Node([0, 0]) - ens = nengo.Ensemble(10, 2) - nengo.Connection( - stim, - ens, - transform=nengo_transforms.Convolution( - n_filters=2, input_shape=(1, 2, 1), kernel_size=(1, 2), strides=(1, 1) - ), - ) - - with pytest.raises(BuildError, match="convolutional connections"): - Split(net, precompute=True) - - def test_place_probes(): with nengo.Network() as net: add_params(net) diff --git a/nengo_loihi/tests/test_validate.py b/nengo_loihi/tests/test_validate.py index 0ecc7931a..899d8280d 100644 --- a/nengo_loihi/tests/test_validate.py +++ b/nengo_loihi/tests/test_validate.py @@ -22,8 +22,7 @@ def test_validate_block(): # too many output axons block = LoihiBlock(410) synapse = Synapse(2500) - axon = Axon(5000) - axon.target = synapse + axon = Axon(5000, target=synapse, compartment_map=np.arange(410)) block.add_synapse(synapse) block.add_axon(axon) with pytest.raises(BuildError, match="Output axons"): @@ -33,8 +32,7 @@ def test_validate_block(): block = LoihiBlock(600) synapse = Synapse(500) synapse.set_weights(np.ones((500, 600))) - axon = Axon(500) - axon.target = synapse + axon = Axon(500, target=synapse, compartment_map=np.arange(500)) block.add_synapse(synapse) block.add_axon(axon) with pytest.raises(BuildError, match="synapse bits"): diff --git a/nengo_loihi/validate.py b/nengo_loihi/validate.py index 32ccd83e0..946ff8fa9 100644 --- a/nengo_loihi/validate.py +++ b/nengo_loihi/validate.py @@ -66,7 +66,7 @@ def validate_axon(axon): if isinstance(axon.target, Synapse): if axon.compartment_atoms is not None: idxs = np.arange(len(axon.compartment_atoms)) - axon_ids = axon.map_axon(idxs) + axon_ids = axon.compartment_map[idxs] for atom, axon_id in zip(axon.compartment_atoms, axon_ids): n_populations = axon.target.axon_populations(axon_id) assert 0 <= atom < n_populations @@ -83,7 +83,11 @@ def validate_synapse(synapse): ) if synapse.pop_type == 16: if synapse.axon_compartment_bases is not None: - assert all(b % d(b"NA==", int) == 0 for b in synapse.axon_compartment_bases) + assert all( + b % d(b"NA==", int) == 0 + for b in synapse.axon_compartment_bases + if b >= 0 + ) def validate_synapse_cfg(synapse_cfg):