def infer(node: Node): """ MO input edges: | Description: ------------------------------------------------- 0 | x: The sequence input to the LSTM, shape (timelen, batch_size, num_inputs) 1 | w: The weight matrix 2 | b: The bias vector 3 | h_prev: Previous/initial hidden state 4 | cs_prev: Value of the initial cell state """ assert len(node.in_nodes()) == 5 """ MO output edges: | Description: 0 | cs: Output data / output hidden states concatenated over the whole time sequence 1 | h: Output cell states concatenated over the whole time sequence """ assert len(node.out_nodes()) in [1, 2] mark_input_bins(node) input_shape = node.in_node(0).shape assert len(input_shape) == 3 out_shape = input_shape.copy() node.out_port(0).data.set_shape(out_shape) if node.is_out_port_connected(1): node.out_port(1).data.set_shape(out_shape)
def infer(node: Node): if node.has_and_set('extra_inputs'): assert len(node.in_nodes()) == 8 else: assert len(node.in_nodes()) == 5 assert len(node.out_nodes()) in [1, 2] hidden_shape = node.in_node(1).shape.copy() cell_shape = node.in_node(2).shape.copy() mark_input_bins(node, start_port=3) node.out_node(0).shape = hidden_shape if len(node.out_nodes()) == 2: node.out_node(1).shape = cell_shape hidden_size = hidden_shape[1] if node.has_valid('hidden_size'): if node.hidden_size != hidden_size: raise Error( "Input shape {} for hidden size doesn't match pre-defined hidden_size in node {}" .format(node.in_node(1).shape, node.soft_get('name'))) else: node['hidden_size'] = hidden_size assert cell_shape[1] == hidden_size input_shape = node.in_node(0).shape assert input_shape is not None assert compatible_dims(hidden_shape[0], cell_shape[0]) and \ compatible_dims(cell_shape[0], input_shape[0]), 'States are not broadcast-able by batch for node {}' \ ''.format(node.soft_get('name', node.id))
def caffe_inner_product(node): input_shape = node.in_node(0).shape if input_shape is None: return batches = input_shape[0] input_channels = np.prod(input_shape[1:]) if not node.has_valid('out-size'): node['out-size'] = (np.prod(node.in_node(1).shape) / input_channels).astype(np.int64) output_channels = node['out-size'] weights_shape = np.array([output_channels, input_channels], dtype=np.int64) # In case if original weight layout is IO we transpose them if np.array_equal(node.in_node(1).shape, weights_shape[::-1] ) and node.soft_get('transpose_weights') is True: node.in_node(1).value = np.transpose(node.in_node(1).value) node.out_node().shape = np.array([batches, output_channels], dtype=np.int64) # Back propagation of shape to weights node.in_node(1).shape = np.array(weights_shape) node.in_node(1).value.shape = node.in_node(1).shape mark_input_bins(node) assign_dims_to_weights(node.in_node(1), None, 1, 0, 2) PermuteAttrs.set_permutation(node.in_node(1), node, None)
def infer(node: Node): if node.has_and_set('extra_inputs'): assert len(node.in_nodes()) == 8 else: assert len(node.in_nodes()) == 5 assert len(node.out_nodes()) in [1, 2] hidden_shape = node.in_node(1).shape.copy() cell_shape = node.in_node(2).shape.copy() mark_input_bins(node, start_port=3) node.out_node(0).shape = hidden_shape if len(node.out_nodes()) == 2: node.out_node(1).shape = cell_shape hidden_size = hidden_shape[1] if node.has_valid('hidden_size'): if node.hidden_size != hidden_size: raise Error( "Input shape {} for hidden size doesn't match pre-defined hidden_size in node {}" .format(node.in_node(1).shape, node.soft_get('name'))) else: node['hidden_size'] = hidden_size assert cell_shape[1] == hidden_size
def extend(op: Node): if not op.has_valid('activations'): op['activations'] = None mark_input_bins(op, start_port=2) op['need_copy_input_blobs'] = True
def batch_norm_4_infer(node: Node): copy_shape_infer(node) mark_input_bins(node, ['weights', 'biases', 'mean', 'variance']) if node.has('fix_gamma') and node.fix_gamma: # go to the 1-st input weights and set all elements to 1 node.in_node(1).value = np.full_like(node.in_node(1).value, 1, dtype=np.float32)
def prelu_shape_infer(node): if len(node.in_nodes()) == 2: gamma_vector = node.in_node(1) if np.all(gamma_vector.shape == [1]): node['channel_shared'] = 1 else: node['channel_shared'] = 0 mark_input_bins(node) copy_shape_infer(node)
def infer(node: Node): # there are limitations coming from ONNX LSTM definition and normalization rules assert len(node.in_nodes()) >= 3 # X, W and R assert len(node.in_nodes()) <= 7 assert len(node.out_nodes()) <= 3 assert node.batch_dim <= 1 assert node.sequence_dim <= 1 assert node.batch_dim != node.sequence_dim assert node.direction in ['forward', 'reverse', 'bidirectional'] if node.blobs_wrb: mark_input_bins(node, ['W', 'R', 'B']) else: mark_input_bins(node) input_shape = node.in_node(0).shape assert len(input_shape) == 3 for port in [2, 3]: if port in node.in_nodes() and len(node.in_node(port).in_nodes()) > 0 and \ 'zero_shapes' in node.in_node(port).in_node(): for i in node.in_node(port).in_node().zero_shapes: if node.in_node(port).shape[i] != input_shape[i]: node.in_node(port).value = np.repeat( node.in_node(port).value, input_shape[i], axis=i) node.in_node(port).shape[i] = input_shape[i] out_shape = np.array([ input_shape[node.sequence_dim], input_shape[node.batch_dim], node.hidden_size ], dtype=np.int64) assert not node.has_num_directions or node.sequence_dim == 0, \ 'If has_num_directions == True, then node.sequence_dim should be equal 0, but it is {}'.format( node.sequence_dim) num_directions = 2 if node.direction in ['bidirectional'] else 1 num_layers = node.num_layers if node.has_num_directions: # insert extra dimension to output shape for num_directions out_shape = np.insert(out_shape, 1, np.int64(num_directions)) node.out_node(0).shape = out_shape # extra outputs for hidden/cell states state_size = np.array([input_shape[1], node.hidden_size], dtype=np.int64) if node.has_num_directions: state_size = np.insert(state_size, 0, num_directions * num_layers) for i in [1, 2]: if i not in node.out_nodes(): data_node = Op._create_data_node(node.graph, name=node.node + '/ExtraOutput/' + str(i), attrs={'executable': True}) node.graph.add_edge(node.id, data_node.id, key=0, out=i) add_opoutput(node.graph, data_node.id, 0, False) else: data_node = node.out_node(i) data_node.shape = state_size.copy()
def prelu_shape_infer(node): if len(node.in_nodes()) == 2: gamma_vector = node.in_node(1) if len(gamma_vector.shape) == 1 and \ gamma_vector.shape[0] == node.in_node(0).shape[node.graph.graph['layout'].index('C')]: node['channel_shared'] = 0 else: node['channel_shared'] = 1 mark_input_bins(node) copy_shape_infer(node)
def infer(node): if len(node.in_nodes()) == 2: gamma_vector = node.in_node(1) if np.all(gamma_vector.shape == [1]): node['channel_shared'] = 1 else: node['channel_shared'] = 0 if not node.graph.graph['cmd_params'].generate_experimental_IR_V10: mark_input_bins(node) else: node.in_node(1)['correct_data_type'] = True copy_shape_infer(node)
def infer(node: Node): assert len(node.out_nodes()) in [1, 2] hidden_shape = node.in_port(1).data.get_shape().copy() mark_input_bins(node, start_port=2) node.out_port(0).data.set_shape(hidden_shape) hidden_size = hidden_shape[1] if node.has_valid('hidden_size'): if node.hidden_size != hidden_size: raise Error("Input shape {} for hidden size doesn't match pre-defined hidden_size in node {}".format( node.in_node(1).shape, node.soft_get('name'))) else: node['hidden_size'] = hidden_size
def replace_pattern(self, graph: Graph, match: dict): if not match['input_0'].has_valid('value') and not match['input_1'].has_valid('value') or \ not match['input_0'].has_valid('value') and match['input_1'].has_valid('value') and match[ 'input_1'].shape.size > 2: match['fc']['type'] = 'GEMM' elif not match['input_0'].has_valid('value') and match['input_1'].has_valid('value'): match['fc']['type'] = 'FullyConnected' node = match['fc'] mark_input_bins(node) weights_node = match['input_1'] assign_dims_to_weights(weights_node, None, 0, 1, 2) PermuteAttrs.set_permutation(weights_node, node, PermuteAttrs.Permutation(perm=int64_array([1, 0]), inv=int64_array([0, 1]))) weights_shape = weights_node.shape node['out-size'] = weights_shape[1]
def infer(node: Node): """ Deconvolution has an input argument that explicitly determines output shape, so in contrast to the forward Conv2d we shouldn't infer output shape. We just use this output shape as an input shape and pass it to our utilities that computes numeric values for padding. They also deliver output shape that is interpreted here as input shape for convolution. We need to check that the real input shape and shape inferred by those utility functions match. """ output_shape = np.array(node.in_node(2).value) batch = np.array(node.in_node(0).shape)[0] output_shape[0] = batch kernel_shape = node.in_node(1).shape node['kernel_shape'] = kernel_shape if output_shape is None or kernel_shape is None or node.spatial_dims is None or node.stride is None: return if not node.has_valid('kernel_spatial_idx'): node['kernel_spatial_idx'] = np.delete( [x for x in range(len(kernel_shape))], (node.input_feature_channel, node.output_feature_channel)) if not node.has_valid('dilation'): node['dilation'] = np.full([len(output_shape)], 1, dtype=np.int64) spatial_dims = node.spatial_dims output_spatial = np.array(output_shape[spatial_dims]) stride_spatial = np.array(node.stride[spatial_dims]) node['kernel_spatial'] = np.array( kernel_shape[node.kernel_spatial_idx]) node.pad_spatial_shape, input_spatial_for_check = tf_window_op_pad_infer( output_spatial, node.kernel_spatial, stride_spatial, node.auto_pad) assert all( input_spatial_for_check == node.in_node(0).shape[spatial_dims]) pad = np.zeros((len(output_shape), 2), dtype=np.int64) pad[spatial_dims] = node.pad_spatial_shape node.pad = pad node.output = output_shape[node.channel_dims][0] node.output_shape = output_shape node.out_node().shape = output_shape mark_input_bins(node, ['weights'], 1) assign_dims_to_weights(node.in_node(1), node.kernel_spatial_idx, node.input_feature_channel, node.output_feature_channel, len(kernel_shape)) # OK, now we are sure this is a supported Deconvolution layer node.type = 'Deconvolution' node.op = 'Deconv2D' # Add permute_attrs PermuteAttrs.create_permute_attrs( node, attrs=[ ('pad', 'input:0'), ('stride', 'input:0'), ('output_shape', 'input:0'), ('batch_dims', 'input:0'), ('channel_dims', 'input:0'), ('spatial_dims', 'input:0'), ('kernel_shape', 'input:1'), ('kernel_spatial_idx', 'input:1'), ('input_feature_channel', 'input:1'), ('output_feature_channel', 'input:1'), ]) PermuteAttrs.set_permutation( node.in_node(1), node, node.get_weights_permute if node.has_valid('get_weights_permute') else None) PermuteInputs().set_input_permutation(node.in_node(2), node, 'input:0', 'shape') node['force_precision_in_ports'] = {2: 'int64'}
def infer(node: Node): mark_input_bins(node) copy_shape_infer(node)
def infer(node: Node): """ Infers shape of convolution node as it is done in ONNX. It is very similar to one that Caffe does, but slightly different. We made a complete fork of this function because they are supposed to be supported differently by different people. Args: node: graph convolution node """ input_shape = node.in_node(0).shape if input_shape is None: return # bias_term cannot be deduced earlier for frameworks that represent # convolution weights/biases as regular inputs; so the number of inputs # is being checked here and restore correct value for bias_term to # have the rest of the code unchanged. It will be used after we merge # several infer functions for convolution in different FWs to a single one. if not node.has_valid('bias_term'): node['bias_term'] = len(node.in_nodes()) == 3 weights_index = node.weights_index if node.has_valid( 'weights_index') else 1 # Reshape weights kernel to original shape # In case of caffe ot MXNet framework, values for weights has no structed shape like OIHW # so we have to reshape weights to normal shape # For this case, Convolution node should have attribute reshape_kernel = True if node.has_valid('reshape_kernel') and node.reshape_kernel: if not (node.has_valid('output') and node.has_valid('channel_dims') and node.has_valid('group') and node.has_valid('kernel_spatial')): log.error( 'Cannot reshape kernel due to not all required attrs was set to {} node' .format(node.id)) return # layout for Convolution weights is OIHW kernel_shape = np.array([ node.output, input_shape[node.channel_dims].item() / node.group, *[ node.kernel_spatial[i] for i in range(len(node.kernel_spatial)) ] ], dtype=np.int64) if node.type == 'Deconvolution': # layout for Deconvolution weights is IOHW kernel_shape[[0, 1]] = kernel_shape[[1, 0]] #node.input_feature_channel, node.output_feature_channel = node.output_feature_channel, node.input_feature_channel if np.prod(kernel_shape) != np.prod( node.in_node(weights_index).value.shape): log.error( "Size of weights {} does not match kernel shape: {}\n". format(np.prod(node.in_node(weights_index).value.shape), kernel_shape) + " Possible reason is wrong channel number in input shape\n" ) raise Error("Cannot reshape weights to kernel shape") node.in_node(weights_index).shape = np.array(kernel_shape) node.in_node(weights_index).value = np.reshape( node.in_node(weights_index).value, kernel_shape) node.reshape_kernel = False # Pass weights shape to node attribute kernel_shape kernel_shape = node.in_node(weights_index).shape node['kernel_shape'] = kernel_shape # Calculate kernel_spatial_idx and spatial_dims if it is not specified # It is necessary for ONNX dut to convolution can be 1D/2D/3D if not node.has_valid('kernel_spatial_idx'): node['kernel_spatial_idx'] = np.delete( [x for x in range(len(kernel_shape))], (node.input_feature_channel, node.output_feature_channel)) if not node.has_valid('spatial_dims'): node['spatial_dims'] = np.delete( [x for x in range(len(input_shape))], (node.channel_dims[0], node.batch_dims[0])) node['kernel_spatial'] = kernel_shape[node.kernel_spatial_idx] if not node.has_valid('output'): # restore the number of output feature maps from the second argument that is weights if node.type in [ 'Convolution', 'Deconvolution', 'DeformableConvolution' ]: node['output'] = kernel_shape[node.output_feature_channel] else: raise Error( 'Convolution infer function was called for a node {} with unsupported type {}', node.soft_get('name'), node.type) # Set default values for dilation, strides and pads if not set if not node.has_valid('dilation'): node['dilation'] = np.full([len(input_shape)], 1, dtype=np.int64) if not node.has_valid('stride'): node['stride'] = np.full([len(input_shape)], 1, dtype=np.int64) if not node.has_valid('pad'): node['pad'] = np.array([[0, 0]] * len(input_shape), dtype=np.int64) node['pad_spatial_shape'] = node.pad[node.spatial_dims] if not node.has_valid('output_padding'): node['output_padding'] = np.full([len(input_shape)], 0, dtype=np.int64) input_spatial_shape = input_shape[node.spatial_dims] stride_spatial_shape = node.stride[node.spatial_dims] kernel_extent = node.dilation[node.spatial_dims] * ( node.kernel_spatial - 1) + 1 # TensorFlow always has auto_pad attribute that can be either valid or same_upper # In ONNX auto_pad attribute is deprecated but appears in some models (could be valid, same_upper or same_lower) # Caffe do not use auto_pad attribute if node.has_valid( 'auto_pad') and not node.has_valid('output_spatial_shape'): node['pad_spatial_shape'], node[ 'output_spatial_shape'] = tf_window_op_pad_infer( input_spatial_shape, kernel_extent, stride_spatial_shape, node.auto_pad, node.type == 'Deconvolution') pad = np.zeros((len(input_shape), 2), dtype=np.int64) pad[node.spatial_dims] = node.pad_spatial_shape node.pad = pad else: pad_spatial_shape = np.add.reduce(node.pad_spatial_shape, axis=1) if node.type == 'Convolution': float_spatial = Convolution.calc_convolution( input_spatial_shape, stride_spatial_shape, pad_spatial_shape, kernel_extent) node['output_spatial_shape'] = int64_array(float_spatial) elif node.type == 'Deconvolution': # In case of given output_spatial_shape we calculate pads spatial if node.has_valid('output_spatial_shape'): if node.has_valid('get_pad'): node['pad'] = node.get_pad(node, input_shape, kernel_shape) else: log.debug( 'Can\'t calculate paddings due to missing lambda get_pad in {} node' .format(node.id)) return else: output_padding = node.output_padding[ node.spatial_dims] if node.has_valid( 'output_padding') else None if output_padding is not None and any(output_padding): pad_spatial_shape -= output_padding for dim in range(len(pad_spatial_shape)): node.pad_spatial_shape[dim][ 1] -= pad_spatial_shape[dim] node.pad[node.spatial_dims] = node.pad_spatial_shape node['output_padding'] = None float_spatial = Convolution.calc_deconvolution( node, input_spatial_shape, pad_spatial_shape, kernel_extent) node['output_spatial_shape'] = int64_array(float_spatial) elif node.type == 'DeformableConvolution': # get the output spatial shape from the second input with offsets node['output_spatial_shape'] = int64_array( [node.in_node(1).shape[2:4]]) else: assert 'Unsupported layer type "{}"'.format(node.type) # For cases when group attribute wasn't set in extractor we should specify get_group attribute # this attribute should store lambda node: ... (check tf convolution extractor) if node.has_valid('get_group'): node['group'] = node.get_group(node) output_shape = np.full_like(input_shape, -1, dtype=np.int64) output_shape[node.batch_dims] = input_shape[node.batch_dims] # pylint: disable=unsupported-assignment-operation output_shape[node.spatial_dims] = node.output_spatial_shape # pylint: disable=unsupported-assignment-operation # For cases when output attribute wasn't set in extractor we should specify get_output_feature_dim attribute # this attribute should store lambda node: ... (check tf convolution extractor) if node.has_valid('get_output_feature_dim'): node['output'] = node.get_output_feature_dim(node) output_shape[node.channel_dims] = node.output # pylint: disable=unsupported-assignment-operation node['output_shape'] = output_shape for n in node.out_nodes(): node.out_node(n).shape = output_shape mark_input_bins( node, start_port=1 if node.type != 'DeformableConvolution' else 2) assign_dims_to_weights(node.in_node(weights_index), node.kernel_spatial_idx, node.input_feature_channel, node.output_feature_channel, len(kernel_shape)) PermuteAttrs.create_permute_attrs( node, attrs=[ ('pad', 'input:0'), ('stride', 'input:0'), ('dilation', 'input:0'), ('output_shape', 'input:0'), ('batch_dims', 'input:0'), ('channel_dims', 'input:0'), ('spatial_dims', 'input:0'), ('kernel_shape', 'input:{}'.format(weights_index)), ('kernel_spatial_idx', 'input:{}'.format(weights_index)), ('input_feature_channel', 'input:{}'.format(weights_index)), ('output_feature_channel', 'input:{}'.format(weights_index)), ]) PermuteAttrs.set_permutation( node.in_node(weights_index), node, node.get_weights_permute if node.has_valid('get_weights_permute') else None)
def infer(node: Node): """ Deconvolution has an input argument that explicitly determines output shape, so in contrast to the forward Conv2d we shouldn't infer output shape. We just use this output shape as an input shape and pass it to our utilities that computes numeric values for padding. They also deliver output shape that is interpreted here as input shape for convolution. We need to check that the real input shape and shape inferred by those utility functions match. """ output_shape = shape_array(node.in_node(2).value) output_shape[0] = node.in_port(0).data.get_shape()[0] kernel_shape = node.in_port(1).data.get_shape() node['kernel_shape'] = kernel_shape if output_shape is None or kernel_shape is None or node.spatial_dims is None or node.stride is None: return if not node.has_valid('kernel_spatial_idx'): node['kernel_spatial_idx'] = np.delete( [x for x in range(len(kernel_shape))], (node.input_feature_channel, node.output_feature_channel)) if not node.has_valid('dilation'): node['dilation'] = np.full([len(output_shape)], 1, dtype=np.int64) spatial_dims = node.spatial_dims output_spatial = shape_array(output_shape[spatial_dims]) stride_spatial = shape_array(node.stride[spatial_dims]) node['kernel_spatial'] = shape_array( kernel_shape[node.kernel_spatial_idx]) node.pad_spatial_shape, input_spatial_for_check = tf_window_op_pad_infer( output_spatial, node.kernel_spatial, stride_spatial, node.auto_pad) assert compatible_shapes(input_spatial_for_check, node.in_node(0).shape[spatial_dims]) pad = np.zeros((len(output_shape), 2), dtype=np.int64) pad[spatial_dims] = node.pad_spatial_shape node.pad = pad node.output = output_shape[node.channel_dims][0] node.output_shape = output_shape node.out_port(0).data.set_shape(output_shape) mark_input_bins(node, ['weights'], 1) assign_dims_to_weights(node.in_node(1), node.kernel_spatial_idx, node.input_feature_channel, node.output_feature_channel, len(kernel_shape)) # OK, now we are sure this is a supported Deconvolution layer node.type = 'Deconvolution' node.op = 'Deconv2D' # Add permute_attrs PermuteAttrs.create_permute_attrs( node, attrs=[ ('pad', 'input:0'), ('stride', 'input:0'), ('output_shape', 'input:0'), ('batch_dims', 'input:0'), ('channel_dims', 'input:0'), ('spatial_dims', 'input:0'), ('kernel_shape', 'input:1'), ('kernel_spatial_idx', 'input:1'), ('input_feature_channel', 'input:1'), ('output_feature_channel', 'input:1'), ]) # is needed to permute Deconv weights from the original TF [H, W, C_OUT, C_IN] into IE [C_IN, C_OUT, H, W] # but for other nodes in weights subgraph permutations must turned off # by marking with MarkSubGraphsWithCorrectLayout even if graph layout is NCHW. PermuteAttrs.set_permutation( node.in_node(1), node, node.soft_get('get_weights_permute', None)) PermuteInputs().set_input_permutation(node.in_node(1), node, 'input:1', 'transpose') PermuteInputs().set_input_permutation(node.in_node(2), node, 'input:0', 'shape') node['force_precision_in_ports'] = {2: 'int64'}
def rnn_infer(node: Node, out_ports=None): """ General infer function for RNN, GRU, LSTM layers. Assume that 0-port input of node is input data for recurrent layer and node have attrs: hidden_size, """ if out_ports is None: out_ports = [] # 1. Necessary checks (from ONNX specification) assert node.batch_dim <= 1 assert node.sequence_dim <= 1 assert node.batch_dim != node.sequence_dim assert node.direction in ['forward', 'reverse', 'bidirectional'] if node.blobs_wrb: mark_input_bins(node, ['W', 'R', 'B']) else: mark_input_bins(node) # 2. Output shape calculations input_shape = node.in_node(0).shape assert len(input_shape) == 3 # Reshape input nodes for port in [2, 3]: if port in node.in_nodes() and len(node.in_node(port).in_nodes()) > 0 and \ 'zero_shapes' in node.in_node(port).in_node(): for i in node.in_node(port).in_node().zero_shapes: if node.in_node(port).shape[i] != input_shape[i]: node.in_node(port).value = np.repeat( node.in_node(port).value, input_shape[i], axis=i) node.in_node(port).shape[i] = input_shape[i] out_shape = np.array([ input_shape[node.sequence_dim], input_shape[node.batch_dim], node.hidden_size ], dtype=np.int64) if node.batch_dim == 0: out_shape = np.array([ input_shape[node.batch_dim], input_shape[node.sequence_dim], node.hidden_size ], dtype=np.int64) num_directions = 2 if node.direction in ['bidirectional'] else 1 if node.has_num_directions: if node.format == 'mxnet' and node.normalized is False: # In MXNet RNN layer return output with shape [seq_len, batch_size, hidden_size * num_directions] out_shape[-1] *= num_directions else: # ONNX-like, insert extra dimension to output shape for num_directions out_shape = np.insert(out_shape, 1, np.int64(num_directions)) # 0 output is required creating it if doesn't exist if 0 not in node.out_nodes(): data_node = Op._create_data_node(node.graph, name=node.node + '/ExtraOutput/{}'.format(0), attrs={'executable': True}) if 0 not in node.out_ports(): node.add_output_port(0) node.graph.add_edge(node.id, data_node.id, key=0, out=0) add_opoutput(node.graph, data_node.id, 0, False) node.out_port(0).data.set_shape(out_shape) # 3. Extra outputs for hidden/cell states shape calculations (optional) state_size = np.array([input_shape[node.batch_dim], node.hidden_size], dtype=np.int64) if node.has_num_directions: state_size = np.insert(state_size, 0, num_directions) if node.multilayers: # For multilayer case state sizes from every layer will be concatenated by last axis num_layers = node.num_layers state_size[-1] *= num_layers for i in out_ports: # If node hasn't consumers for hidden/cells state -> create them if i not in node.out_nodes(): data_node = Op._create_data_node(node.graph, name=node.node + '/ExtraOutput/' + str(i), attrs={'executable': True}) if i not in node.out_ports(): node.add_output_port(i) node.graph.add_edge(node.id, data_node.id, key=0, out=i) add_opoutput(node.graph, data_node.id, 0, False) else: data_node = node.out_node(i) data_node.shape = state_size.copy()