Repository: openvinotoolkit/openvino
Pull request #30193: [PT FE] Support Tuple as arguments
Author: login: mvafin
Labels: name: category: PyTorch FE
description: OpenVINO PyTorch Frontend
Created: 2025-04-17T14:44:59Z | Merged: 2025-04-25T15:13:32Z

### Details:
 - *Support case when module args are tuple*

### Tickets:
 - Closes #30141 


Linked issue #30141: [Bug]: Cannot convert model, Model wasn't fully converted. No conversion rule found for operations: prim::TupleUnpack
### OpenVINO Version

2024.6.0

### Operating System

Other (Please specify in description)

### Device used for inference

CPU

### Framework

PyTorch

### Model used

ResNet 50

### Issue description

Ubuntu: 22.04
OpenVINO: 2024.6

My code is as follows: 
```
import openvino as ov
import torch
from torchvision.models import resnet50
import numpy as np
from PIL import Image

model = 'model/resnet.pt' 
input_data = np.array(Image.open('test.jpg'), dtype=np.uint8)
ov_model = ov.convert_model(input_model=model, example_input=input_data)
ov.save_model(ov_model, 'resnet.xml')
```

The error message I received is:
```
Traceback (most recent call last):
  File "/home/winnie/benchmark_resnet50/model/convert_pt_ov_ir.py", line 14, in <module>
    ov_model = ov.convert_model(input_model=model, example_input=input_data)
  File "/home/winnie/resnet_env/lib/python3.10/site-packages/openvino/tools/ovc/convert.py", line 101, in convert_model
    ov_model, _ = _convert(cli_parser, params, True)
  File "/home/winnie/resnet_env/lib/python3.10/site-packages/openvino/tools/ovc/convert_impl.py", line 563, in _convert
    raise e
  File "/home/winnie/resnet_env/lib/python3.10/site-packages/openvino/tools/ovc/convert_impl.py", line 503, in _convert
    ov_model = driver(argv, {"conversion_parameters": non_default_params})
  File "/home/winnie/resnet_env/lib/python3.10/site-packages/openvino/tools/ovc/convert_impl.py", line 251, in driver
    ov_model = moc_emit_ir(prepare_ir(argv), argv)
  File "/home/winnie/resnet_env/lib/python3.10/site-packages/openvino/tools/ovc/convert_impl.py", line 195, in prepare_ir
    ov_model = moc_pipeline(argv, moc_front_end)
  File "/home/winnie/resnet_env/lib/python3.10/site-packages/openvino/tools/ovc/moc_frontend/pipeline.py", line 143, in moc_pipeline
    ov_model = moc_front_end.convert(input_model)
  File "/home/winnie/resnet_env/lib/python3.10/site-packages/openvino/frontend/frontend.py", line 18, in convert
    converted_model = super().convert(model)
openvino._pyopenvino.OpConversionFailure: Check 'is_conversion_successful' failed at src/frontends/pytorch/src/frontend.cpp:171:
FrontEnd API failed with OpConversionFailure:
Model wasn't fully converted.
Summary:
-- No conversion rule found for operations: prim::TupleUnpack
```
I’ve tried OpenVINO 2024.6.0 and 2025.1.0, both occurred the same problem

Thank you for helping.


### Step-by-step reproduction

_No response_

### Relevant log output

```shell

```

### Issue submission checklist

- [x] I'm reporting an issue. It's not a question.
- [x] I checked the problem with the documentation, FAQ, open issues, Stack Overflow, etc., and have not found a solution.
- [x] There is reproducer code and related data files such as images, videos, models, etc.
Linked issue comments: user: login: mvafin
created_at: 2025-04-15T13:22:40Z
body: Hello, @winniechj What is `'model/resnet.pt'`? You don't need to save model on disk before converting. Just load the model and pass directly to `convert_model`
author_association: CONTRIBUTOR
user: login: winniechj
created_at: 2025-04-16T02:13:39Z
body: @mvafin `'model/resnet.pt'` is the customized resnet model provided by customer. 
I also couldn't convert it by cli:  `ovc model/resnet.pt`. 
Error: 
```
FrontEnd API failed with OpConversionFailure:
Model wasn't fully converted.
Summary:
-- No conversion rule found for operations: prim::TupleUnpack
```
author_association: NONE

Diff:
diff --git a/src/frontends/pytorch/src/transforms/tuple_unpack_replacer.cpp b/src/frontends/pytorch/src/transforms/tuple_unpack_replacer.cpp
index 5e2f865ae2b514..256e29072dbe8c 100644
--- a/src/frontends/pytorch/src/transforms/tuple_unpack_replacer.cpp
+++ b/src/frontends/pytorch/src/transforms/tuple_unpack_replacer.cpp
@@ -6,6 +6,8 @@
 
 #include "openvino/core/graph_util.hpp"
 #include "openvino/op/if.hpp"
+#include "openvino/op/split.hpp"
+#include "openvino/op/squeeze.hpp"
 #include "openvino/op/util/framework_node.hpp"
 #include "openvino/pass/pattern/matcher.hpp"
 #include "openvino/pass/pattern/op/wrap_type.hpp"
@@ -28,17 +30,25 @@ PrimTupleUnpackReplacer::PrimTupleUnpackReplacer() {
             return false;
         OutputVector outputs;
         auto input_node = tuple_unpack->get_input_node_shared_ptr(0);
-        auto tuple_construct = cast_fw_node(input_node, "prim::TupleConstruct");
-        if (!tuple_construct) {
-            return false;
-        }
-        for (const auto& input : input_node->inputs()) {
-            const auto& out = input.get_source_output();
-            outputs.push_back(out);
-        }
-        replace_node(tuple_unpack, outputs);
+        if (cast_fw_node(input_node, "prim::TupleConstruct")) {
+            for (const auto& input : input_node->inputs()) {
+                const auto& out = input.get_source_output();
+                outputs.push_back(out);
+            }
+            replace_node(tuple_unpack, outputs);
 
-        return true;
+            return true;
+        } else if (ov::as_type_ptr<v0::Constant>(input_node)) {
+            // tuple might have been merged as a single constant
+            auto axis_zero = v0::Constant::create(element::i32, Shape{}, {0});
+            auto split = std::make_shared<v1::Split>(input_node, axis_zero, tuple_unpack->outputs().size());
+            for (size_t i = 0; i < split->get_output_size(); ++i) {
+                auto squeeze = std::make_shared<v15::Squeeze>(split->output(i), axis_zero);
+                replace_output_update_name(tuple_unpack->output(i), squeeze);
+            }
+            return true;
+        }
+        return false;
     };
 
     auto m = std::make_shared<ov::pass::pattern::Matcher>(tuple_unpack,
diff --git a/tests/layer_tests/pytorch_tests/test_tuple_unpack.py b/tests/layer_tests/pytorch_tests/test_tuple_unpack.py
new file mode 100644
index 00000000000000..c8343e4deee836
--- /dev/null
+++ b/tests/layer_tests/pytorch_tests/test_tuple_unpack.py
@@ -0,0 +1,34 @@
+# Copyright (C) 2018-2025 Intel Corporation
+# SPDX-License-Identifier: Apache-2.0
+
+import pytest
+
+from pytorch_layer_test_class import PytorchLayerTest
+
+
+class TestTupleUnpack(PytorchLayerTest):
+    def _prepare_input(self):
+        import numpy as np
+        return (np.random.randn(2, 4, 6, 8).astype(np.float32),)
+
+    def create_model(self):
+
+        import torch
+
+        class TupleArgument(torch.nn.Module):
+            def __init__(self):
+                super().__init__()
+                self.dims = (1, 2, 3, 4)
+
+            def forward(self, input_data):
+                N, _, H, W = self.dims
+                return input_data * N * H * W
+
+        return TupleArgument(), None, "prim::TupleUnpack"
+
+    @pytest.mark.nightly
+    @pytest.mark.precommit
+    @pytest.mark.precommit_fx_backend
+    def test_touple_unpack(self, ie_device, precision, ir_version):
+        self._test(*self.create_model(), ie_device, precision, ir_version,
+                   trace_model=False, freeze_model=False)


Reviews:
user: login: rkazants
body: can we have test for this case? what exactly are you fixing?
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T14:48:27Z
user: login: eaidova
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T14:48:28Z
user: login: rkazants
body: Need test, looks magic
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T14:49:02Z
user: login: rkazants
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T14:58:23Z
user: login: rkazants
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T15:02:43Z
user: login: rkazants
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-17T15:03:22Z
user: login: mvafin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T15:06:41Z

Review comments:
diff_hunk: @@ -29,16 +31,25 @@ PrimTupleUnpackReplacer::PrimTupleUnpackReplacer() {
         OutputVector outputs;
         auto input_node = tuple_unpack->get_input_node_shared_ptr(0);
         auto tuple_construct = cast_fw_node(input_node, "prim::TupleConstruct");
path: src/frontends/pytorch/src/transforms/tuple_unpack_replacer.cpp
user: login: rkazants
body: ```suggestion
```
unused var
created_at: 2025-04-17T14:58:22Z
author_association: COLLABORATOR
diff_hunk: @@ -29,16 +31,25 @@ PrimTupleUnpackReplacer::PrimTupleUnpackReplacer() {
         OutputVector outputs;
         auto input_node = tuple_unpack->get_input_node_shared_ptr(0);
         auto tuple_construct = cast_fw_node(input_node, "prim::TupleConstruct");
-        if (!tuple_construct) {
-            return false;
-        }
-        for (const auto& input : input_node->inputs()) {
-            const auto& out = input.get_source_output();
-            outputs.push_back(out);
-        }
-        replace_node(tuple_unpack, outputs);
+        if (cast_fw_node(input_node, "prim::TupleConstruct")) {
+            for (const auto& input : input_node->inputs()) {
+                const auto& out = input.get_source_output();
+                outputs.push_back(out);
+            }
+            replace_node(tuple_unpack, outputs);
path: src/frontends/pytorch/src/transforms/tuple_unpack_replacer.cpp
user: login: rkazants
body: can it be a case that not all outputs from TupleUnpack is used or its output size is less that `outputs` size?
created_at: 2025-04-17T15:02:43Z
author_association: COLLABORATOR
diff_hunk: @@ -29,16 +31,25 @@ PrimTupleUnpackReplacer::PrimTupleUnpackReplacer() {
         OutputVector outputs;
         auto input_node = tuple_unpack->get_input_node_shared_ptr(0);
         auto tuple_construct = cast_fw_node(input_node, "prim::TupleConstruct");
-        if (!tuple_construct) {
-            return false;
-        }
-        for (const auto& input : input_node->inputs()) {
-            const auto& out = input.get_source_output();
-            outputs.push_back(out);
-        }
-        replace_node(tuple_unpack, outputs);
+        if (cast_fw_node(input_node, "prim::TupleConstruct")) {
+            for (const auto& input : input_node->inputs()) {
+                const auto& out = input.get_source_output();
+                outputs.push_back(out);
+            }
+            replace_node(tuple_unpack, outputs);
path: src/frontends/pytorch/src/transforms/tuple_unpack_replacer.cpp
user: login: mvafin
body: No, I will add such case to tests
created_at: 2025-04-17T15:06:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: mvafin
created_at: 2025-04-17T14:50:00Z
body: > Need test, looks magic

Sorry, forgot to add test file
author_association: CONTRIBUTOR
user: login: rkazants
created_at: 2025-04-17T14:50:26Z
body: If you can't create simple reproducer, you can extend model hub tests to validate resnet50 model from torchvision.models

Best regards,
Roman
author_association: COLLABORATOR
user: login: mvafin
created_at: 2025-04-17T14:52:12Z
body: > If you can't create simple reproducer, you can extend model hub tests to validate resnet50 model from torchvision.models
> 
> Best regards, Roman

That is a custom model
author_association: CONTRIBUTOR