Skip to content

Commit

Permalink
Merge pull request #1286 from emijan-kth/master
Browse files Browse the repository at this point in the history
Fixes to Convolutions
  • Loading branch information
Christian-B authored Feb 20, 2023
2 parents 614f827 + 91eeed6 commit 27137e2
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 63 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright (c) 2021-2022 The University of Manchester
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

APP = $(notdir $(CURDIR))

NEURON_MODEL = $(NEURON_DIR)/neuron/models/neuron_model_lif_impl.c
NEURON_MODEL_H = $(NEURON_DIR)/neuron/models/neuron_model_lif_impl.h
INPUT_TYPE_H = $(NEURON_DIR)/neuron/input_types/input_type_delta.h
NEURON_IMPL_H = $(NEURON_DIR)/neuron/implementations/neuron_impl_standard.h
THRESHOLD_TYPE_H = $(NEURON_DIR)/neuron/threshold_types/threshold_type_static.h
SYNAPSE_TYPE_H = $(NEURON_DIR)/neuron/synapse_types/synapse_types_delta_impl.h
LOCAL_ONLY_IMPL = $(NEURON_DIR)/neuron/local_only/local_only_conv_impl.c

include ../local_only.mk
3 changes: 2 additions & 1 deletion neural_modelling/makefiles/local_only_combined/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

MODELS = IF_curr_exp_conv\
MODELS = IF_curr_delta_conv\
IF_curr_exp_conv\
IF_curr_exp_pool_dense

all:
Expand Down
70 changes: 47 additions & 23 deletions neural_modelling/src/neuron/local_only/local_only_conv_impl.c
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,12 @@ typedef struct {
lc_shape_t kernel;
lc_shape_t padding;
lc_coord_t recip_strides;
lc_coord_t strides;
lc_coord_t recip_pool_strides;
uint16_t positive_synapse_type;
uint16_t negative_synapse_type;
uint32_t delay;
uint32_t strides_delay_step;
lc_weight_t weights[]; // n_weights = next_even(kernel.width * kernel.height)
} connector;

Expand Down Expand Up @@ -138,27 +141,33 @@ bool local_only_impl_initialise(void *address){
return true;
}

//! \brief Multiply an integer by a 16-bit reciprocal and return the floored
//! \brief Calculate the remainder from a division
static inline int16_t calc_remainder(int16_t dividend, int16_t divisor, int16_t quotient) {
int16_t remainder = dividend - quotient * divisor;
log_debug("remainder: %d = %d * %d + %d",
dividend, quotient, divisor, remainder);
return remainder;
}

//! \brief Calculate remainder Multiply an integer by a 16-bit reciprocal and return the floored
//! integer result
static inline int16_t recip_multiply(int16_t integer, int16_t recip) {
int32_t i = integer;
int32_t r = recip;
return (int16_t) ((i * r) >> RECIP_FRACT_BITS);
}

//! \brief Do a mapping from pre to post 2D spaces, we use the standard
//! padding, kernel, strides from Convolutional Neural Networks
//! because of the way we're looping through the kernel, we divide the kernel
//! shape by 2.
static inline lc_coord_t map_pre_to_post(connector *connector, lc_coord_t pre,
int16_t half_kh, int16_t half_kw) {
lc_coord_t post = pre;
post.col = recip_multiply(post.col, connector->recip_pool_strides.col);
post.row = recip_multiply(post.row, connector->recip_pool_strides.row);
post.col = post.col - half_kw + connector->padding.width;
post.row = post.row - half_kh + connector->padding.height;
post.col = recip_multiply(post.col, connector->recip_strides.col);
post.row = recip_multiply(post.row, connector->recip_strides.row);
//! \brief Do a mapping from pre to post 2D spaces
static inline lc_coord_t map_pre_to_post(connector *connector, lc_coord_t pre, lc_coord_t *start_i) {
pre.col = recip_multiply(pre.col, connector->recip_pool_strides.col);
pre.row = recip_multiply(pre.row, connector->recip_pool_strides.row);
pre.col += connector->padding.width;
pre.row += connector->padding.height;
lc_coord_t post;
post.col = recip_multiply(pre.col, connector->recip_strides.col);
post.row = recip_multiply(pre.row, connector->recip_strides.row);
start_i->col = calc_remainder(pre.col, connector->strides.col, post.col);
start_i->row = calc_remainder(pre.row, connector->strides.row, post.row);
return post;
}

Expand All @@ -169,21 +178,34 @@ static inline lc_coord_t map_pre_to_post(connector *connector, lc_coord_t pre,
static inline void do_convolution_operation(
uint32_t time, lc_coord_t pre_coord, connector *connector,
uint16_t *ring_buffers) {
int32_t half_kh = connector->kernel.height / 2;
int32_t half_kw = connector->kernel.width / 2;
lc_coord_t post_coord = map_pre_to_post(connector, pre_coord, half_kh, half_kw);
lc_coord_t start_i;
log_debug("kernel height: %d, kernel width: %d, padding height: %d, padding width: %d, strides row: %d, strides col: %d", connector->kernel.height, connector->kernel.width, connector->padding.height, connector->padding.width, connector->strides.row, connector->strides.col);
lc_coord_t post_coord = map_pre_to_post(connector, pre_coord, &start_i);
log_debug("pre row %d, col %d AS post row %d, col %d",
pre_coord.row, pre_coord.col, post_coord.row, post_coord.col);

int32_t kw = connector->kernel.width;
for (int32_t r = -half_kh, kr = 0; r <= half_kh; r++, kr++) {
int32_t tmp_row = post_coord.row + r;
for (int32_t i_row = start_i.row, tmp_row = post_coord.row; i_row < connector->kernel.height; i_row += connector->strides.row, --tmp_row) {
int32_t kr = connector->kernel.height - 1 - i_row;
log_debug("i_row = %u, kr = %u, tmp_row = %u", i_row, kr, tmp_row);

if ((tmp_row < config.post_start.row) || (tmp_row > config.post_end.row)) {
log_debug("tmp_row outside");
continue;
}
for (int32_t c = -half_kw, kc = 0; c <= half_kw; c++, kc++) {
int32_t tmp_col = post_coord.col + c;

uint32_t delay = connector->delay;
if (connector->strides_delay_step != 0)
{
delay -= start_i.col * connector->strides_delay_step;
log_debug("start_i.col = %u, delay = %u", start_i.col, delay);
}

for (int32_t i_col = start_i.col, tmp_col = post_coord.col; i_col < connector->kernel.width; i_col += connector->strides.col, --tmp_col) {
int32_t kc = connector->kernel.width - 1 - i_col;
log_debug("i_col = %u, kc = %u, tmp_col = %u", i_col, kc, tmp_col);
if ((tmp_col < config.post_start.col) || (tmp_col > config.post_end.col)) {
log_debug("tmp_col outside");
continue;
}

Expand All @@ -192,18 +214,20 @@ static inline void do_convolution_operation(
((tmp_row - config.post_start.row) * config.post_shape.width)
+ (tmp_col - config.post_start.col);
uint32_t k = (kr * kw) + kc;
log_debug("weight index = %u", k);
lc_weight_t weight = connector->weights[k];
if (weight == 0) {
log_debug("zero weight");
continue;
}
uint32_t rb_index = 0;
if (weight > 0) {
rb_index = synapse_row_get_ring_buffer_index(time + 1,
rb_index = synapse_row_get_ring_buffer_index(time + delay,
connector->positive_synapse_type, post_index,
synapse_type_index_bits, synapse_index_bits,
synapse_delay_mask);
} else {
rb_index = synapse_row_get_ring_buffer_index(time + 1,
rb_index = synapse_row_get_ring_buffer_index(time + delay,
connector->negative_synapse_type, post_index,
synapse_type_index_bits, synapse_index_bits,
synapse_delay_mask);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ typedef struct {
uint32_t n_weights;
uint16_t positive_synapse_type;
uint16_t negative_synapse_type;
uint32_t delay;
dimension dimensions[];
// Also follows:
// lc_weight_t weights[];
Expand Down Expand Up @@ -213,12 +214,12 @@ void local_only_impl_process_spike(
}
uint32_t rb_index = 0;
if (weight > 0) {
rb_index = synapse_row_get_ring_buffer_index(time + 1,
rb_index = synapse_row_get_ring_buffer_index(time + connector->delay,
connector->positive_synapse_type, post_index,
synapse_type_index_bits, synapse_index_bits,
synapse_delay_mask);
} else {
rb_index = synapse_row_get_ring_buffer_index(time + 1,
rb_index = synapse_row_get_ring_buffer_index(time + connector->delay,
connector->negative_synapse_type, post_index,
synapse_type_index_bits, synapse_index_bits,
synapse_delay_mask);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@
from spynnaker.pyNN.utilities.utility_calls import get_n_bits
from spynnaker.pyNN.models.abstract_models import HasShapeKeyFields
from spynnaker.pyNN.utilities.constants import SPIKE_PARTITION_ID
from spynnaker.pyNN.data.spynnaker_data_view import SpynnakerDataView

#: The number of 32-bit words in the source_key_info struct
SOURCE_KEY_INFO_WORDS = 7

#: The number of 16-bit shorts in the connector struct,
#: ignoring the source_key_info struct and the weights (which are dynamic)
CONNECTOR_CONFIG_SHORTS = 12
CONNECTOR_CONFIG_SHORTS = 18


class ConvolutionConnector(AbstractConnector):
Expand All @@ -50,13 +51,16 @@ class ConvolutionConnector(AbstractConnector):
"__pool_shape",
"__pool_stride",
"__positive_receptor_type",
"__negative_receptor_type"
"__negative_receptor_type",
"__horizontal_delay_step"
]

def __init__(self, kernel_weights, kernel_shape=None, strides=None,
padding=None, pool_shape=None, pool_stride=None,
positive_receptor_type="excitatory",
negative_receptor_type="inhibitory", safe=True,
negative_receptor_type="inhibitory",
horizontal_delay_step=0,
safe=True,
verbose=False, callback=None):
"""
:param kernel_weights:
Expand Down Expand Up @@ -134,6 +138,8 @@ def __init__(self, kernel_weights, kernel_shape=None, strides=None,
self.__positive_receptor_type = positive_receptor_type
self.__negative_receptor_type = negative_receptor_type

self.__horizontal_delay_step = horizontal_delay_step

@property
def positive_receptor_type(self):
return self.__positive_receptor_type
Expand Down Expand Up @@ -215,11 +221,10 @@ def get_post_shape(self, shape):
shape = (post_pool_shape // self.__pool_stride) + 1

kernel_shape = numpy.array(self.__kernel_weights.shape)
post_shape = (shape - (kernel_shape - 1) +
(2 * self.__padding_shape))
post_shape = shape - kernel_shape + (2 * self.__padding_shape)

return numpy.clip(
post_shape // self.__strides, 1, numpy.inf).astype('int')
post_shape // self.__strides + 1, 1, numpy.inf).astype('int')

@overrides(AbstractConnector.validate_connection)
def validate_connection(self, application_edge, synapse_info):
Expand All @@ -230,7 +235,9 @@ def validate_connection(self, application_edge, synapse_info):
"The ConvolutionConnector only works where the Populations"
" of a Projection are both 2D. Please ensure that both the"
" Populations use a Grid2D structure.")
expected_post_shape = tuple(self.get_post_shape(pre.atoms_shape))
pre_shape = pre.atoms_shape
expected_post_shape = tuple(self.get_post_shape((pre_shape[1], pre_shape[0])))
expected_post_shape = expected_post_shape[1], expected_post_shape[0]
if expected_post_shape != post.atoms_shape:
raise ConfigurationException(
f"With a source population with shape {pre.atoms_shape}, "
Expand Down Expand Up @@ -281,10 +288,20 @@ def get_connected_vertices(self, s_info, source_vertex, target_vertex):
pre_slices = [m_vertex.vertex_slice for m_vertex in pre_vertices]
pre_slices_x = [vtx_slice.get_slice(0) for vtx_slice in pre_slices]
pre_slices_y = [vtx_slice.get_slice(1) for vtx_slice in pre_slices]
pre_ranges = [[[px.start, py.start], [px.stop - 1, py.stop - 1]]
pre_ranges = [[[py.start, px.start], [py.stop - 1, px.stop - 1]]
for px, py in zip(pre_slices_x, pre_slices_y)]
pres_as_posts = self.__pre_as_post(pre_ranges)
hlf_k_w, hlf_k_h = numpy.array(self.__kernel_weights.shape) // 2
pre_vertex_in_post_layer, start_i = self.__pre_as_post(pre_ranges)

pre_vertex_in_post_layer_upper_left = pre_vertex_in_post_layer[:,0]
pre_vertex_in_post_layer_lower_right = pre_vertex_in_post_layer[:,1]

kernel_shape = numpy.array(self.__kernel_weights.shape)

j = (kernel_shape - 1 - start_i) // self.__strides
j_upper_left = j[:,0]

pre_vertex_max_reach_in_post_layer_upper_left = pre_vertex_in_post_layer_upper_left - j_upper_left
pre_vertex_max_reach_in_post_layer_lower_right = pre_vertex_in_post_layer_lower_right

connected = list()
for post in target_vertex.splitter.get_in_coming_vertices(
Expand All @@ -293,18 +310,18 @@ def get_connected_vertices(self, s_info, source_vertex, target_vertex):
post_slice_x = post_slice.get_slice(0)
post_slice_y = post_slice.get_slice(1)

# Get ranges allowed in post
min_x = post_slice_x.start - hlf_k_w
max_x = (post_slice_x.stop + hlf_k_w) - 1
min_y = post_slice_y.start - hlf_k_h
max_y = (post_slice_y.stop + hlf_k_h) - 1
# Get ranges allowed in post vertex
min_x = post_slice_x.start
max_x = post_slice_x.stop - 1
min_y = post_slice_y.start
max_y = post_slice_y.stop - 1

# Test that the start coords are in range i.e. less than max
start_in_range = numpy.logical_not(
numpy.any(pres_as_posts[:, 0] > [max_x, max_y], axis=1))
numpy.any(pre_vertex_max_reach_in_post_layer_upper_left > [max_y, max_x], axis=1))
# Test that the end coords are in range i.e. more than min
end_in_range = numpy.logical_not(
numpy.any(pres_as_posts[:, 1] < [min_x, min_y], axis=1))
numpy.any(pre_vertex_max_reach_in_post_layer_lower_right < [min_y, min_x], axis=1))
# When both things are true, we have a vertex in range
pre_in_range = pre_vertices[
numpy.logical_and(start_in_range, end_in_range)]
Expand All @@ -315,17 +332,18 @@ def get_connected_vertices(self, s_info, source_vertex, target_vertex):
def __pre_as_post(self, pre_coords):
""" Write pre coords as post coords.
:param Iterable pre_coords: An iterable of (x, y) coordinates
:param Iterable pre_coords: An iterable of (y, x) coordinates
:rtype: numpy.ndarray
"""
coords = numpy.array(pre_coords)
if self.__pool_stride is not None:
coords //= self.__pool_stride

kernel_shape = numpy.array(self.__kernel_weights.shape)
coords = coords - kernel_shape // 2 + self.__padding_shape
coords //= self.__strides
return coords
coords += self.__padding_shape
coord_by_strides = coords // self.__strides
start_i = coords % self.__strides

return coord_by_strides, start_i

@property
def local_only_n_bytes(self):
Expand All @@ -343,9 +361,9 @@ def write_local_only_data(
weight_scales):
# Get info about things
kernel_shape = self.__kernel_weights.shape
ps_x, ps_y = 1, 1
ps_y, ps_x = 1, 1
if self.__pool_stride is not None:
ps_x, ps_y = self.__pool_stride
ps_y, ps_x = self.__pool_stride

# Write source key info
spec.write_value(key, data_type=DataType.UINT32)
Expand Down Expand Up @@ -376,13 +394,17 @@ def write_local_only_data(
# Write remaining connector details
spec.write_value(start[1], data_type=DataType.INT16)
spec.write_value(start[0], data_type=DataType.INT16)
spec.write_value(kernel_shape[1], data_type=DataType.INT16)
spec.write_value(kernel_shape[0], data_type=DataType.INT16)
spec.write_value(self.__padding_shape[1], data_type=DataType.INT16)
spec.write_value(kernel_shape[1], data_type=DataType.INT16)
spec.write_value(self.__padding_shape[0], data_type=DataType.INT16)
spec.write_value(self.__padding_shape[1], data_type=DataType.INT16)
spec.write_value(self.__recip(self.__strides[0]),
data_type=DataType.INT16)
spec.write_value(self.__recip(self.__strides[1]),
data_type=DataType.INT16)
spec.write_value(self.__recip(self.__strides[0]),
spec.write_value(self.__strides[0],
data_type=DataType.INT16)
spec.write_value(self.__strides[1],
data_type=DataType.INT16)
spec.write_value(self.__recip(ps_y), data_type=DataType.INT16)
spec.write_value(self.__recip(ps_x), data_type=DataType.INT16)
Expand All @@ -395,6 +417,12 @@ def write_local_only_data(
spec.write_value(pos_synapse_type, data_type=DataType.UINT16)
spec.write_value(neg_synapse_type, data_type=DataType.UINT16)

# Write delay
spec.write_value(app_edge.post_vertex.synapse_dynamics.delay *
SpynnakerDataView.get_simulation_time_step_per_ms())

spec.write_value(self.__horizontal_delay_step, data_type=DataType.UINT32)

# Encode weights with weight scaling
encoded_kernel_weights = self.__kernel_weights.flatten()
if len(encoded_kernel_weights) % 2 != 0:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@
from collections.abc import Iterable
from spinn_front_end_common.utilities.exceptions import ConfigurationException
from spynnaker.pyNN.models.abstract_models import HasShapeKeyFields
from spynnaker.pyNN.data.spynnaker_data_view import SpynnakerDataView


_DIMENSION_SIZE = (2 * BYTES_PER_WORD) + (6 * BYTES_PER_SHORT)
_KEY_INFO_SIZE = 3 * BYTES_PER_WORD
_CONN_SIZE = _KEY_INFO_SIZE + (2 * BYTES_PER_WORD) + (2 * BYTES_PER_SHORT)
_CONN_SIZE = _KEY_INFO_SIZE + (3 * BYTES_PER_WORD) + (2 * BYTES_PER_SHORT)
_DIM_DTYPE = [("mask", "uint32"), ("shift", "uint32"), ("pre_start", "uint16"),
("pre_in_post_start", "uint16"), ("pre_in_post_end", "uint16"),
("pre_in_post_shape", "uint16"), ("recip_pool_stride", "uint16"),
Expand Down Expand Up @@ -283,6 +284,10 @@ def write_local_only_data(
spec.write_value(pos_synapse_type, data_type=DataType.UINT16)
spec.write_value(neg_synapse_type, data_type=DataType.UINT16)

# Write delay
spec.write_value(app_edge.post_vertex.synapse_dynamics.delay *
SpynnakerDataView.get_simulation_time_step_per_ms())

# Generate the dimension information
dim_info = numpy.zeros(n_dims, dtype=_DIM_DTYPE)
if self.__pool_stride is not None:
Expand Down
Loading

0 comments on commit 27137e2

Please sign in to comment.