Skip to content

Commit abcf95c

Browse files
enlupiEnrico LupiEnrico LupiJanFSchulte
authored
Bidirectional RNN layer support for Keras frontend and Vitis backend (#1310)
* ADD parsing for bidirectional RNN layers * Implement bidirectional rnn layers * ADD fixes * FIX resource strategy * FIX infer precision for bidirectional rnn * FIX eliminate activation after bidirectional rnn * FIX bidirectional layers name * ADD tests for bidirectional layer * FIX weight name and ADD backward layer architecture check * FIX static and non-static Bidirectional layers * ADD parse general bidirectional layer with possibly different architectures * ADD paring for general bidirectional layer * ADD gnerale bidirectional wrapper * ADD Bidirectional layers support * ADD support for reverse order layers * ADD feature check for merge mode and layers order * ADD io type feature check * FIX n_out in case of merge_mode != concat * ADD pytest for Bidirectional layer * FIX posible directions for LSTM and GRU * FIX spelling mistake * FIX order * FIX remove unused import * FIX blank space * RM old comments * MV check for out-of-order layers from passes to parsing * RM check for bidirectional layers order (now done during parsing) * ADD recurrent layers support for keras V3 * change code order * FIX test for keras v3 * FIX keras v3 support for bidirectional layers * FIX add back all test cases * FIX import keras --------- Co-authored-by: Enrico Lupi <enlupi@olhsw-01.cern.ch> Co-authored-by: Enrico Lupi <enlupi@kamino.cern.ch> Co-authored-by: Jan-Frederik Schulte <jschulte@cern.ch>
1 parent b1d6550 commit abcf95c

15 files changed

Lines changed: 939 additions & 75 deletions

File tree

hls4ml/backends/fpga/fpga_backend.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
Activation,
1515
BatchNormalization,
1616
BatchNormOnnx,
17+
Bidirectional,
1718
Conv,
1819
Conv1D,
1920
Conv2D,
@@ -68,6 +69,7 @@ def __init__(self, name):
6869
SimpleRNN,
6970
LSTM,
7071
GRU,
72+
Bidirectional,
7173
Dot,
7274
Conv,
7375
MatMul,
@@ -232,6 +234,16 @@ def get_layer_mult_size(self, layer):
232234
n_out_recr = n_out
233235
return n_in, n_out, n_in_recr, n_out_recr
234236

237+
if 'Bidirectional' in layer.class_name:
238+
result = []
239+
for d in ['forward', 'backward']:
240+
n_in = layer.get_attr('n_in')
241+
n_out = layer.get_attr(f'{d}_n_states') * 3
242+
n_in_recr = layer.get_attr(f'{d}_n_states')
243+
n_out_recr = n_out
244+
result.append((n_in, n_out, n_in_recr, n_out_recr))
245+
return result
246+
235247
raise Exception(f'Cannot get mult size for layer {layer.name} ({layer.class_name})')
236248

237249
def get_valid_reuse_factors(self, n_in, n_out):
@@ -282,6 +294,7 @@ def set_closest_reuse_factor(self, layer, n_in, n_out, attribute='reuse_factor',
282294
if not include_max_rf:
283295
valid_rf.pop()
284296
chosen_rf = layer.get_attr(attribute)
297+
print("\n\nREuse factor:", chosen_rf, "\n\n")
285298
if chosen_rf not in valid_rf:
286299
closest_rf = self.get_closest_reuse_factor(valid_rf, chosen_rf)
287300
valid_rf_str = ','.join(map(str, valid_rf))

hls4ml/backends/vitis/passes/feature_check.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,41 @@ def transform(self, model, node):
4949
f'WARNING: "ResourceUnrolled" strategy in "{node.name}" ({node.class_name}) may have unexpected II in'
5050
'Vitis backend.\nVerify that the final design satisfies the latency/II constraints.'
5151
)
52+
53+
54+
class ValidateBidirectionalMergeMode(OptimizerPass):
55+
_unrolled_layer_cls = ['Bidirectional']
56+
57+
def match(self, node):
58+
is_bidirectional_rnn_layer = (
59+
len([layer_cls for layer_cls in self._unrolled_layer_cls if layer_cls in node.class_name]) > 0
60+
)
61+
is_merge_mode_not_concat = node.get_attr('merge_mode', 'concat') != 'concat'
62+
63+
return is_bidirectional_rnn_layer and is_merge_mode_not_concat
64+
65+
def transform(self, model, node):
66+
merge_mode = node.get_attr('merge_mode', 'concat')
67+
print(
68+
f'WARNING: "{merge_mode}" merge mode in "{node.name}" ({node.class_name}) is not supported in Vitis backend. '
69+
'Switching to "concat" merge mode.'
70+
)
71+
node.set_attr('merge_mode', 'concat')
72+
73+
74+
class ValidateBidirectionalIoType(OptimizerPass):
75+
_unrolled_layer_cls = ['Bidirectional']
76+
77+
def match(self, node):
78+
is_bidirectional_rnn_layer = (
79+
len([layer_cls for layer_cls in self._unrolled_layer_cls if layer_cls in node.class_name]) > 0
80+
)
81+
is_layer_io_type_stream = node.model.config.config['IOType'] != 'io_parallel'
82+
83+
return is_bidirectional_rnn_layer and is_layer_io_type_stream
84+
85+
def transform(self, model, node):
86+
raise Exception(
87+
f'WARNING: "{node.model.config.config["IOType"]}" IO Type is not supported in Vitis backend '
88+
f'for "{node.name}" ({node.class_name}). Please use "io_parallel".'
89+
)

hls4ml/backends/vitis/vitis_backend.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ def _register_flows(self):
2828
'vitis:validate_conv_implementation',
2929
'vitis:validate_resource_strategy',
3030
'vitis:validate_resource_unrolled_strategy',
31+
'vitis:validate_bidirectional_merge_mode',
32+
'vitis:validate_bidirectional_io_type',
3133
]
3234
validation_flow = register_flow('validation', validation_passes, requires=['vivado:init_layers'], backend=self.name)
3335

hls4ml/backends/vivado/passes/recurrent_templates.py

Lines changed: 213 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from hls4ml.backends.backend import get_backend
22
from hls4ml.backends.template import FunctionCallTemplate, LayerConfigTemplate
3-
from hls4ml.model.layers import GRU, LSTM, TimeDistributed
3+
from hls4ml.model.layers import GRU, LSTM, Bidirectional, TimeDistributed
44

55
# recurrent multiplication template
66

@@ -86,10 +86,52 @@
8686
static const bool pytorch_order = {pytorch};
8787
}};\n"""
8888

89+
# Bidirectional templates
90+
91+
single_config_template = """struct config{index} : nnet::single_layer_config {{
92+
typedef {accum_t.name} accum_t;
93+
typedef {weight_t.name} weight_t; // Matrix
94+
typedef {recurrent_weight_t.name} recurrent_weight_t; // Matrix
95+
typedef {bias_t.name} bias_t; // Vector
96+
typedef {recurrent_bias_t.name} recurrent_bias_t; // Vector
97+
typedef {config_mult_t1} mult_config1;
98+
typedef {config_mult_t2} mult_config2;
99+
typedef {recr_act_t} ACT_CONFIG_{RECR_TYPE};
100+
template<class x_T, class y_T, class config_T>
101+
using activation_recr = nnet::activation::{recurrent_activation}<x_T, y_T, config_T>;
102+
typedef {act_t} ACT_CONFIG_T;
103+
template<class x_T, class y_T, class config_T>
104+
using activation = nnet::activation::{activation}<x_T, y_T, config_T>;
105+
static const unsigned n_in = {n_in};
106+
static const unsigned n_state = {n_state};
107+
static const unsigned n_mult = {n_mult};
108+
static const bool pytorch_order = {pytorch};
109+
}};\n"""
110+
111+
bidirectional_config_template = """struct config{index} : nnet::bidirectional_config {{
112+
typedef {forward_t} FORWARD_CONFIG;
113+
template<class x_T, class y_T, typename config_T, bool backward>
114+
using RNNfunc_forward = nnet::{forward_layer}<x_T, y_T, config_T, backward>;
115+
typedef {backward_t} BACKWARD_CONFIG;
116+
template<class x_T, class y_T, typename config_T, bool backward>
117+
using RNNfunc_backward = nnet::{backward_layer}<x_T, y_T, config_T, backward>;
118+
static const unsigned n_in = {n_in};
119+
static const unsigned n_out = {n_out};
120+
static const unsigned n_sequence = {n_sequence};
121+
static const unsigned n_sequence_out = {n_sequence_out};
122+
static const unsigned io_type = nnet::{strategy};
123+
static const unsigned reuse_factor = {reuse};
124+
static const bool store_weights_in_bram = false;
125+
static const bool use_static = {static};
126+
static const bool pytorch_order = {pytorch};
127+
}};\n"""
128+
89129
recr_function_template = 'nnet::{recr_type}_stack<{input_t}, {output_t}, {config}>({input}, {output}, {w}, {wr}, {b}, {br});'
90130
recr_function_template_initial_states_lstm = 'nnet::{recr_type}_stack<{input_t}, {input2_t}, {input3_t}, {output_t}, {config}>({input}, {input2}, {input3}, {output}, {w}, {wr}, {b}, {br});' # noqa: E501
91131
recr_function_template_initial_states_gru = 'nnet::{recr_type}_stack<{input_t}, {input2_t}, {output_t}, {config}>({input}, {input2}, {output}, {w}, {wr}, {b}, {br});' # noqa: E501
92132

133+
bidirectional_function_template = 'nnet::bidirectional_stack<{input_t}, {output_t}, {config}>({input}, {output}, {w}, {wr}, {b}, {br}, {w_b}, {wr_b}, {b_b}, {br_b});' # noqa: E501
134+
93135
recr_include_list = ['nnet_utils/nnet_recurrent.h']
94136

95137

@@ -207,6 +249,153 @@ def format(self, node):
207249
return mult_config1 + '\n' + mult_config2 + '\n' + recr_act_config + '\n' + act_config + '\n' + recr_config
208250

209251

252+
class BidirectionalConfigTemplate(LayerConfigTemplate):
253+
def __init__(self):
254+
super().__init__(Bidirectional)
255+
self.template = bidirectional_config_template
256+
self.layer_template = single_config_template
257+
self.act_template = activ_config_template
258+
self.recr_act_template = recr_activ_config_template
259+
self.mult1_template = recr_mult_config_template_1
260+
self.mult2_template = recr_mult_config_template_2
261+
262+
def format(self, node):
263+
264+
# ----- Bidirectional Layer Config -----#
265+
params = self._default_config_params(node)
266+
267+
params['n_in'] = node.get_input_variable().dim_names[1]
268+
params['n_sequence'] = node.get_input_variable().dim_names[0]
269+
if node.get_attr('return_sequences'):
270+
params['n_sequence_out'] = node.get_output_variable().dim_names[0]
271+
else:
272+
params['n_sequence_out'] = 1
273+
params['n_out'] = node.get_attr('n_out')
274+
params['strategy'] = node.get_attr('strategy')
275+
params['static'] = 'true' if node.attributes['static'] else 'false'
276+
params['pytorch'] = 'true' if node.get_attr('pytorch', False) else 'false'
277+
params['forward_t'] = f'config{node.index}_forward'
278+
params['backward_t'] = f'config{node.index}_backward'
279+
params['forward_layer'] = node.get_attr('forward_class_name').lower() + '_class'
280+
params['backward_layer'] = node.get_attr('backward_class_name').lower() + '_class'
281+
if node.attributes['static']:
282+
params['forward_layer'] += '_static'
283+
params['backward_layer'] += '_static'
284+
285+
recr_config = self.template.format(**params)
286+
287+
# ----- Forward and Backward Layers Config -----#
288+
result = ''
289+
for d in ['forward', 'backward']:
290+
if node.get_attr(f'{d}_class_name') == 'LSTM':
291+
n_recr_mult = 4
292+
else: # GRU
293+
n_recr_mult = 3
294+
295+
# ----- Layer Config -----#
296+
layer_params = self._default_config_params(node)
297+
layer_params['n_in'] = params['n_in']
298+
layer_params['pytorch'] = params['pytorch']
299+
layer_params['n_state'] = node.get_attr(f'{d}_n_states')
300+
layer_params['n_mult'] = 4
301+
if node.get_attr(f'{d}_class_name').lower() == 'gru':
302+
layer_params['n_mult'] = 3
303+
layer_params['config_mult_t1'] = f'config{node.index}_1_{d[0]}'
304+
layer_params['config_mult_t2'] = f'config{node.index}_2_{d[0]}'
305+
layer_params['recr_act_t'] = '{}_config{}_recr'.format(
306+
node.get_attr(f'{d}_recurrent_activation'), str(node.index) + f'_{d[0]}'
307+
)
308+
layer_params['act_t'] = '{}_config{}'.format(node.get_attr(f'{d}_activation'), str(node.index) + f'_{d[0]}')
309+
layer_params['RECR_TYPE'] = node.get_attr(f'{d}_class_name')
310+
311+
layer_params['weight_t'] = layer_params[f'{d}_weight_t']
312+
layer_params['recurrent_weight_t'] = layer_params[f'{d}_recurrent_weight_t']
313+
layer_params['bias_t'] = layer_params[f'{d}_bias_t']
314+
layer_params['recurrent_bias_t'] = layer_params[f'{d}_recurrent_bias_t']
315+
layer_params['activation'] = layer_params[f'{d}_activation']
316+
layer_params['recurrent_activation'] = layer_params[f'{d}_recurrent_activation']
317+
318+
layer_params['index'] = str(node.index) + f'_{d}'
319+
320+
layer_config = self.layer_template.format(**layer_params)
321+
322+
# ----- Activations Config -----#
323+
act_params = self._default_config_params(node)
324+
recr_act_params = self._default_config_params(node)
325+
326+
act_params['type'] = node.get_attr(f'{d}_activation')
327+
recr_act_params['type'] = node.get_attr(f'{d}_recurrent_activation')
328+
act_params['index'] = str(node.index) + f'_{d[0]}'
329+
recr_act_params['index'] = str(node.index) + f'_{d[0]}'
330+
act_params['n_in'] = node.get_attr(f'{d}_n_states')
331+
recr_act_params['n_in'] = node.get_attr(f'{d}_n_states') * (n_recr_mult - 1)
332+
333+
act_config = self.act_template.format(**act_params)
334+
recr_act_config = self.recr_act_template.format(**recr_act_params)
335+
336+
# ----- Mult Config -----#
337+
mult_params1 = self._default_config_params(node)
338+
mult_params2 = self._default_config_params(node)
339+
340+
mult_params1['n_in'] = node.get_input_variable().shape[1]
341+
mult_params1['n_out'] = node.get_attr(f'{d}_n_states') * n_recr_mult
342+
mult_params1['product_type'] = get_backend('vivado').product_type(
343+
node.get_input_variable().type.precision, node.get_weights(f'{d}_weight').type.precision
344+
)
345+
mult_params1['reuse'] = params['reuse']
346+
mult_params1['index'] = str(node.index) + f'_1_{d[0]}'
347+
mult_params1['nzeros'] = node.get_weights(f'{d}_weight').nzeros
348+
mult_params1['nonzeros'] = node.get_weights(f'{d}_weight').nonzeros
349+
350+
mult_params1['bias_t'] = mult_params1[f'{d}_bias_t']
351+
mult_params1['weight_t'] = mult_params1[f'{d}_weight_t']
352+
mult_params2['recurrent_bias_t'] = mult_params2[f'{d}_recurrent_bias_t']
353+
mult_params2['recurrent_weight_t'] = mult_params2[f'{d}_recurrent_weight_t']
354+
355+
namespace = params['namespace']
356+
357+
if node.get_attr('strategy').lower() == 'latency':
358+
mult_params1['dense_function'] = 'nnet::DenseLatency'
359+
elif node.get_attr('strategy').lower() == 'resource':
360+
if int(mult_params1[f'{d}_reuse_factor']) <= int(mult_params1['n_in']):
361+
mult_params1['dense_function'] = 'nnet::DenseResource_rf_leq_nin'
362+
else:
363+
mult_params1['dense_function'] = 'nnet::DenseResource_rf_gt_nin_rem0'
364+
# The 3rd case is never used
365+
elif node.get_attr('strategy').lower() == 'resource_unrolled':
366+
mult_params1['dense_function'] = f'{namespace}::dense_resource_unrolled_{node.index}_1'
367+
368+
mult_params2['n_in'] = node.get_attr(f'{d}_n_states')
369+
mult_params2['n_out'] = node.get_attr(f'{d}_n_states') * n_recr_mult
370+
mult_params2['product_type'] = get_backend('vivado').product_type(
371+
node.get_input_variable().type.precision, node.get_weights(f'{d}_recurrent_weight').type.precision
372+
)
373+
mult_params2['reuse'] = node.attributes[f'{d}_recurrent_reuse_factor']
374+
mult_params2['index'] = str(node.index) + f'_2_{d[0]}'
375+
mult_params2['nzeros'] = node.get_weights(f'{d}_recurrent_weight').nzeros
376+
mult_params2['nonzeros'] = node.get_weights(f'{d}_recurrent_weight').nonzeros
377+
378+
if node.get_attr('strategy').lower() == 'latency':
379+
mult_params2['dense_function'] = 'nnet::DenseLatency'
380+
elif node.get_attr('strategy').lower() == 'resource':
381+
if int(mult_params2[f'{d}_reuse_factor']) <= int(mult_params2['n_in']):
382+
mult_params2['dense_function'] = 'nnet::DenseResource_rf_leq_nin'
383+
else:
384+
mult_params2['dense_function'] = 'nnet::DenseResource_rf_gt_nin_rem0'
385+
# The 3rd case is never used
386+
elif node.get_attr('strategy').lower() == 'resource_unrolled':
387+
mult_params2['dense_function'] = f'{namespace}::dense_resource_unrolled_{node.index}_2'
388+
389+
mult_config1 = self.mult1_template.format(**mult_params1)
390+
mult_config2 = self.mult2_template.format(**mult_params2)
391+
392+
result += (
393+
mult_config1 + '\n' + mult_config2 + '\n' + recr_act_config + '\n' + act_config + '\n' + layer_config + '\n'
394+
)
395+
396+
return result + recr_config
397+
398+
210399
class RecurrentFunctionTemplate(FunctionCallTemplate):
211400
def __init__(self):
212401
super().__init__((LSTM, GRU), include_header=recr_include_list)
@@ -239,6 +428,29 @@ def format(self, node):
239428
return template.format(**params)
240429

241430

431+
class BidirectionalFunctionTemplate(FunctionCallTemplate):
432+
def __init__(self):
433+
super().__init__((Bidirectional), include_header=recr_include_list)
434+
435+
def format(self, node):
436+
params = self._default_function_params(node)
437+
438+
# TO DO: Add initial states functions for pytorch settings
439+
440+
params['w'] = node.get_weights('forward_weight').name
441+
params['b'] = node.get_weights('forward_bias').name
442+
params['wr'] = node.get_weights('forward_recurrent_weight').name
443+
params['br'] = node.get_weights('forward_recurrent_bias').name
444+
params['w_b'] = node.get_weights('backward_weight').name
445+
params['b_b'] = node.get_weights('backward_bias').name
446+
params['wr_b'] = node.get_weights('backward_recurrent_weight').name
447+
params['br_b'] = node.get_weights('backward_recurrent_bias').name
448+
449+
template = bidirectional_function_template
450+
451+
return template.format(**params)
452+
453+
242454
time_distributed_config_template = """struct config{index} : nnet::time_distributed_config {{
243455
static const unsigned dim = {dim};
244456

hls4ml/backends/vivado/passes/resource_strategy.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
import numpy as np
22

3-
from hls4ml.model.layers import GRU, LSTM, Conv1D, Conv2D, Dense, SeparableConv1D, SeparableConv2D
3+
from hls4ml.model.layers import (
4+
GRU,
5+
LSTM,
6+
Bidirectional,
7+
Conv1D,
8+
Conv2D,
9+
Dense,
10+
SeparableConv1D,
11+
SeparableConv2D,
12+
)
413
from hls4ml.model.optimizer import OptimizerPass
514

615

716
class ApplyResourceStrategy(OptimizerPass):
817
'''Transposes the weights to use the dense_resource matrix multiply routine'''
918

1019
def match(self, node):
11-
node_matches = isinstance(node, (Dense, Conv1D, SeparableConv1D, Conv2D, SeparableConv2D, LSTM, GRU))
20+
node_matches = isinstance(node, (Dense, Conv1D, SeparableConv1D, Conv2D, SeparableConv2D, LSTM, GRU, Bidirectional))
1221
is_resource_strategy = node.get_attr('strategy', '').lower() in ['resource', 'resource_unrolled']
1322
already_transformed = node.get_attr('_weights_transposed', False) is True
14-
1523
return node_matches and is_resource_strategy and not already_transformed
1624

1725
def transform(self, model, node):
@@ -37,6 +45,10 @@ def transform(self, model, node):
3745
node.weights['pointwise'].data = np.transpose(
3846
node.weights['pointwise'].data, axes=[3, 0, 1, 2]
3947
) # (H,W,C,F) => (F,H,W,C)
48+
elif isinstance(node, (Bidirectional)):
49+
for d in ['forward', 'backward']:
50+
node.weights[f'{d}_weight'].data = np.transpose(node.weights[f'{d}_weight'].data)
51+
node.weights[f'{d}_recurrent_weight'].data = np.transpose(node.weights[f'{d}_recurrent_weight'].data)
4052
elif isinstance(node, (LSTM, GRU)):
4153
node.weights['weight'].data = np.transpose(node.weights['weight'].data)
4254
node.weights['recurrent_weight'].data = np.transpose(node.weights['recurrent_weight'].data)

0 commit comments

Comments
 (0)