Skip to content

Commit 0925a3d

Browse files
committed
complete implementation of seperable -> dw + pw, untested
1 parent 1c8c9ed commit 0925a3d

File tree

6 files changed

+219
-5
lines changed

6 files changed

+219
-5
lines changed

hls4ml/backends/vivado/passes/convolution_templates.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ def format(self, node):
280280
# Override bias and bias_t since these are zeros in depthwise step of SepConv1D
281281
params['bias'] = params['zero_bias']
282282
params['bias_t'] = params['zero_bias_t']
283-
params['n_filt'] = params['n_chan'] # In depthwise step n_chan == n_filt
283+
params['n_filt'] = params['n_chan'] * node.get_attr('depth_multiplier') # In depthwise step n_chan == n_filt
284284
params['dilation'] = node.get_attr('dilation', 1)
285285
params['nzeros'] = node.get_weights('depthwise').nzeros
286286
params['index'] = str(node.index) + '_depthwise'

hls4ml/converters/keras/convolution.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ def parse_conv2d_layer(keras_layer, input_names, input_shapes, data_reader):
6060

6161
layer['bias_data'] = get_weights_data(data_reader, layer['name'], 'bias')
6262

63+
if 'depth_multiplier' in keras_layer['config']:
64+
layer['depth_multiplier'] = keras_layer['config']['depth_multiplier']
65+
6366
if 'filters' in keras_layer['config']:
6467
layer['n_filt'] = keras_layer['config']['filters']
6568
else:

hls4ml/model/graph.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,44 @@ def replace_node(self, old_node, new_node):
615615
self.graph = OrderedDict((new_node.name, new_node) if k == old_node.name else (k, v) for k, v in self.graph.items())
616616
self._update_model_outputs()
617617

618+
def split_node(self, old_node, new_node1, new_node2):
619+
"""Replace an existing node in the graph with two nodes in sequence.
620+
621+
Args:
622+
old_node (Layer): The node to replace
623+
new_node1 (Layer): The first new node in sequence
624+
new_node2 (Layer): The second new node in sequence
625+
626+
"""
627+
628+
# fmt: off
629+
assert len(new_node1.inputs) == len(old_node.inputs), \
630+
f'{new_node1.name} and {old_node.name} have different number of inputs'
631+
assert len(new_node2.outputs) == len(old_node.outputs), \
632+
f'{new_node2.name} and {old_node.name} have different number of outputs'
633+
# fmt: on
634+
635+
repl = {old_name: new_name for old_name, new_name in zip(old_node.outputs, new_node2.outputs)}
636+
repl.update({old_name: new_name for old_name, new_name in zip(old_node.inputs, new_node1.inputs)})
637+
638+
for node in self.graph.values():
639+
for i, n in enumerate(node.inputs):
640+
if n in repl:
641+
node.inputs[i] = repl[n]
642+
for i, n in enumerate(node.outputs):
643+
if n in repl:
644+
node.outputs[i] = repl[n]
645+
646+
new_graph = OrderedDict()
647+
for key, value in self.graph.items():
648+
if key == old_node.name:
649+
new_graph[new_node1.name] = new_node1
650+
new_graph[new_node2.name] = new_node2
651+
else:
652+
new_graph[key] = value
653+
self.graph = new_graph
654+
self._update_model_outputs()
655+
618656
def _update_model_outputs(self):
619657
'''Update the model outputs
620658

hls4ml/model/layers.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,7 @@ class SeparableConv1D(Layer):
447447
Attribute('out_width'),
448448
Attribute('n_chan'),
449449
Attribute('n_filt'),
450+
Attribute('depth_multiplier', default=1),
450451
Attribute('filt_width'),
451452
Attribute('stride_width'),
452453
Attribute('pad_left'),
@@ -484,12 +485,27 @@ def initialize(self):
484485

485486

486487
class DepthwiseConv1D(Conv1D):
488+
_expected_attributes = [
489+
Attribute('in_width'),
490+
Attribute('out_width'),
491+
Attribute('n_chan'),
492+
Attribute('depth_multiplier', default=1),
493+
Attribute('filt_width'),
494+
Attribute('stride_width'),
495+
Attribute('pad_left'),
496+
Attribute('pad_right'),
497+
WeightAttribute('depthwise'),
498+
WeightAttribute('bias'),
499+
TypeAttribute('depthwise'),
500+
TypeAttribute('bias'),
501+
]
502+
487503
def initialize(self):
488504
if self.get_attr('data_format') == 'channels_last':
489-
shape = [self.attributes['out_width'], self.attributes['n_chan']]
505+
shape = [self.attributes['out_width'], self.attributes['n_chan'] * self.attributes['depth_multiplier']]
490506
dims = [f'OUT_HEIGHT_{self.index}', f'N_CHAN_{self.index}']
491507
else:
492-
shape = [self.attributes['n_chan'], self.attributes['out_width']]
508+
shape = [self.attributes['n_chan'] * self.attributes['depth_multiplier'], self.attributes['out_width']]
493509
dims = [f'N_CHAN_{self.index}', f'OUT_WIDTH_{self.index}']
494510
self.add_output_variable(shape, dims)
495511

@@ -498,6 +514,7 @@ def initialize(self):
498514
)
499515

500516
self.add_bias(quantizer=self.get_attr('bias_quantizer'))
517+
self.set_attr('n_filt', self.get_attr('n_chan') * self.get_attr('depth_multiplier'))
501518

502519

503520
class Conv2D(Layer):
@@ -594,6 +611,7 @@ class SeparableConv2D(Layer):
594611
Attribute('out_width'),
595612
Attribute('n_chan'),
596613
Attribute('n_filt'),
614+
Attribute('depth_multiplier', default=1),
597615
Attribute('filt_height'),
598616
Attribute('filt_width'),
599617
Attribute('stride_height'),
@@ -634,12 +652,41 @@ def initialize(self):
634652

635653

636654
class DepthwiseConv2D(Conv2D):
655+
_expected_attributes = [
656+
Attribute('in_height'),
657+
Attribute('in_width'),
658+
Attribute('out_height'),
659+
Attribute('out_width'),
660+
Attribute('n_chan'),
661+
Attribute('depth_multiplier', default=1),
662+
Attribute('filt_height'),
663+
Attribute('filt_width'),
664+
Attribute('stride_height'),
665+
Attribute('stride_width'),
666+
Attribute('pad_top'),
667+
Attribute('pad_bottom'),
668+
Attribute('pad_left'),
669+
Attribute('pad_right'),
670+
WeightAttribute('weight'),
671+
WeightAttribute('bias'),
672+
TypeAttribute('weight'),
673+
TypeAttribute('bias'),
674+
]
675+
637676
def initialize(self):
638677
if self.get_attr('data_format') == 'channels_last':
639-
shape = [self.attributes['out_height'], self.attributes['out_width'], self.attributes['n_chan']]
678+
shape = [
679+
self.attributes['out_height'],
680+
self.attributes['out_width'],
681+
self.attributes['n_chan'] * self.attributes['depth_multiplier'],
682+
]
640683
dims = [f'OUT_HEIGHT_{self.index}', f'OUT_WIDTH_{self.index}', f'N_CHAN_{self.index}']
641684
else:
642-
shape = [self.attributes['n_chan'], self.attributes['out_height'], self.attributes['out_width']]
685+
shape = [
686+
self.attributes['n_chan'] * self.attributes['depth_multiplier'],
687+
self.attributes['out_height'],
688+
self.attributes['out_width'],
689+
]
643690
dims = [f'N_CHAN_{self.index}', f'OUT_HEIGHT_{self.index}', f'OUT_WIDTH_{self.index}']
644691
self.add_output_variable(shape, dims)
645692

@@ -648,6 +695,7 @@ def initialize(self):
648695
)
649696

650697
self.add_bias(quantizer=self.get_attr('bias_quantizer'))
698+
self.set_attr('n_filt', self.get_attr('n_chan') * self.get_attr('depth_multiplier'))
651699

652700

653701
class Pooling1D(Layer):

hls4ml/model/optimizer/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
register_flow(
3434
'convert',
3535
[
36+
'seperable_to_depthwise_and_conv', # has to be before precision inference
3637
'infer_precision_types',
3738
'channels_last_converter',
3839
'remove_transpose_before_flatten',
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""
2+
This optimizer converts a seperable convolution to a depthwise followed by a regular convolution.
3+
For backends with a custom pointwise implementations the regular convolution will subsequently
4+
be converted to a pointwise convolution by a different optimizer.
5+
"""
6+
7+
import copy
8+
9+
from hls4ml.model.layers import SeparableConv1D, SeparableConv2D
10+
from hls4ml.model.optimizer import OptimizerPass
11+
12+
13+
class SeperableToDepthwiseAndConv(OptimizerPass):
14+
"""Convert Seperable to DepthwiseConv + Conv (potentially later Pointwise)"""
15+
16+
_dw_attributes = (
17+
'in_width',
18+
'out_width',
19+
'n_chan',
20+
'depth_multiplier',
21+
'pad_left',
22+
'pad_right',
23+
'filt_width',
24+
'stride_width',
25+
'dilation_width',
26+
'in_height',
27+
'out_height',
28+
'pad_top',
29+
'pad_bottom',
30+
'filt_height',
31+
'stride_height',
32+
'dilation_height',
33+
'data_format',
34+
'depthwise_data',
35+
'depthwise_quantizer',
36+
)
37+
38+
_pw_attributes = ('out_width', 'n_filt', 'dilation_width', 'out_height', 'dilation_height', 'data_format', 'use_bias')
39+
40+
def match(self, node):
41+
return isinstance(node, (SeparableConv1D, SeparableConv2D))
42+
43+
def transform(self, model, node):
44+
dim = node.__class__.__name__[-2:] # '1D' or '2D'
45+
46+
# get the layer configuration name
47+
layer_config = model.config.get_layer_config(node)
48+
49+
# First do depthwise
50+
dw_name = f'{node.name}_depthwise'
51+
52+
# now the layer config (so that set configuration get copied)
53+
dw_layer_config = copy.deepcopy(layer_config)
54+
55+
if dw_layer_config:
56+
dw_precision_cfg = dw_layer_config.setdefault('Precision', {})
57+
if 'depthwise' in dw_precision_cfg:
58+
dw_precision_cfg['weight'] = dw_precision_cfg['depthwise']
59+
del dw_precision_cfg['depthwise']
60+
if 'depthwise_accum' in dw_precision_cfg:
61+
dw_precision_cfg['accum'] = dw_precision_cfg['depthwise_accum']
62+
del dw_precision_cfg['depthwise_accum']
63+
if 'depthwise_result' in dw_precision_cfg:
64+
dw_precision_cfg['result'] = dw_precision_cfg['depthwise_result']
65+
del dw_precision_cfg['depthwise_result']
66+
dw_precision_cfg.pop('pointwise', None)
67+
dw_precision_cfg.pop('pointwise_accum', None)
68+
model.config.set_name_config(dw_name, dw_layer_config)
69+
model.config.parse_name_config(dw_name, dw_layer_config)
70+
71+
# creating the attributes
72+
dw_attributes = {k: node.attributes.get(k, None) for k in SeperableToDepthwiseAndConv._dw_attributes}
73+
74+
dw_attributes['use_bias'] = False
75+
76+
new_dw = model.make_node('DepthwiseConv' + dim, dw_name, dw_attributes, [node.inputs[0]])
77+
78+
# Then do convolution
79+
pw_name = f'{node.name}_pointwise'
80+
81+
# now the layer config (so that set configuration get copied)
82+
pw_layer_config = copy.deepcopy(layer_config)
83+
84+
if pw_layer_config:
85+
pw_precision_cfg = pw_layer_config.setdefault('Precision', {})
86+
if 'pointwise' in pw_precision_cfg:
87+
pw_precision_cfg['weight'] = pw_precision_cfg['pointwise']
88+
del pw_precision_cfg['pointwise']
89+
if 'pointwise_accum' in pw_precision_cfg:
90+
pw_precision_cfg['accum'] = pw_precision_cfg['pointwise_accum']
91+
del pw_precision_cfg['pointwise_accum']
92+
if 'pointwise_result' in pw_precision_cfg:
93+
pw_precision_cfg['result'] = pw_precision_cfg['pointwise_result']
94+
del pw_precision_cfg['pointwise_result']
95+
pw_precision_cfg.pop('depthwise', None)
96+
pw_precision_cfg.pop('depthwise_accum', None)
97+
model.config.set_name_config(pw_name, pw_layer_config)
98+
model.config.parse_name_config(pw_name, pw_layer_config)
99+
100+
# creating the attributes
101+
pw_attributes = {k: node.attributes.get(k, None) for k in SeperableToDepthwiseAndConv._pw_attributes}
102+
pw_attributes['filt_width'] = 1
103+
pw_attributes['filt_height'] = 1
104+
pw_attributes['stride_width'] = 1
105+
pw_attributes['stride_height'] = 1
106+
pw_attributes['pad_left'] = 0
107+
pw_attributes['pad_right'] = 0
108+
pw_attributes['pad_top'] = 0
109+
pw_attributes['pad_bottom'] = 0
110+
pw_attributes['in_width'] = pw_attributes['out_width']
111+
pw_attributes['in_height'] = pw_attributes['out_height']
112+
pw_attributes['n_chan'] = node.get_attr('n_chan') * node.get_attr('depth_multiplier')
113+
pw_attributes['weight_data'] = node.get_attr('pointwise_data')
114+
pw_attributes['weight_quantizer'] = node.get_attr('pointwise_quantizer')
115+
pw_attributes['bias_data'] = node.get_attr('bias_data')
116+
pw_attributes['bias_quantizer'] = node.get_attr('bias_quantizer')
117+
118+
# note this is just regular convolution. It is replaced by a special pointwise implementation
119+
# if available by another optimizer
120+
new_pw = model.make_node('Conv' + dim, pw_name, pw_attributes, [dw_name])
121+
122+
model.split_node(node, new_dw, new_pw)
123+
124+
return True

0 commit comments

Comments
 (0)