From 84b296865e5e19b2b1dba84153398f70df03bf4f Mon Sep 17 00:00:00 2001 From: Yuduo Wu Date: Wed, 29 Oct 2025 11:51:53 -0700 Subject: [PATCH 1/3] Allow get clip constant min/max from Dequantize --- onnxruntime/core/optimizer/utils.cc | 102 ++++++++++++++++-- .../test/optimizer/qdq_transformer_test.cc | 67 ++++++++++++ 2 files changed, 160 insertions(+), 9 deletions(-) diff --git a/onnxruntime/core/optimizer/utils.cc b/onnxruntime/core/optimizer/utils.cc index 627e8ade2946f..3eb352859d0fb 100644 --- a/onnxruntime/core/optimizer/utils.cc +++ b/onnxruntime/core/optimizer/utils.cc @@ -330,28 +330,112 @@ bool GetClipConstantMinMax(const Graph& graph, const Node& node, float& min, flo return true; } - bool is_constant = true; + bool is_constant = false; const ONNX_NAMESPACE::TensorProto* initializer = graph.GetConstantInitializer(input->Name(), true); if (initializer) { Initializer i(graph, *initializer, graph.ModelPath()); switch (initializer->data_type()) { case ONNX_NAMESPACE::TensorProto_DataType_FLOAT: value = *i.data(); + is_constant = true; break; - // double isn't currently supported - // case ONNX_NAMESPACE::TensorProto_DataType_DOUBLE: - // value = static_cast(*i.data()); - // break; case ONNX_NAMESPACE::TensorProto_DataType_FLOAT16: value = math::halfToFloat(i.data()->val); + is_constant = true; break; default: - ORT_THROW("Unexpected data type for Clip input of ", initializer->data_type()); + is_constant = false; + break; + } + return is_constant; + } + const Node* producer = graph.GetProducerNode(input->Name()); + if (producer && producer->OpType() == "DequantizeLinear") { + const auto& dq_inputs = producer->InputDefs(); + const ONNX_NAMESPACE::TensorProto* dq_input = graph.GetConstantInitializer(dq_inputs[0]->Name(), true); + const ONNX_NAMESPACE::TensorProto* dq_scale = graph.GetConstantInitializer(dq_inputs[1]->Name(), true); + const ONNX_NAMESPACE::TensorProto* dq_zero_point = graph.GetConstantInitializer(dq_inputs[2]->Name(), true); + if (!dq_input || !dq_scale || !dq_zero_point) { + return false; + } + // Check scale and zero_point are scalar + Initializer scale_initializer(graph, *dq_scale, graph.ModelPath()); + Initializer zero_point_initializer(graph, *dq_zero_point, graph.ModelPath()); + if (scale_initializer.dims().size() != 0 || zero_point_initializer.dims().size() != 0) { + return false; // Not scalar + } + float scale = 1.0f; + float zero_point = 0.0f; + // Get scale + switch (dq_scale->data_type()) { + case ONNX_NAMESPACE::TensorProto_DataType_FLOAT: { + scale = *scale_initializer.data(); + break; + } + case ONNX_NAMESPACE::TensorProto_DataType_FLOAT16: { + scale = math::halfToFloat(scale_initializer.data()->val); + break; + } + default: + return false; + } + // Get zero_point + switch (dq_zero_point->data_type()) { + case ONNX_NAMESPACE::TensorProto_DataType_UINT8: { + zero_point = static_cast(*zero_point_initializer.data()); + break; + } + case ONNX_NAMESPACE::TensorProto_DataType_INT8: { + zero_point = static_cast(*zero_point_initializer.data()); + break; + } + case ONNX_NAMESPACE::TensorProto_DataType_UINT16: { + zero_point = static_cast(*zero_point_initializer.data()); + break; + } + case ONNX_NAMESPACE::TensorProto_DataType_INT16: { + zero_point = static_cast(*zero_point_initializer.data()); + break; + } + case ONNX_NAMESPACE::TensorProto_DataType_INT32: { + zero_point = static_cast(*zero_point_initializer.data()); + break; + } + default: + return false; + } + // Restore original input value + Initializer x_initializer(graph, *dq_input, graph.ModelPath()); + switch (dq_input->data_type()) { + case ONNX_NAMESPACE::TensorProto_DataType_UINT8: { + value = scale * (static_cast(*x_initializer.data()) - zero_point); + is_constant = true; + break; + } + case ONNX_NAMESPACE::TensorProto_DataType_INT8: { + value = scale * (static_cast(*x_initializer.data()) - zero_point); + is_constant = true; + break; + } + case ONNX_NAMESPACE::TensorProto_DataType_UINT16: { + value = scale * (static_cast(*x_initializer.data()) - zero_point); + is_constant = true; + break; + } + case ONNX_NAMESPACE::TensorProto_DataType_INT16: { + value = scale * (static_cast(*x_initializer.data()) - zero_point); + is_constant = true; + break; + } + case ONNX_NAMESPACE::TensorProto_DataType_INT32: { + value = scale * (static_cast(*x_initializer.data()) - zero_point); + is_constant = true; + break; + } + default: + return false; } - } else { - is_constant = false; } - return is_constant; }; diff --git a/onnxruntime/test/optimizer/qdq_transformer_test.cc b/onnxruntime/test/optimizer/qdq_transformer_test.cc index ba2b942f73320..62bded57ed6eb 100644 --- a/onnxruntime/test/optimizer/qdq_transformer_test.cc +++ b/onnxruntime/test/optimizer/qdq_transformer_test.cc @@ -4197,6 +4197,73 @@ TEST(QDQTransformerTests, QDQ_Selector_Test) { } } +TEST(QDQTransformerTests, QDQ_Selector_Test_Conv_Clip) { + const auto& logger = DefaultLoggingManager().DefaultLogger(); + + auto build_test_case = [&](ModelTestBuilder& builder) { + auto* input_arg = builder.MakeInput({1, 2, 4, 4}, std::numeric_limits::min(), + std::numeric_limits::max()); + auto* weight_arg = builder.MakeInput({2, 1, 3, 3}, std::numeric_limits::min(), + std::numeric_limits::max()); + auto* bias_arg = + builder.MakeInput({2}, std::numeric_limits::min(), std::numeric_limits::max()); + auto* dq_input = builder.MakeIntermediate(); + auto* dq_weight = builder.MakeIntermediate(); + auto* dq_bias = builder.MakeIntermediate(); + builder.AddDequantizeLinearNode(input_arg, 0.02348f, uint8_t(0), dq_input, false); + builder.AddDequantizeLinearNode(weight_arg, 0.307f, uint8_t(0), dq_weight, false); + builder.AddDequantizeLinearNode(bias_arg, 0.007f, int32_t(0), dq_bias, false); + + // Conv + auto* conv_output = builder.MakeIntermediate(); + Node& conv_node = builder.AddNode("Conv", {dq_input, dq_weight, dq_bias}, {conv_output}); + conv_node.AddAttribute("pads", std::vector{1, 1, 1, 1}); + conv_node.AddAttribute("strides", std::vector{1, 1}); + conv_node.AddAttribute("dilations", std::vector{1, 1}); + conv_node.AddAttribute("group", int64_t(2)); + conv_node.AddAttribute("kernel_shape", std::vector{3, 3}); + + // Clip + NodeArg* clip_min = builder.MakeScalarInitializer(128); // -> 0.0f + NodeArg* clip_max = builder.MakeScalarInitializer(255); // -> 0.6f + NodeArg* min_dq = builder.MakeIntermediate(); + NodeArg* max_dq = builder.MakeIntermediate(); + builder.AddDequantizeLinearNode(clip_min, 0.00784313772f, static_cast(128), min_dq, false); + builder.AddDequantizeLinearNode(clip_max, 0.0235293377f, static_cast(0), max_dq, false); + NodeArg* clip_fp32 = builder.MakeIntermediate(); + builder.AddNode("Clip", {conv_output, min_dq, max_dq}, {clip_fp32}); + NodeArg* clip_q = builder.MakeIntermediate(); + NodeArg* clip_dq = builder.MakeOutput(); + builder.AddQuantizeLinearNode(clip_fp32, 0.0082940589f, static_cast(0), clip_q, false); + builder.AddDequantizeLinearNode(clip_q, 0.0082940589f, static_cast(0), clip_dq, false); + }; + // Build the model for this test. + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 18; + domain_to_version[kMSDomain] = 1; + Model model("TransformerTester", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, {}, logger); + Graph& graph = model.MainGraph(); + ModelTestBuilder helper(graph); + build_test_case(helper); + helper.SetGraphOutputs(); + ASSERT_STATUS_OK(model.MainGraph().Resolve()); + const GraphViewer whole_graph_viewer(graph); + + // Make sure node 3 is the conv node + const auto* conv_node = graph.GetNode(3); + ASSERT_TRUE(nullptr != conv_node); + ASSERT_EQ("Conv", conv_node->OpType()); + + // Make sure the conv QDQ group is selected + onnxruntime::QDQ::ConvNodeGroupSelector conv_selector; + const auto result = conv_selector.GetQDQSelection(whole_graph_viewer, *conv_node); + ASSERT_TRUE(result.has_value()); + const auto& qdq_group = *result; + ASSERT_EQ(NodeIndex(3), qdq_group.target_node); + ASSERT_EQ(NodeIndex(6), qdq_group.redundant_clip_node); +} + TEST(QDQTransformerTests, QDQ_Selector_Test_Conv_Relu) { const auto& logger = DefaultLoggingManager().DefaultLogger(); From 2cfe25c6f3ea1fd2902321b0226313e185e4a210 Mon Sep 17 00:00:00 2001 From: Yuduo Wu Date: Thu, 30 Oct 2025 15:42:56 -0700 Subject: [PATCH 2/3] Address comment -- non scalar and more tests --- onnxruntime/core/optimizer/utils.cc | 12 +++- .../test/optimizer/qdq_transformer_test.cc | 69 ++++++++++++++++++- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/onnxruntime/core/optimizer/utils.cc b/onnxruntime/core/optimizer/utils.cc index 3eb352859d0fb..d12065a0c1cc4 100644 --- a/onnxruntime/core/optimizer/utils.cc +++ b/onnxruntime/core/optimizer/utils.cc @@ -161,6 +161,11 @@ bool IsAttributeWithExpectedValues(const Node& node, const std::string& attr_nam return true; } +bool IsScalarOr1Element1DTensor(gsl::span tensor_shape) { + const size_t rank = tensor_shape.size(); + return (rank == 0) || ((rank == 1) && (tensor_shape[0] == 1)); +} + bool AppendTensorFromInitializer(const Graph& graph, const NodeArg& input_arg, InlinedVector& data, bool require_constant) { if (require_constant && !graph_utils::IsConstantInitializer(graph, input_arg.Name(), true)) { return false; @@ -361,8 +366,8 @@ bool GetClipConstantMinMax(const Graph& graph, const Node& node, float& min, flo // Check scale and zero_point are scalar Initializer scale_initializer(graph, *dq_scale, graph.ModelPath()); Initializer zero_point_initializer(graph, *dq_zero_point, graph.ModelPath()); - if (scale_initializer.dims().size() != 0 || zero_point_initializer.dims().size() != 0) { - return false; // Not scalar + if (!IsScalarOr1Element1DTensor(scale_initializer.dims()) || !IsScalarOr1Element1DTensor(zero_point_initializer.dims())) { + return false; } float scale = 1.0f; float zero_point = 0.0f; @@ -406,6 +411,9 @@ bool GetClipConstantMinMax(const Graph& graph, const Node& node, float& min, flo } // Restore original input value Initializer x_initializer(graph, *dq_input, graph.ModelPath()); + if (!IsScalarOr1Element1DTensor(x_initializer.dims())) { + return false; + } switch (dq_input->data_type()) { case ONNX_NAMESPACE::TensorProto_DataType_UINT8: { value = scale * (static_cast(*x_initializer.data()) - zero_point); diff --git a/onnxruntime/test/optimizer/qdq_transformer_test.cc b/onnxruntime/test/optimizer/qdq_transformer_test.cc index 62bded57ed6eb..2a82e7d1f6986 100644 --- a/onnxruntime/test/optimizer/qdq_transformer_test.cc +++ b/onnxruntime/test/optimizer/qdq_transformer_test.cc @@ -4197,7 +4197,7 @@ TEST(QDQTransformerTests, QDQ_Selector_Test) { } } -TEST(QDQTransformerTests, QDQ_Selector_Test_Conv_Clip) { +TEST(QDQTransformerTests, QDQ_Selector_Test_ConvClip) { const auto& logger = DefaultLoggingManager().DefaultLogger(); auto build_test_case = [&](ModelTestBuilder& builder) { @@ -4264,6 +4264,73 @@ TEST(QDQTransformerTests, QDQ_Selector_Test_Conv_Clip) { ASSERT_EQ(NodeIndex(6), qdq_group.redundant_clip_node); } +TEST(QDQTransformerTests, QDQ_Selector_Test_ConvClipNonScalar) { + const auto& logger = DefaultLoggingManager().DefaultLogger(); + + auto build_test_case = [&](ModelTestBuilder& builder) { + auto* input_arg = builder.MakeInput({1, 2, 4, 4}, std::numeric_limits::min(), + std::numeric_limits::max()); + auto* weight_arg = builder.MakeInput({2, 1, 3, 3}, std::numeric_limits::min(), + std::numeric_limits::max()); + auto* bias_arg = + builder.MakeInput({2}, std::numeric_limits::min(), std::numeric_limits::max()); + auto* dq_input = builder.MakeIntermediate(); + auto* dq_weight = builder.MakeIntermediate(); + auto* dq_bias = builder.MakeIntermediate(); + builder.AddDequantizeLinearNode(input_arg, 0.02348f, uint8_t(0), dq_input, false); + builder.AddDequantizeLinearNode(weight_arg, 0.307f, uint8_t(0), dq_weight, false); + builder.AddDequantizeLinearNode(bias_arg, 0.007f, int32_t(0), dq_bias, false); + + // Conv + auto* conv_output = builder.MakeIntermediate(); + Node& conv_node = builder.AddNode("Conv", {dq_input, dq_weight, dq_bias}, {conv_output}); + conv_node.AddAttribute("pads", std::vector{1, 1, 1, 1}); + conv_node.AddAttribute("strides", std::vector{1, 1}); + conv_node.AddAttribute("dilations", std::vector{1, 1}); + conv_node.AddAttribute("group", int64_t(2)); + conv_node.AddAttribute("kernel_shape", std::vector{3, 3}); + + // Clip + NodeArg* clip_min = builder.Make1DInitializer({128}); // -> 0.0f + NodeArg* clip_max = builder.Make1DInitializer({255}); // -> 0.6f + NodeArg* min_dq = builder.MakeIntermediate(); + NodeArg* max_dq = builder.MakeIntermediate(); + builder.AddDequantizeLinearNode(clip_min, 0.00784313772f, static_cast(128), min_dq, false); + builder.AddDequantizeLinearNode(clip_max, 0.0235293377f, static_cast(0), max_dq, false); + NodeArg* clip_fp32 = builder.MakeIntermediate(); + builder.AddNode("Clip", {conv_output, min_dq, max_dq}, {clip_fp32}); + NodeArg* clip_q = builder.MakeIntermediate(); + NodeArg* clip_dq = builder.MakeOutput(); + builder.AddQuantizeLinearNode(clip_fp32, 0.0082940589f, static_cast(0), clip_q, false); + builder.AddDequantizeLinearNode(clip_q, 0.0082940589f, static_cast(0), clip_dq, false); + }; + // Build the model for this test. + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 18; + domain_to_version[kMSDomain] = 1; + Model model("TransformerTester", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, {}, logger); + Graph& graph = model.MainGraph(); + ModelTestBuilder helper(graph); + build_test_case(helper); + helper.SetGraphOutputs(); + ASSERT_STATUS_OK(model.MainGraph().Resolve()); + const GraphViewer whole_graph_viewer(graph); + + // Make sure node 3 is the conv node + const auto* conv_node = graph.GetNode(3); + ASSERT_TRUE(nullptr != conv_node); + ASSERT_EQ("Conv", conv_node->OpType()); + + // Make sure the conv QDQ group is selected + onnxruntime::QDQ::ConvNodeGroupSelector conv_selector; + const auto result = conv_selector.GetQDQSelection(whole_graph_viewer, *conv_node); + ASSERT_TRUE(result.has_value()); + const auto& qdq_group = *result; + ASSERT_EQ(NodeIndex(3), qdq_group.target_node); + ASSERT_EQ(NodeIndex(6), qdq_group.redundant_clip_node); +} + TEST(QDQTransformerTests, QDQ_Selector_Test_Conv_Relu) { const auto& logger = DefaultLoggingManager().DefaultLogger(); From b25e851f9937cd77974ea3ab33fdfc7ae2c28932 Mon Sep 17 00:00:00 2001 From: Yuduo Wu Date: Mon, 3 Nov 2025 15:04:07 -0800 Subject: [PATCH 3/3] Minor fix --- onnxruntime/core/optimizer/utils.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/onnxruntime/core/optimizer/utils.cc b/onnxruntime/core/optimizer/utils.cc index d12065a0c1cc4..4a323eefe1fe7 100644 --- a/onnxruntime/core/optimizer/utils.cc +++ b/onnxruntime/core/optimizer/utils.cc @@ -161,11 +161,6 @@ bool IsAttributeWithExpectedValues(const Node& node, const std::string& attr_nam return true; } -bool IsScalarOr1Element1DTensor(gsl::span tensor_shape) { - const size_t rank = tensor_shape.size(); - return (rank == 0) || ((rank == 1) && (tensor_shape[0] == 1)); -} - bool AppendTensorFromInitializer(const Graph& graph, const NodeArg& input_arg, InlinedVector& data, bool require_constant) { if (require_constant && !graph_utils::IsConstantInitializer(graph, input_arg.Name(), true)) { return false; @@ -310,6 +305,11 @@ bool IsOperationDeterministic(const std::string& domain, const std::string& op) #if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) +bool IsScalarOr1Element1DTensor(gsl::span tensor_shape) { + const size_t rank = tensor_shape.size(); + return (rank == 0) || ((rank == 1) && (tensor_shape[0] == 1)); +} + bool GetClipConstantMinMax(const Graph& graph, const Node& node, float& min, float& max) { min = std::numeric_limits::lowest(); max = std::numeric_limits::max();