Repository: apple/coremltools

Issue:
ValueError: list.remove(x): x not in list
## 🐞Describe the bug
I'm training a multi-input CNN (`tf.keras`) with `TF2.0.0`. I keep getting `ValueError: list.remove(x): x not in list`, but I don't understand what's the list, what's x, do I need to rename nodes?

I can see that the converter starts, but the error comes after some conversion. I tried `tfcoreml` and converting from saved weights with no success. 
Is there something I'm missing?

This is the process I follow for conversion:

```python

tf.keras.backend.clear_session()
tf.keras.backend.set_learning_phase(0)

# model is a .h5 file produced from a model.fit callback (modelcheckpoint)
# I have a custom dense layer that has __init__, build, call and config implemented
model = tf.keras.models.load_model(model, custom_objects = {'DenseLayer': DenseLayer})

tf.saved_model.save(model, export_dir = pb_model) # save the model as pb for conversion

# my target ios is 12
coremltools_model = coremltools.converters.tensorflow.convert(
        _PB_MODEL,
        input_name_shape_dict={
            'B': [1, 20, 40, 1],
            'G': [1, 20, 40, 1],
            'R': [1, 20, 40, 1],
            'X': [1, 2],
            'Y': [1, 4]},
        output_feature_names=['dense_layer_18/Identity'],
        minimum_ios_deployment_target='12'
    )

coremltools_model.save(ml_model)
```

## Trace
```python
Converting pb model to .mlmodel......
0 assert nodes deleted
872 nodes deleted
36 nodes deleted
0 nodes deleted
[Op Fusion] fuse_bias_add() deleted 24 nodes.
77 identity nodes deleted
Traceback (most recent call last):
  File "tf2coreml.py", line 198, in <module>
convertModel(model=_TRAIN_MODEL, ml_model=_ML_MODEL, pb_model=_PB_MODEL)
  File "tf2coreml.py", line 174, in convertModel
minimum_ios_deployment_target='12'
  File "/home/ubuntu/anaconda3/envs/tensorflow_p36/lib/python3.6/site-packages/coremltools/converters/tensorflow/_tf_converter.py", line 193, in convert
optional_inputs=optional_inputs)
  File "/home/ubuntu/anaconda3/envs/tensorflow_p36/lib/python3.6/site-packages/coremltools/converters/nnssa/coreml/ssa_converter.py", line 130, in ssa_convert
p(ssa)
  File "/home/ubuntu/anaconda3/envs/tensorflow_p36/lib/python3.6/site-packages/coremltools/converters/nnssa/coreml/graph_pass/op_removals.py", line 20, in
remove_no_ops_and_shift_control_dependencies
    f.graph[each_control_input].control_outputs.remove(node.name)
ValueError: list.remove(x): x not in list
```
## System environment (please complete the following information):
 - coremltools version: 3.2
 - OS: Ubuntu 16.04 LTS (AWS EC2)
 - Python version: 3.6.5
 - any other relevant information:
     - Tensorflow: 2.0.0


Discussion:

Resolving patch:
diff --git a/coremltools/converters/nnssa/coreml/graph_pass/op_removals.py b/coremltools/converters/nnssa/coreml/graph_pass/op_removals.py
--- a/coremltools/converters/nnssa/coreml/graph_pass/op_removals.py
+++ b/coremltools/converters/nnssa/coreml/graph_pass/op_removals.py
@@ -70,23 +70,28 @@ def _remove_internal_identity_nodes(nnssa):
     delete_count = 0
     for fn_key in list(nnssa.functions.keys()):
         f = nnssa.functions[fn_key]
-        keys = list(f.graph.keys())
-        for k in keys:
-            if k not in f.graph:
+        for name in list(f.graph.keys()):
+            if name not in f.graph:
                 continue
-            node = f.graph[k]
-            if len(node.inputs) != 1 or len(node.outputs) != 1:
+            node = f.graph[name]
+
+            # Check if the node is in graph outputs
+            if len(node.inputs) != 1:
+                continue
+            if len(node.outputs) == 0 and len(node.control_outputs) == 0:
                 continue
+
+            # Remove identity node
             inp_node = f.graph[node.inputs[0]]
             if node.op == 'Identity' and inp_node.op != 'get_tuple':
                 delete_count += 1
-                parent_name = f.graph[k].inputs[0]
-                disconnect_edge(f.graph, parent_name, k)
-                for control_input in f.graph[k].control_inputs:
-                    replace_control_dest(f.graph, control_input, k, parent_name)
+                parent_name = f.graph[name].inputs[0]
+                disconnect_edge(f.graph, parent_name, name)
+                for control_input in f.graph[name].control_inputs:
+                    replace_control_dest(f.graph, control_input, name, parent_name)
 
-                replace_node(f.graph, k, parent_name)  # join parent to children
-                delete_node(f.graph, k)
+                replace_node(f.graph, name, parent_name)  # join parent to children
+                delete_node(f.graph, name)
 
     return delete_count
 


Test patch:
diff --git a/coremltools/converters/tensorflow/test/test_tf_2x.py b/coremltools/converters/tensorflow/test/test_tf_2x.py
--- a/coremltools/converters/tensorflow/test/test_tf_2x.py
+++ b/coremltools/converters/tensorflow/test/test_tf_2x.py
@@ -325,6 +325,92 @@ class TestTensorflow2Model(unittest.TestCase):
     def setUp(self):
         self.saved_model_dir = tempfile.mkdtemp()
 
+    def test_two_layers_control_dependency(self):
+
+        class model(tf.Module):
+
+            def __init__(self, name=None):
+                super(model, self).__init__(name=name)
+                self.w = tf.constant(tf.random.normal(shape=[1, 10]), name='bias', dtype=tf.float32)
+
+            @tf.function(input_signature=[tf.TensorSpec(shape=[1, 10], dtype=tf.float32),
+                                          tf.TensorSpec(shape=[1, 10], dtype=tf.float32),
+                                          tf.TensorSpec(shape=[1, 10], dtype=tf.float32)])
+            def __call__(self, x, y, z):
+                with tf.control_dependencies([x]):
+                    with tf.control_dependencies([y]):
+                        return self.w + z
+        model = model()
+        tf.saved_model.save(model, self.saved_model_dir)
+        mlmodel = coremltools.converters.tensorflow.convert(
+            self.saved_model_dir,
+            input={'x':[1,10], 'y':[1,10], 'z':[1,10]},
+            outputs=['Identity']
+        )
+
+        x, y, z = np.random.rand(1,10), np.random.rand(1, 10), np.random.rand(1, 10)
+        tf_output = model(x, y, z).numpy()
+        ml_output = mlmodel.predict({'x':x, 'y':y, 'z':z})['Identity']
+
+        np.testing.assert_almost_equal(tf_output, ml_output, decimal=3)
+
+
+    def test_two_control_inputs(self):
+
+        class model(tf.Module):
+
+            def __init__(self, name=None):
+                super(model, self).__init__(name=name)
+                self.w = tf.constant(tf.random.normal(shape=[1, 10]), name='bias', dtype=tf.float32)
+
+            @tf.function(input_signature=[tf.TensorSpec(shape=[1, 10], dtype=tf.float32),
+                                          tf.TensorSpec(shape=[1, 10], dtype=tf.float32),
+                                          tf.TensorSpec(shape=[1, 10], dtype=tf.float32)])
+            def __call__(self, x, y, z):
+                with tf.control_dependencies([x, y]):
+                    return self.w + z
+        model = model()
+        tf.saved_model.save(model, self.saved_model_dir)
+        mlmodel = coremltools.converters.tensorflow.convert(
+            self.saved_model_dir,
+            input={'x':[1,10], 'y':[1,10], 'z':[1,10]},
+            outputs=['Identity']
+        )
+
+        x, y, z = np.random.rand(1,10), np.random.rand(1, 10), np.random.rand(1, 10)
+        tf_output = model(x, y, z).numpy()
+        ml_output = mlmodel.predict({'x':x, 'y':y, 'z':z})['Identity']
+
+        np.testing.assert_almost_equal(tf_output, ml_output, decimal=3)
+
+
+    def test_control_inputs_with_node_with_no_outputs(self):
+
+        class model(tf.Module):
+
+            def __init__(self, name=None):
+                super(model, self).__init__(name=name)
+                self.w = tf.constant(tf.random.normal(shape=[1, 10]), name='bias', dtype=tf.float32)
+
+            @tf.function(input_signature=[tf.TensorSpec(shape=[1, 10], dtype=tf.float32),
+                                          tf.TensorSpec(shape=[1, 10], dtype=tf.float32)])
+            def __call__(self, x, y):
+                with tf.control_dependencies([x]):
+                    return self.w + y
+        model = model()
+        tf.saved_model.save(model, self.saved_model_dir)
+        mlmodel = coremltools.converters.tensorflow.convert(
+            self.saved_model_dir,
+            input={'x':[1,10], 'y':[1,10]},
+            outputs=['Identity']
+        )
+
+        x, y = np.random.rand(1,10), np.random.rand(1, 10)
+        tf_output = model(x, y).numpy()
+        ml_output = mlmodel.predict({'x':x, 'y':y})['Identity']
+
+        np.testing.assert_almost_equal(tf_output, ml_output, decimal=3)
+
     def test_save_and_load_low_level_model(self):
         class model(tf.Module):
             def __init__(self, in_features, output_features, name=None):