def replace_pattern(self, graph: Graph, match: dict):
        """
            Adds layers with type 'Const' that produce blob from 'bin' file. The pass finds data nodes with one output which
            doesn't have edge with 'bin' attribute (or with two outputs and at least one output havent 'bin' attr)
            and generate Const op node before the node and data node before the Const node. The data node before 'Const'
            node is needed because the op node dumps input tensors to bin file.
        """
        node = match['data']
        if len(node.in_nodes()) > 0:
            return

        if self._check_bin_attrs(node):
            if node.has_valid('value'):
                const_node_name = graph.unique_id(node.id + '_const')
                log.debug("Added Const node '{}'".format(const_node_name))
                const_node = Const(
                    graph, {
                        'name':
                        const_node_name,
                        'value':
                        node.value,
                        'force_shape':
                        node.soft_get('force_shape', None),
                        'override_output_shape':
                        node.has_valid('force_shape'),
                        'force_type':
                        node.soft_get('force_type', None),
                        'correct_data_type':
                        node.soft_get('correct_data_type', None),
                    }).create_node()
                const_node.add_input_port(0)
                graph.add_edges_from([(const_node_name, node.id, {'out': 0})])

                node_copy = node.copy_node()
                const_node.type_infer(const_node)
                graph.add_edges_from([(node_copy.id, const_node_name, {
                    'in': 0,
                    'bin': 'custom'
                })])
            elif not self._check_that_node_from_body(node):
                log.debug('node = {}'.format(node.graph.node[node.id]))
                raise Error(
                    'Discovered data node without inputs and value, node.name = {}, consumer.name = {}. '
                    + refer_to_faq_msg(23), node.soft_get('name'),
                    node.out_node().soft_get('name')
                    if len(node.out_nodes()) else "<no consumer>")
Example #2
0
    def extract(cls, node: Node):
        unsupported_attrs = []
        for attr_name in [
                'adjoint_a', 'adjoint_b', 'a_is_sparse', 'b_is_sparse'
        ]:
            if attr_name in node.pb.attr and node.pb.attr[attr_name].b:
                unsupported_attrs.append(attr_name)
        if len(unsupported_attrs) != 0:
            raise Error('MatMul operation {} use unsupported attrs: {}'.format(
                node.id, unsupported_attrs))

        MatMul.update_node_stat(
            node, {
                'transpose_a': node.pb.attr['transpose_a'].b,
                'transpose_b': node.pb.attr['transpose_b'].b,
            })
        return cls.enabled
Example #3
0
def convert_blob(blob: np.ndarray, dst_type: type):
    if blob.dtype == dst_type:
        return blob, None, None

    converted_blob = blob.astype(dtype=dst_type, casting="unsafe")
    if dst_type in (np.int32, np.int64, np.uint8,
                    np.int8) and not np.array_equal(blob, converted_blob):
        raise Error(
            'The conversion of blob with value "{}" to dst_type "{}" results in rounding'
            .format(blob, dst_type))

    finite_match = (np.isfinite(blob) != np.isfinite(converted_blob))
    zero_match = ((blob == 0) != (converted_blob == 0))
    finite_match_count = np.count_nonzero(finite_match)
    zero_match_count = np.count_nonzero(zero_match)

    return converted_blob, finite_match_count, zero_match_count
Example #4
0
 def __init__(self, graph: Graph, attrs: dict):
     mandatory_props = {
         'type': __class__.op,
         'op': __class__.op,
         'levels': None,
         'is_eltwise': True,
         # flag to switch between dumping FakeQuantize as statistics and keeping it as layer in IR
         'keep_in_IR': None,
         'infer': __class__.infer,
         'in_ports_count': 5,
         'out_ports_count': 1,
     }
     super().__init__(graph, mandatory_props, attrs)
     if self.attrs['levels'] is None:
         raise Error("FakeQuantize operation has no levels parameter")
     # TODO remove following lines after FakeQuantize supported for int8 workflow
     self.attrs['keep_in_IR'] = self.attrs['levels'] == 2 or graph.graph['cmd_params'].keep_quantize_ops_in_IR
Example #5
0
def _check_unique_ids():
    """
    Check that idxs is unique for all registered replacements.
    """
    unique_idxs = set()
    for class_type, classes_set in _registered_classes_dict.items():
        for cls in classes_set:
            replacers = [c for c in cls.registered_cls if not hasattr(c, 'op')] + \
                        [c for op, c in cls.registered_ops.items() if c]
            for replacer_cls in replacers:
                if hasattr(replacer_cls, 'id'):
                    id_cls = getattr(replacer_cls, 'id')

                    if id_cls in unique_idxs:
                        raise Error('Found replacer {} with not unique id!'.format(replacer_cls))
                    unique_idxs.add(id_cls)
    log.debug("All replacers has unique idxs.")
Example #6
0
def collect_until_token(file_desc: io.BufferedReader, token):
    """
    Read from file until the token
    :param file_desc: file descriptor
    :return:
    """
    while True:
        # usually there is the following structure <CellDim> DIM<ClipGradient> VALUEFM
        res = collect_until_whitespace(file_desc)
        if res == token or res[-len(token):] == token:
            return
        if isinstance(file_desc, io.BytesIO):
            size = len(file_desc.getbuffer())
        elif isinstance(file_desc, io.BufferedReader):
            size = os.fstat(file_desc.fileno()).st_size
        if file_desc.tell() == size:
            raise Error('End of the file. Token {} not found. {}'.format(token, file_desc.tell()))
Example #7
0
def convert(graph: nx.MultiDiGraph, data_type_str: str):
    for node_name, node_attrs in graph.nodes(data=True):
        node = Node(graph, node_name)
        # if the data type is forcibly set then use it
        if node.has_valid('force_precision'):
            real_data_type_str = node_attrs['force_precision']
        else:
            real_data_type_str = data_type_str
        node_attrs['precision'] = data_type_str_to_precision(
            real_data_type_str)
        if node.kind == 'data' and node.value is not None:
            try:
                convert_blob(graph, node,
                             data_type_str_to_np(real_data_type_str))
            except Exception as e:
                raise Error('Coudn\'t convert blob {}, details: {}',
                            node.soft_get('name'), e) from e
Example #8
0
    def merge_nodes_permutations(graph: Graph):
        # Iterate over all data nodes and check all permutations for similarity
        # In case of equal permutations, this permutation will be set as attribute for data node
        # otherwise exception will be raised
        for node in graph.nodes():
            node = Node(graph, node)
            if node.kind != 'data':
                continue

            permutations = []

            # Get all permutations from in edges
            for in_node in node.in_nodes():
                edge_attrs = node.graph.get_edge_data(in_node.id, node.id)[0]
                if 'permutation' in edge_attrs:
                    permutations.append(edge_attrs['permutation'])

            # Get all permutations from out edges
            for out_node in node.out_nodes():
                edge_attrs = node.graph.get_edge_data(node.id, out_node.id)[0]
                if 'permutation' in edge_attrs:
                    permutations.append(edge_attrs['permutation'])

            # Check that all permutations are equal
            final_permutations = []
            for p in permutations:
                if p is not None:
                    final_permutations.append(p.perm)
                else:
                    final_permutations.append(
                        int64_array(np.arange(node.shape.size)))

            if len(final_permutations) == 0:
                continue

            if not all([
                    np.array_equal(final_permutations[0], perm)
                    for perm in final_permutations
            ]):
                raise Error(
                    'Permutations requested for {} data node are not equal! List of permutations: {}'
                    ''.format(node.name, [p.perm for p in permutations]))

            assert not node.has_valid('permutation') or np.array_equal(
                node.permutation, permutations[0])
            node['permutation'] = permutations[0]
Example #9
0
 def extract(cls, node):
     attrs = get_mxnet_layer_attrs(node.symbol_dict)
     act_type = attrs.str('act_type', 'relu')
     if act_type == 'sigmoid':
         act_class = Sigmoid
     elif act_type == 'tanh':
         act_class = Tanh
     elif act_type == 'relu':
         act_class = ReLU
     elif act_type == 'softrelu':
         act_class = SoftPlus
     else:
         raise Error(
             "Operation '{}' not supported. Please register it as custom op. "
             + refer_to_faq_msg(86), act_type)
     act_class.update_node_stat(node)
     return cls.enabled
Example #10
0
def decode_name_with_port(input_model: InputModel, node_name: str):
    """
    Decode name with optional port specification w/o traversing all the nodes in the graph
    TODO: in future node_name can specify input/output port groups and indices (58562)
    :param input_model: Input Model
    :param node_name: user provided node name
    :return: decoded place in the graph
    """
    # Check exact match with one of the names in the graph first
    node = input_model.get_place_by_tensor_name(node_name)
    if node:
        return node

    # TODO: Add support for input/output group name and port index here (58562)
    # Legacy frontends use format "number:name:number" to specify input and output port indices
    # For new frontends this logic shall be extended to additionally support input and output group names
    raise Error('There is no node with name {}'.format(node_name))
Example #11
0
 def check_shapes_consistency(self):
     data_nodes = self.get_data_nodes()
     data_nodes_with_wrong_shapes = []
     for data_node in data_nodes:
         if not data_node.has('shape'):
             data_nodes_with_wrong_shapes.append(
                 (data_node.name, "no shape attribute"))
             continue
         if data_node.shape is not None and not isinstance(
                 data_node.shape, np.ndarray):
             data_nodes_with_wrong_shapes.append(
                 (data_node.name, type(data_node.shape)))
     if len(data_nodes_with_wrong_shapes) > 0:
         raise Error(
             "Graph contains data nodes ({}) with inconsistent shapes: {}".
             format(len(data_nodes_with_wrong_shapes),
                    data_nodes_with_wrong_shapes))
Example #12
0
def onnx_attr(node: Node, name: str, field: str, default=None, dst_type=None):
    """ Retrieves ONNX attribute with name `name` from ONNX protobuf `node.pb`.
        The final value is casted to dst_type if attribute really exists.
        The function returns `default` otherwise.
    """
    attrs = [a for a in node.pb.attribute if a.name == name]
    if len(attrs) == 0:
        # there is no requested attribute in the protobuf message
        return default
    elif len(attrs) > 1:
        raise Error('Found multiple entries for attribute name {} when at most one is expected. Protobuf message with '
                    'the issue: {}.', name, node.pb)
    else:
        res = getattr(attrs[0], field)
        if dst_type is not None:
            return dst_type(res)
        else:
            return res
Example #13
0
def shape_inference(graph: Graph):
    nodes = pseudo_topological_sort(graph)
    for node in nodes:
        node = Node(graph, node)
        if node.has_and_set('need_shape_inference'):
            old_out_shapes = [
                port.data.get_shape() for port in node.out_ports().values()
            ]
            node.infer(node)
            new_out_shapes = [
                port.data.get_shape() for port in node.out_ports().values()
            ]
            for shape1, shape2 in zip(old_out_shapes, new_out_shapes):
                if shape1 is not None and not np.array_equal(shape1, shape2):
                    raise Error(
                        "After partial shape inference were found shape collision for node {} (old shape: {}, new shape: {})"
                        .format(node.name, shape1, shape2))
            node.need_shape_inference = False
 def replace_sub_graph(self, graph: Graph, match: [dict, SubgraphMatch]):
     node = match['reduce']
     connected_in_ports = [
         port for port in node.in_ports().values()
         if not port.disconnected()
     ]
     if len(connected_in_ports) == 1:
         # if the 'axis' is None then we still add a second input to the layer with a 1D array with 1 element equal
         # to None. The infer function handles this case because the input shape is known at this stage only
         if node.has('axis'):
             const = Const(graph, {'value': node.axis}).create_node()
             node.add_input_port(1, skip_if_exist=True)
             const.out_port(0).connect(node.in_port(1))
             del graph.node[node.id]['axis']
         else:
             raise Error(
                 'Can not deduce `reduce_axis` for {}: only one in_port and no `axis` parameter.'
                 ''.format(node.op))
    def find_and_replace_pattern(self, graph: Graph):
        for node in graph.get_op_nodes():
            node_type = node.soft_get('type').lower()
            name = node.soft_get('name', node.id)

            if node.soft_get('version', None) == 'opset1' and node_type not in self.opset_1_types:
                raise Error('Node {} has `version` attribute set to `opset1`, but it is a reserved word, '
                            'please use another'.format(name))

            if not node.has_valid('version'):
                if node_type in self.opset_1_types:
                    node['version'] = 'opset1'
                elif node_type in self.opset_1_experimental_ops:
                    node['version'] = 'experimental'
                else:
                    node['version'] = 'extension'
                    log.error('Please set `version` attribute for node {} with type={}'
                              ''.format(name, node.soft_get('type')), extra={'is_warning': True})
 def replace_sub_graph(self, graph: Graph, match: [dict, SubgraphMatch]):
     node = match['transpose']
     connected_in_ports = [
         port for port in node.in_ports().values()
         if not port.disconnected()
     ]
     if len(connected_in_ports) == 1:
         if node.has_valid('order'):
             const = Const(graph, {'value': node.order}).create_node()
             node.add_input_port(1, skip_if_exist=True)
             const.out_port(0).connect(node.in_port(1))
             del graph.node[node.id]['order']
         elif node.has('order') and node.order is None:
             assert node.has_and_set('reverse_order')
         else:
             raise Error(
                 'Can not deduce transpose `order` for {}: only one in_port and no `order` parameter.'
                 ''.format(node.op))
Example #17
0
 def find_and_replace_pattern(self, graph: Graph):
     for squeeze_node in graph.get_op_nodes(op='Squeeze'):
         if len(squeeze_node.in_nodes()) == 1 and squeeze_node.has_valid(
                 'squeeze_dims'):
             dims_node = Const(
                 graph, {
                     'name': squeeze_node.id + '/Dims',
                     'value': int64_array(squeeze_node.squeeze_dims)
                 }).create_node()
             squeeze_node.in_port(1).connect(dims_node.out_port(0))
             del squeeze_node['squeeze_dims']
         elif len(squeeze_node.in_nodes()) == 2:
             log.debug('The Squeeze node "{}" is already normalized'.format(
                 squeeze_node.name))
         else:
             raise Error(
                 'The Squeeze layer "{}" should either have 2 inputs or one input and an "squeeze_dims" '
                 'attribute'.format(squeeze_node.soft_get('name')))
Example #18
0
    def extract(cls, node):
        if get_onnx_opset_version(node) < 10:
            starts = int64_array(onnx_attr(node, 'starts', 'ints', default=[]))
            ends = int64_array(onnx_attr(node, 'ends', 'ints', default=[]))
            axes = int64_array(onnx_attr(node, 'axes', 'ints', default=[]))

            if len(starts) == 0 or len(ends) == 0:
                raise Error(
                    "starts or/and ends are not specified for the node {}".
                    format(node.name))
            if len(axes) == 0:
                axes = np.arange(len(starts), dtype=np.int)

            attrs = {'axes': axes, 'starts': starts, 'ends': ends}
            AttributedSlice.update_node_stat(node, attrs)
        else:  # onnx_opset_version >= 10
            Slice.update_node_stat(node)
        return cls.enabled
Example #19
0
 def backend_attrs(self):
     version = self.get_opset()
     if version == 'opset2':
         return [
             'eps',
             ('across_channels',
              lambda node: bool_to_str(node, 'across_channels')),
             ('normalize_variance',
              lambda node: bool_to_str(node, 'normalize_variance'))
         ]
     elif version == 'opset6':
         return [
             'eps', 'eps_mode',
             ('normalize_variance',
              lambda node: bool_to_str(node, 'normalize_variance'))
         ]
     else:
         raise Error('Unsupported MVN opset version "{}"'.format(version))
Example #20
0
 def extract_port(node_port):
     if isinstance(node_port, tuple):
         node = node_port[0]
         port = node_port[1]
     else:
         node = node_port
         port = 0
     # 'data' nodes do not have 'out' edge attribute but always has one output
     out_ids = [
         attr['out']
         for _, __, attr in node.graph.out_edges(node.id, data=True)
         if 'out' in attr
     ]
     if len(set(out_ids)) > 1 and not isinstance(node_port, tuple):
         raise Error(
             'Node {} has more than one outputs. Provide output port explicitly. '
             .format(node.name))
     return node, port
Example #21
0
    def find_and_replace_pattern(self, graph: Graph):
        iter_get_next_shapes = defaultdict(list)
        for iter_get_next in graph.get_op_nodes(op='IteratorGetNext'):
            iter_get_next_name = iter_get_next.soft_get(
                'name', iter_get_next.id)
            for port in iter_get_next.out_ports():
                if not np_data_type_to_precision(
                        iter_get_next.types[port]) in SUPPORTED_DATA_TYPES:
                    raise Error(
                        "In IteratorGetNext node '{}' data type '{}' is not supported"
                        .format(iter_get_next_name, iter_get_next.types[port]))

                iter_get_next_shapes[iter_get_next_name].append(
                    dict(shape=iter_get_next.shapes[port],
                         out=port,
                         data_type=iter_get_next.types[port]))

        add_input_ops(graph, iter_get_next_shapes, True)
Example #22
0
def tf_tensor_content(tf_dtype, shape, pb_tensor):
    type_helper = tf_data_type_decode[tf_dtype] if tf_dtype in tf_data_type_decode else None
    if type_helper is None:
        raise Error("Data type is unsupported: {}. " +
                    refer_to_faq_msg(50), tf_dtype)

    if pb_tensor.tensor_content:
        value = np.array(np.frombuffer(pb_tensor.tensor_content, type_helper[0]))
    else:
        # load typed value
        if type_helper[0] != np.str:
            value = np.array(type_helper[1](pb_tensor), dtype=type_helper[0])
        else:
            try:
                value = np.array(type_helper[1](pb_tensor), dtype=type_helper[0])
            except UnicodeDecodeError:
                log.error(
                    'Failed to parse a tensor with Unicode characters. Note that Inference Engine does not support '
                    'string literals, so the string constant should be eliminated from the graph.',
                    extra={'is_warning': True})
                value = np.array(type_helper[1](pb_tensor))

    if len(shape) == 0 or shape.prod() == 0:
        if len(value) == 1:
            # return scalar if shape is [] otherwise broadcast according to shape
            return np.array(value[0], dtype=type_helper[0])
        else:
            # no shape, return value as is
            return value

    if len(value) != shape.prod():
        log.warning("Shape and content size of tensor don't match, shape: {} content size: {}".
                    format(shape, len(value)))
        # broadcast semantics according to TensorFlow v1.5 documentation:
        # The argument value can be a constant value, or a list of values of type dtype. If value is a list,
        # then the length of the list must be less than or equal to the number of elements implied by the shape
        # argument (if specified). In the case where the list length is less than the number of elements specified
        # by shape, the last element in the list will be used to fill the remaining entries.
        value_flatten = value.flatten()
        add_value = value_flatten[-1]
        add_length = shape.prod() - len(value_flatten)
        value = np.concatenate([value_flatten, np.full([add_length], add_value)])

    return value.reshape(shape)
Example #23
0
 def find_and_replace_pattern(self, graph: Graph):
     for node in graph.get_op_nodes(squeeze_axis=True):
         name = node.soft_get('name', node.id)
         for out_port in node.out_ports().values():
             if node.has_valid('axis'):
                 squeeze_node = create_op_with_const_inputs(
                     graph, Squeeze, {1: np.array(node.axis)},
                     {'name': name + '/Squeeze_'})
                 out_port.get_connection().insert_node(squeeze_node)
             elif node.is_in_port_connected(1):
                 squeeze_node = Squeeze(graph, {
                     'name': name + '/Squeeze_'
                 }).create_node()
                 out_port.get_connection().insert_node(squeeze_node)
                 node.in_port(1).get_connection().add_destination(
                     squeeze_node.in_port(1))
             else:
                 raise Error(
                     'Unknown axis to squeeze for node {}'.format(name))
Example #24
0
def shape_insert(shape: [np.ndarray, list], pos: int,
                 obj: [int, list, np.ndarray, dynamic_dimension]):
    """
    Insert element(s) in the input tensor shape (presumably the numpy masked array) specified by position pos.
    The function is implemented to avoid usage of np.insert which corrupts information about the masked elements.

    :param shape: the shape object to insert element(s) to
    :param pos: the position to insert the elements into
    :param obj: the list or a single integer or the dynamic_dimension_value or numpy array to insert
    :return: shape with inserted elements
    """
    if isinstance(obj,
                  (int, np.int64, np.int32)) or obj is dynamic_dimension_value:
        return shape_insert(shape, pos, [obj])
    elif isinstance(obj, (np.ndarray, list)):
        return np.ma.concatenate((shape_array(shape[:pos]), shape_array(obj),
                                  shape_array(shape[pos:])))
    else:
        raise Error('Incorrect parameter type of "obj": {}'.format(type(obj)))
Example #25
0
    def extract(cls, node):
        pb = node.parameters
        collect_until_token(pb, b'<LinearParams>')
        weights, weights_shape = read_binary_matrix(pb)
        tag = find_next_tag(pb)
        read_placeholder(pb, 1)
        if tag != '<BiasParams>':
            raise Error('FixedAffineComponent must contain BiasParams')
        biases = read_binary_vector(pb)

        mapping_rule = {
            'out-size': weights_shape[0],
            'transpose_weights': True,
        }
        embed_input(mapping_rule, 1, 'weights', weights)
        embed_input(mapping_rule, 2, 'biases', biases)

        FullyConnected.update_node_stat(node, mapping_rule)
        return cls.enabled
Example #26
0
    def _set_value(self, value):
        if self.node.graph.stage == 'front':
            raise Error("set_value is not applicable for graph front phase")
        else:
            data_node_caller = self.node.in_node if self.type == 'in' else self.node.out_node
            data_node = data_node_caller(self.idx, control_flow=self.control_flow)
            const_node = data_node.in_node(control_flow=self.control_flow) if self.type == 'in' else self.node

            force_shape = data_node.soft_get('force_shape', const_node.soft_get('force_shape', None))
            shape = int64_array(value.shape if force_shape is None else force_shape)

            # Set value to data node
            data_node.value = value
            data_node.shape = shape

            # Set value to constant producer
            if const_node.soft_get('type') == 'Const':
                const_node.value = value
                const_node.shape = shape
 def save_custom_replacement_config_file(descriptions: list,
                                         file_name: str):
     """
     Save custom layer(s) description(s) to the file.
     :param file_name: file to save description information to.
     :param descriptions: list with instances of the CustomLayerDescriptor classes.
     :return: True if operation is successful.
     """
     try:
         json.dump([
             replacement_desc.get_config_file_representation()
             for replacement_desc in descriptions
         ],
                   open(file_name, "w"),
                   indent=4,
                   sort_keys=True)
     except Exception as ex:
         raise Error("failed to update configuration file {}: {}".format(
             file_name, str(ex)))
Example #28
0
    def find_and_replace_pattern(self, graph: Graph):
        unsupported_nodes = []
        for node in graph.nodes():
            node = Node(graph, node)
            if node.kind == 'op' and node.soft_get('type') in self.unsupported_operations:
                input_shape = node.in_node(0).shape
                if len(input_shape) > 4:
                    unsupported_nodes.append((node.id, node.type))

        if len(unsupported_nodes) == 0:
            return

        error_message = "\nOperations below were marked as unsupported due to they expect more than two spatial dims" \
                        " (input shape length more than 4)\n"
        error_message += "List of unsupported operations ({})\n".format(len(unsupported_nodes))
        for node, type in unsupported_nodes:
            error_message += "      {} {}\n".format(type, node)

        raise Error(error_message)
Example #29
0
def read_token_value(file_desc: io.BufferedReader, token: bytes = b'', value_type: type = np.uint32):
    """
    Get value of the token.
    Read next token (until whitespace) and check if next teg equals token
    :param file_desc: file descriptor
    :param token: token
    :param value_type:  type of the reading value
    :return: value of the token
    """
    getters = {
        np.uint32: read_binary_integer32_token,
        np.uint64: read_binary_integer64_token,
        np.bool: read_binary_bool_token
    }
    current_token = collect_until_whitespace(file_desc)
    if token != b'' and token != current_token:
        raise Error('Can not load token {} from Kaldi model'.format(token) +
                    refer_to_faq_msg(94))
    return getters[value_type](file_desc)
    def replace_op(self, graph: Graph, node: Node):
        ss_node = create_op_with_const_inputs(graph, Split, {1: int64_array(1)}, {'name': 'Split_eltwise_' + node.name,
                                                                                  'num_splits': node['num_inputs']})

        inp = node.get_inputs()
        in_node = inp[0][0]
        edge_attrs = inp[0][1]
        graph.add_edge(in_node, ss_node.id, **edge_attrs)
        if ss_node.num_splits == 2:
            eltwise_node = Eltwise(graph, attrs={'name': 'Eltwise_' + node.name,
                                                 'operation': node['operation']}).create_node()
        elif ss_node.num_splits > 2:
            eltwise_node = EltwiseN(graph, attrs={'name': 'Eltwise_' + node.name,
                                                  'operation': node['operation']}).create_node()
        else:
            raise Error('Error on replacing Kaldi eltwise')
        for i in range(ss_node.num_splits):
            ss_node.out_port(i).get_connection().set_destination(eltwise_node.in_port(i))
        return [eltwise_node.id]