Repository: SpiNNakerManchester/sPyNNaker (language: python)

Issue:
Reduce overhead data spec
Once #1195 is merged, this can follow.

Updates the way data specification is done to make things faster, using repeated data specification from various parts and caching it.


Discussion / hints:

Fix patch:
diff --git a/neural_modelling/src/delay_extension/delay_extension.c b/neural_modelling/src/delay_extension/delay_extension.c
index 2420ef1b1e..4b5bc12f38 100644
--- a/neural_modelling/src/delay_extension/delay_extension.c
+++ b/neural_modelling/src/delay_extension/delay_extension.c
@@ -160,6 +160,15 @@ static uint32_t n_background_overloads = 0;
 //! The maximum number of background tasks queued
 static uint32_t max_backgrounds_queued = 0;
 
+//! The number of colour bits (both from source and to send)
+static uint32_t n_colour_bits = 0;
+
+//! The mask to apply to get the colour from the current timestep or key
+static uint32_t colour_mask = 0;
+
+//! The colour for the current time step
+static uint32_t colour = 0;
+
 //---------------------------------------
 // Because we don't want to include string.h or strings.h for memset
 //! \brief Sets an array of counters to zero
@@ -245,6 +254,9 @@ static bool read_parameters(struct delay_parameters *params) {
         zero_spike_counters(spike_counters[s], num_neurons);
     }
 
+    n_colour_bits = params->n_colour_bits;
+    colour_mask = (1 << n_colour_bits) - 1;
+
     log_debug("read_parameters: completed successfully");
     return true;
 }
@@ -348,13 +360,6 @@ static inline index_t key_n(key_t k) {
 //! \brief Processes spikes queued by ::incoming_spike_callback()
 static inline void spike_process(void) {
 
-    // Get current time slot of incoming spike counters
-    uint32_t current_time_slot = time & num_delay_slots_mask;
-    uint8_t *current_time_slot_spike_counters =
-            spike_counters[current_time_slot];
-
-    log_debug("%d: Current time slot %u", time, current_time_slot);
-
     // While there are any incoming spikes
     spike_t s;
     uint32_t state = spin1_int_disable();
@@ -364,18 +369,29 @@ static inline void spike_process(void) {
 
         if ((s & incoming_mask) == incoming_key) {
             // Mask out neuron ID
-            uint32_t neuron_id = key_n(s);
+            uint32_t spike_id = key_n(s);
+            uint32_t spike_colour = spike_id & colour_mask;
+            uint32_t neuron_id = spike_id >> n_colour_bits;
             if (neuron_id < num_neurons) {
+
+            	// Account for delayed spikes
+            	int32_t colour_diff = colour - spike_colour;
+            	uint32_t colour_delay = colour_diff & colour_mask;
+
+            	// Get current time slot of incoming spike counters
+				uint32_t time_slot = (time + colour_delay) & num_delay_slots_mask;
+				uint8_t *time_slot_spike_counters = spike_counters[time_slot];
+
                 // Increment counter
-                if (current_time_slot_spike_counters[neuron_id] ==
+                if (time_slot_spike_counters[neuron_id] ==
                         COUNTER_SATURATION_VALUE) {
                     saturation_count += 1;
                 } else {
-                    current_time_slot_spike_counters[neuron_id]++;
+                	time_slot_spike_counters[neuron_id]++;
                 }
                 log_debug("Incrementing counter %u = %u\n",
                         neuron_id,
-                        current_time_slot_spike_counters[neuron_id]);
+						time_slot_spike_counters[neuron_id]);
                 n_spikes_added++;
             } else {
                 n_packets_dropped_due_to_invalid_neuron_value += 1;
@@ -428,7 +444,7 @@ static void background_callback(uint local_time, UNUSED uint timer_count) {
                 // Calculate key all spikes coming from this neuron will be
                 // sent with
                 uint32_t neuron_index = ((d * num_neurons) + n);
-                uint32_t spike_key = neuron_index + key;
+                uint32_t spike_key = (key + (neuron_index << n_colour_bits)) | colour;
 
                 log_debug("Neuron %u sending %u spikes after delay"
                         "stage %u with key %x",
@@ -504,6 +520,9 @@ static void timer_callback(uint timer_count, UNUSED uint unused1) {
         return;
     }
 
+    // Set the colour for the time step
+    colour = time & colour_mask;
+
     if (!spin1_schedule_callback(background_callback, time, timer_count, BACKGROUND)) {
         // We have failed to do this timer tick!
         n_background_overloads++;
diff --git a/neural_modelling/src/delay_extension/delay_extension.h b/neural_modelling/src/delay_extension/delay_extension.h
index 26d317a03d..ee4963536e 100644
--- a/neural_modelling/src/delay_extension/delay_extension.h
+++ b/neural_modelling/src/delay_extension/delay_extension.h
@@ -48,6 +48,7 @@ struct delay_parameters {
     uint32_t n_delay_stages;      //!< Number of delay stages
     uint32_t n_delay_in_a_stage;  //!< Number of delays in a given stage
     uint32_t clear_packets;       //!< Clear packets each timestep?
+    uint32_t n_colour_bits;       //!< The number of bits used for colour
 };
 
 //! \brief Encode a delay as a 16-bit integer
diff --git a/neural_modelling/src/neuron/c_main_neurons.c b/neural_modelling/src/neuron/c_main_neurons.c
index 683b7ea648..2ea0261677 100644
--- a/neural_modelling/src/neuron/c_main_neurons.c
+++ b/neural_modelling/src/neuron/c_main_neurons.c
@@ -270,10 +270,6 @@ static bool initialise(void) {
             SDRAM_PARAMS_REGION, ds_regions);
     spin1_memcpy(&sdram_inputs, sdram_config, sizeof(struct sdram_config));
 
-    log_info("Transferring ring buffers from 0x%08x for %d cores using %d bytes!"
-            " per core", sdram_inputs.address, sdram_inputs.n_synapse_cores,
-            sdram_inputs.size_in_bytes);
-
     uint32_t n_words = sdram_inputs.size_in_bytes >> 2;
     for (uint32_t i = 0; i < N_SYNAPTIC_BUFFERS; i++) {
         synaptic_contributions[i] = spin1_malloc(sdram_inputs.size_in_bytes);
diff --git a/neural_modelling/src/neuron/c_main_synapse_common.h b/neural_modelling/src/neuron/c_main_synapse_common.h
index 5344f93ab4..5a0f01f57d 100644
--- a/neural_modelling/src/neuron/c_main_synapse_common.h
+++ b/neural_modelling/src/neuron/c_main_synapse_common.h
@@ -57,6 +57,12 @@ struct synapse_provenance {
     uint32_t n_invalid_master_pop_table_hits;
     //! The number of spikes that a bit field filtered, stopping a DMA
     uint32_t n_filtered_by_bitfield;
+    //! The number of synapses that were skipped due to late spikes
+    uint32_t n_synapses_skipped;
+    //! The number of spikes that were detected as late
+    uint32_t n_late_spikes;
+    //! The maximum lateness of a spike
+    uint32_t max_late_spike;
 };
 
 //! \brief Callback to store synapse provenance data (format: synapse_provenance).
@@ -72,6 +78,9 @@ static inline void store_synapse_provenance(struct synapse_provenance *prov) {
     prov->n_failed_bitfield_reads = failed_bit_field_reads;
     prov->n_invalid_master_pop_table_hits = invalid_master_pop_hits;
     prov->n_filtered_by_bitfield = bit_field_filtered_packets;
+    prov->n_synapses_skipped = skipped_synapses;
+    prov->n_late_spikes = late_spikes;
+    prov->max_late_spike = max_late_spike;
 }
 
 //! \brief Read data to set up synapse processing
diff --git a/neural_modelling/src/neuron/local_only/local_only_conv_impl.c b/neural_modelling/src/neuron/local_only/local_only_conv_impl.c
index eaa45f91da..5b82bdbd95 100644
--- a/neural_modelling/src/neuron/local_only/local_only_conv_impl.c
+++ b/neural_modelling/src/neuron/local_only/local_only_conv_impl.c
@@ -42,6 +42,7 @@ typedef struct {
 typedef struct {
     uint32_t key;
     uint32_t mask;
+    uint32_t n_colour_bits;
     uint32_t col_mask;
     uint32_t col_shift;
     uint32_t row_mask;
@@ -227,9 +228,10 @@ static inline bool key_to_index_lookup(uint32_t spike, connector **conn,
     for (uint32_t i = 0; i < config.n_connectors; i++) {
         connector *c = connectors[i];
         if ((spike & c->key_info.mask) == c->key_info.key) {
+        	uint32_t local_spike = (spike & ~c->key_info.mask) >> c->key_info.n_colour_bits;
             *conn = c;
-            *core_local_col = (spike & c->key_info.col_mask) >> c->key_info.col_shift;
-            *core_local_row = (spike & c->key_info.row_mask) >> c->key_info.row_shift;
+            *core_local_col = (local_spike & c->key_info.col_mask) >> c->key_info.col_shift;
+            *core_local_row = (local_spike & c->key_info.row_mask) >> c->key_info.row_shift;
             return true;
         }
     }
diff --git a/neural_modelling/src/neuron/local_only/local_only_pool_dense_impl.c b/neural_modelling/src/neuron/local_only/local_only_pool_dense_impl.c
index 007ab173bb..9dfbf3840c 100644
--- a/neural_modelling/src/neuron/local_only/local_only_pool_dense_impl.c
+++ b/neural_modelling/src/neuron/local_only/local_only_pool_dense_impl.c
@@ -42,6 +42,7 @@ typedef struct {
 typedef struct {
     uint32_t key;
     uint32_t mask;
+    uint32_t n_colour_bits;
 } source_key_info;
 
 // A reciprocal of a 16-bit signed integer will have 1 sign bit, 1 integer bit
@@ -149,6 +150,7 @@ static inline bool key_to_index_lookup(uint32_t spike, connector **conn,
     for (uint32_t i = 0; i < config.n_connectors; i++) {
         connector *c = connectors[i];
         if ((spike & c->key_info.mask) == c->key_info.key) {
+        	uint32_t local_spike = (spike & ~c->key_info.mask) & c->key_info.n_colour_bits;
             *conn = c;
 
             // Now work out the index into the weights from the coordinates
@@ -158,7 +160,7 @@ static inline bool key_to_index_lookup(uint32_t spike, connector **conn,
                 dimension *dim = &c->dimensions[j];
 
                 // Get the coordinate for this dimension from the spike
-                uint32_t coord = (spike & dim->mask) >> dim->shift;
+                uint32_t coord = (local_spike & dim->mask) >> dim->shift;
 
                 // Work out the position in the global space
                 coord += dim->pre_start;
diff --git a/neural_modelling/src/neuron/neuron.c b/neural_modelling/src/neuron/neuron.c
index c5cff91883..b3dd259b7b 100644
--- a/neural_modelling/src/neuron/neuron.c
+++ b/neural_modelling/src/neuron/neuron.c
@@ -39,6 +39,9 @@ uint32_t latest_send_time = 0xFFFFFFFF;
 //! Earliest time in a timestep that any neuron has sent a spike
 uint32_t earliest_send_time = 0;
 
+//! The colour of the time step to handle delayed spikes
+uint32_t colour = 0;
+
 //! The number of neurons on the core
 static uint32_t n_neurons;
 
@@ -48,6 +51,9 @@ static uint32_t n_neurons_peak;
 //! The number of synapse types
 static uint32_t n_synapse_types;
 
+//! The mask of the colour
+static uint32_t colour_mask;
+
 //! Amount to left shift the ring buffer by to make it an input
 static uint32_t *ring_buffer_to_input_left_shifts;
 
@@ -65,6 +71,7 @@ struct neuron_core_parameters {
     uint32_t has_key;
     uint32_t n_neurons_to_simulate;
     uint32_t n_neurons_peak;
+    uint32_t n_colour_bits;
     uint32_t n_synapse_types;
     uint32_t ring_buffer_shifts[];
     // Following this struct in memory (as it can't be expressed in C) is:
@@ -111,6 +118,9 @@ bool neuron_initialise(
     n_neurons_peak = params->n_neurons_peak;
     n_synapse_types = params->n_synapse_types;
 
+    // Get colour details
+    colour_mask = (1 << params->n_colour_bits) - 1;
+
     // Set up ring buffer left shifts
     uint32_t ring_buffer_bytes = n_synapse_types * sizeof(uint32_t);
     ring_buffer_to_input_left_shifts = spin1_malloc(ring_buffer_bytes);
@@ -187,6 +197,9 @@ void neuron_do_timestep_update(timer_t time, uint timer_count) { // EXPORTED
 
     // Record the recorded variables
     neuron_recording_record(time);
+
+    // Update the colour
+    colour = (colour + 1) & colour_mask;
 }
 
 void neuron_transfer(weight_t *syns) { // EXPORTED
diff --git a/neural_modelling/src/neuron/plasticity/stdp/synapse_dynamics_stdp_common.h b/neural_modelling/src/neuron/plasticity/stdp/synapse_dynamics_stdp_common.h
index ba5ca448ee..43d7ab8981 100644
--- a/neural_modelling/src/neuron/plasticity/stdp/synapse_dynamics_stdp_common.h
+++ b/neural_modelling/src/neuron/plasticity/stdp/synapse_dynamics_stdp_common.h
@@ -153,7 +153,7 @@ uint32_t synapse_dynamics_get_plastic_saturation_count(void) {
 }
 
 static inline fixed_stdp_synapse synapse_dynamics_stdp_get_fixed(
-        uint32_t control_word, uint32_t time) {
+        uint32_t control_word, uint32_t time, uint32_t colour_delay) {
     // Extract control-word components
     // **NOTE** cunningly, control word is just the same as lower
     // 16-bits of 32-bit fixed synapse so same functions can be used
@@ -171,7 +171,7 @@ static inline fixed_stdp_synapse synapse_dynamics_stdp_get_fixed(
                 control_word, synapse_index_mask),
        .type_index = type_index,
        .ring_buffer_index = synapse_row_get_ring_buffer_index_combined(
-                delay_axonal + delay_dendritic + time, type_index,
+                (delay_axonal + delay_dendritic + time) - colour_delay, type_index,
                 synapse_type_index_bits, synapse_delay_mask)
     };
 }
diff --git a/neural_modelling/src/neuron/plasticity/stdp/synapse_dynamics_stdp_izhikevich_neuromodulation.c b/neural_modelling/src/neuron/plasticity/stdp/synapse_dynamics_stdp_izhikevich_neuromodulation.c
index 394edde0d0..04be151774 100644
--- a/neural_modelling/src/neuron/plasticity/stdp/synapse_dynamics_stdp_izhikevich_neuromodulation.c
+++ b/neural_modelling/src/neuron/plasticity/stdp/synapse_dynamics_stdp_izhikevich_neuromodulation.c
@@ -77,6 +77,8 @@ static int16_lut *tau_d_lookup;
 
 static uint32_t *nm_weight_shift;
 
+extern uint32_t skipped_synapses;
+
 #define DECAY_LOOKUP_TAU_C(time) \
     maths_lut_exponential_decay(time, tau_c_lookup)
 #define DECAY_LOOKUP_TAU_D(time) \
@@ -290,10 +292,6 @@ bool synapse_dynamics_initialise(
     nm_params_t *sdram_params = (nm_params_t *) address;
     spin1_memcpy(&nm_params, sdram_params, sizeof(nm_params_t));
 
-    log_info("Constant %k, min weight %k, max weight %k",
-            nm_params.weight_update_constant_component,
-            nm_params.min_weight, nm_params.max_weight);
-
     // Read lookup tables
     address_t lut_address = (void *) &sdram_params[1];
     tau_c_lookup = maths_copy_int16_lut(&lut_address);
@@ -307,7 +305,6 @@ bool synapse_dynamics_initialise(
     }
     for (uint32_t s = 0; s < n_synapse_types; s++) {
         nm_weight_shift[s] = ring_buffer_to_input_buffer_left_shifts[s];
-        log_info("Weight shift %u = %u", s, nm_weight_shift[s]);
     }
 
     return true;
@@ -395,8 +392,9 @@ void synapse_dynamics_process_post_synaptic_event(
 static inline neuromodulated_synapse_t process_plastic_synapse(
         uint32_t control_word, uint32_t last_pre_time, pre_trace_t last_pre_trace,
 		pre_trace_t new_pre_trace, weight_t *ring_buffers, uint32_t time,
-		neuromodulated_synapse_t synapse) {
-    fixed_stdp_synapse s = synapse_dynamics_stdp_get_fixed(control_word, time);
+		uint32_t colour_delay, neuromodulated_synapse_t synapse) {
+    fixed_stdp_synapse s = synapse_dynamics_stdp_get_fixed(control_word, time,
+            colour_delay);
 
     // Create update state from the plastic synaptic word
     nm_update_state_t current_state = get_nm_update_state(synapse, s.type);
@@ -408,12 +406,17 @@ static inline neuromodulated_synapse_t process_plastic_synapse(
 	}
 	nm_final_state_t final_state =
 	    izhikevich_neuromodulation_plasticity_update_synapse(
-            time, last_pre_time, last_pre_trace, new_pre_trace,
+            time - colour_delay, last_pre_time, last_pre_trace, new_pre_trace,
             post_delay, s.delay_axonal, current_state,
             &post_event_history[s.index]);
 
-	// Add weight to ring-buffer entry
-	synapse_dynamics_stdp_update_ring_buffers(ring_buffers, s, final_state.weight);
+	// Add weight to ring-buffer entry, but only if not too late
+    if (s.delay_dendritic + s.delay_axonal >= colour_delay) {
+        synapse_dynamics_stdp_update_ring_buffers(ring_buffers, s,
+                final_state.weight);
+    } else {
+        skipped_synapses++;
+    }
 
     return get_nm_final_synaptic_word(final_state);
 }
@@ -458,7 +461,8 @@ static inline void process_neuromodulation(
 bool synapse_dynamics_process_plastic_synapses(
         synapse_row_plastic_data_t *plastic_region_address,
         synapse_row_fixed_part_t *fixed_region,
-        weight_t *ring_buffers, uint32_t time, bool *write_back) {
+        weight_t *ring_buffers, uint32_t time, uint32_t colour_delay,
+        bool *write_back) {
 
     // If the flag is set, this is neuromodulation
     if (plastic_region_address->neuromodulation.is_neuromodulation) {
@@ -481,9 +485,9 @@ bool synapse_dynamics_process_plastic_synapses(
 
     // Update pre-synaptic trace
     log_debug("Adding pre-synaptic event to trace at time:%u", time);
-    plastic_region_address->history.prev_time = time;
+    plastic_region_address->history.prev_time = time - colour_delay;
     plastic_region_address->history.prev_trace =
-            timing_add_pre_spike(time, last_pre_time, last_pre_trace);
+            timing_add_pre_spike(time - colour_delay, last_pre_time, last_pre_trace);
 
     // Loop through plastic synapses
     for (; n_plastic_synapses > 0; n_plastic_synapses--) {
@@ -493,7 +497,7 @@ bool synapse_dynamics_process_plastic_synapses(
         plastic_words[0] = process_plastic_synapse(
                 control_word, last_pre_time, last_pre_trace,
                 plastic_region_address->history.prev_trace, ring_buffers, time,
-                plastic_words[0]);
+                colour_delay, plastic_words[0]);
         plastic_words++;
     }
     *write_back = true;
diff --git a/neural_modelling/src/neuron/plasticity/stdp/synapse_dynamics_stdp_mad_impl.c b/neural_modelling/src/neuron/plasticity/stdp/synapse_dynamics_stdp_mad_impl.c
index f33cdc9593..bb690f1d99 100644
--- a/neural_modelling/src/neuron/plasticity/stdp/synapse_dynamics_stdp_mad_impl.c
+++ b/neural_modelling/src/neuron/plasticity/stdp/synapse_dynamics_stdp_mad_impl.c
@@ -195,8 +195,9 @@ void synapse_dynamics_process_post_synaptic_event(
 static inline plastic_synapse_t process_plastic_synapse(
         uint32_t control_word, uint32_t last_pre_time, pre_trace_t last_pre_trace,
 		pre_trace_t new_pre_trace, weight_t *ring_buffers, uint32_t time,
-		plastic_synapse_t synapse) {
-    fixed_stdp_synapse s = synapse_dynamics_stdp_get_fixed(control_word, time);
+		uint32_t colour_delay, plastic_synapse_t synapse) {
+    fixed_stdp_synapse s = synapse_dynamics_stdp_get_fixed(control_word, time,
+            colour_delay);
 
 	// Create update state from the plastic synaptic word
 	update_state_t current_state = synapse_structure_get_update_state(
@@ -208,13 +209,15 @@ static inline plastic_synapse_t process_plastic_synapse(
 		post_delay = 0;
 	}
 	final_state_t final_state = plasticity_update_synapse(
-			time, last_pre_time, last_pre_trace, new_pre_trace,
+			time - colour_delay, last_pre_time, last_pre_trace, new_pre_trace,
 			post_delay, s.delay_axonal, current_state,
 			&post_event_history[s.index]);
 
-	// Add weight to ring-buffer entry
-	int32_t weight = synapse_structure_get_final_weight(final_state);
-	synapse_dynamics_stdp_update_ring_buffers(ring_buffers, s, weight);
+	// Add weight to ring-buffer entry, but only if not too late
+	if (s.delay_axonal + s.delay_dendritic >= colour_delay) {
+	    int32_t weight = synapse_structure_get_final_weight(final_state);
+	    synapse_dynamics_stdp_update_ring_buffers(ring_buffers, s, weight);
+	}
 
 	return synapse_structure_get_final_synaptic_word(final_state);
 }
@@ -222,7 +225,8 @@ static inline plastic_synapse_t process_plastic_synapse(
 bool synapse_dynamics_process_plastic_synapses(
         synapse_row_plastic_data_t *plastic_region_address,
         synapse_row_fixed_part_t *fixed_region,
-        weight_t *ring_buffers, uint32_t time, bool *write_back) {
+        weight_t *ring_buffers, uint32_t time, uint32_t colour_delay,
+        bool *write_back) {
     // Extract separate arrays of plastic synapses (from plastic region),
     // Control words (from fixed region) and number of plastic synapses
     plastic_synapse_t *plastic_words = plastic_region_address->synapses;
@@ -237,9 +241,9 @@ bool synapse_dynamics_process_plastic_synapses(
 
     // Update pre-synaptic trace
     log_debug("Adding pre-synaptic event to trace at time:%u", time);
-    plastic_region_address->history.prev_time = time;
+    plastic_region_address->history.prev_time = time - colour_delay;
     plastic_region_address->history.prev_trace =
-            timing_add_pre_spike(time, last_pre_time, last_pre_trace);
+            timing_add_pre_spike(time - colour_delay, last_pre_time, last_pre_trace);
 
     // Loop through plastic synapses
     for (; n_plastic_synapses > 0; n_plastic_synapses--) {
@@ -249,7 +253,7 @@ bool synapse_dynamics_process_plastic_synapses(
         plastic_words[0] = process_plastic_synapse(
                 control_word, last_pre_time, last_pre_trace,
                 plastic_region_address->history.prev_trace, ring_buffers, time,
-                plastic_words[0]);
+                colour_delay, plastic_words[0]);
         plastic_words++;
     }
     *write_back = true;
diff --git a/neural_modelling/src/neuron/plasticity/stdp/timing_dependence/timing_recurrent_dual_fsm_impl.c b/neural_modelling/src/neuron/plasticity/stdp/timing_dependence/timing_recurrent_dual_fsm_impl.c
index 6fa03f7129..19d2d15681 100644
--- a/neural_modelling/src/neuron/plasticity/stdp/timing_dependence/timing_recurrent_dual_fsm_impl.c
+++ b/neural_modelling/src/neuron/plasticity/stdp/timing_dependence/timing_recurrent_dual_fsm_impl.c
@@ -45,8 +45,6 @@ typedef struct {
 // Functions
 //---------------------------------------
 uint32_t *timing_initialise(address_t address) {
-    log_info("timing_initialise: starting");
-    log_info("\tRecurrent dual-FSM STDP rule");
     dual_fsm_config_t *config = (dual_fsm_config_t *) address;
 
     // Copy plasticity region data from address
@@ -56,17 +54,11 @@ uint32_t *timing_initialise(address_t address) {
     plasticity_trace_region_data.accumulator_potentiation_minus_one =
             config->accumulator_potentiation_minus_one;
 
-    log_info("\tAccumulator depression=%d, Accumulator potentiation=%d",
-            plasticity_trace_region_data.accumulator_depression_plus_one - 1,
-            plasticity_trace_region_data.accumulator_potentiation_minus_one + 1);
-
     // Copy LUTs from following memory
     spin1_memcpy(pre_exp_dist_lookup, config->pre_exp_dist_lookup,
             sizeof(config->pre_exp_dist_lookup));
     spin1_memcpy(post_exp_dist_lookup, config->post_exp_dist_lookup,
             sizeof(config->post_exp_dist_lookup));
 
-    log_info("timing_initialise: completed successfully");
-
     return config->following_data;
 }
diff --git a/neural_modelling/src/neuron/plasticity/synapse_dynamics.h b/neural_modelling/src/neuron/plasticity/synapse_dynamics.h
index 7a1899ef0b..2d4ea532eb 100644
--- a/neural_modelling/src/neuron/plasticity/synapse_dynamics.h
+++ b/neural_modelling/src/neuron/plasticity/synapse_dynamics.h
@@ -46,7 +46,8 @@ bool synapse_dynamics_initialise(
 bool synapse_dynamics_process_plastic_synapses(
         synapse_row_plastic_data_t *plastic_region_data,
         synapse_row_fixed_part_t *fixed_region,
-        weight_t *ring_buffers, uint32_t time, bool *write_back);
+        weight_t *ring_buffers, uint32_t time, uint32_t colour_delay,
+        bool *write_back);
 
 //! \brief Inform the synapses that the neuron fired
 //! \param[in] time: The current simulation time
diff --git a/neural_modelling/src/neuron/plasticity/synapse_dynamics_static_impl.c b/neural_modelling/src/neuron/plasticity/synapse_dynamics_static_impl.c
index dd588a690b..dfe2393e30 100644
--- a/neural_modelling/src/neuron/plasticity/synapse_dynamics_static_impl.c
+++ b/neural_modelling/src/neuron/plasticity/synapse_dynamics_static_impl.c
@@ -43,7 +43,8 @@ void synapse_dynamics_process_post_synaptic_event(
 bool synapse_dynamics_process_plastic_synapses(
         UNUSED synapse_row_plastic_data_t *plastic_region_data,
         UNUSED synapse_row_fixed_part_t *fixed_region,
-        UNUSED weight_t *ring_buffer, UNUSED uint32_t time, bool *write_back) {
+        UNUSED weight_t *ring_buffer, UNUSED uint32_t time,
+        UNUSED uint32_t colour_delay, bool *write_back) {
     log_error("There should be no plastic synapses!");
     *write_back = false;
     return false;
diff --git a/neural_modelling/src/neuron/population_table/population_table.h b/neural_modelling/src/neuron/population_table/population_table.h
index 16146bf09b..7fa3f21b6e 100644
--- a/neural_modelling/src/neuron/population_table/population_table.h
+++ b/neural_modelling/src/neuron/population_table/population_table.h
@@ -54,9 +54,9 @@ typedef struct master_population_table_entry {
     //! The mask to select the relevant bits of \p key for matching
     uint32_t mask;
     //! The index into ::address_list for this entry
-    uint32_t start: 15;
-    //! Flag to indicate if core mask etc. is valid
-    uint32_t extra_info_flag: 1;
+    uint32_t start: 13;
+    //! The number of bits of key used for colour (0 if no colour)
+    uint32_t n_colour_bits: 3;
     //! The number of entries in ::address_list for this entry
     uint32_t count: 16;
     //! The mask to apply to the key once shifted to get the core index
@@ -90,6 +90,21 @@ typedef struct {
     master_population_table_entry data[];
 } pop_table_config_t;
 
+//! \brief A structure to hold a response to a population table lookup
+typedef struct {
+	// Updated with the address of the row
+	synaptic_row_t row_address;
+
+	// Updated with the number of bytes to read
+    size_t n_bytes_to_transfer;
+
+    // Updated with the spike colour
+    uint32_t colour;
+
+    // Updated with the spike colour mask
+    uint32_t colour_mask;
+} pop_table_lookup_result_t;
+
 //! \name Support functions
 //! \{
 
@@ -158,23 +173,22 @@ static inline uint32_t get_local_neuron_id(
 //! \param[in] item The address list item
 //! \param[in] synaptic_rows_based_address The address of all synaptic rows
 //! \param[in] neuron_id The incoming neuron to get the address of
-//! \param[out] row_address Holder of the row address on return
-//! \param[out] n_bytes_to_transfer Holder of the bytes to transfer on return
+//! \param[out] result The place to write results to
 static inline void get_row_addr_and_size(address_list_entry item,
 		uint32_t synaptic_rows_base_address, uint32_t neuron_id,
-		synaptic_row_t *row_address, uint32_t *n_bytes_to_transfer) {
+		pop_table_lookup_result_t *result) {
 	uint32_t row_length = get_row_length(item);
 	uint32_t block_address = get_address(item, synaptic_rows_base_address);
 	uint32_t stride = (row_length + N_SYNAPSE_ROW_HEADER_WORDS);
 	uint32_t neuron_offset = neuron_id * stride * sizeof(uint32_t);
 
-	*row_address = (synaptic_row_t) (block_address + neuron_offset);
-	*n_bytes_to_transfer = stride * sizeof(uint32_t);
+	result->row_address = (synaptic_row_t) (block_address + neuron_offset);
+	result->n_bytes_to_transfer = stride * sizeof(uint32_t);
 
     log_debug("neuron_id = %u, block_address = 0x%.8x, "
             "row_length = %u, row_address = 0x%.8x, n_bytes = %u",
-            neuron_id, block_address, row_length, *row_address,
-            *n_bytes_to_transfer);
+            neuron_id, block_address, row_length, result->row_address,
+            result->n_bytes_to_transfer);
 }
 
 //! \brief the number of times a DMA resulted in 0 entries
@@ -225,12 +239,10 @@ bool population_table_load_bitfields(filter_region_t *filter_region);
 
 //! \brief Get the first row data for the given input spike
 //! \param[in] spike: The spike received
-//! \param[out] row_address: Updated with the address of the row
-//! \param[out] n_bytes_to_transfer: Updated with the number of bytes to read
+//! \param[out] result: Updated with the lookup details
 //! \return True if there is a row to read, False if not
 bool population_table_get_first_address(
-        spike_t spike, synaptic_row_t* row_address,
-        size_t* n_bytes_to_transfer);
+        spike_t spike, pop_table_lookup_result_t *result);
 
 //! \brief Determine if there are more items with the same key
 //! \return Whether there are more items
@@ -241,11 +253,9 @@ static inline bool population_table_is_next(void) {
 //! \brief Get the next row data for a previously given spike.  If no spike has
 //!        been given, return False.
 //! \param[out] spike: The initiating spike
-//! \param[out] row_address: Updated with the address of the row
-//! \param[out] n_bytes_to_transfer: Updated with the number of bytes to read
+//! \param[out] result: Updated with the lookup details
 //! \return True if there is a row to read, False if not
 bool population_table_get_next_address(
-        spike_t *spike, synaptic_row_t* row_address,
-        size_t* n_bytes_to_transfer);
+        spike_t *spike, pop_table_lookup_result_t *result);
 
 #endif // _POPULATION_TABLE_H_
diff --git a/neural_modelling/src/neuron/population_table/population_table_binary_search_impl.c b/neural_modelling/src/neuron/population_table/population_table_binary_search_impl.c
index c8d8085075..7ffccd8cc1 100644
--- a/neural_modelling/src/neuron/population_table/population_table_binary_search_impl.c
+++ b/neural_modelling/src/neuron/population_table/population_table_binary_search_impl.c
@@ -38,6 +38,12 @@ static uint32_t synaptic_rows_base_address;
 //! \brief The last spike received
 static spike_t last_spike = 0;
 
+//! \brief The last colour received
+static uint32_t last_colour = 0;
+
+//! \brief The last colour mask used
+static uint32_t last_colour_mask = 0;
+
 //! \brief The last neuron id for the key
 static uint32_t last_neuron_id = 0;
 
@@ -75,10 +81,8 @@ static inline void print_master_population_table(void) {
         log_info("key: 0x%08x, mask: 0x%08x", entry.key, entry.mask);
         int count = entry.count;
         int start = entry.start;
-        if (entry.extra_info_flag) {
-            log_info("    core_mask: 0x%08x, core_shift: %u, n_neurons: %u, n_words: %u",
-                    entry.core_mask, entry.mask_shift, entry.n_neurons, entry.n_words);
-        }
+		log_info("    core_mask: 0x%08x, core_shift: %u, n_neurons: %u, n_words: %u, n_colour_bits: %u",
+				entry.core_mask, entry.mask_shift, entry.n_neurons, entry.n_words, entry.n_colour_bits);
         for (uint16_t j = start; j < (start + count); j++) {
             address_list_entry addr = address_list[j];
             if (addr.address == INVALID_ADDRESS) {
@@ -119,7 +123,6 @@ bool population_table_load_bitfields(filter_region_t *filter_region) {
     }
     // No filters = nothing to load
     if (filter_region->n_filters == 0) {
-        log_info("No bitfields detected!");
         return true;
     }
     // try allocating DTCM for starting array for bitfields
@@ -160,13 +163,6 @@ bool population_table_load_bitfields(filter_region_t *filter_region) {
              uint32_t size = sizeof(bit_field_t) * n_words;
              connectivity_bit_field[mp_i] = spin1_malloc(size);
              if (connectivity_bit_field[mp_i] == NULL) {
-                 // If allocation fails, we can still continue
-                 log_info(
-                         "Could not initialise bit field for key %d, packets with "
-                         "that key will use a DMA to check if the packet targets "
-                         "anything within this core. Potentially slowing down the "
-                         "execution of neurons on this core.",
-                         master_population_table[mp_i].key);
                  // There might be more than one that has failed
                  failed_bit_field_reads += 1;
              } else {
@@ -266,9 +262,7 @@ bool population_table_initialise(
     return true;
 }
 
-bool population_table_get_first_address(
-        spike_t spike, synaptic_row_t *row_address,
-        size_t *n_bytes_to_transfer) {
+bool population_table_get_first_address(spike_t spike, pop_table_lookup_result_t *result) {
 
     // check we don't have a complete miss
     uint32_t position;
@@ -282,12 +276,16 @@ bool population_table_get_first_address(
     last_spike = spike;
     next_item = entry.start;
     items_to_go = entry.count;
-    if (entry.extra_info_flag) {
-        uint32_t local_neuron_id = get_local_neuron_id(entry, spike);
-        last_neuron_id = local_neuron_id + get_core_sum(entry, spike);
-    } else {
-        last_neuron_id = get_neuron_id(entry, spike);
-    }
+	uint32_t local_neuron_id = get_local_neuron_id(entry, spike);
+	if (entry.n_colour_bits) {
+		last_colour_mask = (1 << entry.n_colour_bits) - 1;
+	    last_colour = local_neuron_id & last_colour_mask;
+	    last_neuron_id = (local_neuron_id >> entry.n_colour_bits) + get_core_sum(entry, spike);
+	} else {
+		last_colour = 0;
+		last_colour_mask = 0;
+		last_neuron_id = local_neuron_id + get_core_sum(entry, spike);
+	}
 
     // check we have a entry in the bit field for this (possible not to due to
     // DTCM limitations or router table compression). If not, go to DMA check.
@@ -306,8 +304,7 @@ bool population_table_get_first_address(
     // A local address is used here as the interface requires something
     // to be passed in but using the address of an argument is odd!
     uint32_t local_spike_id;
-    bool get_next = population_table_get_next_address(
-            &local_spike_id, row_address, n_bytes_to_transfer);
+    bool get_next = population_table_get_next_address(&local_spike_id, result);
 
     // tracks surplus DMAs
     if (!get_next) {
@@ -316,9 +313,7 @@ bool population_table_get_first_address(
     return get_next;
 }
 
-bool population_table_get_next_address(
-        spike_t *spike, synaptic_row_t *row_address,
-        size_t *n_bytes_to_transfer) {
+bool population_table_get_next_address(spike_t *spike, pop_table_lookup_result_t *result) {
     // If there are no more items in the list, return false
     if (items_to_go == 0) {
         return false;
@@ -330,8 +325,10 @@ bool population_table_get_next_address(
         if (item.address != INVALID_ADDRESS) {
 
         	get_row_addr_and_size(item, synaptic_rows_base_address,
-        			last_neuron_id, row_address, n_bytes_to_transfer);
+        			last_neuron_id, result);
             *spike = last_spike;
+            result->colour = last_colour;
+            result->colour_mask = last_colour_mask;
             is_valid = true;
         }
 
diff --git a/neural_modelling/src/neuron/send_spike.h b/neural_modelling/src/neuron/send_spike.h
index 75541d5276..6406829cf5 100644
--- a/neural_modelling/src/neuron/send_spike.h
+++ b/neural_modelling/src/neuron/send_spike.h
@@ -32,6 +32,9 @@ extern uint32_t earliest_send_time;
 //! Latest time from neuron.c
 extern uint32_t latest_send_time;
 
+//! The time step colour to account for delay
+extern uint32_t colour;
+
 //! \brief Performs the sending of a spike.  Inlined for speed.
 //! \param[in] timer_count The global timer count when the time step started
 //! \param[in] time The current time step
@@ -42,7 +45,7 @@ static inline void send_spike(UNUSED uint32_t timer_count, uint32_t time,
     synapse_dynamics_process_post_synaptic_event(time, neuron_index);
 
     if (use_key) {
-        send_spike_mc(neuron_keys[neuron_index]);
+        send_spike_mc(neuron_keys[neuron_index] | colour);
 
         // Keep track of provenance data
         uint32_t clocks = tc[T1_COUNT];
diff --git a/neural_modelling/src/neuron/spike_processing.c b/neural_modelling/src/neuron/spike_processing.c
index 7eb9cca317..654427c299 100644
--- a/neural_modelling/src/neuron/spike_processing.c
+++ b/neural_modelling/src/neuron/spike_processing.c
@@ -40,6 +40,12 @@ typedef struct dma_buffer {
     //! Number of bytes transferred in the read
     uint32_t n_bytes_transferred;
 
+    //! Spike colour
+    uint32_t colour;
+
+    //! Spike colour mask
+    uint32_t colour_mask;
+
     //! Row data
     synaptic_row_t row;
 } dma_buffer;
@@ -115,40 +121,39 @@ static uint32_t p_per_ts_region;
 /* PRIVATE FUNCTIONS - static for inlining */
 
 //! \brief Perform a DMA read of a synaptic row
-//! \param[in] row: Where in SDRAM to read the row from
-//! \param[in] n_bytes_to_transfer: The size of the synaptic row
 //! \param[in] spike: The spike that triggered this read
+//! \param[in] result: The result of the lookup
 static inline void do_dma_read(
-        synaptic_row_t row, size_t n_bytes_to_transfer, spike_t spike) {
+        spike_t spike, pop_table_lookup_result_t *result) {
     // Write the SDRAM address of the plastic region and the
     // Key of the originating spike to the beginning of DMA buffer
     dma_buffer *next_buffer = &dma_buffers[next_buffer_to_fill];
-    next_buffer->sdram_writeback_address = row;
+    next_buffer->sdram_writeback_address = result->row_address;
     next_buffer->originating_spike = spike;
-    next_buffer->n_bytes_transferred = n_bytes_to_transfer;
+    next_buffer->n_bytes_transferred = result->n_bytes_to_transfer;
+    next_buffer->colour = result->colour;
+    next_buffer->colour_mask = result->colour_mask;
 
     // Start a DMA transfer to fetch this synaptic row into current
     // buffer
     buffer_being_read = next_buffer_to_fill;
     while (!spin1_dma_transfer(
-            DMA_TAG_READ_SYNAPTIC_ROW, row, next_buffer->row, DMA_READ,
-            n_bytes_to_transfer)) {
+            DMA_TAG_READ_SYNAPTIC_ROW, result->row_address, next_buffer->row, DMA_READ,
+            result->n_bytes_to_transfer)) {
         // Do Nothing
     }
     next_buffer_to_fill = (next_buffer_to_fill + 1) % N_DMA_BUFFERS;
 }
 
 //! \brief Check if there is anything to do. If not, DMA is not busy
-//! \param[out] row:
-//!     The address of the synaptic row that has been processed
-//! \param[out] n_bytes_to_transfer: The size of the processed synaptic row
 //! \param[out] spike: The spike being processed
+//! \param[out] result: The result of the pop table lookup
 //! \param[in,out] n_rewire: Accumulator of number of rewirings
 //! \param[in,out] n_process_spike: Accumulator of number of processed spikes
 //! \return True if there's something to do
 static inline bool is_something_to_do(
-        synaptic_row_t *row, size_t *n_bytes_to_transfer,
-        spike_t *spike, uint32_t *n_rewire, uint32_t *n_process_spike) {
+        spike_t *spike, pop_table_lookup_result_t *result, uint32_t *n_rewire,
+		uint32_t *n_process_spike) {
     // Disable interrupts here as dma_busy modification is a critical section
     uint cpsr = spin1_int_disable();
 
@@ -156,8 +161,7 @@ static inline bool is_something_to_do(
     while (rewires_to_do) {
         rewires_to_do--;
         spin1_mode_restore(cpsr);
-        if (synaptogenesis_dynamics_rewire(time, spike, row,
-                n_bytes_to_transfer)) {
+        if (synaptogenesis_dynamics_rewire(time, spike, result)) {
             *n_rewire += 1;
             return true;
         }
@@ -166,7 +170,7 @@ static inline bool is_something_to_do(
 
     // Is there another address in the population table?
     spin1_mode_restore(cpsr);
-    if (population_table_get_next_address(spike, row, n_bytes_to_transfer)) {
+    if (population_table_get_next_address(spike, result)) {
         *n_process_spike += 1;
         return true;
     }
@@ -183,8 +187,7 @@ static inline bool is_something_to_do(
         // Enable interrupts while looking up in the master pop table,
         // as this can be slow
         spin1_mode_restore(cpsr);
-        if (population_table_get_first_address(
-                *spike, row, n_bytes_to_transfer)) {
+        if (population_table_get_first_address(*spike, result)) {
             synaptogenesis_spike_received(time, *spike);
             *n_process_spike += 1;
             return true;
@@ -218,18 +221,17 @@ static inline bool is_something_to_do(
 static bool setup_synaptic_dma_read(dma_buffer *current_buffer,
         uint32_t *n_rewires, uint32_t *n_synapse_processes) {
     // Set up to store the DMA location and size to read
-    synaptic_row_t row;
-    size_t n_bytes_to_transfer;
     spike_t spike;
+    pop_table_lookup_result_t result;
     dma_n_spikes = 0;
     dma_n_rewires = 0;
 
     // Keep looking if there is something to do until a DMA can be done
     bool setup_done = false;
-    while (!setup_done && is_something_to_do(&row, &n_bytes_to_transfer,
-            &spike, &dma_n_rewires, &dma_n_spikes)) {
+    while (!setup_done && is_something_to_do(
+            &spike, &result, &dma_n_rewires, &dma_n_spikes)) {
         if (current_buffer != NULL &&
-                current_buffer->sdram_writeback_address == row) {
+                current_buffer->sdram_writeback_address == result.row_address) {
             // If we can reuse the row, add on what we can use it for
             // Note that only one of these will have a value of 1 with the
             // other being set to 0, but we add both as it is simple
@@ -237,9 +239,11 @@ static bool setup_synaptic_dma_read(dma_buffer *current_buffer,
             *n_synapse_processes += dma_n_spikes;
             dma_n_rewires = 0;
             dma_n_spikes = 0;
+            // Although we can reuse the buffer, the colour might be different
+            current_buffer->colour = result.colour;
         } else {
             // If the row is in SDRAM, set up the transfer and we are done
-            do_dma_read(row, n_bytes_to_transfer, spike);
+            do_dma_read(spike, &result);
             setup_done = true;
         }
 
@@ -360,7 +364,8 @@ static void dma_complete_callback(UNUSED uint unused, UNUSED uint tag) {
         // it's going to be processed
         bool write_back_now = false;
         if (!synapses_process_synaptic_row(
-                time, current_buffer->row, &write_back_now)) {
+                time, current_buffer->colour, current_buffer->colour_mask,
+				current_buffer->row, &write_back_now)) {
             log_error(
                     "Error processing spike 0x%.8x for address 0x%.8x"
                     " (local=0x%.8x)",
diff --git a/neural_modelling/src/neuron/spike_processing_fast.c b/neural_modelling/src/neuron/spike_processing_fast.c
index 019c77cf35..a668ae23bc 100644
--- a/neural_modelling/src/neuron/spike_processing_fast.c
+++ b/neural_modelling/src/neuron/spike_processing_fast.c
@@ -40,6 +40,12 @@ typedef struct dma_buffer {
     //! Number of bytes transferred in the read
     uint32_t n_bytes_transferred;
 
+    //! Spike colour
+    uint32_t colour;
+
+    //! Spike colour mask
+    uint32_t colour_mask;
+
     //! Row data
     synaptic_row_t row;
 } dma_buffer;
@@ -202,13 +208,14 @@ static inline void process_end_of_time_step(uint32_t time) {
 }
 
 //! \brief Read a synaptic row from SDRAM into a local buffer.
-static inline void read_synaptic_row(spike_t spike, synaptic_row_t row,
-        uint32_t n_bytes) {
+static inline void read_synaptic_row(spike_t spike, pop_table_lookup_result_t *result) {
     dma_buffer *buffer = &dma_buffers[next_buffer_to_fill];
-    buffer->sdram_writeback_address = row;
+    buffer->sdram_writeback_address = result->row_address;
     buffer->originating_spike = spike;
-    buffer->n_bytes_transferred = n_bytes;
-    do_fast_dma_read(row, buffer->row, n_bytes);
+    buffer->n_bytes_transferred = result->n_bytes_to_transfer;
+    buffer->colour = result->colour;
+    buffer->colour_mask = result->colour_mask;
+    do_fast_dma_read(result->row_address, buffer->row, result->n_bytes_to_transfer);
     next_buffer_to_fill = (next_buffer_to_fill + 1) & DMA_BUFFER_MOD_MASK;
 }
 
@@ -229,7 +236,7 @@ static inline bool get_next_spike(uint32_t time, spike_t *spike) {
         // Process event if not self-connected (if it is, this happens later)
     	if (!key_config.self_connected) {
             synapse_dynamics_process_post_synaptic_event(
-                    time, *spike & key_config.spike_id_mask);
+                    time, (*spike & key_config.spike_id_mask) >> key_config.colour_shift);
     	}
         return key_config.self_connected;
     }
@@ -241,14 +248,14 @@ static inline bool get_next_spike(uint32_t time, spike_t *spike) {
 //! \param[in] time Simulation time step
 //! \param[in/out] spike Starts as the first spike received, but might change
 //!                      if the first spike doesn't cause a DMA
+//! \param[out] result The result of the lookup
 //! \return True if a DMA was started
-static inline bool start_first_dma(uint32_t time, spike_t *spike) {
-    synaptic_row_t row;
-    uint32_t n_bytes;
+static inline bool start_first_dma(uint32_t time, spike_t *spike,
+		pop_table_lookup_result_t *result) {
 
     do {
-        if (population_table_get_first_address(*spike, &row, &n_bytes)) {
-            read_synaptic_row(*spike, row, n_bytes);
+        if (population_table_get_first_address(*spike, result)) {
+            read_synaptic_row(*spike, result);
             return true;
         }
     } while (!is_end_of_time_step() && get_next_spike(time, spike));
@@ -259,18 +266,16 @@ static inline bool start_first_dma(uint32_t time, spike_t *spike) {
 //! \brief Get the details for the next DMA, but don't start it.
 //! \param[in] time Simulation time step
 //! \param[out] spike Pointer to receive the spike the DMA relates to
-//! \param[out] row Pointer to receive the address to be transferred
-//! \param[out] n_bytes Pointer to receive the number of bytes to transfer
+//! \param[out] result The details of the transfer to do
 //! \return True if there is a DMA to do
-static inline bool get_next_dma(uint32_t time, spike_t *spike, synaptic_row_t *row,
-        uint32_t *n_bytes) {
-    if (population_table_is_next() && population_table_get_next_address(
-            spike, row, n_bytes)) {
+static inline bool get_next_dma(uint32_t time, spike_t *spike,
+		pop_table_lookup_result_t *result) {
+    if (population_table_is_next() && population_table_get_next_address(spike, result)) {
         return true;
     }
 
     while (!is_end_of_time_step() && get_next_spike(time, spike)) {
-        if (population_table_get_first_address(*spike, row, n_bytes)) {
+        if (population_table_get_first_address(*spike, result)) {
             return true;
         }
     }
@@ -321,7 +326,8 @@ static inline void process_current_row(uint32_t time, bool dma_in_progress) {
     bool write_back = false;
     dma_buffer *buffer = &dma_buffers[next_buffer_to_process];
 
-    if (!synapses_process_synaptic_row(time, buffer->row, &write_back)) {
+    if (!synapses_process_synaptic_row(time, buffer->colour, buffer->colour_mask,
+    		buffer->row, &write_back)) {
         handle_row_error(buffer);
     }
     synaptogenesis_spike_received(time, buffer->originating_spike);
@@ -431,9 +437,7 @@ static inline bool prepare_timestep(uint32_t time) {
 //! \param[in] n_rewires The number of rewirings to try
 static inline void do_rewiring(uint32_t time, uint32_t n_rewires) {
     uint32_t spike;
-    synaptic_row_t row;
-    uint32_t n_bytes;
-
+    pop_table_lookup_result_t result;
     uint32_t current_buffer = 0;
     uint32_t next_buffer = 0;
     bool dma_in_progress = false;
@@ -441,10 +445,11 @@ static inline void do_rewiring(uint32_t time, uint32_t n_rewires) {
     // Start the first transfer
     uint32_t rewires_to_go = n_rewires;
     while (rewires_to_go > 0 && !dma_in_progress) {
-        if (synaptogenesis_dynamics_rewire(time, &spike, &row, &n_bytes)) {
-            dma_buffers[next_buffer].sdram_writeback_address = row;
-            dma_buffers[next_buffer].n_bytes_transferred = n_bytes;
-            do_fast_dma_read(row, dma_buffers[next_buffer].row, n_bytes);
+        if (synaptogenesis_dynamics_rewire(time, &spike, &result)) {
+            dma_buffers[next_buffer].sdram_writeback_address = result.row_address;
+            dma_buffers[next_buffer].n_bytes_transferred = result.n_bytes_to_transfer;
+            do_fast_dma_read(result.row_address, dma_buffers[next_buffer].row,
+            		result.n_bytes_to_transfer);
             next_buffer = (next_buffer + 1) & DMA_BUFFER_MOD_MASK;
             dma_in_progress = true;
         }
@@ -457,7 +462,7 @@ static inline void do_rewiring(uint32_t time, uint32_t n_rewires) {
         // Start the next DMA if possible
         dma_in_progress = false;
         while (rewires_to_go > 0 && !dma_in_progress) {
-            if (synaptogenesis_dynamics_rewire(time, &spike, &row, &n_bytes)) {
+            if (synaptogenesis_dynamics_rewire(time, &spike, &result)) {
                 dma_in_progress = true;
             }
             rewires_to_go--;
@@ -468,9 +473,10 @@ static inline void do_rewiring(uint32_t time, uint32_t n_rewires) {
 
         // Start the next DMA read
         if (dma_in_progress) {
-            dma_buffers[next_buffer].sdram_writeback_address = row;
-            dma_buffers[next_buffer].n_bytes_transferred = n_bytes;
-            do_fast_dma_read(row, dma_buffers[next_buffer].row, n_bytes);
+            dma_buffers[next_buffer].sdram_writeback_address = result.row_address;
+            dma_buffers[next_buffer].n_bytes_transferred = result.n_bytes_to_transfer;
+            do_fast_dma_read(result.row_address, dma_buffers[next_buffer].row,
+            		result.n_bytes_to_transfer);
             next_buffer = (next_buffer + 1) & DMA_BUFFER_MOD_MASK;
         }
 
@@ -525,7 +531,8 @@ void spike_processing_fast_time_step_loop(uint32_t time, uint32_t n_rewires) {
         }
 
         // There must be a spike!  Start a DMA processing loop...
-        bool dma_in_progress = start_first_dma(time, &spike);
+        pop_table_lookup_result_t result;
+        bool dma_in_progress = start_first_dma(time, &spike, &result);
         while (dma_in_progress && !is_end_of_time_step()) {
 
             // If self-connected looped back spike then process post event here
@@ -536,9 +543,7 @@ void spike_processing_fast_time_step_loop(uint32_t time, uint32_t n_rewires) {
             }
 
             // See if there is another DMA to do
-            synaptic_row_t row;
-            uint32_t n_bytes;
-            dma_in_progress = get_next_dma(time, &spike, &row, &n_bytes);
+            dma_in_progress = get_next_dma(time, &spike, &result);
 
             // Finish the current DMA before starting the next
             if (!wait_for_dma_to_complete_or_end()) {
@@ -547,7 +552,7 @@ void spike_processing_fast_time_step_loop(uint32_t time, uint32_t n_rewires) {
             }
             dma_complete_count++;
             if (dma_in_progress) {
-                read_synaptic_row(spike, row, n_bytes);
+                read_synaptic_row(spike, &result);
             }
 
             // Process the row we already have while the DMA progresses
diff --git a/neural_modelling/src/neuron/spike_processing_fast.h b/neural_modelling/src/neuron/spike_processing_fast.h
index 43048c449e..3ee096cd9a 100644
--- a/neural_modelling/src/neuron/spike_processing_fast.h
+++ b/neural_modelling/src/neuron/spike_processing_fast.h
@@ -44,6 +44,8 @@ struct key_config {
     uint32_t mask;
     //! The mask to get the spike ID
     uint32_t spike_id_mask;
+    //! The colour shift to apply after masking
+    uint32_t colour_shift;
     //! Is the node self connected
     uint32_t self_connected;
 };
diff --git a/neural_modelling/src/neuron/structural_plasticity/synaptogenesis/partner_selection/random_selection_impl.h b/neural_modelling/src/neuron/structural_plasticity/synaptogenesis/partner_selection/random_selection_impl.h
index 817de88d69..08c86c89c6 100644
--- a/neural_modelling/src/neuron/structural_plasticity/synaptogenesis/partner_selection/random_selection_impl.h
+++ b/neural_modelling/src/neuron/structural_plasticity/synaptogenesis/partner_selection/random_selection_impl.h
@@ -67,14 +67,14 @@ static inline bool potential_presynaptic_partner(
         }
     }
     *sub_population_id = subpop_id;
+    key_atom_info_t *kai = &preapppop_info->key_atom_info[subpop_id];
 
     // Select a presynaptic neuron ID
-    n_id = ulrbits(mars_kiss64_seed(rewiring_data.local_seed)) *
-            preapppop_info->key_atom_info[subpop_id].n_atoms;
+    n_id = ulrbits(mars_kiss64_seed(rewiring_data.local_seed)) * kai->n_atoms;
 
     *neuron_id = n_id;
-    *spike = preapppop_info->key_atom_info[subpop_id].key | n_id;
-    *m_pop_index = preapppop_info->key_atom_info[subpop_id].m_pop_index;
+    *spike = kai->key | (n_id << kai->n_colour_bits);
+    *m_pop_index = kai->m_pop_index;
     return true;
 }
 
diff --git a/neural_modelling/src/neuron/structural_plasticity/synaptogenesis/sp_structs.h b/neural_modelling/src/neuron/structural_plasticity/synaptogenesis/sp_structs.h
index f83ba0e3ab..c07e295a49 100644
--- a/neural_modelling/src/neuron/structural_plasticity/synaptogenesis/sp_structs.h
+++ b/neural_modelling/src/neuron/structural_plasticity/synaptogenesis/sp_structs.h
@@ -49,6 +49,7 @@ typedef struct post_to_pre_entry {
 typedef struct {
     uint32_t key;
     uint32_t mask;
+    uint32_t n_colour_bits;
     uint32_t n_atoms;
     uint32_t lo_atom;
     uint32_t m_pop_index;
@@ -149,7 +150,7 @@ static inline bool sp_structs_find_by_spike(
             if ((spike & kai->mask) == kai->key) {
                 *population_id = i;
                 *sub_population_id = j;
-                *neuron_id = spike & ~kai->mask;
+                *neuron_id = (spike & ~kai->mask) >> kai->n_colour_bits;
                 *m_pop_index = kai->m_pop_index;
                 return true;
             }
@@ -176,11 +177,12 @@ static inline bool sp_structs_get_sub_pop_info(
             pre_pop_info_table->prepop_info[population_id];
     uint32_t neuron_id = pop_neuron_id;
     for (uint32_t i = 0; i < app_pop_info->no_pre_vertices; i++) {
-        uint32_t n_atoms = app_pop_info->key_atom_info[i].n_atoms;
+    	key_atom_info_t *kai = &app_pop_info->key_atom_info[i];
+        uint32_t n_atoms = kai->n_atoms;
         if (neuron_id < n_atoms) {
             *sub_population_id = i;
             *sub_pop_neuron_id = neuron_id;
-            *spike = app_pop_info->key_atom_info[i].key | neuron_id;
+            *spike = kai->key | (neuron_id << kai->n_colour_bits);
             return true;
         }
         neuron_id -= n_atoms;
diff --git a/neural_modelling/src/neuron/structural_plasticity/synaptogenesis/topographic_map_impl.c b/neural_modelling/src/neuron/structural_plasticity/synaptogenesis/topographic_map_impl.c
index 75aa6458ac..6dcbe04df3 100644
--- a/neural_modelling/src/neuron/structural_plasticity/synaptogenesis/topographic_map_impl.c
+++ b/neural_modelling/src/neuron/structural_plasticity/synaptogenesis/topographic_map_impl.c
@@ -219,8 +219,7 @@ bool synaptogenesis_dynamics_initialise(
 }
 
 bool synaptogenesis_dynamics_rewire(
-        uint32_t time, spike_t *spike, synaptic_row_t *synaptic_row,
-        uint32_t *n_bytes) {
+        uint32_t time, spike_t *spike, pop_table_lookup_result_t *result) {
 
     // Randomly choose a postsynaptic (application neuron)
     uint32_t post_id = rand_int(rewiring_data.app_no_atoms,
@@ -253,17 +252,17 @@ bool synaptogenesis_dynamics_rewire(
     pre_info_t *prepop_info = pre_info.prepop_info[pre_app_pop];
     key_atom_info_t *key_atom_info = &prepop_info->key_atom_info[pre_sub_pop];
     if (entry.neuron_index != 0xFFFF) {
-        *spike = key_atom_info->key | neuron_id;
+        *spike = key_atom_info->key | (neuron_id << key_atom_info->n_colour_bits);
         m_pop_index = key_atom_info->m_pop_index;
     }
 
-    if (!population_table_get_first_address(*spike, synaptic_row, n_bytes)) {
+    if (!population_table_get_first_address(*spike, result)) {
         log_error("FAIL@key %d", *spike);
         rt_error(RTE_SWERR);
     }
     uint32_t index = 0;
     while (index < m_pop_index) {
-        if (!population_table_get_next_address(spike, synaptic_row, n_bytes)) {
+        if (!population_table_get_next_address(spike, result)) {
             log_error("FAIL@key %d, index %d (failed at %d)",
                     *spike, m_pop_index, index);
             rt_error(RTE_SWERR);
diff --git a/neural_modelling/src/neuron/structural_plasticity/synaptogenesis_dynamics.h b/neural_modelling/src/neuron/structural_plasticity/synaptogenesis_dynamics.h
index 9aa18f244c..32222acf75 100644
--- a/neural_modelling/src/neuron/structural_plasticity/synaptogenesis_dynamics.h
+++ b/neural_modelling/src/neuron/structural_plasticity/synaptogenesis_dynamics.h
@@ -26,6 +26,7 @@
 #define _SYNAPTOGENESIS_DYNAMICS_H_
 
 #include <common/neuron-typedefs.h>
+#include <neuron/population_table/population_table.h>
 
 //! \brief Initialisation of synaptic rewiring (synaptogenesis)
 //!     parameters (random seed, spread of receptive field etc.)
@@ -41,11 +42,10 @@ bool synaptogenesis_dynamics_initialise(
 //! \details Usually called on a timer registered in c_main()
 //! \param[in] time: the current timestep
 //! \param[out] spike: variable to hold the spike
-//! \param[out] synaptic_row: variable to hold the address of the row
-//! \param[out] n_bytes: variable to hold the size of the row
+//! \param[out] result: The result of the population table lookup
 //! \return True if a row is to be transferred, false otherwise
 bool synaptogenesis_dynamics_rewire(uint32_t time,
-        spike_t *spike, synaptic_row_t *synaptic_row, uint32_t *n_bytes);
+        spike_t *spike, pop_table_lookup_result_t *result);
 
 //! \brief Perform the actual restructuring of a row
 //! \param[in] time: The time of the restructure
diff --git a/neural_modelling/src/neuron/structural_plasticity/synaptogenesis_dynamics_static_impl.c b/neural_modelling/src/neuron/structural_plasticity/synaptogenesis_dynamics_static_impl.c
index 78bb72518a..4dc7feb03e 100644
--- a/neural_modelling/src/neuron/structural_plasticity/synaptogenesis_dynamics_static_impl.c
+++ b/neural_modelling/src/neuron/structural_plasticity/synaptogenesis_dynamics_static_impl.c
@@ -33,7 +33,7 @@ bool synaptogenesis_dynamics_initialise(
 
 bool synaptogenesis_dynamics_rewire(
         UNUSED uint32_t time, UNUSED spike_t *spike,
-        UNUSED synaptic_row_t *synaptic_row, UNUSED uint32_t *n_bytes) {
+        UNUSED pop_table_lookup_result_t *result) {
     return false;
 }
 
diff --git a/neural_modelling/src/neuron/synapses.c b/neural_modelling/src/neuron/synapses.c
index 137c7dc001..92f64942c0 100644
--- a/neural_modelling/src/neuron/synapses.c
+++ b/neural_modelling/src/neuron/synapses.c
@@ -80,9 +80,22 @@ uint32_t synapse_delay_mask;
 //! Count of the number of times the ring buffers have saturated
 uint32_t synapses_saturation_count = 0;
 
+//! Count of the synapses that have been skipped because the delay wasn't
+//! big enough given how long the spike took to arrive
+uint32_t skipped_synapses = 0;
+
+//! Count of the spikes that are received late
+uint32_t late_spikes = 0;
+
+//! The maximum lateness of a spike
+uint32_t max_late_spike = 0;
+
 //! Number of neurons
 static uint32_t n_neurons_peak;
 
+//! The mask of the delay shifted into position i.e. pre-shift
+static uint32_t synapse_delay_mask_shifted = 0;
+
 
 /* PRIVATE FUNCTIONS */
 
@@ -192,20 +205,28 @@ static inline void print_ring_buffers(uint32_t time) {
 //! \param[in] time: The current simulation time
 //! \return Always true
 static inline bool process_fixed_synapses(
-        synapse_row_fixed_part_t *fixed_region, uint32_t time) {
+        synapse_row_fixed_part_t *fixed_region, uint32_t time,
+        uint32_t colour_delay) {
     uint32_t *synaptic_words = synapse_row_fixed_weight_controls(fixed_region);
     uint32_t fixed_synapse = synapse_row_num_fixed_synapses(fixed_region);
 
     num_fixed_pre_synaptic_events += fixed_synapse;
 
-    // Pre-mask the time
-    uint32_t masked_time = (time & synapse_delay_mask) << synapse_type_index_bits;
+    // Pre-mask the time and account for colour delay
+    uint32_t colour_delay_shifted = colour_delay << synapse_type_index_bits;
+    uint32_t masked_time = ((time - colour_delay) & synapse_delay_mask) << synapse_type_index_bits;
 
     for (; fixed_synapse > 0; fixed_synapse--) {
         // Get the next 32 bit word from the synaptic_row
         // (should auto increment pointer in single instruction)
         uint32_t synaptic_word = *synaptic_words++;
 
+        // If the delay is too small, skip
+        if ((synaptic_word & synapse_delay_mask_shifted) < colour_delay_shifted) {
+            skipped_synapses++;
+            continue;
+        }
+
         // The ring buffer index can be found by adding on the time to the delay
         // in the synaptic word directly, and then masking off the whole index.
         // The addition of the masked time to the delay even with the mask might
@@ -287,6 +308,7 @@ bool synapses_initialise(
     synapse_type_mask = (1 << log_n_synapse_types) - 1;
     synapse_delay_bits = log_max_delay;
     synapse_delay_mask = (1 << synapse_delay_bits) - 1;
+    synapse_delay_mask_shifted = synapse_delay_mask << synapse_type_index_bits;
 
     n_neurons_peak = 1 << log_n_neurons;
 
@@ -328,7 +350,25 @@ void synapses_flush_ring_buffers(timer_t time) {
 }
 
 bool synapses_process_synaptic_row(
-        uint32_t time, synaptic_row_t row, bool *write_back) {
+        uint32_t time, uint32_t spike_colour, uint32_t colour_mask,
+		synaptic_row_t row, bool *write_back) {
+
+    // Work out how much delay takes off or adds on to the actual delay because
+    // of a delayed spike arrival time, or delayed change of time step in the
+    // current core.  Spikes can be as late as the bits in colour_mask dictates.
+	// Masked difference is used to calculate this, which will always be
+	// positive because the mask removes the negative bit.
+	// Example: time colour 8, spike colour 13, colour mask 0xF means time
+	// colour has gone up to 15 and then wrapped since spike was sent.
+    // 8 - 13 = -5; -5 & 0xF = 11, so spike was sent 11 steps ago.
+    uint32_t time_colour = time & colour_mask;
+    int32_t colour_diff = time_colour - spike_colour;
+    uint32_t colour_delay = colour_diff & colour_mask;
+
+    late_spikes += colour_delay & 0x1;
+    if (colour_delay > max_late_spike) {
+        max_late_spike = colour_delay;
+    }
 
     // By default don't write back
     *write_back = false;
@@ -348,7 +388,7 @@ bool synapses_process_synaptic_row(
         profiler_write_entry_disable_fiq(
                 PROFILER_ENTER | PROFILER_PROCESS_PLASTIC_SYNAPSES);
         if (!synapse_dynamics_process_plastic_synapses(plastic_data,
-                fixed_region, ring_buffers, time, write_back)) {
+                fixed_region, ring_buffers, time, colour_delay, write_back)) {
             return false;
         }
         profiler_write_entry_disable_fiq(
@@ -359,7 +399,7 @@ bool synapses_process_synaptic_row(
     // **NOTE** this is done after initiating DMA in an attempt
     // to hide cost of DMA behind this loop to improve the chance
     // that the DMA controller is ready to read next synaptic row afterwards
-    return process_fixed_synapses(fixed_region, time);
+    return process_fixed_synapses(fixed_region, time, colour_delay);
     //}
 }
 
diff --git a/neural_modelling/src/neuron/synapses.h b/neural_modelling/src/neuron/synapses.h
index 5408c583e6..fd83b01b56 100644
--- a/neural_modelling/src/neuron/synapses.h
+++ b/neural_modelling/src/neuron/synapses.h
@@ -52,6 +52,16 @@ extern uint32_t synapse_delay_mask;
 //! Count of the number of times the synapses have saturated their weights.
 extern uint32_t synapses_saturation_count;
 
+//! Count of the synapses that have been skipped because the delay wasn't
+//! big enough given how long the spike took to arrive
+extern uint32_t skipped_synapses;
+
+//! Count of the spikes that are received late
+extern uint32_t late_spikes;
+
+//! The maximum lateness of a spike
+extern uint32_t max_late_spike;
+
 
 //! \brief Print the weight of a synapse
 //! \param[in] weight: the weight to print in synapse-row form
@@ -89,11 +99,14 @@ bool synapses_initialise(
 
 //! \brief process a synaptic row
 //! \param[in] time: the simulated time
+//! \param[in] spike_colour: the colour extracted from the spike key
+//! \param[in] colour_mask: the colour mask extracted from the pop table
 //! \param[in] row: the synaptic row in question
 //! \param[out] write_back: whether to write back to SDRAM
 //! \return True if successful
 bool synapses_process_synaptic_row(
-        uint32_t time, synaptic_row_t row, bool *write_back);
+        uint32_t time, uint32_t spike_colour, uint32_t colour_mask,
+		synaptic_row_t row, bool *write_back);
 
 //! \brief returns the counters for plastic and fixed pre synaptic events based
 //!        on (if the model was compiled with SYNAPSE_BENCHMARK parameter) or
diff --git a/neural_modelling/src/spike_source/poisson/spike_source_poisson.c b/neural_modelling/src/spike_source/poisson/spike_source_poisson.c
index 4f2ebbd772..4199a106c9 100644
--- a/neural_modelling/src/spike_source/poisson/spike_source_poisson.c
+++ b/neural_modelling/src/spike_source/poisson/spike_source_poisson.c
@@ -137,6 +137,8 @@ typedef struct global_parameters {
     uint32_t n_spike_sources;
     //! Maximum expected spikes per tick (for recording)
     uint32_t max_spikes_per_tick;
+    //! Number of bits to use for colour
+    uint32_t n_colour_bits;
     //! The seed for the Poisson generation process
     rng_seed_t spike_source_seed;
 } global_parameters;
@@ -244,8 +246,15 @@ static uint16_t *input_this_timestep;
 //! The timesteps per second
 static UREAL ts_per_second;
 
+//! Buffer for rate change packets
 static circular_buffer rate_change_buffer;
 
+//! The colour of the current time step
+static uint32_t colour;
+
+//! The mask to apply to the time to get the colour
+static uint32_t colour_mask;
+
 //! \brief Random number generation for the Poisson sources.
 //!        This is a local version for speed of operation.
 //! \return A random number
@@ -490,6 +499,8 @@ static bool read_global_parameters(global_parameters *sdram_globals) {
     }
     spin1_memcpy(keys, &(sdram_globals[1]), keys_size);
 
+    colour_mask = (1 << ssp_params.n_colour_bits) - 1;
+
     log_info("\tset rate mask = %08x",
             ssp_params.set_rate_neuron_id_mask);
     log_info("\tseed = %u %u %u %u", ssp_params.spike_source_seed.c,
@@ -870,7 +881,7 @@ static void process_fast_source(index_t s_id, spike_source_t *source) {
             // If no key has been given, do not send spikes to fabric
             if (ssp_params.has_key) {
                 // Send spikes
-                const uint32_t spike_key = keys[s_id];
+                const uint32_t spike_key = keys[s_id] | colour;
                 send_spike_mc_payload(spike_key, num_spikes);
             } else if (sdram_inputs->address != 0) {
                 input_this_timestep[sdram_inputs->offset + s_id] +=
@@ -907,7 +918,8 @@ static void process_slow_source(index_t s_id, spike_source_t *source) {
             // if no key has been given, do not send spike to fabric.
             if (ssp_params.has_key) {
                 // Send package
-                send_spike_mc_payload(keys[s_id], count);
+                const uint32_t spike_key = keys[s_id] | colour;
+                send_spike_mc_payload(spike_key, count);
             } else if (sdram_inputs->address != 0) {
                 input_this_timestep[sdram_inputs->offset + s_id] +=
                     sdram_inputs->weights[s_id] * count;
@@ -954,6 +966,9 @@ static void timer_callback(UNUSED uint timer_count, UNUSED uint unused) {
         return;
     }
 
+    // Set the colour for the time step
+    colour = time & colour_mask;
+
     // Do any rate changes
     while (circular_buffer_size(rate_change_buffer) >= 2) {
     	uint32_t id = 0;
diff --git a/neural_modelling/src/synapse_expander/bit_field_expander.h b/neural_modelling/src/synapse_expander/bit_field_expander.h
index 1ca69a6852..c75b63c5c2 100644
--- a/neural_modelling/src/synapse_expander/bit_field_expander.h
+++ b/neural_modelling/src/synapse_expander/bit_field_expander.h
@@ -37,19 +37,18 @@ static address_list_entry *address_list;
 //!     how many bytes to read to get the synaptic row
 //! \return Whether there is target
 static bool do_sdram_read_and_test(
-        synaptic_row_t row_data, synaptic_row_t row,
-        uint32_t n_bytes_to_transfer) {
-    spin1_memcpy(row_data, row, n_bytes_to_transfer);
+        synaptic_row_t row_data, pop_table_lookup_result_t *result) {
+    spin1_memcpy(row_data, result->row_address, result->n_bytes_to_transfer);
     log_debug("Process synaptic row");
 
     // get address of plastic region from row
-    if (synapse_row_plastic_size(row) > 0) {
+    if (synapse_row_plastic_size(row_data) > 0) {
         log_debug("Plastic row had entries, so cant be pruned");
         return true;
     }
 
     // Get address of non-plastic region from row
-    synapse_row_fixed_part_t *fixed_region = synapse_row_fixed_region(row);
+    synapse_row_fixed_part_t *fixed_region = synapse_row_fixed_region(row_data);
     uint32_t fixed_synapse = synapse_row_num_fixed_synapses(fixed_region);
     if (fixed_synapse == 0) {
         log_debug("Plastic and fixed do not have entries, so can be pruned");
@@ -136,13 +135,12 @@ static inline bool generate_bit_field(filter_region_t *bitfield_filters,
 						continue;
 					}
 
-					synaptic_row_t row;
-					uint32_t n_bytes_to_transfer;
+					pop_table_lookup_result_t result;
 					get_row_addr_and_size(entry, (uint32_t) synaptic_matrix,
-					        n, &row, &n_bytes_to_transfer);
+					        n, &result);
 
 					// Check if the row is non-empty and if so set a bit
-					if (do_sdram_read_and_test(row_data, row, n_bytes_to_transfer)) {
+					if (do_sdram_read_and_test(row_data, &result)) {
 						bit_field_set(bit_field, n);
 					}
 				}
diff --git a/spynnaker/pyNN/external_devices_models/external_device_lif_control.py b/spynnaker/pyNN/external_devices_models/external_device_lif_control.py
index f95564b685..663435d85d 100644
--- a/spynnaker/pyNN/external_devices_models/external_device_lif_control.py
+++ b/spynnaker/pyNN/external_devices_models/external_device_lif_control.py
@@ -97,7 +97,8 @@ def __init__(
     def create_vertex(
             self, n_neurons, label, spikes_per_second,
             ring_buffer_sigma, incoming_spike_buffer_size,
-            n_steps_per_timestep, drop_late_spikes, splitter, seed):
+            n_steps_per_timestep, drop_late_spikes, splitter, seed,
+            n_colour_bits):
         if n_neurons != len(self._devices):
             raise ConfigurationException(
                 "Number of neurons does not match number of devices in {}"
@@ -107,4 +108,5 @@ def create_vertex(
         return ExternalDeviceLifControlVertex(
             self._devices, self._create_edges, max_atoms, self._model, self,
             self._translator, spikes_per_second, label, ring_buffer_sigma,
-            incoming_spike_buffer_size, drop_late_spikes, splitter, seed)
+            incoming_spike_buffer_size, drop_late_spikes, splitter, seed,
+            n_colour_bits)
diff --git a/spynnaker/pyNN/external_devices_models/external_device_lif_control_vertex.py b/spynnaker/pyNN/external_devices_models/external_device_lif_control_vertex.py
index ca74e51100..75b4539b0c 100644
--- a/spynnaker/pyNN/external_devices_models/external_device_lif_control_vertex.py
+++ b/spynnaker/pyNN/external_devices_models/external_device_lif_control_vertex.py
@@ -44,7 +44,8 @@ def __init__(
             self, devices, create_edges, max_atoms_per_core, neuron_impl,
             pynn_model, translator=None, spikes_per_second=None, label=None,
             ring_buffer_sigma=None, incoming_spike_buffer_size=None,
-            drop_late_spikes=None, splitter=None, seed=None):
+            drop_late_spikes=None, splitter=None, seed=None,
+            n_colour_bits=None):
         """
         :param list(AbstractMulticastControllableDevice) devices:
             The AbstractMulticastControllableDevice instances to be controlled
@@ -66,12 +67,14 @@ def __init__(
         :param splitter: splitter from app to machine
         :type splitter: None or
             ~pacman.model.partitioner_splitters.abstract_splitters.AbstractSplitterCommon
+        :param int n_colour_bits: The number of colour bits to use
         """
         # pylint: disable=too-many-arguments
         super().__init__(
             len(devices), label, max_atoms_per_core,
             spikes_per_second, ring_buffer_sigma, incoming_spike_buffer_size,
-            neuron_impl, pynn_model, drop_late_spikes, splitter, seed)
+            neuron_impl, pynn_model, drop_late_spikes, splitter, seed,
+            n_colour_bits)
 
         if not devices:
             raise ConfigurationException("No devices specified")
diff --git a/spynnaker/pyNN/extra_algorithms/delay_support_adder.py b/spynnaker/pyNN/extra_algorithms/delay_support_adder.py
index 310f642cce..baf7380e82 100644
--- a/spynnaker/pyNN/extra_algorithms/delay_support_adder.py
+++ b/spynnaker/pyNN/extra_algorithms/delay_support_adder.py
@@ -145,7 +145,7 @@ def _create_delay_app_vertex_and_pre_edge(
             delay_name = "{}_delayed".format(app_edge.pre_vertex.label)
             delay_app_vertex = DelayExtensionVertex(
                 app_outgoing_edge_partition, delay_per_stage, n_delay_stages,
-                label=delay_name)
+                app_edge.pre_vertex.n_colour_bits, label=delay_name)
 
             # set trackers
             delay_app_vertex.splitter = SplitterDelayVertexSlice()
diff --git a/spynnaker/pyNN/models/common/eieio_spike_recorder.py b/spynnaker/pyNN/models/common/eieio_spike_recorder.py
index 863926bc6f..46a229cb20 100644
--- a/spynnaker/pyNN/models/common/eieio_spike_recorder.py
+++ b/spynnaker/pyNN/models/common/eieio_spike_recorder.py
@@ -60,7 +60,7 @@ def set_recording(self, new_state, sampling_interval=None):
         self.__record = new_state
 
     def get_spikes(self, label, region,
-                   application_vertex, base_key_function):
+                   application_vertex, base_key_function, n_colour_bits):
         """ Get the recorded spikes from the object
 
         :param str label:
@@ -71,6 +71,7 @@ def get_spikes(self, label, region,
         :param base_key_function:
         :type base_key_function:
             callable(~pacman.model.graphs.machine.MachineVertex,int)
+        :param int n_colour_bits:
         :return: A numpy array of 2-element arrays of (neuron_id, time)
             ordered by time, one element per event
         :rtype: ~numpy.ndarray(tuple(int,int))
@@ -103,7 +104,8 @@ def get_spikes(self, label, region,
                     missing.append(placement)
                 self._process_spike_data(
                     vertex_slice, application_vertex.atoms_shape,
-                    raw_spike_data, base_key_function(vertex), results)
+                    raw_spike_data, base_key_function(vertex), results,
+                    n_colour_bits)
 
         if missing:
             missing_str = recording_utils.make_missing_string(missing)
@@ -117,17 +119,21 @@ def get_spikes(self, label, region,
 
     @staticmethod
     def _process_spike_data(
-            vertex_slice, atoms_shape, spike_data, base_key, results):
+            vertex_slice, atoms_shape, spike_data, base_key, results,
+            n_colour_bits):
         """
         :param ~pacman.model.graphs.common.Slice vertex_slice:
         :param bytearray spike_data:
         :param int base_key:
         :param list(~numpy.ndarray) results:
+        :param int n_colour_bits:
         """
         number_of_bytes_written = len(spike_data)
         offset = 0
-        indices = get_field_based_index(base_key, vertex_slice)
+        indices = get_field_based_index(base_key, vertex_slice, n_colour_bits)
         slice_ids = vertex_slice.get_raster_ids(atoms_shape)
+        colour_mask = (2 ** n_colour_bits) - 1
+        inv_colour_mask = ~colour_mask & 0xFFFFFFFF
         while offset < number_of_bytes_written:
             length, time = _TWO_WORDS.unpack_from(spike_data, offset)
             time *= SpynnakerDataView.get_simulation_time_step_ms()
@@ -144,6 +150,7 @@ def _process_spike_data(
             keys = numpy.frombuffer(
                 spike_data, dtype="<u{}".format(key_bytes),
                 count=eieio_header.count, offset=data_offset)
+            keys = numpy.bitwise_and(keys, inv_colour_mask)
             local_ids = numpy.array([indices[key] for key in keys])
             neuron_ids = slice_ids[local_ids]
             offset += length + 2 * BYTES_PER_WORD
diff --git a/spynnaker/pyNN/models/common/population_application_vertex.py b/spynnaker/pyNN/models/common/population_application_vertex.py
index 9b952b6bd5..787894976a 100644
--- a/spynnaker/pyNN/models/common/population_application_vertex.py
+++ b/spynnaker/pyNN/models/common/population_application_vertex.py
@@ -12,10 +12,13 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
-from pacman.model.graphs.application import ApplicationVertex
 from enum import Enum
+from spinn_utilities.overrides import overrides
 from spinn_utilities.helpful_functions import is_singleton
+from pacman.model.graphs.application import ApplicationVertex
 from spinn_front_end_common.utilities.exceptions import ConfigurationException
+from spinn_front_end_common.abstract_models import HasCustomAtomKeyMap
+from pacman.utilities.utility_calls import get_field_based_keys
 
 
 class RecordingType(Enum):
@@ -26,7 +29,7 @@ class RecordingType(Enum):
     EVENT = 2
 
 
-class PopulationApplicationVertex(ApplicationVertex):
+class PopulationApplicationVertex(ApplicationVertex, HasCustomAtomKeyMap):
     """ A vertex that can be used in a Population.
 
         Provides some default functions that can be overridden if the vertex
@@ -326,3 +329,24 @@ def inject(self, current_source, selector=None):
         """
         raise ConfigurationException(
             "This Population doesn't support injection")
+
+    @property
+    def n_colour_bits(self):
+        """ The number of colour bits sent by this vertex.
+
+            Assumed 0 unless overridden
+
+        :rtype: int
+        """
+        return 0
+
+    @overrides(HasCustomAtomKeyMap.get_atom_key_map)
+    def get_atom_key_map(self, pre_vertex, partition_id, routing_info):
+        base_key = routing_info.get_first_key_from_pre_vertex(
+            pre_vertex, partition_id)
+        # This might happen if there are no edges
+        if base_key is None:
+            base_key = 0
+        vertex_slice = pre_vertex.vertex_slice
+        keys = get_field_based_keys(base_key, vertex_slice, self.n_colour_bits)
+        return enumerate(keys, vertex_slice.lo_atom)
diff --git a/spynnaker/pyNN/models/neural_projections/connectors/convolution_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/convolution_connector.py
index ddb02a64c8..55d699f554 100644
--- a/spynnaker/pyNN/models/neural_projections/connectors/convolution_connector.py
+++ b/spynnaker/pyNN/models/neural_projections/connectors/convolution_connector.py
@@ -29,7 +29,7 @@
 from spynnaker.pyNN.utilities.constants import SPIKE_PARTITION_ID
 
 #: The number of 32-bit words in the source_key_info struct
-SOURCE_KEY_INFO_WORDS = 6
+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)
@@ -339,7 +339,8 @@ def local_only_n_bytes(self):
             (n_weights * BYTES_PER_SHORT))
 
     def write_local_only_data(
-            self, spec, app_edge, vertex_slice, key, mask, weight_scales):
+            self, spec, app_edge, vertex_slice, key, mask, n_colour_bits,
+            weight_scales):
         # Get info about things
         kernel_shape = self.__kernel_weights.shape
         ps_x, ps_y = 1, 1
@@ -349,6 +350,7 @@ def write_local_only_data(
         # Write source key info
         spec.write_value(key, data_type=DataType.UINT32)
         spec.write_value(mask, data_type=DataType.UINT32)
+        spec.write_value(n_colour_bits, data_type=DataType.UINT32)
 
         # Write the column and row mask and shifts to extract the column and
         # row from the incoming spike
diff --git a/spynnaker/pyNN/models/neural_projections/connectors/pool_dense_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/pool_dense_connector.py
index d90a0fa14a..bc37b15829 100644
--- a/spynnaker/pyNN/models/neural_projections/connectors/pool_dense_connector.py
+++ b/spynnaker/pyNN/models/neural_projections/connectors/pool_dense_connector.py
@@ -28,7 +28,7 @@
 
 
 _DIMENSION_SIZE = (2 * BYTES_PER_WORD) + (6 * BYTES_PER_SHORT)
-_KEY_INFO_SIZE = 2 * BYTES_PER_WORD
+_KEY_INFO_SIZE = 3 * BYTES_PER_WORD
 _CONN_SIZE = _KEY_INFO_SIZE + (2 * 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"),
@@ -261,11 +261,12 @@ def local_only_n_bytes(self, incoming_slices, n_post_atoms):
 
     def write_local_only_data(
             self, spec, app_edge, pre_vertex_slice, post_vertex_slice,
-            key, mask, weight_scales):
+            key, mask, n_colour_bits, weight_scales):
 
         # Write source key info
         spec.write_value(key, data_type=DataType.UINT32)
         spec.write_value(mask, data_type=DataType.UINT32)
+        spec.write_value(n_colour_bits, data_type=DataType.UINT32)
 
         # Write numbers of things
         n_dims = len(pre_vertex_slice.shape)
diff --git a/spynnaker/pyNN/models/neuron/abstract_population_vertex.py b/spynnaker/pyNN/models/neuron/abstract_population_vertex.py
index f8761c7e02..3df06fb025 100644
--- a/spynnaker/pyNN/models/neuron/abstract_population_vertex.py
+++ b/spynnaker/pyNN/models/neuron/abstract_population_vertex.py
@@ -140,7 +140,8 @@ class AbstractPopulationVertex(
         "__connection_cache",
         "__read_initial_values",
         "__have_read_initial_values",
-        "__last_parameter_read_time"]
+        "__last_parameter_read_time",
+        "__n_colour_bits"]
 
     #: recording region IDs
     _SPIKE_RECORDING_REGION = 0
@@ -157,14 +158,15 @@ class AbstractPopulationVertex(
     _SYNAPSE_BASE_N_CPU_CYCLES_PER_NEURON = 22
     _SYNAPSE_BASE_N_CPU_CYCLES = 10
 
-    # 5 elements before the start of global parameters
-    # 1. has key, 2. key, 3. n atoms, 4. n_atoms_peak 5. n_synapse_types
+    # Elements before the start of global parameters
+    # 1. has key, 2. key, 3. n atoms, 4. n_atoms_peak 5. n_colour_bits
     CORE_PARAMS_BASE_SIZE = 5 * BYTES_PER_WORD
 
     def __init__(
             self, n_neurons, label, max_atoms_per_core,
             spikes_per_second, ring_buffer_sigma, incoming_spike_buffer_size,
-            neuron_impl, pynn_model, drop_late_spikes, splitter, seed):
+            neuron_impl, pynn_model, drop_late_spikes, splitter, seed,
+            n_colour_bits):
         """
         :param int n_neurons: The number of neurons in the population
         :param str label: The label on the population
@@ -190,6 +192,7 @@ def __init__(
         :param seed:
             The Population seed, used to ensure the same random generation
             on each run.
+        :param int n_colour_bits: The number of colour bits to use
         """
 
         # pylint: disable=too-many-arguments
@@ -226,6 +229,10 @@ def __init__(
         self.__initial_state_variables = RangeDictionary(n_neurons)
         self.__neuron_impl.add_state_variables(self.__initial_state_variables)
         self.__state_variables = self.__initial_state_variables.copy()
+        self.__n_colour_bits = n_colour_bits
+        if self.__n_colour_bits is None:
+            self.__n_colour_bits = get_config_int(
+                "Simulation", "n_colour_bits")
 
         # Set up for recording
         neuron_recordable_variables = list(
@@ -1413,6 +1420,11 @@ def __tell_neuron_vertices_to_regenerate(self):
             if isinstance(vertex, PopulationMachineNeurons):
                 vertex.set_do_neuron_regeneration()
 
+    @property
+    @overrides(PopulationApplicationVertex.n_colour_bits)
+    def n_colour_bits(self):
+        return self.__n_colour_bits
+
 
 class _Stats(object):
     """ Object to keep hold of and process statistics for ring buffer scaling
diff --git a/spynnaker/pyNN/models/neuron/abstract_pynn_neuron_model.py b/spynnaker/pyNN/models/neuron/abstract_pynn_neuron_model.py
index 4c6dff50b9..c5d382da36 100644
--- a/spynnaker/pyNN/models/neuron/abstract_pynn_neuron_model.py
+++ b/spynnaker/pyNN/models/neuron/abstract_pynn_neuron_model.py
@@ -25,7 +25,7 @@
 _population_parameters = {
     "spikes_per_second": None, "ring_buffer_sigma": None,
     "incoming_spike_buffer_size": None, "drop_late_spikes": None,
-    "splitter": None, "seed": None
+    "splitter": None, "seed": None, "n_colour_bits": None
 }
 
 
@@ -48,10 +48,11 @@ def _model(self):
                additional_arguments=_population_parameters.keys())
     def create_vertex(
             self, n_neurons, label, spikes_per_second, ring_buffer_sigma,
-            incoming_spike_buffer_size, drop_late_spikes, splitter, seed):
+            incoming_spike_buffer_size, drop_late_spikes, splitter, seed,
+            n_colour_bits):
         # pylint: disable=arguments-differ
         max_atoms = self.get_model_max_atoms_per_dimension_per_core()
         return AbstractPopulationVertex(
             n_neurons, label, max_atoms, spikes_per_second, ring_buffer_sigma,
             incoming_spike_buffer_size, self.__model, self, drop_late_spikes,
-            splitter, seed)
+            splitter, seed, n_colour_bits)
diff --git a/spynnaker/pyNN/models/neuron/abstract_pynn_neuron_model_standard.py b/spynnaker/pyNN/models/neuron/abstract_pynn_neuron_model_standard.py
index d970273728..edcee5930c 100644
--- a/spynnaker/pyNN/models/neuron/abstract_pynn_neuron_model_standard.py
+++ b/spynnaker/pyNN/models/neuron/abstract_pynn_neuron_model_standard.py
@@ -56,10 +56,11 @@ def __init__(
     def create_vertex(
             self, n_neurons, label, spikes_per_second,
             ring_buffer_sigma, incoming_spike_buffer_size,
-            n_steps_per_timestep, drop_late_spikes, splitter, seed):
+            n_steps_per_timestep, drop_late_spikes, splitter, seed,
+            n_colour_bits):
         # pylint: disable=arguments-differ
         self._model.n_steps_per_timestep = n_steps_per_timestep
         return super().create_vertex(
             n_neurons, label, spikes_per_second,
             ring_buffer_sigma, incoming_spike_buffer_size, drop_late_spikes,
-            splitter, seed)
+            splitter, seed, n_colour_bits)
diff --git a/spynnaker/pyNN/models/neuron/local_only/local_only_convolution.py b/spynnaker/pyNN/models/neuron/local_only/local_only_convolution.py
index 545f4d6309..7e27e8e278 100644
--- a/spynnaker/pyNN/models/neuron/local_only/local_only_convolution.py
+++ b/spynnaker/pyNN/models/neuron/local_only/local_only_convolution.py
@@ -119,7 +119,7 @@ def write_parameters(self, spec, region, machine_vertex, weight_scales):
             app_edge = incoming._projection_edge
             s_info.connector.write_local_only_data(
                 spec, app_edge, source.vertex_slice, source.key,
-                source.mask, weight_scales)
+                source.mask, app_edge.pre_vertex.n_colour_bits, weight_scales)
 
     def __merge_key_and_mask(self, key_a, mask_a, key_b, mask_b):
         new_xs = ~(key_a ^ key_b)
diff --git a/spynnaker/pyNN/models/neuron/local_only/local_only_pool_dense.py b/spynnaker/pyNN/models/neuron/local_only/local_only_pool_dense.py
index 0d4432bbc1..da63852a5d 100644
--- a/spynnaker/pyNN/models/neuron/local_only/local_only_pool_dense.py
+++ b/spynnaker/pyNN/models/neuron/local_only/local_only_pool_dense.py
@@ -101,7 +101,8 @@ def write_parameters(self, spec, region, machine_vertex, weight_scales):
                     group_key, group_mask = self.__merge_key_and_mask(
                         group_key, group_mask, r_info.key, r_info.mask)
                 incoming_info.append(
-                    (incoming, vertex_slice, group_key, group_mask))
+                    (incoming, vertex_slice, group_key, group_mask,
+                     app_edge.pre_vertex.n_colour_bits))
 
         size = self.get_parameters_usage_in_bytes(
             machine_vertex.vertex_slice.n_atoms,
@@ -117,13 +118,13 @@ def write_parameters(self, spec, region, machine_vertex, weight_scales):
 
         # Write spec for each connector, sorted by key
         incoming_info.sort(key=lambda e: e[3])
-        for incoming, vertex_slice, key, mask in incoming_info:
+        for incoming, vertex_slice, key, mask, n_colour_bits in incoming_info:
             # pylint: disable=protected-access
             s_info = incoming._synapse_information
             app_edge = incoming._projection_edge
             s_info.connector.write_local_only_data(
                 spec, app_edge, vertex_slice, post_slice, key, mask,
-                weight_scales)
+                n_colour_bits, weight_scales)
 
     def __merge_key_and_mask(self, key_a, mask_a, key_b, mask_b):
         new_xs = ~(key_a ^ key_b)
diff --git a/spynnaker/pyNN/models/neuron/master_pop_table.py b/spynnaker/pyNN/models/neuron/master_pop_table.py
index 82df999068..ec8d4433f1 100644
--- a/spynnaker/pyNN/models/neuron/master_pop_table.py
+++ b/spynnaker/pyNN/models/neuron/master_pop_table.py
@@ -75,16 +75,16 @@ class _MasterPopEntryCType(ctypes.LittleEndianStructure):
         # The mask to select the relevant bits of key for matching
         ("mask", ctypes.c_uint32),
         # The index into address_list for this entry
-        ("start", ctypes.c_uint32, 15),
-        # Flag to indicate if an extra_info struct is present
-        ("extra_info_flag", ctypes.c_uint32, 1),
+        ("start", ctypes.c_uint32, 13),
+        # The number of colour bits used (or 0 if no colour bits)
+        ("n_colour_bits", ctypes.c_uint32, 3),
         # The number of entries in ::address_list for this entry
         ("count", ctypes.c_uint32, 16),
         # The mask to apply to the key once shifted get the core index
         ("core_mask", ctypes.c_uint32, 16),
         # The shift to apply to the key to get the core part (0-31)
         ("mask_shift", ctypes.c_uint32, 16),
-        # The number of neurons per core (up to 2048)
+        # The number of neurons per core
         ("n_neurons", ctypes.c_uint32, 16),
         # The number of words required for n_neurons
         ("n_words", ctypes.c_uint32, 16)
@@ -164,9 +164,13 @@ class _MasterPopEntry(object):
         # Where in the key that the core id is held
         "__core_shift",
         # The number of neurons on every core except the last
-        "__n_neurons"]
+        "__n_neurons",
+        # The number of bits reserved for the colour
+        "__n_colour_bits"
+        ]
 
-    def __init__(self, routing_key, mask, core_mask, core_shift, n_neurons):
+    def __init__(self, routing_key, mask, core_mask, core_shift, n_neurons,
+                 n_colour_bits):
         """
         :param int routing_key: The key to match for this entry
         :param int mask: The mask to match for this entry
@@ -181,6 +185,7 @@ def __init__(self, routing_key, mask, core_mask, core_shift, n_neurons):
         self.__core_mask = core_mask
         self.__core_shift = core_shift
         self.__n_neurons = n_neurons
+        self.__n_colour_bits = n_colour_bits
         self.__addresses_and_row_lengths = list()
 
     def append(self, address, row_length):
@@ -282,8 +287,7 @@ def write_to_table(self, entry, address_list, start):
         next_addr = start
         n_entries = count
 
-        # Add extra info
-        entry.extra_info_flag = True
+        entry.n_colour_bits = self.__n_colour_bits
         entry.core_mask = self.__core_mask
         entry.n_words = int(math.ceil(
             self.__n_neurons / BIT_IN_A_WORD))
@@ -390,7 +394,7 @@ def initialise_table(self):
 
     def add_application_entry(
             self, block_start_addr, row_length, key_and_mask, core_mask,
-            core_shift, n_neurons):
+            core_shift, n_neurons, n_colour_bits):
         """ Add an entry for an application-edge to the population table.
 
         :param int block_start_addr: where the synaptic matrix block starts
@@ -402,6 +406,8 @@ def add_application_entry(
         :param int core_shift: The shift of the mask to get to the core_mask
         :param int n_neurons:
             The number of neurons in each machine vertex (bar the last)
+        :param int n_colour_bits:
+            The number of bits to use for colour
         :return: The index of the entry, to be used to retrieve it
         :rtype: int
         :raises ~spynnaker.pyNN.exceptions.SynapticConfigurationException:
@@ -421,11 +427,11 @@ def add_application_entry(
 
         return self.__update_master_population_table(
             block_start_addr, row_length, key_and_mask, core_mask, core_shift,
-            n_neurons)
+            n_neurons, n_colour_bits)
 
     def __update_master_population_table(
             self, block_start_addr, row_length, key_and_mask, core_mask,
-            core_shift, n_neurons):
+            core_shift, n_neurons, n_colour_bits):
         """ Add an entry in the binary search to deal with the synaptic matrix
 
         :param int block_start_addr: where the synaptic matrix block starts
@@ -437,6 +443,7 @@ def __update_master_population_table(
         :param int core_shift: The shift of the mask to get to the core_mask
         :param int n_neurons:
             The number of neurons in each machine vertex (bar the last)
+        :param int n_colour_bits: The number of bits to use for colour
         :return: The index of the entry, to be used to retrieve it
         :rtype: int
         :raises ~spynnaker.pyNN.exceptions.SynapticConfigurationException:
@@ -455,13 +462,14 @@ def __update_master_population_table(
         row_length = self.get_allowed_row_length(row_length)
 
         entry = self.__add_entry(
-            key_and_mask, core_mask, core_shift, n_neurons)
+            key_and_mask, core_mask, core_shift, n_neurons, n_colour_bits)
         index = entry.append(start_addr, row_length - 1)
         self.__n_addresses += 1
         return index
 
     def add_invalid_application_entry(
-            self, key_and_mask, core_mask=0, core_shift=0, n_neurons=0):
+            self, key_and_mask, core_mask, core_shift, n_neurons,
+            n_colour_bits):
         """ Add an entry to the table from an application vertex that doesn't
             point to anywhere.  Used to keep indices in synchronisation between
             e.g. normal and delay entries and between entries on different
@@ -477,6 +485,8 @@ def add_invalid_application_entry(
         :param int core_shift: The shift of the mask to get to the core_mask
         :param int n_neurons:
             The number of neurons in each machine vertex (bar the last)
+        :param int n_colour_bits:
+            The number of bits to use for colour
         :return: The index of the added entry
         :rtype: int
         """
@@ -492,10 +502,11 @@ def add_invalid_application_entry(
                 "The core mask of {} is too big (maximum {})".format(
                     core_mask, _MAX_CORE_MASK))
         return self.__add_invalid_entry(
-            key_and_mask, core_mask, core_shift, n_neurons)
+            key_and_mask, core_mask, core_shift, n_neurons, n_colour_bits)
 
     def __add_invalid_entry(
-            self, key_and_mask, core_mask, core_shift, n_neurons):
+            self, key_and_mask, core_mask, core_shift, n_neurons,
+            n_colour_bits):
         """ Add an entry to the table that doesn't point to anywhere.  Used
             to keep indices in synchronisation between e.g. normal and delay
             entries and between entries on different cores.
@@ -510,16 +521,20 @@ def __add_invalid_entry(
         :param int core_shift: The shift of the mask to get to the core_mask
         :param int n_neurons:
             The number of neurons in each machine vertex (bar the last)
+        :param int n_colour_bits:
+            The number of bits used for colour
         :return: The index of the added entry
         :rtype: int
         """
         entry = self.__add_entry(
-            key_and_mask, core_mask, core_shift, n_neurons)
+            key_and_mask, core_mask, core_shift, n_neurons, n_colour_bits)
         index = entry.append_invalid()
         self.__n_addresses += 1
         return index
 
-    def __add_entry(self, key_and_mask, core_mask, core_shift, n_neurons):
+    def __add_entry(
+            self, key_and_mask, core_mask, core_shift, n_neurons,
+            n_colour_bits):
         if self.__n_addresses >= _MAX_ADDRESS_START:
             raise SynapticConfigurationException(
                 "The table already contains {} entries;"
@@ -527,7 +542,7 @@ def __add_entry(self, key_and_mask, core_mask, core_shift, n_neurons):
         if key_and_mask.key not in self.__entries:
             entry = _MasterPopEntry(
                 key_and_mask.key, key_and_mask.mask, core_mask, core_shift,
-                n_neurons)
+                n_neurons, n_colour_bits)
             self.__entries[key_and_mask.key] = entry
             return entry
         entry = self.__entries[key_and_mask.key]
diff --git a/spynnaker/pyNN/models/neuron/population_machine_common.py b/spynnaker/pyNN/models/neuron/population_machine_common.py
index aa84ebb69c..0a772ad5b2 100644
--- a/spynnaker/pyNN/models/neuron/population_machine_common.py
+++ b/spynnaker/pyNN/models/neuron/population_machine_common.py
@@ -154,3 +154,8 @@ def _write_common_data_spec(self, spec, rec_regions):
     @overrides(AbstractHasAssociatedBinary.get_binary_file_name)
     def get_binary_file_name(self):
         return self.__binary_file_name
+
+    @overrides(MachineVertex.get_n_keys_for_partition)
+    def get_n_keys_for_partition(self, partition_id):
+        # Colour each time slot with up to 16 colours to allow for delays
+        return self._vertex_slice.n_atoms * 16
diff --git a/spynnaker/pyNN/models/neuron/population_machine_neurons.py b/spynnaker/pyNN/models/neuron/population_machine_neurons.py
index 18c3f3c3bf..c0d4a213ab 100644
--- a/spynnaker/pyNN/models/neuron/population_machine_neurons.py
+++ b/spynnaker/pyNN/models/neuron/population_machine_neurons.py
@@ -217,15 +217,18 @@ def _write_neuron_core_parameters(self, spec, ring_buffer_shifts):
             spec.write_value(data=0)
             keys = [0] * n_atoms
         else:
+            n_colour_bits = self._app_vertex.n_colour_bits
             spec.write_value(data=1)
             # Quick and dirty way to avoid using field based keys in cases
             # which use grids but not local-only neuron models
             if isinstance(self._app_vertex.synapse_dynamics,
                           AbstractLocalOnly):
-                keys = get_field_based_keys(self._key, self._vertex_slice)
+                keys = get_field_based_keys(
+                    self._key, self._vertex_slice, n_colour_bits)
             else:
                 # keys are consecutive from the base value
-                keys = [self._key + nn for nn in range(n_atoms)]
+                keys = [self._key + (nn << n_colour_bits)
+                        for nn in range(n_atoms)]
 
         # Write the number of neurons in the block:
         spec.write_value(data=n_atoms)
@@ -233,6 +236,9 @@ def _write_neuron_core_parameters(self, spec, ring_buffer_shifts):
         # Write the maximum number of neurons based on the max atoms per core
         spec.write_value(data=2**get_n_bits(self._max_atoms_per_core))
 
+        # Write the number of colour bits
+        spec.write_value(self._app_vertex.n_colour_bits)
+
         # Write the ring buffer data
         # This is only the synapse types that need a ring buffer i.e. not
         # those stored in synapse dynamics
diff --git a/spynnaker/pyNN/models/neuron/population_machine_synapses_provenance.py b/spynnaker/pyNN/models/neuron/population_machine_synapses_provenance.py
index 9419dce7b7..de7236a231 100644
--- a/spynnaker/pyNN/models/neuron/population_machine_synapses_provenance.py
+++ b/spynnaker/pyNN/models/neuron/population_machine_synapses_provenance.py
@@ -34,7 +34,13 @@ class SynapseProvenance(ctypes.LittleEndianStructure):
         # The number of population table hits on INVALID entries
         ("n_invalid_pop_table_hits", ctypes.c_uint32),
         # The number of spikes that didn't transfer empty rows
-        ("n_filtered_by_bitfield", ctypes.c_uint32)
+        ("n_filtered_by_bitfield", ctypes.c_uint32),
+        # The number of synapses skipped due to late spikes
+        ("n_skipped_synapses", ctypes.c_uint32),
+        # The number of spikes detecte as late
+        ("n_late_spikes", ctypes.c_uint32),
+        # The maximum lateness of a spike
+        ("max_late_spike", ctypes.c_uint32)
     ]
 
     N_ITEMS = len(_fields_)
@@ -56,6 +62,9 @@ class PopulationMachineSynapsesProvenance(object):
     INVALID_MASTER_POP_HITS = "Invalid Master Pop hits"
     BIT_FIELD_FILTERED_PACKETS = \
         "How many packets were filtered by the bitfield filterer."
+    SYNAPSES_SKIPPED = "Skipped synapses"
+    LATE_SPIKES = "Late spikes"
+    MAX_LATE_SPIKE = "Max late spike"
 
     @abstractproperty
     def _app_vertex(self):
@@ -129,3 +138,17 @@ def _parse_synapse_provenance(self, label,  x, y, p, provenance_data):
             db.insert_core(
                 x, y, p, self.BIT_FIELD_FILTERED_PACKETS,
                 synapse_prov.n_filtered_by_bitfield)
+
+            db.insert_core(
+                x, y, p, self.SYNAPSES_SKIPPED,
+                synapse_prov.n_skipped_synapses)
+            if synapse_prov.n_skipped_synapses > 0:
+                db.insert_report(
+                    f"On {label}, there were {synapse_prov.n_skipped_synapses}"
+                    " that were skipped because a spike was received later"
+                    " than the delay in the synapse")
+
+            db.insert_core(
+                x, y, p, self.LATE_SPIKES, synapse_prov.n_late_spikes)
+            db.insert_core(
+                x, y, p, self.MAX_LATE_SPIKE, synapse_prov.max_late_spike)
diff --git a/spynnaker/pyNN/models/neuron/population_machine_vertex.py b/spynnaker/pyNN/models/neuron/population_machine_vertex.py
index 7121ffc59f..209f374b68 100644
--- a/spynnaker/pyNN/models/neuron/population_machine_vertex.py
+++ b/spynnaker/pyNN/models/neuron/population_machine_vertex.py
@@ -397,3 +397,8 @@ def set_do_neuron_regeneration(self):
     @overrides(PopulationMachineSynapses.set_do_synapse_regeneration)
     def set_do_synapse_regeneration(self):
         self.__regenerate_synapse_data = True
+
+    @overrides(PopulationMachineCommon.get_n_keys_for_partition)
+    def get_n_keys_for_partition(self, partition_id):
+        n_colours = 2 ** self._app_vertex.n_colour_bits
+        return self._vertex_slice.n_atoms * n_colours
diff --git a/spynnaker/pyNN/models/neuron/population_neurons_machine_vertex.py b/spynnaker/pyNN/models/neuron/population_neurons_machine_vertex.py
index 5d2e1a5fcb..00ca953b67 100644
--- a/spynnaker/pyNN/models/neuron/population_neurons_machine_vertex.py
+++ b/spynnaker/pyNN/models/neuron/population_neurons_machine_vertex.py
@@ -292,3 +292,8 @@ def sdram_requirement(self, sdram_machine_edge):
     def set_do_neuron_regeneration(self):
         self.__regenerate_data = True
         self.__neuron_data.reset_generation()
+
+    @overrides(PopulationMachineCommon.get_n_keys_for_partition)
+    def get_n_keys_for_partition(self, partition_id):
+        n_colours = 2 ** self._app_vertex.n_colour_bits
+        return self._vertex_slice.n_atoms * n_colours
diff --git a/spynnaker/pyNN/models/neuron/population_synapses_machine_vertex_common.py b/spynnaker/pyNN/models/neuron/population_synapses_machine_vertex_common.py
index d3505bed96..6ffef53a08 100644
--- a/spynnaker/pyNN/models/neuron/population_synapses_machine_vertex_common.py
+++ b/spynnaker/pyNN/models/neuron/population_synapses_machine_vertex_common.py
@@ -33,8 +33,9 @@
 SDRAM_PARAMS_SIZE = 3 * BYTES_PER_WORD
 
 # Size of the Key config params = 1 work for key + 1 word for mask
-#  + 1 word for spike mask + 1 word for self connection boolean
-KEY_CONFIG_SIZE = 4 * BYTES_PER_WORD
+#  + 1 word for spike mask + 1 word for colour shift
+#  + 1 word for self connection boolean
+KEY_CONFIG_SIZE = 5 * BYTES_PER_WORD
 
 
 class SpikeProcessingFastProvenance(ctypes.LittleEndianStructure):
@@ -240,6 +241,7 @@ def _write_key_spec(self, spec):
             spec.write_value(r_info.key)
             spec.write_value(r_info.mask)
             spec.write_value(~r_info.mask & 0xFFFFFFFF)
+            spec.write_value(self._app_vertex.n_colour_bits)
             spec.write_value(int(self._app_vertex.self_projection is not None))
 
     @overrides(SendsSynapticInputsOverSDRAM.sdram_requirement)
diff --git a/spynnaker/pyNN/models/neuron/synapse_dynamics/synapse_dynamics_structural_common.py b/spynnaker/pyNN/models/neuron/synapse_dynamics/synapse_dynamics_structural_common.py
index e8e0df0a8f..a62bf56024 100644
--- a/spynnaker/pyNN/models/neuron/synapse_dynamics/synapse_dynamics_structural_common.py
+++ b/spynnaker/pyNN/models/neuron/synapse_dynamics/synapse_dynamics_structural_common.py
@@ -52,8 +52,9 @@ class SynapseDynamicsStructuralCommon(
     # + 3 32-bit numbers (weight; connection_type; total_no_atoms)
     _PRE_POP_INFO_BASE_SIZE = (4 * BYTES_PER_SHORT) + (3 * BYTES_PER_WORD)
 
-    # 5 32-bit numbers (key; mask; n_atoms; lo_atom; m_pop_index)
-    _KEY_ATOM_INFO_SIZE = (5 * BYTES_PER_WORD)
+    # 6 32-bit numbers (key; mask; n_atoms; n_colour_bits; lo_atom;
+    # m_pop_index)
+    _KEY_ATOM_INFO_SIZE = (6 * BYTES_PER_WORD)
 
     # 1 16-bit number (neuron_index)
     # + 2 8-bit numbers (sub_pop_index; pop_index)
@@ -269,6 +270,7 @@ def __write_prepopulation_info(
                 vertex_slice = m_vertex.vertex_slice
                 spec.write_value(r_info.key)
                 spec.write_value(r_info.mask)
+                spec.write_value(m_vertex.app_vertex.n_colour_bits)
                 spec.write_value(vertex_slice.n_atoms)
                 spec.write_value(vertex_slice.lo_atom)
                 spec.write_value(synaptic_matrices.get_index(
diff --git a/spynnaker/pyNN/models/neuron/synaptic_matrices.py b/spynnaker/pyNN/models/neuron/synaptic_matrices.py
index b7b343e071..fc21ed3ae2 100644
--- a/spynnaker/pyNN/models/neuron/synaptic_matrices.py
+++ b/spynnaker/pyNN/models/neuron/synaptic_matrices.py
@@ -355,7 +355,8 @@ def __write_synapse_expander_data_spec(
         spec.write_array(self.__generated_data)
         spec.write_array(self.__bit_field_key_map)
 
-    def __get_app_key_and_mask(self, r_info, n_stages, max_atoms_per_core):
+    def __get_app_key_and_mask(
+            self, r_info, n_stages, max_atoms_per_core, n_colour_bits):
         """ Get a key and mask for an incoming application vertex as a whole
 
         :param list(tuple(int, Slice)) keys:
@@ -363,6 +364,7 @@ def __get_app_key_and_mask(self, r_info, n_stages, max_atoms_per_core):
             application vertex
         :param int mask: The mask that covers all keys
         :param n_stages: The number of delay stages
+        :param n_colour_bits: The number of colour bits sent
         :rtype: None or _AppKeyInfo
         """
 
@@ -373,7 +375,7 @@ def __get_app_key_and_mask(self, r_info, n_stages, max_atoms_per_core):
         n_atoms = min(max_atoms_per_core, pre.n_atoms)
 
         return _AppKeyInfo(r_info.key, r_info.mask, core_mask,
-                           mask_size, n_atoms * n_stages)
+                           mask_size, n_atoms * n_stages, n_colour_bits)
 
     def __app_key_and_mask(self, app_edge):
         """ Get a key and mask for an incoming application vertex as a whole
@@ -388,7 +390,8 @@ def __app_key_and_mask(self, app_edge):
         if r_info is None:
             return None
         return self.__get_app_key_and_mask(
-            r_info, 1, app_edge.pre_vertex.get_max_atoms_per_core())
+            r_info, 1, app_edge.pre_vertex.get_max_atoms_per_core(),
+            app_edge.pre_vertex.n_colour_bits)
 
     def __delay_app_key_and_mask(self, app_edge):
         """ Get a key and mask for a whole incoming delayed application\
@@ -409,7 +412,8 @@ def __delay_app_key_and_mask(self, app_edge):
         # is split according to this
         return self.__get_app_key_and_mask(
             r_info, app_edge.n_delay_stages,
-            app_edge.pre_vertex.get_max_atoms_per_core())
+            app_edge.pre_vertex.get_max_atoms_per_core(),
+            app_edge.pre_vertex.n_colour_bits)
 
     def get_connections_from_machine(self, placement, app_edge, synapse_info):
         """ Get the synaptic connections from the machine
@@ -462,9 +466,11 @@ class _AppKeyInfo(object):
         details
     """
 
-    __slots__ = ["app_key", "app_mask", "core_mask", "core_shift", "n_neurons"]
+    __slots__ = ["app_key", "app_mask", "core_mask", "core_shift", "n_neurons",
+                 "n_colour_bits"]
 
-    def __init__(self, app_key, app_mask, core_mask, core_shift, n_neurons):
+    def __init__(self, app_key, app_mask, core_mask, core_shift, n_neurons,
+                 n_colour_bits):
         """
 
         :param int app_key: The application-level key
@@ -473,12 +479,14 @@ def __init__(self, app_key, app_mask, core_mask, core_shift, n_neurons):
         :param int core_shift: The shift to get the core from the key
         :param int n_neurons:
             The neurons in each core (except possibly the last)
+        :param int n_colour_bits: The number of colour bits sent
         """
         self.app_key = app_key
         self.app_mask = app_mask
         self.core_mask = core_mask
         self.core_shift = core_shift
         self.n_neurons = n_neurons
+        self.n_colour_bits = n_colour_bits
 
     @property
     def key_and_mask(self):
diff --git a/spynnaker/pyNN/models/neuron/synaptic_matrix_app.py b/spynnaker/pyNN/models/neuron/synaptic_matrix_app.py
index 8dbbe1174d..1a8aa75c9e 100644
--- a/spynnaker/pyNN/models/neuron/synaptic_matrix_app.py
+++ b/spynnaker/pyNN/models/neuron/synaptic_matrix_app.py
@@ -156,7 +156,8 @@ def __reserve_app_matrix(self, block_addr, poptable):
             self.__index = poptable.add_invalid_application_entry(
                 self.__app_key_info.key_and_mask,
                 self.__app_key_info.core_mask, self.__app_key_info.core_shift,
-                self.__app_key_info.n_neurons)
+                self.__app_key_info.n_neurons,
+                self.__app_key_info.n_colour_bits)
             return block_addr
 
         # Write a matrix for the whole application vertex
@@ -164,7 +165,8 @@ def __reserve_app_matrix(self, block_addr, poptable):
         self.__index = poptable.add_application_entry(
             block_addr,  self.__max_row_info.undelayed_max_words,
             self.__app_key_info.key_and_mask, self.__app_key_info.core_mask,
-            self.__app_key_info.core_shift, self.__app_key_info.n_neurons)
+            self.__app_key_info.core_shift, self.__app_key_info.n_neurons,
+            self.__app_key_info.n_colour_bits)
         self.__syn_mat_offset = block_addr
         block_addr = self.__next_addr(block_addr, self.__matrix_size)
         return block_addr
@@ -189,7 +191,8 @@ def __reserve_delay_app_matrix(self, block_addr, poptable):
                 self.__delay_app_key_info.key_and_mask,
                 self.__delay_app_key_info.core_mask,
                 self.__delay_app_key_info.core_shift,
-                self.__delay_app_key_info.n_neurons)
+                self.__delay_app_key_info.n_neurons,
+                self.__delay_app_key_info.n_colour_bits)
             return block_addr
 
         # Write a matrix for the whole application vertex
@@ -199,7 +202,8 @@ def __reserve_delay_app_matrix(self, block_addr, poptable):
             self.__delay_app_key_info.key_and_mask,
             self.__delay_app_key_info.core_mask,
             self.__delay_app_key_info.core_shift,
-            self.__delay_app_key_info.n_neurons)
+            self.__delay_app_key_info.n_neurons,
+            self.__delay_app_key_info.n_colour_bits)
         self.__delay_syn_mat_offset = block_addr
         block_addr = self.__next_addr(block_addr, self.__delay_matrix_size)
         return block_addr
diff --git a/spynnaker/pyNN/models/spike_source/spike_source_array.py b/spynnaker/pyNN/models/spike_source/spike_source_array.py
index 01f6241abf..a29662fcc1 100644
--- a/spynnaker/pyNN/models/spike_source/spike_source_array.py
+++ b/spynnaker/pyNN/models/spike_source/spike_source_array.py
@@ -21,7 +21,7 @@
 class SpikeSourceArray(AbstractPyNNModel):
 
     default_population_parameters = {
-        "splitter": None}
+        "splitter": None, "n_colour_bits": None}
 
     def __init__(self, spike_times=None):
         if spike_times is None:
@@ -31,11 +31,12 @@ def __init__(self, spike_times=None):
     @overrides(AbstractPyNNModel.create_vertex,
                additional_arguments=default_population_parameters.keys())
     def create_vertex(
-            self, n_neurons, label, splitter):
+            self, n_neurons, label, splitter, n_colour_bits):
         # pylint: disable=arguments-differ
         max_atoms = self.get_model_max_atoms_per_dimension_per_core()
         return SpikeSourceArrayVertex(
-            n_neurons, self.__spike_times, label, max_atoms, self, splitter)
+            n_neurons, self.__spike_times, label, max_atoms, self, splitter,
+            n_colour_bits)
 
     @property
     def _spike_times(self):
diff --git a/spynnaker/pyNN/models/spike_source/spike_source_array_machine_vertex.py b/spynnaker/pyNN/models/spike_source/spike_source_array_machine_vertex.py
new file mode 100644
index 0000000000..8fba9b5f4d
--- /dev/null
+++ b/spynnaker/pyNN/models/spike_source/spike_source_array_machine_vertex.py
@@ -0,0 +1,63 @@
+# Copyright (c) 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/>.
+import numpy
+from spinn_utilities.overrides import overrides
+from pacman.utilities.utility_calls import get_field_based_keys
+from spinn_front_end_common.utility_models import (
+    ReverseIPTagMulticastSourceMachineVertex)
+from spynnaker.pyNN.data.spynnaker_data_view import SpynnakerDataView
+
+
+class SpikeSourceArrayMachineVertex(ReverseIPTagMulticastSourceMachineVertex):
+    """ Extended to add colour
+    """
+
+    @overrides(
+        ReverseIPTagMulticastSourceMachineVertex.get_n_keys_for_partition)
+    def get_n_keys_for_partition(self, partition_id):
+        n_keys = super().get_n_keys_for_partition(partition_id)
+        n_colours = 2 ** self.app_vertex.n_colour_bits
+        return n_keys * n_colours
+
+    @overrides(ReverseIPTagMulticastSourceMachineVertex._fill_send_buffer_1d)
+    def _fill_send_buffer_1d(self, key_base):
+        first_time_step = SpynnakerDataView.get_first_machine_time_step()
+        end_time_step = SpynnakerDataView.get_current_run_timesteps()
+        if first_time_step == end_time_step:
+            return
+        keys = get_field_based_keys(
+            key_base, self._vertex_slice, self.app_vertex.n_colour_bits)
+        key_list = numpy.array(
+            [keys[atom] for atom in range(self._vertex_slice.n_atoms)])
+        colour_mask = (2 ** self.app_vertex.n_colour_bits) - 1
+        for tick in sorted(self._send_buffer_times):
+            if self._is_in_range(tick, first_time_step, end_time_step):
+                self._send_buffer.add_keys(
+                    tick, key_list + (tick & colour_mask))
+
+    @overrides(ReverseIPTagMulticastSourceMachineVertex._fill_send_buffer_2d)
+    def _fill_send_buffer_2d(self, key_base):
+        first_time_step = SpynnakerDataView.get_first_machine_time_step()
+        end_time_step = SpynnakerDataView.get_current_run_timesteps()
+        if first_time_step == end_time_step:
+            return
+        keys = get_field_based_keys(
+            key_base, self._vertex_slice, self.app_vertex.n_colour_bits)
+        colour_mask = (2 ** self.app_vertex.n_colour_bits) - 1
+        for atom in range(self._vertex_slice.n_atoms):
+            for tick in sorted(self._send_buffer_times[atom]):
+                if self._is_in_range(tick, first_time_step, end_time_step):
+                    self._send_buffer.add_key(
+                        tick, keys[atom] + (tick & colour_mask))
diff --git a/spynnaker/pyNN/models/spike_source/spike_source_array_vertex.py b/spynnaker/pyNN/models/spike_source/spike_source_array_vertex.py
index 03f6954dc7..503191f01d 100644
--- a/spynnaker/pyNN/models/spike_source/spike_source_array_vertex.py
+++ b/spynnaker/pyNN/models/spike_source/spike_source_array_vertex.py
@@ -16,8 +16,10 @@
 from collections import Counter
 import logging
 import numpy
+from pyNN.space import Grid2D, Grid3D
 from spinn_utilities.log import FormatAdapter
 from spinn_utilities.overrides import overrides
+from spinn_utilities.config_holder import get_config_int
 from spinn_front_end_common.utility_models import ReverseIpTagMultiCastSource
 from spynnaker.pyNN.data import SpynnakerDataView
 from spynnaker.pyNN.models.common import EIEIOSpikeRecorder
@@ -27,7 +29,7 @@
 from spynnaker.pyNN.models.abstract_models import SupportsStructure
 from spynnaker.pyNN.utilities.ranged import SpynnakerRangedList
 from spynnaker.pyNN.models.common import ParameterHolder
-from pyNN.space import Grid2D, Grid3D
+from .spike_source_array_machine_vertex import SpikeSourceArrayMachineVertex
 
 logger = FormatAdapter(logging.getLogger(__name__))
 
@@ -61,13 +63,14 @@ class SpikeSourceArrayVertex(
                  "__model",
                  "__structure",
                  "_spike_times",
-                 "__spike_recorder"]
+                 "__spike_recorder",
+                 "__n_colour_bits"]
 
     SPIKE_RECORDING_REGION_ID = 0
 
     def __init__(
             self, n_neurons, spike_times, label,
-            max_atoms_per_core, model, splitter):
+            max_atoms_per_core, model, splitter, n_colour_bits):
         # pylint: disable=too-many-arguments
         self.__model_name = "SpikeSourceArray"
         self.__model = model
@@ -92,6 +95,34 @@ def __init__(
         self._check_spike_density(spike_times)
         # handle recording
         self.__spike_recorder = EIEIOSpikeRecorder()
+        # Do colouring
+        self.__n_colour_bits = n_colour_bits
+        if self.__n_colour_bits is None:
+            self.__n_colour_bits = get_config_int(
+                "Simulation", "n_colour_bits")
+
+    @overrides(ReverseIpTagMultiCastSource.create_machine_vertex)
+    def create_machine_vertex(
+            self, vertex_slice, sdram, label=None):
+        send_buffer_times = self._filtered_send_buffer_times(vertex_slice)
+        machine_vertex = SpikeSourceArrayMachineVertex(
+            vertex_slice=vertex_slice,
+            label=label, app_vertex=self,
+            receive_port=self._receive_port,
+            receive_sdp_port=self._receive_sdp_port,
+            receive_tag=self._receive_tag,
+            receive_rate=self._receive_rate,
+            virtual_key=self._virtual_key, prefix=self._prefix,
+            prefix_type=self._prefix_type, check_keys=self._check_keys,
+            send_buffer_times=send_buffer_times,
+            send_buffer_partition_id=self._send_buffer_partition_id,
+            reserve_reverse_ip_tag=self._reserve_reverse_ip_tag,
+            injection_partition_id=self._injection_partition_id)
+        machine_vertex.enable_recording(self._is_recording)
+        # Known issue with ReverseIPTagMulticastSourceMachineVertex
+        if sdram:
+            assert sdram == machine_vertex.sdram_required
+        return machine_vertex
 
     def _check_spike_density(self, spike_times):
         if len(spike_times):
@@ -277,7 +308,8 @@ def get_recorded_data(self, name):
             lambda vertex:
                 vertex.virtual_key
                 if vertex.virtual_key is not None
-                else 0)
+                else 0,
+            self.__n_colour_bits)
 
     @overrides(PopulationApplicationVertex.get_recording_sampling_interval)
     def get_recording_sampling_interval(self, name):
@@ -329,3 +361,8 @@ def describe(self):
             "parameters": parameters,
         }
         return context
+
+    @property
+    @overrides(PopulationApplicationVertex.n_colour_bits)
+    def n_colour_bits(self):
+        return self.__n_colour_bits
diff --git a/spynnaker/pyNN/models/spike_source/spike_source_poisson.py b/spynnaker/pyNN/models/spike_source/spike_source_poisson.py
index 92d51f6870..adf7dd78e1 100644
--- a/spynnaker/pyNN/models/spike_source/spike_source_poisson.py
+++ b/spynnaker/pyNN/models/spike_source/spike_source_poisson.py
@@ -19,7 +19,8 @@
 from spinn_utilities.classproperty import classproperty
 
 _population_parameters = {
-    "seed": None,  "max_rate": None, "splitter": None}
+    "seed": None,  "max_rate": None, "splitter": None,
+    "n_colour_bits": None}
 
 # Technically, this is ~2900 in terms of DTCM, but is timescale dependent
 # in terms of CPU (2900 at 10 times slow down is fine, but not at
@@ -44,10 +45,10 @@ def absolute_max_atoms_per_core(cls):  # pylint: disable=no-self-argument
     @overrides(AbstractPyNNModel.create_vertex,
                additional_arguments=default_population_parameters.keys())
     def create_vertex(
-            self, n_neurons, label, seed, max_rate, splitter):
+            self, n_neurons, label, seed, max_rate, splitter, n_colour_bits):
         # pylint: disable=arguments-differ
         max_atoms = self.get_model_max_atoms_per_dimension_per_core()
         return SpikeSourcePoissonVertex(
             n_neurons, label, seed, max_atoms, self,
             rate=self.__rate, start=self.__start, duration=self.__duration,
-            max_rate=max_rate, splitter=splitter)
+            max_rate=max_rate, splitter=splitter, n_colour_bits=n_colour_bits)
diff --git a/spynnaker/pyNN/models/spike_source/spike_source_poisson_machine_vertex.py b/spynnaker/pyNN/models/spike_source/spike_source_poisson_machine_vertex.py
index db4dd840d5..e41682b6a4 100644
--- a/spynnaker/pyNN/models/spike_source/spike_source_poisson_machine_vertex.py
+++ b/spynnaker/pyNN/models/spike_source/spike_source_poisson_machine_vertex.py
@@ -118,9 +118,10 @@ def _u3232_to_uint64(array):
 # 5. REAL slow_rate_per_tick_cutoff; 6. REAL fast_rate_per_tick_cutoff;
 # 7. unt32_t first_source_id; 8. uint32_t n_spike_sources;
 # 9. uint32_t max_spikes_per_timestep;
-# 10,11,12,13 mars_kiss64_seed_t (uint[4]) spike_source_seed;
-# 14. Rate changed flag
-PARAMS_BASE_WORDS = 14
+# 10. uint32_t n_colour_bits;
+# 11,12,13,14 mars_kiss64_seed_t (uint[4]) spike_source_seed;
+# 15. Rate changed flag
+PARAMS_BASE_WORDS = 15
 
 # uint32_t n_rates; uint32_t index
 PARAMS_WORDS_PER_NEURON = 2
@@ -454,7 +455,8 @@ def _write_poisson_parameters(self, spec):
             keys = [0] * self.vertex_slice.n_atoms
         else:
             spec.write_value(1)
-            keys = get_field_based_keys(key, self.vertex_slice)
+            keys = get_field_based_keys(
+                key, self.vertex_slice, self.app_vertex.n_colour_bits)
 
         # Write the incoming mask if there is one
         incoming_mask = 0
@@ -493,6 +495,9 @@ def _write_poisson_parameters(self, spec):
         # Write the maximum spikes per tick
         spec.write_value(data=self.max_spikes_per_ts())
 
+        # Write the number of colour bits
+        spec.write_value(data=self.app_vertex.n_colour_bits)
+
         # Write the random seed (4 words), generated randomly!
         spec.write_array(self._app_vertex.kiss_seed(self.vertex_slice))
 
@@ -548,3 +553,8 @@ def sdram_requirement(self, sdram_machine_edge):
             return sdram_machine_edge.post_vertex.n_bytes_for_transfer
         raise SynapticConfigurationException(
             "Unknown post vertex type in edge {}".format(sdram_machine_edge))
+
+    @overrides(MachineVertex.get_n_keys_for_partition)
+    def get_n_keys_for_partition(self, partition_id):
+        n_colours = 2 ** self.app_vertex.n_colour_bits
+        return self._vertex_slice.n_atoms * n_colours
diff --git a/spynnaker/pyNN/models/spike_source/spike_source_poisson_vertex.py b/spynnaker/pyNN/models/spike_source/spike_source_poisson_vertex.py
index 0140c37b99..c941421918 100644
--- a/spynnaker/pyNN/models/spike_source/spike_source_poisson_vertex.py
+++ b/spynnaker/pyNN/models/spike_source/spike_source_poisson_vertex.py
@@ -84,14 +84,15 @@ class SpikeSourcePoissonVertex(
         "__outgoing_projections",
         "__incoming_control_edge",
         "__structure",
-        "__allowed_parameters"]
+        "__allowed_parameters",
+        "__n_colour_bits"]
 
     SPIKE_RECORDING_REGION_ID = 0
 
     def __init__(
             self, n_neurons, label, seed, max_atoms_per_core, model,
             rate=None, start=None, duration=None, rates=None, starts=None,
-            durations=None, max_rate=None, splitter=None):
+            durations=None, max_rate=None, splitter=None, n_colour_bits=None):
         """
         :param int n_neurons:
         :param str label:
@@ -250,6 +251,11 @@ def __init__(
 
         self.__last_rate_read_time = None
 
+        self.__n_colour_bits = n_colour_bits
+        if self.__n_colour_bits is None:
+            self.__n_colour_bits = get_config_int(
+                "Simulation", "n_colour_bits")
+
     @overrides(SupportsStructure.set_structure)
     def set_structure(self, structure):
         self.__structure = structure
@@ -565,3 +571,7 @@ def incoming_control_edge(self):
     @property
     def data(self):
         return self.__data
+
+    @property
+    def n_colour_bits(self):
+        return self.__n_colour_bits
diff --git a/spynnaker/pyNN/models/utility_models/delays/delay_extension_machine_vertex.py b/spynnaker/pyNN/models/utility_models/delays/delay_extension_machine_vertex.py
index 7a03842c33..c3805c93c3 100644
--- a/spynnaker/pyNN/models/utility_models/delays/delay_extension_machine_vertex.py
+++ b/spynnaker/pyNN/models/utility_models/delays/delay_extension_machine_vertex.py
@@ -210,7 +210,8 @@ def parse_extra_provenance_items(self, label, x, y, p, provenance_data):
     @overrides(MachineVertex.get_n_keys_for_partition)
     def get_n_keys_for_partition(self, partition_id):
         n_keys = super().get_n_keys_for_partition(partition_id)
-        return n_keys * self.app_vertex.n_delay_stages
+        n_colours = 2 ** self.app_vertex.n_colour_bits
+        return n_keys * self.app_vertex.n_delay_stages * n_colours
 
     @overrides(AbstractHasAssociatedBinary.get_binary_file_name)
     def get_binary_file_name(self):
@@ -321,3 +322,6 @@ def write_delay_parameters(
 
         # write whether to throw away spikes
         spec.write_value(data=int(self._app_vertex.drop_late_spikes))
+
+        # Write the number of colour bits
+        spec.write_value(data=self.app_vertex.n_colour_bits)
diff --git a/spynnaker/pyNN/models/utility_models/delays/delay_extension_vertex.py b/spynnaker/pyNN/models/utility_models/delays/delay_extension_vertex.py
index cd97d5425d..18cb4ce283 100644
--- a/spynnaker/pyNN/models/utility_models/delays/delay_extension_vertex.py
+++ b/spynnaker/pyNN/models/utility_models/delays/delay_extension_vertex.py
@@ -22,7 +22,7 @@
 from spynnaker.pyNN.utilities.constants import (
     POP_TABLE_MAX_ROW_LENGTH)
 
-_DELAY_PARAM_HEADER_WORDS = 8
+_DELAY_PARAM_HEADER_WORDS = 9
 
 
 class DelayExtensionVertex(
@@ -36,7 +36,8 @@ class DelayExtensionVertex(
         "__delay_per_stage",
         "__n_delay_stages",
         "__drop_late_spikes",
-        "__outgoing_edges"]
+        "__outgoing_edges",
+        "__n_colour_bits"]
 
     # this maps to what master assumes
     MAX_SLOTS = 8
@@ -48,7 +49,7 @@ class DelayExtensionVertex(
         "yet feasible. Please report it to Spinnaker user mail list.")
 
     def __init__(
-            self, partition, delay_per_stage, n_delay_stages,
+            self, partition, delay_per_stage, n_delay_stages, n_colour_bits,
             label="DelayExtension"):
         """
         :param partition: The parition this Delay is supporting
@@ -71,6 +72,8 @@ def __init__(
 
         self.__outgoing_edges = list()
 
+        self.__n_colour_bits = n_colour_bits
+
     @property
     def n_atoms(self):
         return self.__partition.pre_vertex.n_atoms
@@ -137,3 +140,7 @@ def outgoing_edges(self):
         :rtype: list(DelayApplicationEdge)
         """
         return self.__outgoing_edges
+
+    @property
+    def n_colour_bits(self):
+        return self.__n_colour_bits
diff --git a/spynnaker/pyNN/models/utility_models/spike_injector/spike_injector_vertex.py b/spynnaker/pyNN/models/utility_models/spike_injector/spike_injector_vertex.py
index f9e321c079..2b4cfd316c 100644
--- a/spynnaker/pyNN/models/utility_models/spike_injector/spike_injector_vertex.py
+++ b/spynnaker/pyNN/models/utility_models/spike_injector/spike_injector_vertex.py
@@ -106,7 +106,8 @@ def get_recorded_data(self, name):
             lambda vertex:
                 vertex.virtual_key
                 if vertex.virtual_key is not None
-                else 0)
+                else 0,
+            self.n_colour_bits)
 
     @overrides(PopulationApplicationVertex.get_recording_sampling_interval)
     def get_recording_sampling_interval(self, name):
diff --git a/spynnaker/pyNN/spynnaker.cfg b/spynnaker/pyNN/spynnaker.cfg
index 6480295439..f5e053bd05 100644
--- a/spynnaker/pyNN/spynnaker.cfg
+++ b/spynnaker/pyNN/spynnaker.cfg
@@ -29,12 +29,16 @@ ring_buffer_sigma = 5
 incoming_spike_buffer_size = 256
 
 # performance limiter to throw away packets not processed in a given time step
-drop_late_spikes = True
+drop_late_spikes = False
 
 # The overhead to add to the transfer clocks
 # when using a split synapse neuron model
 transfer_overhead_clocks = 200
 
+# The number of "colour" bits to use by default.  This is used to account for
+# delays over the network that are bigger than 1 time step
+n_colour_bits = 4
+
 [Mapping]
 # Setting delay_support_adder to None will skip the adder
 delay_support_adder = DelaySupportAdder
diff --git a/spynnaker/pyNN/spynnaker_external_device_plugin_manager.py b/spynnaker/pyNN/spynnaker_external_device_plugin_manager.py
index ef5a567441..cca1ef6189 100644
--- a/spynnaker/pyNN/spynnaker_external_device_plugin_manager.py
+++ b/spynnaker/pyNN/spynnaker_external_device_plugin_manager.py
@@ -131,7 +131,12 @@ def activate_live_output_for(
         if host is None:
             host = get_config_str("Recording", "live_spike_host")
 
-        # add new edge and vertex if required to SpiNNaker graph
+        # Use the right-shift to remove the colour from translated keys
+        n_colour_bits = population._vertex.n_colour_bits
+        translated_key_right_shift = n_colour_bits
+
+        # Use the mask to remove the colour from non-translated keys
+        received_key_mask = 0xFFFFFFFF & ~((2 ** n_colour_bits) - 1)
 
         # pylint: disable=too-many-arguments, too-many-locals
         params = LivePacketGatherParameters(
@@ -144,7 +149,9 @@ def activate_live_output_for(
             payload_right_shift=payload_right_shift,
             number_of_packets_sent_per_time_step=(
                 number_of_packets_sent_per_time_step),
-            label="LiveSpikeReceiver", translate_keys=translate_keys)
+            label="LiveSpikeReceiver", received_key_mask=received_key_mask,
+            translate_keys=translate_keys,
+            translated_key_right_shift=translated_key_right_shift)
         SpynnakerExternalDevicePluginManager.update_live_packet_gather_tracker(
             population._vertex, params, [SPIKE_PARTITION_ID])
 
@@ -188,21 +195,7 @@ def update_live_packet_gather_tracker(
         :type vertex_to_record_from:
             ~pacman.model.graphs.application.ApplicationVertex or
             ~pacman.model.graphs.machine.MachineVertex
-        :param str lpg_label:
-        :param int port:
-        :param str hostname:
-        :param int tag:
-        :param bool strip_sdp:
-        :param bool use_prefix:
-        :param int key_prefix:
-        :param ~spinnman.messages.eieio.EIEIOPrefix prefix_type:
-        :param ~spinnman.messages.eieio.EIEIOType message_type:
-        :param int right_shift:
-        :param bool payload_as_time_stamps:
-        :param bool use_payload_prefix:
-        :param int payload_prefix:
-        :param int payload_right_shift:
-        :param int number_of_packets_sent_per_time_step:
+        :param LivePacketGatherParameters params:
         :param list(str) partition_ids:
         :param bool translate_keys:
         """


Test patch:
diff --git a/spynnaker_integration_tests/test_various/test_multi_delay.py b/spynnaker_integration_tests/test_various/test_multi_delay.py
index 358b6627ad..41a9815b8b 100644
--- a/spynnaker_integration_tests/test_various/test_multi_delay.py
+++ b/spynnaker_integration_tests/test_various/test_multi_delay.py
@@ -23,7 +23,7 @@ class TestMultiDelay(BaseTestCase):
     """
 
     def test_run(self):
-        n_neurons = 76   # 76 fails
+        n_neurons = 70
         sim.setup(timestep=1.0)
 
         pop_1 = sim.Population(n_neurons, sim.IF_curr_exp(), label="pop_1")
diff --git a/unittests/model_tests/neuron/test_spike_source/test_spike_source_array_vertex.py b/unittests/model_tests/neuron/test_spike_source/test_spike_source_array_vertex.py
index 3a836b55b9..cb7ad94fc7 100644
--- a/unittests/model_tests/neuron/test_spike_source/test_spike_source_array_vertex.py
+++ b/unittests/model_tests/neuron/test_spike_source/test_spike_source_array_vertex.py
@@ -28,7 +28,8 @@ def test_no_spikes(self):
         with LogCapture() as lc:
             v = SpikeSourceArrayVertex(
                 n_neurons=5, spike_times=[], label="test",
-                max_atoms_per_core=None, model=None, splitter=None)
+                max_atoms_per_core=None, model=None, splitter=None,
+                n_colour_bits=None)
         found = False
         for record in lc.records:
             if "no spike" in record.msg.fmt:
@@ -43,13 +44,15 @@ def test_no_spikes(self):
     def test_singleton_list(self):
         v = SpikeSourceArrayVertex(
             n_neurons=5, spike_times=[1, 11, 22],
-            label="test", max_atoms_per_core=None, model=None, splitter=None)
+            label="test", max_atoms_per_core=None, model=None, splitter=None,
+            n_colour_bits=None)
         v.set_parameter_values("spike_times", [2, 12, 32])
 
     def test_double_list(self):
         SpikeSourceArrayVertex(
             n_neurons=3, spike_times=[[1], [11], [22]],
-            label="test", max_atoms_per_core=None, model=None, splitter=None)
+            label="test", max_atoms_per_core=None, model=None, splitter=None,
+            n_colour_bits=None)
 
     def test_big_double_list(self):
         spike_list1 = [1, 2, 6, 8, 9]
@@ -64,7 +67,7 @@ def test_big_double_list(self):
             SpikeSourceArrayVertex(
                 n_neurons=3, spike_times=spike_list,
                 label="test", max_atoms_per_core=None, model=None,
-                splitter=None)
+                splitter=None, n_colour_bits=None)
             found = False
             for record in lc.records:
                 if "too many spikes" in record.msg.fmt:
@@ -78,7 +81,7 @@ def test_shared_list_big(self):
             v = SpikeSourceArrayVertex(
                 n_neurons=3, spike_times=None,
                 label="test", max_atoms_per_core=None, model=None,
-                splitter=None)
+                splitter=None, n_colour_bits=None)
             v.set_parameter_values("spike_times", [34] * 35)
             found = False
             for record in lc.records:
@@ -92,7 +95,7 @@ def test_list_big(self):
             SpikeSourceArrayVertex(
                 n_neurons=1, spike_times=[37] * 109,
                 label="test", max_atoms_per_core=None, model=None,
-                splitter=None)
+                splitter=None, n_colour_bits=None)
             found = False
             for record in lc.records:
                 if "too many spikes" in record.msg.fmt:
diff --git a/unittests/model_tests/neuron/test_synaptic_manager.py b/unittests/model_tests/neuron/test_synaptic_manager.py
index e2cfda71a1..efc27f0113 100644
--- a/unittests/model_tests/neuron/test_synaptic_manager.py
+++ b/unittests/model_tests/neuron/test_synaptic_manager.py
@@ -224,7 +224,7 @@ def test_set_synapse_dynamics():
         n_neurons=10, label="post", spikes_per_second=None,
         ring_buffer_sigma=None, incoming_spike_buffer_size=None,
         n_steps_per_timestep=1, drop_late_spikes=True, splitter=None,
-        seed=None)
+        seed=None, n_colour_bits=None)
 
     static = SynapseDynamicsStatic()
     stdp = SynapseDynamicsSTDP(
@@ -335,7 +335,7 @@ def test_set_synapse_dynamics():
         n_neurons=10, label="post", spikes_per_second=None,
         ring_buffer_sigma=None, incoming_spike_buffer_size=None,
         n_steps_per_timestep=1, drop_late_spikes=True, splitter=None,
-        seed=None)
+        seed=None, n_colour_bits=None)
 
     # STDP followed by structural STDP should result in Structural STDP
     post_app_vertex.synapse_dynamics = stdp
@@ -358,7 +358,7 @@ def test_set_synapse_dynamics():
         n_neurons=10, label="post", spikes_per_second=None,
         ring_buffer_sigma=None, incoming_spike_buffer_size=None,
         n_steps_per_timestep=1, drop_late_spikes=True, splitter=None,
-        seed=None)
+        seed=None, n_colour_bits=None)
 
     # Static followed by static structural should result in static
     # structural
@@ -396,7 +396,7 @@ def test_set_synapse_dynamics():
         n_neurons=10, label="post", spikes_per_second=None,
         ring_buffer_sigma=None, incoming_spike_buffer_size=None,
         n_steps_per_timestep=1, drop_late_spikes=True, splitter=None,
-        seed=None)
+        seed=None, n_colour_bits=None)
     post_app_vertex.synapse_dynamics = static_struct
     post_app_vertex.synapse_dynamics = stdp_struct
 
diff --git a/unittests/test_populations/test_vertex.py b/unittests/test_populations/test_vertex.py
index dc6e3cf701..42dac5aba6 100644
--- a/unittests/test_populations/test_vertex.py
+++ b/unittests/test_populations/test_vertex.py
@@ -106,7 +106,8 @@ def __init__(self):
             max_atoms_per_core=None, spikes_per_second=None,
             ring_buffer_sigma=None, incoming_spike_buffer_size=None,
             neuron_impl=foo_bar.model, pynn_model=foo_bar,
-            drop_late_spikes=True, splitter=None, seed=None)
+            drop_late_spikes=True, splitter=None, seed=None,
+            n_colour_bits=None)
 
 
 def test_initializable():