From a82a0907e0afda1b0bd623792699e2a7cd0dc7eb Mon Sep 17 00:00:00 2001 From: linkerzhang Date: Fri, 23 Nov 2018 11:56:14 -0800 Subject: [PATCH 1/3] add ops for quantization support. --- onnxruntime/contrib_ops/contrib_ops.cc | 223 +++++++++++++++++++++++++ 1 file changed, 223 insertions(+) diff --git a/onnxruntime/contrib_ops/contrib_ops.cc b/onnxruntime/contrib_ops/contrib_ops.cc index 49c8a133ea..d698cdcbd3 100644 --- a/onnxruntime/contrib_ops/contrib_ops.cc +++ b/onnxruntime/contrib_ops/contrib_ops.cc @@ -104,6 +104,229 @@ The quantization formula is y = (x / y_scale) + y_zero_point. For (x / y_scale), The linear de-quantization operator. It consumes a quantized data, a scale, a zero point and computes the full precision data. The dequantization formula is y = (x - x_zero_point) * x_scale. Scale and zero point must have same shape. They must be either scalar (per tensor) or 1-D tensor (per 'axis').)DOC"); + + const char* auto_pad_doc = + "auto_pad must be either NOTSET, SAME_UPPER, SAME_LOWER or VALID. Where " + "default value is NOTSET, which means explicit padding is used. " + "SAME_UPPER or SAME_LOWER mean pad the input so that the output size match the input." + "In case of odd number add the extra padding at the end for SAME_UPPER and at the " + "beginning for SAME_LOWER. VALID mean no padding."; + + ONNX_CONTRIB_OPERATOR_SCHEMA(QLinearConv) + .SetDomain(kMSDomain) + .SinceVersion(1) + .SetDoc(R"DOC( +The convolution operator consumes a quantized input tensor, its scale and zero point, +a quantized filter, its scale and zero point, and output’s scale and zero point, +and computes the quantized output. Each scale and zero point pair must have same shape. +It means they must be either scalars (per tensor) or 1-D tensors (per channel).)DOC") + .Input( + 0, + "x", + "Input data tensor from previous layer; " + "has size (N x C x H x W), where N is the batch size, " + "C is the number of channels, and H and W are the " + "height and width. Note that this is for the 2D image. " + "Otherwise the size is (N x C x D1 x D2 ... x Dn). " + "Optionally, if dimension denotation is " + "in effect, the operation expects input data tensor " + "to arrive with the dimension denotation of [DATA_BATCH, " + "DATA_CHANNEL, DATA_FEATURE, DATA_FEATURE ...].", + "T1") + .Input(1, "x_scale", "Scale tensor for input ‘x’. It could be a scalar or a 1-D tensor, which means a per-tensor or per-channel quantization. If it’s a 1-D tensor, its number of elements should be equal to the number of channels of input ‘x’.", "T3") + .Input(2, "x_zero_point", "Zero point tensor for input ‘x’. It could be a scalar or a 1-D tensor, which means a per-tensor or per-channel quantization. If it’s a 1-D tensor, its number of elements should be equal to the number of channels of input ‘x’.", "T1") + .Input( + 3, + "w", + "The weight tensor that will be used in the " + "convolutions; has size (M x C/group x kH x kW), where C " + "is the number of channels, and kH and kW are the " + "height and width of the kernel, and M is the number " + "of feature maps. For more than 2 dimensions, the " + "kernel shape will be (M x C/group x k1 x k2 x ... x kn), " + "where (k1 x k2 x ... kn) is the dimension of the kernel. " + "Optionally, if dimension denotation is in effect, " + "the operation expects the weight tensor to arrive " + "with the dimension denotation of [FILTER_OUT_CHANNEL, " + "FILTER_IN_CHANNEL, FILTER_SPATIAL, FILTER_SPATIAL ...]. " + "X.shape[1] == (W.shape[1] * group) == C " + "(assuming zero based indices for the shape array). " + "Or in other words FILTER_IN_CHANNEL should be equal to DATA_CHANNEL. ", + "T1") + .Input(4, "w_scale", "Scale tensor for input ‘w’. It could be a scalar or a 1-D tensor, which means a per-tensor or per-channel quantization. If it’s a 1-D tensor, its number of elements should be equal to the number of channels of input ‘w’.", "T3") + .Input(5, "w_zero_point", "Scale tensor for input ‘w’. It could be a scalar or a 1-D tensor, which means a per-tensor or per-channel quantization. If it’s a 1-D tensor, its number of elements should be equal to the number of channels of input ‘w’.", "T1") + .Input(6, "y_scale", "Scale tensor for output ‘y’. It could be a scalar or a 1-D tensor, which means a per-tensor or per-channel quantization. If it’s a 1-D tensor, its number of elements should be equal to the number of channels of input ‘y’.", "T3") + .Input(7, "y_zero_point", "Scale tensor for output ‘y’. It could be a scalar or a 1-D tensor, which means a per-tensor or per-channel quantization. If it’s a 1-D tensor, its number of elements should be equal to the number of channels of input ‘y’.", "T1") + .Input(8, "B", "Optional 1D bias to be added to the convolution, has size of M.", "T2", OpSchema::Optional) + .Output( + 0, + "y", + "Output data tensor that contains the result of the " + "convolution. The output dimensions are functions " + "of the kernel size, stride size, and pad lengths.", + "T1") + .TypeConstraint( + "T1", + {"tensor(int8)", "tensor(uint8)"}, + "Constrain input, filter, and output types to 8-bit integer tensors.") + .TypeConstraint("T2", {"tensor(int32)", "tensor(uint32)"}, "Constrain bias type to 32-bit integer tensor.") + .TypeConstraint("T3", {"tensor(float)"}, "Constrain scale of input, filter and output to float tensor.") + .Attr( + "auto_pad", + auto_pad_doc, + AttributeProto::STRING, + std::string("NOTSET")) + .Attr( + "kernel_shape", + "The shape of the convolution kernel. If not present, should be inferred from input 'w'.", + AttributeProto::INTS, + OPTIONAL) + .Attr( + "dilations", + "dilation value along each axis of the filter. If not present, the dilation defaults to 1 along each axis.", + AttributeProto::INTS, + OPTIONAL) + .Attr( + "strides", "Stride along each axis. If not present, the stride defaults to 1 along each axis.", AttributeProto::INTS, OPTIONAL) + .Attr("pads", + "Padding for the beginning and ending along each axis, it can take any value greater than or equal to 0." + "The value represent the number of pixels added to the beginning and end part of the corresponding axis." + "`pads` format should be as follow [x1_begin, x2_begin...x1_end, x2_end,...], where xi_begin the number of" + "pixels added at the beginning of axis `i` and xi_end, the number of pixels added at the end of axis `i`." + "This attribute cannot be used simultaneously with auto_pad attribute. If not present, the padding defaults" + "to 0 along start and end of each axis.", + AttributeProto::INTS, OPTIONAL) + .Attr( + "group", + "number of groups input channels and output channels are divided into. default is 1.", + AttributeProto::INT, + static_cast(1)); + + ONNX_CONTRIB_OPERATOR_SCHEMA(ConvInteger) + .SetDomain(kMSDomain) + .SinceVersion(1) + .SetDoc(R"DOC( +The integer convolution operator consumes an input tensor, a filter, and a padding value, + and computes the output. The production MUST never overflow. The accumulation may overflow + if and only if in 32 bits.)DOC") + .Input( + 0, + "x", + "Input data tensor from previous layer; " + "has size (N x C x H x W), where N is the batch size, " + "C is the number of channels, and H and W are the " + "height and width. Note that this is for the 2D image. " + "Otherwise the size is (N x C x D1 x D2 ... x Dn). " + "Optionally, if dimension denotation is " + "in effect, the operation expects input data tensor " + "to arrive with the dimension denotation of [DATA_BATCH, " + "DATA_CHANNEL, DATA_FEATURE, DATA_FEATURE ...].", + "T1") + .Input( + 1, + "w", + "The weight tensor that will be used in the " + "convolutions; has size (M x C/group x kH x kW), where C " + "is the number of channels, and kH and kW are the " + "height and width of the kernel, and M is the number " + "of feature maps. For more than 2 dimensions, the " + "kernel shape will be (M x C/group x k1 x k2 x ... x kn), " + "where (k1 x k2 x ... kn) is the dimension of the kernel. " + "Optionally, if dimension denotation is in effect, " + "the operation expects the weight tensor to arrive " + "with the dimension denotation of [FILTER_OUT_CHANNEL, " + "FILTER_IN_CHANNEL, FILTER_SPATIAL, FILTER_SPATIAL ...]. " + "X.shape[1] == (W.shape[1] * group) == C " + "(assuming zero based indices for the shape array). " + "Or in other words FILTER_IN_CHANNEL should be equal to DATA_CHANNEL. ", + "T2") + .Input(2, "z", "Padding value (zero_point normally), it's optional and default value is 0.", "T1", OpSchema::Optional) + .Output( + 0, + "y", + "Output data tensor that contains the result of the " + "convolution. The output dimensions are functions " + "of the kernel size, stride size, and pad lengths.", + "T1") + .TypeConstraint("T1", {"tensor(int8)", "tensor(uint8)"}, "Constrain input X and Z data types as 8-bit integer tensors") + .TypeConstraint("T2", {"tensor(int8)", "tensor(uint8)"}, "Constrain input W data types as 8-bit integer tensor") + .TypeConstraint("T3", + {"tensor(int32)", "tensor(uint32)"}, + "Constrain output Y data types as 32-bits integer tensors." + "T3 must be tensor(uint32) when both T1 and T2 are tensor(uint8)," + "or must be tensor(int32) when either T1 or T2 is tensor(int8).") + .Attr( + "auto_pad", + auto_pad_doc, + AttributeProto::STRING, + std::string("NOTSET")) + .Attr( + "kernel_shape", + "The shape of the convolution kernel. If not present, should be inferred from input 'w'.", + AttributeProto::INTS, + OPTIONAL) + .Attr( + "dilations", + "dilation value along each axis of the filter. If not present, the dilation defaults to 1 along each axis.", + AttributeProto::INTS, + OPTIONAL) + .Attr( + "strides", "Stride along each axis. If not present, the stride defaults to 1 along each axis.", AttributeProto::INTS, OPTIONAL) + .Attr("pads", + "Padding for the beginning and ending along each axis, it can take any value greater than or equal to 0." + "The value represent the number of pixels added to the beginning and end part of the corresponding axis." + "`pads` format should be as follow [x1_begin, x2_begin...x1_end, x2_end,...], where xi_begin the number of" + "pixels added at the beginning of axis `i` and xi_end, the number of pixels added at the end of axis `i`." + "This attribute cannot be used simultaneously with auto_pad attribute. If not present, the padding defaults" + "to 0 along start and end of each axis.", + AttributeProto::INTS, OPTIONAL) + .Attr( + "group", + "number of groups input channels and output channels are divided into. default is 1.", + AttributeProto::INT, + static_cast(1)); + + ONNX_CONTRIB_OPERATOR_SCHEMA(MatMulInteger) + .SetDomain(kMSDomain) + .SinceVersion(1) + .SetDoc(R"DOC( +Matrix product that behaves like numpy.matmul: https://docs.scipy.org/doc/numpy-1.13.0/reference/generated/numpy.matmul.html. + The production MUST never overflow. The accumulation may overflow if and only if in 32 bits.)DOC") + .Input(0, "A", "N-dimensional matrix A", "T1") + .Input(0, "B", "N-dimensional matrix B", "T2") + .Output(0, "Y", "Matrix multiply results from A * B", "T3") + .TypeConstraint("T1", {"tensor(int8)", "tensor(uint8)"}, "Constrain input A data types as 8-bit integer tensor") + .TypeConstraint("T2", {"tensor(int8)", "tensor(uint8)"}, "Constrain input B data types as 8-bit integer tensor") + .TypeConstraint("T3", + {"tensor(int32)", "tensor(uint32)"}, + "Constrain output Y data types as 32-bit integer tensor." + "T3 must be tensor(uint32) when both T1 and T2 are tensor(uint8)," + "or must be tensor(int32) when either T1 or T2 is tensor(int8)."); + + ONNX_CONTRIB_OPERATOR_SCHEMA(ReduceSumInteger) + .SetDomain(kMSDomain) + .SinceVersion(1) + .SetDoc(R"DOC( +Computes the sum of the low-precision input tensor's element along the provided axes. +The resulting tensor has the same rank as the input if keepdims equal 1. If keepdims equal 0, +then the resulting tensor have the reduced dimension pruned. The above behavior is similar to numpy, +with the exception that numpy default keepdims to False instead of True.)DOC") + .Input(0, "data", "An input tensor.", "T1") + .Output(0, "reduced", "Reduced output tensor.", "T2") + .TypeConstraint("T1", {"tensor(int8)", "tensor(uint8)"}, "Constrain input type to 8-bit integer tensor.") + .TypeConstraint("T2", + {"tensor(int32)", "tensor(uint32)"}, + "Constrain output data type to 32-bit integer tensor." + "T2 must be tensor(uint32) when T1 is tensor(uint8)," + "or must be tensor(int32) when T1 is tensor(int8).") + .Attr( + "axes", + "A list of integers, along which to reduce. The default is to reduce over all the dimensions of the input tensor.", + AttributeProto::INTS) + .Attr( + "keepdims", + "Keep the reduced dimension or not, default 1 mean keep reduced dimension.", + AttributeProto::INT); } class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, float, SampleOp); From 03d7d25989dc21d08ec2aa70632483092bfdce98 Mon Sep 17 00:00:00 2001 From: Scott McKay Date: Mon, 26 Nov 2018 10:23:26 +1000 Subject: [PATCH 2/3] Support scalars (zero dimensions) in Scan by allowing the parameters to Scan to have no dimension for the input data. --- .../core/framework/mlvalue_tensor_slicer.cc | 4 +- .../core/providers/cpu/controlflow/scan.cc | 5 +- .../providers/cpu/controlflow/scan_test.cc | 68 +++++++++++++------ 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/onnxruntime/core/framework/mlvalue_tensor_slicer.cc b/onnxruntime/core/framework/mlvalue_tensor_slicer.cc index 11d2fde87b..294e95668f 100644 --- a/onnxruntime/core/framework/mlvalue_tensor_slicer.cc +++ b/onnxruntime/core/framework/mlvalue_tensor_slicer.cc @@ -15,8 +15,8 @@ MLValueTensorSlicer MLValueTensorSlicer::Create(T& mlvalue, int64_t slice_ ONNXRUNTIME_ENFORCE(mlvalue.IsAllocated(), "MLValue has not been allocated so can't be sliced."); auto& tensor_shape{mlvalue.template Get().Shape()}; - ONNXRUNTIME_ENFORCE(gsl::narrow_cast(tensor_shape.NumDimensions()) > slice_dimension, - "Insufficient dimensions to slice on ", slice_dimension, ". Shape:", tensor_shape); + ONNXRUNTIME_ENFORCE(gsl::narrow_cast(tensor_shape.NumDimensions()) >= slice_dimension, + "Insufficient dimensions to slice on ", slice_dimension, ". Shape:", tensor_shape); auto dim0_size = tensor_shape[0]; ONNXRUNTIME_ENFORCE(dim0_offset < dim0_size, "Invalid dim0_offset of ", dim0_offset, ". Dimension 0 is ", dim0_size); diff --git a/onnxruntime/core/providers/cpu/controlflow/scan.cc b/onnxruntime/core/providers/cpu/controlflow/scan.cc index cbd6044c82..3366c68cb7 100644 --- a/onnxruntime/core/providers/cpu/controlflow/scan.cc +++ b/onnxruntime/core/providers/cpu/controlflow/scan.cc @@ -303,8 +303,9 @@ static const MLValue& GetSubgraphInputMLValue(const OpKernelContextInternal& con // Validate that the subgraph input has valid shapes Status ScanImpl::ValidateSubgraphInput(int start_input, int end_input, bool has_seq_len_dim, const std::vector& graph_inputs) { - // first dim is batch size. optional sequence dim. dim/s for the data - auto min_dims_required = has_seq_len_dim ? 3 : 2; + // first dim is batch size. optional sequence dim. dim/s for the data. + // if there is no dim for the data treat it as a scalar. + auto min_dims_required = has_seq_len_dim ? 2 : 1; for (int i = start_input; i < end_input; ++i) { auto& input_tensor = GetSubgraphInputTensor(context_, i); diff --git a/onnxruntime/test/providers/cpu/controlflow/scan_test.cc b/onnxruntime/test/providers/cpu/controlflow/scan_test.cc index 965b7295d8..b943f90655 100644 --- a/onnxruntime/test/providers/cpu/controlflow/scan_test.cc +++ b/onnxruntime/test/providers/cpu/controlflow/scan_test.cc @@ -17,6 +17,7 @@ struct RunOptions { bool include_dim_values_in_subgraph = true; bool include_types_in_subgraph = true; bool include_outer_scope_add = false; + bool scalar_loop_state_value = false; bool add_bad_shape = false; }; @@ -37,13 +38,13 @@ class ScanOpTester : public OpTester { // add outer_scope_0 node. push the value through an extra Identity node as a Constant gets lifted into an // initializer which results in different treatment by the allocation planner { - TypeProto float_scalar; - float_scalar.mutable_tensor_type()->set_elem_type(TensorProto_DataType_FLOAT); - auto mutable_dim = float_scalar.mutable_tensor_type()->mutable_shape()->add_dim(); + TypeProto float_single_value; + float_single_value.mutable_tensor_type()->set_elem_type(TensorProto_DataType_FLOAT); + auto mutable_dim = float_single_value.mutable_tensor_type()->mutable_shape()->add_dim(); mutable_dim->set_dim_value(1); { - auto& outer_scope_constant = graph.GetOrCreateNodeArg("outer_scope_constant", &float_scalar); + auto& outer_scope_constant = graph.GetOrCreateNodeArg("outer_scope_constant", &float_single_value); auto* constant = graph.AddNode("outer_scope_constant", "Constant", "Constant with value kOuterNodeAddValue", {}, {&outer_scope_constant}); @@ -54,7 +55,7 @@ class ScanOpTester : public OpTester { constant->AddAttribute("value", value_tensor); - auto& outer_scope_node_arg = graph.GetOrCreateNodeArg("outer_scope_0", &float_scalar); + auto& outer_scope_node_arg = graph.GetOrCreateNodeArg("outer_scope_0", &float_single_value); graph.AddNode("outer_scope_id", "Identity", "Identity for outer_scope_0", {&outer_scope_constant}, {&outer_scope_node_arg}); } @@ -66,7 +67,7 @@ class ScanOpTester : public OpTester { }; static void CreateSubgraph(Graph& graph, RunOptions& options, const std::string& failure_message) { - bool include_shapes = options.include_dim_values_in_subgraph; + bool include_dim_values = options.include_dim_values_in_subgraph; bool include_types = options.include_types_in_subgraph; std::vector inputs; @@ -94,21 +95,27 @@ static void CreateSubgraph(Graph& graph, RunOptions& options, const std::string& inputs = {}; outputs = {}; - TypeProto float_scalar; + TypeProto float_input; // inputs must have type information and a rank - float_scalar.mutable_tensor_type()->set_elem_type(TensorProto_DataType_FLOAT); - auto mutable_dim = float_scalar.mutable_tensor_type()->mutable_shape()->add_dim(); - if (include_shapes) - mutable_dim->set_dim_value(1); + float_input.mutable_tensor_type()->set_elem_type(TensorProto_DataType_FLOAT); + auto mutable_shape = float_input.mutable_tensor_type()->mutable_shape(); + if (options.scalar_loop_state_value) { + // no dims + } else { + auto mutable_dim = mutable_shape->add_dim(); // set rank + if (include_dim_values) + mutable_dim->set_dim_value(1); + } { - auto& output_arg = graph.GetOrCreateNodeArg("constant_1", &float_scalar); + auto& output_arg = graph.GetOrCreateNodeArg("constant_1", &float_input); outputs.push_back(&output_arg); auto* constant = graph.AddNode("constant", "Constant", "Constant with value 1", inputs, outputs); TensorProto value_tensor; - value_tensor.add_dims(1); + if (!options.scalar_loop_state_value) + value_tensor.add_dims(1); value_tensor.add_float_data(1.f); value_tensor.set_data_type(onnx::TensorProto_DataType_FLOAT); @@ -118,7 +125,7 @@ static void CreateSubgraph(Graph& graph, RunOptions& options, const std::string& inputs = outputs; // start with output from Constant node outputs = {}; - auto& input_arg = graph.GetOrCreateNodeArg("loop_state_in_1", &float_scalar); + auto& input_arg = graph.GetOrCreateNodeArg("loop_state_in_1", &float_input); inputs.push_back(&input_arg); TypeProto loop_state_output_tensor; @@ -128,15 +135,17 @@ static void CreateSubgraph(Graph& graph, RunOptions& options, const std::string& // it has to come from here. bool type_and_shape_required = options.include_dim_values_in_main_graph == false; - if (include_shapes || type_and_shape_required) - loop_state_output_tensor.mutable_tensor_type()->mutable_shape()->add_dim()->set_dim_value(1); + if (include_dim_values || type_and_shape_required) { + mutable_shape = loop_state_output_tensor.mutable_tensor_type()->mutable_shape(); + if (!options.scalar_loop_state_value) + mutable_shape->add_dim()->set_dim_value(1); + } TypeProto* type_proto = include_types || type_and_shape_required ? &loop_state_output_tensor : nullptr; auto& output_arg = graph.GetOrCreateNodeArg("loop_state_out_1", type_proto); outputs.push_back(&output_arg); - auto* add = graph.AddNode("add", "Add", "Add 1 to the loop state", inputs, outputs); - (void)add; + graph.AddNode("add", "Add", "Add 1 to the loop state", inputs, outputs); } // subgraph with multiple inputs and outputs to test variadic behaviour. @@ -152,7 +161,7 @@ static void CreateSubgraph(Graph& graph, RunOptions& options, const std::string& // inputs must have type information and rank, but dimension can have no value if we're not providing shape info. concat_input_tensor.mutable_tensor_type()->set_elem_type(TensorProto_DataType_FLOAT); auto mutable_dim = concat_input_tensor.mutable_tensor_type()->mutable_shape()->add_dim(); - if (include_shapes) { + if (include_dim_values) { mutable_dim->set_dim_value(2); if (options.add_bad_shape) { @@ -168,7 +177,7 @@ static void CreateSubgraph(Graph& graph, RunOptions& options, const std::string& // one output from concatenate of {4} tensor TypeProto concat_output_tensor; concat_output_tensor.mutable_tensor_type()->set_elem_type(TensorProto_DataType_FLOAT); - if (include_shapes) + if (include_dim_values) concat_output_tensor.mutable_tensor_type()->mutable_shape()->add_dim()->set_dim_value(4); TypeProto* type_proto = include_types ? &concat_output_tensor : nullptr; @@ -277,13 +286,18 @@ void RunTest(const std::string test_name, int64_t batch_size, int64_t max_sequen test.AddInput("sequence_lens", sequence_lens_dims, *sequence_lens); } - test.AddInput("scan_loop_state_in_0", {batch_size, 1}, loop_state_in_0); + std::vector loop_state_shape{batch_size}; + if (!options.scalar_loop_state_value) { + loop_state_shape.push_back(1); + } + + test.AddInput("scan_loop_state_in_0", loop_state_shape, loop_state_in_0); std::vector input_shape{batch_size, max_sequence_len, input_size}; test.AddInput("scan_input_0", input_shape, input_0); test.AddInput("scan_input_1", input_shape, input_1); - test.AddOutput("scan_loop_state_out_0", {batch_size, 1}, loop_state_out_0); + test.AddOutput("scan_loop_state_out_0", loop_state_shape, loop_state_out_0); std::vector output_shape{batch_size, max_sequence_len, 1}; test.AddOutput("scan_output_0", output_shape, output_0); @@ -353,6 +367,16 @@ TEST(Scan, ShortSequenceOneInBatchOneLoopStateVar_NoShapeInMainGraph_NoTypeAndSh ShortSequenceOneInBatchOneLoopStateVar(options); } +TEST(Scan, OnnxScalarLoopState) { + RunOptions options{}; + options.include_dim_values_in_main_graph = true; + options.include_types_in_subgraph = false; + options.include_dim_values_in_subgraph = false; + options.scalar_loop_state_value = true; + + ShortSequenceOneInBatchOneLoopStateVar(options); +} + // test when there is an operator in the subgraph that uses a value coming from outer scope TEST(Scan, OuterScopeAccess_NoShapeInMainGraph_TypeAndShapeInSubgraph) { RunOptions options{}; From 84fa1018a3fb939941775e8f6f88f41fb114beb4 Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Mon, 26 Nov 2018 01:14:09 -0800 Subject: [PATCH 3/3] Create CODEOWNERS (#27) --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000..ad7e05b9cf --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +@Microsoft/onnxruntime