From 0542756d66ad0deedf0d12c9be2c6b95fe91b110 Mon Sep 17 00:00:00 2001 From: Fanchen Kong Date: Tue, 9 Jun 2026 15:37:21 +0800 Subject: [PATCH 1/6] Recover Conv/ConvTranspose rank from weight when input shape is unknown The layout transformer skips converting a node to NHWC when input[0] has no inferred shape. But the NCHW<->NHWC permutation depends only on rank. For Conv/ConvTranspose the data input and weight share the same rank, so when input[0]'s rank is unknown, recover it from the weight at input[1]. --- .../layout_transformation.cc | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/onnxruntime/core/optimizer/layout_transformation/layout_transformation.cc b/onnxruntime/core/optimizer/layout_transformation/layout_transformation.cc index f611c992e0f57..653ed773be4fa 100644 --- a/onnxruntime/core/optimizer/layout_transformation/layout_transformation.cc +++ b/onnxruntime/core/optimizer/layout_transformation/layout_transformation.cc @@ -122,14 +122,21 @@ Status TransformLayoutForEP(Graph& graph, bool& modified, const IExecutionProvid continue; } - // Skip if unknown rank - auto shape = api_graph->GetValueInfo(node->Inputs()[0])->Shape(); - if (!shape.has_value()) { + // The NCHW<->NHWC permutation depends only on rank. For Conv/ConvTranspose the data input and the weight share + // the same rank, so an unknown input[0] rank can be recovered from the weight at input[1]. + std::optional input_rank = api_graph->GetValueInfo(node->Inputs()[0])->ShapeRank(); + if (!input_rank.has_value() && (op_type == "Conv" || op_type == "ConvTranspose") && + node->Inputs().size() > 1 && !node->Inputs()[1].empty()) { + input_rank = api_graph->GetValueInfo(node->Inputs()[1])->ShapeRank(); + } + + // Skip if rank is still unknown. + if (!input_rank.has_value()) { continue; } // Convert to channels last - size_t rank = shape->size(); + size_t rank = *input_rank; bool has_channel_last_attr = node->GetAttributeInt("channels_last").has_value() ? true : false; if (has_channel_last_attr) { From 9eb512d0d3a91fa535176264bc39e3951a43398d Mon Sep 17 00:00:00 2001 From: Fanchen Kong Date: Wed, 24 Jun 2026 11:15:48 +0800 Subject: [PATCH 2/6] Simplify rank recovery check by removing redundant guard --- .../optimizer/layout_transformation/layout_transformation.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/onnxruntime/core/optimizer/layout_transformation/layout_transformation.cc b/onnxruntime/core/optimizer/layout_transformation/layout_transformation.cc index 653ed773be4fa..0f0de9e8a22d6 100644 --- a/onnxruntime/core/optimizer/layout_transformation/layout_transformation.cc +++ b/onnxruntime/core/optimizer/layout_transformation/layout_transformation.cc @@ -125,8 +125,7 @@ Status TransformLayoutForEP(Graph& graph, bool& modified, const IExecutionProvid // The NCHW<->NHWC permutation depends only on rank. For Conv/ConvTranspose the data input and the weight share // the same rank, so an unknown input[0] rank can be recovered from the weight at input[1]. std::optional input_rank = api_graph->GetValueInfo(node->Inputs()[0])->ShapeRank(); - if (!input_rank.has_value() && (op_type == "Conv" || op_type == "ConvTranspose") && - node->Inputs().size() > 1 && !node->Inputs()[1].empty()) { + if (!input_rank.has_value() && (op_type == "Conv" || op_type == "ConvTranspose")) { input_rank = api_graph->GetValueInfo(node->Inputs()[1])->ShapeRank(); } From 88889e32ae1a255d22c5e2e7f1f4f38ea79a30ee Mon Sep 17 00:00:00 2001 From: Fanchen Kong Date: Wed, 24 Jun 2026 11:15:54 +0800 Subject: [PATCH 3/6] Add test for Conv rank recovery from weight when input rank is unknown --- .../optimizer/transpose_optimizer_test.cc | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/onnxruntime/test/optimizer/transpose_optimizer_test.cc b/onnxruntime/test/optimizer/transpose_optimizer_test.cc index 080c382db5d93..c401e13934e5b 100644 --- a/onnxruntime/test/optimizer/transpose_optimizer_test.cc +++ b/onnxruntime/test/optimizer/transpose_optimizer_test.cc @@ -4664,6 +4664,56 @@ TEST(TransposeOptimizerTests, LayoutTransformDoesNotRetargetNhwcFusedConv) { EXPECT_EQ(nhwc_fused_conv_count, 1); } +// Verifies that layout transformation recovers Conv rank from weight when input rank is unknown. +TEST(TransposeOptimizerTests, LayoutTransformConvRecoverRankFromWeight) { + std::unordered_map domain_to_version{{kOnnxDomain, 13}}; + Model model("LayoutTransformConvRecoverRankFromWeight", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), domain_to_version, {}, + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + ModelTestBuilder builder(graph); + + // Create input with unknown shape (cleared). + auto* input_arg = builder.MakeInput({1, 3, 7, 7}, -1.0f, 1.0f); + input_arg->ClearShape(); + + // Weight has known shape [8, 3, 3, 3] - rank 4. + auto* weight_arg = builder.MakeInitializer({8, 3, 3, 3}, -1.0f, 1.0f); + auto* output_arg = builder.MakeOutput(); + + auto& conv = builder.AddNode("Conv", {input_arg, weight_arg}, {output_arg}); + conv.AddAttribute("pads", std::vector{1, 1, 1, 1}); + conv.AddAttribute("strides", std::vector{1, 1}); + conv.AddAttribute("kernel_shape", std::vector{3, 3}); + + builder.SetGraphOutputs(); + ASSERT_STATUS_OK(graph.Resolve()); + + std::string model_data; + model.ToProto().SerializeToString(&model_data); + + SessionOptions so; + using InternalTestingEP = internal_testing_ep::InternalTestingExecutionProvider; + const std::unordered_set empty_set; + auto internal_testing_ep = std::make_unique(empty_set, empty_set, DataLayout::NHWC); + internal_testing_ep->EnableStaticKernels().TakeAllNodes(); + + InferenceSessionWrapper session{so, GetEnvironment()}; + ASSERT_STATUS_OK(session.RegisterExecutionProvider(std::move(internal_testing_ep))); + ASSERT_STATUS_OK(session.Load(model_data.data(), static_cast(model_data.size()))); + ASSERT_STATUS_OK(session.Initialize()); + + const auto& optimized_graph = session.GetGraph(); + const auto op_to_count = CountOpsInGraph(optimized_graph); + const auto get_op_count = [&op_to_count](std::string_view op_type) { + const auto it = op_to_count.find(std::string{op_type}); + return it == op_to_count.end() ? 0 : it->second; + }; + + // Transpose nodes should be inserted, proving that layout transformation proceeded after recovering rank from weight. + EXPECT_GT(get_op_count("Transpose"), 0) << "Layout transformation should insert Transpose nodes for NCHW->NHWC conversion"; +} + TEST(TransposeOptimizerTests, QnnTransposeReshapeQDQ) { Status status; auto model_uri = ORT_TSTR("testdata/layout_transform_reshape.qdq.onnx"); From 5c934f1869ee760595430ca7147df86493106fc0 Mon Sep 17 00:00:00 2001 From: Fanchen Kong Date: Wed, 24 Jun 2026 15:44:11 +0800 Subject: [PATCH 4/6] Update comments Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../optimizer/layout_transformation/layout_transformation.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/onnxruntime/core/optimizer/layout_transformation/layout_transformation.cc b/onnxruntime/core/optimizer/layout_transformation/layout_transformation.cc index 0f0de9e8a22d6..16b90d762a16e 100644 --- a/onnxruntime/core/optimizer/layout_transformation/layout_transformation.cc +++ b/onnxruntime/core/optimizer/layout_transformation/layout_transformation.cc @@ -122,8 +122,9 @@ Status TransformLayoutForEP(Graph& graph, bool& modified, const IExecutionProvid continue; } - // The NCHW<->NHWC permutation depends only on rank. For Conv/ConvTranspose the data input and the weight share - // the same rank, so an unknown input[0] rank can be recovered from the weight at input[1]. + // The NCHW<->NHWC permutation depends only on rank. For Conv/ConvTranspose (and FusedConv, which is treated as Conv + // here) the data input and the weight share the same rank, so an unknown input[0] rank can be recovered from the + // weight at input[1]. std::optional input_rank = api_graph->GetValueInfo(node->Inputs()[0])->ShapeRank(); if (!input_rank.has_value() && (op_type == "Conv" || op_type == "ConvTranspose")) { input_rank = api_graph->GetValueInfo(node->Inputs()[1])->ShapeRank(); From c6c3039c291eae3fab5fafa62f328e8246e5b045 Mon Sep 17 00:00:00 2001 From: Fanchen Kong Date: Wed, 24 Jun 2026 15:50:01 +0800 Subject: [PATCH 5/6] Add test for ConvTranspose rank recovery from weight when input rank is unknown --- .../optimizer/transpose_optimizer_test.cc | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/onnxruntime/test/optimizer/transpose_optimizer_test.cc b/onnxruntime/test/optimizer/transpose_optimizer_test.cc index c401e13934e5b..e6545586b7ca9 100644 --- a/onnxruntime/test/optimizer/transpose_optimizer_test.cc +++ b/onnxruntime/test/optimizer/transpose_optimizer_test.cc @@ -4714,6 +4714,56 @@ TEST(TransposeOptimizerTests, LayoutTransformConvRecoverRankFromWeight) { EXPECT_GT(get_op_count("Transpose"), 0) << "Layout transformation should insert Transpose nodes for NCHW->NHWC conversion"; } +// Verifies that layout transformation recovers ConvTranspose rank from weight when input rank is unknown. +TEST(TransposeOptimizerTests, LayoutTransformConvTransposeRecoverRankFromWeight) { + std::unordered_map domain_to_version{{kOnnxDomain, 13}}; + Model model("LayoutTransformConvTransposeRecoverRankFromWeight", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), domain_to_version, {}, + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + ModelTestBuilder builder(graph); + + // Create input with unknown shape (cleared). + auto* input_arg = builder.MakeInput({1, 3, 7, 7}, -1.0f, 1.0f); + input_arg->ClearShape(); + + // Weight has known shape [3, 8, 3, 3] - rank 4. + auto* weight_arg = builder.MakeInitializer({3, 8, 3, 3}, -1.0f, 1.0f); + auto* output_arg = builder.MakeOutput(); + + auto& conv_transpose = builder.AddNode("ConvTranspose", {input_arg, weight_arg}, {output_arg}); + conv_transpose.AddAttribute("pads", std::vector{1, 1, 1, 1}); + conv_transpose.AddAttribute("strides", std::vector{1, 1}); + conv_transpose.AddAttribute("kernel_shape", std::vector{3, 3}); + + builder.SetGraphOutputs(); + ASSERT_STATUS_OK(graph.Resolve()); + + std::string model_data; + model.ToProto().SerializeToString(&model_data); + + SessionOptions so; + using InternalTestingEP = internal_testing_ep::InternalTestingExecutionProvider; + const std::unordered_set empty_set; + auto internal_testing_ep = std::make_unique(empty_set, empty_set, DataLayout::NHWC); + internal_testing_ep->EnableStaticKernels().TakeAllNodes(); + + InferenceSessionWrapper session{so, GetEnvironment()}; + ASSERT_STATUS_OK(session.RegisterExecutionProvider(std::move(internal_testing_ep))); + ASSERT_STATUS_OK(session.Load(model_data.data(), static_cast(model_data.size()))); + ASSERT_STATUS_OK(session.Initialize()); + + const auto& optimized_graph = session.GetGraph(); + const auto op_to_count = CountOpsInGraph(optimized_graph); + const auto get_op_count = [&op_to_count](std::string_view op_type) { + const auto it = op_to_count.find(std::string{op_type}); + return it == op_to_count.end() ? 0 : it->second; + }; + + // Transpose nodes should be inserted, proving that layout transformation proceeded after recovering rank from weight. + EXPECT_GT(get_op_count("Transpose"), 0) << "Layout transformation should insert Transpose nodes for NCHW->NHWC conversion"; +} + TEST(TransposeOptimizerTests, QnnTransposeReshapeQDQ) { Status status; auto model_uri = ORT_TSTR("testdata/layout_transform_reshape.qdq.onnx"); From 28cc22560595230a4a4fbc053573398dfeec0639 Mon Sep 17 00:00:00 2001 From: Fanchen Kong Date: Thu, 25 Jun 2026 09:34:07 +0800 Subject: [PATCH 6/6] Refactor Conv/ConvTranspose rank recovery tests to use helper function --- .../optimizer/transpose_optimizer_test.cc | 71 +++++-------------- 1 file changed, 16 insertions(+), 55 deletions(-) diff --git a/onnxruntime/test/optimizer/transpose_optimizer_test.cc b/onnxruntime/test/optimizer/transpose_optimizer_test.cc index e6545586b7ca9..4b12c1e872f35 100644 --- a/onnxruntime/test/optimizer/transpose_optimizer_test.cc +++ b/onnxruntime/test/optimizer/transpose_optimizer_test.cc @@ -4664,10 +4664,11 @@ TEST(TransposeOptimizerTests, LayoutTransformDoesNotRetargetNhwcFusedConv) { EXPECT_EQ(nhwc_fused_conv_count, 1); } -// Verifies that layout transformation recovers Conv rank from weight when input rank is unknown. -TEST(TransposeOptimizerTests, LayoutTransformConvRecoverRankFromWeight) { +// Helper function to test layout transformation with unknown input rank but known weight rank. +static void TestLayoutTransformWithUnknownInputRank(const std::string& op_type, + const std::vector& weight_shape) { std::unordered_map domain_to_version{{kOnnxDomain, 13}}; - Model model("LayoutTransformConvRecoverRankFromWeight", false, ModelMetaData(), PathString(), + Model model("LayoutTransform_" + op_type + "_RecoverRankFromWeight", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), domain_to_version, {}, DefaultLoggingManager().DefaultLogger()); Graph& graph = model.MainGraph(); @@ -4677,14 +4678,14 @@ TEST(TransposeOptimizerTests, LayoutTransformConvRecoverRankFromWeight) { auto* input_arg = builder.MakeInput({1, 3, 7, 7}, -1.0f, 1.0f); input_arg->ClearShape(); - // Weight has known shape [8, 3, 3, 3] - rank 4. - auto* weight_arg = builder.MakeInitializer({8, 3, 3, 3}, -1.0f, 1.0f); + // Weight has known shape with rank 4. + auto* weight_arg = builder.MakeInitializer(weight_shape, -1.0f, 1.0f); auto* output_arg = builder.MakeOutput(); - auto& conv = builder.AddNode("Conv", {input_arg, weight_arg}, {output_arg}); - conv.AddAttribute("pads", std::vector{1, 1, 1, 1}); - conv.AddAttribute("strides", std::vector{1, 1}); - conv.AddAttribute("kernel_shape", std::vector{3, 3}); + auto& node = builder.AddNode(op_type, {input_arg, weight_arg}, {output_arg}); + node.AddAttribute("pads", std::vector{1, 1, 1, 1}); + node.AddAttribute("strides", std::vector{1, 1}); + node.AddAttribute("kernel_shape", std::vector{3, 3}); builder.SetGraphOutputs(); ASSERT_STATUS_OK(graph.Resolve()); @@ -4714,54 +4715,14 @@ TEST(TransposeOptimizerTests, LayoutTransformConvRecoverRankFromWeight) { EXPECT_GT(get_op_count("Transpose"), 0) << "Layout transformation should insert Transpose nodes for NCHW->NHWC conversion"; } +// Verifies that layout transformation recovers Conv rank from weight when input rank is unknown. +TEST(TransposeOptimizerTests, LayoutTransformConvRecoverRankFromWeight) { + TestLayoutTransformWithUnknownInputRank("Conv", {8, 3, 3, 3}); +} + // Verifies that layout transformation recovers ConvTranspose rank from weight when input rank is unknown. TEST(TransposeOptimizerTests, LayoutTransformConvTransposeRecoverRankFromWeight) { - std::unordered_map domain_to_version{{kOnnxDomain, 13}}; - Model model("LayoutTransformConvTransposeRecoverRankFromWeight", false, ModelMetaData(), PathString(), - IOnnxRuntimeOpSchemaRegistryList(), domain_to_version, {}, - DefaultLoggingManager().DefaultLogger()); - Graph& graph = model.MainGraph(); - ModelTestBuilder builder(graph); - - // Create input with unknown shape (cleared). - auto* input_arg = builder.MakeInput({1, 3, 7, 7}, -1.0f, 1.0f); - input_arg->ClearShape(); - - // Weight has known shape [3, 8, 3, 3] - rank 4. - auto* weight_arg = builder.MakeInitializer({3, 8, 3, 3}, -1.0f, 1.0f); - auto* output_arg = builder.MakeOutput(); - - auto& conv_transpose = builder.AddNode("ConvTranspose", {input_arg, weight_arg}, {output_arg}); - conv_transpose.AddAttribute("pads", std::vector{1, 1, 1, 1}); - conv_transpose.AddAttribute("strides", std::vector{1, 1}); - conv_transpose.AddAttribute("kernel_shape", std::vector{3, 3}); - - builder.SetGraphOutputs(); - ASSERT_STATUS_OK(graph.Resolve()); - - std::string model_data; - model.ToProto().SerializeToString(&model_data); - - SessionOptions so; - using InternalTestingEP = internal_testing_ep::InternalTestingExecutionProvider; - const std::unordered_set empty_set; - auto internal_testing_ep = std::make_unique(empty_set, empty_set, DataLayout::NHWC); - internal_testing_ep->EnableStaticKernels().TakeAllNodes(); - - InferenceSessionWrapper session{so, GetEnvironment()}; - ASSERT_STATUS_OK(session.RegisterExecutionProvider(std::move(internal_testing_ep))); - ASSERT_STATUS_OK(session.Load(model_data.data(), static_cast(model_data.size()))); - ASSERT_STATUS_OK(session.Initialize()); - - const auto& optimized_graph = session.GetGraph(); - const auto op_to_count = CountOpsInGraph(optimized_graph); - const auto get_op_count = [&op_to_count](std::string_view op_type) { - const auto it = op_to_count.find(std::string{op_type}); - return it == op_to_count.end() ? 0 : it->second; - }; - - // Transpose nodes should be inserted, proving that layout transformation proceeded after recovering rank from weight. - EXPECT_GT(get_op_count("Transpose"), 0) << "Layout transformation should insert Transpose nodes for NCHW->NHWC conversion"; + TestLayoutTransformWithUnknownInputRank("ConvTranspose", {3, 8, 3, 3}); } TEST(TransposeOptimizerTests, QnnTransposeReshapeQDQ) {