// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#include <deque>
#include "core/graph/graph_utils.h"
#include "core/optimizer/initializer.h"
#include "core/optimizer/nchwc_transformer.h"
#include "core/mlas/inc/mlas.h"

using namespace ONNX_NAMESPACE;
using namespace ::onnxruntime::common;
namespace onnxruntime {

class NchwcTransformerImpl {
 public:
  NchwcTransformerImpl(Graph& graph) noexcept : graph_(graph) {}

  void Transform(Node& node);
  void Finalize(bool& modified);

  static constexpr int kNchwcBatchChannelDims = 2;
  static constexpr int kNchwcSpatialDims = 2;
  static constexpr int kNchwcDims = kNchwcBatchChannelDims + kNchwcSpatialDims;

 private:
  // Associate the following state with each created NCHWc output keyed off the
  // original NodeArg.
  struct NchwcArgument {
    // Symbolic shape information for this NCHWc output. Each dimension stores
    // the original NodeArg* that sourced the value. Spatial dimensions also
    // track the number of times the original value has been shifted down due
    // to a stride count of 2.
    //
    // For example, the first Conv node that takes NCHW input will create a
    // NchwcArgument with the shape referencing itself. Other NCHWc nodes that
    // use this first Conv node then do a limited shape inference. The shape
    // inference carries forward any of the first Conv node's dimensions that
    // are unchanged or resets to the NodeArg* of the updated output node.
    //
    // The benefit of doing this is for models where the model inputs are not
    // fixed. For example, The YoloV3 model has the image height and width as
    // parameters. The model has branches that are candidates for Conv/Add
    // fusion that can be detected using this additional shape hint.
    struct Shape {
      const NodeArg* dims_[kNchwcDims];
      size_t shifts_[kNchwcSpatialDims];

      Shape(const NodeArg* initial_dim) {
        std::fill_n(dims_, kNchwcDims, initial_dim);
        std::fill_n(shifts_, kNchwcSpatialDims, 0);
      }

      bool IsDimEqual(const Shape& other, int dim) const {
        bool is_dim_equal = false;
        // Test if this dimension is derived from the same NodeArg.
        if (dims_[dim] == other.dims_[dim]) {
          if (dim >= kNchwcBatchChannelDims) {
            // Test if the NodeArg has been shifted down the same number of
            // times due to striding.
            int spatial_dim = dim - kNchwcBatchChannelDims;
            if (shifts_[spatial_dim] == other.shifts_[spatial_dim]) {
              is_dim_equal = true;
            }
          } else {
            is_dim_equal = true;
          }
        }
        return is_dim_equal;
      }
    };

    // Stores the node that generated the NCHWc output.
    Node& output_node_;

    // Stores the NodeArg that represents the NCHWc output.
    NodeArg* nchwc_arg_;

    // Stores the original number of uses for the original NodeArg. Edges are
    // removed from the graph as nodes are converted to NCHWc form.
    const size_t starting_original_uses_;

    // Stores the remaining number of uses for the original NodeArg. The count
    // is decremented as uses are converted to NCHWc format. Nodes are inserted
    // to reorder the output if this count is non-zero.
    size_t remaining_original_uses_;

    // Stores the logical number of channels for this NCHWc output. The NCHWc
    // NodeArg is zero padded to the NCHWc block size. If the output needs to
    // be reordered back to a standard tensor format, this channel count is
    // used to generate the expected number of channels.
    const int64_t channels_;

    // Stores the proto shape for the NCHWc output.
    const NchwcArgument::Shape shape_;

    NchwcArgument(Node& output_node, NodeArg* output_nchwc_arg, size_t original_uses, int64_t channels, const NchwcArgument::Shape& shape)
        : output_node_(output_node),
          nchwc_arg_(output_nchwc_arg),
          starting_original_uses_(original_uses),
          remaining_original_uses_(original_uses),
          channels_(channels),
          shape_(shape) {
    }
  };

  NchwcArgument* LookupNchwcArgument(NodeArg* arg) {
    auto it = nchwc_args_.find(arg);
    return (it != nchwc_args_.end()) ? it->second.get() : nullptr;
  }

  size_t RemoveOutputEdges(Node& node);
  void CreateNchwcArgument(Node& node, Node& nchwc_node, int64_t channels, const NchwcArgument::Shape& shape);
  void FuseNchwcArgument(Node& node, const NchwcArgument& nchwc_arg);
  void InsertReorderInput(Node& node);

  void ConvPoolShapeInference(const Node& node,
                              const NchwcArgument::Shape& input_shape,
                              NchwcArgument::Shape& output_shape,
                              const ONNX_NAMESPACE::TensorProto* filter_shape);
  Node& InsertReshape(NodeArg* input_arg, NodeArg* output_arg, bool split_channels);

  void TransformConv(Node& node);
  void TransformPool(Node& node);
  void TransformBinary(Node& node, bool add_node);
  void TransformConcat(Node& node);
  void TransformActivation(Node& node);
  void TransformBatchNormalization(Node& node);
  void TransformTransposeToNhwc(Node& node);
  void TransformResize(Node& node);
  void TrackTransposeFromNhwc(Node& node);

  Graph& graph_;

  // Stores a queue of nodes to be removed after walking through the graph.
  std::deque<NodeIndex> removed_nodes_;

  // Stores a mapping from the original NodeArg outputs to the NCHWc variants
  // created inside this graph transform.
  InlinedHashMap<NodeArg*, std::unique_ptr<NchwcArgument>> nchwc_args_;

  // Stores a mapping of NodeArg inputs that have already been reordered, so
  // multiple nodes can share the NCHWc input.
  InlinedHashMap<NodeArg*, NodeArg*> reorder_inputs_;

  // Stores a mapping of NodeArg filters that have already been reordered, so
  // multiple nodes can share the NCHWc filter.
  InlinedHashMap<NodeArg*, NodeArg*> filters_OIHWBo_;
  InlinedHashMap<NodeArg*, NodeArg*> filters_OIHWBiBo_;

  // Stores a mapping of NodeArg biases that have already been aligned to the
  // NCHWc block size, so multiple nodes can share the NCHWc biases.
  InlinedHashMap<NodeArg*, NodeArg*> aligned_biases_;

  // Stores the shape initializers for Reshape to split or unsplit the channels
  // dimension of a tensor.
  NodeArg* reshape_split_arg_{nullptr};
  NodeArg* reshape_unsplit_arg_{nullptr};

  // Tracks the last Transpose node and output NodeArg that transposed from
  // NHWC to NCHW format.
  Node* transpose_from_nhwc_node_{nullptr};
  NodeArg* transpose_from_nhwc_output_arg_{nullptr};
};

size_t NchwcTransformerImpl::RemoveOutputEdges(Node& node) {
  size_t output_edges_count = node.GetOutputEdgesCount();
  if (output_edges_count > 0) {
    graph_utils::RemoveNodeOutputEdges(graph_, node);
  }
  // Bias the edge count to handle the case of a node that produces a graph
  // output.
  if (graph_.NodeProducesGraphOutput(node)) {
    output_edges_count++;
  }
  return output_edges_count;
}

void NchwcTransformerImpl::CreateNchwcArgument(Node& node,
                                               Node& nchwc_node,
                                               int64_t channels,
                                               const NchwcArgument::Shape& shape) {
  size_t original_uses = RemoveOutputEdges(node);

  // Create a new NodeArg to track the output from the NCHWc node.
  auto& output_defs = nchwc_node.MutableOutputDefs();
  auto* output_original_arg = output_defs[0];
  std::string output_reorder_def_name = graph_.GenerateNodeArgName("reorder");
  auto* output_nchwc_arg = &graph_.GetOrCreateNodeArg(output_reorder_def_name, nullptr);
  nchwc_args_[output_original_arg] =
      std::make_unique<NchwcArgument>(nchwc_node, output_nchwc_arg, original_uses, channels, shape);
  output_defs[0] = output_nchwc_arg;
}

void NchwcTransformerImpl::FuseNchwcArgument(Node& node, const NchwcArgument& nchwc_arg) {
  size_t original_uses = RemoveOutputEdges(node);

  // Associate the existing NCHWc NodeArg with the output from this node.
  auto* output_original_arg = node.MutableOutputDefs()[0];
  auto& nchwc_node = nchwc_arg.output_node_;
  auto* output_nchwc_arg = nchwc_node.MutableOutputDefs()[0];
  nchwc_args_[output_original_arg] =
      std::make_unique<NchwcArgument>(nchwc_node, output_nchwc_arg, original_uses, nchwc_arg.channels_, nchwc_arg.shape_);
}

void NchwcTransformerImpl::InsertReorderInput(Node& node) {
  auto& input_defs = node.MutableInputDefs();
  auto* input_original_arg = input_defs[0];

  auto it = reorder_inputs_.find(input_original_arg);
  if (it == reorder_inputs_.end()) {
    std::string input_reorder_def_name = graph_.GenerateNodeArgName("reorder");
    auto* input_nchwc_arg = &graph_.GetOrCreateNodeArg(input_reorder_def_name, nullptr);
    reorder_inputs_[input_original_arg] = input_nchwc_arg;
    Node& reorder_input_node = graph_.AddNode(graph_.GenerateNodeName("ReorderInput"),
                                              "ReorderInput",
                                              "ReorderInput",
                                              {input_original_arg},
                                              {input_nchwc_arg},
                                              nullptr,
                                              kMSNchwcDomain);
    reorder_input_node.SetExecutionProviderType(kCpuExecutionProvider);
    input_defs[0] = input_nchwc_arg;

    // Attempt to fuse the ReorderInput with a previous Transpose of NHWC->NCHW.
    // If the last known node to transpose from NHWC is the same as this input
    // argument, then the Transpose node can be removed and the ReorderInput node
    // is modified to consume a tensor with NHWC layout order. The transpose was
    // already determined to have a single use and not be a graph output.
    if (transpose_from_nhwc_output_arg_ == input_original_arg) {
      reorder_input_node.MutableInputDefs()[0] = transpose_from_nhwc_node_->MutableInputDefs()[0];
      reorder_input_node.AddAttribute("channels_last", static_cast<int64_t>(1));
      graph_utils::RemoveNodeOutputEdges(graph_, *transpose_from_nhwc_node_);
      removed_nodes_.push_front(transpose_from_nhwc_node_->Index());
      transpose_from_nhwc_node_ = nullptr;
    }

  } else {
    input_defs[0] = it->second;
  }
}

void NchwcTransformerImpl::ConvPoolShapeInference(const Node& node,
                                                  const NchwcArgument::Shape& input_shape,
                                                  NchwcArgument::Shape& output_shape,
                                                  const ONNX_NAMESPACE::TensorProto* filter_shape) {
  // Skip the leading batch and channel counts.
  constexpr int kernel_size = kNchwcSpatialDims;

  // Maintain the batch count dimension from the NCHWc input.
  output_shape.dims_[0] = input_shape.dims_[0];

  const auto* pads_attr = graph_utils::GetNodeAttribute(node, "pads");
  const auto* strides_attr = graph_utils::GetNodeAttribute(node, "strides");
  const auto* dilations_attr = graph_utils::GetNodeAttribute(node, "dilations");

  if ((pads_attr != nullptr && pads_attr->ints_size() != kernel_size * 2) ||
      (strides_attr != nullptr && strides_attr->ints_size() != kernel_size) ||
      (dilations_attr != nullptr && dilations_attr->ints_size() != kernel_size)) {
    return;
  }

  // Require the kernel_shape attribute for pooling operators. Convolution
  // uses the weight tensor shape to derive the kernel shape.
  const ONNX_NAMESPACE::AttributeProto* kernel_shape_attr = nullptr;
  if (filter_shape == nullptr) {
    kernel_shape_attr = graph_utils::GetNodeAttribute(node, "kernel_shape");
    if (kernel_shape_attr == nullptr || kernel_shape_attr->ints_size() != kernel_size) {
      return;
    }
  }

  const auto* auto_pad_attr = graph_utils::GetNodeAttribute(node, "auto_pad");
  bool auto_pad_same_shape = false;
  if (auto_pad_attr != nullptr && utils::HasString(*auto_pad_attr)) {
    auto& auto_pad = auto_pad_attr->s();
    if (auto_pad != "NOTSET") {
      if (auto_pad == "SAME_UPPER" || auto_pad == "SAME_LOWER") {
        auto_pad_same_shape = true;
      } else if (auto_pad != "VALID") {
        return;
      }
      pads_attr = nullptr;
    }
  }

  for (int i = 0; i < kernel_size; i++) {
    if (dilations_attr != nullptr && dilations_attr->ints(i) != 1) {
      continue;
    }

    int64_t stride = 1;
    if (strides_attr != nullptr) {
      stride = strides_attr->ints(i);
      if (stride != 1 && stride != 2) {
        continue;
      }
    }

    int64_t padding = 0;
    if (pads_attr != nullptr) {
      padding = pads_attr->ints(i) + pads_attr->ints(i + kernel_size);
    }

    int64_t kernel;
    if (kernel_shape_attr != nullptr) {
      kernel = kernel_shape_attr->ints(i);
    } else {
      kernel = filter_shape->dims(2 + i);
    }

    // Maintain the spatial dimension from the NCHWc input if the implicit or
    // explicit padding results in the same symbolic dimension before applying
    // the stride. When the stride is 2, then the actual output dimensions is
    // half the original value. Track the number of times the symbolic dimension
    // has been halved in the shifts field.
    if (padding + 1 == kernel || auto_pad_same_shape) {
      output_shape.dims_[kNchwcBatchChannelDims + i] = input_shape.dims_[kNchwcBatchChannelDims + i];
      output_shape.shifts_[i] = input_shape.shifts_[i] + static_cast<size_t>(stride) - 1;
    }
  }
}

void NchwcTransformerImpl::TransformConv(Node& node) {
  auto& input_defs = node.MutableInputDefs();
  auto& output_defs = node.MutableOutputDefs();

  // Require that the weights tensor be static.
  const ONNX_NAMESPACE::TensorProto* conv_W_tensor_proto = nullptr;
  if (!graph_utils::NodeArgIsConstant(graph_, *input_defs[1]) ||
      !graph_.GetInitializedTensor(input_defs[1]->Name(), conv_W_tensor_proto) ||
      (conv_W_tensor_proto->data_type() != ONNX_NAMESPACE::TensorProto_DataType_FLOAT) ||
      (conv_W_tensor_proto->dims_size() != 4)) {
    return;
  }

  const int64_t output_channels = conv_W_tensor_proto->dims(0);
  const int64_t input_channels = conv_W_tensor_proto->dims(1);

  int64_t group_count;
  const auto* group_attr = graph_utils::GetNodeAttribute(node, "group");
  if (group_attr != nullptr && utils::HasInt(*group_attr)) {
    group_count = group_attr->i();
  } else {
    group_count = 1;
  }

  const size_t nchwc_block_size = MlasNchwcGetBlockSize();
  const int64_t nchwc_output_channels = (output_channels + nchwc_block_size - 1) & ~(nchwc_block_size - 1);

  bool do_reorder_input = true;
  bool reorder_filter_OIHWBo = false;
  int64_t filter_input_channels = input_channels;
  int64_t nchwc_group_count = group_count;

  // The current implementation of ReorderInput requires the channel count to be
  // aligned to this value.
  constexpr int64_t channel_alignment = 4;

  if (group_count > 1) {
    if ((output_channels % channel_alignment) != 0) {
      return;
    }
    if (input_channels == 1 && output_channels == group_count) {
      // Depthwise convolution.
      reorder_filter_OIHWBo = true;
      nchwc_group_count = nchwc_output_channels;
    } else if (((input_channels % nchwc_block_size) != 0) ||
               ((output_channels % group_count) != 0) ||
               (((output_channels / group_count) % nchwc_block_size) != 0)) {
      return;
    }
  } else {
    if (static_cast<size_t>(input_channels) < nchwc_block_size) {
      // Use NCHW input buffer directly.
      reorder_filter_OIHWBo = true;
      do_reorder_input = false;
    } else {
      if ((input_channels % channel_alignment) != 0) {
        return;
      }
      filter_input_channels = (input_channels + nchwc_block_size - 1) & ~(nchwc_block_size - 1);
    }
  }

  // Also require that the optional bias tensor be static.
  const ONNX_NAMESPACE::TensorProto* conv_B_tensor_proto = nullptr;
  if (input_defs.size() >= 3) {
    if (!graph_utils::NodeArgIsConstant(graph_, *input_defs[2]) ||
        !graph_.GetInitializedTensor(input_defs[2]->Name(), conv_B_tensor_proto) ||
        (conv_B_tensor_proto->data_type() != ONNX_NAMESPACE::TensorProto_DataType_FLOAT) ||
        (conv_B_tensor_proto->dims_size() != 1) ||
        (conv_B_tensor_proto->dims(0) != output_channels)) {
      return;
    }
  }

  // Check if the filter has already been converted to the target format.
  InlinedHashMap<NodeArg*, NodeArg*>* filters_map;
  if (reorder_filter_OIHWBo) {
    filters_map = &filters_OIHWBo_;
  } else {
    filters_map = &filters_OIHWBiBo_;
  }

  NodeArg* nchwc_conv_W_arg;
  auto filters_it = filters_map->find(input_defs[1]);
  if (filters_it != filters_map->end()) {
    // Reuse the existing NodeArg.
    nchwc_conv_W_arg = filters_it->second;
  } else {
    Initializer conv_W{*conv_W_tensor_proto, graph_.ModelPath()};
    const auto conv_W_dims = conv_W.dims();

    int64_t reordered_filter_size = nchwc_output_channels * filter_input_channels;
    for (size_t i = 2; i < 4; i++) {
      reordered_filter_size *= conv_W_dims[i];
    }
    InlinedVector<float> reordered_filter(gsl::narrow<size_t>(reordered_filter_size));

    // Reorder the weights tensor statically.
    if (reorder_filter_OIHWBo) {
      MlasReorderFilterOIHWBo(conv_W_dims.data(), conv_W.data<float>(), reordered_filter.data());
    } else {
      MlasReorderFilterOIHWBiBo(conv_W_dims.data(), conv_W.data<float>(), reordered_filter.data());
    }

    ONNX_NAMESPACE::TensorProto nchwc_conv_W_tensor_proto;

    nchwc_conv_W_tensor_proto.set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT);
    nchwc_conv_W_tensor_proto.set_name(graph_.GenerateNodeArgName("reorder"));
    utils::SetRawDataInTensorProto(nchwc_conv_W_tensor_proto, reordered_filter.data(),
                                   reordered_filter.size() * sizeof(float));

    nchwc_conv_W_tensor_proto.add_dims(nchwc_output_channels);
    nchwc_conv_W_tensor_proto.add_dims(filter_input_channels);
    for (size_t i = 2; i < 4; i++) {
      nchwc_conv_W_tensor_proto.add_dims(conv_W_dims[i]);
    }

    nchwc_conv_W_arg = &graph_utils::AddInitializer(graph_, nchwc_conv_W_tensor_proto);
    filters_map->emplace(input_defs[1], nchwc_conv_W_arg);
  }

  // Align the optional bias tensor up to the number of NCHWc output channels.
  NodeArg* nchwc_conv_B_arg = nullptr;
  if ((conv_B_tensor_proto != nullptr) && (output_channels != nchwc_output_channels)) {
    auto biases_it = aligned_biases_.find(input_defs[2]);
    if (biases_it != aligned_biases_.end()) {
      // Reuse the existing NodeArg.
      nchwc_conv_B_arg = biases_it->second;
    } else {
      Initializer conv_B{*conv_B_tensor_proto, graph_.ModelPath()};

      InlinedVector<float> aligned_bias(gsl::narrow<size_t>(nchwc_output_channels));
      ORT_ENFORCE(output_channels <= nchwc_output_channels, "Buffer overflow");
      std::copy_n(conv_B.data<float>(), output_channels, aligned_bias.data());

      ONNX_NAMESPACE::TensorProto nchwc_conv_B_tensor_proto;

      nchwc_conv_B_tensor_proto.set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT);
      nchwc_conv_B_tensor_proto.set_name(graph_.GenerateNodeArgName("reorder"));
      utils::SetRawDataInTensorProto(nchwc_conv_B_tensor_proto, aligned_bias.data(),
                                     gsl::narrow<size_t>(nchwc_output_channels) * sizeof(float));

      nchwc_conv_B_tensor_proto.add_dims(nchwc_output_channels);

      nchwc_conv_B_arg = &graph_utils::AddInitializer(graph_, nchwc_conv_B_tensor_proto);
      aligned_biases_.emplace(input_defs[2], nchwc_conv_B_arg);
    }
  }

  // Create the replacement node.
  std::string nchwc_node_name = graph_.GenerateNodeName(output_defs[0]->Name() + "_nchwc");
  Node& nchwc_node = graph_.AddNode(nchwc_node_name,
                                    "Conv",
                                    nchwc_node_name,
                                    input_defs,
                                    output_defs,
                                    &node.GetAttributes(),
                                    kMSNchwcDomain);
  nchwc_node.SetExecutionProviderType(kCpuExecutionProvider);
  if (nchwc_group_count != group_count) {
    nchwc_node.AddAttribute("group", nchwc_group_count);
  }

  nchwc_node.MutableInputDefs()[1] = nchwc_conv_W_arg;

  if (nchwc_conv_B_arg != nullptr) {
    nchwc_node.MutableInputDefs()[2] = nchwc_conv_B_arg;
  }

  NchwcArgument::Shape output_shape(output_defs[0]);

  if (do_reorder_input) {
    auto* nchwc_input = LookupNchwcArgument(input_defs[0]);
    if (nchwc_input == nullptr) {
      InsertReorderInput(nchwc_node);
    } else {
      nchwc_node.MutableInputDefs()[0] = nchwc_input->nchwc_arg_;
      nchwc_input->remaining_original_uses_--;
      ConvPoolShapeInference(node, nchwc_input->shape_, output_shape, conv_W_tensor_proto);
    }
  }

  CreateNchwcArgument(node, nchwc_node, output_channels, output_shape);
  removed_nodes_.push_front(node.Index());
}

void NchwcTransformerImpl::TransformPool(Node& node) {
  auto& input_defs = node.MutableInputDefs();
  auto& output_defs = node.MutableOutputDefs();

  // Bail out if MaxPool has the optional index tensor specified.
  if (output_defs.size() > 1) {
    return;
  }

  const size_t nchwc_block_size = MlasNchwcGetBlockSize();

  const auto* input_type = input_defs[0]->TypeAsProto();
  if ((input_type == nullptr) || (input_type->tensor_type().elem_type() != TensorProto_DataType_FLOAT)) {
    return;
  }
  const auto* input_shape = input_defs[0]->Shape();
  if ((input_shape == nullptr) || (input_shape->dim_size() != 4)) {
    return;
  }
  auto& channels_dim = input_shape->dim(1);
  if (!utils::HasDimValue(channels_dim)) {
    return;
  }
  const int64_t channels = channels_dim.dim_value();
  if ((channels % nchwc_block_size) != 0) {
    return;
  }

  // Create the replacement node.
  std::string nchwc_node_name = graph_.GenerateNodeName(output_defs[0]->Name() + "_nchwc");
  Node& nchwc_node = graph_.AddNode(nchwc_node_name,
                                    node.OpType(),
                                    nchwc_node_name,
                                    input_defs,
                                    output_defs,
                                    &node.GetAttributes(),
                                    kMSNchwcDomain);
  nchwc_node.SetExecutionProviderType(kCpuExecutionProvider);

  NchwcArgument::Shape output_shape(output_defs[0]);

  auto* nchwc_input = LookupNchwcArgument(input_defs[0]);
  if (nchwc_input == nullptr) {
    InsertReorderInput(nchwc_node);
  } else {
    nchwc_node.MutableInputDefs()[0] = nchwc_input->nchwc_arg_;
    nchwc_input->remaining_original_uses_--;
    ConvPoolShapeInference(node, nchwc_input->shape_, output_shape, nullptr);
  }

  CreateNchwcArgument(node, nchwc_node, channels, output_shape);
  removed_nodes_.push_front(node.Index());
}

Node& NchwcTransformerImpl::InsertReshape(NodeArg* input_arg,
                                          NodeArg* output_arg,
                                          bool split_channels) {
  auto& shape_arg = split_channels ? reshape_split_arg_ : reshape_unsplit_arg_;
  if (shape_arg == nullptr) {
    ONNX_NAMESPACE::TensorProto shape_tensor_proto;
    shape_tensor_proto.set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT64);
    shape_tensor_proto.set_name(graph_.GenerateNodeArgName("Reshape"));
    // Passthrough the batch and spatial dimensions. Compute the channel count based
    // on the remaining tensor size and whether this is a split or unsplit.
    shape_tensor_proto.add_int64_data(0);
    shape_tensor_proto.add_int64_data(-1);
    for (int i = 0; i < kNchwcSpatialDims; i++) {
      shape_tensor_proto.add_int64_data(0);
    }
    if (split_channels) {
      shape_tensor_proto.add_int64_data(static_cast<int64_t>(MlasNchwcGetBlockSize()));
    }
    shape_tensor_proto.add_dims(split_channels ? kNchwcDims + 1 : kNchwcDims);

    shape_arg = &graph_utils::AddInitializer(graph_, shape_tensor_proto);
  }

  Node& reshape_node = graph_.AddNode(graph_.GenerateNodeName("Reshape"),
                                      "Reshape",
                                      "Reshape",
                                      {input_arg, shape_arg},
                                      {output_arg});
  reshape_node.SetExecutionProviderType(kCpuExecutionProvider);

  return reshape_node;
}

void NchwcTransformerImpl::TransformBinary(Node& node, bool add_node) {
  auto& input_defs = node.MutableInputDefs();
  auto& output_defs = node.MutableOutputDefs();

  // Verify that all of the inputs to this operator are from NCHWc outputs.
  InlinedVector<NchwcArgument*> nchwc_inputs;
  size_t input_defs_count = input_defs.size();
  nchwc_inputs.reserve(input_defs_count);
  for (size_t i = 0; i < input_defs_count; i++) {
    auto* nchwc_input = LookupNchwcArgument(input_defs[i]);
    if (nchwc_input == nullptr) {
      return;
    }
    nchwc_inputs.push_back(nchwc_input);
  }

  auto* nchwc_input_0 = nchwc_inputs[0];
  const int64_t channels = nchwc_inputs[0]->channels_;

  // Test if all of the NCHWc inputs have an equal shape.
  bool all_shapes_match = true;
  auto* input_0_shape = input_defs[0]->Shape();
  for (size_t n = 1; n < input_defs_count; n++) {
    auto* nchwc_input_n = nchwc_inputs[n];
    // Require that all inputs have the same logical number of channels.
    if (nchwc_input_n->channels_ != channels) {
      return;
    }
    for (int i = 0; i < kNchwcDims; i++) {
      // Test if this dimension is derived from the same NodeArg.
      if (!nchwc_input_0->shape_.IsDimEqual(nchwc_input_n->shape_, i)) {
        // Check if ONNX shape inferencing has computed a precise dimension value.
        auto* input_n_shape = input_defs[n]->Shape();
        if ((input_0_shape == nullptr) || (input_n_shape == nullptr)) {
          all_shapes_match = false;
        } else {
          auto& input_0_dim = input_0_shape->dim(i);
          auto& input_n_dim = input_n_shape->dim(i);
          if (!utils::HasDimValue(input_0_dim) ||
              !utils::HasDimValue(input_n_dim) ||
              (input_0_dim.dim_value() <= 0) ||
              (input_0_dim.dim_value() != input_n_dim.dim_value())) {
            if (!utils::HasDimParam(input_0_dim) ||
                !utils::HasDimParam(input_n_dim) ||
                (input_0_dim.dim_param() != input_n_dim.dim_param())) {
              all_shapes_match = false;
              break;
            }
          }
        }
      }
    }
  }

  if (all_shapes_match) {
    // Update the node to directly use the NCHWc inputs directly and decrement
    // the original use counts of the NCHWc inputs.
    for (size_t n = 0; n < input_defs_count; n++) {
      input_defs[n] = nchwc_inputs[n]->nchwc_arg_;
      nchwc_inputs[n]->remaining_original_uses_--;
    }

    // If one of the inputs to the Add/Sum node is a NCHWc convolution, then
    // attempt to fuse the addition into the convolution itself.
    if (add_node && input_defs_count == 2) {
      for (size_t n = 0; n < 2; n++) {
        auto* nchwc_input_n = nchwc_inputs[n];
        auto& nchwc_node = nchwc_input_n->output_node_;
        auto& nchwc_input_defs = nchwc_node.MutableInputDefs();
        auto& nchwc_input_args_count = nchwc_node.MutableInputArgsCount();
        size_t nchwc_input_defs_count = nchwc_input_defs.size();
        // Check if this is a single use NCHWc convolution that hasn't already
        // been fused with another Add/Sum node. The Add/Sum can also only be
        // fused if the convolution isn't itself fused with an activation.
        if ((nchwc_node.OpType() == "Conv") && (nchwc_node.Domain() == kMSNchwcDomain) &&
            (nchwc_input_defs_count < 4) && (nchwc_input_args_count.size() < 4) &&
            (nchwc_input_n->starting_original_uses_ == 1) &&
            (graph_utils::GetNodeAttribute(nchwc_node, "activation") == nullptr)) {
          // Feed the output of the other NCHWc node into the selected convolution
          // node.
          nchwc_input_defs.resize(4);
          nchwc_input_args_count.resize(4);
          if (nchwc_input_defs_count < 3) {
            // The optional bias parameter is empty so set to an empty string.
            nchwc_input_defs[2] = &graph_.GetOrCreateNodeArg("", nullptr);
            nchwc_input_args_count[2] = 1;
          }
          nchwc_input_defs[3] = nchwc_inputs[n ^ 1]->output_node_.MutableOutputDefs()[0];
          nchwc_input_args_count[3] = 1;

          FuseNchwcArgument(node, *nchwc_input_n);
          removed_nodes_.push_front(node.Index());
          return;
        }
      }
    }

    CreateNchwcArgument(node, node, nchwc_input_0->channels_, nchwc_input_0->shape_);
    return;
  }

  if (add_node) {
    // The input shapes cannot be shown to be identical, but the channel dimension
    // is the same. Reshape the tensors to explicitly use the true NCHWc shape in
    // order to perform the binary operation.
    //
    // Typically, both tensors are of the same shape at inferencing time, but this
    // could not be proven using symbolic dimensions. This reshaping avoids the
    // alternative of reordering the tensors back to NCHW.
    //
    // This optimization is restricted to Add/Sum nodes. Mul nodes would also work
    // using this code, however the common case here is multiplying a NxCxHxW
    // matrix by a NxCx1x1 vector. The implementation of Mul does not currently
    // vectorize well for the case of broadcasting a NCHWc sized channel block.
    // This case should be handled by a ScaleShift kernel that can broadcast a
    // multiply/add vector (BatchNormalization can also be reimplemented with this).
    for (size_t n = 0; n < input_defs_count; n++) {
      std::string reshape_input_def_name = graph_.GenerateNodeArgName("reshape");
      auto* reshape_input_arg = &graph_.GetOrCreateNodeArg(reshape_input_def_name, nullptr);
      InsertReshape(nchwc_inputs[n]->nchwc_arg_, reshape_input_arg, true);

      input_defs[n] = reshape_input_arg;
      nchwc_inputs[n]->remaining_original_uses_--;
    }

    std::string output_reshaped_def_name = graph_.GenerateNodeArgName("reshape");
    auto* output_reshaped_arg = &graph_.GetOrCreateNodeArg(output_reshaped_def_name, nullptr);
    Node& nchwc_node = InsertReshape(output_reshaped_arg, output_defs[0], false);

    NchwcArgument::Shape output_shape(output_defs[0]);

    CreateNchwcArgument(node, nchwc_node, channels, output_shape);
    output_defs[0] = output_reshaped_arg;
    return;
  }
}

void NchwcTransformerImpl::TransformConcat(Node& node) {
  auto& input_defs = node.MutableInputDefs();
  auto& output_defs = node.MutableOutputDefs();

  // Verify that this is a concatenation along the channel axis.
  const auto* axis_attr = graph_utils::GetNodeAttribute(node, "axis");
  if (axis_attr == nullptr || !utils::HasInt(*axis_attr) || axis_attr->i() != 1) {
    return;
  }

  const size_t nchwc_block_size = MlasNchwcGetBlockSize();

  // Verify that all of the inputs to this operator are from NCHWc outputs.
  InlinedVector<NchwcArgument*> nchwc_inputs;
  size_t input_defs_count = input_defs.size();
  nchwc_inputs.reserve(input_defs_count);
  int64_t total_channels = 0;
  for (size_t i = 0; i < input_defs_count; i++) {
    auto* nchwc_input = LookupNchwcArgument(input_defs[i]);
    if (nchwc_input == nullptr) {
      return;
    }
    // Verify that the logical number of channels is block aligned.
    int64_t input_channels = nchwc_input->channels_;
    if ((input_channels % nchwc_block_size) != 0) {
      return;
    }
    total_channels += input_channels;
    nchwc_inputs.push_back(nchwc_input);
  }

  // Update the node to directly use the NCHWc inputs directly and decrement
  // the original use counts of the NCHWc inputs.
  for (size_t n = 0; n < input_defs_count; n++) {
    input_defs[n] = nchwc_inputs[n]->nchwc_arg_;
    nchwc_inputs[n]->remaining_original_uses_--;
  }

  // Copy the shape from any of the NCHWc inputs, but use the current node for
  // the channel dimension.
  NchwcArgument::Shape output_shape = nchwc_inputs[0]->shape_;
  output_shape.dims_[1] = output_defs[0];

  CreateNchwcArgument(node, node, total_channels, output_shape);
}

// After doing a Conv/Add fusion, there may be an activation node that could now
// be fused into the Conv node as well. Otherwise, this is an elementwise
// operation that can directly use the NCHWc input.
void NchwcTransformerImpl::TransformActivation(Node& node) {
  auto& input_defs = node.MutableInputDefs();

  auto* nchwc_input = LookupNchwcArgument(input_defs[0]);
  if (nchwc_input != nullptr) {
    input_defs[0] = nchwc_input->nchwc_arg_;
    nchwc_input->remaining_original_uses_--;

    // Check if this is a single use NCHWc convolution that hasn't already
    // been fused with another activation.
    auto& nchwc_node = nchwc_input->output_node_;
    if ((nchwc_node.OpType() == "Conv") && (nchwc_node.Domain() == kMSNchwcDomain) &&
        (nchwc_input->starting_original_uses_ == 1) &&
        (graph_utils::GetNodeAttribute(nchwc_node, "activation") == nullptr)) {
      nchwc_node.AddAttribute("activation", node.OpType());
      FuseNchwcArgument(node, *nchwc_input);
      removed_nodes_.push_front(node.Index());
    } else {
      CreateNchwcArgument(node, node, nchwc_input->channels_, nchwc_input->shape_);
    }
  }
}

// Transform BatchNormalization to a depthwise separable 1x1 convolution. This
// enables reuse of the existing NCHWc convolution operator and other fusions
// such as BatchNormalization+Relu using Conv+Relu.
void NchwcTransformerImpl::TransformBatchNormalization(Node& node) {
  auto& input_defs = node.MutableInputDefs();
  auto& output_defs = node.MutableOutputDefs();

  // Bail out if the node has the optional training outputs specified.
  if (output_defs.size() > 1) {
    return;
  }

  // Don't transform the node if the input is not already in NCHWc format.
  auto* nchwc_input = LookupNchwcArgument(input_defs[0]);
  if (nchwc_input == nullptr) {
    return;
  }

  // Require that BatchNormalization-7 uses spatial normalization.
  const auto* spatial_attr = graph_utils::GetNodeAttribute(node, "spatial");
  if (spatial_attr != nullptr && utils::HasInt(*spatial_attr) && spatial_attr->i() != 1) {
    return;
  }

  const auto* epsilon_attr = graph_utils::GetNodeAttribute(node, "epsilon");
  if (epsilon_attr == nullptr || !utils::HasFloat(*epsilon_attr)) {
    return;
  }
  float epsilon = static_cast<float>(epsilon_attr->f());

  const int64_t channels = nchwc_input->channels_;

  auto get_bn_tensor_proto = [this, channels](const std::string& input_name) {
    const auto* tensor_proto = graph_utils::GetConstantInitializer(graph_, input_name);
    if (tensor_proto != nullptr) {
      if ((tensor_proto->data_type() != ONNX_NAMESPACE::TensorProto_DataType_FLOAT) ||
          (tensor_proto->dims_size() != 1) ||
          (tensor_proto->dims(0) != channels)) {
        tensor_proto = nullptr;
      }
    }
    return tensor_proto;
  };

  const auto* bn_scale_tensor_proto = get_bn_tensor_proto(input_defs[1]->Name());
  if (bn_scale_tensor_proto == nullptr) {
    return;
  }
  const auto* bn_B_tensor_proto = get_bn_tensor_proto(input_defs[2]->Name());
  if (bn_B_tensor_proto == nullptr) {
    return;
  }
  const auto* bn_mean_tensor_proto = get_bn_tensor_proto(input_defs[3]->Name());
  if (bn_mean_tensor_proto == nullptr) {
    return;
  }
  const auto* bn_var_tensor_proto = get_bn_tensor_proto(input_defs[4]->Name());
  if (bn_var_tensor_proto == nullptr) {
    return;
  }

  Initializer bn_scale{*bn_scale_tensor_proto, graph_.ModelPath()};
  Initializer bn_B{*bn_B_tensor_proto, graph_.ModelPath()};
  Initializer bn_mean{*bn_mean_tensor_proto, graph_.ModelPath()};
  Initializer bn_var{*bn_var_tensor_proto, graph_.ModelPath()};

  // Calculate the scale and bias for the replacement convolution.
  bn_var.add(epsilon);
  bn_var.sqrt();
  bn_scale.div(bn_var);
  bn_mean.mul(bn_scale);
  bn_B.sub(bn_mean);

  const size_t nchwc_block_size = MlasNchwcGetBlockSize();
  const int64_t nchwc_channels = (channels + nchwc_block_size - 1) & ~(nchwc_block_size - 1);

  InlinedVector<float> padded_buffer(gsl::narrow<size_t>(nchwc_channels));

  std::copy_n(bn_scale.data<float>(), channels, padded_buffer.data());

  ONNX_NAMESPACE::TensorProto nchwc_conv_W_tensor_proto;
  nchwc_conv_W_tensor_proto.set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT);
  nchwc_conv_W_tensor_proto.set_name(graph_.GenerateNodeArgName("bn_scale"));
  utils::SetRawDataInTensorProto(nchwc_conv_W_tensor_proto, padded_buffer.data(),
                                 gsl::narrow<size_t>(nchwc_channels) * sizeof(float));
  nchwc_conv_W_tensor_proto.add_dims(nchwc_channels);
  nchwc_conv_W_tensor_proto.add_dims(1);
  nchwc_conv_W_tensor_proto.add_dims(1);
  nchwc_conv_W_tensor_proto.add_dims(1);

  auto* nchwc_conv_W_arg = &graph_utils::AddInitializer(graph_, nchwc_conv_W_tensor_proto);

  std::copy_n(bn_B.data<float>(), channels, padded_buffer.data());

  ONNX_NAMESPACE::TensorProto nchwc_conv_B_tensor_proto;
  nchwc_conv_B_tensor_proto.set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT);
  nchwc_conv_B_tensor_proto.set_name(graph_.GenerateNodeArgName("bn_B"));
  utils::SetRawDataInTensorProto(nchwc_conv_B_tensor_proto, padded_buffer.data(),
                                 gsl::narrow<size_t>(nchwc_channels) * sizeof(float));
  nchwc_conv_B_tensor_proto.add_dims(nchwc_channels);

  auto* nchwc_conv_B_arg = &graph_utils::AddInitializer(graph_, nchwc_conv_B_tensor_proto);

  // Create the replacement node.
  std::string nchwc_node_name = graph_.GenerateNodeName(output_defs[0]->Name() + "_bn_nchwc");
  Node& nchwc_node = graph_.AddNode(nchwc_node_name,
                                    "Conv",
                                    nchwc_node_name,
                                    std::array{nchwc_input->nchwc_arg_, nchwc_conv_W_arg, nchwc_conv_B_arg},
                                    output_defs,
                                    nullptr,
                                    kMSNchwcDomain);
  nchwc_node.SetExecutionProviderType(kCpuExecutionProvider);
  nchwc_node.AddAttribute("group", nchwc_channels);

  nchwc_input->remaining_original_uses_--;

  CreateNchwcArgument(node, nchwc_node, channels, nchwc_input->shape_);
  removed_nodes_.push_front(node.Index());
}

void NchwcTransformerImpl::TransformTransposeToNhwc(Node& node) {
  auto& input_defs = node.MutableInputDefs();
  auto& output_defs = node.MutableOutputDefs();

  // Don't transform the node if the input is not already in NCHWc format.
  auto* nchwc_input = LookupNchwcArgument(input_defs[0]);
  if (nchwc_input == nullptr) {
    return;
  }

  const auto* perm_attr = graph_utils::GetNodeAttribute(node, "perm");
  if (perm_attr == nullptr || perm_attr->ints_size() != 4) {
    return;
  }

  // Test if this transposes from NCHW to NHWC layout order.
  const int64_t* perm_data = perm_attr->ints().data();
  if (perm_data[0] != 0 || perm_data[1] != 2 || perm_data[2] != 3 || perm_data[3] != 1) {
    return;
  }

  // Create the replacement node.
  Node& reorder_output_node = graph_.AddNode(graph_.GenerateNodeName("ReorderOutput"),
                                             "ReorderOutput",
                                             "ReorderOutput",
                                             std::array<NodeArg*, 1>{nchwc_input->nchwc_arg_},
                                             output_defs,
                                             nullptr,
                                             kMSNchwcDomain);
  reorder_output_node.SetExecutionProviderType(kCpuExecutionProvider);
  reorder_output_node.AddAttribute("channels", nchwc_input->channels_);
  reorder_output_node.AddAttribute("channels_last", static_cast<int64_t>(1));

  nchwc_input->remaining_original_uses_--;

  graph_utils::RemoveNodeOutputEdges(graph_, node);

  removed_nodes_.push_front(node.Index());
}

void NchwcTransformerImpl::TransformResize(Node& node) {
  auto& input_defs = node.MutableInputDefs();
  auto& output_defs = node.MutableOutputDefs();

  // Don't transform the node if the input is not already in NCHWc format.
  auto* nchwc_input = LookupNchwcArgument(input_defs[0]);
  if (nchwc_input == nullptr) {
    return;
  }

  // Support nearest (default) and linear modes.
  const auto* mode_attr = graph_utils::GetNodeAttribute(node, "mode");
  bool nearest_mode = true;
  if (mode_attr != nullptr && utils::HasString(*mode_attr)) {
    if (mode_attr->s() != "nearest") {
      if (mode_attr->s() == "linear") {
        nearest_mode = false;
      } else {
        return;
      }
    }
  }

  const ONNX_NAMESPACE::AttributeProto* transformation_mode_attr = nullptr;

  NodeArg* sizes_arg = nullptr;
  NodeArg* scales_arg = nullptr;

  if (node.SinceVersion() >= 11) {
    if (input_defs.size() >= 4) {
      sizes_arg = input_defs[3];
    }
    if (sizes_arg == nullptr) {
      scales_arg = input_defs[2];
    }

    transformation_mode_attr = graph_utils::GetNodeAttribute(node, "coordinate_transformation_mode");
    if ((transformation_mode_attr == nullptr) ||
        !utils::HasString(*transformation_mode_attr)) {
      return;
    }
    if (transformation_mode_attr->s() != "asymmetric") {
      // Nearest mode kernel support asymmetric transformation mode only.
      if (nearest_mode) {
        return;
      }
      if ((transformation_mode_attr->s() != "align_corners") &&
          (transformation_mode_attr->s() != "half_pixel")) {
        return;
      }
    }

    if (nearest_mode) {
      // Only support the floor rounding mode.
      const auto* nearest_mode_attr = graph_utils::GetNodeAttribute(node, "nearest_mode");
      if ((nearest_mode_attr == nullptr) ||
          !utils::HasString(*nearest_mode_attr) ||
          (nearest_mode_attr->s() != "floor")) {
        return;
      }
    }
  } else {
    scales_arg = input_defs[1];
  }

  InlinedVector<int64_t> scales_attr(4);

  if (sizes_arg != nullptr) {
    // Require that the sizes tensor be static.
    const auto* sizes_tensor_proto = graph_utils::GetConstantInitializer(graph_, sizes_arg->Name());
    if ((sizes_tensor_proto == nullptr) ||
        (sizes_tensor_proto->data_type() != ONNX_NAMESPACE::TensorProto_DataType_INT64) ||
        (sizes_tensor_proto->dims_size() != 1) ||
        (sizes_tensor_proto->dims(0) != 4)) {
      return;
    }

    const auto* input_shape = input_defs[0]->Shape();
    if (input_shape == nullptr) {
      return;
    }

    Initializer sizes{*sizes_tensor_proto, graph_.ModelPath()};
    auto* sizes_data = sizes.data<int64_t>();

    // The sizes data can only be used if the input shape is static and the
    // effective scaling must be an integer.
    for (int i = 0; i < 4; i++) {
      const auto& dim = input_shape->dim(i);
      if (!utils::HasDimValue(dim)) {
        return;
      }
      int64_t dim_value = dim.dim_value();
      if (dim_value <= 0) {
        return;
      }
      scales_attr[i] = sizes_data[i] / dim_value;
      if (scales_attr[i] * dim_value != sizes_data[i]) {
        return;
      }
    }

  } else if (scales_arg != nullptr) {
    // Require that the scales tensor be static.
    const auto* scales_tensor_proto = graph_utils::GetConstantInitializer(graph_, scales_arg->Name());
    if ((scales_tensor_proto == nullptr) ||
        (scales_tensor_proto->data_type() != ONNX_NAMESPACE::TensorProto_DataType_FLOAT) ||
        (scales_tensor_proto->dims_size() != 1) ||
        (scales_tensor_proto->dims(0) != 4)) {
      return;
    }

    Initializer scales{*scales_tensor_proto, graph_.ModelPath()};
    auto* scales_data = scales.data<float>();

    // Cast the scales to integers and verify that the scales are positive and
    // round trip back to floating point.
    for (size_t n = 0; n < 4; n++) {
      int64_t scale_value = static_cast<int64_t>(scales_data[n]);
      if (scale_value <= 0 || static_cast<float>(scale_value) != scales_data[n]) {
        return;
      }
      scales_attr[n] = scale_value;
    }

  } else {
    return;
  }

  // Only support spatial scaling at this time (batch and channel are unscaled).
  if (scales_attr[0] != 1 || scales_attr[1] != 1) {
    return;
  }

  std::string nchwc_node_name = graph_.GenerateNodeName(output_defs[0]->Name() + "_nchwc");
  Node& nchwc_node = graph_.AddNode(nchwc_node_name,
                                    "Upsample",
                                    nchwc_node_name,
                                    {nchwc_input->nchwc_arg_},
                                    output_defs,
                                    nullptr,
                                    kMSNchwcDomain);
  nchwc_node.SetExecutionProviderType(kCpuExecutionProvider);
  nchwc_node.AddAttribute("scales", scales_attr);
  if (!nearest_mode) {
    nchwc_node.AddAttribute("mode", mode_attr->s());
    if (transformation_mode_attr != nullptr) {
      nchwc_node.AddAttribute("coordinate_transformation_mode", transformation_mode_attr->s());
    }
  }

  nchwc_input->remaining_original_uses_--;

  NchwcArgument::Shape output_shape(output_defs[0]);

  CreateNchwcArgument(node, nchwc_node, nchwc_input->channels_, output_shape);
  removed_nodes_.push_front(node.Index());
}

void NchwcTransformerImpl::TrackTransposeFromNhwc(Node& node) {
  const auto* perm_attr = graph_utils::GetNodeAttribute(node, "perm");
  if (perm_attr == nullptr || perm_attr->ints_size() != 4) {
    return;
  }

  // Test if this transposes from NHWC to NCHW layout order.
  const int64_t* perm_data = perm_attr->ints().data();
  if (perm_data[0] != 0 || perm_data[1] != 3 || perm_data[2] != 1 || perm_data[3] != 2) {
    return;
  }

  // Verify that the node does not produce a graph output and produces output
  // for a single node.
  if (graph_.NodeProducesGraphOutput(node) || node.GetOutputEdgesCount() != 1) {
    return;
  }

  transpose_from_nhwc_node_ = &node;
  transpose_from_nhwc_output_arg_ = node.MutableOutputDefs()[0];
}

void NchwcTransformerImpl::Transform(Node& node) {
  if (graph_utils::IsSupportedOptypeVersionAndDomain(node, "Transpose", {1, 13})) {
    TrackTransposeFromNhwc(node);
  }

  if (graph_utils::IsSupportedOptypeVersionAndDomain(node, "Conv", {1, 11}) ||
      graph_utils::IsSupportedOptypeVersionAndDomain(node, "FusedConv", {1}, kMSDomain)) {
    TransformConv(node);
  } else if (graph_utils::IsSupportedOptypeVersionAndDomain(node, "MaxPool", {1, 8, 10, 11, 12}) ||
             graph_utils::IsSupportedOptypeVersionAndDomain(node, "AveragePool", {1, 7, 10, 11})) {
    TransformPool(node);
  } else if (node.GetInputEdgesCount() == 0 && node.InputDefs().size() != 0) {
    // The following transforms only run when the input edge count has already
    // been decremented to zero by earlier transforms. This is a hint that the
    // node may already have all inputs converted to NCHWc format and is not
    // needed for correct operation. This avoids doing extra string checks for
    // nodes unrelated to this transformer.
    if (graph_utils::IsSupportedOptypeVersionAndDomain(node, "Add", {7, 13, 14}) ||
        graph_utils::IsSupportedOptypeVersionAndDomain(node, "Sum", {6, 8, 13})) {
      TransformBinary(node, true);
    } else if (graph_utils::IsSupportedOptypeVersionAndDomain(node, "Mul", {7, 13, 14})) {
      TransformBinary(node, false);
    } else if (graph_utils::IsSupportedOptypeVersionAndDomain(node, "Concat", {4, 11, 13})) {
      TransformConcat(node);
    } else if (graph_utils::IsSupportedOptypeVersionAndDomain(node, "Relu", {6, 13, 14}) ||
               graph_utils::IsSupportedOptypeVersionAndDomain(node, "Sigmoid", {6, 13}) ||
               graph_utils::IsSupportedOptypeVersionAndDomain(node, "Tanh", {6, 13})) {
      TransformActivation(node);
    } else if (graph_utils::IsSupportedOptypeVersionAndDomain(node, "BatchNormalization", {7, 9, 14})) {
      TransformBatchNormalization(node);
    } else if (graph_utils::IsSupportedOptypeVersionAndDomain(node, "Transpose", {1, 13})) {
      TransformTransposeToNhwc(node);
    } else if (graph_utils::IsSupportedOptypeVersionAndDomain(node, "Upsample", {9, 13}) ||
               graph_utils::IsSupportedOptypeVersionAndDomain(node, "Resize", {10, 11, 13})) {
      TransformResize(node);
    } else if (graph_utils::IsSupportedOptypeVersionAndDomain(node, "GlobalMaxPool", {1}) ||
               graph_utils::IsSupportedOptypeVersionAndDomain(node, "GlobalAveragePool", {1})) {
      // Convert these pooling types only if the input is already in NCHWc format.
      TransformPool(node);
    }
  }

  // The node may not match any of the checks above or may not have been
  // transformed for other reasons such as unsupported attributes or alignment.
  // However, the node may still use an input that has been produced by a NCHWc
  // node. Finalize() walks through the list of NCHWc outputs and inserts the
  // needed reorder operations to ensure that these inputs remain in NCHW
  // format.
}

void NchwcTransformerImpl::Finalize(bool& modified) {
  // Create ReorderOutput nodes for any NCHWc outputs that still have uses with
  // the original tensor format.
  for (auto& nchwc_output : nchwc_args_) {
    if (nchwc_output.second->remaining_original_uses_ > 0) {
      auto* output_original_arg = nchwc_output.first;
      auto* output_nchwc_arg = nchwc_output.second->nchwc_arg_;
      Node& reorder_output_node = graph_.AddNode(graph_.GenerateNodeName("ReorderOutput"),
                                                 "ReorderOutput",
                                                 "ReorderOutput",
                                                 {output_nchwc_arg},
                                                 {output_original_arg},
                                                 nullptr,
                                                 kMSNchwcDomain);
      reorder_output_node.SetExecutionProviderType(kCpuExecutionProvider);
      reorder_output_node.AddAttribute("channels", nchwc_output.second->channels_);
    }
  }

  for (auto index : removed_nodes_) {
    graph_.RemoveNode(index);
  }

  if (!removed_nodes_.empty()) {
    modified = true;
  }
}

Status NchwcTransformer::ApplyImpl(Graph& graph, bool& modified, int graph_level, const logging::Logger& logger) const {
  NchwcTransformerImpl impl(graph);
  GraphViewer graph_viewer(graph);

  for (auto index : graph_viewer.GetNodesInTopologicalOrder()) {
    auto& node = *graph.GetNode(index);
    ORT_RETURN_IF_ERROR(Recurse(node, modified, graph_level, logger));
    if (node.GetExecutionProviderType() == kCpuExecutionProvider) {
      impl.Transform(node);
    }
  }
  impl.Finalize(modified);
  return Status::OK();
}

}  // namespace onnxruntime
