sinabs 3.0.4.dev25__py3-none-any.whl → 3.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sinabs/activation/reset_mechanism.py +3 -3
- sinabs/activation/surrogate_gradient_fn.py +4 -4
- sinabs/backend/dynapcnn/__init__.py +5 -4
- sinabs/backend/dynapcnn/chip_factory.py +33 -61
- sinabs/backend/dynapcnn/chips/dynapcnn.py +182 -86
- sinabs/backend/dynapcnn/chips/speck2e.py +6 -5
- sinabs/backend/dynapcnn/chips/speck2f.py +6 -5
- sinabs/backend/dynapcnn/config_builder.py +39 -59
- sinabs/backend/dynapcnn/connectivity_specs.py +48 -0
- sinabs/backend/dynapcnn/discretize.py +91 -155
- sinabs/backend/dynapcnn/dvs_layer.py +59 -101
- sinabs/backend/dynapcnn/dynapcnn_layer.py +185 -119
- sinabs/backend/dynapcnn/dynapcnn_layer_utils.py +335 -0
- sinabs/backend/dynapcnn/dynapcnn_network.py +602 -325
- sinabs/backend/dynapcnn/dynapcnnnetwork_module.py +370 -0
- sinabs/backend/dynapcnn/exceptions.py +122 -3
- sinabs/backend/dynapcnn/io.py +51 -91
- sinabs/backend/dynapcnn/mapping.py +111 -75
- sinabs/backend/dynapcnn/nir_graph_extractor.py +877 -0
- sinabs/backend/dynapcnn/sinabs_edges_handler.py +1024 -0
- sinabs/backend/dynapcnn/utils.py +214 -459
- sinabs/backend/dynapcnn/weight_rescaling_methods.py +53 -0
- sinabs/conversion.py +2 -2
- sinabs/from_torch.py +23 -1
- sinabs/hooks.py +38 -41
- sinabs/layers/alif.py +16 -16
- sinabs/layers/crop2d.py +2 -2
- sinabs/layers/exp_leak.py +1 -1
- sinabs/layers/iaf.py +11 -11
- sinabs/layers/lif.py +9 -9
- sinabs/layers/neuromorphic_relu.py +9 -8
- sinabs/layers/pool2d.py +5 -5
- sinabs/layers/quantize.py +1 -1
- sinabs/layers/stateful_layer.py +10 -7
- sinabs/layers/to_spike.py +9 -9
- sinabs/network.py +14 -12
- sinabs/synopcounter.py +10 -7
- sinabs/utils.py +155 -7
- sinabs/validate_memory_speck.py +0 -5
- {sinabs-3.0.4.dev25.dist-info → sinabs-3.1.0.dist-info}/METADATA +2 -1
- sinabs-3.1.0.dist-info/RECORD +65 -0
- {sinabs-3.0.4.dev25.dist-info → sinabs-3.1.0.dist-info}/licenses/AUTHORS +1 -0
- sinabs-3.1.0.dist-info/pbr.json +1 -0
- sinabs-3.0.4.dev25.dist-info/RECORD +0 -59
- sinabs-3.0.4.dev25.dist-info/pbr.json +0 -1
- {sinabs-3.0.4.dev25.dist-info → sinabs-3.1.0.dist-info}/WHEEL +0 -0
- {sinabs-3.0.4.dev25.dist-info → sinabs-3.1.0.dist-info}/licenses/LICENSE +0 -0
- {sinabs-3.0.4.dev25.dist-info → sinabs-3.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1024 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Implements the pre-processing of edges into blocks of nodes (modules) for future
|
|
3
|
+
creation of DynapcnnLayer objects.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Deque, Dict, List, Optional, Set, Tuple, Type, Union
|
|
7
|
+
|
|
8
|
+
from torch import Size, nn
|
|
9
|
+
|
|
10
|
+
from sinabs.layers import SumPool2d
|
|
11
|
+
|
|
12
|
+
from .connectivity_specs import VALID_SINABS_EDGE_TYPES
|
|
13
|
+
from .crop2d import Crop2d
|
|
14
|
+
from .dvs_layer import DVSLayer
|
|
15
|
+
from .exceptions import (
|
|
16
|
+
InvalidEdge,
|
|
17
|
+
InvalidGraphStructure,
|
|
18
|
+
default_invalid_structure_string,
|
|
19
|
+
)
|
|
20
|
+
from .flipdims import FlipDims
|
|
21
|
+
from .utils import Edge, merge_bn
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def remap_edges_after_drop(
|
|
25
|
+
dropped_node: int, source_of_dropped_node: int, edges: Set[Edge]
|
|
26
|
+
) -> Set[Edge]:
|
|
27
|
+
"""Creates a new set of edges from `edges`. All edges where `dropped_node`
|
|
28
|
+
is the source node will be used to generate a new edge where
|
|
29
|
+
`source_of_dropped_node` becomes the source node (the target is kept).
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
dropped_node (int):
|
|
33
|
+
source_of_dropped_node (int):
|
|
34
|
+
edges (set): tuples describing the connections between layers in
|
|
35
|
+
`spiking_model`.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
New set of edges with `source_of_dropped_node` as the source node where
|
|
39
|
+
`dropped_node` used to be.
|
|
40
|
+
"""
|
|
41
|
+
remapped_edges = set()
|
|
42
|
+
|
|
43
|
+
for src, tgt in edges:
|
|
44
|
+
if src == dropped_node:
|
|
45
|
+
remapped_edges.add((source_of_dropped_node, tgt))
|
|
46
|
+
|
|
47
|
+
return remapped_edges
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def handle_batchnorm_nodes(
|
|
51
|
+
edges: Set[Edge],
|
|
52
|
+
indx_2_module_map: Dict[int, nn.Module],
|
|
53
|
+
name_2_indx_map: Dict[str, int],
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Merges `BatchNorm2d`/`BatchNorm1d` layers into `Conv2d`/`Linear` ones.
|
|
56
|
+
The batch norm nodes will be removed from the graph (by updating all
|
|
57
|
+
variables passed as arguments in-place) after their properties are used to
|
|
58
|
+
re-scale the weights of the convolutional/linear layers associated with
|
|
59
|
+
batch normalization via the `weight-batchnorm` edges found in the original
|
|
60
|
+
graph.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
edges (set): tuples describing the connections between layers in
|
|
64
|
+
`spiking_model`.
|
|
65
|
+
indx_2_module_map (dict): the mapping between a node (`key` as an `int`)
|
|
66
|
+
and its module (`value` as a `nn.Module`).
|
|
67
|
+
name_2_indx_map (dict): Map from node names to unique indices.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# Gather indexes of the BatchNorm2d/BatchNorm1d nodes.
|
|
71
|
+
bnorm_nodes = {
|
|
72
|
+
index
|
|
73
|
+
for index, module in indx_2_module_map.items()
|
|
74
|
+
if isinstance(module, (nn.BatchNorm2d, nn.BatchNorm1d))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if len(bnorm_nodes) == 0:
|
|
78
|
+
# There are no edges with batch norm - nothing to do here.
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# Find weight-bnorm edges.
|
|
82
|
+
weight_bnorm_edges = {
|
|
83
|
+
(src, tgt)
|
|
84
|
+
for (src, tgt) in edges
|
|
85
|
+
if (
|
|
86
|
+
isinstance(indx_2_module_map[src], nn.Conv2d)
|
|
87
|
+
and isinstance(indx_2_module_map[tgt], nn.BatchNorm2d)
|
|
88
|
+
)
|
|
89
|
+
or (
|
|
90
|
+
isinstance(indx_2_module_map[src], nn.Linear)
|
|
91
|
+
and isinstance(indx_2_module_map[tgt], nn.BatchNorm1d)
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Merge conv/linear and bnorm layers using 'weight-bnorm' edges.
|
|
96
|
+
for edge in weight_bnorm_edges:
|
|
97
|
+
bnorm = indx_2_module_map[edge[1]]
|
|
98
|
+
weight = indx_2_module_map[edge[0]]
|
|
99
|
+
|
|
100
|
+
# merge and update weight node.
|
|
101
|
+
indx_2_module_map[edge[0]] = merge_bn(weight, bnorm)
|
|
102
|
+
|
|
103
|
+
# Point weight nodes to the targets of their respective batch norm nodes.
|
|
104
|
+
new_edges = set()
|
|
105
|
+
for weight_id, bnorm_id in weight_bnorm_edges:
|
|
106
|
+
new_edges.update(
|
|
107
|
+
remap_edges_after_drop(
|
|
108
|
+
dropped_node=bnorm_id, source_of_dropped_node=weight_id, edges=edges
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
# Remove all edges to and from a batch norm node and replace with new edges
|
|
112
|
+
bnorm_edges = {e for e in edges if bnorm_nodes.intersection(e)}
|
|
113
|
+
edges.difference_update(bnorm_edges)
|
|
114
|
+
edges.update(new_edges)
|
|
115
|
+
|
|
116
|
+
# Remove references to the bnorm node.
|
|
117
|
+
for idx in bnorm_nodes:
|
|
118
|
+
indx_2_module_map.pop(idx)
|
|
119
|
+
|
|
120
|
+
for name in [name for name, indx in name_2_indx_map.items() if indx in bnorm_nodes]:
|
|
121
|
+
name_2_indx_map.pop(name)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def fix_dvs_module_edges(
|
|
125
|
+
edges: Set[Edge],
|
|
126
|
+
indx_2_module_map: Dict[int, nn.Module],
|
|
127
|
+
name_2_indx_map: Dict[str, int],
|
|
128
|
+
entry_nodes: Set[Edge],
|
|
129
|
+
) -> None:
|
|
130
|
+
"""All arguments are modified in-place to fix wrong node extractions from
|
|
131
|
+
NIRtorch when a DVSLayer istance is the first layer in the network.
|
|
132
|
+
Modifies `edges` to re-structure the edges related witht the DVSLayer
|
|
133
|
+
instance. The DVSLayer's forward method feeds data in the sequence
|
|
134
|
+
'DVS -> DVS.pool -> DVS.crop -> DVS.flip', so we remove edges involving
|
|
135
|
+
these nodes (that are internaly implementend in the DVSLayer) from the
|
|
136
|
+
graph and point the node of DVSLayer to the node where it should send its
|
|
137
|
+
output to. This is also removes a self-recurrent node with edge '(FlipDims,
|
|
138
|
+
FlipDims)' that is wrongly extracted.
|
|
139
|
+
Modifies `indx_2_module_map` and `name_2_indx_map` to remove the internal
|
|
140
|
+
DVSLayer nodes (Crop2d, FlipDims and DVSLayer's pooling) since these should
|
|
141
|
+
not be independent nodes in the graph.
|
|
142
|
+
Modifies `entry_nodes` such that the DVSLayer becomes the only entry node
|
|
143
|
+
of the graph.
|
|
144
|
+
|
|
145
|
+
This fix is to imply there's something odd with the extracted adges for the
|
|
146
|
+
forward pass implemented by the DVSLayer. For now this function is fixing
|
|
147
|
+
these edges to have them representing the information flow through this
|
|
148
|
+
layer as **it should be** but the graph tracing of NIR should be looked
|
|
149
|
+
into to solve the root problem.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
edges (set): tuples describing the connections between layers in
|
|
153
|
+
`spiking_model`.
|
|
154
|
+
indx_2_module_map (dict): the mapping between a node (`key` as an `int`)
|
|
155
|
+
and its module (`value` as a `nn.Module`).
|
|
156
|
+
name_2_indx_map (dict): Map from node names to unique indices.
|
|
157
|
+
entry_nodes (set): IDs of nodes acting as entry points for the network
|
|
158
|
+
(i.e., receiving external input).
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
# spot nodes (ie, modules) used in a DVSLayer instance's forward pass (including the DVSLayer node itself).
|
|
162
|
+
dvslayer_nodes = {
|
|
163
|
+
index: module
|
|
164
|
+
for index, module in indx_2_module_map.items()
|
|
165
|
+
if any(
|
|
166
|
+
isinstance(module, dvs_node) for dvs_node in (DVSLayer, Crop2d, FlipDims)
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if len(dvslayer_nodes) <= 1:
|
|
171
|
+
# No module within the DVSLayer instance appears as an independent node - nothing to do here.
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
# TODO - a `SumPool2d` is also a node that's used inside a DVSLayer instance. In what follows we try to find it
|
|
175
|
+
# by looking for pooling nodes that appear in a (pool, crop) edge - the assumption being that if the pooling is
|
|
176
|
+
# inputing into a crop layer than the pool is inside the DVSLayer instance. It feels like a hacky way to do it
|
|
177
|
+
# so we should revise this.
|
|
178
|
+
dvslayer_nodes.update(
|
|
179
|
+
{
|
|
180
|
+
edge[0]: indx_2_module_map[edge[0]]
|
|
181
|
+
for edge in edges
|
|
182
|
+
if isinstance(indx_2_module_map[edge[0]], SumPool2d)
|
|
183
|
+
and isinstance(indx_2_module_map[edge[1]], Crop2d)
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# NIR is extracting an edge (FlipDims, FlipDims) from the DVSLayer: remove self-recurrent nodes from the graph.
|
|
188
|
+
for edge in [
|
|
189
|
+
(src, tgt)
|
|
190
|
+
for (src, tgt) in edges
|
|
191
|
+
if (src == tgt and isinstance(indx_2_module_map[src], FlipDims))
|
|
192
|
+
]:
|
|
193
|
+
edges.remove(edge)
|
|
194
|
+
|
|
195
|
+
# Since NIR is not extracting the edges for the DVSLayer correctly, remove all edges involving the DVS.
|
|
196
|
+
for edge in [
|
|
197
|
+
(src, tgt)
|
|
198
|
+
for (src, tgt) in edges
|
|
199
|
+
if (src in dvslayer_nodes or tgt in dvslayer_nodes)
|
|
200
|
+
]:
|
|
201
|
+
edges.remove(edge)
|
|
202
|
+
|
|
203
|
+
# Get node's indexes based on the module type - just for validation.
|
|
204
|
+
dvs_node = [
|
|
205
|
+
key for key, value in dvslayer_nodes.items() if isinstance(value, DVSLayer)
|
|
206
|
+
]
|
|
207
|
+
dvs_pool_node = [
|
|
208
|
+
key for key, value in dvslayer_nodes.items() if isinstance(value, SumPool2d)
|
|
209
|
+
]
|
|
210
|
+
dvs_crop_node = [
|
|
211
|
+
key for key, value in dvslayer_nodes.items() if isinstance(value, Crop2d)
|
|
212
|
+
]
|
|
213
|
+
dvs_flip_node = [
|
|
214
|
+
key for key, value in dvslayer_nodes.items() if isinstance(value, FlipDims)
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
if any(
|
|
218
|
+
len(node) > 1
|
|
219
|
+
for node in [dvs_node, dvs_pool_node, dvs_crop_node, dvs_flip_node]
|
|
220
|
+
):
|
|
221
|
+
raise ValueError(
|
|
222
|
+
f"Internal DVS nodes should be single instances but multiple have been found: dvs_node: {len(dvs_node)} dvs_pool_node: {len(dvs_pool_node)} dvs_crop_node: {len(dvs_crop_node)} dvs_flip_node: {len(dvs_flip_node)}"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Remove dvs_pool, dvs_crop and dvs_flip nodes from `indx_2_module_map` (these operate within the DVS, not as independent nodes of the final graph).
|
|
226
|
+
indx_2_module_map.pop(dvs_pool_node[-1])
|
|
227
|
+
indx_2_module_map.pop(dvs_crop_node[-1])
|
|
228
|
+
indx_2_module_map.pop(dvs_flip_node[-1])
|
|
229
|
+
|
|
230
|
+
# Remove internal DVS modules from name/index map.
|
|
231
|
+
# Iterate over copy to prevent iterable from changing size.
|
|
232
|
+
n2i_map_copy = {k: v for k, v in name_2_indx_map.items()}
|
|
233
|
+
for name, index in n2i_map_copy.items():
|
|
234
|
+
if index in [dvs_pool_node[-1], dvs_crop_node[-1], dvs_flip_node[-1]]:
|
|
235
|
+
name_2_indx_map.pop(name)
|
|
236
|
+
|
|
237
|
+
dvs_node = dvs_node[0]
|
|
238
|
+
if edges:
|
|
239
|
+
# Add edges from 'dvs' node to the entry point of the graph.
|
|
240
|
+
all_sources, all_targets = zip(*edges)
|
|
241
|
+
local_entry_nodes = set(all_sources) - set(all_targets)
|
|
242
|
+
edges.update({(dvs_node, node) for node in local_entry_nodes})
|
|
243
|
+
|
|
244
|
+
# DVS becomes the only entry node of the graph.
|
|
245
|
+
entry_nodes.clear()
|
|
246
|
+
entry_nodes.add(dvs_node)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def collect_dynapcnn_layer_info(
|
|
250
|
+
indx_2_module_map: Dict[int, nn.Module],
|
|
251
|
+
edges: Set[Edge],
|
|
252
|
+
nodes_io_shapes: Dict[int, Dict[str, Tuple[Size, Size]]],
|
|
253
|
+
entry_nodes: Set[int],
|
|
254
|
+
) -> Tuple[Dict[int, Dict], Union[Dict, None]]:
|
|
255
|
+
"""Collect information to construct DynapcnnLayer instances.
|
|
256
|
+
|
|
257
|
+
Validate and sort edges based on the type of nodes they connect.
|
|
258
|
+
Iterate over edges in order of their type. For each neuron->weight edge
|
|
259
|
+
generate a new dict to collect information for the corresponding dynapcnn
|
|
260
|
+
layer. Then add pooling based on neuron->pooling type edges. Collect
|
|
261
|
+
additional pooling from pooling->pooling type edges. Finally set layer
|
|
262
|
+
destinations based on neuron/pooling->weight type of edges.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
indx_2_module_map (dict): Maps node IDs of the graph as `key` to their
|
|
266
|
+
associated module as `value`.
|
|
267
|
+
edges (set of tuples): Represent connections between two nodes in
|
|
268
|
+
computational graph.
|
|
269
|
+
nodes_io_shapes (dict): Map from node ID to dict containing node's in-
|
|
270
|
+
and output shapes.
|
|
271
|
+
entry_nodes (set of int): IDs of nodes that receive external input.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
A tuple with a dictionary where each 'key' is the index of a future
|
|
275
|
+
'DynapcnnLayer' and 'value' is a dictionary, with keys 'conv',
|
|
276
|
+
'neuron', and 'destinations', containing corresponding node ids and
|
|
277
|
+
modules required to build the layer, and if a DVSLayer is part of the
|
|
278
|
+
network, another dictionary containing the layer itself and its
|
|
279
|
+
destination indices. If not, None.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
# Sort edges by edge type (type of layers they connect)
|
|
283
|
+
edges_by_type: Dict[str, Set[Edge]] = sort_edges_by_type(
|
|
284
|
+
edges=edges, indx_2_module_map=indx_2_module_map
|
|
285
|
+
)
|
|
286
|
+
edge_counts_by_type = {t: len(e) for t, e in edges_by_type.items()}
|
|
287
|
+
|
|
288
|
+
# Dict to collect information for each future dynapcnn layer
|
|
289
|
+
dynapcnn_layer_info = dict()
|
|
290
|
+
# Map node IDs to dynapcnn layer ID
|
|
291
|
+
node_2_layer_map = dict()
|
|
292
|
+
|
|
293
|
+
# Each weight->neuron connection instantiates a new, unique dynapcnn layer
|
|
294
|
+
weight_neuron_edges = edges_by_type.get("weight-neuron", set())
|
|
295
|
+
while weight_neuron_edges:
|
|
296
|
+
edge = weight_neuron_edges.pop()
|
|
297
|
+
init_new_dynapcnnlayer_entry(
|
|
298
|
+
dynapcnn_layer_info,
|
|
299
|
+
edge,
|
|
300
|
+
indx_2_module_map,
|
|
301
|
+
nodes_io_shapes,
|
|
302
|
+
node_2_layer_map,
|
|
303
|
+
entry_nodes,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Process all edges related to DVS layer
|
|
307
|
+
dvs_layer_info = dvs_setup(
|
|
308
|
+
edges_by_type, indx_2_module_map, node_2_layer_map, nodes_io_shapes
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Process all edges connecting two dynapcnn layers that do not include pooling
|
|
312
|
+
neuron_weight_edges = edges_by_type.get("neuron-weight", set())
|
|
313
|
+
while neuron_weight_edges:
|
|
314
|
+
edge = neuron_weight_edges.pop()
|
|
315
|
+
set_neuron_layer_destination(
|
|
316
|
+
dynapcnn_layer_info,
|
|
317
|
+
edge,
|
|
318
|
+
node_2_layer_map,
|
|
319
|
+
nodes_io_shapes,
|
|
320
|
+
indx_2_module_map,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Add pooling based on neuron->pooling connections
|
|
324
|
+
pooling_pooling_edges = edges_by_type.get("pooling-pooling", set())
|
|
325
|
+
neuron_pooling_edges = edges_by_type.get("neuron-pooling", set())
|
|
326
|
+
while neuron_pooling_edges:
|
|
327
|
+
edge = neuron_pooling_edges.pop()
|
|
328
|
+
# Search pooling-pooling edges for chains of pooling and add to existing entry
|
|
329
|
+
pooling_chains, edges_used = trace_paths(edge[1], pooling_pooling_edges)
|
|
330
|
+
add_pooling_to_entry(
|
|
331
|
+
dynapcnn_layer_info,
|
|
332
|
+
edge,
|
|
333
|
+
pooling_chains,
|
|
334
|
+
indx_2_module_map,
|
|
335
|
+
node_2_layer_map,
|
|
336
|
+
)
|
|
337
|
+
# Remove handled pooling-pooling edges
|
|
338
|
+
pooling_pooling_edges.difference_update(edges_used)
|
|
339
|
+
|
|
340
|
+
# After adding pooling make sure all pooling-pooling edges have been handled
|
|
341
|
+
if len(pooling_pooling_edges) > 0:
|
|
342
|
+
unmatched_layers = {edge[0] for edge in pooling_pooling_edges}
|
|
343
|
+
raise InvalidGraphStructure(
|
|
344
|
+
f"Pooling layers {unmatched_layers} could not be assigned to a "
|
|
345
|
+
"DynapCNN layer. This is likely due to an unsupported SNN "
|
|
346
|
+
"architecture. Pooling layers must always be preceded by a "
|
|
347
|
+
"spiking layer (`IAFSqueeze`), another pooling layer, or"
|
|
348
|
+
"DVS input"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Add all edges connecting pooling to a new dynapcnn layer
|
|
352
|
+
pooling_weight_edges = edges_by_type.get("pooling-weight", set())
|
|
353
|
+
while pooling_weight_edges:
|
|
354
|
+
edge = pooling_weight_edges.pop()
|
|
355
|
+
set_pooling_layer_destination(
|
|
356
|
+
dynapcnn_layer_info,
|
|
357
|
+
edge,
|
|
358
|
+
node_2_layer_map,
|
|
359
|
+
nodes_io_shapes,
|
|
360
|
+
indx_2_module_map,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Make sure we have taken care of all edges
|
|
364
|
+
assert all(len(edges) == 0 for edges in edges_by_type.values())
|
|
365
|
+
|
|
366
|
+
# Set minimal destination entries for layers without child nodes, to act as network outputs
|
|
367
|
+
set_exit_destinations(dynapcnn_layer_info)
|
|
368
|
+
|
|
369
|
+
# Assert formal correctness of layer info
|
|
370
|
+
verify_layer_info(dynapcnn_layer_info, edge_counts_by_type)
|
|
371
|
+
|
|
372
|
+
return dynapcnn_layer_info, dvs_layer_info
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def get_valid_edge_type(
|
|
376
|
+
edge: Edge,
|
|
377
|
+
layers: Dict[int, nn.Module],
|
|
378
|
+
valid_edge_ids: Dict[Tuple[Type, Type], int],
|
|
379
|
+
) -> int:
|
|
380
|
+
"""Checks if the modules each node in 'edge' represent are a valid
|
|
381
|
+
connection between a sinabs network to be loaded on Speck and return the
|
|
382
|
+
edge type.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
edge (tuple of two int): The edge whose type is to be inferred.
|
|
386
|
+
layers (Dict): Dict with node IDs as keys and layer instances as values.
|
|
387
|
+
valid_edge_ids: Dict with valid edge-types (tuples of Types) as keys
|
|
388
|
+
and edge-type-ID as value
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
The edge type specified in 'valid_edges_map' ('None' if edge is not valid).
|
|
392
|
+
"""
|
|
393
|
+
source_type = type(layers[edge[0]])
|
|
394
|
+
target_type = type(layers[edge[1]])
|
|
395
|
+
|
|
396
|
+
return valid_edge_ids.get((source_type, target_type), None)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def sort_edges_by_type(
|
|
400
|
+
edges: Set[Edge], indx_2_module_map: Dict[int, Type]
|
|
401
|
+
) -> Dict[str, Set[Edge]]:
|
|
402
|
+
"""Sort edges by the type of nodes they connect
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
edges (set of tuples): Represent connections between two nodes in
|
|
406
|
+
computational graph.
|
|
407
|
+
indx_2_module_map (dict): Maps node IDs of the graph as `key` to their
|
|
408
|
+
associated module as `value`.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Dictionary with possible keys "weight-neuron", "neuron-weight",
|
|
412
|
+
"neuron-pooling", "pooling-pooling", and "pooling-weight".
|
|
413
|
+
Values are sets of edges corresponding to these types.
|
|
414
|
+
"""
|
|
415
|
+
edges_by_type: Dict[str, Set[Edge]] = dict()
|
|
416
|
+
|
|
417
|
+
for edge in edges:
|
|
418
|
+
edge_type = get_valid_edge_type(
|
|
419
|
+
edge, indx_2_module_map, VALID_SINABS_EDGE_TYPES
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# Validate edge type
|
|
423
|
+
if edge_type is None:
|
|
424
|
+
raise InvalidEdge(
|
|
425
|
+
edge, type(indx_2_module_map[edge[0]]), type(indx_2_module_map[edge[1]])
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
if edge_type in edges_by_type:
|
|
429
|
+
edges_by_type[edge_type].add(edge)
|
|
430
|
+
else:
|
|
431
|
+
edges_by_type[edge_type] = {edge}
|
|
432
|
+
|
|
433
|
+
return edges_by_type
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def init_new_dynapcnnlayer_entry(
|
|
437
|
+
dynapcnn_layer_info: Dict[int, Dict[int, Dict]],
|
|
438
|
+
edge: Edge,
|
|
439
|
+
indx_2_module_map: Dict[int, nn.Module],
|
|
440
|
+
nodes_io_shapes: Dict[int, Dict[str, Tuple[Size, Size]]],
|
|
441
|
+
node_2_layer_map: Dict[int, int],
|
|
442
|
+
entry_nodes: Set[int],
|
|
443
|
+
) -> None:
|
|
444
|
+
"""Initiate dict to hold information for new dynapcnn layer based on a
|
|
445
|
+
"weight->neuron" edge. Change `dynapcnn_layer_info` in-place.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
dynapcnn_layer_info: Dict with one entry for each future dynapcnn
|
|
449
|
+
layer. Key is unique dynapcnn layer ID, value is dict with nodes of
|
|
450
|
+
the layer will be updated in-place.
|
|
451
|
+
edge: Tuple of 2 integers, indicating edge between two nodes in graph.
|
|
452
|
+
Edge source has to be within an existing entry of
|
|
453
|
+
`dynapcnn_layer_info`.
|
|
454
|
+
indx_2_module_map (dict): Maps node IDs of the graph as `key` to their
|
|
455
|
+
associated module as `value`.
|
|
456
|
+
nodes_io_shapes (dict): Map from node ID to dict containing node's in-
|
|
457
|
+
and output shapes.
|
|
458
|
+
node_2_layer_map (dict): Maps each node ID to the ID of the layer it is
|
|
459
|
+
assigned to. Will be updated in-place.
|
|
460
|
+
entry_nodes (set of int): IDs of nodes that receive external input.
|
|
461
|
+
"""
|
|
462
|
+
# Make sure there are no existing entries holding any of the modules connected by `edge`
|
|
463
|
+
assert edge[0] not in node_2_layer_map
|
|
464
|
+
assert edge[1] not in node_2_layer_map
|
|
465
|
+
|
|
466
|
+
# Take current length of the dict as new, unique ID
|
|
467
|
+
layer_id = len(dynapcnn_layer_info)
|
|
468
|
+
assert layer_id not in dynapcnn_layer_info
|
|
469
|
+
|
|
470
|
+
dynapcnn_layer_info[layer_id] = {
|
|
471
|
+
"input_shape": nodes_io_shapes[edge[0]]["input"],
|
|
472
|
+
"conv": {
|
|
473
|
+
"module": indx_2_module_map[edge[0]],
|
|
474
|
+
"node_id": edge[0],
|
|
475
|
+
},
|
|
476
|
+
"neuron": {
|
|
477
|
+
"module": indx_2_module_map[edge[1]],
|
|
478
|
+
"node_id": edge[1],
|
|
479
|
+
},
|
|
480
|
+
# This will be used later to account for average pooling in preceding layers
|
|
481
|
+
"rescale_factors": set(),
|
|
482
|
+
"is_entry_node": edge[0] in entry_nodes,
|
|
483
|
+
# Will be populated by `set_[pooling/neuron]_layer_destination`
|
|
484
|
+
"destinations": [],
|
|
485
|
+
}
|
|
486
|
+
node_2_layer_map[edge[0]] = layer_id
|
|
487
|
+
node_2_layer_map[edge[1]] = layer_id
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def add_pooling_to_entry(
|
|
491
|
+
dynapcnn_layer_info: Dict[int, Dict],
|
|
492
|
+
edge: Edge,
|
|
493
|
+
pooling_chains: List[Deque[int]],
|
|
494
|
+
indx_2_module_map: Dict[int, nn.Module],
|
|
495
|
+
node_2_layer_map: Dict[int, int],
|
|
496
|
+
) -> None:
|
|
497
|
+
"""Add or extend destination information with pooling for existing entry in
|
|
498
|
+
`dynapcnn_layer_info`.
|
|
499
|
+
|
|
500
|
+
Correct entry is identified by existing neuron node. Destination information
|
|
501
|
+
is a dict containing list of IDs and list of modules for each chains of
|
|
502
|
+
pooling nodes.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
dynapcnn_layer_info: Dict with one entry for each future DynapCNN layer.
|
|
506
|
+
Key is unique DynapCNN layer ID, value is dict with nodes of the
|
|
507
|
+
layer. Will be updated in-place.
|
|
508
|
+
edge: Tuple of 2 integers, indicating edge between a neuron node and the
|
|
509
|
+
pooling node that starts all provided `pooling_chains`. Edge source
|
|
510
|
+
has to be a neuron node within an existing entry of
|
|
511
|
+
`dynapcnn_layer_info`, i.e. it has to have been processed already.
|
|
512
|
+
pooling_chains: List of deque of int. All sequences ("chains") of
|
|
513
|
+
connected pooling nodes, starting from edge[1].
|
|
514
|
+
indx_2_module_map (dict): Maps node IDs of the graph as `key` to their
|
|
515
|
+
associated module as `value`.
|
|
516
|
+
node_2_layer_map (dict): Maps each node ID to the ID of the layer it is
|
|
517
|
+
assigned to. Will be updated in-place.
|
|
518
|
+
"""
|
|
519
|
+
# Find layer containing edge[0]
|
|
520
|
+
try:
|
|
521
|
+
layer_idx = node_2_layer_map[edge[0]]
|
|
522
|
+
except KeyError:
|
|
523
|
+
neuron_layer = indx_2_module_map[edge[0]]
|
|
524
|
+
raise InvalidGraphStructure(
|
|
525
|
+
f"Spiking layer {neuron_layer} cannot be assigned to a DynapCNN layer. "
|
|
526
|
+
"This is likely due to an unsupported SNN architecture. Spiking "
|
|
527
|
+
"layers have to be preceded by a weight layer (`nn.Conv2d` or "
|
|
528
|
+
"`nn.Linear`)."
|
|
529
|
+
)
|
|
530
|
+
# Make sure all pooling chains start with expected node
|
|
531
|
+
assert all(chain[0] == edge[1] for chain in pooling_chains)
|
|
532
|
+
|
|
533
|
+
# Keep track of all nodes that have been added
|
|
534
|
+
new_nodes = set()
|
|
535
|
+
|
|
536
|
+
# For each pooling chain initialize new destination
|
|
537
|
+
layer_info = dynapcnn_layer_info[layer_idx]
|
|
538
|
+
for chain in pooling_chains:
|
|
539
|
+
layer_info["destinations"].append(
|
|
540
|
+
{
|
|
541
|
+
"pooling_ids": chain,
|
|
542
|
+
"pooling_modules": [indx_2_module_map[idx] for idx in chain],
|
|
543
|
+
# Setting `destination_layer` to `None` allows for this layer
|
|
544
|
+
# to act as network exit point if not destination is added later
|
|
545
|
+
"destination_layer": None,
|
|
546
|
+
}
|
|
547
|
+
)
|
|
548
|
+
new_nodes.update(set(chain))
|
|
549
|
+
|
|
550
|
+
for node in new_nodes:
|
|
551
|
+
# Make sure new pooling nodes have not been used elsewhere
|
|
552
|
+
assert node not in node_2_layer_map
|
|
553
|
+
node_2_layer_map[node] = layer_idx
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def dvs_setup(
|
|
557
|
+
edges_by_type: Dict[str, Set[Edge]],
|
|
558
|
+
indx_2_module_map: Dict[int, nn.Module],
|
|
559
|
+
node_2_layer_map: Dict[int, int],
|
|
560
|
+
nodes_io_shapes: Dict[int, Dict[str, Tuple[Size, Size]]],
|
|
561
|
+
) -> Union[None, Dict]:
|
|
562
|
+
"""Generate dict containing information to set up DVS layer.
|
|
563
|
+
|
|
564
|
+
Args:
|
|
565
|
+
edges_by_type (dict of sets of edges): Keys are edge types (str),
|
|
566
|
+
values are sets of edges.
|
|
567
|
+
indx_2_module_map (dict): Maps node IDs of the graph as `key` to their
|
|
568
|
+
associated module as `value`.
|
|
569
|
+
node_2_layer_map (dict): Maps each node ID to the ID of the layer it is
|
|
570
|
+
assigned to. Will be updated in-place.
|
|
571
|
+
nodes_io_shapes (dict): Map from node ID to dict containing node's in-
|
|
572
|
+
and output shapes
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
Dictionary containing information about the DVSLayer.
|
|
576
|
+
"""
|
|
577
|
+
# Process all outgoing edges of a DVSLayer
|
|
578
|
+
dvs_weight_edges = edges_by_type.get("dvs-weight", set())
|
|
579
|
+
dvs_pooling_edges = edges_by_type.get("dvs-pooling", set())
|
|
580
|
+
|
|
581
|
+
# Process all dvs->weight edges connecting the DVS camera to a dynapcnn layer.
|
|
582
|
+
if dvs_weight_edges:
|
|
583
|
+
if dvs_pooling_edges:
|
|
584
|
+
raise InvalidGraphStructure(
|
|
585
|
+
"DVS layer has destinations with and without pooling. Unlike "
|
|
586
|
+
"with CNN layers, pooling of the DVS has to be the same for "
|
|
587
|
+
"all destinations."
|
|
588
|
+
)
|
|
589
|
+
return init_dvs_entry(
|
|
590
|
+
dvs_weight_edges,
|
|
591
|
+
indx_2_module_map,
|
|
592
|
+
node_2_layer_map,
|
|
593
|
+
nodes_io_shapes,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
# Process dvs->pooling edges adding pooling to a DVS Layer
|
|
597
|
+
elif dvs_pooling_edges:
|
|
598
|
+
# Make sure there is exactly one dvs->pooling edge
|
|
599
|
+
if len(dvs_pooling_edges) > 1:
|
|
600
|
+
raise InvalidGraphStructure(
|
|
601
|
+
"DVSLayer has connects to multiple pooling layers. Unlike "
|
|
602
|
+
"with CNN layers, pooling of the DVS has to be the same for "
|
|
603
|
+
"all destinations, therefore the DVSLayer can connect to at "
|
|
604
|
+
"most one pooling layer."
|
|
605
|
+
)
|
|
606
|
+
dvs_pooling_edge = dvs_pooling_edges.pop()
|
|
607
|
+
# Find pooling-weight edges that connect DVS layer to dynapcnn layers.
|
|
608
|
+
pooling_weight_edges = edges_by_type.get("pooling-weight", set())
|
|
609
|
+
dvs_pooling_weight_edges = find_edges_by_source(
|
|
610
|
+
pooling_weight_edges, dvs_pooling_edge[1]
|
|
611
|
+
)
|
|
612
|
+
# Remove handled pooling-weight edges
|
|
613
|
+
pooling_weight_edges.difference_update(dvs_pooling_weight_edges)
|
|
614
|
+
|
|
615
|
+
return init_dvs_entry_with_pooling(
|
|
616
|
+
dvs_pooling_edge,
|
|
617
|
+
dvs_pooling_weight_edges,
|
|
618
|
+
indx_2_module_map,
|
|
619
|
+
node_2_layer_map,
|
|
620
|
+
nodes_io_shapes,
|
|
621
|
+
)
|
|
622
|
+
else:
|
|
623
|
+
# If no edges related to DVS have been found return None
|
|
624
|
+
return
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def init_dvs_entry(
|
|
628
|
+
dvs_weight_edges: Set[Edge],
|
|
629
|
+
indx_2_module_map: Dict[int, nn.Module],
|
|
630
|
+
node_2_layer_map: Dict[int, int],
|
|
631
|
+
nodes_io_shapes: Dict[int, Dict[str, Tuple[Size, Size]]],
|
|
632
|
+
) -> Dict:
|
|
633
|
+
"""Initiate dict to hold information for a DVS Layer configuration
|
|
634
|
+
based on "dvs-weight" edges.
|
|
635
|
+
|
|
636
|
+
Args:
|
|
637
|
+
dvs_weight_edges: Set of edges between two nodes in graph.
|
|
638
|
+
Edge source has to be a DVSLayer and the same for all edges.
|
|
639
|
+
Edge target has to be within an existing entry of
|
|
640
|
+
`dynapcnn_layer_info`.
|
|
641
|
+
indx_2_module_map (dict): Maps node IDs of the graph as `key` to their
|
|
642
|
+
associated module as `value`.
|
|
643
|
+
node_2_layer_map (dict): Maps each node ID to the ID of the layer it is
|
|
644
|
+
assigned to. Will be updated in-place.
|
|
645
|
+
nodes_io_shapes (dict): Map from node ID to dict containing node's in-
|
|
646
|
+
and output shapes
|
|
647
|
+
|
|
648
|
+
Returns:
|
|
649
|
+
Dictionary containing information about the DVSLayer.
|
|
650
|
+
"""
|
|
651
|
+
|
|
652
|
+
# Pick any of the edges in set to get the DVS node ID. Should be same for all.
|
|
653
|
+
dvs_node_id = next(dvs_weight_edges.__iter__())[0]
|
|
654
|
+
|
|
655
|
+
# This should never fail
|
|
656
|
+
if not all(edge[0] == dvs_node_id for edge in dvs_weight_edges):
|
|
657
|
+
raise InvalidGraphStructure(
|
|
658
|
+
"The provided network seems to consist of multiple DVS layers. "
|
|
659
|
+
"This is not supported."
|
|
660
|
+
)
|
|
661
|
+
assert isinstance(
|
|
662
|
+
(dvs_layer := indx_2_module_map[dvs_node_id]), DVSLayer
|
|
663
|
+
), f"Source node in edges {dvs_weight_edges} is of type {type(dvs_layer)} (it should be a DVSLayer instance)."
|
|
664
|
+
|
|
665
|
+
# Initialize dvs config dict
|
|
666
|
+
dvs_layer_info = {
|
|
667
|
+
"node_id": dvs_node_id,
|
|
668
|
+
"input_shape": nodes_io_shapes[dvs_node_id]["input"],
|
|
669
|
+
"module": dvs_layer,
|
|
670
|
+
"pooling": None,
|
|
671
|
+
}
|
|
672
|
+
node_2_layer_map[dvs_node_id] = "dvs"
|
|
673
|
+
|
|
674
|
+
# Find destination layer indices
|
|
675
|
+
destinations = []
|
|
676
|
+
while dvs_weight_edges:
|
|
677
|
+
edge = dvs_weight_edges.pop()
|
|
678
|
+
try:
|
|
679
|
+
destination_layer_idx = node_2_layer_map[edge[1]]
|
|
680
|
+
except KeyError:
|
|
681
|
+
weight_layer = indx_2_module_map[edge[1]]
|
|
682
|
+
raise InvalidGraphStructure(
|
|
683
|
+
f"Weight layer {weight_layer} cannot be assigned to a DynapCNN layer. "
|
|
684
|
+
"This is likely due to an unsupported SNN architecture. Weight "
|
|
685
|
+
"layers have to be followed by a spiking layer (`sl.IAFSqueeze`)."
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
# Update entry for DVS with new destination.
|
|
689
|
+
assert destination_layer_idx not in destinations
|
|
690
|
+
destinations.append(destination_layer_idx)
|
|
691
|
+
|
|
692
|
+
if destinations:
|
|
693
|
+
dvs_layer_info["destinations"] = destinations
|
|
694
|
+
else:
|
|
695
|
+
dvs_layer_info["destinations"] = None
|
|
696
|
+
|
|
697
|
+
return dvs_layer_info
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def init_dvs_entry_with_pooling(
|
|
701
|
+
dvs_pooling_edge: Edge,
|
|
702
|
+
pooling_weight_edges: Set[Edge],
|
|
703
|
+
indx_2_module_map: Dict[int, nn.Module],
|
|
704
|
+
node_2_layer_map: Dict[int, int],
|
|
705
|
+
nodes_io_shapes: Dict[int, Dict[str, Tuple[Size, Size]]],
|
|
706
|
+
) -> Dict:
|
|
707
|
+
"""Initiate dict to hold information for a DVS Layer configuration with additional pooling
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
dvs_pooling_edge: Edge from DVSLayer to pooling layer.
|
|
711
|
+
pooling_weight_edges: Set of edges between pooling layer and weight
|
|
712
|
+
layer. Edge source has to be the target of `dvs_pooling_edge`.
|
|
713
|
+
Edge targets have to be within an existing entry of
|
|
714
|
+
`dynapcnn_layer_info`.
|
|
715
|
+
indx_2_module_map (dict): Maps node IDs of the graph as `key` to their
|
|
716
|
+
associated module as `value`.
|
|
717
|
+
node_2_layer_map (dict): Maps each node ID to the ID of the layer it is
|
|
718
|
+
assigned to. Will be updated in-place.
|
|
719
|
+
nodes_io_shapes (dict): Map from node ID to dict containing node's in-
|
|
720
|
+
and output shapes
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
Dictionary containing information about the DVSLayer.
|
|
724
|
+
"""
|
|
725
|
+
|
|
726
|
+
dvs_node_id, pooling_id = dvs_pooling_edge
|
|
727
|
+
|
|
728
|
+
# This should never fail
|
|
729
|
+
assert all(edge[0] == pooling_id for edge in pooling_weight_edges)
|
|
730
|
+
assert isinstance(
|
|
731
|
+
(dvs_layer := indx_2_module_map[dvs_node_id]), DVSLayer
|
|
732
|
+
), f"Source node in edge {dvs_pooling_edge} is of type {type(dvs_layer)} (it should be a DVSLayer instance)."
|
|
733
|
+
|
|
734
|
+
# Initialize dvs config dict
|
|
735
|
+
dvs_layer_info = {
|
|
736
|
+
"node_id": dvs_node_id,
|
|
737
|
+
"input_shape": nodes_io_shapes[dvs_node_id]["input"],
|
|
738
|
+
"module": dvs_layer,
|
|
739
|
+
"pooling": {"module": indx_2_module_map[pooling_id], "node_id": pooling_id},
|
|
740
|
+
}
|
|
741
|
+
node_2_layer_map[dvs_node_id] = "dvs"
|
|
742
|
+
|
|
743
|
+
# Find destination layer indices
|
|
744
|
+
destinations = []
|
|
745
|
+
for edge in pooling_weight_edges:
|
|
746
|
+
try:
|
|
747
|
+
destination_layer_idx = node_2_layer_map[edge[1]]
|
|
748
|
+
except KeyError:
|
|
749
|
+
weight_layer = indx_2_module_map[edge[1]]
|
|
750
|
+
raise InvalidGraphStructure(
|
|
751
|
+
f"Weight layer {weight_layer} cannot be assigned to a DynapCNN layer. "
|
|
752
|
+
"This is likely due to an unsupported SNN architecture. Weight "
|
|
753
|
+
"layers have to be followed by a spiking layer (`sl.IAFSqueeze`)."
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
# Update entry for DVS with new destination.
|
|
757
|
+
assert destination_layer_idx not in destinations
|
|
758
|
+
destinations.append(destination_layer_idx)
|
|
759
|
+
|
|
760
|
+
if destinations:
|
|
761
|
+
dvs_layer_info["destinations"] = destinations
|
|
762
|
+
else:
|
|
763
|
+
dvs_layer_info["destinations"] = None
|
|
764
|
+
|
|
765
|
+
return dvs_layer_info
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def set_exit_destinations(dynapcnn_layer: Dict) -> None:
|
|
769
|
+
"""Set minimal destination entries for layers that don't have any.
|
|
770
|
+
|
|
771
|
+
This ensures that the forward methods of the resulting DynapcnnLayer
|
|
772
|
+
instances return an output, letting these layers act as exit points
|
|
773
|
+
of the network.
|
|
774
|
+
The destination layer will be `None`, and no pooling applied.
|
|
775
|
+
|
|
776
|
+
Args:
|
|
777
|
+
dynapcnn_layer_info: Dict with one entry for each future dynapcnn layer.
|
|
778
|
+
Key is unique dynapcnn layer ID, value is dict with nodes of the
|
|
779
|
+
layer. Will be updated in-place.
|
|
780
|
+
"""
|
|
781
|
+
for layer_info in dynapcnn_layer.values():
|
|
782
|
+
if not (destinations := layer_info["destinations"]):
|
|
783
|
+
# Add `None` destination to empty destination lists
|
|
784
|
+
destinations.append(
|
|
785
|
+
{
|
|
786
|
+
"pooling_ids": [],
|
|
787
|
+
"pooling_modules": [],
|
|
788
|
+
"destination_layer": None,
|
|
789
|
+
}
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def set_neuron_layer_destination(
|
|
794
|
+
dynapcnn_layer_info: Dict[int, Dict],
|
|
795
|
+
edge: Edge,
|
|
796
|
+
node_2_layer_map: Dict[int, int],
|
|
797
|
+
nodes_io_shapes: Dict[int, Dict[str, Tuple[Size, Size]]],
|
|
798
|
+
indx_2_module_map: Dict[int, nn.Module],
|
|
799
|
+
) -> None:
|
|
800
|
+
"""Set destination layer without pooling for existing entry in `dynapcnn_layer_info`.
|
|
801
|
+
|
|
802
|
+
Args:
|
|
803
|
+
dynapcnn_layer_info: Dict with one entry for each future DynapCNN layer.
|
|
804
|
+
Key is unique DynapCNN layer ID, value is dict with nodes of the
|
|
805
|
+
layer. Will be updated in-place.
|
|
806
|
+
edge: Tuple of 2 integers, indicating edge between two nodes in graph.
|
|
807
|
+
Edge source has to be a neuron layer within an existing entry of
|
|
808
|
+
`dynapcnn_layer_info`. Edge target has to be the weight layer of
|
|
809
|
+
another DynapCNN layer.
|
|
810
|
+
node_2_layer_map (dict): Maps each node ID to the ID of the layer it is
|
|
811
|
+
assigned to. Will be updated in-place.
|
|
812
|
+
nodes_io_shapes (dict): Map from node ID to dict containing node's in-
|
|
813
|
+
and output shapes
|
|
814
|
+
indx_2_module_map (dict): Maps node IDs of the graph as `key` to their
|
|
815
|
+
associated module as `value`
|
|
816
|
+
"""
|
|
817
|
+
# Make sure both source (neuron layer) and target (weight layer) have been previously processed
|
|
818
|
+
try:
|
|
819
|
+
source_layer_idx = node_2_layer_map[edge[0]]
|
|
820
|
+
except KeyError:
|
|
821
|
+
neuron_layer = indx_2_module_map[edge[0]]
|
|
822
|
+
raise InvalidGraphStructure(
|
|
823
|
+
f"Spiking layer {neuron_layer} cannot be assigned to a DynapCNN layer. "
|
|
824
|
+
"This is likely due to an unsupported SNN architecture. Spiking "
|
|
825
|
+
"layers have to be preceded by a weight layer (`nn.Conv2d` or "
|
|
826
|
+
"`nn.Linear`)."
|
|
827
|
+
)
|
|
828
|
+
try:
|
|
829
|
+
destination_layer_idx = node_2_layer_map[edge[1]]
|
|
830
|
+
except KeyError:
|
|
831
|
+
weight_layer = indx_2_module_map[edge[1]]
|
|
832
|
+
raise InvalidGraphStructure(
|
|
833
|
+
f"Weight layer {weight_layer} cannot be assigned to a DynapCNN layer. "
|
|
834
|
+
"This is likely due to an unsupported SNN architecture. Weight "
|
|
835
|
+
"layers have to be followed by a spiking layer (`IAFSqueeze`)."
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
# Add new destination
|
|
839
|
+
output_shape = nodes_io_shapes[edge[0]]["output"]
|
|
840
|
+
layer_info = dynapcnn_layer_info[source_layer_idx]
|
|
841
|
+
layer_info["destinations"].append(
|
|
842
|
+
{
|
|
843
|
+
"pooling_ids": [],
|
|
844
|
+
"pooling_modules": [],
|
|
845
|
+
"destination_layer": destination_layer_idx,
|
|
846
|
+
"output_shape": output_shape,
|
|
847
|
+
}
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def set_pooling_layer_destination(
|
|
852
|
+
dynapcnn_layer_info: Dict[int, Dict],
|
|
853
|
+
edge: Edge,
|
|
854
|
+
node_2_layer_map: Dict[int, int],
|
|
855
|
+
nodes_io_shapes: Dict[int, Dict[str, Tuple[Size, Size]]],
|
|
856
|
+
indx_2_module_map: Dict[int, nn.Module],
|
|
857
|
+
) -> None:
|
|
858
|
+
"""Set destination layer with pooling for existing entry in `dynapcnn_layer_info`.
|
|
859
|
+
|
|
860
|
+
Args:
|
|
861
|
+
dynapcnn_layer_info: Dict with one entry for each future DynapCNN layer.
|
|
862
|
+
Key is unique DynapCNN layer ID, value is dict with nodes of the
|
|
863
|
+
layer. Will be updated in-place.
|
|
864
|
+
edge: Tuple of 2 integers, indicating edge between two nodes in graph.
|
|
865
|
+
Edge source has to be a pooling layer that is at the end of at
|
|
866
|
+
least one pooling chain within an existing entry of
|
|
867
|
+
`dynapcnn_layer_info`. Edge target has to be a weight layer within
|
|
868
|
+
an existing entry of `dynapcnn_layer_info`.
|
|
869
|
+
node_2_layer_map (dict): Maps each node ID to the ID of the layer it is
|
|
870
|
+
assigned to. Will be updated in-place.
|
|
871
|
+
nodes_io_shapes (dict): Map from node ID to dict containing node's in-
|
|
872
|
+
and output shapes
|
|
873
|
+
indx_2_module_map (dict): Maps node IDs of the graph as `key` to their
|
|
874
|
+
associated module as `value`
|
|
875
|
+
"""
|
|
876
|
+
# Make sure both source (pooling layer) and target (weight layer) have been previously processed
|
|
877
|
+
try:
|
|
878
|
+
source_layer_idx = node_2_layer_map[edge[0]]
|
|
879
|
+
except KeyError:
|
|
880
|
+
poolin_layer = indx_2_module_map[edge[0]]
|
|
881
|
+
raise InvalidGraphStructure(
|
|
882
|
+
f"Layer {poolin_layer} cannot be assigned to a DynapCNN layer. "
|
|
883
|
+
"This is likely due to an unsupported SNN architecture. Pooling "
|
|
884
|
+
"layers have to be preceded by a spiking layer (`IAFSqueeze`), "
|
|
885
|
+
"another pooling layer, or DVS input"
|
|
886
|
+
)
|
|
887
|
+
try:
|
|
888
|
+
destination_layer_idx = node_2_layer_map[edge[1]]
|
|
889
|
+
except KeyError:
|
|
890
|
+
weight_layer = indx_2_module_map[edge[1]]
|
|
891
|
+
raise InvalidGraphStructure(
|
|
892
|
+
f"Weight layer {weight_layer} cannot be assigned to a DynapCNN layer. "
|
|
893
|
+
"This is likely due to an unsupported SNN architecture. Weight "
|
|
894
|
+
"layers have to be preceded by a spiking layer (`IAFSqueeze`), "
|
|
895
|
+
"another pooling layer, or DVS input"
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
# Find current source node within destinations
|
|
899
|
+
layer_info = dynapcnn_layer_info[source_layer_idx]
|
|
900
|
+
matched = False
|
|
901
|
+
for destination in layer_info["destinations"]:
|
|
902
|
+
if destination["pooling_ids"][-1] == edge[0]:
|
|
903
|
+
if destination["destination_layer"] is not None:
|
|
904
|
+
# Destination is already linked to a postsynaptic layer. This happens when
|
|
905
|
+
# pooling nodes have outgoing edges to different weight layer.
|
|
906
|
+
# Copy the destination
|
|
907
|
+
# TODO: Add unit test for this case
|
|
908
|
+
destination = {k: v for k, v in destination.items()}
|
|
909
|
+
layer_info["destinations"].append(destination)
|
|
910
|
+
matched = True
|
|
911
|
+
break
|
|
912
|
+
if not matched:
|
|
913
|
+
pooling_layer = indx_2_module_map[edge[0]]
|
|
914
|
+
raise InvalidGraphStructure(
|
|
915
|
+
f"Layer {pooling_layer} cannot be assigned to a DynapCNN layer. "
|
|
916
|
+
"This is likely due to an unsupported SNN architecture. Pooling "
|
|
917
|
+
"layers have to be preceded by a spiking layer (`IAFSqueeze`), "
|
|
918
|
+
"another pooling layer, or DVS input"
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
# Set destination layer within destination dict that holds current source node
|
|
922
|
+
destination["destination_layer"] = destination_layer_idx
|
|
923
|
+
output_shape = nodes_io_shapes[edge[0]]["output"]
|
|
924
|
+
destination["output_shape"] = output_shape
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def trace_paths(node: int, remaining_edges: Set[Edge]) -> List[Deque[int]]:
|
|
928
|
+
"""Trace any path of collected edges through the graph.
|
|
929
|
+
|
|
930
|
+
Start with `node`, and recursively look for paths of connected nodes
|
|
931
|
+
within `remaining edges.`
|
|
932
|
+
|
|
933
|
+
Args:
|
|
934
|
+
node (int): ID of current node.
|
|
935
|
+
remaining_edges: Set of remaining edges still to be searched.
|
|
936
|
+
|
|
937
|
+
Returns:
|
|
938
|
+
List of deque of int, all paths of connected edges starting from `node`
|
|
939
|
+
and a set of edges that are part of the returned paths.
|
|
940
|
+
"""
|
|
941
|
+
paths = []
|
|
942
|
+
processed_edges = set()
|
|
943
|
+
for src, tgt in remaining_edges:
|
|
944
|
+
if src == node:
|
|
945
|
+
processed_edges.add((src, tgt))
|
|
946
|
+
# For each edge with `node` as source, find subsequent pooling nodes recursively
|
|
947
|
+
new_remaining = remaining_edges.difference({(src, tgt)})
|
|
948
|
+
branches, new_processed = trace_paths(tgt, new_remaining)
|
|
949
|
+
# Make sure no edge was processed twice
|
|
950
|
+
assert len(processed_edges.intersection(new_processed)) == 0
|
|
951
|
+
|
|
952
|
+
# Keep track of newly processed edges
|
|
953
|
+
processed_edges.update(new_processed)
|
|
954
|
+
|
|
955
|
+
# Collect all branching paths of pooling, inserting src at beginning
|
|
956
|
+
for branch in branches:
|
|
957
|
+
branch.appendleft(src)
|
|
958
|
+
paths.append(branch)
|
|
959
|
+
|
|
960
|
+
if not paths:
|
|
961
|
+
# End of recursion: instantiate a deque only with node
|
|
962
|
+
paths = [Deque([node])]
|
|
963
|
+
|
|
964
|
+
return paths, processed_edges
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def find_edges_by_source(edges: Set[Edge], source: int) -> Set[Edge]:
|
|
968
|
+
"""Utility function to find all edges with a given source node.
|
|
969
|
+
|
|
970
|
+
Args:
|
|
971
|
+
edges: Set of `Edge` instances to be searched.
|
|
972
|
+
source (int): Node ID that returned edges should have as source.
|
|
973
|
+
|
|
974
|
+
Returns:
|
|
975
|
+
All sets from `edges` that have `source` as source.
|
|
976
|
+
"""
|
|
977
|
+
return {(src, tgt) for (src, tgt) in edges if src == source}
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
def verify_layer_info(
|
|
981
|
+
dynapcnn_layer_info: Dict[int, Dict], edge_counts: Optional[Dict[str, int]] = None
|
|
982
|
+
):
|
|
983
|
+
"""Verify that `dynapcnn_layer_info` matches formal requirements.
|
|
984
|
+
|
|
985
|
+
Every layer needs to have at least a `conv`, `neuron`, and `destinations`
|
|
986
|
+
entry.
|
|
987
|
+
If `edge_counts` is provided, also make sure that number of layer matches
|
|
988
|
+
numbers of edges.
|
|
989
|
+
|
|
990
|
+
Args:
|
|
991
|
+
dynapcnn_layer_info: Dict with information to construct and connect
|
|
992
|
+
DynapcnnLayer instances
|
|
993
|
+
edge_counts: Optional Dict with edge counts for each edge type. If not
|
|
994
|
+
`None`, will be used to do further verifications on
|
|
995
|
+
`dynapcnn_layer_info`.
|
|
996
|
+
|
|
997
|
+
Raises:
|
|
998
|
+
InvalidGraphStructure: if any verification fails.
|
|
999
|
+
"""
|
|
1000
|
+
|
|
1001
|
+
# Make sure that each dynapcnn layer has at least a weight layer and a neuron layer
|
|
1002
|
+
for idx, info in dynapcnn_layer_info.items():
|
|
1003
|
+
if not "conv" in info:
|
|
1004
|
+
raise InvalidGraphStructure(
|
|
1005
|
+
f"DynapCNN layer {idx} has no weight assigned. "
|
|
1006
|
+
+ default_invalid_structure_string
|
|
1007
|
+
)
|
|
1008
|
+
if not "neuron" in info:
|
|
1009
|
+
raise InvalidGraphStructure(
|
|
1010
|
+
f"DynapCNN layer {idx} has no spiking layer assigned. "
|
|
1011
|
+
+ default_invalid_structure_string
|
|
1012
|
+
)
|
|
1013
|
+
if not "destinations" in info:
|
|
1014
|
+
raise InvalidGraphStructure(
|
|
1015
|
+
f"DynapCNN layer {idx} has no destination info assigned. "
|
|
1016
|
+
+ default_invalid_structure_string
|
|
1017
|
+
)
|
|
1018
|
+
if edge_counts is not None:
|
|
1019
|
+
# Make sure there are as many layers as edges from weight to neuron
|
|
1020
|
+
if edge_counts.get("weight-neuron", 0) - len(dynapcnn_layer_info) > 0:
|
|
1021
|
+
raise InvalidGraphStructure(
|
|
1022
|
+
"Not all weight-to-neuron edges have been processed. "
|
|
1023
|
+
+ default_invalid_structure_string
|
|
1024
|
+
)
|