onnxruntime/onnxruntime/core/optimizer/nchwc_transformer.cc
Tracy Sharpe 16297a8e61
Implement NCHWc Upsample linear mode (#7623)
Extend the existing NCHWc Upsample operator to support linear modes too.
2021-05-10 12:16:16 -07:00

1249 lines
50 KiB
C++

// 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, size_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) {
}
};
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, int64_t channels, 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.
std::unordered_map<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.
std::unordered_map<NodeArg*, NodeArg*> reorder_inputs_;
// Stores a mapping of NodeArg filters that have already been reordered, so
// multiple nodes can share the NCHWc filter.
std::unordered_map<NodeArg*, NodeArg*> filters_OIHWBo_;
std::unordered_map<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.
std::unordered_map<NodeArg*, NodeArg*> aligned_biases_;
// Stores a mapping of shape initializers for use by Reshape when splitting
// or unsplitting the channels dimension of a tensor.
std::unordered_map<int64_t, NodeArg*> reshape_split_;
std::unordered_map<int64_t, NodeArg*> reshape_unsplit_;
// 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_.GetNodeOutputsInGraphOutputs(node).empty()) {
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.
const 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.
std::unordered_map<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];
}
std::vector<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"));
nchwc_conv_W_tensor_proto.set_raw_data(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()};
std::vector<float> aligned_bias(gsl::narrow<size_t>(nchwc_output_channels));
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"));
nchwc_conv_B_tensor_proto.set_raw_data(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 it = nchwc_args_.find(input_defs[0]);
if (it == nchwc_args_.end()) {
InsertReorderInput(nchwc_node);
} else {
auto* nchwc_input = it->second.get();
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 it = nchwc_args_.find(input_defs[0]);
if (it == nchwc_args_.end()) {
InsertReorderInput(nchwc_node);
} else {
auto* nchwc_input = it->second.get();
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,
int64_t channels,
bool split_channels) {
const int64_t nchwc_block_size = static_cast<int64_t>(MlasNchwcGetBlockSize());
const int64_t nchwc_channels = (channels + nchwc_block_size - 1) & ~(nchwc_block_size - 1);
// Reuse the shape initializer across reshapes for the same channel configuration.
auto& shape_arg_map = split_channels ? reshape_split_ : reshape_unsplit_;
NodeArg* shape_arg = shape_arg_map[nchwc_channels];
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 dimension.
shape_tensor_proto.add_int64_data(0);
if (split_channels) {
shape_tensor_proto.add_int64_data(nchwc_channels / nchwc_block_size);
} else {
shape_tensor_proto.add_int64_data(nchwc_channels);
}
// Passthrough the spatial dimensions.
for (int i = 0; i < kNchwcSpatialDims; i++) {
shape_tensor_proto.add_int64_data(0);
}
if (split_channels) {
shape_tensor_proto.add_int64_data(nchwc_block_size);
shape_tensor_proto.add_dims(kNchwcDims + 1);
} else {
shape_tensor_proto.add_dims(kNchwcDims);
}
shape_arg = &graph_utils::AddInitializer(graph_, shape_tensor_proto);
shape_arg_map[nchwc_channels] = shape_arg;
}
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.
std::vector<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 it = nchwc_args_.find(input_defs[i]);
if (it == nchwc_args_.end()) {
return;
}
nchwc_inputs.push_back(it->second.get());
}
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, channels, 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], channels, 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.
std::vector<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 it = nchwc_args_.find(input_defs[i]);
if (it == nchwc_args_.end()) {
return;
}
// Verify that the logical number of channels is block aligned.
int64_t input_channels = it->second->channels_;
if ((input_channels % nchwc_block_size) != 0) {
return;
}
total_channels += input_channels;
nchwc_inputs.push_back(it->second.get());
}
// 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 it = nchwc_args_.find(input_defs[0]);
if (it != nchwc_args_.end()) {
auto& nchwc_input = it->second;
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 it = nchwc_args_.find(input_defs[0]);
if (it == nchwc_args_.end()) {
return;
}
auto* nchwc_input = it->second.get();
// 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);
std::vector<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"));
nchwc_conv_W_tensor_proto.set_raw_data(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"));
nchwc_conv_B_tensor_proto.set_raw_data(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,
{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 it = nchwc_args_.find(input_defs[0]);
if (it == nchwc_args_.end()) {
return;
}
auto* nchwc_input = it->second.get();
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",
{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 it = nchwc_args_.find(input_defs[0]);
if (it == nchwc_args_.end()) {
return;
}
auto* nchwc_input = it->second.get();
// 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];
}
std::vector<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.template 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.template 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_.GetNodeOutputsInGraphOutputs(node).empty() || 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}) ||
graph_utils::IsSupportedOptypeVersionAndDomain(node, "Sum", {6, 8, 13})) {
TransformBinary(node, true);
} else if (graph_utils::IsSupportedOptypeVersionAndDomain(node, "Mul", {7, 13})) {
TransformBinary(node, false);
} else if (graph_utils::IsSupportedOptypeVersionAndDomain(node, "Concat", {4, 11, 13})) {
TransformConcat(node);
} else if (graph_utils::IsSupportedOptypeVersionAndDomain(node, "Relu", {6, 13}) ||
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})) {
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