chore: import upstream snapshot with attribution
cffconvert / validate (push) Has been skipped
License Check / license-check (push) Failing after 2s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:14:16 +08:00
commit 8a852e4b4e
36502 changed files with 9277225 additions and 0 deletions
@@ -0,0 +1,108 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "tensorflow/core/platform/errors.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status ConvertExpandDimsToReshape::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto expand_it = model->operators.begin() + op_index;
if (expand_it->get()->type != OperatorType::kExpandDims) {
return absl::OkStatus();
}
ExpandDimsOperator* expand_op =
static_cast<ExpandDimsOperator*>(expand_it->get());
CHECK_EQ(expand_op->inputs.size(), 2);
CHECK_EQ(expand_op->outputs.size(), 1);
const auto& input_array = model->GetArray(expand_op->inputs[0]);
if (!input_array.has_shape()) {
// Yield until input dims have been resolved.
return absl::OkStatus();
}
const auto& axis_array = model->GetArray(expand_op->inputs[1]);
if (!axis_array.has_shape()) {
// Yield until input axis array shape has been resolved.
return absl::OkStatus();
}
CHECK_EQ(RequiredBufferSizeForShape(axis_array.shape()), 1);
if (!axis_array.buffer) {
// Yield until the input axis array is constant
return absl::OkStatus();
}
int axis = axis_array.GetBuffer<ArrayDataType::kInt32>().data[0];
std::vector<int> reshape_dims(input_array.shape().dims());
int original_dims_num = reshape_dims.size();
if (axis > original_dims_num || axis < -(original_dims_num + 1)) {
return absl::InvalidArgumentError(absl::StrCat(
"Invalid axis attribute ", axis, " for original dimension ",
original_dims_num, " in ExpandDims op."));
}
if (axis < 0) {
axis = reshape_dims.size() + 1 + axis;
}
reshape_dims.insert(reshape_dims.begin() + axis, 1);
// The input tensor has shape, and the axis input is constant. We can now
// replace ExpandDims with a Reshape.
auto* reshape_op = new TensorFlowReshapeOperator;
// Copy inputs
reshape_op->inputs.push_back(expand_op->inputs[0]);
reshape_op->outputs = expand_op->outputs;
// Create a new input array
std::string axis_array_name = expand_op->inputs[1];
std::string shape_array_name =
toco::AvailableArrayName(*model, axis_array_name);
Array& shape_array = model->GetOrCreateArray(shape_array_name);
*(shape_array.mutable_shape()->mutable_dims()) = {
1, static_cast<int>(reshape_dims.size())};
reshape_op->inputs.push_back(shape_array_name);
shape_array.data_type = ArrayDataType::kInt32;
auto& shape_buffer = shape_array.GetMutableBuffer<ArrayDataType::kInt32>();
shape_buffer.data = reshape_dims;
// Delete axis array if unused
if (IsDiscardableArray(*model, axis_array_name) &&
CountOpsWithInput(*model, axis_array_name) == 1 &&
!GetOpWithOutput(*model, axis_array_name)) {
model->EraseArray(axis_array_name);
}
// Replace the operator in the graph.
model->operators.emplace(expand_it, reshape_op);
DeleteOpAndArrays(model, expand_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,108 @@
/* Copyright 2019 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "tensorflow/core/lib/core/errors.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
// V3 is only different from V2 because it has an extra attribute (align).
// This attribute doesn't affect V1 so we don't have to keep track of it here.
absl::Status ConvertMatrixDiagV2OrV3ToV1::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
auto it = model->operators.begin() + op_index;
const auto* op = it->get();
if (op->type != OperatorType::kMatrixDiagV2 &&
op->type != OperatorType::kMatrixDiagV3) {
return absl::OkStatus();
}
if (op->inputs.size() != 5) {
return absl::InvalidArgumentError(
absl::StrCat("The input size of op %s should be 5", LogName(*op)));
}
const auto& input_k = model->GetArray(op->inputs[1]);
const auto& input_num_rows = model->GetArray(op->inputs[2]);
const auto& input_num_cols = model->GetArray(op->inputs[3]);
const auto& input_padding_value = model->GetArray(op->inputs[4]);
if (!input_k.buffer || !input_num_rows.buffer || !input_num_cols.buffer ||
!input_padding_value.buffer) {
return absl::OkStatus();
}
if (input_k.GetBuffer<ArrayDataType::kInt32>().data.size() != 1 ||
input_num_rows.GetBuffer<ArrayDataType::kInt32>().data.size() != 1 ||
input_num_cols.GetBuffer<ArrayDataType::kInt32>().data.size() != 1) {
return absl::InvalidArgumentError(
absl::StrCat("Array for argument k / num_rows / num_cols of op ",
LogName(*op), " should contains exact one element"));
}
int k = input_k.GetBuffer<ArrayDataType::kInt32>().data[0];
int num_rows = input_num_rows.GetBuffer<ArrayDataType::kInt32>().data[0];
int num_cols = input_num_cols.GetBuffer<ArrayDataType::kInt32>().data[0];
const auto& padding_value_vector =
input_padding_value.GetBuffer<ArrayDataType::kUint8>().data;
if (k != 0) {
return absl::InvalidArgumentError(absl::StrCat(
"parameter k of op ", LogName(*op),
" is expected to be 0, other values are not supported currently"));
}
if (num_rows != -1) {
return absl::InvalidArgumentError(absl::StrCat(
"parameter num_rows of op ", LogName(*op),
" is expected to be -1, other values are not supported currently"));
}
if (num_cols != -1) {
return absl::InvalidArgumentError(absl::StrCat(
"parameter num_cols of op ", LogName(*op),
" is expected to be -1, other values are not supported currently"));
}
for (auto byte : padding_value_vector) {
if (byte != 0) {
return absl::InvalidArgumentError(absl::StrCat(
"parameter padding_value of op ", LogName(*op),
" is expected to be 0, other values are not supported currently"));
}
}
auto* matrix_diag_op = new MatrixDiagOperator;
matrix_diag_op->inputs.push_back(op->inputs[0]);
matrix_diag_op->outputs.push_back(op->outputs[0]);
AddMessageF("Replacing %s with %s", LogName(*op), LogName(*matrix_diag_op));
// Replace the operator in the graph.
model->operators.emplace(it, matrix_diag_op);
DeleteOpAndArrays(model, op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,83 @@
/* Copyright 2019 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "tensorflow/core/lib/core/errors.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
// V3 is only different from V2 because it has an extra attribute (align).
// This attribute doesn't affect V1 so we don't have to keep track of it here.
absl::Status ConvertMatrixSetDiagV2OrV3ToV1::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
auto it = model->operators.begin() + op_index;
const auto* op = it->get();
if (op->type != OperatorType::kMatrixSetDiagV2 &&
op->type != OperatorType::kMatrixSetDiagV3) {
return absl::OkStatus();
}
if (op->inputs.size() != 3) {
return absl::InvalidArgumentError(
absl::StrCat("The input size of op %s should be 3", LogName(*op)));
}
const auto& input_k = model->GetArray(op->inputs[2]);
if (!input_k.buffer) {
return absl::OkStatus();
}
if (input_k.GetBuffer<ArrayDataType::kInt32>().data.size() != 1) {
return absl::InvalidArgumentError(absl::StrCat(
"Array for argument k of op %s should contains exact one element",
LogName(*op)));
}
int k = input_k.GetBuffer<ArrayDataType::kInt32>().data[0];
if (k != 0) {
return absl::InvalidArgumentError(absl::StrCat(
"parameter k of op ", LogName(*op),
" is expected to be 0, other values are not supported currently"));
}
auto* matrix_set_diag_op = new MatrixSetDiagOperator;
matrix_set_diag_op->inputs.push_back(op->inputs[0]);
matrix_set_diag_op->inputs.push_back(op->inputs[1]);
matrix_set_diag_op->outputs.push_back(op->outputs[0]);
AddMessageF("Replacing %s with %s", LogName(*op),
LogName(*matrix_set_diag_op));
// Replace the operator in the graph.
model->operators.emplace(it, matrix_set_diag_op);
DeleteOpAndArrays(model, op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,118 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status ConvertPureConvToDepthwise::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto conv_it = model->operators.begin() + op_index;
if (conv_it->get()->type != OperatorType::kConv) {
return absl::OkStatus();
}
const auto* conv_op = static_cast<ConvOperator*>(conv_it->get());
if (conv_op->stride_width != conv_op->stride_height) {
return absl::OkStatus();
}
if ((conv_op->dilation_width_factor != 1) ||
(conv_op->dilation_height_factor != 1)) {
// Depthwise conv does not support dilation
return absl::OkStatus();
}
auto& input_array = model->GetArray(conv_op->inputs[0]);
if (!input_array.has_shape()) {
// Shapes not propagated yet
return absl::OkStatus();
}
if (input_array.shape().dims(3) != 1) {
// Not a pure convolution: Conv does accumulation across the depth
// dimension.
return absl::OkStatus();
}
const auto& weights_name = conv_op->inputs[1];
if (CountOpsWithInput(*model, weights_name) > 1) {
// TODO(yunluli): Come up with a way to do the weights shuffling only once.
AddMessageF(
"Not changing %s to DepthwiseConv because the weights is consumed by "
"another op.",
LogName(*conv_op));
return absl::OkStatus();
}
auto& weights_array = model->GetArray(weights_name);
if (!weights_array.buffer) {
// Yield until the weights are resolved as a constant array.
return absl::OkStatus();
}
if (weights_array.data_type != ArrayDataType::kFloat) {
return absl::OkStatus();
}
// At this point we know we have a pure conv. Rewrite it as DepthwiseConv.
AddMessageF(
"%s is purely convolutional (input/weights depth is 1), replacing it by "
"a DepthwiseConv.",
LogName(*conv_op));
auto* depthwiseconv_op = new DepthwiseConvOperator;
// Conv and DepthwiseConv take the same inputs
depthwiseconv_op->inputs = conv_op->inputs;
// Conv may have a 2nd output for im2col
depthwiseconv_op->outputs = {conv_op->outputs[0]};
if (conv_op->outputs.size() > 1) {
// delete the im2col array.
model->EraseArray(conv_op->outputs[1]);
}
depthwiseconv_op->fused_activation_function =
conv_op->fused_activation_function;
// Let PropagateFixedSizes recompute fixed padding, just in case some day it
// may be different for Conv vs DepthwiseConv.
depthwiseconv_op->padding.type = conv_op->padding.type;
depthwiseconv_op->stride_height = conv_op->stride_height;
depthwiseconv_op->stride_width = conv_op->stride_width;
depthwiseconv_op->depth_multiplier = weights_array.shape().dims(0);
// Replace the operator in the graph.
model->operators.emplace(conv_it, depthwiseconv_op);
DeleteOpAndArrays(model, conv_op);
// Shuffle the weights.
const auto& weights_shape = weights_array.shape();
auto& weights_buffer =
weights_array.GetMutableBuffer<ArrayDataType::kFloat>();
const std::vector<float>& conv_weights_data = weights_buffer.data;
std::vector<float> depthwise_conv_weights_data(conv_weights_data.size());
const int depth = weights_shape.dims(0);
const int width = weights_shape.dims(1);
const int height = weights_shape.dims(2);
const int width_height = width * height;
for (int c = 0; c < depth; c++) {
for (int xy = 0; xy < width_height; xy++) {
depthwise_conv_weights_data[c + depth * xy] =
conv_weights_data[xy + width_height * c];
}
}
*weights_array.mutable_shape()->mutable_dims() = {1, width, height, depth};
weights_buffer.data = depthwise_conv_weights_data;
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,153 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
// Creates a Reshape operator from ReorderAxes operator.
TensorFlowReshapeOperator* CreateReshapeFromReorderAxes(
Model* model, ReorderAxesOperator* reorder_op, const Shape& input_shape) {
auto* reshape_op = new TensorFlowReshapeOperator;
// Copy inputs and outputs to Reshape.
reshape_op->inputs.push_back(reorder_op->inputs[0]);
reshape_op->outputs = reorder_op->outputs;
// Create reshape dimensions based on input shape. Conversion from
// ReorderAxes to Reshape requires a 4D input shape.
CHECK_EQ(input_shape.dimensions_count(), 4);
std::vector<int> reshape_dims = {1, input_shape.dims(0), input_shape.dims(1),
input_shape.dims(3) * input_shape.dims(2)};
// Create a new input array for Reshape.
std::string reshape_array_name =
AvailableArrayName(*model, reshape_op->outputs[0]);
reshape_op->inputs.push_back(reshape_array_name);
Array& reshape_array = model->GetOrCreateArray(reshape_array_name);
*(reshape_array.mutable_shape()->mutable_dims()) = {
1, static_cast<int>(reshape_dims.size())};
reshape_array.data_type = ArrayDataType::kInt32;
auto& reshape_buffer =
reshape_array.GetMutableBuffer<ArrayDataType::kInt32>();
reshape_buffer.data = reshape_dims;
return reshape_op;
}
// Creates a Transpose operator from ReorderAxes operator.
TransposeOperator* CreateTransposeFromReorderAxes(
Model* model, ReorderAxesOperator* reorder_op, const Shape& input_shape,
const AxesOrder& input_axes_order, const AxesOrder& output_axes_order) {
auto* transpose_op = new TransposeOperator;
// Copy inputs and outputs to Transpose.
transpose_op->inputs.push_back(reorder_op->inputs[0]);
transpose_op->outputs = reorder_op->outputs;
// Create permutations data based on input and output axes order.
std::vector<int> permutations_data;
GetShuffleShape(input_axes_order, output_axes_order, &permutations_data);
// Create a new input permutations array for Transpose.
std::string perm_array_name =
AvailableArrayName(*model, transpose_op->outputs[0]);
transpose_op->inputs.push_back(perm_array_name);
Array& perm_array = model->GetOrCreateArray(perm_array_name);
*(perm_array.mutable_shape()->mutable_dims()) = {
static_cast<int>(permutations_data.size())};
perm_array.data_type = ArrayDataType::kInt32;
auto& perm_buffer = perm_array.GetMutableBuffer<ArrayDataType::kInt32>();
perm_buffer.data = permutations_data;
return transpose_op;
}
// Converts ReorderAxes into Transpose and Reshape which are compatible with the
// TFLite interpreter.
absl::Status ConvertReorderAxes::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto reorder_it = model->operators.begin() + op_index;
if (reorder_it->get()->type != OperatorType::kReorderAxes)
return absl::OkStatus();
auto* reorder_op = static_cast<ReorderAxesOperator*>(reorder_it->get());
CHECK_EQ(reorder_op->inputs.size(), 1);
CHECK_EQ(reorder_op->outputs.size(), 1);
const auto& input_array_name = reorder_op->inputs[0];
const auto& output_array_name = reorder_op->outputs[0];
auto& input_array = model->GetArray(input_array_name);
auto& output_array = model->GetArray(output_array_name);
// Get input array. If kFakeQuant is the input into ReorderAxes, get the input
// array passed into kFakeQuant. kFakeQuant op is dropped when possible.
std::string constant_input_array_name = input_array_name;
if (!input_array.buffer) {
const auto* op_producing_input = GetOpWithOutput(*model, input_array_name);
if (op_producing_input &&
op_producing_input->type == OperatorType::kFakeQuant) {
constant_input_array_name = op_producing_input->inputs[0];
}
}
// Yield if input array contains constants or if output array size has not
// been adjusted to reflect the permutations in ReorderAxes. ReorderAxes will
// be merged into a constant array when possible.
if (IsConstantParameterArray(*model, constant_input_array_name))
return absl::OkStatus();
if (!output_array.has_shape()) return absl::OkStatus();
const auto input_axes_order = reorder_op->input_axes_order;
const auto output_axes_order = reorder_op->output_axes_order;
const Shape input_shape = input_array.shape();
// Creates a Reshape or Transpose operator depending on the conversion.
if (input_axes_order == AxesOrder::kHWIM &&
output_axes_order == AxesOrder::k1HWO) {
// Add Reshape operator into the graph. This special case is not just a
// permutation. The input dimensions get merged into 3 dimensions while the
// order of the elements does not change.
auto* reshape_op =
CreateReshapeFromReorderAxes(model, reorder_op, input_shape);
model->operators.emplace(reorder_it, reshape_op);
} else {
// Add Transpose operator into the graph.
auto* transpose_op = CreateTransposeFromReorderAxes(
model, reorder_op, input_shape, input_axes_order, output_axes_order);
model->operators.emplace(reorder_it, transpose_op);
}
// Remove ReorderAxes operator from the graph.
DeleteOpAndArrays(model, reorder_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,87 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
// Replaces a tf.squeeze operator with a reshape.
// Squeeze removes dimensions == 1 (if in the list of squeeze_dims). This
// means that the data layout will never change with this op, just the shape.
// By converting these to reshapes once we have run shape propagation we allow
// standard reshape optimization transforms to do their magic.
absl::Status ConvertSqueezeToReshape::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto squeeze_it = model->operators.begin() + op_index;
if (squeeze_it->get()->type != OperatorType::kSqueeze) {
return absl::OkStatus();
}
auto squeeze_op = static_cast<SqueezeOperator*>(squeeze_it->get());
CHECK_EQ(squeeze_op->inputs.size(), 1);
CHECK_EQ(squeeze_op->outputs.size(), 1);
const auto& input_array = model->GetArray(squeeze_op->inputs[0]);
if (!input_array.has_shape()) {
// Yield until input dims have been resolved.
return absl::OkStatus();
}
if (input_array.shape().dimensions_count() == 0) {
// Input array cannot be 0-D.
return absl::OkStatus();
}
if (!model->HasArray(squeeze_op->outputs[0]) ||
!model->GetArray(squeeze_op->outputs[0]).has_shape()) {
// Yield until shape propagation has set the output shape for us.
return absl::OkStatus();
}
// We use the output shape that has been calculated by shape propagation.
const auto& output_shape = model->GetArray(squeeze_op->outputs[0]).shape();
// Empty shapes will not work as empty data arrays.
if (output_shape.dimensions_count() == 0) {
return absl::OkStatus();
}
auto* reshape_op = new TensorFlowReshapeOperator;
reshape_op->inputs = {
squeeze_op->inputs[0],
CreateInt32Array(model, squeeze_op->outputs[0] + "_shape",
output_shape.dims()),
};
reshape_op->outputs = squeeze_op->outputs;
AddMessageF("Replacing %s with %s", LogName(*squeeze_op),
LogName(*reshape_op));
// Replace the operator in the graph.
model->operators.emplace(squeeze_it, reshape_op);
DeleteOpAndArrays(model, squeeze_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,58 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
// This pass will convert an AddN operator with only 2 inputs into a regular Add
// operator, to which more optimizations may apply.
absl::Status ConvertTrivialAddNToAdd::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto addn_it = model->operators.begin() + op_index;
if (addn_it->get()->type != OperatorType::kAddN) {
return absl::OkStatus();
}
AddNOperator* addn_op = static_cast<AddNOperator*>(addn_it->get());
CHECK_GE(addn_op->inputs.size(), 2);
CHECK_EQ(addn_op->outputs.size(), 1);
// We only reduce AddN with N=2 to a regular Add.
if (addn_op->inputs.size() != 2) {
return absl::OkStatus();
}
// Copy inputs & outputs to regular Add.
auto* add_op = new AddOperator;
add_op->inputs.push_back(addn_op->inputs[0]);
add_op->inputs.push_back(addn_op->inputs[1]);
add_op->outputs = addn_op->outputs;
// Replace the AddN operator in the graph.
model->operators.emplace(addn_it, add_op);
DeleteOpAndArrays(model, addn_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,89 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status ConvertTrivialPackToReshape::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
auto pack_it = model->operators.begin() + op_index;
if (pack_it->get()->type != OperatorType::kPack) {
return absl::OkStatus();
}
auto* pack_op = static_cast<PackOperator*>(pack_it->get());
if (pack_op->inputs.size() > 1) {
// Not trivial.
return absl::OkStatus();
}
CHECK_EQ(pack_op->outputs.size(), 1);
const auto& input_array = model->GetArray(pack_op->inputs[0]);
if (!input_array.has_shape()) {
// Yield until input dims have been resolved.
return absl::OkStatus();
}
if (input_array.shape().dimensions_count() == 0) {
// Input array cannot be 0-D.
// (Unsure if this is TF behavior, but was required to get a test to pass.)
return absl::OkStatus();
}
AddMessageF("Converting trivial %s to a reshape", LogName(*pack_op));
// Note that we could convert to ExpandDims but toco prefers reshapes.
auto* reshape_op = new TensorFlowReshapeOperator;
reshape_op->inputs = {pack_op->inputs[0]};
reshape_op->outputs = pack_op->outputs;
// Create shape param.
std::string shape_array_name =
AvailableArrayName(*model, pack_op->outputs[0] + "_shape");
Array& shape_array = model->GetOrCreateArray(shape_array_name);
const int shape_array_dims = 1 + input_array.shape().dimensions_count();
*(shape_array.mutable_shape()->mutable_dims()) = {shape_array_dims};
reshape_op->inputs.push_back(shape_array_name);
shape_array.data_type = ArrayDataType::kInt32;
auto& shape_buffer = shape_array.GetMutableBuffer<ArrayDataType::kInt32>();
// Insert '1' at the 'axis' dimension of the output shape.
int index = 0;
for (int dim = 0; dim < shape_array_dims; ++dim) {
dim == pack_op->axis
? shape_buffer.data.push_back(1)
: shape_buffer.data.push_back(input_array.shape().dims(index++));
}
// Replace the operator in the graph.
model->operators.emplace(pack_it, reshape_op);
DeleteOpAndArrays(model, pack_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,98 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <vector>
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/toco_types.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status ConvertTrivialTileToConcat::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto tile_it = model->operators.begin() + op_index;
if (tile_it->get()->type != OperatorType::kTile) {
return absl::OkStatus();
}
auto* tile_op = static_cast<TransposeOperator*>(tile_it->get());
const auto& input_array = model->GetArray(tile_op->inputs[0]);
const auto& multiples_array = model->GetArray(tile_op->inputs[1]);
const auto& output_array = model->GetArray(tile_op->outputs[0]);
if (!input_array.has_shape() || !multiples_array.has_shape() ||
!output_array.has_shape()) {
// Yield until PropagateFixedSizes has been run on this op.
return absl::OkStatus();
}
// Note: We can assume we have error checked inputs in PropagateFixedSizes.
if (!multiples_array.buffer) {
// Yield until the multiples is constant.
return absl::OkStatus();
}
std::vector<int32> const& multiples =
multiples_array.GetBuffer<ArrayDataType::kInt32>().data;
// We can simplify the tile if only a single dimension is being multiplied.
// It then just becomes a concat along that dimension.
int non_one_dims = 0;
int concat_axis = 0;
for (size_t i = 0; i < multiples.size(); ++i) {
if (multiples[i] != 1) {
++non_one_dims;
concat_axis = i;
}
}
if (non_one_dims != 1) {
// The tile is non-trivial. Good luck.
AddMessageF("Tile %s is non-trivial (has more than one multiply dimension)",
LogName(*tile_op));
return absl::OkStatus();
}
// The tile is like a concat.
AddMessageF("Simplifying %s to a Concat along a single axis %d",
LogName(*tile_op), concat_axis);
auto* concat_op = new ConcatenationOperator;
// Copy input and output.
// Note that we multiply out the input by the number of times requested.
for (int i = 0; i < multiples[concat_axis]; ++i) {
concat_op->inputs.push_back(tile_op->inputs[0]);
}
concat_op->axis = concat_axis;
concat_op->outputs = tile_op->outputs;
// Delete multiples array if unused.
if (IsDiscardableArray(*model, tile_op->inputs[1]) &&
CountOpsWithInput(*model, tile_op->inputs[1]) == 1) {
model->EraseArray(tile_op->inputs[1]);
}
// Replace the operator in the graph.
model->operators.emplace(tile_it, concat_op);
DeleteOpAndArrays(model, tile_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,123 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
bool TransposeAffectsMemoryOrder(std::vector<int> perm,
std::vector<int> in_shape) {
CHECK_EQ(perm.size(), in_shape.size());
// See what the ordering of the non-unary columns are before and after
// transpose permutation. If the major indices stay in the same order (not
// just the shape) then the flat buffer representation shouldn't change.
std::vector<int> old_major_index_ordering;
std::vector<int> new_major_index_ordering;
for (int i = 0, end = in_shape.size(); i < end; i++) {
if (in_shape[i] != 1) {
old_major_index_ordering.push_back(i);
}
if (in_shape[perm[i]] != 1) {
new_major_index_ordering.push_back(perm[i]);
}
}
CHECK_EQ(new_major_index_ordering.size(), old_major_index_ordering.size());
return old_major_index_ordering != new_major_index_ordering;
}
} // namespace
absl::Status ConvertTrivialTransposeToReshape::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
auto transpose_it = model->operators.begin() + op_index;
if (transpose_it->get()->type != OperatorType::kTranspose) {
return absl::OkStatus();
}
TransposeOperator* transpose_op =
static_cast<TransposeOperator*>(transpose_it->get());
const auto& input_array = model->GetArray(transpose_op->inputs[0]);
const auto& output_array = model->GetArray(transpose_op->outputs[0]);
if (!input_array.has_shape() || !output_array.has_shape()) {
// Yield until PropagateFixedSizes has been run on this op.
return absl::OkStatus();
}
// Note: We can assume we have error checked inputs in PropagateFixedSizes.
// Check that the permutation has propagated.
std::vector<int> const& perm = transpose_op->perm;
if (perm.empty()) {
return absl::OkStatus();
}
// This transpose is trivial if non-unitary dimensions remain in the same
// order.
std::vector<int> const& input_dims = input_array.shape().dims();
std::vector<int> const& output_dims = output_array.shape().dims();
if (TransposeAffectsMemoryOrder(perm, input_dims)) {
return absl::OkStatus();
}
// This transpose is trivial. Replace it with a Reshape op.
auto* reshape_op = new TensorFlowReshapeOperator;
// Copy input and output
reshape_op->inputs.push_back(transpose_op->inputs[0]);
reshape_op->outputs = transpose_op->outputs;
// Create a new input array for the shape input
std::string perm_array_name = transpose_op->inputs[1];
std::string shape_array_name =
toco::AvailableArrayName(*model, perm_array_name);
Array& shape_array = model->GetOrCreateArray(shape_array_name);
*(shape_array.mutable_shape()->mutable_dims()) = {
1, static_cast<int>(output_dims.size())};
reshape_op->inputs.push_back(shape_array_name);
shape_array.data_type = ArrayDataType::kInt32;
auto& shape_buffer = shape_array.GetMutableBuffer<ArrayDataType::kInt32>();
shape_buffer.data = output_dims;
// Delete perm array if unused
if (IsDiscardableArray(*model, perm_array_name) &&
CountOpsWithInput(*model, perm_array_name) == 1) {
model->EraseArray(perm_array_name);
}
// Replace the operator in the graph.
model->operators.emplace(transpose_it, reshape_op);
DeleteOpAndArrays(model, transpose_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,97 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
bool ProcessConvOperator(Model* model, ConvOperator* op) {
if (op->outputs.size() == 2) {
// We already have an im2col array
return false;
}
const auto& weights_array = model->GetArray(op->inputs[1]);
if (!weights_array.has_shape()) {
// We need to yield until weights dims have been resolved, because
// from the weights dims we determine whether an im2col array is
// needed.
return false;
}
const auto& weights_shape = weights_array.shape();
const int kheight = weights_shape.dims(1);
const int kwidth = weights_shape.dims(2);
if (kwidth == 1 && kheight == 1 && op->stride_width == 1 &&
op->stride_height == 1 && op->dilation_width_factor == 1 &&
op->dilation_height_factor == 1) {
// 1x1 unstrided undilated conv does not need an im2col array.
return false;
}
// Create the im2col array.
CHECK_EQ(op->outputs.size(), 1);
const std::string& im2col_array_name =
AvailableArrayName(*model, op->inputs[0] + "_im2col");
model->GetOrCreateArray(im2col_array_name);
op->outputs.push_back(im2col_array_name);
return true;
}
bool ProcessTransposeConvOperator(Model* model, TransposeConvOperator* op) {
if (op->outputs.size() == 2) {
// We already have an im2col array
return false;
}
// Always create an im2col array for transpose_conv.
CHECK_EQ(op->outputs.size(), 1);
const std::string& im2col_array_name = AvailableArrayName(
*model, op->inputs[TransposeConvOperator::DATA_INPUT] + "_im2col");
model->GetOrCreateArray(im2col_array_name);
op->outputs.push_back(im2col_array_name);
return true;
}
absl::Status CreateIm2colArrays::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto it = model->operators.begin() + op_index;
auto* op = it->get();
switch (op->type) {
case OperatorType::kConv:
*modified = ProcessConvOperator(model, static_cast<ConvOperator*>(op));
return absl::OkStatus();
case OperatorType::kTransposeConv:
*modified = ProcessTransposeConvOperator(
model, static_cast<TransposeConvOperator*>(op));
return absl::OkStatus();
default:
return absl::OkStatus();
}
}
} // namespace toco
@@ -0,0 +1,236 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <cstdlib>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/remove_trivial_passthrough.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
template <ArrayDataType A>
void DequantizeBuffer(Array* array) {
const auto old_data = array->GetBuffer<A>().data;
array->buffer = nullptr;
array->data_type = ArrayDataType::kFloat;
auto& new_data = array->GetMutableBuffer<ArrayDataType::kFloat>().data;
new_data.resize(old_data.size());
const auto& qparams = array->GetQuantizationParams();
for (int i = 0, end = old_data.size(); i < end; i++) {
new_data[i] = qparams.scale * (old_data[i] - qparams.zero_point);
}
}
std::vector<std::unique_ptr<Operator>>::iterator FindFirstOpWithInput(
Model* model, const std::string& array_name) {
for (auto it = model->operators.begin(); it != model->operators.end(); ++it) {
for (const auto& input : it->get()->inputs) {
if (input == array_name) {
return it;
}
}
}
return model->operators.end();
}
void ClearArrayQuantizationParams(const std::string& array_name, Model* model) {
auto* array = &model->GetArray(array_name);
CHECK(array->quantization_params);
for (auto& input_array : *model->flags.mutable_input_arrays()) {
if (input_array.name() == array_name) {
auto& qparams = *array->quantization_params;
const double new_std_value = 1. / qparams.scale;
const double new_mean_value = qparams.zero_point;
if (input_array.has_std_value()) {
CHECK_LE(std::abs(new_std_value - input_array.std_value()), 0.001);
} else {
input_array.set_std_value(new_std_value);
}
if (input_array.has_mean_value()) {
CHECK_LE(std::abs(new_mean_value - input_array.mean_value()), 0.001);
} else {
input_array.set_mean_value(new_mean_value);
}
}
}
array->quantization_params = nullptr;
}
bool DequantizeArray(const std::string& array_name,
GraphTransformation* transformation, Model* model) {
auto* array = &model->GetArray(array_name);
if (!array->quantization_params) {
return false;
}
transformation->AddMessageF("Dequantizing array: %s", array_name);
// Dequantize any buffer
if (array->buffer) {
if (array->data_type == ArrayDataType::kUint8) {
DequantizeBuffer<ArrayDataType::kUint8>(array);
} else if (array->data_type == ArrayDataType::kInt32) {
DequantizeBuffer<ArrayDataType::kInt32>(array);
} else {
LOG(FATAL) << "Unhandled data type";
}
CHECK(array->data_type == ArrayDataType::kFloat);
CHECK(array->buffer->type == ArrayDataType::kFloat);
// Clear quantization params, officially makes this a non-quantized array.
ClearArrayQuantizationParams(array_name, model);
return true;
} else {
array->data_type = ArrayDataType::kFloat;
}
// Clear quantization params, officially makes this a non-quantized array.
ClearArrayQuantizationParams(array_name, model);
if (array->buffer) {
return true;
}
auto* op_outputting_array = GetOpWithOutput(*model, array_name);
if (op_outputting_array) {
if (op_outputting_array->type == OperatorType::kReshape) {
return true;
}
}
// If there was no minmax info, we can return now. Indeed,
// the below only serves to create a FakeQuant node, but some arrays are
// quantized without MinMax (see the CHECK above) and that corresponds to
// places where a FakeQuant node is actually not wanted, because the
// quantization params are meant to be inferred in another way (e.g. bias
// vector for a Conv op, see their special-casing in quantize.cc).
if (!array->minmax) {
return true;
}
// Determine whether to insert a FakeQuant before or after
// this array.
bool must_insert_fakequant_before = false;
bool must_insert_fakequant_after = false;
if (IsInputArray(*model, array_name)) {
must_insert_fakequant_after = true;
}
for (const std::string& output_array : model->flags.output_arrays()) {
if (array_name == output_array) {
must_insert_fakequant_before = true;
}
}
for (const auto& rnn_state : model->flags.rnn_states()) {
if (array_name == rnn_state.state_array()) {
must_insert_fakequant_after = true;
}
if (array_name == rnn_state.back_edge_source_array()) {
must_insert_fakequant_before = true;
}
}
CHECK(!(must_insert_fakequant_before && must_insert_fakequant_after));
// Create and insert the FakeQuant node
auto* fakequant_op = new FakeQuantOperator;
model->operators.emplace(FindFirstOpWithInput(model, array_name),
fakequant_op);
const std::string& new_array_name = AvailableArrayName(*model, array_name);
auto& new_array = model->GetOrCreateArray(new_array_name);
new_array.data_type = ArrayDataType::kFloat;
new_array.copy_shape(array->shape());
new_array.GetOrCreateMinMax() = array->GetMinMax();
fakequant_op->minmax = std::make_unique<MinMax>();
*fakequant_op->minmax = array->GetMinMax();
fakequant_op->narrow_range = array->narrow_range;
if (must_insert_fakequant_before) {
for (const auto& op : model->operators) {
for (std::string& output : op->outputs) {
if (output == array_name) {
output = new_array_name;
}
}
}
fakequant_op->inputs = {new_array_name};
fakequant_op->outputs = {array_name};
} else {
for (const auto& op : model->operators) {
for (std::string& input : op->inputs) {
if (input == array_name) {
input = new_array_name;
}
}
}
fakequant_op->inputs = {array_name};
fakequant_op->outputs = {new_array_name};
}
return true;
}
} // namespace
absl::Status Dequantize::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto op_it = model->operators.begin() + op_index;
auto* op = op_it->get();
if (op->type == OperatorType::kDequantize) {
auto& input_array = model->GetArray(op->inputs[0]);
if (input_array.data_type == ArrayDataType::kFloat) {
return absl::OkStatus();
}
if (input_array.final_data_type != ArrayDataType::kFloat) {
return absl::OkStatus();
}
input_array.data_type = ArrayDataType::kFloat;
input_array.quantization_params = nullptr;
auto& output_array = model->GetArray(op->outputs[0]);
output_array.data_type = ArrayDataType::kFloat;
output_array.quantization_params = nullptr;
*modified = RemoveTrivialPassthroughOp(this, model, op_index);
return absl::OkStatus();
}
std::vector<std::string> arrays;
arrays.reserve(op->inputs.size());
for (const std::string& input : op->inputs) {
arrays.push_back(input);
}
for (const std::string& output : op->outputs) {
arrays.push_back(output);
}
bool changed = false;
for (const std::string& array : arrays) {
if (!model->IsOptionalArray(array)) {
changed |= DequantizeArray(array, this, model);
}
}
*modified = changed;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,59 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/remove_trivial_passthrough.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status DropFakeQuant::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto fakequant_it = model->operators.begin() + op_index;
auto* fakequant_base_op = fakequant_it->get();
if (fakequant_base_op->type != OperatorType::kFakeQuant) {
return absl::OkStatus();
}
auto* fakequant_op = static_cast<FakeQuantOperator*>(fakequant_base_op);
if (!fakequant_op->minmax) {
return absl::OkStatus();
}
const auto& output_array = model->GetArray(fakequant_op->outputs[0]);
if (!output_array.minmax) {
return absl::OkStatus();
}
// Drop min/max inputs
for (int i = 1, end = fakequant_op->inputs.size(); i < end; i++) {
if (CountOpsWithInput(*model, fakequant_op->inputs[i]) == 1) {
model->EraseArray(fakequant_op->inputs[i]);
}
}
fakequant_op->inputs.resize(1);
*modified = RemoveTrivialPassthroughOp(this, model, op_index);
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,50 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status DropIm2colArrays::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto conv_it = model->operators.begin() + op_index;
if (conv_it->get()->type != OperatorType::kConv) {
return absl::OkStatus();
}
auto* conv_op = static_cast<ConvOperator*>(conv_it->get());
if (conv_op->outputs.size() < 2) {
// Conv op does not have im2col.
return absl::OkStatus();
}
// Drop the im2col array.
CHECK_EQ(conv_op->outputs.size(), 2);
model->EraseArray(conv_op->outputs[1]);
conv_op->outputs.resize(1);
AddMessageF("Dropped an im2col array for %s", LogName(*conv_op));
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,96 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <string>
#include <vector>
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
int GetOutputDepthFromWeights(const Model& model, const Operator& op) {
const std::string& weights_name = op.inputs[1];
const auto& weights_shape = model.GetArray(weights_name).shape();
if (op.type == OperatorType::kConv ||
op.type == OperatorType::kFullyConnected ||
op.type == OperatorType::kTransposeConv) {
return weights_shape.dims(0);
}
if (op.type == OperatorType::kDepthwiseConv) {
return weights_shape.dims(3);
}
LOG(FATAL) << "Unhandled operator type";
return 0;
}
bool CheckOpInputSize(const Operator& op) {
if (op.type == OperatorType::kConv ||
op.type == OperatorType::kFullyConnected ||
op.type == OperatorType::kDepthwiseConv) {
return (op.inputs.size() >= 3);
} else if (op.type == OperatorType::kTransposeConv) {
return (op.inputs.size() >= 4);
}
return true;
}
bool ProcessLinearOperator(Model* model, Operator* op) {
if (CheckOpInputSize(*op)) {
return false;
}
const std::string& output_name = op->outputs[0];
const std::string& weights_name = op->inputs[1];
if (!model->GetArray(weights_name).has_shape()) {
return false;
}
const int depth = GetOutputDepthFromWeights(*model, *op);
const std::string& bias_name =
AvailableArrayName(*model, output_name + "_bias");
op->inputs.push_back(bias_name);
auto& bias_array = model->GetOrCreateArray(bias_name);
bias_array.data_type = ArrayDataType::kFloat;
bias_array.mutable_shape()->mutable_dims()->push_back(depth);
auto& bias_buffer = bias_array.GetMutableBuffer<ArrayDataType::kFloat>();
bias_buffer.data.resize(depth, 0.f);
return true;
}
} // namespace
absl::Status EnsureBiasVectors::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto* op = model->operators[op_index].get();
if (op->type == OperatorType::kConv ||
op->type == OperatorType::kDepthwiseConv ||
op->type == OperatorType::kFullyConnected ||
op->type == OperatorType::kTransposeConv) {
if (ProcessLinearOperator(model, op)) {
AddMessageF("Added bias vector to %s as %s", LogName(*op), op->inputs[2]);
*modified = true;
return absl::OkStatus();
}
}
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,228 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <iostream>
#include <memory>
#include <ostream>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/runtime/types.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
// === Summary ===
//
// TLDR: Some of our 8-bit arithmetic operations require uint8 weight values
// to avoid the value 0, thus ranging only in [1, 255]. This enables faster
// runtime arithmetic kernels on ARM NEON. This is not relevant on most
// other hardware architectures, and will cease to be relevant on ARM NEON
// in the future. These topics are elaborated below ("Context").
//
// Having just one isolated uint8 value equal to 0 is fine. The bad case is when
// two uint8 values are both zero and are less than 16 bytes apart.
//
// By default, toco generates a fatal error when that happens. The user may opt
// in to more lax behavior by passing
// --allow_nudging_weights_to_use_fast_gemm_kernel.
// This causes toco to nudge such bad 0 values into the value 1, thus avoiding
// the problem in exchange for compromising on accuracy.
//
// The present graph transformation implements both the default fatal-erroring
// behavior, and, when allow_nudging_weights is set, also the lax nudging
// behavior.
//
//
// === Context ===
//
// Since March 2017, we have been using a trick to perform faster
// 8bit matrix multiplications, to our knowledge first implemented in gemmlowp
// here:
// https://github.com/google/gemmlowp/commit/25b2989415b99e797e1ab977837111b2e231f81f
//
// This trick is explained in Appendix B of our paper,
// https://arxiv.org/abs/1712.05877
//
// Here is the relevant paragraph:
//
// For efficient NEON implementation of the matrix multiplications
// core accumulation, we use the following trick.
// In the multiply-add operation in (10), we first change the
// operands type from uint8 to int8 (which can be done by
// subtracting 128 from the quantized values and zero-points).
// Thus the core multiply-add becomes
//
// int32 += int8 * int8. (B.1)
//
// As mentioned in section 3, with a minor tweak of the quantized
// training process, we can ensure that the weights, once
// quantized as int8 values, never take the value 128. Hence,
// the product in (B.1) is never 128 128, and is therefore
// always less than 2^14 in absolute value. Hence, (B.1)
// can accumulate two products on a local int16 accumulator
// before that needs to be accumulated into the true int32 accumulator.
// This allows the use of an 8-way SIMD multiplication
// (SMULL on int8 operands), followed by an 8-way
// SIMD multiply-add (SMLAL on int8 operands), followed
// by a pairwise-add-and-accumulate into the int32 accumulators
// (SADALP).
//
// As that paragraph notes, quantized training should be suitably modified to
// ensure that quantized uint8 weights value only range in [1, 255]. So the
// problem that we are dealing with is only about the existing 8-bit quantized
// models that haven't been trained specifically to get 8-bit weights only in
// [1, 255].
//
// This spreadsheet shows the speed benefit of this trick across many existing
// ARM-architecture CPUs:
//
// https://docs.google.com/spreadsheets/d/1-0LjdMvW0XtH1bYknC0bQINoFaxjTuL9eplZZcitykI/edit?usp=sharing
//
// Compare Row 18 (fast int8 trick) to Row 20 (regular uint8 kernel).
//
// The introduction of the 'dotprod' extension to ARM NEON, specifically the
// SDOT instruction, renders this eventually moot. See the experimental
// kernels contributed by ARM here,
//
// https://github.com/google/gemmlowp/pull/116
//
// However, as of April 2018, there don't seem to be any commercially available
// CPU supporting these instructions (yet); we are waiting for
// Cortex-A{75,55}-r1 to become available; the "-r1" is key here. Even if such
// CPUs become available soon, it will presumably take years for them to
// overtake the large volume of existing CPUs not supporting these new
// instructions, especially in current and future low-end devices. All in all,
// we can foresee these 'fast int8 kernels' to remain important to have into
// the 2020s.
//
absl::Status EnsureUint8WeightsSafeForFastInt8Kernels::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
const auto& op = *model->operators[op_index];
int weights_index = 0;
switch (op.type) {
case OperatorType::kConv:
weights_index = 1;
break;
case OperatorType::kLstmCell:
weights_index = 2;
break;
case OperatorType::kFullyConnected: {
weights_index = 1;
const auto& fc_op = static_cast<const toco::FullyConnectedOperator&>(op);
CHECK(fc_op.weights_format == FullyConnectedWeightsFormat::kDefault)
<< "This graph transformation expects to run before FC weights get "
"shuffled.";
break;
}
default:
// Other operator types are unaffected by this graph transformation,
// because their runtime implementations don't use the fast int8 trick.
// In particular that's the case of DepthwiseConv at the moment.
// We have to update this logic when that changes, e.g. if in the future
// some DepthwiseConv kernel wants to use the trick.
//
// The reason why that's not so likely, hence why it's fairly safe to
// stay conservative in the list of operators that we handle here, is that
// the fast int8 kernel trick is only applicable to ops that either are
// implemented as a GEMM, or use symmetric ranges for both weights and
// activations. The reason why GEMM is special (can use the trick even
// without symmetric ranges) is that it is so arithmetic-intense that
// it can use techniques reducing its implementation to the symmetric
// ranges case, with limited relative overhead (O(N^2) overhead vs
// O(N^3) GEMM cost). See https://arxiv.org/pdf/1712.05877, section
// 2.3 Efficient handling of zero-points.
//
// That's why at the moment we only handle operators that use a GEMM
// (Conv, fully-connected --- note that LSTM merely wraps a
// fully-connected operator).
return absl::OkStatus();
}
const std::string& name = op.inputs[weights_index];
auto& array = model->GetArray(name);
if (!array.buffer) {
return absl::OkStatus();
}
if (array.data_type != ArrayDataType::kUint8) {
return absl::OkStatus();
}
auto& buffer_data = array.GetMutableBuffer<ArrayDataType::kUint8>().data;
int count_bad = 0;
int index_of_previous_bad_value = 0;
bool changed = false;
for (int i = 0, end = buffer_data.size(); i < end; i++) {
if (buffer_data[i] == 0) {
count_bad++;
if (count_bad > 1) {
const int distance = i - index_of_previous_bad_value;
// Semi-arbitrary threshold. The idea is that trouble only occurs
// when two bad values are very close to each other so that they
// are jointly used within registers inside some GEMM kernel.
// The details of that depend on the kernel. Our current fast ARM64
// kernel, for instance, only has an issue when the distance between
// consecutive bad values is exactly 8. We do not want to track such
// kernel details too closely here, so we pick a threshold that's
// a bit larger than that, to give us room to change kernels in the
// future without worrying.
static constexpr int kMinDistanceBetweenBadValues = 16;
if (distance < kMinDistanceBetweenBadValues) {
if (allow_nudging_weights() || has_default_ranges_flag()) {
buffer_data[i] = 1;
changed = true;
continue;
}
LOG(FATAL) << "Bad value for " << name << " at index " << i
<< ", previous bad value at index "
<< index_of_previous_bad_value << ", distance=" << distance
<< ", kMinDistanceBetweenBadValues="
<< kMinDistanceBetweenBadValues << ". Consider passing "
<< "--allow_nudging_weights_to_use_fast_gemm_kernel "
<< "if you don't care about accuracy.";
}
}
index_of_previous_bad_value = i;
}
}
if (changed) {
if (has_default_ranges_flag()) {
std::cerr
<< "Since the specified values of --default_ranges_min and "
"--default_ranges_max result in values incompatible with TFLite's "
"fast int8 kernels, "
"--allow_nudging_weights_to_use_fast_gemm_kernel "
"has been enabled. This may affect the accuracy of the model."
<< std::endl;
}
AddMessageF("Tweaked weights values for %s", LogName(op));
}
*modified = changed;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,109 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/runtime/types.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status FuseActivationFunctions::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto ac_it = model->operators.begin() + op_index;
const auto* ac_op = ac_it->get();
if (ac_op->type != OperatorType::kRelu6 &&
ac_op->type != OperatorType::kRelu1 &&
ac_op->type != OperatorType::kRelu) {
return absl::OkStatus();
}
// Find the op producing the array passed to this activation function
Operator* op = GetOpWithOutput(*model, ac_op->inputs[0]);
if (!op) return absl::OkStatus();
if (CountTrueOutputs(*model, *op) > 1) {
AddMessageF(
"Not fusing activation function %s into %s because it has more than "
"one consumed output",
LogName(*ac_op), LogName(*op));
return absl::OkStatus();
}
CHECK_EQ(op->outputs[0], ac_op->inputs[0]);
int count_ops_consuming_output = CountOpsWithInput(*model, ac_op->inputs[0]);
DCHECK_GE(count_ops_consuming_output, 1);
if (count_ops_consuming_output > 1) {
AddMessageF(
"Not fusing activation function %s into %s because it is consumed by "
"more than 1 other operator",
LogName(*ac_op), LogName(*op));
return absl::OkStatus();
}
if (!IsDiscardableArray(*model, op->outputs[0])) {
AddMessageF(
"Not fusing activation function %s into %s because output %s it is not "
"discardable",
LogName(*ac_op), LogName(*op), op->outputs[0]);
return absl::OkStatus();
}
if (op->fused_activation_function != FusedActivationFunctionType::kNone) {
AddMessageF(
"Not fusing activation function %s into %s because it already has a "
"fused activation function",
LogName(*ac_op), LogName(*op));
return absl::OkStatus();
}
if (!OperatorSupportsFusedActivation(op->type)) {
AddMessageF(
"Not fusing activation function %s because the %s op doesn't support "
"it",
LogName(*ac_op), LogName(*op));
return absl::OkStatus();
}
AddMessageF("Fusing activation function %s into the preceding %s",
LogName(*ac_op), LogName(*op));
if (ac_op->type == OperatorType::kRelu6) {
op->fused_activation_function = FusedActivationFunctionType::kRelu6;
} else if (ac_op->type == OperatorType::kRelu1) {
op->fused_activation_function = FusedActivationFunctionType::kRelu1;
} else if (ac_op->type == OperatorType::kRelu) {
op->fused_activation_function = FusedActivationFunctionType::kRelu;
} else {
LOG(FATAL) << "Unhandled activation function type";
}
op->outputs[0] = ac_op->outputs[0];
DeleteOpAndArrays(model, ac_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,306 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/runtime/types.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
void FuseAddOrSubParamsIntoFollowingAffine(Model* model, Operator* following_op,
const Operator* add_or_sub_op,
int index_of_constant_input) {
CHECK(add_or_sub_op->type == OperatorType::kAdd ||
add_or_sub_op->type == OperatorType::kSub);
CHECK(index_of_constant_input == 0 || index_of_constant_input == 1);
// If the op is a subtraction, the constant input should be the right hand
// side.
// This should have been checked before this point.
CHECK(add_or_sub_op->type != OperatorType::kSub ||
index_of_constant_input == 1);
if (following_op->inputs.size() < 3) {
LOG(FATAL) << "Missing bias parameter";
}
const auto& weights = model->GetArray(following_op->inputs[1]);
auto& bias = model->GetArray(following_op->inputs[2]);
bias.minmax = nullptr;
const auto& operand =
model->GetArray(add_or_sub_op->inputs[index_of_constant_input]);
// We're only supporting the case of a scalar operand. Should have
// been checked earlier.
CHECK_EQ(RequiredBufferSizeForShape(operand.shape()), 1);
const float scalar_operand =
operand.GetBuffer<ArrayDataType::kFloat>().data[0];
// At this point we reduce the case of subtraction to that of addition
// by negating the operand.
float add_scalar_operand = 0.f;
if (add_or_sub_op->type == OperatorType::kAdd) {
add_scalar_operand = scalar_operand;
} else if (add_or_sub_op->type == OperatorType::kSub &&
index_of_constant_input == 1) {
add_scalar_operand = -scalar_operand;
} else {
LOG(FATAL) << "Should not get here";
}
// From here on we are fusing an addition. add_or_sub_op->type does not
// matter anymore.
const Shape& weights_shape = weights.shape();
const Shape& bias_shape = bias.shape();
const auto& weights_buffer = weights.GetBuffer<ArrayDataType::kFloat>();
const float* const weights_data = weights_buffer.data.data();
auto& bias_buffer = bias.GetMutableBuffer<ArrayDataType::kFloat>();
float* const bias_data = bias_buffer.data.data();
if (following_op->type == OperatorType::kConv ||
following_op->type == OperatorType::kFullyConnected) {
const int output_depth = weights_shape.dims(0);
// TODO(b/62904716): Bias array should become 1-D when padding removed.
CHECK_EQ(output_depth, bias_shape.dims(bias_shape.dimensions_count() - 1));
const int weights_size = RequiredBufferSizeForShape(weights_shape);
const int weights_per_depth = weights_size / output_depth;
CHECK_EQ(weights_size, weights_per_depth * output_depth);
for (int d = 0; d < output_depth; d++) {
float accumulation = 0;
for (int i = 0; i < weights_per_depth; i++) {
accumulation +=
add_scalar_operand * weights_data[d * weights_per_depth + i];
}
bias_data[d] += accumulation;
}
} else if (following_op->type == OperatorType::kDepthwiseConv) {
const int output_depth =
weights_shape.dims(weights_shape.dimensions_count() - 1);
const int weights_size = RequiredBufferSizeForShape(weights_shape);
const int weights_per_depth = weights_size / output_depth;
CHECK_EQ(weights_size, weights_per_depth * output_depth);
for (int c = 0; c < output_depth; c++) {
float accumulation = 0;
for (int k = 0; k < weights_per_depth; k++) {
accumulation += add_scalar_operand * weights_data[k * output_depth + c];
}
bias_data[c] += accumulation;
}
} else {
LOG(FATAL) << "Should not get here.";
}
}
void FuseMulOrDivParamsIntoFollowingAffine(Model* model, Operator* following_op,
const Operator* mul_or_div_op,
int index_of_constant_input) {
CHECK(mul_or_div_op->type == OperatorType::kMul ||
mul_or_div_op->type == OperatorType::kDiv);
CHECK(index_of_constant_input == 0 || index_of_constant_input == 1);
// If the op is a division, the constant input should be the right hand side.
// This should have been checked before this point.
CHECK(mul_or_div_op->type != OperatorType::kDiv ||
index_of_constant_input == 1);
const auto& weights_name = following_op->inputs[1];
const auto& bias_name = following_op->inputs[2];
auto& weights = model->GetArray(weights_name);
DropMinMax(model, weights_name);
DropMinMax(model, bias_name);
const auto& operand =
model->GetArray(mul_or_div_op->inputs[index_of_constant_input]);
// We're only supporting the case of a scalar operand. Should have
// been checked earlier.
CHECK_EQ(RequiredBufferSizeForShape(operand.shape()), 1);
const float scalar_operand =
operand.GetBuffer<ArrayDataType::kFloat>().data[0];
float* weights_data =
weights.GetMutableBuffer<ArrayDataType::kFloat>().data.data();
const int weights_size = RequiredBufferSizeForShape(weights.shape());
for (int i = 0; i < weights_size; i++) {
if (mul_or_div_op->type == OperatorType::kMul) {
weights_data[i] *= scalar_operand;
} else if (mul_or_div_op->type == OperatorType::kDiv) {
weights_data[i] /= scalar_operand;
} else {
LOG(FATAL) << "Should not get here";
}
}
}
} // namespace
absl::Status FuseBinaryIntoFollowingAffine::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
const auto binary_it = model->operators.begin() + op_index;
auto* binary_op = binary_it->get();
if (binary_op->type != OperatorType::kAdd &&
binary_op->type != OperatorType::kMul &&
binary_op->type != OperatorType::kSub &&
binary_op->type != OperatorType::kDiv) {
return absl::OkStatus();
}
CHECK_EQ(binary_op->inputs.size(), 2);
// We only can fuse an binary when the two operands break down as follows:
// 1. One operand is the (variable) output of a typical affine (linear plus
// bias)
// op of a finite list of possible types: at the moment Conv,
// DepthwiseConv and
// FullyConnected are supported.
// 2. The other operand is a constant param array.
const bool is_input_constant[2] = {
IsConstantParameterArray(*model, binary_op->inputs[0]),
IsConstantParameterArray(*model, binary_op->inputs[1]),
};
if (!is_input_constant[0] && !is_input_constant[1]) {
// Neither input is constant, so nothing we can fuse into a constant.
return absl::OkStatus();
}
if (is_input_constant[0] && is_input_constant[1]) {
// Both inputs are constants. That's a job for constants
// propagation, not for us to handle here.
return absl::OkStatus();
}
const int index_of_constant_input = is_input_constant[0] ? 0 : 1;
const int index_of_variable_input = is_input_constant[0] ? 1 : 0;
CHECK(is_input_constant[index_of_constant_input]);
CHECK(!is_input_constant[index_of_variable_input]);
// For division, we can only fuse if the denominator is constant.
if (binary_op->type == OperatorType::kDiv) {
if (index_of_constant_input != 1) {
AddMessageF("Not fusing %s because the denominator is not constant",
LogName(*binary_op));
return absl::OkStatus();
}
}
const auto& operand_shape =
model->GetArray(binary_op->inputs[index_of_constant_input]).shape();
for (const auto& dim : operand_shape.dims()) {
if (dim > 1) {
AddMessageF(
"Not fusing %s into the following affine op, because we only know "
"how to do so when the constant operand is a scalar",
LogName(*binary_op));
return absl::OkStatus();
}
}
if (binary_op->fused_activation_function !=
FusedActivationFunctionType::kNone) {
AddMessageF("Not fusing %s because it has a fused activation function",
LogName(*binary_op));
return absl::OkStatus();
}
if (CountOpsWithInput(*model, binary_op->outputs[0]) != 1) {
AddMessageF("Not fusing %s because it's consumed by multiple ops",
LogName(*binary_op));
return absl::OkStatus();
}
Operator* following_op = GetOpWithInput(*model, binary_op->outputs[0]);
if (!following_op) {
AddMessageF("Not fusing %s because it is not consumed by any op",
LogName(*binary_op));
return absl::OkStatus();
}
if (following_op->type != OperatorType::kConv &&
following_op->type != OperatorType::kFullyConnected &&
following_op->type != OperatorType::kDepthwiseConv) {
AddMessageF(
"Not fusing %s because the following %s is not of one of the supported "
"types",
LogName(*binary_op), LogName(*following_op));
return absl::OkStatus();
}
if (following_op->inputs.size() < 3) {
AddMessageF(
"Not fusing %s because the following %s does not have a bias vector",
LogName(*following_op), LogName(*binary_op));
return absl::OkStatus();
}
const auto& weights = model->GetArray(following_op->inputs[1]);
const auto& bias = model->GetArray(following_op->inputs[2]);
if (!weights.buffer || !bias.buffer) {
AddMessageF(
"Not fusing %s because the following %s has non-constant weights or "
"bias arrays",
LogName(*binary_op), LogName(*following_op));
return absl::OkStatus();
}
// Try to fuse the binary params into the following op's params
if (binary_op->type == OperatorType::kAdd ||
binary_op->type == OperatorType::kSub) {
if (following_op->type == OperatorType::kConv) {
if (static_cast<ConvOperator*>(following_op)->padding.type !=
PaddingType::kValid) {
AddMessageF(
"Not fusing %s because the following %s does not use VALID padding",
LogName(*binary_op), LogName(*following_op));
return absl::OkStatus();
}
}
if (following_op->type == OperatorType::kDepthwiseConv) {
if (static_cast<DepthwiseConvOperator*>(following_op)->padding.type !=
PaddingType::kValid) {
AddMessageF(
"Not fusing %s because the following %s does not use VALID padding",
LogName(*binary_op), LogName(*following_op));
return absl::OkStatus();
}
}
FuseAddOrSubParamsIntoFollowingAffine(model, following_op, binary_op,
index_of_constant_input);
} else if (binary_op->type == OperatorType::kMul ||
binary_op->type == OperatorType::kDiv) {
FuseMulOrDivParamsIntoFollowingAffine(model, following_op, binary_op,
index_of_constant_input);
} else {
LOG(FATAL) << "should not get here";
}
AddMessageF("Fusing %s into the following %s", LogName(*binary_op),
LogName(*following_op));
model->EraseArray(binary_op->outputs[0]);
following_op->inputs[0] = binary_op->inputs[index_of_variable_input];
DeleteOpAndArrays(model, binary_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,396 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/runtime/types.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
int GetBiasIndex(const Operator& op) {
if (op.type == OperatorType::kConv ||
op.type == OperatorType::kFullyConnected ||
op.type == OperatorType::kDepthwiseConv) {
return 2;
} else if (op.type == OperatorType::kTransposeConv) {
return 3;
}
LOG(FATAL) << "Unhandled operator type";
return 0;
}
void FuseAddOrSubParamsIntoPrecedingAffine(Model* model, Operator* preceding_op,
const Operator* add_or_sub_op,
int index_of_constant_input) {
CHECK(add_or_sub_op->type == OperatorType::kAdd ||
add_or_sub_op->type == OperatorType::kSub);
CHECK(index_of_constant_input == 0 || index_of_constant_input == 1);
if (preceding_op->inputs.size() < 3) {
LOG(FATAL) << "Missing bias parameter";
}
const auto bias_ind = GetBiasIndex(*preceding_op);
auto& bias = model->GetArray(preceding_op->inputs[bias_ind]);
bias.minmax = nullptr;
const auto& operand =
model->GetArray(add_or_sub_op->inputs[index_of_constant_input]);
const Shape& bias_shape = bias.shape();
const Shape& operand_shape = operand.shape();
auto& bias_buffer = bias.GetMutableBuffer<ArrayDataType::kFloat>();
float* const bias_data = bias_buffer.data.data();
const auto& operand_buffer = operand.GetBuffer<ArrayDataType::kFloat>();
const float* const operand_data = operand_buffer.data.data();
// TODO(b/62904716): Bias array should become 1-D when padding removed.
const int depth = bias_shape.dims(bias_shape.dimensions_count() - 1);
int operand_channel_increment = 0;
if (operand_shape.dimensions_count() >= 1 &&
operand_shape.dims(operand_shape.dimensions_count() - 1) ==
bias_shape.dims(bias_shape.dimensions_count() - 1)) {
operand_channel_increment = 1;
} else if (operand_shape.dimensions_count() == 0 ||
operand_shape.dims(operand_shape.dimensions_count() - 1) == 1) {
operand_channel_increment = 0;
} else {
LOG(FATAL) << "Operand shape mismatch.";
}
enum class OpType { BiasPlusOperand, BiasMinusOperand, OperandMinusBias };
const OpType optype = (add_or_sub_op->type == OperatorType::kAdd)
? OpType::BiasPlusOperand
: (index_of_constant_input == 1)
? OpType::BiasMinusOperand
: OpType::OperandMinusBias;
int operand_channel = 0;
for (int i = 0; i < depth; i++) {
float& bias_val = bias_data[i];
const float operand_val = operand_data[operand_channel];
if (optype == OpType::BiasPlusOperand) {
bias_val += operand_val;
} else if (optype == OpType::BiasMinusOperand) {
bias_val -= operand_val;
} else if (optype == OpType::OperandMinusBias) {
bias_val = operand_val - bias_val;
} else {
LOG(FATAL) << "Should not get here.";
}
operand_channel += operand_channel_increment;
}
}
void FuseMulOrDivParamsIntoPrecedingAffine(Model* model, Operator* preceding_op,
const Operator* mul_or_div_op,
int index_of_constant_input) {
CHECK(mul_or_div_op->type == OperatorType::kMul ||
mul_or_div_op->type == OperatorType::kDiv);
CHECK(index_of_constant_input == 0 || index_of_constant_input == 1);
// If the op is a division, the constant input should be the right hand side.
// This should have been checked before this point.
CHECK(mul_or_div_op->type != OperatorType::kDiv ||
index_of_constant_input == 1);
if (preceding_op->inputs.size() < 3) {
LOG(FATAL) << "Missing bias parameter";
}
const auto& weights_name = preceding_op->inputs[1];
const auto bias_ind = GetBiasIndex(*preceding_op);
const auto& bias_name = preceding_op->inputs[bias_ind];
auto& weights = model->GetArray(weights_name);
DropMinMax(model, weights_name);
auto& bias = model->GetArray(bias_name);
DropMinMax(model, bias_name);
const auto& operand =
model->GetArray(mul_or_div_op->inputs[index_of_constant_input]);
const Shape& weights_shape = weights.shape();
const Shape& bias_shape = bias.shape();
const Shape& operand_shape = operand.shape();
auto& weights_buffer = weights.GetMutableBuffer<ArrayDataType::kFloat>();
float* const weights_data = weights_buffer.data.data();
auto& bias_buffer = bias.GetMutableBuffer<ArrayDataType::kFloat>();
float* const bias_data = bias_buffer.data.data();
const auto& operand_buffer = operand.GetBuffer<ArrayDataType::kFloat>();
const float* const operand_data = operand_buffer.data.data();
// We support broadcasting the operand along the depth dimension,
// when the operand's depth is 1.
int operand_channel_increment = 0;
if (operand_shape.dimensions_count() >= 1 &&
operand_shape.dims(operand_shape.dimensions_count() - 1) ==
bias_shape.dims(bias_shape.dimensions_count() - 1)) {
operand_channel_increment = 1;
} else if (operand_shape.dimensions_count() == 0 ||
operand_shape.dims(operand_shape.dimensions_count() - 1) == 1) {
operand_channel_increment = 0;
} else {
LOG(FATAL) << "Operand shape mismatch.";
}
int output_depth;
if (preceding_op->type == OperatorType::kConv ||
preceding_op->type == OperatorType::kFullyConnected ||
preceding_op->type == OperatorType::kTransposeConv) {
output_depth = weights_shape.dims(0);
} else if (preceding_op->type == OperatorType::kDepthwiseConv) {
output_depth = weights_shape.dims(weights_shape.dimensions_count() - 1);
} else {
LOG(FATAL) << "Should not get here";
}
const int weights_size = RequiredBufferSizeForShape(weights_shape);
const int weights_per_depth = weights_size / output_depth;
CHECK_EQ(weights_size, weights_per_depth * output_depth);
int operand_channel = 0;
for (int c = 0; c < output_depth; c++) {
if (mul_or_div_op->type == OperatorType::kMul) {
bias_data[c] *= operand_data[operand_channel];
} else if (mul_or_div_op->type == OperatorType::kDiv) {
bias_data[c] /= operand_data[operand_channel];
} else {
LOG(FATAL) << "Should not get here";
}
if (preceding_op->type == OperatorType::kConv ||
preceding_op->type == OperatorType::kFullyConnected) {
for (int i = 0; i < weights_per_depth; i++) {
if (mul_or_div_op->type == OperatorType::kMul) {
weights_data[c * weights_per_depth + i] *=
operand_data[operand_channel];
} else if (mul_or_div_op->type == OperatorType::kDiv) {
weights_data[c * weights_per_depth + i] /=
operand_data[operand_channel];
} else {
LOG(FATAL) << "Should not get here";
}
}
} else if (preceding_op->type == OperatorType::kDepthwiseConv) {
for (int k = 0; k < weights_per_depth; k++) {
if (mul_or_div_op->type == OperatorType::kMul) {
weights_data[k * output_depth + c] *= operand_data[operand_channel];
} else if (mul_or_div_op->type == OperatorType::kDiv) {
weights_data[k * output_depth + c] /= operand_data[operand_channel];
} else {
LOG(FATAL) << "Should not get here";
}
}
} else {
LOG(FATAL) << "Should not get here";
}
operand_channel += operand_channel_increment;
}
}
} // namespace
absl::Status FuseBinaryIntoPrecedingAffine::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
const auto binary_it = model->operators.begin() + op_index;
const auto* binary_op = binary_it->get();
if (binary_op->type != OperatorType::kAdd &&
binary_op->type != OperatorType::kMul &&
binary_op->type != OperatorType::kSub &&
binary_op->type != OperatorType::kDiv) {
return absl::OkStatus();
}
CHECK_EQ(binary_op->inputs.size(), 2);
// We only can fuse an binary when the two operands break down as follows:
// 1. One operand is the (variable) output of a typical affine (linear plus
// bias)
// op of a finite list of possible types: at the moment Conv,
// DepthwiseConv and
// FullyConnected are supported.
// 2. The other operand is a constant param array.
const bool is_input_constant[2] = {
IsConstantParameterArray(*model, binary_op->inputs[0]),
IsConstantParameterArray(*model, binary_op->inputs[1]),
};
if (!is_input_constant[0] && !is_input_constant[1]) {
// Neither input is constant, so nothing we can fuse into a constant.
return absl::OkStatus();
}
if (is_input_constant[0] && is_input_constant[1]) {
// Both inputs are constants. That's a job for constants
// propagation, not for us to handle here.
return absl::OkStatus();
}
const int index_of_constant_input = is_input_constant[0] ? 0 : 1;
const int index_of_variable_input = is_input_constant[0] ? 1 : 0;
CHECK(is_input_constant[index_of_constant_input]);
CHECK(!is_input_constant[index_of_variable_input]);
// For division, we can only fuse if the denominator is constant.
if (binary_op->type == OperatorType::kDiv) {
if (index_of_constant_input != 1) {
AddMessageF("Not fusing %s because the denominator is not constant",
LogName(*binary_op));
return absl::OkStatus();
}
}
Operator* preceding_op =
GetOpWithOutput(*model, binary_op->inputs[index_of_variable_input]);
if (!preceding_op) {
AddMessageF("Not fusing %s because it is not the output of another op",
LogName(*binary_op));
return absl::OkStatus();
}
for (const std::string& output_array : model->flags.output_arrays()) {
if (preceding_op->outputs[0] == output_array) {
return absl::OkStatus();
}
}
if (preceding_op->type != OperatorType::kConv &&
preceding_op->type != OperatorType::kFullyConnected &&
preceding_op->type != OperatorType::kDepthwiseConv &&
preceding_op->type != OperatorType::kTransposeConv) {
AddMessageF(
"Not fusing %s because the preceding %s is not of one of the supported "
"types",
LogName(*binary_op), LogName(*preceding_op));
return absl::OkStatus();
}
if (preceding_op->type == OperatorType::kTransposeConv &&
binary_op->type != OperatorType::kAdd) {
AddMessageF("Not fusing %s to preceding %s", LogName(*binary_op),
LogName(*preceding_op));
return absl::OkStatus();
}
if (preceding_op->fused_activation_function !=
FusedActivationFunctionType::kNone) {
AddMessageF(
"Not fusing %s because the preceding %s has a fused activation "
"function",
LogName(*binary_op), LogName(*preceding_op));
return absl::OkStatus();
}
if (preceding_op->inputs.size() < 3) {
AddMessageF(
"Not fusing %s because the preceding %s does not have a bias vector",
LogName(*binary_op), LogName(*preceding_op));
return absl::OkStatus();
}
const auto& weights_name = preceding_op->inputs[1];
const auto bias_ind = GetBiasIndex(*preceding_op);
const auto& bias_name = preceding_op->inputs[bias_ind];
const auto& weights = model->GetArray(weights_name);
const auto& bias = model->GetArray(bias_name);
if (weights.data_type != ArrayDataType::kFloat ||
bias.data_type != ArrayDataType::kFloat) {
AddMessageF(
"Not fusing %s into preceding %s because one of weights or bias array "
"is not float (types are %s and %s)",
LogName(*binary_op), LogName(*preceding_op),
ArrayDataTypeName(weights.data_type),
ArrayDataTypeName(bias.data_type));
return absl::OkStatus();
}
const int count_ops_consuming_bias = CountOpsWithInput(*model, bias_name);
const int count_ops_consuming_weights =
CountOpsWithInput(*model, weights_name);
if (binary_op->type == OperatorType::kAdd ||
binary_op->type == OperatorType::kSub) {
if (!bias.buffer) {
AddMessageF(
"Not fusing %s because the preceding %s has a non-constant bias "
"array",
LogName(*binary_op), LogName(*preceding_op));
return absl::OkStatus();
}
if (count_ops_consuming_bias > 1) {
AddMessageF(
"Not fusing %s because the bias of the preceding %s is consumed by "
"another op",
LogName(*binary_op), LogName(*preceding_op));
return absl::OkStatus();
}
} else {
if (!weights.buffer || !bias.buffer) {
AddMessageF(
"Not fusing %s because the preceding %s has non-constant weights or "
"bias arrays",
LogName(*binary_op), LogName(*preceding_op));
return absl::OkStatus();
}
if (count_ops_consuming_weights > 1 || count_ops_consuming_bias > 1) {
AddMessageF(
"Not fusing %s because the weights or bias of the preceding %s is "
"consumed by another op",
LogName(*binary_op), LogName(*preceding_op));
return absl::OkStatus();
}
}
int count_ops_consuming_output =
CountOpsWithInput(*model, preceding_op->outputs[0]);
DCHECK_GE(count_ops_consuming_output, 1);
if (count_ops_consuming_output > 1) {
AddMessageF(
"Not fusing %s because the output of the preceding %s is consumed by "
"another op",
LogName(*binary_op), LogName(*preceding_op));
return absl::OkStatus();
}
AddMessageF("Fusing %s into the preceding %s", LogName(*binary_op),
LogName(*preceding_op));
if (binary_op->type == OperatorType::kAdd ||
binary_op->type == OperatorType::kSub) {
FuseAddOrSubParamsIntoPrecedingAffine(model, preceding_op, binary_op,
index_of_constant_input);
} else if (binary_op->type == OperatorType::kMul ||
binary_op->type == OperatorType::kDiv) {
FuseMulOrDivParamsIntoPrecedingAffine(model, preceding_op, binary_op,
index_of_constant_input);
} else {
LOG(FATAL) << "should not get here";
}
model->EraseArray(preceding_op->outputs[0]);
preceding_op->outputs[0] = binary_op->outputs[0];
preceding_op->fused_activation_function =
binary_op->fused_activation_function;
DeleteOpAndArrays(model, binary_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,107 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
// Returns true if the given op is strictly a broadcasting operation.
// This is commonly seen as a Concat of the same input multiple times, and is
// often generated from Tile ops that were converted via the
// convert_trivial_tile_to_concat transformation.
bool IsBroadcastingOp(const Model& model, Operator* op) {
// Concatenation of identical inputs is usually a broadcast.
if (op->type == OperatorType::kConcatenation) {
// Verify that all inputs are the same.
for (size_t i = 1; i < op->inputs.size(); ++i) {
if (op->inputs[i] != op->inputs[0]) {
return false;
}
}
return true;
}
// There are other things we could look for (Stack/etc) when needed.
return false;
}
} // namespace
// Finds an operation that looks like a broadcast (concat of the same sources
// along the last dimension) and drops it by relying on the ability of certain
// binary ops to perform an implicit broadcast.
absl::Status FuseBroadcastIntoFollowingBinary::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
const auto binary_it = model->operators.begin() + op_index;
auto* binary_op = binary_it->get();
// Test for binary ops of types that we know how to resolve
if (binary_op->inputs.size() != 2) {
return absl::OkStatus();
}
if (binary_op->type != OperatorType::kAdd &&
binary_op->type != OperatorType::kMul &&
binary_op->type != OperatorType::kSub &&
binary_op->type != OperatorType::kDiv) {
return absl::OkStatus();
}
// NOTE: either of these ops may be nullptr if the input array is constant.
Operator* const op[2] = {
GetOpWithOutput(*model, binary_op->inputs[0]),
GetOpWithOutput(*model, binary_op->inputs[1]),
};
// Check whether either input is a broadcast-like concat.
bool is_op_0_broadcast = op[0] && IsBroadcastingOp(*model, op[0]);
bool is_op_1_broadcast = op[1] && IsBroadcastingOp(*model, op[1]);
if (!is_op_0_broadcast && !is_op_1_broadcast) {
// Neither input is a broadcast-looking thing.
AddMessageF("Neither input looks broadcasty");
return absl::OkStatus();
} else if (is_op_0_broadcast && is_op_1_broadcast) {
AddMessageF(
"Unable to fuse broadcast into %s as both inputs (%s, %s) are "
"broadcasts",
LogName(*binary_op), op[0] ? LogName(*op[0]) : "(?)",
op[1] ? LogName(*op[1]) : "(?)");
return absl::OkStatus();
}
int broadcast_index = is_op_0_broadcast ? 0 : 1;
// Just pull out the input of the broadcast op and pass it directly to the
// binary op.
AddMessageF("Fusing broadcast op %s into the following binary %s",
LogName(*op[broadcast_index]), LogName(*binary_op));
binary_op->inputs[broadcast_index] = op[broadcast_index]->inputs[0];
// We leave the broadcast op in; it'll get cleaned up if it's not used later.
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,216 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include <algorithm>
#include <memory>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/format_port.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/model_flags.pb.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
void PrintModelStats(const std::string& label, const Model& model) {
int quantized_arrays = 0;
for (const auto& array : model.GetArrayMap()) {
if (array.second->quantization_params) {
quantized_arrays++;
}
}
LOG(INFO) << label << ": " << model.operators.size() << " operators, "
<< model.GetArrayMap().size() << " arrays (" << quantized_arrays
<< " quantized)";
}
// Some graphs have RNN back-edges that are discardable, having been
// created typically by TensorFlow import rather than specified by the user.
// Such graphs might have cycles (closed by RNN back-edges) that may be pruned.
// Local graph transformations can't identify such global features,
// so this function performs this global transformation.
//
// The other (and related) thing that is peculiar about RNN back-edges
// is that they do not prevent the arrays that they touch, from being
// pruned. Thus, they may refer to array names which no longer exist.
// The intent is for that to result in the eventual pruning of such
// 'dangling' RNN back-edges. We perform this pruning at the end of this
// function, as the pruning of connected components done here may leave
// more RNN back-edges dangling.
void DiscardUselessConnectedComponentsAndRNNBackEdges(Model* model) {
// Identify the set of arrays that are in 'useful' connected components
// of the graph, which means connected to output arrays.
std::unordered_set<std::string> useful_arrays;
for (const std::string& output_array : model->flags.output_arrays()) {
useful_arrays.insert(output_array);
}
bool found_new_useful_arrays;
do {
found_new_useful_arrays = false;
for (const auto& op : model->operators) {
bool op_touches_useful_arrays = false;
for (const std::string& output : op->outputs) {
op_touches_useful_arrays |= useful_arrays.count(output);
}
if (op_touches_useful_arrays) {
for (const std::string& input : op->inputs) {
found_new_useful_arrays |= !useful_arrays.count(input);
useful_arrays.insert(input);
}
for (const std::string& output : op->outputs) {
found_new_useful_arrays |= !useful_arrays.count(output);
useful_arrays.insert(output);
}
}
}
for (const auto& rnn_state : model->flags.rnn_states()) {
bool rnn_back_edge_touches_useful_arrays =
useful_arrays.count(rnn_state.state_array());
if (rnn_back_edge_touches_useful_arrays) {
found_new_useful_arrays |=
!useful_arrays.count(rnn_state.back_edge_source_array());
useful_arrays.insert(rnn_state.back_edge_source_array());
}
}
} while (found_new_useful_arrays);
// Erase arrays that aren't useful, and that are discardable.
model->EraseArrays([&](const std::string& name) {
return (!useful_arrays.count(name) && IsDiscardableArray(*model, name));
});
// Erase operators that do not produce a useful output array.
for (auto it = model->operators.begin(); it != model->operators.end();) {
// Only need to test the first output, as we simultaneously added all of
// an operator's outputs to the list of output arrays.
if (useful_arrays.count((*it)->outputs[0])) {
++it;
} else {
for (const std::string& output : (*it)->outputs) {
CHECK(!useful_arrays.count(output));
}
it = model->operators.erase(it);
}
}
// Erase RNN back-edges that are 'dangling' i.e. that touch an array
// that no longer exists. This should only happen for discardable RNN
// back-edges.
std::vector<RnnState> rnn_states_to_keep;
for (const auto& rnn_state : model->flags.rnn_states()) {
const bool dangling =
!model->HasArray(rnn_state.back_edge_source_array()) ||
!model->HasArray(rnn_state.state_array());
if (dangling) {
CHECK(rnn_state.discardable());
} else {
rnn_states_to_keep.push_back(rnn_state);
}
}
model->flags.clear_rnn_states();
for (const auto& rnn_state : rnn_states_to_keep) {
*model->flags.add_rnn_states() = rnn_state;
}
}
bool GraphTransformationsPass(int increment, Model* model,
const GraphTransformationsSet& transformations,
absl::Status* status) {
CHECK(increment == 1 || increment == -1);
bool changed = false;
if (model->operators.empty()) {
LOG(INFO) << "Model is empty!!!";
return false;
}
int op_index = increment == 1 ? 0 : model->operators.size() - 1;
while (true) {
bool changed_now = false;
// Loop over all transformations at the current position in the graph.
for (const auto& transformation : transformations) {
CHECK(!changed_now);
CHECK(transformation->Messages().empty());
*status = transformation->Run(model, op_index, &changed_now);
if (!status->ok()) {
return false;
}
const char* made_a_change_msg =
changed_now ? "made a change" : "did NOT make a change";
const int log_level =
changed_now ? kLogLevelModelChanged : kLogLevelModelUnchanged;
if (transformation->Messages().empty()) {
VLOG(log_level) << transformation->Name() << " " << made_a_change_msg
<< " at op_index=" << op_index << "/"
<< model->operators.size() - 1;
}
for (const std::string& message : transformation->Messages()) {
VLOG(log_level) << transformation->Name() << " " << made_a_change_msg
<< " at op_index=" << op_index << "/"
<< model->operators.size() - 1 << ": " << message;
}
transformation->ClearMessages();
if (changed_now) {
DumpGraphvizVideoFrame(*model);
if (model->operators.empty()) return true;
op_index = std::min<int>(op_index, model->operators.size() - 1);
// Uncomment for debugging
// CheckInvariants(*model);
}
if (changed_now) {
break;
}
}
if (changed_now) {
changed = true;
} else {
const int op_index_last =
increment == 1 ? model->operators.size() - 1 : 0;
if (op_index == op_index_last) {
break;
}
op_index += increment;
}
}
DiscardUselessConnectedComponentsAndRNNBackEdges(model);
return changed;
}
} // namespace
absl::Status RunGraphTransformationsWithStatus(
Model* model, const std::string& msg,
const GraphTransformationsSet& transformations) {
PrintModelStats(toco::port::StringF("Before %s", msg), *model);
int pass_index = 0;
absl::Status status;
while (GraphTransformationsPass((pass_index % 2) ? -1 : 1, model,
transformations, &status)) {
pass_index++;
const auto& label =
toco::port::StringF("After %s pass %d", msg, pass_index);
PrintModelStats(label, *model);
CheckInvariants(*model);
}
return status;
}
} // namespace toco
@@ -0,0 +1,307 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#ifndef TENSORFLOW_LITE_TOCO_GRAPH_TRANSFORMATIONS_GRAPH_TRANSFORMATIONS_H_
#define TENSORFLOW_LITE_TOCO_GRAPH_TRANSFORMATIONS_GRAPH_TRANSFORMATIONS_H_
#include <cstddef>
#include <initializer_list>
#include <string>
#include <unordered_set>
#include <vector>
#include "absl/log/check.h"
#include "absl/strings/str_format.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/toco_port.h"
namespace toco {
class GraphTransformation {
public:
virtual absl::Status Run(Model* model, std::size_t op_index,
bool* modified) = 0;
virtual const char* Name() const = 0;
virtual ~GraphTransformation() {}
// Returns the list of messages that this graph transformation
// generated since ClearMessages() was called.
const std::vector<std::string>& Messages() const { return messages_; }
// Clears the list of messages; should be called after every
// run of this graph transformation.
void ClearMessages() { return messages_.clear(); }
// Adds a message; normally only called by the graph transformation
// itself during its run (this function could be protected).
template <typename... Args>
void AddMessageF(const absl::FormatSpec<Args...>& format,
const Args&... args) {
return messages_.push_back(toco::port::StringF(format, args...));
}
protected:
GraphTransformation() {}
// List of messages generated by this graph transformation.
std::vector<std::string> messages_;
private:
GraphTransformation(const GraphTransformation& other) = delete;
GraphTransformation(const GraphTransformation&& other) = delete;
};
class GraphTransformationsSet {
public:
// The choice of a container with fully-specified iteration order
// ensures that graph transformations are always run in the same order,
// which avoids having toco randomly fail or produce different results
// depending on the toolchain. Ideally success/results should be independent
// of the order in which graph transformations are run, but that's
// unfortunately not currently guaranteed to be the case.
using TransformationsContainer =
std::vector<std::unique_ptr<GraphTransformation>>;
GraphTransformationsSet() {}
GraphTransformationsSet(
const std::initializer_list<GraphTransformation*> transformations) {
for (GraphTransformation* t : transformations) {
Add(t);
}
}
void Add(GraphTransformation* transformation) {
const std::string& name = transformation->Name();
CHECK(!names_.count(name));
names_.insert(name);
transformations_.emplace_back(transformation);
}
TransformationsContainer::const_iterator begin() const {
return transformations_.begin();
}
TransformationsContainer::const_iterator end() const {
return transformations_.end();
}
bool empty() const { return transformations_.empty(); }
private:
GraphTransformationsSet(const GraphTransformationsSet& other) = delete;
GraphTransformationsSet(const GraphTransformationsSet&& other) = delete;
std::vector<std::unique_ptr<GraphTransformation>> transformations_;
// Names of transformations in the set. Only used to guard against dupes.
std::unordered_set<std::string> names_;
};
// Run the given list of graph transformations on the model.
// The message is only for logging purposes.
// The transformations is a rvalue reference, indicating that
// nothing else will use these pointers. The user is supposed to
// construct GraphTransformation objects by using 'new', pass us
// the resulting raw pointers, and this RunGraphTransformations
// takes care of delete'ing these pointers.
absl::Status RunGraphTransformationsWithStatus(
Model* model, const std::string& msg,
const GraphTransformationsSet& transformations);
inline void RunGraphTransformations(
Model* model, const std::string& msg,
const GraphTransformationsSet& transformations) {
auto s = RunGraphTransformationsWithStatus(model, msg, transformations);
CHECK(s.ok()) << s.message();
}
#define DECLARE_GRAPH_TRANSFORMATION(GTName) \
class GTName : public GraphTransformation { \
public: \
::tensorflow::Status Run(Model* model, std::size_t op_index, \
bool* modified) override; \
const char* Name() const override { return #GTName; } \
};
// List of all graph transformations
DECLARE_GRAPH_TRANSFORMATION(ConvertExpandDimsToReshape)
DECLARE_GRAPH_TRANSFORMATION(ConvertMatrixSetDiagV2OrV3ToV1)
DECLARE_GRAPH_TRANSFORMATION(ConvertMatrixDiagV2OrV3ToV1)
DECLARE_GRAPH_TRANSFORMATION(ConvertPureConvToDepthwise)
DECLARE_GRAPH_TRANSFORMATION(ConvertReorderAxes)
DECLARE_GRAPH_TRANSFORMATION(ConvertSqueezeToReshape)
DECLARE_GRAPH_TRANSFORMATION(ConvertTrivialAddNToAdd)
DECLARE_GRAPH_TRANSFORMATION(ConvertTrivialPackToReshape)
DECLARE_GRAPH_TRANSFORMATION(ConvertTrivialTileToConcat)
DECLARE_GRAPH_TRANSFORMATION(ConvertTrivialTransposeToReshape)
DECLARE_GRAPH_TRANSFORMATION(EnsureBiasVectors)
DECLARE_GRAPH_TRANSFORMATION(FuseActivationFunctions)
DECLARE_GRAPH_TRANSFORMATION(FuseBinaryIntoFollowingAffine)
DECLARE_GRAPH_TRANSFORMATION(FuseBinaryIntoPrecedingAffine)
DECLARE_GRAPH_TRANSFORMATION(FuseBroadcastIntoFollowingBinary)
DECLARE_GRAPH_TRANSFORMATION(GroupBidirectionalSequenceLstm)
DECLARE_GRAPH_TRANSFORMATION(GroupBidirectionalSequenceRnn)
DECLARE_GRAPH_TRANSFORMATION(GroupDynamicBidirectionalSequenceLstm)
DECLARE_GRAPH_TRANSFORMATION(GroupDynamicBidirectionalSequenceRnn)
DECLARE_GRAPH_TRANSFORMATION(IdentifyL2Normalization)
DECLARE_GRAPH_TRANSFORMATION(IdentifyL2Pool)
DECLARE_GRAPH_TRANSFORMATION(IdentifyLstmCell)
DECLARE_GRAPH_TRANSFORMATION(IdentifyHardSwish)
DECLARE_GRAPH_TRANSFORMATION(SplitLstmCellInputs)
DECLARE_GRAPH_TRANSFORMATION(MergeLstmCellInputs)
DECLARE_GRAPH_TRANSFORMATION(MergeReshapeIntoPrecedingTranspose)
DECLARE_GRAPH_TRANSFORMATION(IdentifyRelu1)
DECLARE_GRAPH_TRANSFORMATION(IdentifyPRelu)
DECLARE_GRAPH_TRANSFORMATION(MakeInitialDequantizeOperator)
DECLARE_GRAPH_TRANSFORMATION(MoveBinaryOperatorBeforeReshape)
DECLARE_GRAPH_TRANSFORMATION(PropagateActivationFunctionIntoConstants)
DECLARE_GRAPH_TRANSFORMATION(PropagateArrayDataTypes)
DECLARE_GRAPH_TRANSFORMATION(PropagateFakeQuantNumBits)
DECLARE_GRAPH_TRANSFORMATION(PropagateFixedSizes)
DECLARE_GRAPH_TRANSFORMATION(HardcodeMinMax)
DECLARE_GRAPH_TRANSFORMATION(Quantize)
DECLARE_GRAPH_TRANSFORMATION(RemoveFinalDequantizeOp)
DECLARE_GRAPH_TRANSFORMATION(RemoveSuccessiveTranspose)
DECLARE_GRAPH_TRANSFORMATION(RemoveTensorFlowAssert)
DECLARE_GRAPH_TRANSFORMATION(RemoveTensorFlowIdentity)
DECLARE_GRAPH_TRANSFORMATION(RemoveTrivialBinaryOperator)
DECLARE_GRAPH_TRANSFORMATION(RemoveTrivialConcatenation)
DECLARE_GRAPH_TRANSFORMATION(RemoveTrivialConcatenationInput)
DECLARE_GRAPH_TRANSFORMATION(RemoveTrivialFakeQuant)
DECLARE_GRAPH_TRANSFORMATION(RemoveTrivialSlice)
DECLARE_GRAPH_TRANSFORMATION(RemoveTrivialQuantizedActivationFunc)
DECLARE_GRAPH_TRANSFORMATION(RemoveTrivialQuantizedMinMax)
DECLARE_GRAPH_TRANSFORMATION(RemoveUnusedOp)
DECLARE_GRAPH_TRANSFORMATION(ResolveBatchNormalization)
DECLARE_GRAPH_TRANSFORMATION(ResolveConstantBinaryOperator)
DECLARE_GRAPH_TRANSFORMATION(ResolveConstantUnaryOperator)
DECLARE_GRAPH_TRANSFORMATION(CreateIm2colArrays)
DECLARE_GRAPH_TRANSFORMATION(DropIm2colArrays)
DECLARE_GRAPH_TRANSFORMATION(ReadArrayMinmaxAndNarrowRangeFromFakeQuant)
DECLARE_GRAPH_TRANSFORMATION(ReorderElementwiseUnary)
DECLARE_GRAPH_TRANSFORMATION(ReorderReshapeTranspose)
DECLARE_GRAPH_TRANSFORMATION(ResolveReorderAxes)
DECLARE_GRAPH_TRANSFORMATION(ResolveTensorFlowConcat)
DECLARE_GRAPH_TRANSFORMATION(ResolveTensorFlowMatMul)
DECLARE_GRAPH_TRANSFORMATION(ResolveTensorFlowMerge)
DECLARE_GRAPH_TRANSFORMATION(ResolveSqueezeAttributes)
DECLARE_GRAPH_TRANSFORMATION(ResolveTensorFlowSwitch)
DECLARE_GRAPH_TRANSFORMATION(ResolveConstantConcatenation)
DECLARE_GRAPH_TRANSFORMATION(ResolveConstantReshape)
DECLARE_GRAPH_TRANSFORMATION(ResolveConstantTranspose)
DECLARE_GRAPH_TRANSFORMATION(DropFakeQuant)
DECLARE_GRAPH_TRANSFORMATION(UnfuseActivationFunctions)
DECLARE_GRAPH_TRANSFORMATION(UnrollBatchMatMul)
DECLARE_GRAPH_TRANSFORMATION(ResolveSpaceToBatchNDAttributes)
DECLARE_GRAPH_TRANSFORMATION(ResolveBatchToSpaceNDAttributes)
DECLARE_GRAPH_TRANSFORMATION(ResolvePadAttributes)
DECLARE_GRAPH_TRANSFORMATION(ResolvePadV2Attributes)
DECLARE_GRAPH_TRANSFORMATION(ResolveReduceAttributes)
DECLARE_GRAPH_TRANSFORMATION(ResolveReshapeAttributes)
DECLARE_GRAPH_TRANSFORMATION(ResolveSliceAttributes)
DECLARE_GRAPH_TRANSFORMATION(ResolveStridedSliceAttributes)
DECLARE_GRAPH_TRANSFORMATION(ResolveTransposeAttributes)
DECLARE_GRAPH_TRANSFORMATION(ResolveConstantPack)
DECLARE_GRAPH_TRANSFORMATION(ResolveConstantRandomUniform)
DECLARE_GRAPH_TRANSFORMATION(ResolveConstantRange)
DECLARE_GRAPH_TRANSFORMATION(ResolveConstantShapeOrRank)
DECLARE_GRAPH_TRANSFORMATION(ResolveConstantSlice)
DECLARE_GRAPH_TRANSFORMATION(ResolveConstantStridedSlice)
DECLARE_GRAPH_TRANSFORMATION(ResolveConstantFill)
DECLARE_GRAPH_TRANSFORMATION(ResolveConstantGather)
DECLARE_GRAPH_TRANSFORMATION(ResolveConstantSelect)
DECLARE_GRAPH_TRANSFORMATION(ResolveConstantTile)
DECLARE_GRAPH_TRANSFORMATION(ResolveMultiplyByZero)
DECLARE_GRAPH_TRANSFORMATION(Dequantize)
DECLARE_GRAPH_TRANSFORMATION(UnpartitionEmbeddingLookup)
DECLARE_GRAPH_TRANSFORMATION(ShuffleFCWeights)
DECLARE_GRAPH_TRANSFORMATION(ResolveFakeQuantArgsFromVars)
DECLARE_GRAPH_TRANSFORMATION(ResolveGatherAttributes)
DECLARE_GRAPH_TRANSFORMATION(IdentifyNearestUpsample)
class PropagateDefaultMinMax : public GraphTransformation {
public:
absl::Status Run(Model* model, std::size_t op_index, bool* modified) override;
const char* Name() const override { return "PropagateDefaultMinMax"; }
bool has_any_ranges_defined() const { return !type_ranges_.empty(); }
void DefineTypeRange(ArrayDataType data_type, double min, double max) {
MinMax minmax;
minmax.min = min;
minmax.max = max;
type_ranges_.emplace_back(data_type, minmax);
}
private:
bool SetArrayMinMax(const std::string& array_name, Array* array);
std::vector<std::pair<ArrayDataType, MinMax>> type_ranges_;
};
class RemoveTrivialReshape : public GraphTransformation {
public:
absl::Status Run(Model* model, std::size_t op_index, bool* modified) override;
const char* Name() const override { return "RemoveTrivialReshape"; }
bool treat_expand_dims_as_trivial() const {
return treat_expand_dims_as_trivial_;
}
void set_treat_expand_dims_as_trivial(bool val) {
treat_expand_dims_as_trivial_ = val;
}
private:
bool treat_expand_dims_as_trivial_ = false;
};
class ResolveConstantFakeQuant : public GraphTransformation {
public:
absl::Status Run(Model* model, std::size_t op_index, bool* modified) override;
const char* Name() const override { return "ResolveConstantFakeQuant"; }
// True if the num_bits should adjust the final data type.
bool propagate_fake_quant_num_bits() const {
return propagate_fake_quant_num_bits_;
}
void set_propagate_fake_quant_num_bits(bool val) {
propagate_fake_quant_num_bits_ = val;
}
private:
bool propagate_fake_quant_num_bits_ = false;
};
class EnsureUint8WeightsSafeForFastInt8Kernels : public GraphTransformation {
public:
absl::Status Run(Model* model, std::size_t op_index, bool* modified) override;
const char* Name() const override {
return "EnsureUint8WeightsSafeForFastInt8Kernels";
}
bool allow_nudging_weights() const { return allow_nudging_weights_; }
void set_allow_nudging_weights(bool val) { allow_nudging_weights_ = val; }
bool has_default_ranges_flag() const { return has_default_ranges_flag_; }
void set_has_default_ranges_flag(bool val) { has_default_ranges_flag_ = val; }
private:
bool allow_nudging_weights_ = false;
bool has_default_ranges_flag_ = false;
};
class IdentifyDilatedConv : public GraphTransformation {
public:
absl::Status Run(Model* model, std::size_t op_index, bool* modified) override;
const char* Name() const override { return "IdentifyDilatedConv"; }
bool identify_depthwise_conv() const { return identify_depthwise_conv_; }
void set_identify_depthwise_conv(bool val) { identify_depthwise_conv_ = val; }
private:
bool identify_depthwise_conv_ = true;
};
#undef DECLARE_GRAPH_TRANSFORMATION
} // end namespace toco
#endif // TENSORFLOW_LITE_TOCO_GRAPH_TRANSFORMATIONS_GRAPH_TRANSFORMATIONS_H_
@@ -0,0 +1,645 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <algorithm>
#include <cstdio>
#include <memory>
#include <stack>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/lib/core/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
std::vector<std::unique_ptr<Operator>>::iterator FindOperator(
Model* model, const Operator& op) {
return std::find_if(
model->operators.begin(), model->operators.end(),
[&op](const std::unique_ptr<Operator>& ptr) { return ptr.get() == &op; });
}
bool MatchTwoUnpackOps(const Operator& op, const Model& model,
Operator** fw_output, Operator** bw_output) {
if (op.inputs.size() != 2) {
return false;
}
*fw_output = GetOpWithOutput(model, op.inputs[0]);
*bw_output = GetOpWithOutput(model, op.inputs[1]);
if (*fw_output == nullptr || *bw_output == nullptr) {
return false;
}
if ((*fw_output)->type != OperatorType::kUnpack ||
(*bw_output)->type != OperatorType::kUnpack) {
return false;
}
// TODO(renjieliu): Check the shapes are matching.
return true;
}
bool MatchDynamicBidirectionalSequenceOutputs(Operator* op, const Model& model,
Operator** fw_output,
Operator** bw_output) {
if (op->inputs.size() != 2) {
return false;
}
// The concat op is already the fw_rnn_output.
*fw_output = op;
auto* reverse_output = GetOpWithOutput(model, op->inputs[1]);
if (*fw_output == nullptr || reverse_output == nullptr) {
return false;
}
if (reverse_output->type != OperatorType::kReverseV2 &&
reverse_output->type != OperatorType::kReverseSequence) {
return false;
}
*bw_output = reverse_output;
return true;
}
bool FindUnidirectionalSequenceOp(const Model& model, const Operator& output_op,
OperatorType operator_type,
std::stack<Operator*>* sequence_ops,
Operator** input_op) {
Operator* op_it = nullptr;
op_it = GetOpWithOutput(model, output_op.inputs[0]);
if (op_it == nullptr) {
return false;
}
while (op_it->type == operator_type) {
sequence_ops->push(op_it);
// Check the first input of the unidirectional sequence op.
op_it = GetOpWithOutput(model, op_it->inputs[0]);
if (op_it == nullptr) {
return false;
}
}
*input_op = op_it;
return true;
}
bool CheckTwoUnidirectionalSequenceOpsAreValid(
const Model& model, std::stack<Operator*> fw_unidirectional_sequence_ops,
std::stack<Operator*> bw_unidirectional_sequence_ops,
const Operator* first_fw_sequence_op_input,
const Operator* first_bw_sequence_op_input, bool is_dynamic_rnn) {
if (fw_unidirectional_sequence_ops.size() !=
bw_unidirectional_sequence_ops.size() ||
fw_unidirectional_sequence_ops.empty()) {
return false;
}
// Fw & bw sequence ops are allowed to have different input shapes, but they
// need to have the same data type.
while (!fw_unidirectional_sequence_ops.empty()) {
Operator* fw_sequence_op = fw_unidirectional_sequence_ops.top();
Operator* bw_sequence_op = bw_unidirectional_sequence_ops.top();
if (fw_sequence_op->inputs.size() != bw_sequence_op->inputs.size() ||
fw_sequence_op->outputs.size() != bw_sequence_op->outputs.size())
return false;
// Make sure the inputs datatype matches.
for (size_t i = 0; i < fw_sequence_op->inputs.size(); ++i) {
const auto& fw_input_array_name = fw_sequence_op->inputs[i];
const auto& bw_input_array_name = bw_sequence_op->inputs[i];
if (model.HasArray(fw_input_array_name) &&
model.HasArray(bw_input_array_name)) {
if (model.GetArray(fw_input_array_name).data_type !=
model.GetArray(bw_input_array_name).data_type)
return false;
}
}
// Make sure the outputs datatype matches.
for (size_t i = 0; i < fw_sequence_op->outputs.size(); ++i) {
const auto& fw_output_array_name = fw_sequence_op->outputs[i];
const auto& bw_output_array_name = bw_sequence_op->outputs[i];
if (model.HasArray(fw_output_array_name) &&
model.HasArray(bw_output_array_name)) {
if (model.GetArray(fw_output_array_name).data_type !=
model.GetArray(bw_output_array_name).data_type)
return false;
}
}
fw_unidirectional_sequence_ops.pop();
bw_unidirectional_sequence_ops.pop();
}
if (is_dynamic_rnn) {
// For dynamic bidirectional sequence ops, bw_sequence will have a reverse
// op.
if (first_bw_sequence_op_input->type != OperatorType::kReverseV2 &&
first_bw_sequence_op_input->type != OperatorType::kReverseSequence) {
return false;
}
const auto* bw_real_input_op =
GetOpWithOutput(model, first_bw_sequence_op_input->inputs[0]);
if (first_fw_sequence_op_input != bw_real_input_op) {
return false;
}
} else {
// For static bidirectional sequence ops, we should have two pack ops.
if (first_fw_sequence_op_input->type != OperatorType::kPack ||
first_bw_sequence_op_input->type != OperatorType::kPack) {
return false;
}
// fw_lstm & bw_lstm should point to the same input, but reversed sequence.
for (size_t i = 0; i < first_fw_sequence_op_input->inputs.size(); ++i) {
if (first_fw_sequence_op_input->inputs[i] !=
first_bw_sequence_op_input
->inputs[first_fw_sequence_op_input->inputs.size() - i - 1]) {
return false;
}
}
}
return true;
}
void ConstructBidirectionalSequenceOp(
const Operator& fw_lstm_op, const Operator& bw_lstm_op, Model* model,
BidirectionalSequenceLstmOperator** bi_op) {
// TODO(renjieliu): Check the shapes & configurations are equal.
constexpr int kBidirectionalSequenceLstmInputsCount = 47;
constexpr int kFwLstmInputsStartIndex = 1;
constexpr int kBwLstmInputsStartIndex = 18;
constexpr int kFwInputActivationStartIndex = 35;
constexpr int kBwInputActivationStartIndex = 37;
constexpr int kAuxInputStartIndex = 39;
(*bi_op)->inputs.reserve(kBidirectionalSequenceLstmInputsCount);
const std::string& input_array_name =
AvailableArrayName(*model, "bidirectional_sequence_lstm_input_0");
model->GetOrCreateArray(input_array_name);
// The input will be changed later.
(*bi_op)->inputs.push_back(input_array_name);
int i = 1;
// Fill in the fw_lstm weights.
for (; i < kBwLstmInputsStartIndex; ++i) {
(*bi_op)->inputs.push_back(fw_lstm_op.inputs[i]);
}
// Fill in the bw_lstm weights. bidirectional lstm backward weights start
// from 18.
for (; i < kFwInputActivationStartIndex; ++i) {
(*bi_op)->inputs.push_back(
bw_lstm_op
.inputs[i - (kBwLstmInputsStartIndex - kFwLstmInputsStartIndex)]);
}
// Fill in fw_lstm previous states.
for (; i < kBwInputActivationStartIndex; ++i) {
(*bi_op)->inputs.push_back(
fw_lstm_op.inputs[i - (kFwInputActivationStartIndex -
kBwLstmInputsStartIndex)]);
}
// Fill in bw_lstm previous states.
for (; i < kAuxInputStartIndex; ++i) {
(*bi_op)->inputs.push_back(
bw_lstm_op.inputs[i - (kBwInputActivationStartIndex -
kBwLstmInputsStartIndex)]);
}
// TODO(renjieliu): Deal with Auxiliary input and weights for 39 - 47.
for (; i <= kBidirectionalSequenceLstmInputsCount; ++i) {
const std::string& temp_array_name = AvailableArrayName(
*model, "bidirectional_sequence_lstm_temp_" + std::to_string(i));
model->CreateOptionalArray(temp_array_name);
(*bi_op)->inputs.push_back(temp_array_name);
}
// Deal with outputs.
(*bi_op)->outputs.reserve(2);
const std::string& fw_output_array_name =
AvailableArrayName(*model, "bidirectional_sequence_lstm_fw_output_0");
const std::string& bw_output_array_name =
AvailableArrayName(*model, "bidirectional_sequence_lstm_bw_output_0");
model->GetOrCreateArray(fw_output_array_name);
model->GetOrCreateArray(bw_output_array_name);
(*bi_op)->outputs.push_back(fw_output_array_name);
(*bi_op)->outputs.push_back(bw_output_array_name);
(*bi_op)->merge_outputs = false;
}
void ConstructBidirectionalSequenceOp(
const Operator& fw_rnn_op, const Operator& bw_rnn_op, Model* model,
BidirectionalSequenceRnnOperator** bi_op) {
// TODO(renjieliu): Check the shapes & configurations are equal.
constexpr int kBidirectionalSequenceRnnInputsCount = 12;
constexpr int kFwInputsStartIndex = 1;
constexpr int kBwInputsStartIndex = 5;
constexpr int kAuxInputsStartIndex = 9;
(*bi_op)->inputs.reserve(kBidirectionalSequenceRnnInputsCount);
const std::string& input_array_name =
AvailableArrayName(*model, "bidirectional_sequence_rnn_input_0");
model->GetOrCreateArray(input_array_name);
// The input will be changed later.
(*bi_op)->inputs.push_back(input_array_name);
int i = 1;
// Fill in the fw_rnn weights.
for (; i < kBwInputsStartIndex; ++i) {
(*bi_op)->inputs.push_back(fw_rnn_op.inputs[i]);
}
// Fill in the bw_rnn weights.
for (; i < kAuxInputsStartIndex; ++i) {
(*bi_op)->inputs.push_back(
bw_rnn_op.inputs[i - (kBwInputsStartIndex - kFwInputsStartIndex)]);
}
// TODO(renjieliu): Deal with optional weights.
for (; i < kBidirectionalSequenceRnnInputsCount; ++i) {
const std::string& temp_array_name = AvailableArrayName(
*model, "bidirectional_sequence_rnn_temp_" + std::to_string(i));
model->CreateOptionalArray(temp_array_name);
(*bi_op)->inputs.push_back(temp_array_name);
}
// Deal with outputs.
(*bi_op)->outputs.reserve(2);
const std::string& fw_output_array_name =
AvailableArrayName(*model, "bidirectional_sequence_rnn_fw_output_0");
const std::string& bw_output_array_name =
AvailableArrayName(*model, "bidirectional_sequence_rnn_bw_output_0");
model->GetOrCreateArray(fw_output_array_name);
model->GetOrCreateArray(bw_output_array_name);
(*bi_op)->outputs.push_back(fw_output_array_name);
(*bi_op)->outputs.push_back(bw_output_array_name);
(*bi_op)->merge_outputs = false;
}
template <typename T>
void GroupFwBwSequenceOps(Model* model, std::stack<Operator*> fw_sequence_ops,
std::stack<Operator*> bw_sequence_ops,
std::vector<T*>* bidirectional_sequence_ops) {
while (!fw_sequence_ops.empty()) {
Operator* fw_sequence_op = fw_sequence_ops.top();
Operator* bw_sequence_op = bw_sequence_ops.top();
T* bidirectional_sequence_op = new T;
ConstructBidirectionalSequenceOp(*fw_sequence_op, *bw_sequence_op, model,
&bidirectional_sequence_op);
bidirectional_sequence_ops->push_back(bidirectional_sequence_op);
fw_sequence_ops.pop();
bw_sequence_ops.pop();
}
}
template <typename T>
void RewireBidirectionalSequenceSequenceOpsConnections(
OperatorType operator_type, const std::string& input_array_name,
const std::vector<T*>& bidirectional_sequence_ops,
std::vector<std::unique_ptr<Operator>>::iterator* op_it, Model* model) {
int aux_input_index = -1;
switch (operator_type) {
case OperatorType::kBidirectionalSequenceLstm:
aux_input_index = 39;
break;
case OperatorType::kBidirectionalSequenceRnn:
aux_input_index = 9;
break;
default:
// Should not reach here.
DCHECK(false);
}
std::string cur_fw_input = input_array_name;
std::string cur_bw_input = input_array_name;
for (size_t i = 0; i < bidirectional_sequence_ops.size(); ++i) {
DeleteArrayIfUnusedOutsideOfOp(bidirectional_sequence_ops[i]->inputs[0],
bidirectional_sequence_ops[i], model);
bidirectional_sequence_ops[i]->inputs[0] = cur_fw_input;
if (i != 0) {
DeleteArrayIfUnusedOutsideOfOp(
bidirectional_sequence_ops[i]->inputs[aux_input_index],
bidirectional_sequence_ops[i], model);
bidirectional_sequence_ops[i]->inputs[aux_input_index] = cur_bw_input;
}
cur_fw_input = bidirectional_sequence_ops[i]->outputs[0];
cur_bw_input = bidirectional_sequence_ops[i]->outputs[1];
if (i != (bidirectional_sequence_ops.size() - 1)) {
bidirectional_sequence_ops[i]->merge_outputs = false;
} else {
// TODO(renjieliu): We need to check whether the outputs of the last bidi
// lstms needs merged outputs or not.
bidirectional_sequence_ops[i]->merge_outputs = true;
DeleteArrayIfUnused(bidirectional_sequence_ops[i]->outputs[1], model);
bidirectional_sequence_ops[i]->outputs.pop_back();
}
model->operators.emplace(*op_it, bidirectional_sequence_ops[i]);
*op_it += 1;
}
}
template <typename T>
void RewireFinalUnpackOutputs(const UnpackOperator& original_unpack_operator,
UnpackOperator** final_unpack_operator,
T** final_bidi_sequence_operator, Model* model) {
(*final_unpack_operator)
->inputs.push_back((*final_bidi_sequence_operator)->outputs[0]);
(*final_unpack_operator)->axis = original_unpack_operator.axis;
(*final_unpack_operator)->num = original_unpack_operator.num;
for (size_t i = 0; i < original_unpack_operator.outputs.size(); ++i) {
const std::string& output_array_name = original_unpack_operator.outputs[i];
const std::string& final_unpack_output_array_name = AvailableArrayName(
*model, "bidirectional_sequence_unpack_" + std::to_string(i));
model->GetOrCreateArray(final_unpack_output_array_name);
(*final_unpack_operator)->outputs.push_back(final_unpack_output_array_name);
Operator* unpack_following_op = GetOpWithInput(*model, output_array_name);
if (unpack_following_op != nullptr) {
// If there's a following op after the unpack, it must be a concat op.
DCHECK(unpack_following_op->type == OperatorType::kConcatenation);
// For every output of the concat, rewire the outputs.
for (const std::string& concat_output : unpack_following_op->outputs) {
(*final_unpack_operator)->outputs[i] = concat_output;
}
// Remove the concat op.
DeleteOpAndArrays(model, unpack_following_op);
}
}
}
void RemoveUnidirectionalSequenceOps(std::stack<Operator*> uni_sequence_ops,
Model* model) {
while (!uni_sequence_ops.empty()) {
Operator* uni_sequence_op = uni_sequence_ops.top();
DeleteOpAndArrays(model, uni_sequence_op);
uni_sequence_ops.pop();
}
}
template <typename T>
absl::Status GroupDynamicSequenceOps(Model* model, std::size_t op_index,
OperatorType operator_type,
bool* modified) {
*modified = false;
// We assume there's a concatenation right after the bidirectional sequence
// ops, it may not be the case.
auto op_it = model->operators.begin() + op_index;
Operator* final_concat_op = op_it->get();
if (final_concat_op->type != OperatorType::kConcatenation &&
final_concat_op->type != OperatorType::kConcat &&
final_concat_op->type != OperatorType::kConcatV2) {
return absl::OkStatus();
}
// for bw, there will be a reverse op at the end.
Operator *fw_sequence_output, *bw_sequence_output;
if (!MatchDynamicBidirectionalSequenceOutputs(
final_concat_op, *model, &fw_sequence_output, &bw_sequence_output)) {
return absl::OkStatus();
}
// Find all upstream unidirectional sequence ops.
std::stack<Operator*> fw_unidirectional_sequence_ops,
bw_unidirectional_sequence_ops;
OperatorType unidirectional_op_type;
if (operator_type == OperatorType::kBidirectionalSequenceLstm) {
unidirectional_op_type = OperatorType::kUnidirectionalSequenceLstm;
} else {
unidirectional_op_type = OperatorType::kUnidirectionalSequenceRnn;
}
Operator *first_fw_sequence_input, *first_bw_sequence_input;
if (!FindUnidirectionalSequenceOp(
*model, *fw_sequence_output, unidirectional_op_type,
&fw_unidirectional_sequence_ops, &first_fw_sequence_input) ||
!FindUnidirectionalSequenceOp(
*model, *bw_sequence_output, unidirectional_op_type,
&bw_unidirectional_sequence_ops, &first_bw_sequence_input)) {
return absl::OkStatus();
}
if (!CheckTwoUnidirectionalSequenceOpsAreValid(
*model, fw_unidirectional_sequence_ops,
bw_unidirectional_sequence_ops, first_fw_sequence_input,
first_bw_sequence_input, /*is_dynamic_rnn=*/true)) {
return absl::OkStatus();
}
std::vector<T> bidirectional_sequence_ops;
GroupFwBwSequenceOps(model, fw_unidirectional_sequence_ops,
bw_unidirectional_sequence_ops,
&bidirectional_sequence_ops);
// Rewire the inputs & outputs.
std::string current_input = first_fw_sequence_input->outputs[0];
RewireBidirectionalSequenceSequenceOpsConnections(
operator_type, current_input, bidirectional_sequence_ops, &op_it, model);
// Change last bidirectional sequence rnn output to the concat output.
bidirectional_sequence_ops[bidirectional_sequence_ops.size() - 1]
->outputs[0] = final_concat_op->outputs[0];
// Delete unused ops.
RemoveUnidirectionalSequenceOps(fw_unidirectional_sequence_ops, model);
RemoveUnidirectionalSequenceOps(bw_unidirectional_sequence_ops, model);
DeleteOpAndArrays(model, final_concat_op);
// Only keep the fw lstm's input.
DeleteOpAndArrays(model, first_bw_sequence_input);
*modified = true;
return absl::OkStatus();
}
} // namespace
absl::Status GroupBidirectionalSequenceLstm::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
// Bidirectional sequence lstm will generate two separate unidirectional
// sequence lstm ops, for static bidirectional sequence lstm, there will be
// a concatenation op at very end; for dynamic bidirectional sequence lstm,
// it is not guaranteed, but currently we do not support that.
auto op_it = model->operators.begin() + op_index;
Operator* final_concat_op = op_it->get();
if (final_concat_op->type != OperatorType::kConcatenation &&
final_concat_op->type != OperatorType::kConcat &&
final_concat_op->type != OperatorType::kConcatV2) {
return absl::OkStatus();
}
// Match fw unidirectional lstm outputs and bw unidirectional lstm outputs:
// should be two unstack ops.
Operator *fw_lstm_output, *bw_lstm_output;
if (!MatchTwoUnpackOps(*final_concat_op, *model, &fw_lstm_output,
&bw_lstm_output)) {
return absl::OkStatus();
}
// Find all upstream unidirectional lstm ops.
std::stack<Operator*> fw_unidirectional_sequence_lstm_ops,
bw_unidirectional_sequence_lstm_ops;
Operator *first_fw_lstm_input, *first_bw_lstm_input;
if (!FindUnidirectionalSequenceOp(
*model, *fw_lstm_output, OperatorType::kUnidirectionalSequenceLstm,
&fw_unidirectional_sequence_lstm_ops, &first_fw_lstm_input) ||
!FindUnidirectionalSequenceOp(
*model, *bw_lstm_output, OperatorType::kUnidirectionalSequenceLstm,
&bw_unidirectional_sequence_lstm_ops, &first_bw_lstm_input)) {
return absl::OkStatus();
}
if (!CheckTwoUnidirectionalSequenceOpsAreValid(
*model, fw_unidirectional_sequence_lstm_ops,
bw_unidirectional_sequence_lstm_ops, first_fw_lstm_input,
first_bw_lstm_input, /*is_dynamic_rnn=*/false)) {
return absl::OkStatus();
}
std::vector<BidirectionalSequenceLstmOperator*>
bidirectional_sequence_lstm_ops;
GroupFwBwSequenceOps(model, fw_unidirectional_sequence_lstm_ops,
bw_unidirectional_sequence_lstm_ops,
&bidirectional_sequence_lstm_ops);
// Rewire the inputs & outputs.
std::string current_input = first_fw_lstm_input->outputs[0];
RewireBidirectionalSequenceSequenceOpsConnections(
OperatorType::kBidirectionalSequenceLstm, current_input,
bidirectional_sequence_lstm_ops, &op_it, model);
// Insert a unpack op for the output.
UnpackOperator* unpack_operator = new UnpackOperator;
RewireFinalUnpackOutputs(
static_cast<const UnpackOperator&>(*fw_lstm_output), &unpack_operator,
&bidirectional_sequence_lstm_ops[bidirectional_sequence_lstm_ops.size() -
1],
model);
model->operators.emplace(op_it, unpack_operator);
// Delete unused ops.
DeleteOpAndArrays(model, fw_lstm_output);
DeleteOpAndArrays(model, bw_lstm_output);
RemoveUnidirectionalSequenceOps(fw_unidirectional_sequence_lstm_ops, model);
RemoveUnidirectionalSequenceOps(bw_unidirectional_sequence_lstm_ops, model);
// Only keep the fw lstm's pack input.
DeleteOpAndArrays(model, first_bw_lstm_input);
*modified = true;
return absl::OkStatus();
}
absl::Status GroupBidirectionalSequenceRnn::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
// Bidirectional sequence rnn will generate two separate unidirectional
// sequence rnn ops, for static bidirectional sequence rnn, there will be
// a concatenation op at very end; for dynamic bidirectional sequence rnn,
// it is not guaranteed, but currently we do not support that.
auto op_it = model->operators.begin() + op_index;
Operator* final_concat_op = op_it->get();
if (final_concat_op->type != OperatorType::kConcatenation &&
final_concat_op->type != OperatorType::kConcat &&
final_concat_op->type != OperatorType::kConcatV2) {
return absl::OkStatus();
}
// Match fw unidirectional rnn outputs and bw unidirectional rnn outputs:
// should be two unstack ops.
Operator *fw_rnn_output, *bw_rnn_output;
if (!MatchTwoUnpackOps(*final_concat_op, *model, &fw_rnn_output,
&bw_rnn_output)) {
return absl::OkStatus();
}
// Find all upstream unidirectional rnn ops.
std::stack<Operator*> fw_unidirectional_sequence_rnn_ops,
bw_unidirectional_sequence_rnn_ops;
Operator *first_fw_rnn_input, *first_bw_rnn_input;
if (!FindUnidirectionalSequenceOp(
*model, *fw_rnn_output, OperatorType::kUnidirectionalSequenceRnn,
&fw_unidirectional_sequence_rnn_ops, &first_fw_rnn_input) ||
!FindUnidirectionalSequenceOp(
*model, *bw_rnn_output, OperatorType::kUnidirectionalSequenceRnn,
&bw_unidirectional_sequence_rnn_ops, &first_bw_rnn_input)) {
return absl::OkStatus();
}
if (!CheckTwoUnidirectionalSequenceOpsAreValid(
*model, fw_unidirectional_sequence_rnn_ops,
bw_unidirectional_sequence_rnn_ops, first_fw_rnn_input,
first_bw_rnn_input, /*is_dynamic_rnn=*/false)) {
return absl::OkStatus();
}
std::vector<BidirectionalSequenceRnnOperator*> bidirectional_sequence_rnn_ops;
GroupFwBwSequenceOps(model, fw_unidirectional_sequence_rnn_ops,
bw_unidirectional_sequence_rnn_ops,
&bidirectional_sequence_rnn_ops);
// Rewire the inputs & outputs.
std::string current_input = first_fw_rnn_input->outputs[0];
RewireBidirectionalSequenceSequenceOpsConnections(
OperatorType::kBidirectionalSequenceRnn, current_input,
bidirectional_sequence_rnn_ops, &op_it, model);
// Insert a unpack op for the output.
UnpackOperator* unpack_operator = new UnpackOperator;
RewireFinalUnpackOutputs(
static_cast<const UnpackOperator&>(*fw_rnn_output), &unpack_operator,
&bidirectional_sequence_rnn_ops[bidirectional_sequence_rnn_ops.size() -
1],
model);
model->operators.emplace(op_it, unpack_operator);
// Delete unused ops.
DeleteOpAndArrays(model, fw_rnn_output);
DeleteOpAndArrays(model, bw_rnn_output);
RemoveUnidirectionalSequenceOps(fw_unidirectional_sequence_rnn_ops, model);
RemoveUnidirectionalSequenceOps(bw_unidirectional_sequence_rnn_ops, model);
// Only keep the fw rnn's pack input.
DeleteOpAndArrays(model, first_bw_rnn_input);
*modified = true;
return absl::OkStatus();
}
absl::Status GroupDynamicBidirectionalSequenceRnn::Run(Model* model,
std::size_t op_index,
bool* modified) {
return GroupDynamicSequenceOps<BidirectionalSequenceRnnOperator*>(
model, op_index, OperatorType::kBidirectionalSequenceRnn, modified);
}
absl::Status GroupDynamicBidirectionalSequenceLstm::Run(Model* model,
std::size_t op_index,
bool* modified) {
return GroupDynamicSequenceOps<BidirectionalSequenceLstmOperator*>(
model, op_index, OperatorType::kBidirectionalSequenceLstm, modified);
}
} // namespace toco
@@ -0,0 +1,540 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <algorithm>
#include <cstddef>
#include <cstdlib>
#include <limits>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
bool HardcodeMinMaxForIm2colArray(Model* model, Operator* op) {
if (op->outputs.size() != 2) {
return false;
}
auto& im2col_array = model->GetArray(op->outputs[1]);
if (im2col_array.minmax) {
return false;
}
const auto& input_array = model->GetArray(op->inputs[0]);
if (!input_array.minmax) {
return false;
}
const auto& input_minmax = input_array.GetMinMax();
CHECK(!im2col_array.minmax);
auto& im2col_minmax = im2col_array.GetOrCreateMinMax();
im2col_minmax.min = input_minmax.min;
im2col_minmax.max = input_minmax.max;
return true;
}
bool HardcodeMinMaxForL2Normalization(Model* model, Operator* op) {
auto& output_array = model->GetArray(op->outputs[0]);
if (output_array.minmax) {
return false;
}
const auto& input_array = model->GetArray(op->inputs[0]);
if (!input_array.minmax) {
return false;
}
const auto& input_minmax = input_array.GetMinMax();
CHECK(!output_array.minmax);
auto& output_minmax = output_array.GetOrCreateMinMax();
output_minmax.min = input_minmax.min >= 0. ? 0. : -1.;
output_minmax.max = input_minmax.max <= 0. ? 0. : 1.;
return true;
}
bool HardcodeInputMinMaxFromOutput(Model* model, Operator* op) {
auto& input = model->GetArray(op->inputs[0]);
if (input.minmax) {
const auto* minmax = input.minmax.get();
if (minmax) {
return false;
}
}
auto& output = model->GetArray(op->outputs[0]);
if (output.minmax) {
const auto* minmax = model->GetArray(op->outputs[0]).minmax.get();
if (minmax) {
input.GetOrCreateMinMax() = *minmax;
return true;
}
}
return false;
}
bool HardcodeMinMaxForConcatenation(Model* model, Operator* op) {
// Do not early return if the output already has min/max:
// we may still need to adjust the inputs min/max.
bool has_minmax = false;
double overall_min = std::numeric_limits<double>::infinity();
double overall_max = -std::numeric_limits<double>::infinity();
for (const auto& input : op->inputs) {
if (model->GetArray(input).minmax) {
has_minmax = true;
const auto* minmax = model->GetArray(input).minmax.get();
if (minmax) {
overall_min = std::min(overall_min, minmax->min);
overall_max = std::max(overall_max, minmax->max);
}
}
}
auto& output = model->GetArray(op->outputs[0]);
if (output.minmax) {
has_minmax = true;
const auto* minmax = model->GetArray(op->outputs[0]).minmax.get();
if (minmax) {
overall_min = std::min(overall_min, minmax->min);
overall_max = std::max(overall_max, minmax->max);
}
}
if (!has_minmax) {
return false;
}
MinMax overall_minmax;
overall_minmax.min = overall_min;
overall_minmax.max = overall_max;
bool changed = false;
if (model->flags.change_concat_input_ranges()) {
for (const auto& input : op->inputs) {
auto& array = model->GetArray(input);
if (!array.minmax) {
changed = true;
} else if (!(overall_minmax == array.GetMinMax())) {
changed = true;
LOG(WARNING)
<< "Tweaking the MinMax of array " << input << ", which is "
<< "an input to " << LogName(*op) << ", because we want all inputs "
<< "and outputs of a Concatenation operator to have the same "
<< "MinMax so that it can be implemented as a pure byte-copy, no "
"arithmetic.";
}
array.GetOrCreateMinMax() = overall_minmax;
}
}
if (!output.minmax) {
changed = true;
} else if (!(overall_minmax == output.GetMinMax())) {
if (model->flags.change_concat_input_ranges()) {
changed = true;
LOG(WARNING)
<< "Tweaking the MinMax of the output array of " << LogName(*op)
<< ", because we want all inputs "
<< "and outputs of a Concatenation operator to have the same MinMax "
<< "so that it can be implemented as a pure byte-copy, no "
<< "arithmetic.";
} else {
return false;
}
}
output.GetOrCreateMinMax() = overall_minmax;
return changed;
}
bool HardcodeMinMaxForSplit(Model* model, Operator* op) {
// Data is in second input.
auto& input_array = model->GetArray(op->inputs[1]);
if (!input_array.minmax) {
return false;
}
bool changed = false;
for (const auto& output : op->outputs) {
auto& array = model->GetArray(output);
if (!array.minmax || !(array.GetMinMax() == input_array.GetMinMax())) {
changed = true;
array.GetOrCreateMinMax() = *input_array.minmax;
}
}
return changed;
}
// The output of average or max pooling is within the same range as its input.
bool HardcodeMinMaxForAverageOrMaxPool(Model* model, Operator* op) {
auto& output_array = model->GetArray(op->outputs[0]);
if (output_array.minmax) {
return false;
}
const auto& input_array = model->GetArray(op->inputs[0]);
if (!input_array.minmax) {
return false;
}
const auto& input_minmax = input_array.GetMinMax();
CHECK(!output_array.minmax);
auto& output_minmax = output_array.GetOrCreateMinMax();
output_minmax.min = std::min(input_minmax.min, 0.);
output_minmax.max = std::max(input_minmax.max, 0.);
return true;
}
bool HardcodeMinMaxFromFirstInput(Model* model, Operator* op) {
auto& output_array = model->GetArray(op->outputs[0]);
if (output_array.minmax) {
return false;
}
const auto& input_array = model->GetArray(op->inputs[0]);
if (!input_array.minmax) {
return false;
}
const auto& input_minmax = input_array.GetMinMax();
CHECK(!output_array.minmax);
auto& output_minmax = output_array.GetOrCreateMinMax();
output_minmax.min = input_minmax.min;
output_minmax.max = input_minmax.max;
return true;
}
bool HardcodeMinMaxForSelect(Model* model, Operator* op) {
auto& output_array = model->GetArray(op->outputs[0]);
if (output_array.minmax) {
return false;
}
auto& input_array_1 = model->GetArray(op->inputs[1]);
auto& input_array_2 = model->GetArray(op->inputs[2]);
if (!input_array_1.minmax && !input_array_2.minmax) {
return false;
}
// Propagate up if one input is quantized and the other is constant.
if (!input_array_1.minmax &&
IsConstantParameterArray(*model, op->inputs[1])) {
auto& minmax_1 = input_array_1.GetOrCreateMinMax();
const auto& minmax_2 = input_array_2.GetMinMax();
minmax_1.min = minmax_2.min;
minmax_1.max = minmax_2.max;
}
if (!input_array_2.minmax &&
IsConstantParameterArray(*model, op->inputs[2])) {
auto& minmax_2 = input_array_2.GetOrCreateMinMax();
const auto& minmax_1 = input_array_1.GetMinMax();
minmax_2.min = minmax_1.min;
minmax_2.max = minmax_1.max;
}
if (!input_array_1.minmax || !input_array_2.minmax) {
return false;
}
const auto& input_minmax_1 = input_array_1.GetMinMax();
const auto& input_minmax_2 = input_array_2.GetMinMax();
CHECK_EQ(input_minmax_1.min, input_minmax_2.min);
CHECK_EQ(input_minmax_1.max, input_minmax_2.max);
CHECK(!output_array.minmax);
auto& output_minmax = output_array.GetOrCreateMinMax();
output_minmax.min = input_minmax_1.min;
output_minmax.max = input_minmax_1.max;
return true;
}
bool HardcodeMinMaxForOutput(Model* model, Operator* op, double min,
double max) {
CHECK_EQ(op->outputs.size(), 1);
auto& output_array = model->GetArray(op->outputs[0]);
if (output_array.minmax) {
return false;
}
const auto& input_array = model->GetArray(op->inputs[0]);
if (!input_array.minmax) {
return false;
}
CHECK(!output_array.minmax);
auto& output_minmax = output_array.GetOrCreateMinMax();
output_minmax.min = min;
output_minmax.max = max;
return true;
}
bool MinMaxApproximatelyEqual(const MinMax& minmax1, const MinMax& minmax2) {
const double magnitude =
std::min(minmax1.max - minmax1.min, minmax2.max - minmax2.min);
const double tolerated = 1e-6 * magnitude;
return std::abs(minmax1.min - minmax2.min) <= tolerated &&
std::abs(minmax1.max - minmax2.max) <= tolerated;
}
// Propagates MinMax from any of the listed arrays, to all others.
// If multiple of these arrays have MinMax, then these are required
// to agree with each other.
bool PropagateMinMaxAmongArrays(Model* model,
const std::vector<std::string>& array_names) {
std::string reference_array_name;
MinMax* reference_minmax = nullptr;
for (const std::string& array_name : array_names) {
if (model->GetArray(array_name).minmax) {
reference_array_name = array_name;
reference_minmax = model->GetArray(array_name).minmax.get();
break;
}
}
// No MinMax info is available to propagate.
if (!reference_minmax) {
return false;
}
bool changed = false;
for (const std::string& array_name : array_names) {
auto& array = model->GetArray(array_name);
if (array.minmax) {
CHECK(MinMaxApproximatelyEqual(*array.minmax, *reference_minmax))
<< "Both the following arrays have minmax, and they disagree: "
<< reference_array_name << " (" << reference_minmax->min << ","
<< reference_minmax->max << ") and " << array_name << " ("
<< array.minmax->min << "," << array.minmax->max
<< "). Expected that either only one of them would have minmax, or "
"at "
"least that they would agree.";
} else {
array.GetOrCreateMinMax() = *reference_minmax;
changed = true;
}
}
return changed;
}
bool HardcodeMinMaxForReshape(Model* model, Operator* op) {
Array& input = model->GetArray(op->inputs[0]);
Array& output = model->GetArray(op->outputs[0]);
// If input and output both exist or do not exist, do nothing.
if ((!input.minmax && !output.minmax) || (input.minmax && output.minmax)) {
return false;
}
// Otherwise propagate info amongst the input and output array.
return PropagateMinMaxAmongArrays(model, {op->inputs[0], op->outputs[0]});
}
bool HardcodeMinMaxForLstmCell(Model* model, Operator* op) {
CHECK_EQ(op->inputs.size(), LstmCellOperator::NUM_INPUTS);
CHECK_EQ(op->outputs.size(), LstmCellOperator::NUM_OUTPUTS);
bool changed = false;
changed |= PropagateMinMaxAmongArrays(
model, {op->inputs[LstmCellOperator::PREV_STATE_INPUT],
op->outputs[LstmCellOperator::STATE_OUTPUT]});
auto& input_activations =
model->GetArray(op->inputs[LstmCellOperator::DATA_INPUT]);
if (!input_activations.minmax) {
auto& minmax = input_activations.GetOrCreateMinMax();
minmax.min = -1;
minmax.max = 127. / 128.;
changed = true;
}
auto& prev_output_activations =
model->GetArray(op->inputs[LstmCellOperator::PREV_ACTIV_INPUT]);
if (!prev_output_activations.minmax) {
auto& minmax = prev_output_activations.GetOrCreateMinMax();
minmax.min = -1;
minmax.max = 127. / 128.;
changed = true;
}
auto& output_concat_temp =
model->GetArray(op->outputs[LstmCellOperator::CONCAT_TEMP]);
if (!output_concat_temp.minmax) {
auto& minmax = output_concat_temp.GetOrCreateMinMax();
minmax.min = -1;
minmax.max = 127. / 128.;
changed = true;
}
auto& output_activations =
model->GetArray(op->outputs[LstmCellOperator::ACTIV_OUTPUT]);
if (!output_activations.minmax) {
auto& minmax = output_activations.GetOrCreateMinMax();
minmax.min = -1;
minmax.max = 127. / 128.;
changed = true;
}
// (This comment should morph into proper documentation for
// quantization of LSTM models. It isn't just a local implementation detail,
// the training code for LSTM models needs to be adjusted to that.)
//
// Finally, output_activations_temp holds the output of the fully-connected
// node inside the LSTM cell. For it, we hardcode a minmax of [-8, 8].
// The rationale for that is given in a lengthy comment on the LstmCell
// quantized runtime implementation in reference_ops.h.
auto& output_activations_temp =
model->GetArray(op->outputs[LstmCellOperator::ACTIV_TEMP]);
if (!output_activations_temp.minmax) {
auto& minmax = output_activations_temp.GetOrCreateMinMax();
minmax.min = -8;
minmax.max = 8 * 32767. / 32768.;
changed = true;
}
return changed;
}
bool HardcodeMinMaxForPack(Model* model, Operator* op) {
auto& output_array = model->GetArray(op->outputs[0]);
if (output_array.minmax) {
return false;
}
// If all tensors being packed have the same min/max range, hardcode min/max
// for the output.
const auto& first_input_array = model->GetArray(op->inputs[0]);
if (!first_input_array.minmax) {
return false;
}
const auto& first_input_minmax = first_input_array.GetMinMax();
for (size_t i = 1; i < op->inputs.size(); i++) {
const auto& input_array = model->GetArray(op->inputs[i]);
if (!input_array.minmax) {
return false;
}
if (first_input_minmax != input_array.GetMinMax()) {
return false;
}
}
auto& output_minmax = output_array.GetOrCreateMinMax();
output_minmax.min = first_input_minmax.min;
output_minmax.max = first_input_minmax.max;
return true;
}
} // namespace
absl::Status HardcodeMinMax::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto it = model->operators.begin() + op_index;
auto* op = it->get();
bool changed = false;
switch (op->type) {
case OperatorType::kConv:
changed = HardcodeMinMaxForIm2colArray(model, op);
break;
case OperatorType::kL2Normalization:
changed = HardcodeMinMaxForL2Normalization(model, op);
break;
case OperatorType::kRelu:
// For any normalization other than batch norm, the quantizations ranges
// before and after relu are expected to be known. Having a quantization
// op before relu would reduce the number of bits of precision for the
// activation in half. So we deduce the range before relu from that after
// the relu. This would eliminate the need for two fake quantization nodes
// and would not reduce the bits of precision available for activation.
changed = HardcodeInputMinMaxFromOutput(model, op);
break;
case OperatorType::kConcatenation:
changed = HardcodeMinMaxForConcatenation(model, op);
break;
case OperatorType::kSplit:
changed = HardcodeMinMaxForSplit(model, op);
break;
case OperatorType::kAveragePool:
case OperatorType::kMaxPool:
changed = HardcodeMinMaxForAverageOrMaxPool(model, op);
break;
case OperatorType::kResizeBilinear:
case OperatorType::kResizeNearestNeighbor:
case OperatorType::kSlice:
case OperatorType::kStridedSlice:
case OperatorType::kSqueeze:
case OperatorType::kExpandDims:
case OperatorType::kPad:
case OperatorType::kGather:
case OperatorType::kTranspose:
case OperatorType::kMean:
case OperatorType::kReduceMax:
case OperatorType::kReduceMin:
changed = HardcodeMinMaxFromFirstInput(model, op);
break;
case OperatorType::kPack:
changed = HardcodeMinMaxForPack(model, op);
break;
case OperatorType::kSum:
// reduce_sum is expected to change the output range. Hence
// a fake_quant op is necessary in the output to minimize error. However
// in special circumstances like when computing expected value using
// reduce_sum the input range and the output range matches. Hence the
// below code would act as a fallback. If a fake_quant node is observed in
// the output that takes precedence over the hard coding logic below.
changed = HardcodeMinMaxFromFirstInput(model, op);
if (changed) {
LOG(WARNING) << "Using the input range for output in reduce_sum op."
<< "This could have an impact on your model accuracy.";
}
break;
case OperatorType::kSelect:
changed = HardcodeMinMaxForSelect(model, op);
break;
case OperatorType::kLogistic:
// We hardcode quantization_params to: zero_point=0, scale=1/256.
// This choice of minmax is the one that is equivalent to that.
changed = HardcodeMinMaxForOutput(model, op, 0, 255. / 256.);
break;
case OperatorType::kSoftmax:
// We hardcode quantization_params to: zero_point=0, scale=1/256.
// This choice of minmax is the one that is equivalent to that.
changed = HardcodeMinMaxForOutput(model, op, 0, 255. / 256.);
break;
case OperatorType::kTanh:
// We hardcode quantization_params to: zero_point=127, scale=1/128.
// This choice of minmax is the one that is equivalent to that.
changed = HardcodeMinMaxForOutput(model, op, -127. / 128., 1.0);
break;
case OperatorType::kLstmCell:
changed = HardcodeMinMaxForLstmCell(model, op);
break;
case OperatorType::kReshape:
changed = HardcodeMinMaxForReshape(model, op);
break;
default:
break;
}
if (changed) {
AddMessageF("Hardcoded min-max through %s", LogName(*op));
}
*modified = changed;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,241 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
// A dilated convolution can be emulated with a regular convolution by chaining
// SpaceToBatch and BatchToSpace ops before and after it:
//
// SpaceToBatchND -> Conv2D -> BatchToSpaceND
//
// This method was common before Conv2D fully supported dilated convolution in
// TensorFlow. This transformation detects this "emulation", and replaces it
// with a true dilated convolution, eliminating the SpaceToBatch and
// BatchtoSpace ops.
//
// Detecting this alone would be relatively easy. However, in practice some
// extra ops are used, so we detect the following patterns:
//
//
// SpaceToBatchND -> Expand -> Conv2D -> Squeeze -> BatchToSpaceND -> BiasAdd
//
// Pad -> SpaceToBatchND -> Expand -> Conv2D -> Squeeze -> BatchToSpaceND ->
// BiasAdd
//
// SpaceToBatchND -> Expand -> Conv2D -> Squeeze -> Pad -> BatchToSpaceND ->
// BiasAdd
//
// SpaceToBatchND -> Expand -> Conv2D -> Squeeze -> BiasAdd -> BatchToSpaceND
//
// SpaceToBatchND -> Conv2D -> Pad -> BatchToSpaceND -> BiasAdd
//
// SpaceToBatchND -> Conv2D -> BatchToSpaceND -> BiasAdd
//
//
// The Expand/Squeeze combination is used to adapt a 3D array (such as in
// WaveNet) to the 4D arrays that Conv2D requires. Padding and BiasAdd are
// thrown in just for the extra headache. Padding adapts non-conforming input
// sizes, and can be discarded. The bias is necessary, so is kept.
template <typename T>
bool ResolveDilatedConv(Model* model, Operator* conv_base_op, Operator* stb_op,
Operator* post_stb_op, bool has_expand_op,
int dilation_factor) {
auto* conv_op = static_cast<T*>(conv_base_op);
if (conv_op->inputs.size() != 2) {
// The conv op must only have weights, no bias.
return false;
}
CHECK_EQ(conv_op->outputs.size(), 1);
// Squeeze Op
auto* post_conv_op = GetOpWithInput(*model, conv_op->outputs[0]);
if (!post_conv_op) {
return false;
}
if (has_expand_op) {
if (post_conv_op->type != OperatorType::kSqueeze) {
// If an expand op was used, the post-conv op must be a squeeze op
return false;
}
CHECK_EQ(post_conv_op->inputs.size(), 1);
CHECK_EQ(post_conv_op->outputs.size(), 1);
}
// Pad Op
const auto* pad_op = has_expand_op
? GetOpWithInput(*model, post_conv_op->outputs[0])
: GetOpWithInput(*model, conv_op->outputs[0]);
bool has_pad_op = false;
if (pad_op && pad_op->type == OperatorType::kPad) {
has_pad_op = true;
CHECK_EQ(pad_op->inputs.size(), 2);
CHECK_EQ(pad_op->outputs.size(), 1);
}
// TODO(mjmatthews): Perform validity checking on padding dimensions.
// Pre-BatchToSpace Bias Op
auto* next_op = has_pad_op
? GetOpWithInput(*model, pad_op->outputs[0])
: has_expand_op
? GetOpWithInput(*model, post_conv_op->outputs[0])
: GetOpWithInput(*model, conv_op->outputs[0]);
bool has_bias_before_bts = false;
if (next_op->type == OperatorType::kAdd) {
has_bias_before_bts = true;
}
auto final_op = GetOpWithInput(*model, next_op->outputs[0]);
// BatchToSpace Op
const auto* bts_op = has_bias_before_bts ? final_op : next_op;
if (bts_op->type != OperatorType::kBatchToSpaceND) {
return false;
}
CHECK_EQ(bts_op->inputs.size(), 3);
CHECK_EQ(bts_op->outputs.size(), 1);
// Post-BatchToSpace Bias Op
Operator* bias_add_op = !has_bias_before_bts ? final_op : next_op;
if (bias_add_op->type != OperatorType::kAdd) {
// Bias op is required before or after BatchToSpace
return false;
}
CHECK_EQ(bias_add_op->inputs.size(), 2);
CHECK_EQ(bias_add_op->outputs.size(), 1);
// If still Pad Op is not present, there might be possiblity it is added
// before STB Op like below Pad -> SpaceToBatchND -> Expand -> Conv2D ->
// Squeeze -> BatchToSpaceND -> BiasAdd So eliminate this Pad Op as well
if (!has_pad_op) {
auto* pre_stb_pad_op = GetOpWithOutput(*model, stb_op->inputs[0]);
// If it is a Pad Op then just rewire the Input of Pad Op with Input of STB
if (pre_stb_pad_op && pre_stb_pad_op->type == OperatorType::kPad) {
stb_op->inputs[0] = pre_stb_pad_op->inputs[0];
has_pad_op = true;
pad_op = pre_stb_pad_op;
}
}
// 2. RE-WIRE OPERATORS
// ***************************************************************************
// Re-use the existing Conv2D op.
conv_op->dilation_width_factor = dilation_factor;
conv_op->dilation_height_factor = dilation_factor;
conv_op->padding.type = PaddingType::kSame;
// Rewire the ops to bypass SpaceToBatch, BatchToSpace, and Pad.
bias_add_op->outputs[0] = final_op->outputs[0];
if (has_expand_op) {
bias_add_op->inputs[0] = post_conv_op->outputs[0];
post_conv_op->inputs[0] = conv_op->outputs[0];
conv_op->inputs[0] = post_stb_op->outputs[0];
post_stb_op->inputs[0] = stb_op->inputs[0];
} else {
bias_add_op->inputs[0] = conv_op->outputs[0];
conv_op->inputs[0] = stb_op->inputs[0];
}
// TODO(mjmatthews): Connect bias directly into the Conv2D?
// 3. DELETE LEFTOVER OPERATORS
// ***************************************************************************
DeleteOpAndArrays(model, bts_op);
DeleteOpAndArrays(model, stb_op);
if (has_pad_op) {
DeleteOpAndArrays(model, pad_op);
}
return true;
}
absl::Status IdentifyDilatedConv::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto it = model->operators.begin() + op_index;
auto* stb_op = it->get();
// 1. IDENTIFY OPERATORS
// ***************************************************************************
// SpaceToBatch Op.
if (stb_op->type != OperatorType::kSpaceToBatchND) {
return absl::OkStatus();
}
if (stb_op->inputs.size() != 3) {
return absl::OkStatus();
}
CHECK_EQ(stb_op->outputs.size(), 1);
// Extract the dilation factor from Input[1] of SpaceToBatch
// TODO(mjmatthews): Support 2D dilation factors.
const auto& block_shape_array = model->GetArray(stb_op->inputs[1]);
if (!block_shape_array.buffer) {
return absl::OkStatus();
}
CHECK_EQ(block_shape_array.shape().dimensions_count(), 1);
int dilation_factor =
block_shape_array.Array::GetBuffer<ArrayDataType::kInt32>().data[0];
// Expand Op
auto* post_stb_op = GetOpWithInput(*model, stb_op->outputs[0]);
if (!post_stb_op) {
return absl::OkStatus();
}
bool has_expand_op = false;
if (post_stb_op->type == OperatorType::kExpandDims) {
has_expand_op = true;
CHECK_EQ(post_stb_op->inputs.size(), 2);
CHECK_EQ(post_stb_op->outputs.size(), 1);
}
// Conv Op
const std::string& input_of_conv_op =
has_expand_op ? post_stb_op->outputs[0] : stb_op->outputs[0];
auto* conv_base_op = GetOpWithInput(*model, input_of_conv_op);
bool changed = false;
if (conv_base_op->type == OperatorType::kConv) {
changed = ResolveDilatedConv<ConvOperator>(model, conv_base_op, stb_op,
post_stb_op, has_expand_op,
dilation_factor);
if (changed) {
LOG(INFO) << "Replaced sub-network with Dilated Conv2D op outputting \""
<< conv_base_op->outputs[0] << "\".";
}
} else if (identify_depthwise_conv_ &&
conv_base_op->type == OperatorType::kDepthwiseConv) {
changed = ResolveDilatedConv<DepthwiseConvOperator>(
model, conv_base_op, stb_op, post_stb_op, has_expand_op,
dilation_factor);
if (changed) {
LOG(INFO)
<< "Replaced sub-network with Dilated DepthwiseConv2D op outputting "
<< "\"" << conv_base_op->outputs[0] << "\".";
}
}
*modified = changed;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,118 @@
/* Copyright 2019 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <algorithm>
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/identify_util.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/runtime/types.h"
#include "tensorflow/lite/toco/tooling_util.h"
// This transformation rule tries to identify the HardSwish structure generated
// by tensorflow.
// The formula of hardswish is:
// f(x) = x * relu6((x+3))/6
//
// We look for the following tensorflow subgraph:
// x * tf.nn.relu6(x + np.float32(3)) * np.float32(1. / 6.)
namespace toco {
using util::IsBinaryOp;
absl::Status IdentifyHardSwish::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto add_with_relu6_op_it = (model->operators.begin() + op_index);
const auto add_with_relu6_op = add_with_relu6_op_it->get();
if (!util::IsBinaryOp(add_with_relu6_op, OperatorType::kAdd,
FusedActivationFunctionType::kRelu6)) {
return absl::OkStatus();
}
std::vector<const Operator*> ops;
ops.push_back(add_with_relu6_op);
const auto* mul_op = GetOpWithInput(*model, add_with_relu6_op->outputs[0]);
ops.push_back(mul_op);
if (mul_op->type == OperatorType::kFakeQuant) {
mul_op = GetOpWithInput(*model, mul_op->outputs[0]);
ops.push_back(mul_op);
}
if (!IsBinaryOp(mul_op, OperatorType::kMul)) {
return absl::OkStatus();
}
const auto* output_op = GetOpWithInput(*model, mul_op->outputs[0]);
ops.push_back(output_op);
if (output_op->type == OperatorType::kFakeQuant) {
output_op = GetOpWithInput(*model, output_op->outputs[0]);
ops.push_back(output_op);
}
if (!IsBinaryOp(output_op, OperatorType::kMul)) {
return absl::OkStatus();
}
const auto add_3_tensor =
util::GetSingleScalarInputIndexOfBinaryOp(model, add_with_relu6_op, 3.0f);
if (add_3_tensor < 0) {
// Expected 3.0f got something else.;
return absl::OkStatus();
}
const auto input_tensor_name = add_with_relu6_op->inputs[1 - add_3_tensor];
// Now we verify that the 3 mul arguments are respectively:
// 1. non-constant input of add_with_relu6_op
// 2. 1/6
// 3. (and add_with_relu6_op[0].outputs[0] - which we already know!)
std::vector<std::string> mul_inputs = mul_op->inputs;
mul_inputs.insert(mul_inputs.end(), output_op->inputs.begin(),
output_op->inputs.end());
// 1. Check that we have the input tensor as one of the multiplicants
if (std::find(mul_inputs.begin(), mul_inputs.end(), input_tensor_name) ==
mul_inputs.end()) {
// Input tensor not found! << input_tensor_name << std::endl;
return absl::OkStatus();
}
// 2. Find 1/6
bool found = false;
for (const auto& input : mul_inputs) {
found |= util::CheckArrayIsScalarFloat(model, input, 1.f / 6.f);
}
if (!found) {
// Input tensor is not divided by 6!.";
return absl::OkStatus();
}
// Success! Now delete the subgraph and instert new one
const auto output_tensor_name = output_op->outputs[0];
auto* hardswish_op = new HardSwishOperator;
hardswish_op->inputs = {input_tensor_name};
hardswish_op->outputs = {output_tensor_name};
model->operators.emplace(add_with_relu6_op_it, hardswish_op);
AddMessageF("Creating hardswish op (%s) replacing equivalent subgraph",
LogName(*hardswish_op));
while (!ops.empty()) {
DeleteOpAndArrays(model, ops.back());
ops.pop_back();
}
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,151 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cmath>
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status IdentifyL2Normalization::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto div_it = model->operators.begin() + op_index;
const auto* div_or_mul_op = div_it->get();
OperatorType expected_op_type_producing_div_or_mul_input;
if (div_or_mul_op->type == OperatorType::kDiv) {
expected_op_type_producing_div_or_mul_input = OperatorType::kSqrt;
} else if (div_or_mul_op->type == OperatorType::kMul) {
expected_op_type_producing_div_or_mul_input = OperatorType::kRsqrt;
} else {
return absl::OkStatus();
}
CHECK_EQ(div_or_mul_op->inputs.size(), 2);
Operator* op_producing_div_or_mul_input[2] = {
GetOpWithOutput(*model, div_or_mul_op->inputs[0]),
GetOpWithOutput(*model, div_or_mul_op->inputs[1]),
};
if (!op_producing_div_or_mul_input[1] ||
op_producing_div_or_mul_input[1]->type !=
expected_op_type_producing_div_or_mul_input) {
return absl::OkStatus();
}
Operator* sqrt_or_rsqrt_op = op_producing_div_or_mul_input[1];
CHECK_EQ(sqrt_or_rsqrt_op->inputs.size(), 1);
Operator* op_producing_sqrt_or_rsqrt_input =
GetOpWithOutput(*model, sqrt_or_rsqrt_op->inputs[0]);
if (!op_producing_sqrt_or_rsqrt_input) {
return absl::OkStatus();
}
// There may be an Add or a Maximum here, adding or clamping to a "small"
// constant scalar.
// Reported bug: b/29395854
Operator* add_op = nullptr;
Operator* op_producing_add_input = nullptr;
if (op_producing_sqrt_or_rsqrt_input->type == OperatorType::kAdd ||
op_producing_sqrt_or_rsqrt_input->type == OperatorType::kMaximum) {
add_op = op_producing_sqrt_or_rsqrt_input;
bool add_can_be_removed = false;
CHECK_EQ(op_producing_sqrt_or_rsqrt_input->inputs.size(), 2);
for (int i = 0; i < 2; i++) {
const auto& input_array =
model->GetArray(op_producing_sqrt_or_rsqrt_input->inputs[i]);
if (!input_array.buffer) {
continue;
}
if (input_array.buffer->type != ArrayDataType::kFloat) {
continue;
}
if (RequiredBufferSizeForShape(input_array.shape()) != 1) {
continue;
}
const auto& input_float_data =
input_array.GetBuffer<ArrayDataType::kFloat>().data;
if (std::abs(input_float_data[0]) > 1e-3f) {
continue;
}
add_can_be_removed = true;
op_producing_add_input = GetOpWithOutput(*model, add_op->inputs[1 - i]);
break;
}
if (!add_can_be_removed) {
AddMessageF(
"Giving up trying to identify L2Normalization subgraph "
" because the operator producing the input to the square root, %s,"
", does not match the expected pattern",
LogName(*op_producing_sqrt_or_rsqrt_input));
return absl::OkStatus();
}
}
Operator* sum_op =
add_op ? op_producing_add_input : op_producing_sqrt_or_rsqrt_input;
if (sum_op->type != OperatorType::kSum) {
AddMessageF(
"Giving up trying to identify L2Normalization subgraph: "
"expected Sum op, got %s",
LogName(*sum_op));
return absl::OkStatus();
}
Operator* square_op = GetOpWithOutput(*model, sum_op->inputs[0]);
if (square_op->type != OperatorType::kSquare) {
AddMessageF(
"Giving up trying to identify L2Normalization subgraph: "
"expected Square op, got %s",
LogName(*square_op));
return absl::OkStatus();
}
CHECK_EQ(square_op->inputs.size(), 1);
if (square_op->inputs[0] != div_or_mul_op->inputs[0]) {
AddMessageF(
"Giving up trying to identify L2Normalization subgraph: %s does not "
"take the same input as the Mul/Div node",
LogName(*square_op));
return absl::OkStatus();
}
// Create and emplace the new L2Normalization
auto* l2norm_op = new L2NormalizationOperator;
l2norm_op->inputs = {div_or_mul_op->inputs[0]};
l2norm_op->outputs = div_or_mul_op->outputs;
model->operators.emplace(div_it, l2norm_op);
AddMessageF("Creating %s replacing equivalent subgraph", LogName(*l2norm_op));
// Erase the subgraph that is now replaced by L2Normalization
DeleteOpAndArrays(model, square_op);
DeleteOpAndArrays(model, sum_op);
if (add_op) {
DeleteOpAndArrays(model, add_op);
}
DeleteOpAndArrays(model, sqrt_or_rsqrt_op);
DeleteOpAndArrays(model, div_or_mul_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,99 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status IdentifyL2Pool::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto sqrt_it = model->operators.begin() + op_index;
const auto* sqrt_op = sqrt_it->get();
if (sqrt_op->type != OperatorType::kSqrt) {
return absl::OkStatus();
}
CHECK_EQ(sqrt_op->inputs.size(), 1);
CHECK_EQ(sqrt_op->outputs.size(), 1);
const AveragePoolOperator* avpool_op;
const Operator* square_op;
Operator* prev_to_sqrt_op = GetOpWithOutput(*model, sqrt_op->inputs[0]);
if (prev_to_sqrt_op == nullptr) {
AddMessageF(
"Giving up trying to identify L2Pool subgraph: "
"expected AveragePool op, but Sqrt op has no preceding op");
return absl::OkStatus();
}
if (prev_to_sqrt_op->type != OperatorType::kAveragePool) {
AddMessageF(
"Giving up trying to identify L2Pool subgraph: "
"expected AveragePool op, got %s",
LogName(*prev_to_sqrt_op));
return absl::OkStatus();
}
avpool_op = static_cast<const AveragePoolOperator*>(prev_to_sqrt_op);
CHECK_EQ(avpool_op->inputs.size(), 1);
square_op = GetOpWithOutput(*model, avpool_op->inputs[0]);
CHECK_EQ(square_op->inputs.size(), 1);
if (square_op->type != OperatorType::kSquare) {
AddMessageF(
"Giving up trying to identify L2Pool subgraph: "
"expected Square op, got %s",
LogName(*square_op));
return absl::OkStatus();
}
// Create and emplace L2Pool node.
auto* l2pool_op = new L2PoolOperator;
l2pool_op->inputs = {square_op->inputs[0]};
l2pool_op->outputs = sqrt_op->outputs;
l2pool_op->padding.type = avpool_op->padding.type;
// Note that we do not setup avpool_op->padding.fixed here. This is done by
// the PropagateFixedSizes graph transformation.
l2pool_op->stride_height = avpool_op->stride_height;
l2pool_op->stride_width = avpool_op->stride_width;
l2pool_op->kheight = avpool_op->kheight;
l2pool_op->kwidth = avpool_op->kwidth;
model->operators.emplace(sqrt_it, l2pool_op);
AddMessageF("Creating %s replacing equivalent subgraph", LogName(*l2pool_op));
DeleteOpAndArrays(model, square_op);
DeleteOpAndArrays(model, avpool_op);
DeleteOpAndArrays(model, sqrt_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,315 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/runtime/types.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
std::vector<std::unique_ptr<Operator>>::iterator FindOperator(
Model* model, const Operator& op) {
auto it = model->operators.begin();
for (; it != model->operators.end(); ++it) {
if (it->get() == &op) {
break;
}
}
return it;
}
bool ValidateSourceOp(const Model& model, const std::string& array_name,
OperatorType op_type, Operator** source_op) {
if (op_type == OperatorType::kNone) {
CHECK(!source_op);
} else {
CHECK(source_op);
*source_op = GetOpWithOutput(model, array_name);
if (*source_op == nullptr) {
return false;
}
// Check that first operator, if connected, is of correct type
if ((*source_op)->type != op_type) {
return false;
}
}
return true;
}
// Returns true if the given operator has exactly 1 input, and is connected to
// the given op_type.
// We use kNone to indicate an input unattached to an operator output. Usually
// these are the static input arrays.
bool MatchOperatorInputs(const Operator& op, const Model& model,
OperatorType op_type, Operator** connected_op) {
// Check for required number of inputs
if (op.inputs.size() != 1) {
return false;
}
// Check if first input is disconnected/connected to an operator
if (!ValidateSourceOp(model, op.inputs[0], op_type, connected_op)) {
return false;
}
return true;
}
// Returns true if the given operator has exactly 2 inputs, which are connected
// to the given op_types.
// We use kNone to indicate an input unattached to an operator output. Usually
// these are the static input arrays.
bool MatchOperatorInputs(const Operator& op, const Model& model,
OperatorType a_op_type, Operator** a_op,
OperatorType b_op_type, Operator** b_op) {
// Check for required number of inputs
if (op.inputs.size() != 2) {
return false;
}
// Check if first input is disconnected/connected to an operator
if (!ValidateSourceOp(model, op.inputs[0], a_op_type, a_op)) {
return false;
}
// Check if second input is disconnected/connected to an operator
if (!ValidateSourceOp(model, op.inputs[1], b_op_type, b_op)) {
return false;
}
return true;
}
// Returns true if the given operator has exactly 3 inputs, which are connected
// to the given op_types.
// We use kNone to indicate an input unattached to an operator output. Usually
// these are the static input arrays.
bool MatchOperatorInputs(const Operator& op, const Model& model,
OperatorType a_op_type, Operator** a_op,
OperatorType b_op_type, Operator** b_op,
OperatorType c_op_type, Operator** c_op) {
// Check for required number of inputs
if (op.inputs.size() != 3) {
return false;
}
// Check if first input is disconnected/connected to an operator
if (!ValidateSourceOp(model, op.inputs[0], a_op_type, a_op)) {
return false;
}
// Check if second input is disconnected/connected to an operator
if (!ValidateSourceOp(model, op.inputs[1], b_op_type, b_op)) {
return false;
}
// Check if third input is disconnected/connected to an operator
if (!ValidateSourceOp(model, op.inputs[2], c_op_type, c_op)) {
return false;
}
return true;
}
} // namespace
absl::Status IdentifyLstmCell::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
// This LSTM cell identification method is not invariant to commutation of
// commutative operator inputs. For example, if input[0] and input[1] of the
// final output multiplication were swapped, this method would not identify it
// as an LSTM cell. This is OK in most cases, because
// tf.rnn.contrib.BasicLSTMCell always generates LSTM cells the same way.
// Final output multiply
auto op_it = model->operators.begin() + op_index;
Operator* final_output_mul = op_it->get();
if (final_output_mul->type != OperatorType::kMul) {
return absl::OkStatus();
}
// final_output_mul->outputs[0] would be one of the two outputs of our
// LstmCell. Exit if it does not already have a data type.
// We won't be able to propagate data types through a fused LstmCell.
if (model->GetArray(final_output_mul->outputs[0]).data_type ==
ArrayDataType::kNone) {
return absl::OkStatus();
}
Operator *state_output_tanh, *fc_output_sig;
if (!MatchOperatorInputs(*final_output_mul, *model, OperatorType::kTanh,
&state_output_tanh, OperatorType::kLogistic,
&fc_output_sig)) {
return absl::OkStatus();
}
// state_output_tanh->inputs[0] would be one of the two outputs of our
// LstmCell. Exit if it does not already have a data type.
// We won't be able to propagate data types through a fused LstmCell.
if (model->GetArray(state_output_tanh->inputs[0]).data_type ==
ArrayDataType::kNone) {
return absl::OkStatus();
}
// State output TanH
// (We don't count an operator as ID'd until we verify it has the correct
// operator types feeding into it.)
Operator* state_combine_add;
if (!MatchOperatorInputs(*state_output_tanh, *model, OperatorType::kAdd,
&state_combine_add)) {
return absl::OkStatus();
}
// State forget & remember addition
Operator *state_forget_mul, *state_remember_mul;
if (!MatchOperatorInputs(*state_combine_add, *model, OperatorType::kMul,
&state_forget_mul, OperatorType::kMul,
&state_remember_mul)) {
return absl::OkStatus();
}
const std::string prev_state = state_forget_mul->inputs[0];
// State forget gate
Operator* state_forget_sig;
if (!MatchOperatorInputs(*state_forget_mul, *model, OperatorType::kNone,
nullptr, OperatorType::kLogistic,
&state_forget_sig)) {
return absl::OkStatus();
}
// State remember gate
Operator *state_remember_sig, *state_info_tanh;
if (!MatchOperatorInputs(*state_remember_mul, *model, OperatorType::kLogistic,
&state_remember_sig, OperatorType::kTanh,
&state_info_tanh)) {
return absl::OkStatus();
}
// State remember "information" activation function
Operator* fc_output_split;
if (!MatchOperatorInputs(*state_info_tanh, *model, OperatorType::kSplit,
&fc_output_split)) {
return absl::OkStatus();
}
// State remember gate activation function
Operator* tmp;
if (!MatchOperatorInputs(*state_remember_sig, *model, OperatorType::kSplit,
&tmp) ||
(tmp != fc_output_split)) {
return absl::OkStatus();
}
// State forget gate activation function
if (!MatchOperatorInputs(*state_forget_sig, *model, OperatorType::kSplit,
&tmp) ||
(tmp != fc_output_split)) {
return absl::OkStatus();
}
// Fully connected output activation function
if (!MatchOperatorInputs(*fc_output_sig, *model, OperatorType::kSplit,
&tmp) ||
(tmp != fc_output_split)) {
return absl::OkStatus();
}
// Fully connected output split
Operator* fully_connected;
if (!MatchOperatorInputs(*fc_output_split, *model, OperatorType::kNone,
nullptr, OperatorType::kFullyConnected,
&fully_connected)) {
return absl::OkStatus();
}
// Fully connected op
Operator* concat_inputs;
if (!MatchOperatorInputs(*fully_connected, *model,
OperatorType::kConcatenation, &concat_inputs,
OperatorType::kNone, nullptr, OperatorType::kNone,
nullptr)) {
return absl::OkStatus();
}
if (static_cast<FullyConnectedOperator*>(fully_connected)->weights_format !=
FullyConnectedWeightsFormat::kDefault) {
// Not yet implemented: experimental shuffled weights in fused LSTM cell.
return absl::OkStatus();
}
// Emplace a new LSTM cell operator
auto* lstm_cell_op = new LstmCellOperator;
lstm_cell_op->inputs.resize(LstmCellOperator::NUM_INPUTS);
lstm_cell_op->inputs[LstmCellOperator::DATA_INPUT] = concat_inputs->inputs[0];
lstm_cell_op->inputs[LstmCellOperator::PREV_ACTIV_INPUT] =
concat_inputs->inputs[1];
lstm_cell_op->inputs[LstmCellOperator::WEIGHTS_INPUT] =
fully_connected->inputs[1];
lstm_cell_op->inputs[LstmCellOperator::BIASES_INPUT] =
fully_connected->inputs[2];
lstm_cell_op->inputs[LstmCellOperator::PREV_STATE_INPUT] = prev_state;
lstm_cell_op->outputs.resize(LstmCellOperator::NUM_OUTPUTS);
lstm_cell_op->outputs[LstmCellOperator::STATE_OUTPUT] =
state_output_tanh->inputs[0];
lstm_cell_op->outputs[LstmCellOperator::ACTIV_OUTPUT] =
final_output_mul->outputs[0];
model->operators.emplace(op_it, lstm_cell_op);
AddMessageF("Creating %s replacing equivalent subgraph",
LogName(*lstm_cell_op));
// Create temp arrays used internally during runtime.
const std::string base_name(FindLongestCommonPrefix(
lstm_cell_op->outputs[LstmCellOperator::STATE_OUTPUT],
lstm_cell_op->outputs[LstmCellOperator::ACTIV_OUTPUT]));
const std::string& concat_temp_array_name =
AvailableArrayName(*model, base_name + "concat_temp");
auto& concat_temp_array = model->GetOrCreateArray(concat_temp_array_name);
concat_temp_array.data_type =
model->GetArray(concat_inputs->outputs[0]).data_type;
lstm_cell_op->outputs[LstmCellOperator::CONCAT_TEMP] = concat_temp_array_name;
const std::string& activ_temp_array_name =
AvailableArrayName(*model, base_name + "activ_temp");
auto& activ_temp_array = model->GetOrCreateArray(activ_temp_array_name);
activ_temp_array.data_type =
model->GetArray(fully_connected->outputs[0]).data_type;
lstm_cell_op->outputs[LstmCellOperator::ACTIV_TEMP] = activ_temp_array_name;
AddMessageF("Created temp outputs %s and %s on operator %s",
concat_temp_array_name, activ_temp_array_name,
LogName(*lstm_cell_op));
DeleteOpAndArrays(model, final_output_mul);
DeleteOpAndArrays(model, state_output_tanh);
DeleteOpAndArrays(model, fc_output_sig);
DeleteOpAndArrays(model, state_combine_add);
DeleteOpAndArrays(model, state_forget_mul);
DeleteOpAndArrays(model, state_remember_mul);
DeleteOpAndArrays(model, state_forget_sig);
DeleteOpAndArrays(model, state_info_tanh);
DeleteOpAndArrays(model, state_remember_sig);
DeleteOpAndArrays(model, fc_output_split);
DeleteOpAndArrays(model, fully_connected);
DeleteOpAndArrays(model, concat_inputs);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,181 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/lstm_utils.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status MergeLstmCellInputs::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
// Find lstm cell.
auto op_it = model->operators.begin() + op_index;
auto src_op = op_it->get();
if (src_op->type != OperatorType::kLstmCell) {
return absl::OkStatus();
}
// Already a compact LstmCell. Do not need to merge cell inputs.
const auto* src_lstm_op = static_cast<LstmCellOperator*>(src_op);
if (src_lstm_op->kernel_type != LstmCellOperator::KERNEL_FULL ||
src_lstm_op->inputs.size() != kExtendedLstmInputCount) {
return absl::OkStatus();
}
// Identify prev_activ_input, prev_state_input as required Op inputs,
// using the rnn_states in the model flag.
std::string prev_activ_input;
if (!GetMatchingRnnArray(model, src_op->outputs[kOutputTensor],
&prev_activ_input)) {
return absl::OkStatus();
}
std::string prev_state_input;
if (!GetMatchingRnnArray(model, src_op->outputs[kCellStateTensor],
&prev_state_input)) {
return absl::OkStatus();
}
// Get LstmCell's cell, input, output size.
int num_cell = model->GetArray(src_op->inputs[kInputToInputWeightsTensor])
.shape()
.dims(0);
int num_input = model->GetArray(src_op->inputs[kInputToInputWeightsTensor])
.shape()
.dims(1);
int num_output =
model->GetArray(src_op->inputs[kRecurrentToInputWeightsTensor])
.shape()
.dims(1);
// Make sure n_cell and n_output are equal as there is no projection.
CHECK_EQ(num_cell, num_output);
// Create tensorflow_graphdef style's one big weight tensor.
const std::string base_name(FindLongestCommonPrefix(
src_op->outputs[kOutputTensor], src_op->outputs[kCellStateTensor]));
std::string merged_weights =
AvailableArrayName(*model, base_name + "weights");
auto& array = model->GetOrCreateArray(merged_weights);
array.data_type = ArrayDataType::kFloat;
int weights_dim1 = 4 * num_cell;
int weights_dim2 = num_input + num_output;
Shape shape = Shape({weights_dim1, weights_dim2});
array.copy_shape(shape);
auto& buffer = array.GetMutableBuffer<ArrayDataType::kFloat>();
buffer.data.resize(weights_dim1 * weights_dim2);
// Merge 8 small weight tensors to 1 weight tensor.
CopyArrayToSubArray(
buffer, weights_dim2,
model->GetArray(src_op->inputs[kInputToInputWeightsTensor]), 0, 0);
CopyArrayToSubArray(
buffer, weights_dim2,
model->GetArray(src_op->inputs[kInputToCellWeightsTensor]), num_cell, 0);
CopyArrayToSubArray(
buffer, weights_dim2,
model->GetArray(src_op->inputs[kInputToForgetWeightsTensor]),
num_cell * 2, 0);
CopyArrayToSubArray(
buffer, weights_dim2,
model->GetArray(src_op->inputs[kInputToOutputWeightsTensor]),
num_cell * 3, 0);
CopyArrayToSubArray(
buffer, weights_dim2,
model->GetArray(src_op->inputs[kRecurrentToInputWeightsTensor]), 0,
num_input);
CopyArrayToSubArray(
buffer, weights_dim2,
model->GetArray(src_op->inputs[kRecurrentToCellWeightsTensor]), num_cell,
num_input);
CopyArrayToSubArray(
buffer, weights_dim2,
model->GetArray(src_op->inputs[kRecurrentToForgetWeightsTensor]),
num_cell * 2, num_input);
CopyArrayToSubArray(
buffer, weights_dim2,
model->GetArray(src_op->inputs[kRecurrentToOutputWeightsTensor]),
num_cell * 3, num_input);
// Create tensorflow_graphdef style's one big bias tensor.
std::string merged_biases = AvailableArrayName(*model, base_name + "biases");
auto& bias_array = model->GetOrCreateArray(merged_biases);
bias_array.data_type = ArrayDataType::kFloat;
bias_array.copy_shape(Shape({weights_dim1}));
auto& bias_buffer = bias_array.GetMutableBuffer<ArrayDataType::kFloat>();
bias_buffer.data.resize(weights_dim1);
// Merge 4 small bias tensors into a big one.
CopyArrayToSubArray(bias_buffer, weights_dim2,
model->GetArray(src_op->inputs[kInputGateBiasTensor]), 0,
0);
CopyArrayToSubArray(bias_buffer, weights_dim2,
model->GetArray(src_op->inputs[kCellGateBiasTensor]),
num_cell, 0);
CopyArrayToSubArray(bias_buffer, weights_dim2,
model->GetArray(src_op->inputs[kForgetGateBiasTensor]),
num_cell * 2, 0);
CopyArrayToSubArray(bias_buffer, weights_dim2,
model->GetArray(src_op->inputs[kOutputGateBiasTensor]),
num_cell * 3, 0);
// Emplace a new LSTM cell operator (use basic 5 inputs kernel).
auto lstm_cell_op = std::make_unique<LstmCellOperator>();
lstm_cell_op->kernel_type = LstmCellOperator::KERNEL_BASIC;
// Compact LstmCell's 5 inputs.
lstm_cell_op->inputs.resize(LstmCellOperator::NUM_INPUTS);
lstm_cell_op->inputs[LstmCellOperator::DATA_INPUT] =
src_op->inputs[kInputTensor];
lstm_cell_op->inputs[LstmCellOperator::WEIGHTS_INPUT] = merged_weights;
lstm_cell_op->inputs[LstmCellOperator::BIASES_INPUT] = merged_biases;
lstm_cell_op->inputs[LstmCellOperator::PREV_ACTIV_INPUT] = prev_activ_input;
lstm_cell_op->inputs[LstmCellOperator::PREV_STATE_INPUT] = prev_state_input;
// Reorder LstmCell's 3 outputs.
lstm_cell_op->outputs.resize(LstmCellOperator::NUM_OUTPUTS);
lstm_cell_op->outputs[LstmCellOperator::ACTIV_OUTPUT] =
src_op->outputs[kOutputTensor];
lstm_cell_op->outputs[LstmCellOperator::STATE_OUTPUT] =
src_op->outputs[kCellStateTensor];
lstm_cell_op->outputs[LstmCellOperator::ACTIV_TEMP] =
src_op->outputs[kOutputStateTensor];
// Create a new temp array for the fourth output.
const std::string& concat_temp_array_name =
AvailableArrayName(*model, base_name + "concat_temp");
model->GetOrCreateArray(concat_temp_array_name);
lstm_cell_op->outputs[LstmCellOperator::CONCAT_TEMP] = concat_temp_array_name;
// Add the op into model.
model->operators.emplace(op_it, std::move(lstm_cell_op));
AddMessageF("Creating compact LstmCell replacing previous lstm cell");
DeleteOpAndArrays(model, src_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,173 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/lstm_utils.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status SplitLstmCellInputs::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
// Find lstm cell.
auto op_it = model->operators.begin() + op_index;
auto curr_op = op_it->get();
if (curr_op->type != OperatorType::kLstmCell) {
return absl::OkStatus();
}
const auto* curr_lstm_op = static_cast<LstmCellOperator*>(curr_op);
// Already an extended LstmCell. Do not need to split cell inputs.
if (curr_lstm_op->kernel_type != LstmCellOperator::KERNEL_BASIC ||
curr_lstm_op->inputs.size() != LstmCellOperator::NUM_INPUTS) {
return absl::OkStatus();
}
// Make sure the WEIGHTS_INPUT and BIASES_INPUT are constant arrays,
// that are able to be split into smaller weight and bias tensors.
if (!IsConstantParameterArray(
*model, curr_op->inputs[LstmCellOperator::WEIGHTS_INPUT]) ||
!IsConstantParameterArray(
*model, curr_op->inputs[LstmCellOperator::BIASES_INPUT])) {
return absl::OkStatus();
}
// Make sure propagate_fixed_sizes has defined the size of the output.
if (!model->GetArray(curr_op->outputs[LstmCellOperator::ACTIV_OUTPUT])
.has_shape()) {
return absl::OkStatus();
}
// Emplace a new LstmCell operator with extended inputs (kernel/lstm.cc).
auto lstm_cell_op = std::make_unique<LstmCellOperator>();
lstm_cell_op->kernel_type = LstmCellOperator::KERNEL_FULL;
lstm_cell_op->inputs.resize(kExtendedLstmInputCount);
int num_input = model->GetArray(curr_op->inputs[LstmCellOperator::DATA_INPUT])
.shape()
.dims(1);
// n_cell and n_output have the same size when there is no projection.
int num_cell =
model->GetArray(curr_op->outputs[LstmCellOperator::ACTIV_OUTPUT])
.shape()
.dims(1);
int num_output = num_cell;
// Data input.
lstm_cell_op->inputs[kInputTensor] =
curr_op->inputs[LstmCellOperator::ACTIV_OUTPUT];
// Previous states.
lstm_cell_op->inputs[kInputActivationStateTensor] =
curr_op->inputs[LstmCellOperator::PREV_ACTIV_INPUT];
lstm_cell_op->inputs[kInputCellStateTensor] =
curr_op->inputs[LstmCellOperator::PREV_STATE_INPUT];
// Get original weight tensor and decompose 1 tensor to 8 sub tensors.
Array& kernel =
model->GetArray(curr_op->inputs[LstmCellOperator::WEIGHTS_INPUT]);
const std::string base_name(FindLongestCommonPrefix(
curr_op->outputs[LstmCellOperator::ACTIV_OUTPUT],
curr_op->outputs[LstmCellOperator::STATE_OUTPUT]));
// Input weight tensors of size {n_cell, n_input}.
CopySubArrayToArray(
model, &(lstm_cell_op->inputs[kInputToInputWeightsTensor]),
base_name + "weight_i_i", num_cell, num_input, kernel, 0, 0);
CopySubArrayToArray(model, &(lstm_cell_op->inputs[kInputToCellWeightsTensor]),
base_name + "weight_c_i", num_cell, num_input, kernel,
num_cell, 0);
CopySubArrayToArray(
model, &(lstm_cell_op->inputs[kInputToForgetWeightsTensor]),
base_name + "weight_f_i", num_cell, num_input, kernel, num_cell * 2, 0);
CopySubArrayToArray(
model, &(lstm_cell_op->inputs[kInputToOutputWeightsTensor]),
base_name + "weight_o_i", num_cell, num_input, kernel, num_cell * 3, 0);
// Recurrent weight tensors of size {n_cell, n_output}.
CopySubArrayToArray(
model, &(lstm_cell_op->inputs[kRecurrentToInputWeightsTensor]),
base_name + "weight_i_r", num_cell, num_output, kernel, 0, num_input);
CopySubArrayToArray(model,
&(lstm_cell_op->inputs[kRecurrentToCellWeightsTensor]),
base_name + "weight_c_r", num_cell, num_output, kernel,
num_cell, num_input);
CopySubArrayToArray(model,
&(lstm_cell_op->inputs[kRecurrentToForgetWeightsTensor]),
base_name + "weight_f_r", num_cell, num_output, kernel,
num_cell * 2, num_input);
CopySubArrayToArray(model,
&(lstm_cell_op->inputs[kRecurrentToOutputWeightsTensor]),
base_name + "weight_o_r", num_cell, num_output, kernel,
num_cell * 3, num_input);
// Peephole (optional).
CreateOptionalArray(model, &(lstm_cell_op->inputs[kCellToInputWeightsTensor]),
base_name + "peephole_c_i");
CreateOptionalArray(model,
&(lstm_cell_op->inputs[kCellToForgetWeightsTensor]),
base_name + "peephole_c_f");
CreateOptionalArray(model,
&(lstm_cell_op->inputs[kCellToOutputWeightsTensor]),
base_name + "peephole_c_o");
// Get original bias tensor and decompose 1 tensor to 4 sub tensors
Array& bias =
model->GetArray(curr_op->inputs[LstmCellOperator::BIASES_INPUT]);
CopySubArrayToArray(model, &(lstm_cell_op->inputs[kInputGateBiasTensor]),
base_name + "bias_i", num_cell, 1, bias, 0, 0);
CopySubArrayToArray(model, &(lstm_cell_op->inputs[kCellGateBiasTensor]),
base_name + "bias_c", num_cell, 1, bias, num_cell, 0);
CopySubArrayToArray(model, &(lstm_cell_op->inputs[kForgetGateBiasTensor]),
base_name + "bias_f", num_cell, 1, bias, num_cell * 2, 0);
CopySubArrayToArray(model, &(lstm_cell_op->inputs[kOutputGateBiasTensor]),
base_name + "bias_o", num_cell, 1, bias, num_cell * 3, 0);
// Projection (optional).
CreateOptionalArray(model, &(lstm_cell_op->inputs[kProjectionWeightsTensor]),
base_name + "proj_weight");
CreateOptionalArray(model, &(lstm_cell_op->inputs[kProjectionBiasTensor]),
base_name + "proj_bias");
// Reorder and resize LstmCell's outputs.
lstm_cell_op->outputs.resize(
ExtendedLstmCellOutputs::kExtendedLstmOutputCount);
lstm_cell_op->outputs[kOutputStateTensor] =
curr_op->outputs[LstmCellOperator::ACTIV_TEMP];
lstm_cell_op->outputs[kCellStateTensor] =
curr_op->outputs[LstmCellOperator::STATE_OUTPUT];
lstm_cell_op->outputs[kOutputTensor] =
curr_op->outputs[LstmCellOperator::ACTIV_OUTPUT];
// Add the op into model.
model->operators.emplace(op_it, std::move(lstm_cell_op));
AddMessageF("Creating extended LstmCell replacing previous lstm cell");
DeleteOpAndArrays(model, curr_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,284 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "tensorflow/core/lib/core/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
// Sometimes, user may choose to use broadcast mul to perform nearest neighbor
// upsampling, how it works?
//
// If you want to upsample [batch, height, width, channel] to
// [batch, height * scale1, width * scale2, channel],
// you can do a broadcast mul like following:
// [batch, height, 1, width, 1 channel] with
// ones [1, 1, scale1, 1, scale2, channel].
//
// However, there's a 6-d tensor broadcasting mul operation which is not
// supported by the current runtime, also, it's less efficient for the quantized
// case if we can just do some memcpy.
// So we can transform this broadcast mul pattern to pack:
//
// [batch, height, 1, width, 1, channel]
// || Reshape
// \/
// [batch, height, width, channel]
// || Pack at axis 3, with scale2
// \/
// [batch, height, width, scale2, channel]
// || Pack at axis 2, with scale1
// \/
// [batch, height, scale1, width, scale2, channel]
namespace toco {
namespace {
void CopyShape(const std::vector<int>& new_shape, Array* array) {
for (int dim : new_shape) {
array->mutable_shape()->mutable_dims()->push_back(dim);
}
}
template <ArrayDataType DataType, typename T>
bool HasSameValues(const Array& input_array, T value) {
auto& buffer = input_array.GetBuffer<DataType>();
for (int i = 0; i < buffer.Length(); ++i) {
if (buffer.data.at(i) != value) {
return false;
}
}
return true;
}
std::vector<std::unique_ptr<Operator>>::iterator FindOperator(
Model* model, const Operator& op) {
return std::find_if(
model->operators.begin(), model->operators.end(),
[&op](const std::unique_ptr<Operator>& ptr) { return ptr.get() == &op; });
}
} // namespace
// It's possible the model uses mul-broadcast to implement nearest neighbor
// upsample which may involve 5-d, 6-d tensors. We can actually change this
// pattern to be pack-based which is easier for us to handle.
absl::Status IdentifyNearestUpsample::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto op_it = model->operators.begin() + op_index;
auto* op = op_it->get();
if (op->type != OperatorType::kMul) {
return absl::OkStatus();
}
// We only support one operand being constant.
const std::string& lhs = op->inputs.at(0);
const std::string& rhs = op->inputs.at(1);
const std::string& output = op->outputs.at(0);
Operator* next_op = GetOpWithOutput(*model, output);
if (next_op == nullptr) {
return absl::OkStatus();
}
if (IsConstantParameterArray(*model, lhs) ==
IsConstantParameterArray(*model, rhs)) {
return absl::OkStatus();
}
Array& const_array = IsConstantParameterArray(*model, lhs)
? model->GetArray(lhs)
: model->GetArray(rhs);
Array& nonconst_array = IsConstantParameterArray(*model, lhs)
? model->GetArray(rhs)
: model->GetArray(lhs);
Array& output_array = model->GetArray(output);
// Wait for shape propogation finished.
if (!const_array.has_shape() || !nonconst_array.has_shape() ||
!output_array.has_shape()) {
return absl::OkStatus();
}
// We need to make sure they have same dimension count & the const parameter
// only contain ones.
if (const_array.shape().dimensions_count() !=
nonconst_array.shape().dimensions_count()) {
return absl::OkStatus();
}
if (const_array.data_type == ArrayDataType::kFloat) {
if (!HasSameValues<ArrayDataType::kFloat, float>(const_array, 1))
return absl::OkStatus();
} else if (const_array.data_type == ArrayDataType::kInt32) {
if (!HasSameValues<ArrayDataType::kInt32, int>(const_array, 1))
return absl::OkStatus();
} else if (const_array.data_type == ArrayDataType::kInt8) {
if (!HasSameValues<ArrayDataType::kInt8, int8_t>(const_array, 127))
return absl::OkStatus();
} else if (const_array.data_type == ArrayDataType::kUint8) {
if (!HasSameValues<ArrayDataType::kUint8, uint8_t>(const_array, 255))
return absl::OkStatus();
} else {
return absl::OkStatus();
}
// We're recognizing the following patterns:
// non-const shape: [..., M, 1, ..., N, 1, ...]
// const shape: [..., 1, scale_m, ..., 1, scale_n, ...]
// and base on that, we're imaging the original shape, which is
// [..., M, ... N, ...], then we're a serious of packing starting with the
// lowest axis.
// Scale_factors will store [scale_m, scale_n, ...].
std::vector<int> scale_factors;
// Pack_axis with store [axis_for_M + 1, axis_for_N + 1,...]
std::vector<int> pack_axis;
std::vector<int> imagined_original_shape;
const auto& current_const_shape = const_array.shape();
const auto& current_nonconst_shape = nonconst_array.shape();
int imaged_original_shape_current_axis = 0;
int i = 0;
for (; i < current_const_shape.dimensions_count() - 1;
++i, ++imaged_original_shape_current_axis) {
if (current_const_shape.dims(i) == 1 &&
current_nonconst_shape.dims(i + 1) == 1 &&
current_nonconst_shape.dims(i) != 1) {
pack_axis.push_back(imaged_original_shape_current_axis + 1);
scale_factors.push_back(current_const_shape.dims(i + 1));
imagined_original_shape.push_back(current_nonconst_shape.dims(i));
++i;
} else {
imagined_original_shape.push_back(current_nonconst_shape.dims(i));
}
}
// Push the rest dim.
for (; i < current_nonconst_shape.dimensions_count(); ++i) {
imagined_original_shape.push_back(current_nonconst_shape.dims(i));
}
if (pack_axis.empty()) {
return absl::OkStatus();
}
std::vector<Operator*> to_be_inserted_ops;
// First put the reshape op.
// This reshape op will reshape the input tensor to what we imagined as the
// original shape.
auto* reshape_op = new TensorFlowReshapeOperator;
to_be_inserted_ops.push_back(reshape_op);
const std::string& original_array_name =
IsConstantParameterArray(*model, lhs) ? rhs : lhs;
reshape_op->inputs.push_back(original_array_name);
// Create shape param.
const std::string shape_array_name = AvailableArrayName(*model, "new_shape");
reshape_op->inputs.push_back(shape_array_name);
Array& shape_array = model->GetOrCreateArray(shape_array_name);
const int dim_size = imagined_original_shape.size();
*(shape_array.mutable_shape()->mutable_dims()) = {dim_size};
shape_array.data_type = ArrayDataType::kInt32;
auto& shape_buffer = shape_array.GetMutableBuffer<ArrayDataType::kInt32>();
// This is what imagined as the original shape.
for (size_t i = 0; i < imagined_original_shape.size(); ++i) {
shape_buffer.data.push_back(imagined_original_shape.at(i));
}
const std::string& reshape_output_name =
AvailableArrayName(*model, "reshape_output");
Array& reshape_output_array = model->GetOrCreateArray(reshape_output_name);
reshape_output_array.data_type = output_array.data_type;
CopyShape(imagined_original_shape, &reshape_output_array);
// Copy the quantization/minmax params if applicable.
if (output_array.minmax) {
reshape_output_array.GetOrCreateMinMax().min = output_array.minmax->min;
reshape_output_array.GetOrCreateMinMax().max = output_array.minmax->max;
}
if (output_array.quantization_params) {
reshape_output_array.GetOrCreateQuantizationParams().scale =
output_array.quantization_params->scale;
reshape_output_array.GetOrCreateQuantizationParams().zero_point =
output_array.quantization_params->zero_point;
}
reshape_op->outputs.push_back(reshape_output_name);
// Place the pack op as described in the file comment.
std::string current_pack_input_name = reshape_output_name;
std::vector<int> current_shape = imagined_original_shape;
for (int i = pack_axis.size() - 1; i >= 0; --i) {
auto* pack_op = new PackOperator;
int scale = scale_factors.at(i);
for (int j = 0; j < scale; ++j) {
pack_op->inputs.push_back(current_pack_input_name);
}
// The axis is computed before, the values count is actually the scale.
int axis = pack_axis.at(i);
pack_op->axis = axis;
pack_op->values_count = scale;
const std::string& pack_output_array_name =
AvailableArrayName(*model, absl::StrCat("pack_at_", axis));
// We need to copy the quantization/minmax params if applicable.
Array& pack_output_array = model->GetOrCreateArray(pack_output_array_name);
if (output_array.minmax) {
pack_output_array.GetOrCreateMinMax().min = output_array.minmax->min;
pack_output_array.GetOrCreateMinMax().max = output_array.minmax->max;
}
if (output_array.quantization_params) {
pack_output_array.GetOrCreateQuantizationParams().scale =
output_array.quantization_params->scale;
pack_output_array.GetOrCreateQuantizationParams().zero_point =
output_array.quantization_params->zero_point;
}
pack_output_array.data_type = nonconst_array.data_type;
current_shape.insert(current_shape.begin() + axis, scale);
CopyShape(current_shape, &pack_output_array);
pack_op->outputs.push_back(pack_output_array_name);
// The output is actually the input to the next pack op.
current_pack_input_name = pack_output_array_name;
to_be_inserted_ops.push_back(pack_op);
}
// Rewire the final pack op.
to_be_inserted_ops.at(to_be_inserted_ops.size() - 1)->outputs.clear();
to_be_inserted_ops.at(to_be_inserted_ops.size() - 1)
->outputs.push_back(op->outputs.at(0));
// Insert all added ops in a reverse order.
for (int i = to_be_inserted_ops.size() - 1; i >= 0; --i) {
op_it = model->operators.emplace(op_it, to_be_inserted_ops.at(i));
}
// Delete the mul op.
model->operators.erase(FindOperator(model, *op));
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,133 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/runtime/types.h"
#include "tensorflow/lite/toco/tooling_util.h"
// This transformation rule tries to identify the PRelu structure generated by
// Keras, and convert it to a single op.
//
// The formula of PReLU is:
// f(x) = alpha * x for x < 0, f(x) = x for x >= 0.
//
// `x` is the input, and `alpha` is a trainable tensor which can be broadcasted
// to the shape of `x`.
//
// There's no native PRelu op in TensorFlow, so Keras generates the following
// structure which does the equivalent calculation:
// f(x) = Relu(x) + (-alpha * Relu(-x))
//
// Practically, alpha is always a constant in the inference graph, and Toco have
// other graph transformations which fold the activation functions to other ops.
// Therefore, we're looking for the structure:
//
// f(x) = Relu(x) + (negative_alpha * Neg(x, activation=Relu))
namespace toco {
absl::Status IdentifyPRelu::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto add_op_it = model->operators.begin() + op_index;
const auto* add_op = add_op_it->get();
if (add_op == nullptr || add_op->type != OperatorType::kAdd ||
add_op->inputs.size() != 2 ||
add_op->fused_activation_function != FusedActivationFunctionType::kNone) {
return absl::OkStatus();
}
const auto* relu_input_op = GetOpWithOutput(*model, add_op->inputs[0]);
if (relu_input_op == nullptr || relu_input_op->type != OperatorType::kRelu ||
relu_input_op->inputs.size() != 1 ||
relu_input_op->fused_activation_function !=
FusedActivationFunctionType::kNone) {
return absl::OkStatus();
}
const auto* mul_op = GetOpWithOutput(*model, add_op->inputs[1]);
if (mul_op == nullptr || mul_op->type != OperatorType::kMul ||
mul_op->inputs.size() != 2 ||
mul_op->fused_activation_function != FusedActivationFunctionType::kNone) {
return absl::OkStatus();
}
const auto neg_alpha_tensor_name = mul_op->inputs[0];
const auto* relu_neg_input_op = GetOpWithOutput(*model, mul_op->inputs[1]);
if (relu_neg_input_op == nullptr ||
relu_neg_input_op->inputs.size() != 1) {
return absl::OkStatus();
}
const Operator* final_input_op;
if (relu_neg_input_op->type == OperatorType::kNeg &&
relu_neg_input_op->fused_activation_function ==
FusedActivationFunctionType::kRelu) {
// This detects a Neg op with fused Relu activation function.
final_input_op = relu_neg_input_op;
} else {
// This detects a Neg op followed by a separated Relu op.
const auto* neg_input_op =
GetOpWithOutput(*model, relu_neg_input_op->inputs[0]);
if (neg_input_op == nullptr || neg_input_op->inputs.size() != 1 ||
relu_neg_input_op->type != OperatorType::kRelu ||
relu_neg_input_op->fused_activation_function !=
FusedActivationFunctionType::kNone) {
return absl::OkStatus();
}
final_input_op = neg_input_op;
}
if (relu_input_op->inputs[0] != final_input_op->inputs[0]) {
return absl::OkStatus();
}
const auto input_tensor_name = relu_input_op->inputs[0];
const auto output_tensor_name = add_op->outputs[0];
// Construct a tensor for positive alpha (double negative).
const auto alpha_tensor_name =
AvailableArrayName(*model, neg_alpha_tensor_name + "_neg");
model->GetOrCreateArray(alpha_tensor_name);
auto* neg_neg_alpha_op = new NegOperator;
neg_neg_alpha_op->inputs = {neg_alpha_tensor_name};
neg_neg_alpha_op->outputs = {alpha_tensor_name};
model->operators.emplace(add_op_it, neg_neg_alpha_op);
auto* prelu_op = new PReluOperator;
prelu_op->inputs = {input_tensor_name, alpha_tensor_name};
prelu_op->outputs = {output_tensor_name};
model->operators.emplace(add_op_it, prelu_op);
AddMessageF("Creating %s replacing equivalent subgraph", LogName(*prelu_op));
DeleteArrayIfUnusedOutsideOfOp(neg_alpha_tensor_name, neg_neg_alpha_op,
model);
DeleteArrayIfUnusedOutsideOfOp(mul_op->inputs[1], mul_op, model);
DeleteOpAndArrays(model, add_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,85 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/identify_util.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
using util::GetSingleScalarInputIndexOfBinaryOp;
absl::Status IdentifyRelu1::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
// Follow sequences of min+max and max+min. First get the leading op.
const auto op_it = model->operators.begin() + op_index;
const auto* op_0 = op_it->get();
if (op_0->type != OperatorType::kMinimum &&
op_0->type != OperatorType::kMaximum) {
return absl::OkStatus();
}
// Get the paired op and ensure it's the counter to the first.
const auto* op_1 = GetOpWithInput(*model, op_0->outputs[0]);
if (!op_1 ||
(op_1->type != OperatorType::kMinimum &&
op_1->type != OperatorType::kMaximum) ||
op_0->type == op_1->type) {
return absl::OkStatus();
}
const auto* min_op = op_0->type == OperatorType::kMinimum ? op_0 : op_1;
const auto* max_op = op_0->type == OperatorType::kMaximum ? op_0 : op_1;
if (min_op->inputs.size() != 2 || max_op->inputs.size() != 2) {
return absl::OkStatus();
}
if (min_op->outputs.size() != 1 || max_op->outputs.size() != 1) {
return absl::OkStatus();
}
// Get the original input to the min+max pair.
int min_scalar_input_index =
GetSingleScalarInputIndexOfBinaryOp(model, min_op, 1.0f);
int max_scalar_input_index =
GetSingleScalarInputIndexOfBinaryOp(model, max_op, -1.0f);
if (min_scalar_input_index == -1 || max_scalar_input_index == -1) {
return absl::OkStatus();
}
int op_0_scalar_input_index =
op_0 == min_op ? min_scalar_input_index : max_scalar_input_index;
// Create and emplace Relu1 node.
auto* relu1_op = new Relu1Operator;
AddMessageF("Creating %s replacing equivalent subgraph", LogName(*relu1_op));
relu1_op->inputs = {op_0->inputs[!op_0_scalar_input_index]};
relu1_op->outputs = op_1->outputs;
model->operators.emplace(op_it, relu1_op);
DeleteOpAndArrays(model, op_0);
DeleteOpAndArrays(model, op_1);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,51 @@
/* Copyright 2019 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include "tensorflow/lite/toco/graph_transformations/identify_util.h"
#include <string>
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/runtime/types.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace util {
bool IsBinaryOp(const Operator* op, OperatorType optype,
FusedActivationFunctionType act) {
return op && op->type == optype && op->inputs.size() == 2 &&
op->fused_activation_function == act;
}
bool CheckArrayIsScalarFloat(Model* model, const std::string& name, float val) {
const auto& op_array = model->GetArray(name);
if (!op_array.buffer || op_array.buffer->type != ArrayDataType::kFloat ||
RequiredBufferSizeForShape(op_array.shape()) != 1) {
return false;
}
const auto& op_data = op_array.GetBuffer<ArrayDataType::kFloat>().data;
return op_data[0] == val;
}
int GetSingleScalarInputIndexOfBinaryOp(Model* model, const Operator* op,
float val) {
bool input0_is_scalar = CheckArrayIsScalarFloat(model, op->inputs[0], val);
bool input1_is_scalar = CheckArrayIsScalarFloat(model, op->inputs[1], val);
return input0_is_scalar == input1_is_scalar ? -1 : input0_is_scalar ? 0 : 1;
}
} // namespace util
} // namespace toco
@@ -0,0 +1,38 @@
/* Copyright 2019 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#ifndef TENSORFLOW_LITE_TOCO_GRAPH_TRANSFORMATIONS_IDENTIFY_UTIL_H_
#define TENSORFLOW_LITE_TOCO_GRAPH_TRANSFORMATIONS_IDENTIFY_UTIL_H_
#include <string>
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/runtime/types.h"
namespace toco {
namespace util {
bool IsBinaryOp(
const Operator* op, OperatorType optype,
FusedActivationFunctionType act = FusedActivationFunctionType::kNone);
// Returns true if given array is a scalar and is val.
bool CheckArrayIsScalarFloat(Model* model, const std::string& name, float val);
// Returns index of scalar input that is equal to val, returns -1 otherwise.
int GetSingleScalarInputIndexOfBinaryOp(Model* model, const Operator* op,
float val);
} // namespace util
} // namespace toco
#endif // TENSORFLOW_LITE_TOCO_GRAPH_TRANSFORMATIONS_IDENTIFY_UTIL_H_
@@ -0,0 +1,144 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include "tensorflow/lite/toco/graph_transformations/lstm_utils.h"
#include <string>
#include "absl/log/absl_check.h"
#include "absl/strings/string_view.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/model_flags.pb.h"
#include "tensorflow/lite/toco/toco_types.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
void CreateOptionalArray(Model* model, std::string* input_array_buffer,
absl::string_view array_name) {
ABSL_CHECK(model != nullptr);
ABSL_CHECK(input_array_buffer != nullptr);
*input_array_buffer = array_name;
model->CreateOptionalArray(*input_array_buffer);
}
void CopyArrayData(const Buffer<ArrayDataType::kFloat>& src_buffer,
int src_stride, int src_start_idx1, int src_start_idx2,
Buffer<ArrayDataType::kFloat>* dst_buffer, int dst_stride,
int dst_start_idx1, int dst_start_idx2, int dim1_copy_size,
int dim2_copy_size) {
ABSL_CHECK(dst_buffer != nullptr);
ABSL_CHECK_GE(src_stride, 0);
ABSL_CHECK_GE(dst_stride, 0);
ABSL_CHECK_GE(src_start_idx1, 0);
ABSL_CHECK_GE(src_start_idx2, 0);
ABSL_CHECK_GE(dst_start_idx1, 0);
ABSL_CHECK_GE(dst_start_idx2, 0);
ABSL_CHECK_GE(dim1_copy_size, 0);
ABSL_CHECK_GE(dim2_copy_size, 0);
if (dim1_copy_size == 0 || dim2_copy_size == 0) {
return;
}
ABSL_CHECK_LE(static_cast<int64_t>(src_start_idx2) + dim2_copy_size,
src_stride);
ABSL_CHECK_LE(static_cast<int64_t>(dst_start_idx2) + dim2_copy_size,
dst_stride);
int64_t src_offset =
static_cast<int64_t>(src_start_idx1) * src_stride + src_start_idx2;
int64_t dst_offset =
static_cast<int64_t>(dst_start_idx1) * dst_stride + dst_start_idx2;
ABSL_CHECK_LE(src_offset +
static_cast<int64_t>(dim1_copy_size - 1) * src_stride +
dim2_copy_size,
static_cast<int64_t>(src_buffer.data.size()));
ABSL_CHECK_LE(dst_offset +
static_cast<int64_t>(dim1_copy_size - 1) * dst_stride +
dim2_copy_size,
static_cast<int64_t>(dst_buffer->data.size()));
for (int64_t i = 0; i < dim1_copy_size; i++) {
for (int64_t j = 0; j < dim2_copy_size; j++) {
int64_t idx_src = src_offset + i * src_stride + j;
int64_t idx_dst = dst_offset + i * dst_stride + j;
dst_buffer->data[idx_dst] = src_buffer.data[idx_src];
}
}
}
Buffer<ArrayDataType::kFloat>* CreateFloatArrayBuffer(Model* model,
std::string* array_name,
const Shape& shape) {
ABSL_CHECK(model != nullptr);
ABSL_CHECK(array_name != nullptr);
*array_name = AvailableArrayName(*model, *array_name);
Array& array = model->GetOrCreateArray(*array_name);
array.data_type = ArrayDataType::kFloat;
array.copy_shape(shape);
Buffer<ArrayDataType::kFloat>* buffer =
&(array.GetMutableBuffer<ArrayDataType::kFloat>());
const int required_buffer_size = RequiredBufferSizeForShape(shape);
ABSL_CHECK_GE(required_buffer_size, 0);
buffer->data.resize(required_buffer_size);
return buffer;
}
void CopySubArrayToArray(Model* model, std::string* array_name,
absl::string_view tensor_name, int dim1_size,
int dim2_size, const Array& original_array,
int start_idx1, int start_idx2) {
ABSL_CHECK(model != nullptr);
ABSL_CHECK(array_name != nullptr);
*array_name = tensor_name;
bool is_bias = original_array.shape().dimensions_count() == 1;
Shape shape = is_bias ? Shape({dim1_size}) : Shape({dim1_size, dim2_size});
Buffer<ArrayDataType::kFloat>* buffer =
CreateFloatArrayBuffer(model, array_name, shape);
const Buffer<ArrayDataType::kFloat>& orig_buffer =
original_array.GetBuffer<ArrayDataType::kFloat>();
// Copy data from big tensor.
CopyArrayData(orig_buffer, is_bias ? 1 : original_array.shape().dims(1),
start_idx1, start_idx2, buffer, is_bias ? 1 : dim2_size, 0, 0,
dim1_size, is_bias ? 1 : dim2_size);
}
void CopyArrayToSubArray(Buffer<ArrayDataType::kFloat>& tensor_buffer,
int tensor_stride, const Array& sub_array,
int start_idx1, int start_idx2) {
// Get tensor data.
bool is_bias = sub_array.shape().dimensions_count() == 1;
int dim1_copy_size = sub_array.shape().dims(0);
int dim2_copy_size = is_bias ? 1 : sub_array.shape().dims(1);
const Buffer<ArrayDataType::kFloat>& sub_buffer =
sub_array.GetBuffer<ArrayDataType::kFloat>();
// Copy data from sub tensor.
CopyArrayData(sub_buffer, dim2_copy_size, 0, 0, &tensor_buffer,
is_bias ? 1 : tensor_stride, start_idx1, start_idx2,
dim1_copy_size, dim2_copy_size);
}
bool GetMatchingRnnArray(Model* model, absl::string_view back_edge_source_array,
std::string* rnn_array) {
ABSL_CHECK(model != nullptr);
ABSL_CHECK(rnn_array != nullptr);
for (const RnnState& rnn_state : model->flags.rnn_states()) {
if (rnn_state.back_edge_source_array() == back_edge_source_array) {
*rnn_array = rnn_state.state_array();
return true;
}
}
return false;
}
} // namespace toco
@@ -0,0 +1,103 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#ifndef TENSORFLOW_LITE_TOCO_GRAPH_TRANSFORMATIONS_LSTM_UTILS_H_
#define TENSORFLOW_LITE_TOCO_GRAPH_TRANSFORMATIONS_LSTM_UTILS_H_
#include <string>
#include "absl/strings/string_view.h"
#include "tensorflow/lite/toco/model.h"
namespace toco {
// For consistency with the parameters defined in extended LstmCell's kernel
// (tensorflow/lite/kernels/lstm.cc),
// use kCamelCase for these constants.
enum ExtendedLstmCellInputs {
kInputTensor = 0,
kInputToInputWeightsTensor = 1, // Optional
kInputToForgetWeightsTensor = 2,
kInputToCellWeightsTensor = 3,
kInputToOutputWeightsTensor = 4,
kRecurrentToInputWeightsTensor = 5, // Optional
kRecurrentToForgetWeightsTensor = 6,
kRecurrentToCellWeightsTensor = 7,
kRecurrentToOutputWeightsTensor = 8,
kCellToInputWeightsTensor = 9, // Optional
kCellToForgetWeightsTensor = 10, // Optional
kCellToOutputWeightsTensor = 11, // Optional
kInputGateBiasTensor = 12, // Optional
kForgetGateBiasTensor = 13,
kCellGateBiasTensor = 14,
kOutputGateBiasTensor = 15,
kProjectionWeightsTensor = 16, // Optional
kProjectionBiasTensor = 17, // Optional
kInputActivationStateTensor = 18,
// The op can handle 18 inputs or 20 inputs.
kInputCellStateTensor = 19,
kExtendedLstmInputCount = 20,
};
enum ExtendedLstmCellOutputs {
kOutputStateTensor = 0,
kCellStateTensor = 1,
kOutputTensor = 2,
kExtendedLstmOutputCount = 3
};
// Creates an optional array in the model and populates the input array buffer
// with its name.
void CreateOptionalArray(Model* model, std::string* input_array_buffer,
absl::string_view array_name);
// Creates a new float array with the specified shape in the model's array map
// and returns a non-owning pointer to its mutable buffer.
Buffer<ArrayDataType::kFloat>* CreateFloatArrayBuffer(Model* model,
std::string* array_name,
const Shape& shape);
// Copies a 2D submatrix (or 1D vector, where the second dimension size is 1)
// from a source buffer to a destination buffer. The source and destination
// strides specify the total width (second dimension size) of the respective
// buffers, and the start indices define the top-left offset of the copy region.
void CopyArrayData(const Buffer<ArrayDataType::kFloat>& src_buffer,
int src_stride, int src_start_idx1, int src_start_idx2,
Buffer<ArrayDataType::kFloat>* dst_buffer, int dst_stride,
int dst_start_idx1, int dst_start_idx2, int dim1_copy_size,
int dim2_copy_size);
// Creates a smaller array in the model and populates it with a submatrix
// region copied from the original array.
void CopySubArrayToArray(Model* model, std::string* array_name,
absl::string_view tensor_name, int dim1_size,
int dim2_size, const Array& original_array,
int start_idx1, int start_idx2);
// Copies data from a subarray into a submatrix region of a larger tensor
// buffer.
void CopyArrayToSubArray(Buffer<ArrayDataType::kFloat>& tensor_buffer,
int tensor_stride, const Array& sub_array,
int start_idx1, int start_idx2);
// Searches the model's rnn_states flags for an entry matching the back-edge
// source array. Returns true and populates rnn_array with the state array name
// if a match is found; otherwise returns false.
bool GetMatchingRnnArray(Model* model, absl::string_view back_edge_source_array,
std::string* rnn_array);
} // namespace toco
#endif // TENSORFLOW_LITE_TOCO_GRAPH_TRANSFORMATIONS_LSTM_UTILS_H_
@@ -0,0 +1,126 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/quantization_util.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/model_flags.pb.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
// This inserts an operator whose output is a float array (name:
// flags.input_array()). It has to wait for any existing operators that
// generate this output to be removed by graph transformations. Note that there
// may be more than one operator that takes the input_array as their input, and
// that some of these may be removed by graph transformations.
bool AddDequantizeOperatorToInput(const std::string& input_name,
const Operator* op,
GraphTransformation* transformation,
Model* model) {
// An operator with the required output may be a dequantize operator already
// created. Alternatively it may be an operator that needs to be removed
// because it is unused, in which case we wait for RemoveUnusedOp to do its
// work.
if (GetOpWithOutput(*model, input_name)) {
return false;
}
// We only apply for the first operator if there is more than one. This is
// not strictly necessary for ordering correctness, since we insert the
// dequant operator at the beginning of the op sequence, but it makes the
// insertion more predictable (eg forward vs backwards operator sweep).
if (CountOpsWithInput(*model, input_name) > 1) {
if (op != GetFirstOpWithInput(*model, input_name)) {
return false;
}
}
auto& input_array = model->GetArray(input_name);
if (input_array.data_type != ArrayDataType::kFloat) {
return false;
}
if (input_array.final_data_type == input_array.data_type ||
input_array.final_data_type == ArrayDataType::kNone) {
return false;
}
const auto& dequantized_input_name =
AvailableArrayName(*model, input_name + "_dequantized");
for (auto& other_op : model->operators) {
for (std::string& other_op_input : other_op->inputs) {
if (other_op_input == input_name) {
other_op_input = dequantized_input_name;
}
}
}
auto& dequantized_input_array =
model->GetOrCreateArray(dequantized_input_name);
auto* image_input_op = new DequantizeOperator;
image_input_op->inputs = {input_name};
image_input_op->outputs = {dequantized_input_name};
model->operators.emplace(model->operators.begin(), image_input_op);
dequantized_input_array.data_type = ArrayDataType::kFloat;
const auto& input_minmax = input_array.GetMinMax();
auto& dequantized_input_minmax = dequantized_input_array.GetOrCreateMinMax();
dequantized_input_minmax = input_minmax;
auto& input_qparams = input_array.GetOrCreateQuantizationParams();
input_array.data_type = input_array.final_data_type;
ChooseQuantizationParamsForArrayAndQuantizedDataType(
input_array, input_array.data_type, &input_qparams);
transformation->AddMessageF(
"Created %s"
" to handle quantized input image data, taking over existing"
" mean_value and std_value flags. Cleared those flags.",
LogName(*image_input_op));
return true;
}
absl::Status MakeInitialDequantizeOperator::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
// This is effectively a transformation applied to edges. We iterate over the
// specified node (op) and proceed for input edges.
const auto it = model->operators.begin() + op_index;
const auto* op = it->get();
bool change_made = false;
for (auto& input : op->inputs) {
for (auto& input_array : *model->flags.mutable_input_arrays()) {
if (input_array.name() == input) {
if (AddDequantizeOperatorToInput(input_array.name(), op, this, model)) {
change_made = true;
input_array.clear_mean_value();
input_array.clear_std_value();
}
}
}
}
*modified = change_made;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,195 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/remove_trivial_passthrough.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/toco_types.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
bool OperatorReady(const Model& model, const Operator* op) {
if (!model.HasArray(op->inputs[0]) || !model.HasArray(op->inputs[1]) ||
!model.HasArray(op->outputs[0])) {
// Arrays are missing.
return false;
}
if (!model.GetArray(op->inputs[0]).has_shape() ||
!model.GetArray(op->outputs[0]).has_shape()) {
// Input and output needs the shape.
return false;
}
if (!model.GetArray(op->inputs[1]).buffer) {
// Buffer needs to be a constant.
return false;
}
return true;
}
// Returns whether the reshape could be a transpose.
std::vector<int32> ReshapeToTranspose(const Model& model,
const TensorFlowReshapeOperator* op) {
CHECK(!op->shape.empty());
CHECK(model.HasArray(op->inputs[0]));
CHECK(model.HasArray(op->outputs[0]));
const auto& input_array = model.GetArray(op->inputs[0]);
const auto& output_array = model.GetArray(op->outputs[0]);
CHECK(input_array.has_shape());
CHECK(output_array.has_shape());
std::vector<int> in_shape = input_array.shape().dims();
std::vector<int> out_shape = output_array.shape().dims();
std::vector<int> one_indices;
std::vector<int> not_one_indices;
// Separate into one indices and not one indices.
for (size_t i = 0; i < in_shape.size(); i++) {
if (in_shape[i] == 1) {
one_indices.push_back(i);
} else {
not_one_indices.push_back(i);
}
}
// Reorder the vertices.
std::vector<int> perm;
perm.reserve(in_shape.size());
int one_index = 0;
int not_one_index = 0;
for (const auto val : out_shape) {
if (val == 1) {
perm.push_back(one_indices[one_index]);
one_index++;
} else {
perm.push_back(not_one_indices[not_one_index]);
not_one_index++;
}
}
return perm;
}
} // namespace
// When a transpose is fed into a reshape, it is possible for the two operators
// to be merged if the reshape does not affect memory ordering and does not
// affects the number of dimensions. This only occurs when only unary dimensions
// are shifting position.
absl::Status MergeReshapeIntoPrecedingTranspose::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
auto it = model->operators.begin() + op_index;
auto* reshape_op = ConvertOperator<TensorFlowReshapeOperator*>(
it->get(), OperatorType::kReshape);
if (reshape_op == nullptr) {
return absl::OkStatus();
}
if (!OperatorReady(*model, reshape_op) || reshape_op->shape.empty()) {
return absl::OkStatus();
}
const std::string intermediate_name = reshape_op->inputs[0];
const std::string output_name = reshape_op->outputs[0];
// Guarantee the input is only consume by the reshape.
if (CountOpsWithInput(*model, intermediate_name) != 1) {
return absl::OkStatus();
}
// Check for the parent operator.
const auto& transpose_it = FindOpWithOutput(*model, intermediate_name);
if (transpose_it == model->operators.end()) {
return absl::OkStatus();
}
// Find the parent operator and guarantee it is a transpose.
TransposeOperator* transpose_op = ConvertOperator<TransposeOperator*>(
transpose_it->get(), OperatorType::kTranspose);
if (transpose_op == nullptr) {
return absl::OkStatus();
}
if (!OperatorReady(*model, transpose_op) || transpose_op->perm.empty()) {
return absl::OkStatus();
}
if (!ReshapeIsEquivalentToTranspose(*model, reshape_op,
false /*allow_extra_unary_dimensions*/)) {
return absl::OkStatus();
}
// Check that the intermediate is not an output array.
if (!IsDiscardableArray(*model, intermediate_name)) {
AddMessageF(
"Cannot fuse %s and %s as it would invalidate the transpose "
"output array.",
LogName(*transpose_op), LogName(*reshape_op));
return absl::OkStatus();
}
AddMessageF("Merging operations %s and %s", LogName(*transpose_op),
LogName(*reshape_op));
// const auto& intermediate_array = model->GetArray(intermediate_name);
// const auto& output_array = model->GetArray(output_name);
auto merged_perm = ReshapeToTranspose(*model, reshape_op);
// Combine the permutations.
const auto& transpose_perm = transpose_op->perm;
for (size_t i = 0; i < merged_perm.size(); i++) {
merged_perm[i] = transpose_perm[merged_perm[i]];
}
// Remove the reshape as passthrough operation.
if (!RemoveTrivialPassthroughOp(this, model, op_index)) {
return absl::OkStatus();
}
// Update transpose_op's constant buffer to contain the new permutation.
model->GetArray(transpose_op->inputs[1])
.GetMutableBuffer<ArrayDataType::kInt32>()
.data = merged_perm;
transpose_op->perm = merged_perm;
// transpose_ops's shape will likely has changed.
model->GetArray(transpose_op->outputs[0]).clear_shape();
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,187 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <algorithm>
#include <cstddef>
#include <string>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
bool IsTailOfShape(const Shape& tail, const Shape& shape) {
// Return true if 'tail' dimensions are the same as the ending dimensions of
// 'shape'.
int shape_end = shape.dimensions_count() - 1;
int tail_end = tail.dimensions_count() - 1;
if (tail_end > shape_end) {
// tail cannot be longer than shape.
return false;
}
// Walk dimensions back to front and compare
for (int i = 0; i <= tail_end; i++) {
if (shape.dims(shape_end - i) != tail.dims(tail_end - i)) {
return false;
}
}
return true;
}
} // namespace
// If a binary operator is doing a broadcast operation from a constant array,
// and the constant array shape is the tail of both the other input shape, and a
// subsequent reshape op's output shape, we can swap their order. Since we
// prefer to have reshape ops after mathematic ops, this can allow for the
// collapsing of some reshapes. The WaveNet model in particular benefits from
// this transformation.
//
// Note we are testing for one particular case of a broader set of possible
// binary-reshape op transformations. This transformation could be generalized.
absl::Status MoveBinaryOperatorBeforeReshape::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
const auto binary_it = model->operators.begin() + op_index;
Operator* binary_op = binary_it->get();
if (binary_op->type != OperatorType::kAdd &&
binary_op->type != OperatorType::kMul &&
binary_op->type != OperatorType::kSub &&
binary_op->type != OperatorType::kDiv &&
binary_op->type != OperatorType::kFloorDiv &&
binary_op->type != OperatorType::kFloorMod &&
binary_op->type != OperatorType::kMinimum &&
binary_op->type != OperatorType::kMaximum &&
binary_op->type != OperatorType::kLess &&
binary_op->type != OperatorType::kLessEqual &&
binary_op->type != OperatorType::kGreater &&
binary_op->type != OperatorType::kGreaterEqual) {
return absl::OkStatus();
}
// BINARY OP INPUT CHECKS
CHECK_EQ(binary_op->inputs.size(), 2);
const bool input_is_const[2] = {
IsConstantParameterArray(*model, binary_op->inputs[0]),
IsConstantParameterArray(*model, binary_op->inputs[1]),
};
if (!input_is_const[0] && !input_is_const[1]) {
// To limit our scope, we require one constant input. Though there's no
// reason this transformation wouldn't work with all variable inputs.
return absl::OkStatus();
}
if (input_is_const[0] && input_is_const[1]) {
// Both inputs are constants. Leave this for constants propagation.
return absl::OkStatus();
}
const int constant_input_idx = input_is_const[0] ? 0 : 1;
const int variable_input_idx = input_is_const[0] ? 1 : 0;
CHECK(input_is_const[constant_input_idx]);
CHECK(!input_is_const[variable_input_idx]);
const auto& variable_input_array =
model->GetArray(binary_op->inputs[variable_input_idx]);
if (!variable_input_array.has_shape()) {
AddMessageF(
"Not moving %s because it's non-constant input shape is not resolved.",
LogName(*binary_op));
return absl::OkStatus();
}
if (!IsTailOfShape(
model->GetArray(binary_op->inputs[constant_input_idx]).shape(),
model->GetArray(binary_op->inputs[variable_input_idx]).shape())) {
// Constant array shape must be the latter part of the variable shape.
return absl::OkStatus();
}
// RESHAPE OP CHECKS
auto reshape_it =
FindOpWithOutput(*model, binary_op->inputs[variable_input_idx]);
if (reshape_it == model->operators.end()) {
AddMessageF("Not moving %s because it's variable input is not connected.",
LogName(*binary_op));
return absl::OkStatus();
}
Operator* reshape_op = reshape_it->get();
if (reshape_op->type != OperatorType::kReshape) {
AddMessageF("Not moving %s because the preceding %s is not a reshape op",
LogName(*binary_op), LogName(*reshape_op));
return absl::OkStatus();
}
const auto& reshape_input_array = model->GetArray(reshape_op->inputs[0]);
if (!reshape_input_array.has_shape()) {
AddMessageF(
"Not moving %s because it's non-constant input shape is not resolved "
"yet",
LogName(*binary_op));
return absl::OkStatus();
}
if (!IsTailOfShape(
model->GetArray(binary_op->inputs[constant_input_idx]).shape(),
model->GetArray(reshape_op->outputs[0]).shape())) {
// Constant array shape must be the latter part of the binary op output
// shape.
return absl::OkStatus();
}
// EXTRA CHECKS ON CONNECTING ARRAY
for (const std::string& output_array : model->flags.output_arrays()) {
if (binary_op->inputs[variable_input_idx] == output_array) {
AddMessageF(
"Not moving %s because the output of reshape op %s is an output op.",
LogName(*binary_op), LogName(*reshape_op));
return absl::OkStatus();
}
}
int count_ops_consuming_output =
CountOpsWithInput(*model, binary_op->inputs[variable_input_idx]);
DCHECK_GE(count_ops_consuming_output, 1);
if (count_ops_consuming_output > 1) {
AddMessageF(
"Not moving %s because the output of reshape op %s is consumed by "
"another op",
LogName(*binary_op), LogName(*reshape_op));
return absl::OkStatus();
}
// SWAP ORDER OF BINARY AND RESHAPE OPS
AddMessageF("Moving op %s before reshape op %s", LogName(*binary_op),
LogName(*reshape_op));
// Swap op input and outputs
std::iter_swap(reshape_op->inputs.begin(),
binary_op->inputs.begin() + variable_input_idx);
std::iter_swap(reshape_op->outputs.begin(), binary_op->outputs.begin());
// Swap operator ordering
std::iter_swap(binary_it, reshape_it);
// Clear binary output shape so it will be re-propagated
model->GetArray(binary_op->outputs[0]).clear_shape();
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,127 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/remove_trivial_passthrough.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status PropagateActivationFunctionIntoConstants::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
const auto ac_it = model->operators.begin() + op_index;
const auto* ac_op = ac_it->get();
if (ac_op->type != OperatorType::kRelu6 &&
ac_op->type != OperatorType::kRelu1 &&
ac_op->type != OperatorType::kRelu) {
return absl::OkStatus();
}
// Find the op producing the array passed to this activation function.
auto* src_op = GetOpWithOutput(*model, ac_op->inputs[0]);
if (!src_op) {
return absl::OkStatus();
}
// Ensure the src_op is not used without the activation function applied.
if (CountTrueOutputs(*model, *src_op) > 1) {
AddMessageF(
"Not propagating activation function %s into %s because it has more "
"than one consumed output",
LogName(*ac_op), LogName(*src_op));
}
// Filter to the list of supported ops.
std::string src_op_input;
switch (src_op->type) {
case OperatorType::kGather:
src_op_input = src_op->inputs[0];
break;
default:
return absl::OkStatus();
}
CHECK_EQ(src_op->outputs[0], ac_op->inputs[0]);
// Ensure the input is constant as otherwise this needs to happen at runtime.
// If we bail here, it's still possible that FuseActivationFunctions will fuse
// the activation if it's supported by the op.
if (!IsConstantParameterArray(*model, src_op_input)) {
AddMessageF(
"Not propagating activation function %s into %s:%s because it is not "
"constant",
LogName(*ac_op), LogName(*src_op), src_op_input);
return absl::OkStatus();
}
// Get the array we'll be working with and ensure it's a compatible type.
auto& const_array = model->GetArray(src_op_input);
if (const_array.data_type != ArrayDataType::kFloat) {
AddMessageF(
"Not propagating activation function %s into %s:%s because it is "
"non-float data",
LogName(*ac_op), LogName(*src_op), src_op_input);
return absl::OkStatus();
}
auto& const_array_data =
const_array.GetMutableBuffer<ArrayDataType::kFloat>().data;
// Perform the activation function directly into the constant data array.
for (size_t i = 0; i < const_array_data.size(); ++i) {
const float value = const_array_data[i];
float new_value = value;
switch (ac_op->type) {
case OperatorType::kRelu: {
static constexpr float kLower = 0;
new_value = value < kLower ? kLower : value;
break;
}
case OperatorType::kRelu1: {
static constexpr float kUpper = 1;
static constexpr float kLower = -1;
new_value = value > kUpper ? kUpper : value < kLower ? kLower : value;
break;
}
case OperatorType::kRelu6: {
static constexpr float kUpper = 6;
static constexpr float kLower = 0;
new_value = value > kUpper ? kUpper : value < kLower ? kLower : value;
break;
}
default:
LOG(FATAL) << "Unsupported activation function " << LogName(*ac_op);
return absl::OkStatus();
}
const_array_data[i] = new_value;
}
AddMessageF("Propagated activation function %s into %s:%s", LogName(*ac_op),
LogName(*src_op), src_op_input);
*modified = RemoveTrivialPassthroughOp(this, model, op_index);
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,330 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
namespace toco {
namespace {
void SetDataTypeForAllOutputs(Model* model, Operator* op,
ArrayDataType data_type) {
for (const auto& output : op->outputs) {
model->GetArray(output).data_type = data_type;
}
}
} // namespace
absl::Status PropagateArrayDataTypes::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto it = model->operators.begin() + op_index;
auto* op = it->get();
// If the data type of some input is unknown, we need to yield.
for (const auto& input : op->inputs) {
if (!model->IsOptionalArray(input) &&
model->GetArray(input).data_type == ArrayDataType::kNone) {
return absl::OkStatus();
}
}
// Record data types of output before processing, so we can see at the
// end if we changed anything, and return the correct boolean value.
std::unordered_map<std::string, ArrayDataType> old_output_data_types;
for (const auto& output : op->outputs) {
old_output_data_types[output] = model->GetArray(output).data_type;
}
// Do the actual output data types propagation.
switch (op->type) {
case OperatorType::kDequantize:
// These operators unconditionally produce float outputs
SetDataTypeForAllOutputs(model, op, ArrayDataType::kFloat);
break;
case OperatorType::kLess:
case OperatorType::kLessEqual:
case OperatorType::kGreater:
case OperatorType::kGreaterEqual:
case OperatorType::kEqual:
case OperatorType::kNotEqual:
case OperatorType::kAny:
case OperatorType::kLogicalAnd:
case OperatorType::kLogicalNot:
case OperatorType::kLogicalOr:
// These operators unconditionally produce bool outputs
SetDataTypeForAllOutputs(model, op, ArrayDataType::kBool);
break;
case OperatorType::kRank:
// These operators only produce int32 outputs.
SetDataTypeForAllOutputs(model, op, ArrayDataType::kInt32);
break;
case OperatorType::kShape: {
// Shape op could produce int32 or int64 result. Set the output type
// based on the `output_data_type` field.
auto* shape_op = static_cast<TensorFlowShapeOperator*>(op);
SetDataTypeForAllOutputs(model, op, shape_op->output_data_type);
break;
}
case OperatorType::kSplit:
case OperatorType::kConcat:
case OperatorType::kFill: {
// These operators produce an output with the same type as their 2nd input
CHECK_GE(op->inputs.size(), 2);
const ArrayDataType data_type = model->GetArray(op->inputs[1]).data_type;
SetDataTypeForAllOutputs(model, op, data_type);
break;
}
case OperatorType::kSplitV: {
// These operators produce output with the same type as its 1st input
CHECK_GE(op->inputs.size(), 3);
const ArrayDataType data_type = model->GetArray(op->inputs[0]).data_type;
SetDataTypeForAllOutputs(model, op, data_type);
break;
}
case OperatorType::kTransposeConv: {
// These operators produce an output with the same type as their 3rd input
CHECK_GE(op->inputs.size(), 3);
const ArrayDataType data_type = model->GetArray(op->inputs[2]).data_type;
SetDataTypeForAllOutputs(model, op, data_type);
break;
}
case OperatorType::kCast: {
// Data type of the Cast op is specified.
CHECK_EQ(op->outputs.size(), 1);
auto* cast_op = static_cast<CastOperator*>(op);
model->GetArray(op->outputs[0]).data_type = cast_op->dst_data_type;
break;
}
case OperatorType::kArgMax: {
// Data type of the ArgMax op is specified.
CHECK_EQ(op->outputs.size(), 1);
auto* argmax_op = static_cast<ArgMaxOperator*>(op);
model->GetArray(op->outputs[0]).data_type = argmax_op->output_data_type;
break;
}
case OperatorType::kArgMin: {
// Data type of the ArgMin op is specified.
CHECK_EQ(op->outputs.size(), 1);
auto* argmin_op = static_cast<ArgMinOperator*>(op);
model->GetArray(op->outputs[0]).data_type = argmin_op->output_data_type;
break;
}
case OperatorType::kRange: {
auto* range_op = static_cast<RangeOperator*>(op);
// Output type of the Range op can be set via an attribute
ArrayDataType data_type;
if (range_op->dtype != ArrayDataType::kNone) {
// Use the type if specified
data_type = range_op->dtype;
} else {
// Otherwise use the first input
CHECK_GE(op->inputs.size(), 1);
data_type = model->GetArray(op->inputs[0]).data_type;
}
CHECK_EQ(op->outputs.size(), 1);
SetDataTypeForAllOutputs(model, op, data_type);
break;
}
case OperatorType::kRandomUniform: {
auto* rand_op = static_cast<RandomUniformOperator*>(op);
// The output type of RandomUniform is specified with an attribute
if (rand_op->dtype == ArrayDataType::kNone) {
return absl::OkStatus();
}
CHECK_EQ(op->outputs.size(), 1);
SetDataTypeForAllOutputs(model, op, rand_op->dtype);
break;
}
case OperatorType::kTopK_V2: {
// topk(values: T, k: int32) -> values: T, indices: int32
CHECK_EQ(op->inputs.size(), 2);
CHECK_EQ(op->outputs.size(), 2);
CHECK(model->GetArray(op->inputs[1]).data_type == ArrayDataType::kInt32);
model->GetArray(op->outputs[0]).data_type =
model->GetArray(op->inputs[0]).data_type;
model->GetArray(op->outputs[1]).data_type = ArrayDataType ::kInt32;
break;
}
case OperatorType::kUnsupported: {
auto* unsupported_op = static_cast<TensorFlowUnsupportedOperator*>(op);
// Some output tensors from the op could be eliminated by optimization.
// This can make unsupported_op->output_data_types have more elements than
// op->outputs.
if (unsupported_op->output_data_types.size() < op->outputs.size()) {
return absl::OkStatus();
}
for (size_t i = 0; i < op->outputs.size(); ++i) {
const std::string& output = op->outputs[i];
const ArrayDataType data_type = unsupported_op->output_data_types[i];
model->GetArray(output).data_type = data_type;
}
break;
}
case OperatorType::kExpandDims: {
// Yield on ExpandDim until it is converted to Reshape
return absl::OkStatus();
}
case OperatorType::kSelect: {
// Select produces outputs with the same type as their 2nd input
CHECK_EQ(op->inputs.size(), 3);
const ArrayDataType data_type_x =
model->GetArray(op->inputs[1]).data_type;
const ArrayDataType data_type_y =
model->GetArray(op->inputs[2]).data_type;
CHECK(data_type_x == data_type_y);
SetDataTypeForAllOutputs(model, op, data_type_x);
break;
}
case OperatorType::kSparseToDense: {
// Select produces outputs with the same type as their 3rd input
CHECK_EQ(op->inputs.size(), 4);
const ArrayDataType data_type = model->GetArray(op->inputs[2]).data_type;
const ArrayDataType data_type_default =
model->GetArray(op->inputs[3]).data_type;
CHECK(data_type == data_type_default);
SetDataTypeForAllOutputs(model, op, data_type);
break;
}
case OperatorType::kPow: {
CHECK_EQ(op->inputs.size(), 2);
CHECK(model->GetArray(op->inputs[0]).data_type ==
model->GetArray(op->inputs[1]).data_type);
const ArrayDataType data_type = model->GetArray(op->inputs[0]).data_type;
SetDataTypeForAllOutputs(model, op, data_type);
break;
}
case OperatorType::kPack: {
const ArrayDataType data_type = model->GetArray(op->inputs[0]).data_type;
for (const auto& input : op->inputs) {
CHECK(data_type == model->GetArray(input).data_type);
}
SetDataTypeForAllOutputs(model, op, data_type);
break;
}
case OperatorType::kOneHot: {
CHECK_EQ(op->inputs.size(), 4);
CHECK_EQ(op->outputs.size(), 1);
const ArrayDataType on_value_type =
model->GetArray(op->inputs[OneHotOperator::ON_VALUE_INPUT]).data_type;
const ArrayDataType off_value_type =
model->GetArray(op->inputs[OneHotOperator::OFF_VALUE_INPUT])
.data_type;
CHECK(on_value_type == off_value_type);
model->GetArray(op->outputs[0]).data_type = on_value_type;
break;
}
case OperatorType::kCTCBeamSearchDecoder: {
CHECK_EQ(op->inputs.size(), 2);
// All outputs (sparse tensors) are int32s (although tf uses int64s)
// except the last one (log probabilities) is float.
const int output_size = op->outputs.size();
for (int i = 0; i < output_size - 1; ++i) {
model->GetArray(op->outputs[i]).data_type = ArrayDataType::kInt32;
}
model->GetArray(op->outputs[output_size - 1]).data_type =
ArrayDataType::kFloat;
break;
}
case OperatorType::kUnpack: {
CHECK_EQ(op->inputs.size(), 1);
const int output_size = op->outputs.size();
for (int i = 0; i < output_size; ++i) {
model->GetArray(op->outputs[i]).data_type =
model->GetArray(op->inputs[0]).data_type;
}
break;
}
case OperatorType::kUnidirectionalSequenceLstm: {
const ArrayDataType data_type = model->GetArray(op->inputs[0]).data_type;
if (data_type != ArrayDataType::kFloat) return absl::OkStatus();
SetDataTypeForAllOutputs(model, op, data_type);
break;
}
case OperatorType::kUnidirectionalSequenceRnn: {
const ArrayDataType data_type = model->GetArray(op->inputs[0]).data_type;
if (data_type != ArrayDataType::kFloat) return absl::OkStatus();
SetDataTypeForAllOutputs(model, op, data_type);
break;
}
case OperatorType::kUnique: {
CHECK_EQ(op->outputs.size(), 2);
const UniqueOperator* unique_op = static_cast<UniqueOperator*>(op);
const ArrayDataType data_type = model->GetArray(op->inputs[0]).data_type;
model->GetArray(op->outputs[0]).data_type = data_type;
model->GetArray(op->outputs[1]).data_type = unique_op->idx_out_type;
break;
}
case OperatorType::kBidirectionalSequenceLstm: {
const ArrayDataType data_type = model->GetArray(op->inputs[0]).data_type;
if (data_type != ArrayDataType::kFloat) return absl::OkStatus();
SetDataTypeForAllOutputs(model, op, data_type);
break;
}
case OperatorType::kBidirectionalSequenceRnn: {
const ArrayDataType data_type = model->GetArray(op->inputs[0]).data_type;
if (data_type != ArrayDataType::kFloat) return absl::OkStatus();
SetDataTypeForAllOutputs(model, op, data_type);
break;
}
case OperatorType::kLstmCell: {
// It's tricky to propagate data types through a LstmCell, as that has
// multiple inputs and outputs, and there are quantized cases with
// mixed (8bit vs 16bit) cases. Fortunately, that should never be needed,
// as the data formats, such as TFLITE, that have LstmCell nodes, also
// have data type fields for all their arrays.
break;
}
case OperatorType::kMatrixDiag: {
CHECK_EQ(op->inputs.size(), 1);
CHECK_EQ(op->outputs.size(), 1);
const ArrayDataType data_type = model->GetArray(op->inputs[0]).data_type;
SetDataTypeForAllOutputs(model, op, data_type);
break;
}
case OperatorType::kMatrixSetDiag: {
CHECK_EQ(op->inputs.size(), 2);
CHECK_EQ(op->outputs.size(), 1);
const ArrayDataType data_type = model->GetArray(op->inputs[0]).data_type;
SetDataTypeForAllOutputs(model, op, data_type);
break;
}
default: {
// These operators produce outputs with the same type as their 1st input
CHECK_GT(op->inputs.size(), 0);
const ArrayDataType data_type = model->GetArray(op->inputs[0]).data_type;
SetDataTypeForAllOutputs(model, op, data_type);
break;
}
}
// Return true if any output data type changed, false if none changed.
for (const auto& output : op->outputs) {
if (old_output_data_types[output] != model->GetArray(output).data_type) {
*modified = true;
return absl::OkStatus();
}
}
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,102 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/quantization_util.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
bool SupportsMinMax(const Array& array) {
return array.data_type == ArrayDataType::kFloat;
}
} // namespace
// Propagates default min/max values to any operator input/output array that
// is missing them.
//
// When provided a set of min/max values for uint8 arrays this will rescale
// the values for other data types as required and preserving the floating point
// range within the new type.
absl::Status PropagateDefaultMinMax::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto it = model->operators.begin() + op_index;
const auto* op = it->get();
bool did_change = false;
for (const auto& input : op->inputs) {
auto& input_array = model->GetArray(input);
if (!input_array.minmax && !input_array.buffer &&
SupportsMinMax(input_array)) {
did_change |= SetArrayMinMax(input, &input_array);
}
}
for (const auto& output : op->outputs) {
auto& output_array = model->GetArray(output);
if (!output_array.minmax && !output_array.buffer &&
SupportsMinMax(output_array)) {
did_change |= SetArrayMinMax(output, &output_array);
}
}
*modified = did_change;
return absl::OkStatus();
}
// Sets the min/max on the given array, adjusting the reference_minmax for the
// final data type of the array if it is already specified.
bool PropagateDefaultMinMax::SetArrayMinMax(const std::string& array_name,
Array* array) {
CHECK(!array->minmax);
ArrayDataType quantized_data_type =
GetQuantizedDataType(*array, ArrayDataType::kUint8);
for (const auto& type_range : type_ranges_) {
if (type_range.first == quantized_data_type) {
array->GetOrCreateMinMax() = type_range.second;
break;
}
}
if (!array->minmax) {
AddMessageF(
"No defaults specified for quantized data type %s of array %s, "
"skipping",
ArrayDataTypeName(quantized_data_type), array_name);
return false;
}
AddMessageF("Adding default minmax %g,%g to array %s when quantized as %s",
array->GetMinMax().min, array->GetMinMax().max, array_name,
ArrayDataTypeName(quantized_data_type));
return true;
}
} // namespace toco
@@ -0,0 +1,322 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/quantization_util.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
bool ChangeArrayDataType(GraphTransformation* transformation, Array* array,
ArrayDataType new_data_type,
const MinMax* new_minmax) {
// Ensure the array ends up in the new type (if it hasn't yet been quantized).
bool data_type_changed = array->final_data_type != new_data_type;
array->final_data_type = new_data_type;
if (array->minmax && array->quantization_params && data_type_changed) {
// The array is already quantized and has min/max info.
// As we are changing the data type we need to fix up the existing min/max
// to the new data type range.
double old_quantized_min, old_quantized_max;
CHECK(GetQuantizedDataTypeNumericalRange(
array->data_type, &old_quantized_min, &old_quantized_max))
<< "Existing data type is not quantized: "
<< ArrayDataTypeName(array->data_type);
double new_quantized_min, new_quantized_max;
CHECK(GetQuantizedDataTypeNumericalRange(new_data_type, &new_quantized_min,
&new_quantized_max))
<< "New data type is not quantized: "
<< ArrayDataTypeName(new_data_type);
// Compute new minmax values.
double min = (old_quantized_min - array->quantization_params->zero_point) *
array->quantization_params->scale;
double max =
(old_quantized_max + 1 - array->quantization_params->zero_point) *
array->quantization_params->scale;
max = max - 1.0 / (new_quantized_max + 1);
auto& array_minmax = array->GetOrCreateMinMax();
transformation->AddMessageF(
"Rescaling min/max from %g,%g (%s) to %g,%g (%s)", array_minmax.min,
array_minmax.max, ArrayDataTypeName(array->data_type), min, max,
ArrayDataTypeName(new_data_type));
array_minmax.min = min;
array_minmax.max = max;
ChooseQuantizationParamsForArrayAndQuantizedDataType(
*array, new_data_type, array->quantization_params.get());
// Directly change the type as the array was already quantized.
array->data_type = new_data_type;
return true;
}
// Array has not yet been quantized so we can just set the final data type
// and assign the new min/max value (if provided).
if (!array->quantization_params && !array->minmax && new_minmax) {
transformation->AddMessageF("Forcing new minmax to %g,%g (%s)",
new_minmax->min, new_minmax->max,
ArrayDataTypeName(new_data_type));
auto& array_minmax = array->GetOrCreateMinMax();
array_minmax.min = new_minmax->min;
array_minmax.max = new_minmax->max;
return true;
}
return data_type_changed;
}
// Returns true if the op blocks our backward recursive data type propagation.
bool DoesOpBlockBackwardPropagation(const Operator& op) {
switch (op.type) {
case OperatorType::kConcatenation:
case OperatorType::kConcat:
case OperatorType::kConcatV2:
// Concat shouldn't block propagation, but we do expect that all inputs
// have the same range.
return false;
case OperatorType::kDequantize:
// Dequantize ops are inserted between the value we care about and the
// FakeQuant so make sure we move across them.
case OperatorType::kGather:
// Gathers need their parameters changed to the appropriate data type.
case OperatorType::kReshape:
case OperatorType::kTranspose:
case OperatorType::kSelect:
case OperatorType::kTile:
// Reshapes and transposes don't change values.
case OperatorType::kRelu:
case OperatorType::kRelu1:
case OperatorType::kRelu6:
// Relus only clamp the output. If min/max of parent is unknown, just
// prop the range backward. This only happens for cases where activations
// are not fused to avoid a default being set on the RELU input and
// propagating forward to the RELU output.
return false;
default:
return true;
}
}
// Returns true if the input of an op blocks our backward recursive data type
// propagation.
bool DoesOpInputBlockBackwardPropagation(const Operator& op, int input_index) {
switch (op.type) {
case OperatorType::kSelect:
return input_index == 0;
case OperatorType::kGather:
// Ignore gather indices.
return input_index != 0;
break;
case OperatorType::kReshape:
case OperatorType::kTranspose:
// Ignore reshape/transpose shapes/dimensions.
return input_index != 0;
case OperatorType::kTile:
// Ignore tile multiples.
return input_index != 0;
default:
return false;
}
}
// Propagates the data type up into the input arrays if they are model inputs
// that may need their type changed. May act recursively if the inputs are
// produced by ops that we can move over (such as Dequantize).
bool RecursivelyBackwardPropagateDataType(GraphTransformation* transformation,
Model* model, Operator* op,
ArrayDataType new_data_type,
const MinMax& new_minmax) {
bool did_change = false;
for (size_t input_index = 0; input_index < op->inputs.size(); ++input_index) {
const auto& input = op->inputs[input_index];
auto& input_array = model->GetArray(input);
// Prevent moving into constant param args that we don't want to modify.
if (DoesOpInputBlockBackwardPropagation(*op, input_index)) {
continue;
}
bool array_did_change = ChangeArrayDataType(transformation, &input_array,
new_data_type, &new_minmax);
if (array_did_change) {
transformation->AddMessageF(
"Adjusting input final data type of array %s from %s to %s", input,
ArrayDataTypeName(input_array.final_data_type),
ArrayDataTypeName(new_data_type));
}
did_change |= array_did_change;
// Walk up into all ops producing the inputs to this op.
for (auto& producing_op : model->operators) {
if (!DoesOpBlockBackwardPropagation(*producing_op)) {
for (const auto& output : producing_op->outputs) {
if (input == output) {
did_change |= RecursivelyBackwardPropagateDataType(
transformation, model, producing_op.get(), new_data_type,
new_minmax);
}
}
}
}
}
return did_change;
}
// Returns true if the op blocks our forward recursive data type propagation.
bool DoesOpBlockForwardPropagation(const Operator& op) {
switch (op.type) {
case OperatorType::kFakeQuant:
// Always stop at another FakeQuant, as it will likely have different
// parameters.
return true;
default:
return false;
}
}
// Recurses down the graph setting the data type of all arrays until an operator
// that blocks propagation (like another FakeQuant) or a final_data_type is
// already specified.
bool RecursivelyForwardPropagateDataType(GraphTransformation* transformation,
Model* model, Operator* op,
ArrayDataType new_data_type) {
bool did_change = false;
for (const auto& output : op->outputs) {
auto& output_array = model->GetArray(output);
if (output_array.final_data_type == new_data_type) {
// Final data type is already - skip.
continue;
}
if (output_array.final_data_type == ArrayDataType::kNone ||
output_array.final_data_type != new_data_type) {
transformation->AddMessageF(
"Adjusting output final data type of array %s from %s to %s", output,
ArrayDataTypeName(output_array.final_data_type),
ArrayDataTypeName(new_data_type));
did_change |= ChangeArrayDataType(transformation, &output_array,
new_data_type, nullptr);
// Walk down into all ops consuming the output of this op.
for (auto& consuming_op : model->operators) {
if (!DoesOpBlockForwardPropagation(*consuming_op)) {
for (const auto& input : consuming_op->inputs) {
if (input == output) {
did_change |= RecursivelyForwardPropagateDataType(
transformation, model, consuming_op.get(), new_data_type);
}
}
}
}
}
}
return did_change;
}
} // namespace
// Propagates the num_bits on a FakeQuant operator into the final data types
// of inputs and outputs. For example, if FakeQuant.num_bits==16 then we know
// the output must be int16 and assume all inputs up until the preceding op are
// also 16.
//
// This can be thought of as a bidirectional flood-fill of the num_bits implied
// final_data_type that terminates at other FakeQuant ops (and a few others as
// determined by DoesOpBlockBackwardPropagation/DoesOpBlockForwardPropagation).
// Once all FakeQuant ops have been visited the arrays should all have
// appropriate final_data_types if the source graph was annotated with the
// proper FakeQuant ops.
//
// Annotating a graph requires following a few hard rules:
// - every input MUST have a FakeQuant immediately following it
// - every output MUST have a FakeQuant immediately preceding it
// - important arithmetic ops (such as FullyConnected) SHOULD have a FakeQuant
// immediately following it
// - all trained weights (RHS of FullyConnected ops, params on Gather ops, etc)
// MUST have FakeQuants between them and the consuming op
// Additional FakeQuants may be used if desired, especially in areas that may
// suffer from large precision changes - such as between a Softmax and a
// FullyConnected. Only by validating accuracy differences between float
// inference with the FakeQuant ops simulating quantization and the actually
// quantized graph can you be sure the appropriate FakeQuant ops are present.
//
// You can tell if you're missing some FakeQuants by looking for warnings from
// quantize.cc about minmax ranges being determined by the contents of constant
// arrays. This will almost never produce functional models during inference.
//
// As this op may change the data types and ranges of input and output arrays
// downstream tools must also be sure to parse the output model flags to get the
// post-Transform values that may have changed due to this transformation.
//
// This isn't a GraphTransformation in the traditional respect as it affects ops
// outside of the one under transformation. This is primarily so that we can
// utilize the graph traversal and repeated pass system underlying the
// transformation system to exhaustively find all FakeQuant ops. It also gets us
// nice logging and integration with the graphviz video dumping mode.
// In general you should not copy this style of transformation and stick to
// local-only changes as seen in the other transformations.
absl::Status PropagateFakeQuantNumBits::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto it = model->operators.begin() + op_index;
auto* op = it->get();
if (op->type != OperatorType::kFakeQuant) {
return absl::OkStatus();
}
auto* fakequant_op = static_cast<FakeQuantOperator*>(op);
ArrayDataType quantized_data_type = ArrayDataType::kNone;
if (!InferQuantizedDataTypeFromFakeQuant(*fakequant_op,
&quantized_data_type)) {
AddMessageF("FakeQuant op %s num_bits=%d is out of range, ignoring",
LogName(*op), fakequant_op->num_bits);
return absl::OkStatus();
}
const auto& final_minmax = *fakequant_op->minmax;
AddMessageF(
"Beginning propagation of fake quant %s num_bits=%d min=%g max=%g to %s",
LogName(*op), fakequant_op->num_bits, final_minmax.min, final_minmax.max,
ArrayDataTypeName(quantized_data_type));
bool did_change = false;
// Propagate the FakeQuant information backward up the graph.
// This will possibly adjust input arrays or constant types (like Gather).
did_change |= RecursivelyBackwardPropagateDataType(
this, model, op, quantized_data_type, final_minmax);
// Propagate the FakeQuant information forward down the graph.
// This will possibly adjust output arrays.
did_change |=
RecursivelyForwardPropagateDataType(this, model, op, quantized_data_type);
*modified = did_change;
return absl::OkStatus();
}
} // namespace toco
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,284 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include "tensorflow/lite/toco/graph_transformations/quantization_util.h"
#include <cmath>
#include <cstddef>
#include <limits>
#include <memory>
#include <string>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
bool InferQuantizedDataTypeFromFakeQuant(
const FakeQuantOperator& op, ArrayDataType* out_quantized_data_type) {
if (op.num_bits <= 8) {
*out_quantized_data_type = ArrayDataType::kUint8;
return true;
} else if (op.num_bits <= 16) {
*out_quantized_data_type = ArrayDataType::kInt16;
return true;
} else {
*out_quantized_data_type = ArrayDataType::kNone;
return false;
}
}
bool GetQuantizedDataTypeNumericalRange(ArrayDataType data_type,
double* out_min_value,
double* out_max_value) {
switch (data_type) {
case ArrayDataType::kUint8:
*out_min_value = 0;
*out_max_value = 255;
return true;
case ArrayDataType::kInt16:
*out_min_value = -32768;
*out_max_value = 32767;
return true;
default:
return false;
}
}
ArrayDataType GetQuantizedDataType(const Array& array,
ArrayDataType default_type) {
switch (array.final_data_type) {
case ArrayDataType::kInt8:
case ArrayDataType::kUint8:
case ArrayDataType::kInt16:
case ArrayDataType::kUint16:
case ArrayDataType::kInt32:
case ArrayDataType::kUint32:
case ArrayDataType::kInt64:
case ArrayDataType::kUint64:
return array.final_data_type;
case ArrayDataType::kFloat:
case ArrayDataType::kNone:
return default_type;
default:
LOG(FATAL) << "Unhandled final quantization type "
<< static_cast<int>(array.final_data_type);
}
}
template <ArrayDataType A>
void ChooseQuantizationParamsForArrayAndQuantizedDataType(
const Array& array, QuantizationParams* quantization_params) {
*quantization_params = ::tflite::ChooseQuantizationParams<DataType<A>>(
array.minmax->min, array.minmax->max, array.narrow_range);
}
void ChooseQuantizationParamsForArrayAndQuantizedDataType(
const Array& array, ArrayDataType quantized_data_type,
QuantizationParams* quantization_params) {
switch (quantized_data_type) {
case ArrayDataType::kInt8:
ChooseQuantizationParamsForArrayAndQuantizedDataType<
ArrayDataType::kInt8>(array, quantization_params);
break;
case ArrayDataType::kUint8:
ChooseQuantizationParamsForArrayAndQuantizedDataType<
ArrayDataType::kUint8>(array, quantization_params);
break;
case ArrayDataType::kInt16:
ChooseQuantizationParamsForArrayAndQuantizedDataType<
ArrayDataType::kInt16>(array, quantization_params);
break;
case ArrayDataType::kUint16:
ChooseQuantizationParamsForArrayAndQuantizedDataType<
ArrayDataType::kUint16>(array, quantization_params);
break;
case ArrayDataType::kInt32:
ChooseQuantizationParamsForArrayAndQuantizedDataType<
ArrayDataType::kInt32>(array, quantization_params);
break;
case ArrayDataType::kUint32:
ChooseQuantizationParamsForArrayAndQuantizedDataType<
ArrayDataType::kUint32>(array, quantization_params);
break;
case ArrayDataType::kInt64:
ChooseQuantizationParamsForArrayAndQuantizedDataType<
ArrayDataType::kInt64>(array, quantization_params);
break;
case ArrayDataType::kUint64:
ChooseQuantizationParamsForArrayAndQuantizedDataType<
ArrayDataType::kUint64>(array, quantization_params);
break;
case ArrayDataType::kFloat:
case ArrayDataType::kComplex64:
case ArrayDataType::kNone:
default:
LOG(FATAL) << "Unhandled final quantization type "
<< static_cast<int>(quantized_data_type);
}
}
namespace {
template <ArrayDataType A>
std::unique_ptr<GenericBuffer> QuantizeBuffer(
const Array& array, const QuantizationParams& quantization_params) {
const GenericBuffer& buffer = *array.buffer;
const auto inverse_scale = 1. / quantization_params.scale;
CHECK(buffer.type == ArrayDataType::kFloat);
const auto& float_buffer =
static_cast<const Buffer<ArrayDataType::kFloat>&>(buffer);
auto* quantized_buffer = new Buffer<A>;
quantized_buffer->data.resize(float_buffer.data.size());
for (std::size_t i = 0; i < float_buffer.data.size(); i++) {
const float src_val = float_buffer.data[i];
double scaled_val; // Astonishingly, using 'float' degrades accuracy just
// enough to make a few tests fail!
if (quantization_params.scale == 0) {
CHECK_EQ(src_val, 0) << "The quantization scale for this array is 0, "
<< "so all its values should be 0.";
scaled_val = quantization_params.zero_point;
} else {
scaled_val = quantization_params.zero_point + inverse_scale * src_val;
}
auto integer_val = tflite::SafeCast<DataType<A>>(std::round(scaled_val));
// In addition to its effect on the choice of quantization params upstream
// of here, narrow_range also means nudge the min quantized value by +1,
// so e.g. uint8 values get constrained to [1, 255].
if (integer_val == std::numeric_limits<DataType<A>>::min() &&
array.narrow_range) {
integer_val++;
}
quantized_buffer->data[i] = integer_val;
}
return std::unique_ptr<GenericBuffer>(quantized_buffer);
}
template <ArrayDataType A>
void QuantizeArray(GraphTransformation* transformation, Model* model,
const std::string& name,
const QuantizationParams& quantization_params) {
auto& array = model->GetArray(name);
CHECK(array.data_type == ArrayDataType::kFloat);
CHECK(!array.quantization_params);
array.GetOrCreateQuantizationParams() = quantization_params;
if (array.buffer) {
array.buffer = QuantizeBuffer<A>(array, quantization_params);
}
array.data_type = A;
array.final_data_type = A;
transformation->AddMessageF(
"Quantized array %s to %s zero_point=%g, scale=%g", name,
ArrayDataTypeName(array.data_type), quantization_params.zero_point,
quantization_params.scale);
}
} // namespace
void QuantizeArray(GraphTransformation* transformation, Model* model,
const std::string& name, ArrayDataType quantized_data_type,
const QuantizationParams& quantization_params) {
ArrayDataType adjusted_data_type = quantized_data_type;
auto& array = model->GetArray(name);
if (array.final_data_type == ArrayDataType::kInt16) {
adjusted_data_type = array.final_data_type;
}
switch (adjusted_data_type) {
case ArrayDataType::kUint8:
return QuantizeArray<ArrayDataType::kUint8>(transformation, model, name,
quantization_params);
case ArrayDataType::kInt16:
return QuantizeArray<ArrayDataType::kInt16>(transformation, model, name,
quantization_params);
case ArrayDataType::kInt32:
return QuantizeArray<ArrayDataType::kInt32>(transformation, model, name,
quantization_params);
default:
LOG(FATAL) << "Unhandled case.";
}
}
bool IsArrayQuantizedRangeSubset(GraphTransformation* transformation,
const Array& array, double clamp_min,
double clamp_max) {
ArrayDataType quantized_data_type =
GetQuantizedDataType(array, array.data_type);
if (quantized_data_type == ArrayDataType::kNone ||
quantized_data_type == ArrayDataType::kFloat) {
// The array is not (or never will be) quantized.
return false;
}
QuantizationParams quantization_params;
if (!array.quantization_params) {
if (!array.minmax) {
transformation->AddMessageF("No quantization params and no minmax");
return false;
} else {
// Work around cases where we are asking for this prior to the Quantize
// transformation having added the quantization_params.
ChooseQuantizationParamsForArrayAndQuantizedDataType(
array, quantized_data_type, &quantization_params);
transformation->AddMessageF(
"No quantization params - inferring from data type %s with minmax "
"%g,%g as zero_point=%g, scale=%g",
ArrayDataTypeName(quantized_data_type), array.minmax->min,
array.minmax->max, quantization_params.zero_point,
quantization_params.scale);
}
} else {
quantization_params = array.GetQuantizationParams();
}
double quantized_min, quantized_max;
CHECK(GetQuantizedDataTypeNumericalRange(quantized_data_type, &quantized_min,
&quantized_max))
<< "Type is not quantized";
bool has_nontrivial_min_bound = false;
bool has_nontrivial_max_bound = false;
double lowest_representable_output =
(quantized_min - quantization_params.zero_point) *
quantization_params.scale;
if (lowest_representable_output < clamp_min) {
has_nontrivial_min_bound = true;
transformation->AddMessageF(
"Quantized activation function is not trivial: "
"the lowest representable output value %g"
" less than the clamp min bound %g.",
lowest_representable_output, clamp_min);
}
double highest_representable_output =
(quantized_max - quantization_params.zero_point) *
quantization_params.scale;
if (highest_representable_output > clamp_max) {
has_nontrivial_max_bound = true;
transformation->AddMessageF(
"Quantized activation function is not trivial: "
"the highest representable output value %g"
" is greater than the clamp max bound %g.",
highest_representable_output, clamp_max);
}
return !has_nontrivial_min_bound && !has_nontrivial_max_bound;
}
} // namespace toco
@@ -0,0 +1,65 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#ifndef TENSORFLOW_LITE_TOCO_GRAPH_TRANSFORMATIONS_QUANTIZATION_UTIL_H_
#define TENSORFLOW_LITE_TOCO_GRAPH_TRANSFORMATIONS_QUANTIZATION_UTIL_H_
#include <string>
#include "tensorflow/lite/kernels/internal/quantization_util.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
namespace toco {
// Gets the target quantized data type of an array based on the fake quant op.
// For example, if the num_bits is 8 the data type will be kUint8.
bool InferQuantizedDataTypeFromFakeQuant(
const FakeQuantOperator& op, ArrayDataType* out_quantized_data_type);
// Gets the min/max numerical range for the given quantized data type.
// For example, kUint8 will return [0,255].
// Returns true if the ranges were set and false if the type is not quantized.
bool GetQuantizedDataTypeNumericalRange(ArrayDataType data_type,
double* out_min_value,
double* out_max_value);
// Returns the quantized data type of an array, falling back to the provided
// default data type.
ArrayDataType GetQuantizedDataType(const Array& array,
ArrayDataType default_type);
// Chooses the quantization params for a given array and a given target
// quantized data type (which may not be the array's current data type).
void ChooseQuantizationParamsForArrayAndQuantizedDataType(
const Array& array, ArrayDataType quantized_data_type,
QuantizationParams* quantization_params);
// Quantizes an array by setting its data type and (if constant) quantizing
// all values in the array.
void QuantizeArray(GraphTransformation* transformation, Model* model,
const std::string& name, ArrayDataType quantized_data_type,
const QuantizationParams& quantization_params);
// Returns true if the given array, when quantized, contains only values between
// the provided clamp min/max.
// Either clamp_min or clamp_max may be +/-infinity to indicate that the value
// is unbounded on that side.
bool IsArrayQuantizedRangeSubset(GraphTransformation* transformation,
const Array& array, double clamp_min,
double clamp_max);
} // namespace toco
#endif // TENSORFLOW_LITE_TOCO_GRAPH_TRANSFORMATIONS_QUANTIZATION_UTIL_H_
@@ -0,0 +1,740 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <limits>
#include <memory>
#include <set>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "tensorflow/core/platform/errors.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/quantization_util.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/model_flags.pb.h"
#include "tensorflow/lite/toco/toco_types.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
bool SupportsQuantization(Model* model, const Operator& op) {
auto type = op.type;
if (type == OperatorType::kUnsupported) {
auto* unsupported = static_cast<const TensorFlowUnsupportedOperator*>(&op);
return unsupported->quantized;
}
if (op.type == OperatorType::kRange) {
const auto& array = model->GetArray(op.outputs[0]);
return (array.data_type != ArrayDataType::kFloat &&
array.data_type != ArrayDataType::kFloat16);
}
// Please add op in alpha-beta sequence.
static const std::set<OperatorType> supported_ops{
OperatorType::kAdd,
OperatorType::kArgMax,
OperatorType::kArgMin,
OperatorType::kAveragePool,
OperatorType::kBatchToSpaceND,
OperatorType::kConcatenation,
OperatorType::kConv,
OperatorType::kDepthToSpace,
OperatorType::kDepthwiseConv,
OperatorType::kDiv,
OperatorType::kEqual,
OperatorType::kExpandDims,
OperatorType::kFullyConnected,
OperatorType::kGather,
OperatorType::kGreater,
OperatorType::kGreaterEqual,
OperatorType::kHardSwish,
OperatorType::kL2Normalization,
OperatorType::kLeakyRelu,
OperatorType::kLess,
OperatorType::kLessEqual,
OperatorType::kLogistic,
OperatorType::kLogSoftmax,
OperatorType::kLstmCell,
OperatorType::kMatrixDiag,
OperatorType::kMatrixSetDiag,
OperatorType::kMaximum,
OperatorType::kMaxPool,
OperatorType::kMean,
OperatorType::kMinimum,
OperatorType::kMirrorPad,
OperatorType::kMul,
OperatorType::kPack,
OperatorType::kPad,
OperatorType::kPadV2,
OperatorType::kPRelu,
OperatorType::kRandomUniform,
OperatorType::kReduceMax,
OperatorType::kReduceMin,
OperatorType::kRelu,
OperatorType::kRelu1,
OperatorType::kRelu6,
OperatorType::kReshape,
OperatorType::kResizeBilinear,
OperatorType::kResizeNearestNeighbor,
OperatorType::kSelect,
OperatorType::kShape,
OperatorType::kSlice,
OperatorType::kSoftmax,
OperatorType::kSpaceToBatchND,
OperatorType::kSpaceToDepth,
OperatorType::kSparseToDense,
OperatorType::kSplit,
OperatorType::kSplitV,
OperatorType::kSqueeze,
OperatorType::kStridedSlice,
OperatorType::kSub,
OperatorType::kSum,
OperatorType::kTanh,
OperatorType::kTopK_V2,
OperatorType::kTranspose,
OperatorType::kTransposeConv,
OperatorType::kUnpack,
};
return supported_ops.find(type) != supported_ops.end();
}
// The quantized op allows output arrays of type float using
// the attribute support_output_type_float_in_quantized_op
bool SupportOutputTypeFloatInQuantizedOp(const Operator& op) {
auto type = op.type;
if (type == OperatorType::kUnsupported) {
auto* unsupported = static_cast<const TensorFlowUnsupportedOperator*>(&op);
return unsupported->support_output_type_float_in_quantized_op;
}
return false;
}
const MinMax& GetOrComputeMinMax(Model* model, const std::string& array_name) {
auto& array = model->GetArray(array_name);
// Normally we should have a MinMax recorded on this Array,
// so we just use it.
if (array.minmax != nullptr) {
return *array.minmax;
}
// We don't have a MinMax. That's bad news: we need
// the graph to provide MinMax info for all arrays in order
// for inference to reproduce faithfully the same quantization
// error as the training process had.
//
// But we still want to support a fallback for constant arrays,
// just using the plain min and max computed from array elements.
// We should hopefully never rely on that in production, as that
// will not give very good accuracy as that typically won't be
// exactly what the training process used. But it will be useful
// to allow easily trying out quantization even if the graph
// lacks some minmax information.
if (array.buffer != nullptr) {
CHECK(array.buffer->type == ArrayDataType::kFloat);
const auto& data = array.GetBuffer<ArrayDataType::kFloat>().data;
// We always want [min, max] to contain 0.
float min = 0.f;
float max = 0.f;
for (const auto& val : data) {
min = std::min(min, val);
max = std::max(max, val);
}
if (min == 0.f && max == 0.f) {
// Prevent downstream anger from quantized math that expects min and max
// to not be equal.
max = 1.f;
}
// No need to warn about accuracy if all array values are equal to either
// min or max:
// in that case, quantization is exact, and such arrays are not learned
// weights arrays for which fake-quantization would make sense, rather
// they tend to be hardcoded arrays of zeros or ones used in some graphs.
bool is_quantization_trivially_exact = true;
for (const auto& val : data) {
is_quantization_trivially_exact &= (val == min || val == max);
}
if (!is_quantization_trivially_exact) {
LOG(WARNING)
<< "Constant array " << array_name
<< " lacks MinMax information. To make up for that, we will now "
"compute"
<< " the MinMax from actual array elements. That will result in"
<< " quantization parameters that probably do not match whichever "
"arithmetic"
<< " was used during training, and thus will probably be a cause of "
"poor"
<< " inference accuracy.";
}
auto& minmax = array.GetOrCreateMinMax();
minmax.min = min;
minmax.max = max;
return minmax;
}
LOG(FATAL) << "Array " << array_name
<< " does not have MinMax information, "
"and is not a constant array. Cannot "
"proceed with quantization.";
}
struct QuantizationPoints {
int64_t min_value;
int64_t max_value;
int64_t central_value;
};
template <ArrayDataType A>
QuantizationPoints GetQuantizationPoints() {
QuantizationPoints qp;
using Integer = DataType<A>;
qp.min_value = std::numeric_limits<Integer>::min();
qp.max_value = std::numeric_limits<Integer>::max();
// eg [-128,127]...
qp.central_value = (qp.min_value / 2 + // -128 -> -64.
(qp.max_value - 1) / 2 + // 127 -> 63.
1);
return qp;
}
QuantizationPoints GetQuantizationPoints(ArrayDataType data_type) {
switch (data_type) {
case ArrayDataType::kUint8:
return GetQuantizationPoints<ArrayDataType::kUint8>();
case ArrayDataType::kInt16:
return GetQuantizationPoints<ArrayDataType::kInt16>();
case ArrayDataType::kInt32:
return GetQuantizationPoints<ArrayDataType::kInt32>();
default:
LOG(FATAL) << "Unhandled case.";
}
}
bool ChooseQuantizationForOperatorInput(
GraphTransformation* transformation, Model* model, const Operator& op,
std::size_t input_index, ArrayDataType* quantized_data_type,
QuantizationParams* quantization_params) {
const auto& input = op.inputs[input_index];
auto& array = model->GetArray(input);
if (array.data_type != ArrayDataType::kFloat) {
return false;
}
// Quantization of bias vectors
bool is_bias_vector = false;
int activations_input_index;
int weights_input_index;
if (op.type == OperatorType::kConv ||
op.type == OperatorType::kDepthwiseConv ||
op.type == OperatorType::kFullyConnected) {
if (input_index == 2) {
is_bias_vector = true;
activations_input_index = 0;
weights_input_index = 1;
}
}
if (op.type == OperatorType::kTransposeConv) {
if (input_index == 3) {
is_bias_vector = true;
activations_input_index = 2;
weights_input_index = 1;
}
}
if (op.type == OperatorType::kLstmCell) {
if (input_index == LstmCellOperator::BIASES_INPUT) {
is_bias_vector = true;
activations_input_index = LstmCellOperator::DATA_INPUT;
weights_input_index = LstmCellOperator::WEIGHTS_INPUT;
}
}
if (is_bias_vector) {
// Quantization of bias vector.
// We need both of the mandatory inputs (input activations and weights) to
// have been already quantized.
const auto& input_activations =
model->GetArray(op.inputs[activations_input_index]);
const auto& input_weights = model->GetArray(op.inputs[weights_input_index]);
if (!input_activations.quantization_params ||
!input_weights.quantization_params) {
transformation->AddMessageF(
"Input array %s is a bias vector but has no qparams", input);
return false;
}
const auto input_activations_scale =
input_activations.quantization_params->scale;
const auto input_weights_scale = input_weights.quantization_params->scale;
quantization_params->scale = input_activations_scale * input_weights_scale;
quantization_params->zero_point = 0;
*quantized_data_type = GetQuantizedDataType(array, ArrayDataType::kInt32);
transformation->AddMessageF(
"Input array %s is a bias vector. Choosing quantization params "
"accordingly.",
input);
return true;
}
const MinMax& minmax = GetOrComputeMinMax(model, input);
if (op.type == OperatorType::kLstmCell) {
if (input_index == LstmCellOperator::PREV_STATE_INPUT) {
*quantized_data_type = ArrayDataType::kInt16;
ChooseQuantizationParamsForArrayAndQuantizedDataType(
array, *quantized_data_type, quantization_params);
return true;
}
}
*quantized_data_type = GetQuantizedDataType(array, ArrayDataType::kUint8);
ChooseQuantizationParamsForArrayAndQuantizedDataType(
array, *quantized_data_type, quantization_params);
transformation->AddMessageF(
"For input array %s with min=%g, max=%g, chose to quantize as %s (f=%s) "
"with zero_point=%d, scale=%g",
input, minmax.min, minmax.max, ArrayDataTypeName(*quantized_data_type),
ArrayDataTypeName(array.final_data_type), quantization_params->zero_point,
quantization_params->scale);
return true;
}
bool IsExactlyRepresentable(double real_value, ArrayDataType data_type,
const QuantizationParams& quantization_params) {
const double scaled_value =
quantization_params.zero_point + real_value / quantization_params.scale;
const double fractional_scaled_value =
scaled_value - std::round(scaled_value);
if (std::abs(fractional_scaled_value) > 1e-12) {
return false;
}
const double rounded_scaled_value = std::round(scaled_value);
if (data_type == ArrayDataType::kUint8) {
if (rounded_scaled_value < 0 || rounded_scaled_value > 255) {
return false;
}
}
return true;
}
// Quantized data type is preset to the type of the input before this function.
bool ChooseHardcodedQuantizationForOperatorOutput(
const Operator& op, const Array& array, ArrayDataType* quantized_data_type,
QuantizationParams* quantization_params) {
if (op.type == OperatorType::kL2Normalization) {
// L2Normalization has range: [-1, 1].
// 0 should be exactly representable, as values will typically be centered
// around 0, with many values near 0.
*quantized_data_type = GetQuantizedDataType(array, *quantized_data_type);
const QuantizationPoints qp = GetQuantizationPoints(*quantized_data_type);
quantization_params->zero_point = qp.central_value;
quantization_params->scale = 1. / (qp.central_value - qp.min_value);
CHECK(
IsExactlyRepresentable(0., *quantized_data_type, *quantization_params));
return true;
}
if (op.type == OperatorType::kLogistic || op.type == OperatorType::kSoftmax) {
// Logistic and Softmax have range: [0, 1].
//
// For Logistic, 0.5 should be exactly representable, as implementations
// will typically exploit the symmetry logistic(-x) = 1 - logistic(x), and
// the glueing of the two halves of the graph will only be seamless if we
// are accurately representing logistic(0) == 0.5.
*quantized_data_type = GetQuantizedDataType(array, *quantized_data_type);
const QuantizationPoints qp = GetQuantizationPoints(*quantized_data_type);
quantization_params->zero_point = 0;
quantization_params->scale = 1. / (qp.max_value + 1);
CHECK(IsExactlyRepresentable(0.5, *quantized_data_type,
*quantization_params));
return true;
}
if (op.type == OperatorType::kLogSoftmax) {
// LogSoftmax has range: [LogSoftmaxOperator::kOutputRangeMin, 0].
*quantized_data_type = GetQuantizedDataType(array, *quantized_data_type);
const QuantizationPoints qp = GetQuantizationPoints(*quantized_data_type);
quantization_params->zero_point = qp.max_value;
quantization_params->scale =
-LogSoftmaxOperator::kOutputRangeMin / (qp.max_value + 1);
// While not strictly necessary, it is easier to interpret output data and
// quantization if the scale is similar to others (such as power of 2).
CHECK(IsExactlyRepresentable(LogSoftmaxOperator::kOutputRangeMin / 2,
*quantized_data_type, *quantization_params));
return true;
}
if (op.type == OperatorType::kTanh) {
// Tanh has the range: [-1, 1].
*quantized_data_type = GetQuantizedDataType(array, *quantized_data_type);
const QuantizationPoints qp = GetQuantizationPoints(*quantized_data_type);
quantization_params->zero_point = qp.central_value;
quantization_params->scale = 1. / (qp.central_value - qp.min_value);
// 0 should be exactly representable, as values will typically be centered
// around 0, with many values near 0.
CHECK(
IsExactlyRepresentable(0., *quantized_data_type, *quantization_params));
return true;
}
return false;
}
bool ChooseQuantizationForOperatorOutput(
GraphTransformation* transformation, Model* model, const Operator& op,
std::size_t output_index, ArrayDataType* quantized_data_type,
QuantizationParams* quantization_params) {
const auto& output = op.outputs[output_index];
auto& array = model->GetArray(output);
if (array.data_type != ArrayDataType::kFloat) {
transformation->AddMessageF("Array data type already set to %s, final=%s",
ArrayDataTypeName(array.data_type),
ArrayDataTypeName(array.final_data_type));
return false;
}
*quantized_data_type = model->GetArray(op.inputs[0]).data_type;
if (ChooseHardcodedQuantizationForOperatorOutput(
op, array, quantized_data_type, quantization_params)) {
transformation->AddMessageF(
"Output array %s is produced by a %s operator. Choosing fixed "
"quantization params accordingly.",
output, OperatorTypeName(op.type));
return true;
}
if ((op.type == OperatorType::kConcatenation &&
model->flags.change_concat_input_ranges()) ||
op.type == OperatorType::kDepthToSpace ||
op.type == OperatorType::kSpaceToDepth ||
op.type == OperatorType::kReshape || op.type == OperatorType::kSplit ||
op.type == OperatorType::kRelu || op.type == OperatorType::kRelu1 ||
op.type == OperatorType::kRelu6 || op.type == OperatorType::kPRelu ||
op.type == OperatorType::kUnpack || op.type == OperatorType::kSlice ||
op.type == OperatorType::kStridedSlice ||
op.type == OperatorType::kAveragePool ||
op.type == OperatorType::kMaxPool) {
int data_input_index = 0;
if (op.type == OperatorType::kSplit) {
data_input_index = 1;
}
// Copying and rearrangement ops should preserve the quantization parameters
// of the input array.
const auto& input_array = model->GetArray(op.inputs[data_input_index]);
const auto& input_quantization_params = input_array.GetQuantizationParams();
*quantized_data_type =
GetQuantizedDataType(input_array, ArrayDataType::kUint8);
*quantized_data_type = GetQuantizedDataType(array, *quantized_data_type);
quantization_params->zero_point = input_quantization_params.zero_point;
quantization_params->scale = input_quantization_params.scale;
transformation->AddMessageF(
"Output array %s is produced by a %s operator. Copying quantization "
"params from input array.",
output, OperatorTypeName(op.type));
return true;
}
const MinMax& minmax = GetOrComputeMinMax(model, output);
if (op.type == OperatorType::kLstmCell) {
if (output_index == LstmCellOperator::STATE_OUTPUT ||
output_index == LstmCellOperator::ACTIV_TEMP) {
*quantized_data_type = ArrayDataType::kInt16;
ChooseQuantizationParamsForArrayAndQuantizedDataType(
array, *quantized_data_type, quantization_params);
return true;
}
}
*quantized_data_type = GetQuantizedDataType(array, ArrayDataType::kUint8);
ChooseQuantizationParamsForArrayAndQuantizedDataType(
array, *quantized_data_type, quantization_params);
transformation->AddMessageF(
"For output array %s with min=%g, max=%g"
", chose to quantize as %s with zero_point=%d"
", scale=%g",
output, minmax.min, minmax.max, ArrayDataTypeName(*quantized_data_type),
quantization_params->zero_point, quantization_params->scale);
return true;
}
// Fixes array minmax info to match the quantization parameters.
// This is required for when quantization parameters change for an array during
// quantization (such as ChooseQuantizationForOperatorOutput).
void FixMinMaxPostQuantization(GraphTransformation* transformation,
ArrayDataType quantized_data_type,
const QuantizationParams& quantization_params,
MinMax* minmax) {
double quantized_min, quantized_max;
if (!GetQuantizedDataTypeNumericalRange(quantized_data_type, &quantized_min,
&quantized_max)) {
// Not quantized - no update required.
return;
}
// Compute new minmax values.
double min = (quantized_min - quantization_params.zero_point) *
quantization_params.scale;
double max = (quantized_max - quantization_params.zero_point) *
quantization_params.scale;
// If we are close to the existing minmax values don't bother changing them.
// This prevents propagating small floating point precision errors.
constexpr double kMinMaxThreshold = 1e-5;
const double width = max - min;
if (std::abs(min - minmax->min) > kMinMaxThreshold * width ||
std::abs(max - minmax->max) > kMinMaxThreshold * width) {
transformation->AddMessageF(
"Adjusting min/max from %g,%g to %g,%g to match quantization params",
minmax->min, minmax->max, min, max);
minmax->min = min;
minmax->max = max;
}
}
} // namespace
absl::Status Quantize::Run(Model* model, std::size_t op_index, bool* modified) {
*modified = false;
// Our general "quantization" graph transformation consists in replacing
// QuantizedInputArrays[] ->
// DequantizeOperators[] ->
// FloatInputArrays[] ->
// Operator ->
// FloatOutputArray
// by
// QuantizedInputArrays[] ->
// Operator ->
// QuantizedOutputArray ->
// DequantizeOperator ->
// FloatOutputArray
//
// In other words, this is pushing Dequantize operators to the right of
// other operators.
//
auto& op = *model->operators[op_index];
if (op.type == OperatorType::kDequantize ||
op.type == OperatorType::kFakeQuant) {
return absl::OkStatus();
}
// Our assumption here is that the input arrays are already quantized -
// that is typically the case in models operating on an input bitmap
// image, and MakeInitialDequantizeOp should have already resolved
// the handling of the input image as an initial Dequantize op.
//
// Thus we are building around the assumption that the graph always starts
// with a quantized input array, and only after some Dequantize op do we have
// float arrays. The problem of quantizing the graph thus becomes a problem of
// pushing Dequantize ops to the right of other ops.
//
// Let us just guard this assumption by the following assertion:
for (const auto& input : op.inputs) {
const auto& input_array = model->GetArray(input);
if (IsInputArray(*model, input) &&
input_array.data_type == ArrayDataType::kFloat) {
CHECK(input_array.quantization_params)
<< "Input array " << input << " is missing quantization_params";
}
}
if (!SupportsQuantization(model, op)) {
return absl::InvalidArgumentError(absl::StrCat(
"Unimplemented: this graph contains an operator of type ",
HelpfulOperatorTypeName(op),
" for which the quantized form is not yet implemented. Sorry, and "
"patches welcome (that's a relatively fun patch to write, mostly "
"providing the actual quantized arithmetic code for this op)."));
}
for (const auto& input : op.inputs) {
const auto& array = model->GetArray(input);
if (array.data_type == ArrayDataType::kFloat) {
if (!array.minmax && !array.buffer) {
LOG(WARNING) << "Can't quantize input array " << input
<< " because it lacks min/max info";
return absl::OkStatus();
}
const auto* other_op = GetOpWithOutput(*model, input);
if (other_op && other_op->type != OperatorType::kDequantize) {
AddMessageF(
"Not quantizing %s for now, because its input array %s is not "
"produced by a Dequantize op, "
"which means that we should yield and let other ops "
"get quantized first",
LogName(op), input);
return absl::OkStatus();
}
}
}
bool changed = false;
// Quantize inputs, remove any Dequantize op on the inputs side
for (std::size_t input_index = 0; input_index < op.inputs.size();
input_index++) {
ArrayDataType quantized_data_type;
QuantizationParams quantization_params;
if (ChooseQuantizationForOperatorInput(this, model, op, input_index,
&quantized_data_type,
&quantization_params)) {
const auto& input = op.inputs[input_index];
if (IsConstantParameterArray(*model, input)) {
QuantizeArray(this, model, input, quantized_data_type,
quantization_params);
changed = true;
} else {
auto dequantize_it = FindOpWithOutput(*model, input);
if (dequantize_it != model->operators.end()) {
auto* dequantize_op = dequantize_it->get();
CHECK(dequantize_op->type == OperatorType::kDequantize);
op.inputs[input_index] = dequantize_op->inputs[0];
// Check if the output of that Dequantize op was not used by any
// other operator. We will then erase that Dequantize op.
if (!CountOpsWithInput(*model, dequantize_op->outputs[0])) {
if (!IsDiscardableArray(*model, dequantize_op->outputs[0])) {
// The dequantize output is not discardable. Special care needed.
// If any of the model's output_arrays was pointing to the
// Dequantize op's output, let it point to the Dequantize op's
// input instead.
for (int i = 0; i < model->flags.output_arrays_size(); i++) {
if (model->flags.output_arrays(i) ==
dequantize_op->outputs[0]) {
// TODO(b/78013785): never rename output arrays.
if (IsInputArray(*model, dequantize_op->inputs[0])) {
// The op input is an input array and the output is an
// output array and we can't have an array be both. Insert a
// copy op to ensure the two arrays stay separate.
AddMessageF(
"Tried to rename output array %d while removing "
"dequant "
"op %s but array is also an input; inserting copy %s "
"-> %s",
i, LogName(*dequantize_op),
model->flags.output_arrays(i),
dequantize_op->inputs[0]);
InsertCopyOperator(model, dequantize_op->inputs[0],
dequantize_op->outputs[0]);
} else {
// Op output is strictly used as an output array, so we can
// just rename the array and directly bypass the op.
AddMessageF(
"Renaming output array %d after removing dequant op "
"%s: "
"%s -> %s",
i, LogName(*dequantize_op),
model->flags.output_arrays(i),
dequantize_op->inputs[0]);
model->flags.set_output_arrays(i, dequantize_op->inputs[0]);
}
break;
}
}
}
DeleteOpAndArrays(model, dequantize_op);
}
changed = true;
} else {
// This input array is not produced by a Dequantize op.
// We have encountered this situation in RNN graphs, whose cyclic
// nature defeats the basic assumption underlying the quantization
// algorithm implemented here. For now, when we have seen this
// happening, the array in question was a RNN state array itself,
// so let us just implement this case here, and guard that assumption
// with a CHECK. A more general fix would involve revisiting the
// design of this whole Quantization transformation.
bool is_rnn_state_array = false;
for (const auto& rnn_state : model->flags.rnn_states()) {
if (rnn_state.state_array() == input) {
is_rnn_state_array = true;
break;
}
}
CHECK(is_rnn_state_array);
QuantizeArray(this, model, input, quantized_data_type,
quantization_params);
changed = true;
}
}
}
}
// Quantize outputs, add Dequantize ops as needed on the outputs side
if (SupportOutputTypeFloatInQuantizedOp(op)) {
LOG(WARNING)
<< HelpfulOperatorTypeName(op) << " is a quantized op"
<< " but it has a model flag that sets the output arrays to float.";
} else {
for (std::size_t output_index = 0; output_index < op.outputs.size();
output_index++) {
QuantizationParams quantization_params;
ArrayDataType quantized_data_type;
if (ChooseQuantizationForOperatorOutput(this, model, op, output_index,
&quantized_data_type,
&quantization_params)) {
changed = true;
const auto& output = op.outputs[output_index];
auto& output_array = model->GetArray(output);
// Fix up the min/max information on the output array to match the
// chosen quantization parameters.
CHECK(output_array.minmax)
<< "Output array named " << output << " lacks minmax";
auto& output_minmax = output_array.GetMinMax();
FixMinMaxPostQuantization(this, quantized_data_type,
quantization_params, &output_minmax);
QuantizeArray(this, model, output, quantized_data_type,
quantization_params);
const auto& dequantized_output =
AvailableArrayName(*model, output + "_dequantized");
auto& dequantized_output_array =
model->GetOrCreateArray(dequantized_output);
dequantized_output_array.data_type = ArrayDataType::kFloat;
dequantized_output_array.final_data_type = output_array.data_type;
auto& dequantized_output_minmax =
dequantized_output_array.GetOrCreateMinMax();
dequantized_output_minmax.min = output_minmax.min;
dequantized_output_minmax.max = output_minmax.max;
for (const auto& other_op : model->operators) {
for (auto& other_op_input : other_op->inputs) {
if (other_op_input == output) {
other_op_input = dequantized_output;
}
}
}
auto* dequantize_op = new DequantizeOperator;
dequantize_op->inputs = {output};
dequantize_op->outputs = {dequantized_output};
for (int i = 0; i < model->flags.output_arrays_size(); i++) {
if (model->flags.output_arrays(i) == output) {
// TODO(b/78013785): never rename output arrays.
AddMessageF(
"Renaming output array %d after inserting dequant op %s: %s -> "
"%s",
i, LogName(*dequantize_op), model->flags.output_arrays(i),
dequantized_output);
model->flags.set_output_arrays(i, dequantized_output);
}
}
const auto op_it = FindOp(*model, &op);
model->operators.emplace(op_it + 1, dequantize_op);
}
}
}
*modified = changed;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,83 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
namespace toco {
namespace {
bool ApplyAttrsToArray(GraphTransformation* transformation, Model* model,
const FakeQuantOperator& fq_op,
const std::string& array_name) {
bool changed = false;
auto& annotated_array = model->GetArray(array_name);
if (!annotated_array.minmax) {
const MinMax& minmax = *fq_op.minmax;
annotated_array.GetOrCreateMinMax() = minmax;
transformation->AddMessageF(
"Read min/max annotation for array %s: min=%g, max=%g", array_name,
minmax.min, minmax.max);
changed = true;
}
if (fq_op.narrow_range && !annotated_array.narrow_range) {
annotated_array.narrow_range = true;
transformation->AddMessageF("Read narrow_range annotation for array %s",
array_name);
changed = true;
}
return changed;
}
} // end namespace
absl::Status ReadArrayMinmaxAndNarrowRangeFromFakeQuant::Run(
Model* model, std::size_t op_index, bool* modified) {
*modified = false;
const auto fakequant_it = model->operators.begin() + op_index;
auto* fakequant_base_op = fakequant_it->get();
if (fakequant_base_op->type != OperatorType::kFakeQuant) {
return absl::OkStatus();
}
auto* fq_op = static_cast<FakeQuantOperator*>(fakequant_base_op);
if (!fq_op->minmax) {
// Need to be resolved first by ResolveFakeQuantArgsFromVars.
return absl::OkStatus();
}
// At this point, this FakeQuantOperator should have a MinMax
// attached to it, and should only have 1 input (it should not have
// 2nd and 3rd input arrays giving min and max anymore).
CHECK(fq_op->minmax);
CHECK_EQ(1, fq_op->inputs.size());
bool changed = false;
changed |= ApplyAttrsToArray(this, model, *fq_op, fq_op->inputs[0]);
changed |= ApplyAttrsToArray(this, model, *fq_op, fq_op->outputs[0]);
*modified = changed;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,61 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/model_flags.pb.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status RemoveFinalDequantizeOp::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto dequantize_it = model->operators.begin() + op_index;
const auto* dequantize_op = dequantize_it->get();
if (dequantize_op->type != OperatorType::kDequantize) {
return absl::OkStatus();
}
const auto& output = dequantize_op->outputs[0];
// We can remove any dequantize op whose output is not consumed by
// any op. This is not necessarily equivalent to the output being
// one of the model's output arrays, as some intermediate array
// in the middle of the graph might be designated as an output
// array.
if (CountOpsWithInput(*model, output)) {
return absl::OkStatus();
}
// If one of the model's output arrays was actually the Dequantize op's
// output, then we need to update it to point to the Dequantize op's input.
for (int i = 0; i < model->flags.output_arrays_size(); i++) {
if (output == model->flags.output_arrays(i)) {
model->flags.set_output_arrays(i, dequantize_op->inputs[0]);
}
}
// Remove the node and its output array.
AddMessageF("Removed final %s", LogName(*dequantize_op));
DeleteOpAndArrays(model, dequantize_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,97 @@
/* Copyright 2019 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
bool TransformsToIdentity(std::vector<int> const& perm1,
std::vector<int> const& perm2) {
if (perm2.size() != perm1.size() || perm1.empty()) {
return false;
}
// perm1 is the order of the indices after first transpose. When perm1 is
// reordered according to perm2, if the result is simple increasing sequence
// i.e., range(0, perm1.size()), then the two transposes cancel each other.
for (size_t i = 0; i < perm1.size(); ++i) {
if (perm1[i] < 0 || perm1[i] >= static_cast<int>(perm1.size()) ||
perm2[i] < 0 || perm2[i] >= static_cast<int>(perm1.size())) {
return false;
}
if (perm1[perm2[i]] != static_cast<int>(i)) {
return false;
}
}
return true;
}
void ReplaceOpInputsWith(Model* model, const std::string& lookfor,
const std::string& replacewith) {
for (const auto& op : model->operators) {
for (size_t i = 0; i < op->inputs.size(); ++i) {
if (op->inputs[i] == lookfor) {
op->inputs[i] = replacewith;
}
}
}
}
} // namespace
absl::Status RemoveSuccessiveTranspose::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto op = model->operators.begin() + op_index;
if (op->get()->type != OperatorType::kTranspose) {
return absl::OkStatus();
}
TransposeOperator* t_op = static_cast<TransposeOperator*>(op->get());
if (CountOpsWithInput(*model, t_op->outputs[0]) != 1) {
return absl::OkStatus();
}
Operator* next = GetOpWithInput(*model, t_op->outputs[0]);
if (!next || next->type != OperatorType::kTranspose) {
return absl::OkStatus();
}
TransposeOperator* t_next = static_cast<TransposeOperator*>(next);
if (!CountOpsWithInput(*model, t_next->outputs[0])) {
return absl::OkStatus();
}
if (TransformsToIdentity(t_op->perm, t_next->perm)) {
// Find the input tensor that uses the results of transpose t_next, then
// make it point to the input of t_op, effectively isolating both the
// transposes from the graph.
ReplaceOpInputsWith(model, t_next->outputs[0], t_op->inputs[0]);
DeleteOpAndArrays(model, t_next);
DeleteOpAndArrays(model, t_op);
*modified = true;
}
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,66 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status RemoveTensorFlowAssert::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto assert_it = model->operators.begin() + op_index;
const auto* assert_op = assert_it->get();
if (assert_op->type != OperatorType::kAssert) {
return absl::OkStatus();
}
bool changed = false;
// Remove any other node's dependency on this assert node
for (const auto& op : model->operators) {
auto it = op->inputs.begin();
while (it != op->inputs.end()) {
if (*it == assert_op->outputs[0]) {
op->inputs.erase(it);
changed = true;
} else {
++it;
}
}
}
CHECK(!CountOpsWithInput(*model, assert_op->outputs[0]));
if (changed) {
AddMessageF(
"Prepared for the removal of %s by removing any other op's dependency "
"on it",
LogName(*assert_op));
}
// That's it. We can stop here, no need to duplicate the work that
// RemoveUnusedOp will do removing this now-unused node.
*modified = changed;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,40 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/remove_trivial_passthrough.h"
#include "tensorflow/lite/toco/model.h"
namespace toco {
absl::Status RemoveTensorFlowIdentity::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto passthru_it = model->operators.begin() + op_index;
const auto* passthru_op = passthru_it->get();
if (passthru_op->type != OperatorType::kIdentity) {
return absl::OkStatus();
}
*modified = RemoveTrivialPassthroughOp(this, model, op_index);
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,141 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/remove_trivial_passthrough.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/runtime/types.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
template <typename Scalar>
bool AreAllBufferElementsEqualTo(const std::vector<Scalar>& buffer_data,
Scalar value) {
for (const auto& x : buffer_data) {
if (x != value) {
return false;
}
}
return true;
}
} // namespace
// A binary operator is called trivial when exactly one of its operands is
// a constant and is such that the binary operation is equivalent to
// the identity operation on its other input.
// For example, an Add operator is trivial if
// one of its operands is constant 0, a Mul operator is trivial
// if one of its operands is constant 1, etc.
absl::Status RemoveTrivialBinaryOperator::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
const auto binary_it = model->operators.begin() + op_index;
auto* binary_op = binary_it->get();
if (binary_op->type != OperatorType::kAdd &&
binary_op->type != OperatorType::kMul &&
binary_op->type != OperatorType::kSub &&
binary_op->type != OperatorType::kDiv) {
return absl::OkStatus();
}
CHECK_EQ(binary_op->inputs.size(), 2);
// This graph transformation is only concerned with the case
// when one input is constant and the other is not constant.
const bool is_input_constant[2] = {
IsConstantParameterArray(*model, binary_op->inputs[0]),
IsConstantParameterArray(*model, binary_op->inputs[1]),
};
if (!is_input_constant[0] && !is_input_constant[1]) {
// Neither input is constant, so nothing we can resolve here.
return absl::OkStatus();
}
if (is_input_constant[0] && is_input_constant[1]) {
// Both inputs are constants. That's a job for constants
// propagation, not for us to handle here.
return absl::OkStatus();
}
const int index_of_constant_input = is_input_constant[0] ? 0 : 1;
const int index_of_variable_input = is_input_constant[0] ? 1 : 0;
CHECK(is_input_constant[index_of_constant_input]);
CHECK(!is_input_constant[index_of_variable_input]);
// If this was a broadcasting op we can't remove it as we need the broadcast.
// It's possible we could replace it with a cheaper op, though.
const auto& input_array_0 = model->GetArray(binary_op->inputs[0]);
const auto& input_array_1 = model->GetArray(binary_op->inputs[1]);
if (!input_array_0.has_shape() || !input_array_1.has_shape()) {
// Both input shapes must be known.
return absl::OkStatus();
}
if (input_array_0.shape().dimensions_count() ==
input_array_1.shape().dimensions_count() &&
input_array_0.shape() != input_array_1.shape()) {
AddMessageF(
"Preserving %s even though it's trivial as we need to broadcast "
"(lhs %s, rhs %s)",
LogName(*binary_op), ShapeToString(input_array_0.shape()),
ShapeToString(input_array_1.shape()));
return absl::OkStatus();
}
// Now check if the constant operand makes this binary
// operator trivial.
const auto& constant_input_array =
model->GetArray(binary_op->inputs[index_of_constant_input]);
// For now, we only handle floats here.
if (constant_input_array.data_type != ArrayDataType::kFloat) {
return absl::OkStatus();
}
const auto& constant_input_float_data =
constant_input_array.GetBuffer<ArrayDataType::kFloat>().data;
bool is_trivial = false;
if (binary_op->type == OperatorType::kAdd) {
is_trivial = AreAllBufferElementsEqualTo(constant_input_float_data, 0.f);
} else if (binary_op->type == OperatorType::kSub) {
is_trivial = index_of_constant_input == 1 &&
AreAllBufferElementsEqualTo(constant_input_float_data, 0.f);
} else if (binary_op->type == OperatorType::kMul) {
is_trivial = AreAllBufferElementsEqualTo(constant_input_float_data, 1.f);
} else if (binary_op->type == OperatorType::kDiv) {
is_trivial = index_of_constant_input == 1 &&
AreAllBufferElementsEqualTo(constant_input_float_data, 1.f);
}
is_trivial = is_trivial && binary_op->fused_activation_function ==
FusedActivationFunctionType::kNone;
if (!is_trivial) {
return absl::OkStatus();
}
// Now we know that this node is trivial, so we can remove it.
AddMessageF("Removing trivial %s", LogName(*binary_op));
*modified = RemoveTrivialPassthroughOp(this, model, op_index);
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,42 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/remove_trivial_passthrough.h"
#include "tensorflow/lite/toco/model.h"
namespace toco {
absl::Status RemoveTrivialConcatenation::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto concat_it = model->operators.begin() + op_index;
auto* concat_op = concat_it->get();
if (concat_op->type != OperatorType::kConcatenation) {
return absl::OkStatus();
}
if (concat_op->inputs.size() != 1) {
return absl::OkStatus();
}
*modified = RemoveTrivialPassthroughOp(this, model, op_index);
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,71 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status RemoveTrivialConcatenationInput::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
// TensorFlow allows Concatenation nodes to have 0-D inputs,
// and they are then treated as empty i.e. omitted from concatenation,
// in violation of the notion that 0-D is equivalent to 1x1x1x1.
// Thus we have to drop these 0-D inputs from Concatenation nodes.
// Sometimes, there will remain only one non-trivial input, and
// the other graph transformation RemoveTrivialConcatenation will then drop
// it.
const auto concat_it = model->operators.begin() + op_index;
auto* concat_op = concat_it->get();
if (concat_op->type != OperatorType::kConcatenation) {
return absl::OkStatus();
}
std::vector<std::string> trivial_inputs;
std::vector<std::string> nontrivial_inputs;
for (const std::string& input : concat_op->inputs) {
const auto& input_array = model->GetArray(input);
const bool is_trivial =
input_array.has_shape() && input_array.shape().dimensions_count() == 0;
if (is_trivial) {
trivial_inputs.push_back(input);
} else {
nontrivial_inputs.push_back(input);
}
}
if (trivial_inputs.empty()) {
return absl::OkStatus();
}
// Drop trivial inputs.
concat_op->inputs = nontrivial_inputs;
for (const std::string& input : trivial_inputs) {
DeleteArrayIfUnusedOutsideOfOp(input, concat_op, model);
}
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,90 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/remove_trivial_passthrough.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
bool IsFakeQuantTrivial(GraphTransformation* transformation, const Model& model,
const FakeQuantOperator& fakequant_op) {
CHECK(fakequant_op.type == OperatorType::kFakeQuant);
if (!fakequant_op.minmax) {
// Require ReadFakeQuantMinMax to have run.
return false;
}
// FakeQuants are trivial if they are taking input from another identical
// FakeQuant op.
auto* producing_op = GetOpWithOutput(model, fakequant_op.inputs[0]);
if (!producing_op || producing_op->type != OperatorType::kFakeQuant) {
return false;
}
const auto& producing_fakequant_op =
*static_cast<FakeQuantOperator*>(producing_op);
if (!producing_fakequant_op.minmax) {
// Require ReadFakeQuantMinMax to have run.
return false;
}
if (*fakequant_op.minmax == *producing_fakequant_op.minmax &&
fakequant_op.num_bits == producing_fakequant_op.num_bits) {
transformation->AddMessageF(
"%s is trivial because it is preceded by an identical FakeQuant %s",
LogName(fakequant_op), LogName(producing_fakequant_op));
return true;
}
return false;
}
} // namespace
// Removes FakeQuant ops that are trivial (have no effect, are redundant, etc).
absl::Status RemoveTrivialFakeQuant::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto op_it = model->operators.begin() + op_index;
auto* op = op_it->get();
if (op->type != OperatorType::kFakeQuant) {
return absl::OkStatus();
}
auto* fakequant_op = static_cast<FakeQuantOperator*>(op);
if (!IsFakeQuantTrivial(this, *model, *fakequant_op)) {
AddMessageF("%s is not trivial", LogName(*fakequant_op));
return absl::OkStatus();
}
AddMessageF("Removing trivial %s", LogName(*fakequant_op));
CHECK_EQ(fakequant_op->inputs.size(), 1);
*modified = RemoveTrivialPassthroughOp(this, model, op_index);
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,140 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
// Reroute all edges involving a given discardable array to another
// array instead. from_array is assumed to be discardable, and consequently
// this only updates operator edges (since discardable arrays only
// appear there, and not e.g. in model flags).
void Reroute(const std::string& from, const std::string& to, Model* model) {
for (const auto& op : model->operators) {
for (auto& output : op->outputs) {
if (output == from) {
output = to;
}
}
for (auto& input : op->inputs) {
if (input == from) {
input = to;
}
}
}
const Array& from_array = model->GetArray(from);
Array& to_array = model->GetOrCreateArray(to);
// Preserve minmax information if to_array didn't already have any.
if (from_array.minmax && !to_array.minmax) {
to_array.GetOrCreateMinMax() = from_array.GetMinMax();
// If we're copying minmax info, then we should also be copying
// narrow_range, which affects how minmax info is to be interpreted.
to_array.narrow_range = from_array.narrow_range;
}
// Separately, also preserve final_data_type if to_array didn't already
// have any.
if (from_array.final_data_type != ArrayDataType::kNone &&
to_array.final_data_type == ArrayDataType::kNone) {
to_array.final_data_type = from_array.final_data_type;
}
// The 'from' array may now be unused. We delete it here immediately
// so that this function doesn't violate graph invariants (no unused arrays)
// and as it's not trivial to get this right for the caller since
// DeleteOpAndArrays will no longer delete this array, since it's no longer
// referenced by this op.
DeleteArrayIfUnused(from, model);
}
} // namespace
bool RemoveTrivialPassthroughOp(GraphTransformation* transformation,
Model* model, std::size_t op_index,
int input_index) {
auto passthru_it = model->operators.begin() + op_index;
auto* passthru_op = passthru_it->get();
CHECK_EQ(passthru_op->outputs.size(), 1);
CHECK_GE(passthru_op->inputs.size(), 1);
int main_input_array_index = 0;
if (input_index != -1) {
main_input_array_index = input_index;
} else {
// We call 'main input' the unique nonconstant input array if there is one,
// or else the 0-th input.
int count_nonconstant_input_arrays = 0;
for (size_t i = 0; i < passthru_op->inputs.size(); i++) {
if (!model->GetArray(passthru_op->inputs[i]).buffer) {
count_nonconstant_input_arrays++;
if (count_nonconstant_input_arrays == 1) {
main_input_array_index = i;
}
}
}
}
const std::string main_input_name =
passthru_op->inputs[main_input_array_index];
const std::string output_name = passthru_op->outputs[0];
if (IsDiscardableArray(*model, output_name)) {
transformation->AddMessageF(
"Removing %s, keeping its non-constant input array %s and removing %s",
LogName(*passthru_op), main_input_name, output_name);
Reroute(output_name, main_input_name, model);
} else if (IsDiscardableArray(*model, main_input_name) &&
!IsConstantParameterArray(*model, main_input_name)) {
transformation->AddMessageF(
"Removing %s, keeping its output array %s and removing non-constant "
"input %s",
LogName(*passthru_op), output_name, main_input_name);
Reroute(main_input_name, output_name, model);
} else {
transformation->AddMessageF(
"Cannot remove %s, neither its main input nor its output may be "
"discarded",
LogName(*passthru_op));
if (passthru_op->type != OperatorType::kReshape &&
model->GetArray(main_input_name).has_shape()) {
// We can't remove either array but we can remove the op. Converting it to
// a reshape gives us some hope of later on fixing that (either in the
// final runtime or as an additional fixup step).
//
// Note that we don't try to insert copies in place of reshapes as the
// copy itself is a trivial reshape and we'd go into an infinite loop!
transformation->AddMessageF("Replacing with a copy (reshape) instead");
InsertCopyOperator(model, main_input_name, output_name);
// To avoid using invalidated iterator, evaluate passthru_it again.
passthru_it = model->operators.begin() + op_index;
} else {
return false;
}
}
// Remove the pass-through node.
DeleteOpAndArrays(model, passthru_op);
return true;
}
} // namespace toco
@@ -0,0 +1,58 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#ifndef TENSORFLOW_LITE_TOCO_GRAPH_TRANSFORMATIONS_REMOVE_TRIVIAL_PASSTHROUGH_H_
#define TENSORFLOW_LITE_TOCO_GRAPH_TRANSFORMATIONS_REMOVE_TRIVIAL_PASSTHROUGH_H_
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
namespace toco {
// A "passthrough op" is an op that satisfies the following conditions:
// 1. One of its inputs is (per the semantics of that op) its "main input"
// for some notion of "main input" that is operator-specific; for example,
// for a Reshape op, the main input is the array being reshaped, not the
// other input which gives the new shape.
// 2. It has exactly one output.
// 3. It forwards exactly its main input to its single output.
//
// Examples include:
// 1. TensorFlow Identity ops. (Have one input).
// 2. TensorFlow Reshape ops when the input and output shapes agree.
// 3. Any binary operator, one of whose two inputs is a constant and is the
// neutral value for that operation. For example, a binary Add operator
// where one of its inputs is a constant array filled with zeros.
//
// A passthrough op is "trivial" and can be removed when it is possible to
// discard either its main input or output array, rerouting any
// edge involving it to the other of these two arrays.
//
// It is only possible to discard such an array if it is not explicitly
// designated as a global input/output array of the graph, e.g. the model's
// input arrays, output arrays, and any array involved in a RNN back-edge
// specified by the model.
//
// This function does not check that the given operator is a passthrough op:
// that's the responsibility of the caller.
// Given that it is a passthrough op, this function checks whether it is trivial
// and then discards it and returns true, or, if it's not trivial (if neither
// the input nor the output may be discarded), returns false.
bool RemoveTrivialPassthroughOp(GraphTransformation* transformation,
Model* model, std::size_t op_index,
int input_index = -1);
} // namespace toco
#endif // TENSORFLOW_LITE_TOCO_GRAPH_TRANSFORMATIONS_REMOVE_TRIVIAL_PASSTHROUGH_H_
@@ -0,0 +1,132 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <limits>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/quantization_util.h"
#include "tensorflow/lite/toco/graph_transformations/remove_trivial_passthrough.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/runtime/types.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
bool IsTrivialUnfusedActivationFunc(GraphTransformation* transformation,
const Model& model, OperatorType op_type,
const std::string& input_array_name) {
double clamp_min;
double clamp_max;
switch (op_type) {
case OperatorType::kRelu:
clamp_min = 0.0;
clamp_max = std::numeric_limits<double>::infinity();
break;
case OperatorType::kRelu1:
clamp_min = -1.0;
clamp_max = 1.0;
break;
case OperatorType::kRelu6:
clamp_min = 0.0;
clamp_max = 6.0;
break;
default:
return false;
}
const auto& input_array = model.GetArray(input_array_name);
return IsArrayQuantizedRangeSubset(transformation, input_array, clamp_min,
clamp_max);
}
bool IsTrivialFusedActivationFunc(
GraphTransformation* transformation, const Model& model,
FusedActivationFunctionType activation_function,
const std::string& output_array_name) {
double clamp_min;
double clamp_max;
switch (activation_function) {
case FusedActivationFunctionType::kNone:
return false;
case FusedActivationFunctionType::kRelu:
clamp_min = 0.0;
clamp_max = std::numeric_limits<double>::infinity();
break;
case FusedActivationFunctionType::kRelu1:
clamp_min = -1.0;
clamp_max = 1.0;
break;
case FusedActivationFunctionType::kRelu6:
clamp_min = 0.0;
clamp_max = 6.0;
break;
default:
LOG(FATAL) << "Unsupported fused activation type: "
<< static_cast<int>(activation_function);
return false;
}
const auto& output_array = model.GetArray(output_array_name);
return IsArrayQuantizedRangeSubset(transformation, output_array, clamp_min,
clamp_max);
}
} // namespace
// Attempts to remove both fused and unfused activation functions if the
// quantization params indicate that the representable values fall inside the
// activation range.
absl::Status RemoveTrivialQuantizedActivationFunc::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
const auto it = model->operators.begin() + op_index;
auto* op = it->get();
if (op->inputs.empty()) {
return absl::OkStatus();
}
if (IsTrivialUnfusedActivationFunc(this, *model, op->type, op->inputs[0])) {
AddMessageF(
"Removing trivial unfused activation function %s because the input "
"minmax imply at least as tight a clamp anyway.",
LogName(*op));
*modified = RemoveTrivialPassthroughOp(this, model, op_index);
return absl::OkStatus();
}
if (IsTrivialFusedActivationFunc(this, *model, op->fused_activation_function,
op->outputs[0])) {
op->fused_activation_function = FusedActivationFunctionType::kNone;
AddMessageF(
"Removing trivial quantized activation function on %s "
"because the output quantization parameters imply at least as tight "
"a clamp anyway.",
LogName(*op));
*modified = true;
return absl::OkStatus();
}
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,96 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <limits>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/quantization_util.h"
#include "tensorflow/lite/toco/graph_transformations/remove_trivial_passthrough.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
bool IsTrivialMinMax(GraphTransformation* transformation, const Model& model,
OperatorType op_type, const std::string& input_array_name,
const std::string& clamp_value_array_name) {
const auto& clamp_value_array = model.GetArray(clamp_value_array_name);
if (!IsConstantParameterArray(model, clamp_value_array_name)) {
transformation->AddMessageF("Clip value array %s is non-constant",
clamp_value_array_name);
return false;
}
const auto& clamp_value_buffer =
clamp_value_array.GetBuffer<ArrayDataType::kFloat>();
CHECK_EQ(clamp_value_buffer.Length(), 1);
float clamp_value = clamp_value_buffer.data[0];
double clamp_min;
double clamp_max;
switch (op_type) {
case OperatorType::kMinimum: // Element-wise Minimum
clamp_min = -std::numeric_limits<double>::infinity();
clamp_max = clamp_value;
break;
case OperatorType::kMaximum: // Element-wise Maximum
clamp_min = clamp_value;
clamp_max = std::numeric_limits<double>::infinity();
break;
default:
CHECK(false);
return false;
}
const auto& input_array = model.GetArray(input_array_name);
return IsArrayQuantizedRangeSubset(transformation, input_array, clamp_min,
clamp_max);
}
} // namespace
// Attempts to remove min/max functions if the quantization params indicate that
// the representable values fall inside the clip range.
absl::Status RemoveTrivialQuantizedMinMax::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
const auto it = model->operators.begin() + op_index;
auto* op = it->get();
if ((op->type != OperatorType::kMinimum &&
op->type != OperatorType::kMaximum) ||
op->inputs.size() != 2) {
return absl::OkStatus();
}
if (IsTrivialMinMax(this, *model, op->type, op->inputs[0], op->inputs[1])) {
AddMessageF(
"Removing trivial min/max %s because the quantization parameters imply "
"at least as tight a clamp anyway.",
LogName(*op));
*modified = RemoveTrivialPassthroughOp(this, model, op_index);
return absl::OkStatus();
}
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,106 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/remove_trivial_passthrough.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
bool IsReshapeTrivial(const Model& model, const Operator& op,
RemoveTrivialReshape* transformation) {
CHECK(op.type == OperatorType::kReshape);
// One way in which a reshape can be trivial is if its
// output shape is == its input shape
const auto& input_array = model.GetArray(op.inputs[0]);
const auto& output_array = model.GetArray(op.outputs[0]);
if (input_array.has_shape() && output_array.has_shape()) {
if (transformation->treat_expand_dims_as_trivial() &&
ShapesAgreeUpToExtending(input_array.shape(), output_array.shape())) {
transformation->AddMessageF(
"%s is trivial because its input and output shapes are equal up to "
"extending by 1's, and we are told to aggressively discard such "
"Reshape ops.",
LogName(op));
return true;
}
if (input_array.shape().dims() == output_array.shape().dims()) {
transformation->AddMessageF(
"%s is trivial because its input and output shapes are equal",
LogName(op));
return true;
}
}
// Another way in which a reshape can be trivial is if its output
// is only consumed by another reshape.
if (CountOpsWithInput(model, op.outputs[0]) == 1) {
const auto* next_op = GetOpWithInput(model, op.outputs[0]);
if (next_op->type == OperatorType::kReshape) {
if (!IsDiscardableArray(model, next_op->outputs[0])) {
// If the |next_op| output is used as a model output we need to preserve
// its shape.
transformation->AddMessageF(
"%s cannot be merged into following reshape %s as it is "
"non-discardable and must keep the specified shape",
LogName(op), LogName(*next_op));
return false;
}
transformation->AddMessageF(
"%s is trivial because its output is only consumed by another "
"Reshape op %s",
LogName(op), LogName(*next_op));
return true;
}
}
return false;
}
} // namespace
absl::Status RemoveTrivialReshape::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto reshape_it = model->operators.begin() + op_index;
auto* reshape_op = reshape_it->get();
if (reshape_op->type != OperatorType::kReshape) {
return absl::OkStatus();
}
if (!IsReshapeTrivial(*model, *reshape_op, this)) {
AddMessageF("%s is not trivial", LogName(*reshape_op));
return absl::OkStatus();
}
AddMessageF("Removing trivial %s", LogName(*reshape_op));
CHECK_EQ(reshape_op->inputs.size(), 2);
*modified = RemoveTrivialPassthroughOp(this, model, op_index);
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,73 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/remove_trivial_passthrough.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
bool IsSliceTrivial(const Model& model, const Operator& op,
RemoveTrivialSlice* transformation) {
CHECK(op.type == OperatorType::kSlice);
// Slices are trivial if they are slicing the entire input contents.
const auto& input_array = model.GetArray(op.inputs[0]);
const auto& output_array = model.GetArray(op.outputs[0]);
if (input_array.has_shape() && output_array.has_shape()) {
if (input_array.shape() == output_array.shape()) {
transformation->AddMessageF(
"%s is trivial because its input and output shapes are equal",
LogName(op));
return true;
}
}
return false;
}
} // namespace
absl::Status RemoveTrivialSlice::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto reshape_it = model->operators.begin() + op_index;
auto* slice_op = reshape_it->get();
if (slice_op->type != OperatorType::kSlice) {
return absl::OkStatus();
}
if (!IsSliceTrivial(*model, *slice_op, this)) {
return absl::OkStatus();
}
AddMessageF("Removing trivial %s", LogName(*slice_op));
CHECK_EQ(slice_op->inputs.size(), 3);
*modified = RemoveTrivialPassthroughOp(this, model, op_index);
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,97 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/model_flags.pb.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status RemoveUnusedOp::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto it = model->operators.begin() + op_index;
const auto* op = it->get();
// Bail if any output is used, and is not an input_array of
// the model. We allow specifying an arbitrary input_array,
// treating the part of the graph leading up to it as unused.
for (const auto& output : op->outputs) {
CHECK(model->HasArray(output));
// If this output is provided as the model's input array,
// then we don't need this operator to produce its contents.
if (IsInputArray(*model, output)) {
continue;
}
// If this output is provided as a RNN's state array,
// then we don't need this operator to produce its contents.
// So far this case has only been encountered with TensorFlow
// Fill ops used to zero-initialize RNN states, which is
// redundant for us as we zero-initialize RNN states anyway.
bool found_output_as_rnn_state_array = false;
for (const auto& rnn_state : model->flags.rnn_states()) {
if (output == rnn_state.state_array()) {
CHECK(op->type == OperatorType::kFill ||
op->type == OperatorType::kIdentity);
found_output_as_rnn_state_array = true;
break;
}
}
if (found_output_as_rnn_state_array) {
continue;
}
for (const std::string& output_array : model->flags.output_arrays()) {
if (output == output_array) {
return absl::OkStatus();
}
}
for (const auto& rnn_state : model->flags.rnn_states()) {
if (output == rnn_state.back_edge_source_array()) {
// The output is consumed by a RNN back-edge..
if (!IsDiscardableArray(*model, rnn_state.back_edge_source_array()) ||
!IsDiscardableArray(*model, rnn_state.state_array()) ||
CountOpsWithInput(*model, rnn_state.state_array())) {
return absl::OkStatus();
}
}
}
if (CountOpsWithInput(*model, output)) {
return absl::OkStatus();
}
}
if (op->unresolved_outputs) {
AddMessageF("Not discarding %s because it has unresolved outputs.",
LogName(*op));
return absl::OkStatus();
}
AddMessageF("Discarding %s because none of its outputs is used.",
LogName(*op));
DeleteOpAndArrays(model, op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,158 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
bool IsElementwiseOperator(OperatorType optype) {
switch (optype) {
case OperatorType::kCast:
case OperatorType::kCeil:
case OperatorType::kExp:
case OperatorType::kFloor:
case OperatorType::kNeg:
case OperatorType::kRelu:
case OperatorType::kRelu1:
case OperatorType::kRelu6:
case OperatorType::kRound:
case OperatorType::kTanh:
case OperatorType::kSqrt:
case OperatorType::kSquare:
return true;
default:
return false;
}
}
bool IsMoveOperator(OperatorType optype) {
switch (optype) {
case OperatorType::kDepthToSpace:
case OperatorType::kExpandDims:
case OperatorType::kSpaceToDepth:
case OperatorType::kSqueeze:
case OperatorType::kReshape:
case OperatorType::kTranspose:
return true;
default:
return false;
}
}
} // namespace
// Swap elementwise operators such that all value operators occur before all
// element move operators, e.g. negation then transpose.
absl::Status ReorderElementwiseUnary::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto element_op_it = model->operators.begin() + op_index;
std::unique_ptr<Operator>& element_op = *element_op_it;
if (!IsElementwiseOperator(element_op->type)) {
return absl::OkStatus();
}
const std::string intermediate_name = element_op->inputs[0];
auto it = FindOpWithOutput(*model, intermediate_name);
if (it == model->operators.end()) {
AddMessageF("No preceding operator");
return absl::OkStatus();
}
std::unique_ptr<Operator>& move_op = *it;
if (!IsMoveOperator(move_op->type)) {
AddMessageF("Preceding operator is not a move operator");
return absl::OkStatus();
}
if (CountOpsWithInput(*model, intermediate_name) != 1) {
AddMessageF("Input %s used elsewhere", intermediate_name);
return absl::OkStatus();
}
// Check that the intermediate is discardable.
if (!IsDiscardableArray(*model, intermediate_name)) {
AddMessageF(
"Cannot swap elementwise as it would invalidate %s which is "
"an output array.",
intermediate_name);
return absl::OkStatus();
}
// op->inputs may change so we need to keep a value by copy.
const std::string input_name = move_op->inputs[0];
const std::string output_name = element_op->outputs[0];
AddMessageF("Swapping around operators with %s and %s", LogName(*element_op),
LogName(*move_op));
// If the output array is an exit node for the graph then we need to retain
// the name as an output node. This makes the naming scheme a little confusing
// but is required in this rare case.
if (!IsDiscardableArray(*model, output_name)) {
// The output name of the sequence needs to stay static, so create a new
// array new use for the intermediate.
const auto new_intermediate_name =
AvailableArrayName(*model, element_op->outputs[0] + "_reorder");
AddMessageF("Adding new array %s to preserve output array name %s",
new_intermediate_name, output_name);
element_op->inputs[0] = input_name;
element_op->outputs[0] = new_intermediate_name;
DeleteArrayIfUnused(intermediate_name, model);
move_op->inputs[0] = new_intermediate_name;
move_op->outputs[0] = output_name;
} else {
// The intermediate array is now the output array.
for (size_t i = 0; i < model->operators.size(); i++) {
Operator* consumer = model->operators[i].get();
for (size_t j = 0; j < consumer->inputs.size(); j++) {
if (consumer->inputs[j] == output_name) {
consumer->inputs[j] = intermediate_name;
}
}
}
element_op->inputs[0] = input_name;
move_op->inputs[0] = output_name;
}
// Reset both arrays as shape, type, min/max, etc can all change because of
// the position swap.
model->EraseArray(element_op->outputs[0]);
model->EraseArray(move_op->outputs[0]);
// Reconstruct.
model->GetOrCreateArray(element_op->outputs[0]);
model->GetOrCreateArray(move_op->outputs[0]);
// Swap the order of the operators.
element_op.swap(move_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,261 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <algorithm>
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/container/flat_hash_map.h"
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
bool OperatorReady(const Model& model, const Operator* op) {
if (!model.HasArray(op->inputs[0]) || !model.HasArray(op->inputs[1]) ||
!model.HasArray(op->outputs[0])) {
return false;
}
if (!model.GetArray(op->inputs[0]).has_shape() ||
!model.GetArray(op->outputs[0]).has_shape()) {
// Input and output needs the shape.
return false;
}
if (!model.GetArray(op->inputs[1]).buffer) {
// Buffer needs to be a constant.
return false;
}
return true;
}
// Computes a new permutation used to swap a reshape-transpose to a
// transpose-reshape. In this case the permutation operates on the intermediate
// shape.
std::vector<int> ComputeNewPerm(const std::vector<int>& input_dims,
const std::vector<int>& intermediate_dims,
const std::vector<int>& perm) {
// These are the major axis of the input.
std::vector<int> input_indices;
for (size_t i = 0; i < input_dims.size(); i++) {
if (input_dims[i] != 1) {
input_indices.push_back(i);
}
}
// This maps which indices of the input produced the intermediate indices for
// non-unary dimensions.
absl::flat_hash_map<int, int> intermediate_to_input_indices_map;
int non_unary_count = 0;
for (size_t i = 0; i < intermediate_dims.size(); i++) {
if (intermediate_dims[i] != 1) {
intermediate_to_input_indices_map[i] = input_indices[non_unary_count++];
}
}
// Translate the transpose permutation to a new permutation starting with the
// major indices.
std::vector<int> new_perm;
new_perm.reserve(input_dims.size());
for (size_t i = 0; i < perm.size(); i++) {
if (intermediate_dims[perm[i]] == 1) continue;
new_perm.push_back(intermediate_to_input_indices_map[perm[i]]);
}
// Fill the rest of the transpose in with the ones.
for (size_t index = 0; index < input_dims.size(); index++) {
if (input_dims[index] == 1) {
new_perm.push_back(index);
}
}
CHECK_EQ(new_perm.size(), input_dims.size());
return new_perm;
}
} // namespace
// Swaps reshape-transpose to transpose-reshape whenever possible. This is
// possible when the reshape does not affect memory ordering.
absl::Status ReorderReshapeTranspose::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto transpose_it = model->operators.begin() + op_index;
TransposeOperator* transpose_op = ConvertOperator<TransposeOperator*>(
transpose_it->get(), OperatorType::kTranspose);
if (transpose_op == nullptr) {
return absl::OkStatus();
}
if (!OperatorReady(*model, transpose_op) || transpose_op->perm.empty()) {
// Wait for values to propagate.
return absl::OkStatus();
}
// Find the operator that produces the transpose op.
auto reshape_it = FindOpWithOutput(*model, transpose_op->inputs[0]);
if (reshape_it == model->operators.end()) {
return absl::OkStatus();
}
TensorFlowReshapeOperator* reshape_op =
ConvertOperator<TensorFlowReshapeOperator*>(reshape_it->get(),
OperatorType::kReshape);
if (reshape_op == nullptr) {
return absl::OkStatus();
}
// Ignore if the reshape is uninitialized.
if (!OperatorReady(*model, reshape_op) || reshape_op->shape.empty()) {
return absl::OkStatus();
}
// Need to copy to keep static if permutated.
const std::string input_name = reshape_op->inputs[0];
const std::string intermediate_name = reshape_op->outputs[0];
const std::string output_name = transpose_op->outputs[0];
// Intermediate should not be consumed by any other operators.
if (CountOpsWithInput(*model, intermediate_name) != 1) {
AddMessageF("Input %s used elsewhere", intermediate_name);
return absl::OkStatus();
}
// Check that the intermediate is not an output array.
if (!IsDiscardableArray(*model, intermediate_name)) {
AddMessageF(
"Cannot reorder reshape-transpose as it would invalidate %s which is "
"an output array.",
intermediate_name);
return absl::OkStatus();
}
// Get the arrays.
const Array& input_array = model->GetArray(input_name);
const Array& intermediate_array = model->GetArray(intermediate_name);
const Array& output_array = model->GetArray(output_name);
// Get the shapes of each array.
const Shape& input_shape = input_array.shape();
const Shape& intermediate_shape = intermediate_array.shape();
const Shape& output_shape = output_array.shape();
// Assign ids to non-unary indices.
const std::vector<int>& input_dims = input_shape.dims();
const std::vector<int>& intermediate_dims = intermediate_shape.dims();
const std::vector<int>& output_dims = output_shape.dims();
for (int p : transpose_op->perm) {
if (p < 0 || p >= static_cast<int>(intermediate_dims.size())) {
return absl::InvalidArgumentError(absl::StrCat(
"Invalid perm attribute ", p, " for intermediate tensor of rank ",
intermediate_dims.size(), " in Transpose op."));
}
}
// If the reshape is equivalent to a transpose with fewer/more unary
// dimensions then it can be moved between the transpose.
if (!ReshapeIsEquivalentToTranspose(*model, reshape_op,
true /*allow_extra_unary_dims*/)) {
return absl::OkStatus();
}
if (!IsDiscardableArray(*model, output_name)) {
// The output name of the sequence needs to stay static, so create a new
// array new use for the intermediate.
const std::string new_intermediate_name =
AvailableArrayName(*model, transpose_op->outputs[0] + "_exchange");
AddMessageF("Adding new array %s to preserve output array name %s",
new_intermediate_name, transpose_op->outputs[0]);
transpose_op->inputs[0] = input_name;
transpose_op->outputs[0] = new_intermediate_name;
reshape_op->inputs[0] = new_intermediate_name;
reshape_op->outputs[0] = output_name;
DeleteArrayIfUnused(intermediate_name, model);
} else {
// The intermediate array is now the output array.
for (const std::unique_ptr<Operator>& consumer_ptr : model->operators) {
Operator* consumer = consumer_ptr.get();
for (std::string& input : consumer->inputs) {
if (input == output_name) {
input = intermediate_name;
}
}
}
transpose_op->inputs[0] = input_name;
reshape_op->inputs[0] = output_name;
}
// If transposes constant buffer is used elsewhere, make a new copy.
if (CountOpsWithInput(*model, transpose_op->inputs[1]) != 1) {
transpose_op->inputs[1] =
AvailableArrayName(*model, transpose_op->inputs[1] + "_copy");
}
// Make the new transpose permutation.
const std::vector<int> new_perm =
ComputeNewPerm(input_dims, intermediate_dims, transpose_op->perm);
CHECK_EQ(input_dims.size(), new_perm.size());
Array& transpose_array = model->GetOrCreateArray(transpose_op->inputs[1]);
transpose_array.data_type = ArrayDataType::kInt32;
transpose_array.GetMutableBuffer<ArrayDataType::kInt32>().data = new_perm;
*(transpose_array.mutable_shape()->mutable_dims()) = {
static_cast<int>(new_perm.size())};
transpose_op->perm = new_perm;
// If the reshape's constant buffer is reused, create a new one.
if (CountOpsWithInput(*model, reshape_op->inputs[1]) != 1) {
reshape_op->inputs[1] =
AvailableArrayName(*model, reshape_op->inputs[1] + "_copy");
}
// We need to modify the reshape input array to target the new output size.
Array& reshape_array = model->GetOrCreateArray(reshape_op->inputs[1]);
reshape_array.data_type = ArrayDataType::kInt32;
reshape_array.GetMutableBuffer<ArrayDataType::kInt32>().data = output_dims;
*(reshape_array.mutable_shape()->mutable_dims()) = {
static_cast<int>(output_shape.dimensions_count())};
reshape_op->shape.clear();
AddMessageF("Swapping around operators between %s and %s", input_name,
output_name);
model->GetOrCreateArray(transpose_op->outputs[0]).clear_shape();
model->GetOrCreateArray(reshape_op->outputs[0]).clear_shape();
// Swap the order of the operators.
transpose_it->swap(*reshape_it);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,148 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status ResolveBatchNormalization::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto bn_it = model->operators.begin() + op_index;
if (bn_it->get()->type != OperatorType::kBatchNormalization) {
return absl::OkStatus();
}
const auto* bn_op =
static_cast<const BatchNormalizationOperator*>(bn_it->get());
auto& mean_array = model->GetArray(bn_op->inputs[1]);
const auto& multiplier_array = model->GetArray(bn_op->inputs[2]);
const auto& offset_array = model->GetArray(bn_op->inputs[3]);
// This graph transformation needs to address constant buffers below, so
// we need to exit early if these buffers don't exist yet (i.e. if the params
// haven't yet been resolved as constants) and will process it once they have.
if (!mean_array.buffer || !multiplier_array.buffer || !offset_array.buffer) {
return absl::OkStatus();
}
CHECK(IsConstantParameterArray(*model, bn_op->inputs[1]) &&
IsConstantParameterArray(*model, bn_op->inputs[2]) &&
IsConstantParameterArray(*model, bn_op->inputs[3]))
<< "Batch normalization resolution requires that mean, multiplier and "
"offset arrays be constant.";
// We should only have *float* BatchNormalizations... let's guard this
// assumption by CHECK's.
CHECK(mean_array.data_type == ArrayDataType::kFloat);
CHECK(multiplier_array.data_type == ArrayDataType::kFloat);
CHECK(offset_array.data_type == ArrayDataType::kFloat);
// Create the new Mul, Add operators
auto* mul_op = new MulOperator;
auto* add_op = new AddOperator;
const std::string mul_name =
AvailableArrayName(*model, bn_op->outputs[0] + "_mul");
const std::string add_name =
AvailableArrayName(*model, bn_op->outputs[0] + "_add");
const std::string mul_param_name =
AvailableArrayName(*model, mul_name + "_param");
const std::string add_param_name =
AvailableArrayName(*model, add_name + "_param");
mul_op->inputs = {bn_op->inputs[0], mul_param_name};
mul_op->outputs = {mul_name};
add_op->inputs = {mul_name, add_param_name};
add_op->outputs = {bn_op->outputs[0]};
AddMessageF("Splitting %s into %s and %s", LogName(*bn_op), LogName(*mul_op),
LogName(*add_op));
// Create the intermediate activation array (output of mul, input of add)
auto& intermediate_array = model->GetOrCreateArray(mul_op->outputs[0]);
intermediate_array.data_type = model->GetArray(bn_op->inputs[0]).data_type;
// Insert the new operators in the graph
auto add_it = model->operators.emplace(bn_it, add_op);
auto mul_it = model->operators.emplace(add_it, mul_op);
// update invalidated iterators.
DCHECK_EQ(mul_it->get(), mul_op);
add_it = mul_it + 1;
DCHECK_EQ(add_it->get(), add_op);
bn_it = add_it + 1;
DCHECK_EQ(bn_it->get(), bn_op);
// Create the new param arrays
auto& mean_shape = *mean_array.mutable_shape();
const auto& multiplier_shape = multiplier_array.shape();
const auto& offset_shape = offset_array.shape();
if (mean_shape.dims().empty()) {
*mean_shape.mutable_dims() = multiplier_shape.dims();
auto& data = mean_array.GetMutableBuffer<ArrayDataType::kFloat>().data;
CHECK_EQ(data.size(), 1);
data.resize(RequiredBufferSizeForShape(mean_shape), data[0]);
}
CHECK(mean_shape.dims() == multiplier_shape.dims());
CHECK(mean_shape.dims() == offset_shape.dims());
const auto& param_shape = mean_shape;
const int buffer_size = RequiredBufferSizeForShape(param_shape);
auto& mul_param_array = model->GetOrCreateArray(mul_param_name);
auto& add_param_array = model->GetOrCreateArray(add_param_name);
DropMinMax(model, mul_param_name);
DropMinMax(model, add_param_name);
mul_param_array.copy_shape(param_shape);
add_param_array.copy_shape(param_shape);
mul_param_array.data_type = ArrayDataType::kFloat;
add_param_array.data_type = ArrayDataType::kFloat;
auto& mul_float_data =
mul_param_array.GetMutableBuffer<ArrayDataType::kFloat>().data;
auto& add_float_data =
add_param_array.GetMutableBuffer<ArrayDataType::kFloat>().data;
mul_float_data.resize(buffer_size);
add_float_data.resize(buffer_size);
const auto& mean_float_data =
mean_array.GetBuffer<ArrayDataType::kFloat>().data;
const auto& multiplier_float_data =
multiplier_array.GetBuffer<ArrayDataType::kFloat>().data;
const auto& offset_float_data =
offset_array.GetBuffer<ArrayDataType::kFloat>().data;
size_t buffer_size_for_compare = buffer_size;
CHECK(mul_float_data.size() == buffer_size_for_compare);
CHECK(add_float_data.size() == buffer_size_for_compare);
CHECK(mean_float_data.size() == buffer_size_for_compare);
CHECK(multiplier_float_data.size() == buffer_size_for_compare);
CHECK(offset_float_data.size() == buffer_size_for_compare);
for (int i = 0; i < buffer_size; i++) {
mul_float_data[i] = multiplier_float_data[i];
add_float_data[i] =
offset_float_data[i] - mean_float_data[i] * multiplier_float_data[i];
}
DeleteOpAndArrays(model, bn_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,81 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status ResolveBatchToSpaceNDAttributes::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
const auto op_it = model->operators.begin() + op_index;
if (op_it->get()->type != OperatorType::kBatchToSpaceND)
return absl::OkStatus();
auto* op = static_cast<BatchToSpaceNDOperator*>(op_it->get());
// The attributes are resolved only when the 3 attributes (block_shape,
// before_crops, after_crops) are all constant.
if (!op->block_shape.empty()) {
return absl::OkStatus();
}
CHECK_EQ(op->inputs.size(), 3);
if (!IsConstantParameterArray(*model, op->inputs[1]) ||
!IsConstantParameterArray(*model, op->inputs[2]))
return absl::OkStatus();
// Handle crops
const auto& crops_array = model->GetArray(op->inputs[2]);
if (!crops_array.has_shape()) return absl::OkStatus();
const std::vector<int>& crops_dims = crops_array.shape().dims();
if (crops_dims.size() != 2) {
// Code only handles crops of 2 dimensions. Perhaps another transformation
// will delete this op.
return absl::OkStatus();
}
const std::vector<int>& crops_buffer =
crops_array.GetBuffer<ArrayDataType::kInt32>().data;
for (int i = 0; i < crops_dims[0]; ++i) {
op->before_crops.push_back(crops_buffer[i * 2]);
op->after_crops.push_back(crops_buffer[i * 2 + 1]);
}
// Handle block_shape
const auto& block_shape_array = model->GetArray(op->inputs[1]);
if (!block_shape_array.has_shape()) return absl::OkStatus();
const std::vector<int>& block_shape_dims = block_shape_array.shape().dims();
CHECK_EQ(block_shape_dims.size(), 1);
const std::vector<int>& block_shape_buffer =
block_shape_array.GetBuffer<ArrayDataType::kInt32>().data;
for (int i = 0; i < block_shape_dims[0]; ++i) {
op->block_shape.push_back(block_shape_buffer[i]);
}
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,259 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/runtime/types.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
std::vector<bool> VectorGreaterThan(const std::vector<int>& a,
const std::vector<int>& b) {
DCHECK_EQ(a.size(), b.size());
const int size = a.size();
std::vector<bool> result(size);
for (int i = 0; i < size; i++) {
result[i] = a[i] > b[i];
}
return result;
}
void PairwiseVectorSelect(const std::vector<bool>& selector,
const std::vector<int>& input_a,
const std::vector<int>& input_b,
std::vector<int>* output_a,
std::vector<int>* output_b) {
DCHECK_EQ(input_a.size(), input_b.size());
DCHECK_EQ(output_a->size(), output_b->size());
DCHECK_EQ(input_a.size(), output_a->size());
DCHECK_EQ(selector.size(), input_a.size());
const int size = input_a.size();
for (int i = 0; i < size; i++) {
if (selector[i]) {
(*output_a)[i] = input_a[i];
(*output_b)[i] = input_b[i];
} else {
(*output_a)[i] = input_b[i];
(*output_b)[i] = input_a[i];
}
}
}
template <ArrayDataType InputsDataType, ArrayDataType OutputDataType>
void EvaluateBinaryOperatorOnConstantInputs(Model* model,
const Operator* binary_op) {
CHECK(IsConstantParameterArray(*model, binary_op->inputs[0]));
CHECK(IsConstantParameterArray(*model, binary_op->inputs[1]));
CHECK(binary_op->fused_activation_function ==
FusedActivationFunctionType::kNone);
const auto& input0_array = model->GetArray(binary_op->inputs[0]);
const auto& input1_array = model->GetArray(binary_op->inputs[1]);
const auto& output_name = binary_op->outputs[0];
auto& output_array = model->GetArray(output_name);
CHECK(input0_array.data_type == InputsDataType);
CHECK(input1_array.data_type == InputsDataType);
CHECK(output_array.data_type == OutputDataType);
// We have already tested above for existence of input buffers
// (synonymous to being a constant param).
CHECK(input0_array.buffer);
CHECK(input1_array.buffer);
// On the other hand, the output should not already have a buffer.
CHECK(!output_array.buffer);
const auto& input0_data = input0_array.GetBuffer<InputsDataType>().data;
const auto& input1_data = input1_array.GetBuffer<InputsDataType>().data;
// Create the buffer on the output array, effectively turning it into
// a constant parameter
const Shape& output_shape = output_array.shape();
auto& output_data = output_array.GetMutableBuffer<OutputDataType>().data;
const int output_buffer_size = RequiredBufferSizeForShape(output_shape);
output_data.resize(output_buffer_size);
const int dims_count = output_shape.dimensions_count();
// It will be convenient here to have copies of the operands shapes
// extended to match the number of dimensions of the output shape.
Shape input0_shape = input0_array.shape();
Shape input1_shape = input1_array.shape();
ExtendShape(&input0_shape, dims_count);
ExtendShape(&input1_shape, dims_count);
// Now we may still have operands of different sizes, which would indicate
// that we have to "broadcast" the smaller dimension. We do this using a
// a vector of Booleans indicating which input is the larger in each
// dimension.
CHECK_EQ(input0_shape.dimensions_count(), input1_shape.dimensions_count());
CHECK_EQ(input0_shape.dimensions_count(), dims_count);
const std::vector<bool> input0_larger =
VectorGreaterThan(input0_shape.dims(), input1_shape.dims());
std::vector<int> big_sizes(dims_count);
std::vector<int> small_sizes(dims_count);
PairwiseVectorSelect(input0_larger, input0_shape.dims(), input1_shape.dims(),
&big_sizes, &small_sizes);
// The output should already be correctly sized to match the big dimensions.
for (int i = 0; i < dims_count; i++) {
CHECK_EQ(output_shape.dims(i), big_sizes[i]);
}
std::vector<int> input0_indices(dims_count);
std::vector<int> input1_indices(dims_count);
std::vector<int> modulo_indices(dims_count);
for (int k = 0; k < output_buffer_size; k++) {
const std::vector<int> output_indices = ReverseOffset(output_shape, k);
for (int i = 0; i < dims_count; i++) {
modulo_indices[i] = output_indices[i] % small_sizes[i];
}
PairwiseVectorSelect(input0_larger, output_indices, modulo_indices,
&input0_indices, &input1_indices);
const auto val0 = input0_data[Offset(input0_shape, input0_indices)];
const auto val1 = input1_data[Offset(input1_shape, input1_indices)];
DataType<OutputDataType> outval;
if (binary_op->type == OperatorType::kAdd) {
outval = val0 + val1;
} else if (binary_op->type == OperatorType::kMul) {
outval = val0 * val1;
} else if (binary_op->type == OperatorType::kSub) {
outval = val0 - val1;
} else if (binary_op->type == OperatorType::kDiv) {
outval = val0 / val1;
} else if (binary_op->type == OperatorType::kFloorDiv) {
outval = std::floor(val0 / val1);
} else if (binary_op->type == OperatorType::kFloorMod) {
outval = val0 - (std::floor(val0 / val1) * val1);
} else if (binary_op->type == OperatorType::kMinimum) {
outval = std::min(val0, val1);
} else if (binary_op->type == OperatorType::kMaximum) {
outval = std::max(val0, val1);
} else if (binary_op->type == OperatorType::kLess) {
outval = val0 < val1;
} else if (binary_op->type == OperatorType::kLessEqual) {
outval = val0 <= val1;
} else if (binary_op->type == OperatorType::kGreater) {
outval = val0 > val1;
} else if (binary_op->type == OperatorType::kGreaterEqual) {
outval = val0 >= val1;
} else {
LOG(FATAL) << "should not get here";
}
output_data[Offset(output_shape, output_indices)] = outval;
}
}
bool EvaluateBinaryOperatorOnConstantInputs(Model* model,
const Operator* binary_op) {
const auto inputs_data_type = model->GetArray(binary_op->inputs[0]).data_type;
const auto output_data_type =
model->GetArray(binary_op->outputs[0]).data_type;
#define TOCO_HANDLE_CASE(InputsDataType, OutputDataType) \
if (inputs_data_type == InputsDataType && \
output_data_type == OutputDataType) { \
EvaluateBinaryOperatorOnConstantInputs<InputsDataType, OutputDataType>( \
model, binary_op); \
return true; \
}
TOCO_HANDLE_CASE(ArrayDataType::kFloat, ArrayDataType::kFloat)
TOCO_HANDLE_CASE(ArrayDataType::kFloat, ArrayDataType::kBool)
TOCO_HANDLE_CASE(ArrayDataType::kInt32, ArrayDataType::kInt32)
TOCO_HANDLE_CASE(ArrayDataType::kInt32, ArrayDataType::kBool)
TOCO_HANDLE_CASE(ArrayDataType::kInt64, ArrayDataType::kInt64)
TOCO_HANDLE_CASE(ArrayDataType::kInt64, ArrayDataType::kBool)
return false;
#undef TOCO_HANDLE_CASE
}
} // namespace
absl::Status ResolveConstantBinaryOperator::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
const auto binary_it = model->operators.begin() + op_index;
const auto* binary_op = binary_it->get();
// Test for binary ops of types that we know how to resolve
if (binary_op->type != OperatorType::kAdd &&
binary_op->type != OperatorType::kMul &&
binary_op->type != OperatorType::kSub &&
binary_op->type != OperatorType::kDiv &&
binary_op->type != OperatorType::kFloorDiv &&
binary_op->type != OperatorType::kFloorMod &&
binary_op->type != OperatorType::kMinimum &&
binary_op->type != OperatorType::kMaximum &&
binary_op->type != OperatorType::kLess &&
binary_op->type != OperatorType::kLessEqual &&
binary_op->type != OperatorType::kGreater &&
binary_op->type != OperatorType::kGreaterEqual) {
return absl::OkStatus();
}
CHECK_EQ(binary_op->inputs.size(), 2);
const auto& input0_array = model->GetArray(binary_op->inputs[0]);
const auto& input1_array = model->GetArray(binary_op->inputs[1]);
// Check if both inputs are constant parameters.
if (!input0_array.buffer || !input1_array.buffer) {
return absl::OkStatus();
}
auto& output_array = model->GetArray(binary_op->outputs[0]);
// Yield until the output array dims have been resolved.
if (!output_array.has_shape()) {
return absl::OkStatus();
}
// At the moment we don't want to care about fused activation functions.
// The idea is that we should do the present constants-propagation before
// activation functions get fused.
if (binary_op->fused_activation_function !=
FusedActivationFunctionType::kNone) {
AddMessageF(
"Not resolving constant %s because it has a fused activation function",
LogName(*binary_op));
return absl::OkStatus();
}
// Check that input data types agree.
CHECK(input0_array.data_type == input1_array.data_type)
<< "Dissimilar data types given to op outputting \""
<< binary_op->outputs[0] << "\". 0:\"" << binary_op->inputs[0] << "\"("
<< static_cast<int>(input0_array.data_type) << ") 1:\""
<< binary_op->inputs[1] << "\"("
<< static_cast<int>(input1_array.data_type) << ").";
// Do the actual constants propagation
if (!EvaluateBinaryOperatorOnConstantInputs(model, binary_op)) {
return absl::OkStatus();
}
DeleteOpAndArrays(model, binary_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,217 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <algorithm>
#include <cstddef>
#include <limits>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "absl/strings/str_join.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
// Copies data from multiple source arrays to a destination array based on a
// concatenation dimension. From each array in input_arrays, it copies chunk
// sizes provided in array_copy_size vector (per array). It uses the buffer
// in concatenated_array as destination buffer.
template <ArrayDataType A, typename T>
void CopyTensorSegments(const std::vector<Array*>& input_arrays,
const std::vector<int>& array_copy_size,
const int num_elements_concatenated_array,
Array* concatenated_array) {
for (Array* input_array : input_arrays) {
if (!input_array->buffer) {
return;
}
}
auto& concatenated_array_buffer =
concatenated_array->GetMutableBuffer<A>().data;
concatenated_array_buffer.resize(num_elements_concatenated_array);
// It does not matter which array to use to find the value for the total
// number of copy steps.
CHECK(!input_arrays.empty());
CHECK_NE(array_copy_size[0], 0);
const int total_copy_steps =
input_arrays[0]->GetBuffer<A>().data.size() / array_copy_size[0];
// Initialize the source pointers to point to beginning of the array buffers.
std::vector<const T*> src_ptr;
src_ptr.reserve(input_arrays.size());
for (Array* input_array : input_arrays) {
src_ptr.push_back(input_array->GetBuffer<A>().data.data());
}
// Copy the data from input_arrays to concatenated_array_buffer.
T* dest_ptr = concatenated_array_buffer.data();
for (int s = 0; s < total_copy_steps; s++) {
for (size_t i = 0; i < input_arrays.size(); i++) {
std::copy(src_ptr[i], src_ptr[i] + array_copy_size[i], dest_ptr);
src_ptr[i] += array_copy_size[i];
dest_ptr += array_copy_size[i];
}
}
}
// Receives a series of input arrays of type Array and an integer showing the
// axis on which those arrays will be concatenated. It returns the concatenated
// array.
template <ArrayDataType A>
void ConcatenateTensorBuffers(const std::vector<Array*>& input_arrays,
int concatenation_axis,
Array* concatenated_array) {
int num_elements_concatenated_array = 1;
for (int i = 0; i < concatenated_array->shape().dimensions_count(); i++) {
num_elements_concatenated_array *= concatenated_array->shape().dims()[i];
}
// Prepare the data needed for segmented copy from multiple source arrays to
// a destination array based on a oncatenation dimension.
std::vector<int> array_copy_size(input_arrays.size());
int count = 0;
for (Array* input_array : input_arrays) {
const Shape array_shape = input_array->shape();
array_copy_size[count] = 1;
for (int i = concatenation_axis; i < array_shape.dimensions_count(); i++) {
array_copy_size[count] *= array_shape.dims()[i];
}
count++;
}
// Do the actual data copy.
CopyTensorSegments<A, DataType<A>>(input_arrays, array_copy_size,
num_elements_concatenated_array,
concatenated_array);
}
// Sets the minimum and maximum values for the concatenated array. If it's
// already set (e.g. because of previous pass in TOCO), it doesn't change it and
// returns. Otherwise it uses the input arrays min and max values to compute the
// concatenated array min and max.
void SetMinMaxForConcatenedArray(GraphTransformation* transformation,
const std::vector<Array*>& input_arrays,
Array* concatenated_array) {
CHECK(concatenated_array->data_type == ArrayDataType::kFloat);
// If the minmax is already set, use it
if (concatenated_array->minmax) return;
double concat_min = std::numeric_limits<double>::infinity();
double concat_max = -std::numeric_limits<double>::infinity();
for (Array* input_array : input_arrays) {
// If any of the input arrays minmax is not set, return.
// TODO(ghodrat): shall we add the logic to compute the minmax?
if (!input_array->minmax) return;
const MinMax& input_minmax = input_array->GetMinMax();
concat_min = std::min(concat_min, input_minmax.min);
concat_max = std::max(concat_max, input_minmax.max);
}
MinMax& minmax = concatenated_array->GetOrCreateMinMax();
minmax.min = concat_min;
minmax.max = concat_max;
transformation->AddMessageF("Setting concatenated array min/max to %g,%g",
concat_min, concat_max);
}
} // namespace
// Resolves the concatenation operator if all its inputs are constant arrays.
absl::Status ResolveConstantConcatenation::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
const auto concat_it = model->operators.begin() + op_index;
const auto* concat_base_op = concat_it->get();
if (concat_base_op->type != OperatorType::kConcatenation) {
return absl::OkStatus();
}
const auto* concat_op =
static_cast<const ConcatenationOperator*>(concat_base_op);
for (const std::string& input_name : concat_op->inputs) {
// We only expect constant unquantized arrays as input, otherwise we return.
// We also make sure the shapes of the input arrays are known and they are
// all discardable.
const Operator* input_op = GetOpWithOutput(*model, input_name);
if (input_op) return absl::OkStatus();
if (!IsConstantParameterArray(*model, input_name)) return absl::OkStatus();
if (!model->GetArray(input_name).has_shape()) return absl::OkStatus();
if (model->GetArray(input_name).quantization_params)
return absl::OkStatus();
if (!IsDiscardableArray(*model, input_name)) return absl::OkStatus();
}
const int concatenation_axis = concat_op->axis;
CHECK_EQ(concat_op->outputs.size(), 1);
std::string concatenated_array_name = concat_op->outputs[0];
Array& concatenated_array = model->GetOrCreateArray(concatenated_array_name);
std::vector<Array*> input_arrays;
input_arrays.reserve(concat_op->inputs.size());
for (const std::string& input_name : concat_op->inputs) {
input_arrays.push_back(&model->GetArray(input_name));
}
AddMessageF("Performing constant concat of %s into %s",
absl::StrJoin(concat_op->inputs, ", "), concatenated_array_name);
switch (concatenated_array.data_type) {
case ArrayDataType::kFloat:
ConcatenateTensorBuffers<ArrayDataType::kFloat>(
input_arrays, concatenation_axis, &concatenated_array);
SetMinMaxForConcatenedArray(this, input_arrays, &concatenated_array);
break;
case ArrayDataType::kUint8:
ConcatenateTensorBuffers<ArrayDataType::kUint8>(
input_arrays, concatenation_axis, &concatenated_array);
break;
case ArrayDataType::kInt32:
ConcatenateTensorBuffers<ArrayDataType::kInt32>(
input_arrays, concatenation_axis, &concatenated_array);
break;
case ArrayDataType::kInt64:
ConcatenateTensorBuffers<ArrayDataType::kInt64>(
input_arrays, concatenation_axis, &concatenated_array);
break;
case ArrayDataType::kString:
ConcatenateTensorBuffers<ArrayDataType::kString>(
input_arrays, concatenation_axis, &concatenated_array);
break;
case ArrayDataType::kComplex64:
ConcatenateTensorBuffers<ArrayDataType::kComplex64>(
input_arrays, concatenation_axis, &concatenated_array);
break;
default:
LOG(FATAL) << "ArrayDataType not supported";
}
DeleteOpAndArrays(model, concat_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,144 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <limits>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/kernels/internal/quantization_util.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/quantization_util.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
template <ArrayDataType A>
void GetBoundsForQuantizedDataType(float* min, float* max) {
using limits = std::numeric_limits<DataType<A>>;
*min = limits::min();
*max = limits::max();
}
void GetBoundsForQuantizedDataType(ArrayDataType quantized_data_type,
float* min, float* max) {
// It is important for matching accuracy between TF training and TFLite
// inference, that the min and max values are float to match TF's
// FakeQuantWithMinMaxVarsFunctor.
switch (quantized_data_type) {
case ArrayDataType::kUint8:
return GetBoundsForQuantizedDataType<ArrayDataType::kUint8>(min, max);
case ArrayDataType::kInt8:
return GetBoundsForQuantizedDataType<ArrayDataType::kInt8>(min, max);
case ArrayDataType::kUint16:
return GetBoundsForQuantizedDataType<ArrayDataType::kUint16>(min, max);
case ArrayDataType::kInt16:
return GetBoundsForQuantizedDataType<ArrayDataType::kInt16>(min, max);
case ArrayDataType::kUint32:
return GetBoundsForQuantizedDataType<ArrayDataType::kUint32>(min, max);
case ArrayDataType::kInt32:
return GetBoundsForQuantizedDataType<ArrayDataType::kInt32>(min, max);
case ArrayDataType::kUint64:
return GetBoundsForQuantizedDataType<ArrayDataType::kUint64>(min, max);
case ArrayDataType::kInt64:
return GetBoundsForQuantizedDataType<ArrayDataType::kInt64>(min, max);
default:
LOG(FATAL) << "unhandled quantized data type";
}
}
absl::Status ResolveConstantFakeQuant::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto fakequant_it = model->operators.begin() + op_index;
const auto* fakequant_base_op = fakequant_it->get();
if (fakequant_base_op->type != OperatorType::kFakeQuant) {
return absl::OkStatus();
}
const auto* fakequant_op =
static_cast<const FakeQuantOperator*>(fakequant_base_op);
// Yield until the fakequant MinMax has been resolved.
if (!fakequant_op->minmax) {
return absl::OkStatus();
}
// This transformation only applies when the input array is constant.
if (!IsConstantParameterArray(*model, fakequant_op->inputs[0])) {
return absl::OkStatus();
}
const auto& input_array = model->GetArray(fakequant_op->inputs[0]);
CHECK(input_array.data_type == ArrayDataType::kFloat);
// Determine the final data type in the same way as PropagateFakeQuantNumBits.
ArrayDataType quantized_data_type = input_array.final_data_type;
if (!InferQuantizedDataTypeFromFakeQuant(*fakequant_op,
&quantized_data_type)) {
AddMessageF("Unsupported FakeQuant num_bits=%d", fakequant_op->num_bits);
return absl::OkStatus();
}
AddMessageF("Resolving constant %s", LogName(*fakequant_op));
auto& output_array = model->GetArray(fakequant_op->outputs[0]);
CHECK(input_array.data_type == ArrayDataType::kFloat);
output_array.data_type = ArrayDataType::kFloat;
// We'll set the final data type to what the fake quant indicates we should
// have (and would have been set if this stayed around until
// PropagateFakeQuantNumBits).
if (propagate_fake_quant_num_bits()) {
output_array.final_data_type = quantized_data_type;
}
CHECK(!output_array.buffer);
const auto& input_buffer = input_array.GetBuffer<ArrayDataType::kFloat>();
output_array.GetOrCreateMinMax() = *fakequant_op->minmax;
auto& output_buffer = output_array.GetMutableBuffer<ArrayDataType::kFloat>();
const int size = input_buffer.data.size();
output_buffer.data.resize(size);
QuantizationParams qparams;
ChooseQuantizationParamsForArrayAndQuantizedDataType(
output_array, quantized_data_type, &qparams);
float quantized_min, quantized_max;
GetBoundsForQuantizedDataType(quantized_data_type, &quantized_min,
&quantized_max);
if (fakequant_op->narrow_range) {
quantized_min++;
output_array.narrow_range = true;
}
// It is important for matching accuracy between TF training and TFLite
// inference, that the following variables are float to match TF's
// FakeQuantWithMinMaxVarsFunctor.
const float scale = qparams.scale;
const float nudged_min = (quantized_min - qparams.zero_point) * scale;
const float nudged_max = (quantized_max - qparams.zero_point) * scale;
tflite::FakeQuantizeArray(scale, nudged_min, nudged_max,
input_buffer.data.data(), output_buffer.data.data(),
size);
DeleteOpAndArrays(model, fakequant_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,121 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
template <ArrayDataType Type>
bool ComputeFillArray(Model* model, FillOperator* op) {
const auto& val_array = model->GetArray(op->inputs[1]);
auto& output_array = model->GetArray(op->outputs[0]);
CHECK(val_array.data_type == Type);
CHECK(output_array.data_type == Type);
// Compute the array data
std::vector<DataType<Type>>& data =
output_array.GetMutableBuffer<Type>().data;
data.resize(RequiredBufferSizeForShape(output_array.shape()));
DataType<Type> fill_val = val_array.GetBuffer<Type>().data[0];
for (size_t i = 0; i < data.size(); i++) {
data[i] = fill_val;
}
return true;
}
absl::Status ResolveConstantFill::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto fill_it = model->operators.begin() + op_index;
auto* base_op = fill_it->get();
if (base_op->type != OperatorType::kFill) {
return absl::OkStatus();
}
auto* op = static_cast<FillOperator*>(base_op);
CHECK_EQ(op->inputs.size(), 2);
CHECK_EQ(op->outputs.size(), 1);
auto& output_array = model->GetArray(op->outputs[0]);
if (output_array.data_type == ArrayDataType::kNone) {
// Yield until the output type has been set by PropagateArrayDataTypes
return absl::OkStatus();
}
if (!output_array.has_shape()) {
// Yield until the output shape has been set by PropagateFixedShapes
return absl::OkStatus();
}
const auto& val_array = model->GetArray(op->inputs[1]);
if (!val_array.has_shape()) {
// Yield until the value shape has been resolved.
return absl::OkStatus();
}
if (!IsConstantParameterArray(*model, op->inputs[1])) {
// Yield until the value is constant.
return absl::OkStatus();
}
CHECK_EQ(RequiredBufferSizeForShape(val_array.shape()), 1);
switch (output_array.data_type) {
case ArrayDataType::kFloat:
if (!ComputeFillArray<ArrayDataType::kFloat>(model, op)) {
return absl::OkStatus();
}
break;
case ArrayDataType::kUint8:
if (!ComputeFillArray<ArrayDataType::kUint8>(model, op)) {
return absl::OkStatus();
}
break;
case ArrayDataType::kInt32:
if (!ComputeFillArray<ArrayDataType::kInt32>(model, op)) {
return absl::OkStatus();
}
break;
case ArrayDataType::kInt64:
if (!ComputeFillArray<ArrayDataType::kInt64>(model, op)) {
return absl::OkStatus();
}
break;
case ArrayDataType::kComplex64:
if (!ComputeFillArray<ArrayDataType::kComplex64>(model, op)) {
return absl::OkStatus();
}
break;
default:
LOG(FATAL) << "Unsupported data type given to Fill op with output \""
<< op->outputs[0] << "\"";
break;
}
DeleteOpAndArrays(model, op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,155 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/toco_types.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
// Gathers data from axis 0.
template <ArrayDataType Type>
inline void Gather(const Array& input_array, const Array& coords_array,
Array* output_array) {
const Shape& input_shape = input_array.shape();
const std::vector<DataType<Type>>& input_data =
input_array.GetBuffer<Type>().data;
const Shape& coords_shape = coords_array.shape();
const std::vector<int32>& coords_data =
coords_array.GetBuffer<ArrayDataType::kInt32>().data;
const Shape& output_shape = output_array->shape();
std::vector<DataType<Type>>& output_data =
output_array->GetMutableBuffer<Type>().data;
output_data.resize(RequiredBufferSizeForShape(output_shape));
CHECK_EQ(coords_shape.dims(0), output_array->shape().dims(0));
int stride = 1;
for (int i = 1; i < input_shape.dimensions_count(); ++i) {
stride *= input_shape.dims(i);
}
// Let's make sure we have enough space for all element in the memcpy()
// below, which writes 'stride' elements starting at 'i * stride'.
CHECK_EQ(stride * coords_shape.dims(0), output_data.size());
for (int i = 0; i < coords_shape.dims(0); ++i) {
DCHECK_GE(coords_data[i], 0);
DCHECK_LT(coords_data[i], input_shape.dims(0));
DataType<Type>* out = output_data.data() + i * stride;
const DataType<Type>* in = input_data.data() + coords_data[i] * stride;
memcpy(out, in, sizeof(DataType<Type>) * stride);
}
}
} // namespace
// Resolves a constant Gather operation.
// This simply performs the gather and produces the output array with the
// appropriate values.
absl::Status ResolveConstantGather::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto it = model->operators.begin() + op_index;
const auto* base_op = it->get();
if (base_op->type != OperatorType::kGather) {
return absl::OkStatus();
}
const auto* op = static_cast<const GatherOperator*>(base_op);
CHECK_GE(op->inputs.size(), 2);
CHECK_EQ(op->outputs.size(), 1);
auto& output_array = model->GetArray(op->outputs[0]);
if (output_array.data_type == ArrayDataType::kNone) {
// Yield until the output type has been set by PropagateArrayDataTypes.
return absl::OkStatus();
}
if (!output_array.has_shape()) {
// Yield until the output shape has been set by PropagateFixedShapes.
return absl::OkStatus();
}
if (!op->axis) {
// Yield until axis has been set by ResolveGatherAttributes.
return absl::OkStatus();
}
if (op->axis.value() != 0) {
// Only handling axis=0 for now.
AddMessageF("%s has axis %d; only axis=0 is supported", LogName(*op),
op->axis.value());
return absl::OkStatus();
}
// We require constant inputs.
if (!IsConstantParameterArray(*model, op->inputs[0]) ||
!IsConstantParameterArray(*model, op->inputs[1])) {
return absl::OkStatus();
}
const Array& input_array = model->GetArray(op->inputs[0]);
const Array& coords_array = model->GetArray(op->inputs[1]);
CHECK(coords_array.data_type == ArrayDataType::kInt32)
<< "Only int32 indices are supported";
// Copy min/max info if present. The ranges of the selected values may be
// a subset of the original range but we want to ensure the quantization
// params stay the same.
if (input_array.minmax) {
const auto& input_minmax = input_array.GetMinMax();
auto& output_minmax = output_array.GetOrCreateMinMax();
output_minmax.min = input_minmax.min;
output_minmax.max = input_minmax.max;
}
CHECK(!output_array.buffer);
switch (output_array.data_type) {
case ArrayDataType::kFloat:
Gather<ArrayDataType::kFloat>(input_array, coords_array, &output_array);
break;
case ArrayDataType::kUint8:
Gather<ArrayDataType::kUint8>(input_array, coords_array, &output_array);
break;
case ArrayDataType::kInt32:
Gather<ArrayDataType::kInt32>(input_array, coords_array, &output_array);
break;
case ArrayDataType::kInt64:
Gather<ArrayDataType::kInt64>(input_array, coords_array, &output_array);
break;
case ArrayDataType::kComplex64:
Gather<ArrayDataType::kComplex64>(input_array, coords_array,
&output_array);
break;
default:
LOG(FATAL) << "Unsupported data type given to Gather op with output \""
<< op->outputs[0] << "\"";
break;
}
DeleteOpAndArrays(model, op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,126 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <cstring>
#include <vector>
#include "absl/log/absl_check.h"
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
template <ArrayDataType Type>
void Pack(Model* model, const PackOperator& op) {
auto& output_array = model->GetArray(op.outputs[0]);
ABSL_CHECK(output_array.data_type == Type);
// Create a buffer for the output array
std::vector<DataType<Type>>& output_data =
output_array.GetMutableBuffer<Type>().data;
output_data.resize(RequiredBufferSizeForShape(output_array.shape()));
// Pack inputs into buffer
size_t dst_offset = 0;
for (const auto& input : op.inputs) {
// Append array data to output for each input array
const auto& input_array = model->GetArray(input);
size_t input_size = RequiredBufferSizeForShape(input_array.shape());
ABSL_CHECK_GE(input_array.GetBuffer<Type>().data.size(), input_size);
ABSL_CHECK_LE(dst_offset + input_size, output_data.size());
memcpy(output_data.data() + dst_offset,
input_array.GetBuffer<Type>().data.data(),
input_size * ElementSize(Type));
dst_offset += input_size;
}
ABSL_CHECK_EQ(dst_offset, output_data.size());
}
} // namespace
absl::Status ResolveConstantPack::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto it = model->operators.begin() + op_index;
const auto* base_op = it->get();
if (base_op->type != OperatorType::kPack) {
return absl::OkStatus();
}
const auto* op = static_cast<const PackOperator*>(base_op);
ABSL_CHECK_GE(op->inputs.size(), 1);
ABSL_CHECK_EQ(op->outputs.size(), 1);
auto& output_array = model->GetArray(op->outputs[0]);
if (output_array.data_type == ArrayDataType::kNone) {
// Yield until the output type has been set by PropagateArrayDataTypes
return absl::OkStatus();
}
if (!output_array.has_shape()) {
// Yield until the output shape has been set by PropagateFixedShapes
return absl::OkStatus();
}
for (const auto& input : op->inputs) {
if (!IsConstantParameterArray(*model, input) ||
!model->GetArray(input).has_shape()) {
// Yield if any input is mutable or lacks a shape
return absl::OkStatus();
}
}
int axis = op->axis;
if (axis < 0) {
// Handle negative axis
axis += model->GetArray(op->inputs[0]).shape().dims().size() + 1;
}
ABSL_CHECK_EQ(axis, 0) << "Packing only supported along 0th axis";
ABSL_CHECK(!output_array.buffer);
switch (output_array.data_type) {
case ArrayDataType::kFloat:
Pack<ArrayDataType::kFloat>(model, *op);
break;
case ArrayDataType::kUint8:
Pack<ArrayDataType::kUint8>(model, *op);
break;
case ArrayDataType::kInt32:
Pack<ArrayDataType::kInt32>(model, *op);
break;
case ArrayDataType::kInt64:
Pack<ArrayDataType::kInt64>(model, *op);
break;
case ArrayDataType::kComplex64:
Pack<ArrayDataType::kComplex64>(model, *op);
break;
default:
LOG(FATAL) << "Unsupported data type given to Pack op with output \""
<< op->outputs[0] << "\"";
break;
}
DeleteOpAndArrays(model, op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,119 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <algorithm>
#include <cstddef>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/lib/random/philox_random.h"
#include "tensorflow/core/lib/random/random_distributions.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
template <ArrayDataType Type>
bool ComputeRandomUniformArray(Model* model, RandomUniformOperator* op) {
typedef tensorflow::random::UniformDistribution<
tensorflow::random::PhiloxRandom, DataType<Type>>
Distribution;
// Allocate output
auto& output_array = model->GetArray(op->outputs[0]);
CHECK(output_array.data_type == Type);
std::vector<DataType<Type>>& data =
output_array.GetMutableBuffer<Type>().data;
data.resize(RequiredBufferSizeForShape(output_array.shape()));
// We use the same random number generator and distribution as TensorFlow to
// produce the exact same values given the same seeds. See
// tensorflow::functor::FillPhiloxRandomTask<Distribution, false> in
// //tensorflow/core/kernels/random_op.cc for the implementation.
tensorflow::random::PhiloxRandom generator(op->seed, op->seed2);
Distribution dist;
// The generator creates Distribution::kResultElementCount samples at a time.
size_t offset = 0;
size_t num_samples = Distribution::kResultElementCount;
while (offset < data.size()) {
const typename Distribution::ResultType samples = dist(&generator);
std::copy(&samples[0],
&samples[0] + std::min(num_samples, data.size() - offset),
&data[0] + offset);
offset += num_samples;
}
return true;
}
absl::Status ResolveConstantRandomUniform::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
const auto it = model->operators.begin() + op_index;
auto* base_op = it->get();
if (base_op->type != OperatorType::kRandomUniform) {
return absl::OkStatus();
}
auto* op = static_cast<RandomUniformOperator*>(base_op);
CHECK_EQ(op->inputs.size(), 1);
CHECK_EQ(op->outputs.size(), 1);
auto& output_array = model->GetArray(op->outputs[0]);
if (output_array.data_type == ArrayDataType::kNone) {
// Yield until the output type has been set by PropagateArrayDataTypes
return absl::OkStatus();
}
if (!output_array.has_shape()) {
// Yield until the output shape has been set by PropagateFixedShapes
return absl::OkStatus();
}
if ((op->seed == 0) && (op->seed2 == 0)) {
LOG(WARNING) << "RandomUniform op outputting \"" << op->outputs[0]
<< "\" is truly random (using /dev/random system entropy). "
"Therefore, cannot resolve as constant. Set \"seed\" or "
"\"seed2\" attr non-zero to fix this";
return absl::OkStatus();
}
switch (output_array.data_type) {
case ArrayDataType::kFloat:
if (!ComputeRandomUniformArray<ArrayDataType::kFloat>(model, op)) {
return absl::OkStatus();
}
break;
// For future support of double or half.
// case ArrayDataType::kDouble...
default:
LOG(FATAL)
<< "Unsupported data type given to RandomUniform op with output \""
<< op->outputs[0] << "\"";
break;
}
DeleteOpAndArrays(model, op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,119 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <type_traits>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
template <ArrayDataType A, typename T>
void FillRangeOutput(const Array& start_array, const Array& limit_array,
const Array& delta_array, Array* output_array) {
// Compute buffer contents
T start = start_array.GetBuffer<A>().data[0];
T limit = limit_array.GetBuffer<A>().data[0];
T delta = delta_array.GetBuffer<A>().data[0];
auto& buffer = output_array->GetMutableBuffer<A>();
buffer.data.clear();
int size =
(std::is_integral<T>::value
? ((std::abs(limit - start) + std::abs(delta) - 1) / std::abs(delta))
: std::ceil(std::abs((limit - start) / delta)));
for (int i = 0; i < size; ++i) {
buffer.data.push_back(start + i * delta);
}
CHECK_EQ(std::floor((limit - start) / delta), buffer.data.size());
CHECK_EQ(buffer.data.size(), output_array->shape().dims()[0]);
}
absl::Status ResolveConstantRange::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto it = model->operators.begin() + op_index;
auto* base_op = it->get();
if (base_op->type != OperatorType::kRange) {
return absl::OkStatus();
}
auto* op = static_cast<RangeOperator*>(base_op);
CHECK_EQ(op->inputs.size(), 3);
const auto& start_array = model->GetArray(op->inputs[0]);
if (!start_array.has_shape()) {
// Yield until all input dims have been resolved.
return absl::OkStatus();
}
const auto& limit_array = model->GetArray(op->inputs[1]);
if (!limit_array.has_shape()) {
// Yield until all input dims have been resolved.
return absl::OkStatus();
}
const auto& delta_array = model->GetArray(op->inputs[2]);
if (!delta_array.has_shape()) {
// Yield until all input dims have been resolved.
return absl::OkStatus();
}
for (const auto& input : op->inputs) {
if (!IsConstantParameterArray(*model, input)) {
// yield if any input is mutable
return absl::OkStatus();
}
}
CHECK_EQ(op->outputs.size(), 1);
auto& output_array = model->GetArray(op->outputs[0]);
if (output_array.data_type == ArrayDataType::kNone) {
// Yield until the output type has been set by PropagateArrayDataTypes
return absl::OkStatus();
}
CHECK_EQ(RequiredBufferSizeForShape(start_array.shape()), 1)
<< "Range op inputs must be scalar.";
CHECK_EQ(RequiredBufferSizeForShape(limit_array.shape()), 1)
<< "Range op inputs must be scalar.";
CHECK_EQ(RequiredBufferSizeForShape(delta_array.shape()), 1)
<< "Range op inputs must be scalar.";
CHECK(start_array.data_type == ArrayDataType::kInt32 ||
start_array.data_type == ArrayDataType::kFloat)
<< "Range op inputs must be int32 or float.";
CHECK(limit_array.data_type == start_array.data_type)
<< "Range op inputs type must be equal.";
CHECK(delta_array.data_type == start_array.data_type)
<< "Range op inputs type must be equal.";
if (start_array.data_type == ArrayDataType::kInt32) {
FillRangeOutput<ArrayDataType::kInt32, int32_t>(start_array, limit_array,
delta_array, &output_array);
} else {
FillRangeOutput<ArrayDataType::kFloat, float>(start_array, limit_array,
delta_array, &output_array);
}
DeleteOpAndArrays(model, op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,120 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
// Resolves a constant reshape operation by copying the buffer.
absl::Status ResolveConstantReshape::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto it = model->operators.begin() + op_index;
const auto* base_op = it->get();
if (base_op->type != OperatorType::kReshape) {
return absl::OkStatus();
}
const auto* op = static_cast<const TensorFlowReshapeOperator*>(base_op);
CHECK_EQ(op->inputs.size(), 2);
CHECK_EQ(op->outputs.size(), 1);
// We require constant inputs.
if (!IsConstantParameterArray(*model, op->inputs[0]) ||
!IsConstantParameterArray(*model, op->inputs[1])) {
return absl::OkStatus();
}
auto& output_array = model->GetArray(op->outputs[0]);
if (output_array.data_type == ArrayDataType::kNone) {
// Yield until the output type has been set by PropagateArrayDataTypes.
return absl::OkStatus();
}
if (!output_array.has_shape()) {
// Yield until the output shape has been set by PropagateFixedShapes.
return absl::OkStatus();
}
const Array& input_array = model->GetArray(op->inputs[0]);
if (!ShapesAgreeUpToExtending(input_array.shape(), output_array.shape())) {
AddMessageF("Constant reshape is non-trivial (%s -> %s)",
ShapeToString(input_array.shape()),
ShapeToString(output_array.shape()));
return absl::OkStatus();
}
CHECK(!output_array.buffer);
switch (input_array.data_type) {
case ArrayDataType::kBool:
CopyArrayBuffer<ArrayDataType::kBool>(input_array, &output_array);
break;
case ArrayDataType::kFloat:
CopyArrayBuffer<ArrayDataType::kFloat>(input_array, &output_array);
break;
case ArrayDataType::kInt8:
CopyArrayBuffer<ArrayDataType::kInt8>(input_array, &output_array);
break;
case ArrayDataType::kUint8:
CopyArrayBuffer<ArrayDataType::kUint8>(input_array, &output_array);
break;
case ArrayDataType::kInt16:
CopyArrayBuffer<ArrayDataType::kInt16>(input_array, &output_array);
break;
case ArrayDataType::kUint16:
CopyArrayBuffer<ArrayDataType::kUint16>(input_array, &output_array);
break;
case ArrayDataType::kInt32:
CopyArrayBuffer<ArrayDataType::kInt32>(input_array, &output_array);
break;
case ArrayDataType::kUint32:
CopyArrayBuffer<ArrayDataType::kUint32>(input_array, &output_array);
break;
case ArrayDataType::kInt64:
CopyArrayBuffer<ArrayDataType::kInt64>(input_array, &output_array);
break;
case ArrayDataType::kUint64:
CopyArrayBuffer<ArrayDataType::kUint64>(input_array, &output_array);
break;
case ArrayDataType::kString:
CopyArrayBuffer<ArrayDataType::kString>(input_array, &output_array);
break;
case ArrayDataType::kComplex64:
CopyArrayBuffer<ArrayDataType::kComplex64>(input_array, &output_array);
break;
default:
LOG(FATAL) << "Unsupported data type: "
<< ArrayDataTypeName(input_array.data_type);
return absl::OkStatus();
}
AddMessageF("Resolving constant reshape of %s", LogName(*op));
CopyMinMaxAndQuantizationRelatedFields(input_array, &output_array);
DeleteOpAndArrays(model, op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,86 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/remove_trivial_passthrough.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
// Resolves a constant Select operation.
//
// This implementation is looking strictly for all-or-nothing on the select
// condition. It's possible to enhance this by looking per-element and possibly
// producing a Mul op.
absl::Status ResolveConstantSelect::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto it = model->operators.begin() + op_index;
const auto* base_op = it->get();
if (base_op->type != OperatorType::kSelect) {
return absl::OkStatus();
}
const auto* op = static_cast<const SelectOperator*>(base_op);
CHECK_GE(op->inputs.size(), 3);
CHECK_EQ(op->outputs.size(), 1);
auto& output_array = model->GetArray(op->outputs[0]);
if (output_array.data_type == ArrayDataType::kNone) {
// Yield until the output type has been set by PropagateArrayDataTypes.
return absl::OkStatus();
}
if (!output_array.has_shape()) {
// Yield until the output shape has been set by PropagateFixedShapes.
return absl::OkStatus();
}
// We require the cond input to be constant.
if (!IsConstantParameterArray(*model, op->inputs[0])) {
return absl::OkStatus();
}
const Array& cond_array = model->GetArray(op->inputs[0]);
CHECK(cond_array.data_type == ArrayDataType::kBool)
<< "Only bool conditions are supported";
const auto& cond_data = cond_array.GetBuffer<ArrayDataType::kBool>().data;
if (cond_data.empty()) {
return absl::OkStatus();
}
// Check if the condition is the same for all elements.
bool cond_value = cond_data[0];
for (size_t i = 1; i < cond_data.size(); ++i) {
if (cond_data[i] != cond_value) {
AddMessageF(
"Cannot resolve %s as constant; cond_array has differing "
"per-element values",
LogName(*op));
return absl::OkStatus();
}
}
// Pass-through the selected input.
*modified =
RemoveTrivialPassthroughOp(this, model, op_index, cond_value ? 1 : 2);
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,73 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status ResolveConstantShapeOrRank::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto it = model->operators.begin() + op_index;
const auto* op = it->get();
if (!(op->type == OperatorType::kShape || op->type == OperatorType::kRank)) {
return absl::OkStatus();
}
CHECK_EQ(op->outputs.size(), 1);
auto& output_array = model->GetArray(op->outputs[0]);
if (output_array.data_type == ArrayDataType::kNone) {
// Yield until the output type has been resolved
return absl::OkStatus();
}
const auto& input_array = model->GetArray(op->inputs[0]);
if (!input_array.has_shape()) {
// Yield until the input array's shape has been resolved.
return absl::OkStatus();
}
if (!output_array.has_shape()) {
// Yield until the output shape has been resolved.
return absl::OkStatus();
}
// Compute the output
CHECK(!output_array.buffer);
auto& output_buffer = output_array.GetMutableBuffer<ArrayDataType::kInt32>();
if (op->type == OperatorType::kShape) {
// Copy the input shape into the output buffer.
output_buffer.data = input_array.shape().dims();
} else if (op->type == OperatorType::kRank) {
// Copy the dimension count into the output buffer.
output_buffer.data.resize(1);
output_buffer.data[0] = input_array.shape().dimensions_count();
}
output_array.mutable_shape()->ReplaceDims(
{static_cast<int>(output_buffer.data.size())});
DeleteOpAndArrays(model, op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,170 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
template <ArrayDataType Type>
bool Slice(SliceOperator const& op, Array const& input_array,
Array* output_array) {
// Implementation is taken from the tflite kernel.
CHECK(input_array.data_type == Type);
CHECK(output_array->data_type == Type);
const auto& input_data = input_array.GetBuffer<Type>().data;
// Create a buffer for the output array.
std::vector<DataType<Type>>& output_data =
output_array->GetMutableBuffer<Type>().data;
output_data.resize(RequiredBufferSizeForShape(output_array->shape()));
std::vector<int> size = op.size;
if (size.size() != op.begin.size()) {
// Broadcast the end positions.
CHECK_EQ(op.size.size(), 1);
int broadcast_size = size[0];
while (size.size() < op.begin.size()) size.push_back(broadcast_size);
}
// Calculate begin and end indices along each dimension.
CHECK_LE(op.begin.size(), 4);
CHECK_LE(size.size(), 4);
std::vector<int> begin = op.begin;
std::vector<int> end;
for (size_t i = 0; i < begin.size(); ++i) {
int dim_size = size[i];
if (dim_size == -1) {
// -1 means the rest of the dimension.
dim_size = input_array.shape().dims()[i] - begin[i];
}
CHECK_GE(dim_size, 1);
end.push_back(begin[i] + dim_size - 1);
}
// Pad out so that we always have 4 dims, makes this loop easier.
while (begin.size() < 4) begin.insert(begin.begin(), 0);
while (end.size() < 4) end.insert(end.begin(), 0);
Shape padded_shape = input_array.shape();
while (padded_shape.dimensions_count() < 4) {
padded_shape.mutable_dims()->insert(padded_shape.mutable_dims()->begin(),
1);
}
auto* out_ptr = output_data.data();
for (int in_b = begin[0]; in_b <= end[0]; ++in_b) {
for (int in_h = begin[1]; in_h <= end[1]; ++in_h) {
for (int in_w = begin[2]; in_w <= end[2]; ++in_w) {
for (int in_d = begin[3]; in_d <= end[3]; ++in_d) {
*out_ptr++ =
input_data[Offset(padded_shape, {in_b, in_h, in_w, in_d})];
}
}
}
}
return true;
}
} // namespace
absl::Status ResolveConstantSlice::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto it = model->operators.begin() + op_index;
const auto* base_op = it->get();
if (base_op->type != OperatorType::kSlice) {
return absl::OkStatus();
}
const SliceOperator* op = static_cast<const SliceOperator*>(base_op);
CHECK_EQ(op->outputs.size(), 1);
auto& output_array = model->GetArray(op->outputs[0]);
if (output_array.data_type == ArrayDataType::kNone) {
// Yield until the output type has been set by PropagateArrayDataTypes.
return absl::OkStatus();
}
if (!output_array.has_shape()) {
// Yield until the output shape has been set by PropagateFixedShapes.
return absl::OkStatus();
}
if (op->begin.empty() || op->size.empty()) {
// Attributes have not resolved yet.
return absl::OkStatus();
}
const auto& input_array = model->GetArray(op->inputs[0]);
if (!input_array.has_shape()) {
// Yield until the value shape has been resolved.
return absl::OkStatus();
}
if (!IsConstantParameterArray(*model, op->inputs[0])) {
// Yield until the value is constant.
return absl::OkStatus();
}
CHECK(!output_array.buffer);
switch (output_array.data_type) {
case ArrayDataType::kFloat:
if (!Slice<ArrayDataType::kFloat>(*op, input_array, &output_array)) {
return absl::OkStatus();
}
break;
case ArrayDataType::kUint8:
if (!Slice<ArrayDataType::kUint8>(*op, input_array, &output_array)) {
return absl::OkStatus();
}
break;
case ArrayDataType::kInt32:
if (!Slice<ArrayDataType::kInt32>(*op, input_array, &output_array)) {
return absl::OkStatus();
}
break;
case ArrayDataType::kInt64:
if (!Slice<ArrayDataType::kInt64>(*op, input_array, &output_array)) {
return absl::OkStatus();
}
break;
case ArrayDataType::kComplex64:
if (!Slice<ArrayDataType::kComplex64>(*op, input_array, &output_array)) {
return absl::OkStatus();
}
break;
default:
LOG(FATAL) << "Unsupported data type input to Slice op with output \""
<< op->outputs[0] << "\"";
break;
}
DeleteOpAndArrays(model, op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,176 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/kernels/internal/strided_slice_logic.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
template <ArrayDataType Type>
void StridedSlice(StridedSliceOperator const& op, Array const& input_array,
Array* output_array) {
// The TensorFlow documentation for StridedSlice is a bit ambiguous in places
// (https://www.tensorflow.org/api_docs/cc/class/tensorflow/ops/strided-slice).
// Use the source code at tensorflow/core/util/strided_op.cc as
// "master documentation".
CHECK(input_array.data_type == Type);
CHECK(output_array->data_type == Type);
CHECK_EQ(op.ellipsis_mask, 0);
CHECK_EQ(op.new_axis_mask, 0);
int num_input_axes = op.start_indices.size();
CHECK_EQ(num_input_axes, op.start_indices.size());
CHECK_EQ(num_input_axes, op.stop_indices.size());
CHECK_EQ(num_input_axes, op.strides.size());
// Create a buffer for the output array
std::vector<DataType<Type>>& output_data =
output_array->GetMutableBuffer<Type>().data;
output_data.resize(RequiredBufferSizeForShape(output_array->shape()));
// Initialize source coordinate
Shape const& input_shape = input_array.shape();
Buffer<Type> const& input_buffer = input_array.GetBuffer<Type>();
std::vector<int> src_coord(num_input_axes);
std::vector<int> stop_for_axis(num_input_axes);
const auto strided_slice_params =
tflite::strided_slice::BuildStridedSliceParams(
op.begin_mask, op.end_mask, op.shrink_axis_mask, op.start_indices,
op.stop_indices, op.strides);
for (int axis = 0; axis < num_input_axes; axis++) {
int start_index = tflite::strided_slice::StartForAxis(
strided_slice_params, ToRuntimeShape(input_array.shape()), axis);
src_coord[axis] = start_index;
stop_for_axis[axis] = tflite::strided_slice::StopForAxis(
strided_slice_params, ToRuntimeShape(input_array.shape()), axis,
start_index);
}
// In order to handle any number (N) of dimensions, we copy elements one by
// one and treat the source coordinate as an N digit number (src_coord here).
// Each "digit" is incremented individually (by the stride). When it overflows
// (becomes greater than the stop), that digit is reset and a carry flag is
// used to increment the next digit.
for (size_t dst_offset = 0; dst_offset < output_data.size(); ++dst_offset) {
// Copy element.
output_data[dst_offset] = input_buffer.data[Offset(input_shape, src_coord)];
// Note we consider elements in the highest dimension are stored
// contiguously. So, we increment the stride starting from the highest
// dimension.
for (int axis = num_input_axes - 1; axis >= 0; --axis) {
int stride = op.strides[axis];
src_coord[axis] += stride;
// Check if we've overflowed. If not, we just break from the loop to
// continue w/ the element copy. Otherwise, reset the starting coordinate
// for this axis and move to the next lower axis.
int stop = stop_for_axis[axis];
if (!tflite::strided_slice::LoopCondition(src_coord[axis], stop,
stride)) {
break;
}
src_coord[axis] = tflite::strided_slice::StartForAxis(
strided_slice_params, ToRuntimeShape(input_shape), axis);
}
}
}
} // anonymous namespace
absl::Status ResolveConstantStridedSlice::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
const auto it = model->operators.begin() + op_index;
const auto* base_op = it->get();
if (base_op->type != OperatorType::kStridedSlice) {
return absl::OkStatus();
}
const StridedSliceOperator* op =
static_cast<const StridedSliceOperator*>(base_op);
CHECK_EQ(op->outputs.size(), 1);
auto& output_array = model->GetArray(op->outputs[0]);
if (output_array.data_type == ArrayDataType::kNone) {
// Yield until the output type has been set by PropagateArrayDataTypes
return absl::OkStatus();
}
if (!output_array.has_shape()) {
// Yield until the output shape has been set by PropagateFixedShapes
return absl::OkStatus();
}
if (op->start_indices.empty() || op->stop_indices.empty() ||
op->strides.empty()) {
// Attributes have not resolved yet.
return absl::OkStatus();
}
const auto& input_array = model->GetArray(op->inputs[0]);
if (!input_array.has_shape()) {
// Yield until the value shape has been resolved.
return absl::OkStatus();
}
if (!IsConstantParameterArray(*model, op->inputs[0])) {
// Yield until the value is constant.
return absl::OkStatus();
}
CHECK(!output_array.buffer);
switch (output_array.data_type) {
case ArrayDataType::kFloat:
StridedSlice<ArrayDataType::kFloat>(*op, input_array, &output_array);
break;
case ArrayDataType::kUint8:
StridedSlice<ArrayDataType::kUint8>(*op, input_array, &output_array);
break;
case ArrayDataType::kInt32:
StridedSlice<ArrayDataType::kInt32>(*op, input_array, &output_array);
break;
case ArrayDataType::kInt64:
StridedSlice<ArrayDataType::kInt64>(*op, input_array, &output_array);
break;
case ArrayDataType::kComplex64:
StridedSlice<ArrayDataType::kComplex64>(*op, input_array, &output_array);
break;
default:
LOG(FATAL)
<< "Unsupported data type input to StridedSlice op with output \""
<< op->outputs[0] << "\"";
break;
}
DeleteOpAndArrays(model, op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,176 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <cstdint>
#include <tuple>
#include <utility>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
// NOTE: the Tile implementation here is taken from tflite's Tile kernel.
template <typename T>
void CopyMultipleTimes(const T* in_data, int32_t in_size, int32_t multiplier,
T* out_data) {
for (int i = 0; i < multiplier; ++i) {
const T* in_end = in_data + in_size;
T* new_out_data = std::copy(in_data, in_end, out_data);
in_data = out_data;
out_data = new_out_data;
}
}
template <typename T, typename M>
std::pair<int, int> TileOneDimension(const Shape& in_dimensions,
const T* in_data, const M* multipliers,
T* out_data, int dimension) {
const int dimension_size = in_dimensions.dims(dimension);
if (dimension == in_dimensions.dimensions_count() - 1) {
CopyMultipleTimes(in_data, dimension_size, multipliers[dimension],
out_data);
return std::make_pair(
dimension_size,
dimension_size * static_cast<int>(multipliers[dimension]));
}
int total_stride_size = 0, total_tiled_stride_size = 0;
const T* copy_from_data = in_data;
T* copy_to_data = out_data;
for (int i = 0; i < dimension_size; ++i) {
int stride_size = 0, tiled_stride_size = 0;
std::tie(stride_size, tiled_stride_size) =
TileOneDimension(in_dimensions, copy_from_data, multipliers,
copy_to_data, dimension + 1);
copy_from_data += stride_size;
copy_to_data += tiled_stride_size;
total_stride_size += stride_size;
total_tiled_stride_size += tiled_stride_size;
}
CopyMultipleTimes(out_data, total_tiled_stride_size,
multipliers[dimension] - 1,
out_data + total_tiled_stride_size);
return std::make_pair(
total_stride_size,
static_cast<int>(total_tiled_stride_size * multipliers[dimension]));
}
template <ArrayDataType Type>
inline void Tile(const Array& input_array, const Array& multiples_array,
Array* output_array) {
// Allocate output storage.
auto& output_data = output_array->GetMutableBuffer<Type>().data;
output_data.resize(RequiredBufferSizeForShape(output_array->shape()));
switch (multiples_array.data_type) {
case ArrayDataType::kInt32:
TileOneDimension(
input_array.shape(), input_array.GetBuffer<Type>().data.data(),
multiples_array.GetBuffer<ArrayDataType::kInt32>().data.data(),
output_array->GetMutableBuffer<Type>().data.data(), 0);
break;
case ArrayDataType::kInt64:
TileOneDimension(
input_array.shape(), input_array.GetBuffer<Type>().data.data(),
multiples_array.GetBuffer<ArrayDataType::kInt64>().data.data(),
output_array->GetMutableBuffer<Type>().data.data(), 0);
break;
default:
CHECK(false);
break;
}
}
} // namespace
// Resolves a constant Tile operation.
absl::Status ResolveConstantTile::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto it = model->operators.begin() + op_index;
const auto* base_op = it->get();
if (base_op->type != OperatorType::kTile) {
return absl::OkStatus();
}
const auto* op = static_cast<const TensorFlowTileOperator*>(base_op);
CHECK_GE(op->inputs.size(), 2);
CHECK_EQ(op->outputs.size(), 1);
auto& output_array = model->GetArray(op->outputs[0]);
if (output_array.data_type == ArrayDataType::kNone) {
// Yield until the output type has been set by PropagateArrayDataTypes.
return absl::OkStatus();
}
if (!output_array.has_shape()) {
// Yield until the output shape has been set by PropagateFixedShapes.
return absl::OkStatus();
}
// We require constant inputs.
if (!IsConstantParameterArray(*model, op->inputs[0]) ||
!IsConstantParameterArray(*model, op->inputs[1])) {
return absl::OkStatus();
}
const Array& input_array = model->GetArray(op->inputs[0]);
const Array& multiples_array = model->GetArray(op->inputs[1]);
CHECK(multiples_array.data_type == ArrayDataType::kInt32 ||
multiples_array.data_type == ArrayDataType::kInt64)
<< "Only int32/int64 indices are supported";
CopyMinMaxAndQuantizationRelatedFields(input_array, &output_array);
CHECK(!output_array.buffer);
switch (output_array.data_type) {
case ArrayDataType::kFloat:
Tile<ArrayDataType::kFloat>(input_array, multiples_array, &output_array);
break;
case ArrayDataType::kUint8:
Tile<ArrayDataType::kUint8>(input_array, multiples_array, &output_array);
break;
case ArrayDataType::kInt16:
Tile<ArrayDataType::kInt16>(input_array, multiples_array, &output_array);
break;
case ArrayDataType::kInt32:
Tile<ArrayDataType::kInt32>(input_array, multiples_array, &output_array);
break;
case ArrayDataType::kInt64:
Tile<ArrayDataType::kInt64>(input_array, multiples_array, &output_array);
break;
case ArrayDataType::kComplex64:
Tile<ArrayDataType::kComplex64>(input_array, multiples_array,
&output_array);
break;
default:
LOG(FATAL) << "Unsupported data type given to Tile op with output \""
<< op->outputs[0] << "\"";
break;
}
DeleteOpAndArrays(model, op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,183 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
// Transposes an array up to rank 4.
// This is ShuffleArrayTemplate with non-enum permutation.
template <ArrayDataType Type>
void Transpose(Model* model, const Array& input_array,
const std::vector<int>& perm, Array* output_array) {
const Shape& input_shape = input_array.shape();
const std::vector<DataType<Type>>& input_data =
input_array.GetBuffer<Type>().data;
const Shape& output_shape = output_array->shape();
std::vector<DataType<Type>>& output_data =
output_array->GetMutableBuffer<Type>().data;
output_data.resize(RequiredBufferSizeForShape(output_shape));
CHECK(input_shape.dimensions_count() == output_shape.dimensions_count());
const int dim = input_shape.dimensions_count();
CHECK_LE(dim, 4);
CHECK(static_cast<int>(perm.size()) >= dim);
for (int i = 0; i < dim; i++) {
CHECK(perm[i] >= 0 && perm[i] < dim);
CHECK(input_shape.dims(perm[i]) == output_shape.dims(i));
}
Shape extended_input_shape = input_shape;
ExtendShape(&extended_input_shape, 4);
Shape extended_output_shape = output_shape;
ExtendShape(&extended_output_shape, 4);
std::vector<int> extended_perm;
ExtendShuffle(perm, 4, &extended_perm);
const std::vector<int>& extended_input_dims = extended_input_shape.dims();
const std::vector<int>& extended_output_dims = extended_output_shape.dims();
// TODO(starka): Rework to handle different numbers of dimensions.
int input_strides[4];
input_strides[3] = 1;
input_strides[2] = extended_input_dims[3];
input_strides[1] = input_strides[2] * extended_input_dims[2];
input_strides[0] = input_strides[1] * extended_input_dims[1];
const int input_stride_0 = input_strides[extended_perm[3]];
const int input_stride_1 = input_strides[extended_perm[2]];
const int input_stride_2 = input_strides[extended_perm[1]];
const int input_stride_3 = input_strides[extended_perm[0]];
const int output_size_0 = extended_output_dims[3];
const int output_size_1 = extended_output_dims[2];
const int output_size_2 = extended_output_dims[1];
const int output_size_3 = extended_output_dims[0];
const int output_stride_0 = 1;
const int output_stride_1 = output_size_0;
const int output_stride_2 = output_stride_1 * output_size_1;
const int output_stride_3 = output_stride_2 * output_size_2;
for (int i3 = 0; i3 < output_size_3; i3++) {
const DataType<Type>* const input_ptr_3 =
input_data.data() + i3 * input_stride_3;
DataType<Type>* const output_ptr_3 =
output_data.data() + i3 * output_stride_3;
for (int i2 = 0; i2 < output_size_2; i2++) {
const DataType<Type>* const input_ptr_2 =
input_ptr_3 + i2 * input_stride_2;
DataType<Type>* const output_ptr_2 = output_ptr_3 + i2 * output_stride_2;
for (int i1 = 0; i1 < output_size_1; i1++) {
const DataType<Type>* input_ptr = input_ptr_2 + i1 * input_stride_1;
DataType<Type>* output_ptr = output_ptr_2 + i1 * output_stride_1;
DataType<Type>* const output_ptr_end =
output_ptr + output_size_0 * output_stride_0;
while (output_ptr != output_ptr_end) {
*output_ptr = *input_ptr;
input_ptr += input_stride_0;
output_ptr += output_stride_0;
}
}
}
}
}
} // namespace
absl::Status ResolveConstantTranspose::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto it = model->operators.begin() + op_index;
const auto* base_op = it->get();
if (base_op->type != OperatorType::kTranspose) {
return absl::OkStatus();
}
const auto* op = static_cast<const TransposeOperator*>(base_op);
CHECK_EQ(op->inputs.size(), 2);
CHECK_EQ(op->outputs.size(), 1);
auto& output_array = model->GetArray(op->outputs[0]);
if (output_array.data_type == ArrayDataType::kNone) {
// Yield until the output type has been set by PropagateArrayDataTypes.
return absl::OkStatus();
}
if (!output_array.has_shape()) {
// Yield until the output shape has been set by PropagateFixedShapes.
return absl::OkStatus();
}
// We require constant inputs.
if (!IsConstantParameterArray(*model, op->inputs[0]) ||
!IsConstantParameterArray(*model, op->inputs[1])) {
return absl::OkStatus();
}
const Array& input_array = model->GetArray(op->inputs[0]);
CopyMinMaxAndQuantizationRelatedFields(input_array, &output_array);
if (op->perm.empty()) {
// Yield until perm has been populated by ResolveTransposeAttributes.
return absl::OkStatus();
}
// We currently only support 1-4 dimensions.
CHECK_LE(op->perm.size(), 4);
CHECK(!output_array.buffer);
switch (output_array.data_type) {
case ArrayDataType::kFloat:
Transpose<ArrayDataType::kFloat>(model, input_array, op->perm,
&output_array);
break;
case ArrayDataType::kUint8:
Transpose<ArrayDataType::kUint8>(model, input_array, op->perm,
&output_array);
break;
case ArrayDataType::kInt32:
Transpose<ArrayDataType::kInt32>(model, input_array, op->perm,
&output_array);
break;
case ArrayDataType::kInt64:
Transpose<ArrayDataType::kInt64>(model, input_array, op->perm,
&output_array);
break;
case ArrayDataType::kComplex64:
Transpose<ArrayDataType::kComplex64>(model, input_array, op->perm,
&output_array);
break;
default:
LOG(FATAL) << "Unsupported data type given to Transpose op with output \""
<< op->outputs[0] << "\"";
break;
}
AddMessageF("Resolving constant transpose of %s", LogName(*op));
DeleteOpAndArrays(model, op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,364 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <string.h>
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <functional>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/runtime/types.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
// Using the function reducer, reduce input along all axes in axes.
// Put the reduced data in output, which should already be appropriately sized.
// check_output_shape is set to what this code computes the final shape
// to be, so it can be cross checked with the shape computation logic.
void ReduceGeneric(bool keep_dims, const std::vector<int>& axes,
const Shape& input_shape, const std::vector<float>& input,
Shape* check_output_shape, std::vector<float>* output,
const std::function<float(float, float)>& reducer) {
if (!IsNonEmpty(input_shape)) {
// Zero-dimensions will break the NextIndices() logic, so just early out if
// we have an empty shape.
return;
}
// Set up output_shape to be the same length as input_shape, with
// appropriate dimensions squashed to 1. If keep_dims is false, we'll strip
// out the one dimensions at the end, but it's convenient to leave them for
// now. We recompute the shape because we need the output shape to have
// 1-dims in all the squashed dimensions; the shape from shape computation may
// remove those squashed dimensions, depending on the options used.
Shape output_shape = input_shape;
// Reduction mask will be elementwise multiplied against the input
// indices to figure out the output index for the element.
std::vector<int> reduction_mask(input_shape.dimensions_count(), 1);
for (const auto& axis : axes) {
CHECK_GE(axis, 0);
CHECK_LT(axis, input_shape.dimensions_count());
reduction_mask[axis] = 0;
output_shape.mutable_dims()->at(axis) = 1;
}
std::vector<int> output_indices(input_shape.dimensions_count());
for (size_t input_offset = 0; input_offset < input.size(); ++input_offset) {
std::vector<int> input_indices = ReverseOffset(input_shape, input_offset);
// Calculate the output location by squashing input indices to 0
// in reduced axes.
for (int i = 0; i < input_shape.dimensions_count(); ++i) {
output_indices[i] = input_indices[i] * reduction_mask[i];
}
int output_offset = Offset(output_shape, output_indices);
if (input_indices == output_indices) {
// Base element for the reduced axes
output->at(output_offset) = input.at(input_offset);
} else {
// Reduce with existing element.
output->at(output_offset) =
reducer(output->at(output_offset), input.at(input_offset));
}
}
if (!keep_dims) {
// Strip out the dims from output_shape.
std::vector<int> new_dims;
for (int i = 0; i < output_shape.dimensions_count(); ++i) {
if (reduction_mask[i]) {
new_dims.push_back(output_shape.dims(i));
}
}
output_shape.mutable_dims()->swap(new_dims);
}
*check_output_shape = output_shape;
}
} // namespace
bool CopyMinMaxFromFirstInput(const Operator& op, Model* model) {
auto& output_array = model->GetArray(op.outputs[0]);
if (output_array.minmax) {
return false;
}
const auto& input_array = model->GetArray(op.inputs[0]);
if (!input_array.minmax) {
return false;
}
const auto& input_minmax = input_array.GetMinMax();
CHECK(!output_array.minmax);
auto& output_minmax = output_array.GetOrCreateMinMax();
output_minmax.min = input_minmax.min;
output_minmax.max = input_minmax.max;
return true;
}
absl::Status ResolveConstantUnaryOperator::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
const auto unary_it = model->operators.begin() + op_index;
const auto* unary_op = unary_it->get();
// Test for unary ops of types that we know how to resolve.
switch (unary_op->type) {
case OperatorType::kCast:
case OperatorType::kExp:
case OperatorType::kLog:
case OperatorType::kNeg:
case OperatorType::kRsqrt:
case OperatorType::kSqrt:
case OperatorType::kSquare:
case OperatorType::kSum:
case OperatorType::kReduceMin: // Reduction Min
case OperatorType::kReduceMax: // Reduction Max
case OperatorType::kReshape:
case OperatorType::kRelu6:
case OperatorType::kRelu1:
case OperatorType::kRelu:
break;
default:
return absl::OkStatus();
}
// Check if the input is a constant parameter.
if (!IsConstantParameterArray(*model, unary_op->inputs[0])) {
return absl::OkStatus();
}
// if the unary op involves a tensor required by a rnn state, ignore it
for (const auto& rnn_state : model->flags.rnn_states()) {
if (unary_op->inputs[0] == rnn_state.back_edge_source_array()) {
return absl::OkStatus();
}
if (unary_op->inputs[0] == rnn_state.state_array()) {
return absl::OkStatus();
}
}
auto& output_array = model->GetArray(unary_op->outputs[0]);
if (!output_array.has_shape()) {
// Yield until the output array dims have been resolved.
return absl::OkStatus();
}
// At the moment we don't want to care about fused activation functions.
// The idea is that we should do the present constants-propagation before
// activation functions get fused.
if (unary_op->fused_activation_function !=
FusedActivationFunctionType::kNone) {
AddMessageF(
"Not resolving constant %s "
" because it has a fused activation function",
LogName(*unary_op));
return absl::OkStatus();
}
// The min-max is only copied for ops that copy data without arithmetic.
// In future trivial transpose, etc, can be handled here.
if (unary_op->type == OperatorType::kReshape) {
CopyMinMaxFromFirstInput(*unary_op, model);
}
const auto& input_array = model->GetArray(unary_op->inputs[0]);
// We have already tested above for existence of buffers (synonymous to being
// a constant param).
CHECK(input_array.buffer);
std::vector<DataType<ArrayDataType::kFloat>> const* input_float_data =
nullptr;
if (unary_op->type == OperatorType::kCast) {
CastOperator const* cast_op = static_cast<CastOperator const*>(unary_op);
if (cast_op->dst_data_type != ArrayDataType::kFloat) {
AddMessageF(
"Not resolving constant %s because we currently only support casting "
"to float",
LogName(*unary_op));
return absl::OkStatus();
}
if (cast_op->src_data_type != input_array.buffer->type) {
AddMessageF(
"Not resolving constant %s because cast op source type does not "
"match input type",
LogName(*unary_op));
}
} else {
if (input_array.buffer->type != ArrayDataType::kFloat) {
return absl::OkStatus();
}
input_float_data = &(input_array.GetBuffer<ArrayDataType::kFloat>().data);
}
// Create a float buffer on the output array, which are always constant.
const Shape& output_shape = output_array.shape();
const int output_dims_count = output_shape.dimensions_count();
const int output_buffer_size = RequiredBufferSizeForShape(output_shape);
auto& output_float_data =
output_array.GetMutableBuffer<ArrayDataType::kFloat>().data;
output_float_data.resize(output_buffer_size);
const Shape& input_shape = input_array.shape();
const int input_buffer_size = RequiredBufferSizeForShape(input_shape);
if (unary_op->type == OperatorType::kCast) {
for (int i = 0; i < output_buffer_size; i++) {
float outval = 0.0f;
if (input_array.buffer->type == ArrayDataType::kFloat) {
outval = static_cast<float>(
input_array.GetBuffer<ArrayDataType::kFloat>().data[i]);
} else if (input_array.buffer->type == ArrayDataType::kUint8) {
outval = static_cast<float>(
input_array.GetBuffer<ArrayDataType::kUint8>().data[i]);
} else if (input_array.buffer->type == ArrayDataType::kInt32) {
outval = static_cast<float>(
input_array.GetBuffer<ArrayDataType::kInt32>().data[i]);
} else if (input_array.buffer->type == ArrayDataType::kInt64) {
outval = static_cast<float>(
input_array.GetBuffer<ArrayDataType::kInt64>().data[i]);
} else if (input_array.buffer->type == ArrayDataType::kBool) {
outval = static_cast<float>(
input_array.GetBuffer<ArrayDataType::kBool>().data[i]);
} else {
LOG(FATAL) << "Unsupported cast op input type";
}
output_float_data[i] = outval;
}
} else if (unary_op->type == OperatorType::kReshape) {
CHECK(input_buffer_size == output_buffer_size);
output_float_data = *input_float_data;
} else if (unary_op->type == OperatorType::kSum) {
CHECK_EQ(unary_op->inputs.size(), 2) << "Sum needs 2 inputs";
if (!IsConstantParameterArray(*model, unary_op->inputs[1])) {
AddMessageF("Axis input is non-constant");
return absl::OkStatus();
}
auto& axis_array = model->GetArray(unary_op->inputs[1]);
CHECK(axis_array.data_type == ArrayDataType::kInt32);
// We only support keep_dims=true; shape prop will need to change otherwise.
auto sum_op = static_cast<const TensorFlowSumOperator*>(unary_op);
Shape check_output_shape;
ReduceGeneric(
sum_op->keep_dims, axis_array.GetBuffer<ArrayDataType::kInt32>().data,
input_shape, *input_float_data, &check_output_shape, &output_float_data,
[](float existing, float current) -> float {
return existing + current;
});
CHECK(check_output_shape == output_shape)
<< "Shape propagation output shape doesn't match output shape from op";
} else if (unary_op->type == OperatorType::kReduceMin) {
// At the moment only full reduction across all dimensions is supported.
// TODO(starka): Output should not be padded.
for (int i = 0; i < output_dims_count; i++) {
CHECK_EQ(output_shape.dims(i), 1);
}
float min = (*input_float_data)[0];
for (int i = 0; i < input_buffer_size; i++) {
min = std::min(min, (*input_float_data)[i]);
}
output_float_data[0] = min;
} else if (unary_op->type == OperatorType::kReduceMax) {
// At the moment only full reduction across all dimensions is supported.
// TODO(starka): Output should not be padded.
for (int i = 0; i < output_dims_count; i++) {
CHECK_EQ(output_shape.dims(i), 1);
}
float max = (*input_float_data)[0];
for (int i = 0; i < input_buffer_size; i++) {
max = std::max(max, (*input_float_data)[i]);
}
output_float_data[0] = max;
} else if (unary_op->type == OperatorType::kExp ||
unary_op->type == OperatorType::kNeg ||
unary_op->type == OperatorType::kLog ||
unary_op->type == OperatorType::kRsqrt ||
unary_op->type == OperatorType::kSqrt ||
unary_op->type == OperatorType::kSquare) {
// Element-wise ops. Should have perfectly matching sizes here.
for (int i = 0; i < output_dims_count; i++) {
CHECK_EQ(output_shape.dims(i), input_shape.dims(i));
}
for (int i = 0; i < output_buffer_size; i++) {
const float val = (*input_float_data)[i];
float outval = 0.f;
if (unary_op->type == OperatorType::kExp) {
outval = std::exp(val);
} else if (unary_op->type == OperatorType::kNeg) {
outval = -val;
} else if (unary_op->type == OperatorType::kLog) {
outval = std::log(val);
} else if (unary_op->type == OperatorType::kRsqrt) {
outval = 1.0f / std::sqrt(val);
} else if (unary_op->type == OperatorType::kSqrt) {
outval = std::sqrt(val);
} else if (unary_op->type == OperatorType::kSquare) {
outval = val * val;
} else {
LOG(FATAL) << "should not get here.";
}
output_float_data[i] = outval;
}
} else if (unary_op->type == OperatorType::kRelu6 ||
unary_op->type == OperatorType::kRelu1 ||
unary_op->type == OperatorType::kRelu) {
for (int i = 0; i < output_buffer_size; ++i) {
const float value = (*input_float_data)[i];
float new_value = 0.0f;
switch (unary_op->type) {
case OperatorType::kRelu: {
static constexpr float kLower = 0;
new_value = value < kLower ? kLower : value;
break;
}
case OperatorType::kRelu1: {
static constexpr float kUpper = 1;
static constexpr float kLower = -1;
new_value = value > kUpper ? kUpper : value < kLower ? kLower : value;
break;
}
case OperatorType::kRelu6: {
static constexpr float kUpper = 6;
static constexpr float kLower = 0;
new_value = value > kUpper ? kUpper : value < kLower ? kLower : value;
break;
}
default:
LOG(FATAL) << "Unsupported activation function "
<< LogName(*unary_op);
return absl::OkStatus();
}
output_float_data[i] = new_value;
}
} else {
LOG(FATAL) << "should not get here.";
}
DeleteOpAndArrays(model, unary_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,88 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <algorithm>
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status ResolveFakeQuantArgsFromVars::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
const auto fakequant_it = model->operators.begin() + op_index;
auto* fakequant_base_op = fakequant_it->get();
if (fakequant_base_op->type != OperatorType::kFakeQuant) {
return absl::OkStatus();
}
auto* fakequant_op = static_cast<FakeQuantOperator*>(fakequant_base_op);
if (fakequant_op->minmax) {
// Already resolved.
return absl::OkStatus();
}
CHECK_EQ(fakequant_op->inputs.size(), 3);
// We need to yield until the min and max parameters have been
// resolved to constant arrays.
for (int i = 1; i <= 2; i++) {
if (!IsConstantParameterArray(*model, fakequant_op->inputs[i])) {
return absl::OkStatus();
}
}
// Obtain the final min/max values
const auto& min_array = model->GetArray(fakequant_op->inputs[1]);
const auto& max_array = model->GetArray(fakequant_op->inputs[2]);
CHECK_EQ(RequiredBufferSizeForShape(min_array.shape()), 1);
CHECK_EQ(RequiredBufferSizeForShape(max_array.shape()), 1);
fakequant_op->minmax = std::make_unique<MinMax>();
MinMax& minmax = *fakequant_op->minmax;
minmax.min = min_array.GetBuffer<ArrayDataType::kFloat>().data[0];
minmax.max = max_array.GetBuffer<ArrayDataType::kFloat>().data[0];
// We always want [min, max] to contain 0.
if (minmax.min > 0 || minmax.max < 0) {
LOG(WARNING) << "For " << LogName(*fakequant_op) << " the MinMax range "
<< "[" << minmax.min << ", " << minmax.max
<< "] does not contain 0. "
<< "Proceeding by tweaking it to contain 0, which will result "
"in poor accuracy.";
}
minmax.min = std::min(minmax.min, 0.);
minmax.max = std::max(minmax.max, 0.);
// We won't use the input arrays that provided these min and max
// values, anymore. Delete them unless they are used by something
// else.
for (int i = 1; i <= 2; i++) {
DeleteArrayIfUnusedOutsideOfOp(fakequant_op->inputs[i], fakequant_op,
model);
}
fakequant_op->inputs.resize(1);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,57 @@
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status ResolveGatherAttributes::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto* gather_op = model->operators[op_index].get();
if (gather_op->type != OperatorType::kGather) return absl::OkStatus();
auto* op = static_cast<GatherOperator*>(gather_op);
if (op->axis) {
// Attributes already resolved
return absl::OkStatus();
}
if (op->inputs.size() != 3) return absl::OkStatus();
if (!IsConstantParameterArray(*model, op->inputs[2])) return absl::OkStatus();
const auto& indices_array = model->GetArray(op->inputs[2]);
if (!indices_array.has_shape()) return absl::OkStatus();
const auto& axis_data = indices_array.GetBuffer<ArrayDataType::kInt32>().data;
CHECK_EQ(axis_data.size(), 1)
<< "Multidimensional gather not supported on " << LogName(*op);
op->axis = {axis_data[0]};
// Drop the axis array as we no longer need it.
DeleteArrayIfUnusedOutsideOfOp(op->inputs[2], op, model);
op->inputs.resize(2);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,162 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
template <typename T>
bool AreAllBufferElementsZero(const std::vector<T>& buffer_data) {
for (auto x : buffer_data) {
if (x != T()) {
return false;
}
}
return true;
}
template <ArrayDataType Type>
void FillArrayWithZeros(Array* array) {
CHECK(array->data_type == Type);
std::vector<DataType<Type>>& data = array->GetMutableBuffer<Type>().data;
data.resize(RequiredBufferSizeForShape(array->shape()));
for (size_t i = 0; i < data.size(); i++) {
data[i] = DataType<Type>();
}
}
} // namespace
// Removes a multiplication by array of constant zeros by making the output
// array to an array of constant zeros and removing the input arrays if they
// are no longer needed.
absl::Status ResolveMultiplyByZero::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto mul_it = model->operators.begin() + op_index;
auto* mul_op = mul_it->get();
if (mul_op->type != OperatorType::kMul) {
return absl::OkStatus();
}
const auto& output_array_name = mul_op->outputs[0];
auto& output_array = model->GetArray(output_array_name);
if (!IsDiscardableArray(*model, output_array_name)) {
return absl::OkStatus();
}
if (output_array.data_type == ArrayDataType::kNone) {
// Yield until the output type has been set by PropagateArrayDataTypes
return absl::OkStatus();
}
// Yield if the output shape is not known yet.
if (!output_array.has_shape()) {
return absl::OkStatus();
}
// This transformation only handles the case where one operand is all 0's and
// the other is non-constant. Other cases are handled by constant propagation
// or the trivial binary removal pass.
const bool is_input_constant[2] = {
IsConstantParameterArray(*model, mul_op->inputs[0]),
IsConstantParameterArray(*model, mul_op->inputs[1]),
};
if (!is_input_constant[0] && !is_input_constant[1]) {
// Neither input is constant, so nothing we can resolve here.
return absl::OkStatus();
}
if (is_input_constant[0] && is_input_constant[1]) {
// Both inputs are constants. That's a job for constants propagation, not
// for us to handle here.
return absl::OkStatus();
}
const int index_of_constant_input = is_input_constant[0] ? 0 : 1;
const int index_of_variable_input = is_input_constant[0] ? 1 : 0;
CHECK(is_input_constant[index_of_constant_input]);
CHECK(!is_input_constant[index_of_variable_input]);
const auto& constant_input_array =
model->GetArray(mul_op->inputs[index_of_constant_input]);
CHECK(constant_input_array.data_type == output_array.data_type);
switch (output_array.data_type) {
case ArrayDataType::kFloat: {
const auto& constant_input_data =
constant_input_array.GetBuffer<ArrayDataType::kFloat>().data;
if (!AreAllBufferElementsZero<DataType<ArrayDataType::kFloat>>(
constant_input_data)) {
return absl::OkStatus();
}
FillArrayWithZeros<ArrayDataType::kFloat>(&output_array);
} break;
case ArrayDataType::kUint8: {
const auto& constant_input_data =
constant_input_array.GetBuffer<ArrayDataType::kUint8>().data;
if (!AreAllBufferElementsZero<DataType<ArrayDataType::kUint8>>(
constant_input_data)) {
return absl::OkStatus();
}
FillArrayWithZeros<ArrayDataType::kUint8>(&output_array);
} break;
case ArrayDataType::kInt32: {
const auto& constant_input_data =
constant_input_array.GetBuffer<ArrayDataType::kInt32>().data;
if (!AreAllBufferElementsZero<DataType<ArrayDataType::kInt32>>(
constant_input_data)) {
return absl::OkStatus();
}
FillArrayWithZeros<ArrayDataType::kInt32>(&output_array);
} break;
case ArrayDataType::kInt64: {
const auto& constant_input_data =
constant_input_array.GetBuffer<ArrayDataType::kInt64>().data;
if (!AreAllBufferElementsZero<DataType<ArrayDataType::kInt64>>(
constant_input_data)) {
return absl::OkStatus();
}
FillArrayWithZeros<ArrayDataType::kInt64>(&output_array);
} break;
case ArrayDataType::kComplex64: {
const auto& constant_input_data =
constant_input_array.GetBuffer<ArrayDataType::kComplex64>().data;
if (!AreAllBufferElementsZero<DataType<ArrayDataType::kComplex64>>(
constant_input_data)) {
return absl::OkStatus();
}
FillArrayWithZeros<ArrayDataType::kComplex64>(&output_array);
} break;
default:
AddMessageF(
"Cannot resolve multiply by 0 because of unsupported data type\n");
return absl::OkStatus();
}
DeleteOpAndArrays(model, mul_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,60 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status ResolvePadAttributes::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto pad_it = model->operators.begin() + op_index;
auto* pad_op = pad_it->get();
if (pad_op->type != OperatorType::kPad) return absl::OkStatus();
auto* op = static_cast<PadOperator*>(pad_op);
if (!op->left_padding.empty()) return absl::OkStatus();
CHECK_EQ(op->inputs.size(), 2);
if (!IsConstantParameterArray(*model, op->inputs[1])) return absl::OkStatus();
const auto& array = model->GetArray(op->inputs[1]);
if (!array.has_shape()) return absl::OkStatus();
const std::vector<int>& dims = array.shape().dims();
CHECK_EQ(dims.size(), 2);
std::vector<int> buffer = array.GetBuffer<ArrayDataType::kInt32>().data;
for (int i = 0; i < dims[0]; ++i) {
op->left_padding.push_back(buffer[i * 2]);
op->right_padding.push_back(buffer[i * 2 + 1]);
}
// TODO(dkalenichenko): Delete the extra input?
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,60 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status ResolvePadV2Attributes::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto pad_it = model->operators.begin() + op_index;
auto* pad_op = pad_it->get();
if (pad_op->type != OperatorType::kPadV2) return absl::OkStatus();
auto* op = static_cast<PadV2Operator*>(pad_op);
if (!op->left_padding.empty()) return absl::OkStatus();
CHECK_EQ(op->inputs.size(), 3);
if (!IsConstantParameterArray(*model, op->inputs[1])) return absl::OkStatus();
const auto& array = model->GetArray(op->inputs[1]);
if (!array.has_shape()) return absl::OkStatus();
const std::vector<int>& dims = array.shape().dims();
CHECK_EQ(dims.size(), 2);
std::vector<int> buffer = array.GetBuffer<ArrayDataType::kInt32>().data;
for (int i = 0; i < dims[0]; ++i) {
op->left_padding.push_back(buffer[i * 2]);
op->right_padding.push_back(buffer[i * 2 + 1]);
}
// TODO(dkalenichenko): Delete the extra input?
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,80 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
template <typename T>
bool ResolveAttributes(Model* model, T* op) {
if (!op->axis.empty()) {
// Attributes already resolved
return false;
}
if (op->inputs.size() != 2) return false;
if (!IsConstantParameterArray(*model, op->inputs[1])) return false;
const Array& indices_array = model->GetArray(op->inputs[1]);
if (!indices_array.has_shape()) return false;
// It is ok for indices_array to have a shape for an empty tensor. In that
// case, we don't bother setting 'axis'.
if (indices_array.buffer->Length() == 0) return false;
op->axis = indices_array.GetBuffer<ArrayDataType::kInt32>().data;
return true;
}
absl::Status ResolveReduceAttributes::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
Operator* op = model->operators[op_index].get();
switch (op->type) {
case OperatorType::kMean:
*modified = ResolveAttributes(model, static_cast<MeanOperator*>(op));
return absl::OkStatus();
case OperatorType::kSum:
*modified =
ResolveAttributes(model, static_cast<TensorFlowSumOperator*>(op));
return absl::OkStatus();
case OperatorType::kReduceProd:
*modified =
ResolveAttributes(model, static_cast<TensorFlowProdOperator*>(op));
return absl::OkStatus();
case OperatorType::kReduceMin:
*modified =
ResolveAttributes(model, static_cast<TensorFlowMinOperator*>(op));
return absl::OkStatus();
case OperatorType::kReduceMax:
*modified =
ResolveAttributes(model, static_cast<TensorFlowMaxOperator*>(op));
return absl::OkStatus();
case OperatorType::kAny:
*modified =
ResolveAttributes(model, static_cast<TensorFlowMaxOperator*>(op));
return absl::OkStatus();
default:
return absl::OkStatus();
}
}
} // namespace toco
@@ -0,0 +1,135 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/toco_types.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
void RenameArray(Model* model, const std::string& oldname,
const std::string& desired_newname) {
const std::string& newname = AvailableArrayName(*model, desired_newname);
auto& arrays = model->GetMutableArrayMap();
arrays[newname] = std::move(arrays[oldname]);
arrays.erase(oldname);
for (const auto& op : model->operators) {
for (std::string& input : op->inputs) {
if (input == oldname) {
input = newname;
}
}
for (std::string& output : op->outputs) {
if (output == oldname) {
output = newname;
}
}
}
}
} // namespace
// Reorder the elements of an input_array according to the input_axes_order and
// output_axes_order. Then adjust the shapes of the input and output arrays
// accordingly. Note that input_array must have a buffer (that is, it is a
// constant array).
template <typename T, ArrayDataType DataType>
void ReorderAxes(AxesOrder input_axes_order, AxesOrder output_axes_order,
const Array& input_array, Array* output_array) {
DCHECK(input_array.buffer->type == DataType);
DCHECK(!output_array->buffer);
const auto& input_data = input_array.GetBuffer<DataType>().data;
auto& output_data = output_array->GetMutableBuffer<DataType>().data;
output_data.resize(RequiredBufferSizeForShape(output_array->shape()));
// TODO(b/62904716) Shapes should be used directly.
Shape input_shape = input_array.shape();
Shape output_shape = output_array->shape();
if (AxesCount(input_axes_order) == 2) {
UnextendShape(&input_shape, 2);
UnextendShape(&output_shape, 2);
}
ShuffleArray(input_shape, input_axes_order, output_axes_order, output_shape,
input_data.data(), output_data.data());
if (input_array.minmax) {
output_array->GetOrCreateMinMax() = input_array.GetMinMax();
}
if (input_array.narrow_range) {
output_array->narrow_range = true;
}
}
absl::Status ResolveReorderAxes::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto it = model->operators.begin() + op_index;
auto* op = it->get();
if (op->type != OperatorType::kReorderAxes) {
return absl::OkStatus();
}
auto* reorder_op = static_cast<ReorderAxesOperator*>(op);
// Intentionally copies, not references.
const std::string input_array_name = reorder_op->inputs[0];
const std::string output_array_name = reorder_op->outputs[0];
auto& input_array = model->GetArray(input_array_name);
auto& output_array = model->GetArray(output_array_name);
if (!input_array.buffer) {
return absl::OkStatus();
}
// Yield until output dims have been resolved.
if (!output_array.has_shape()) {
return absl::OkStatus();
}
// Reorder the input array dims and buffer data
if (input_array.buffer->type == ArrayDataType::kFloat) {
ReorderAxes<float, ArrayDataType::kFloat>(reorder_op->input_axes_order,
reorder_op->output_axes_order,
input_array, &output_array);
} else if (input_array.buffer->type == ArrayDataType::kUint8) {
// TODO(benoitjacob): This path seems unused.
// ReorderAxes is only used when importing from
// TensorFlow GraphDef, which does not support quantized nodes.
ReorderAxes<uint8, ArrayDataType::kUint8>(reorder_op->input_axes_order,
reorder_op->output_axes_order,
input_array, &output_array);
} else {
LOG(FATAL) << "Cannot ReorderAxes unless input buffer is float or uint8.";
}
AddMessageF("Reordered axes for array %s", input_array_name);
DeleteOpAndArrays(model, op);
RenameArray(model, output_array_name, input_array_name);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,51 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/status/status.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status ResolveReshapeAttributes::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto reshape_it = model->operators.begin() + op_index;
auto* reshape_op = reshape_it->get();
if (reshape_op->type != OperatorType::kReshape) {
return absl::OkStatus();
}
auto* op = static_cast<TensorFlowReshapeOperator*>(reshape_op);
if (!op->shape.empty()) return absl::OkStatus();
if (IsConstantParameterArray(*model, reshape_op->inputs[1])) {
const auto& constant_input_array = model->GetArray(reshape_op->inputs[1]);
op->shape = constant_input_array.GetBuffer<ArrayDataType::kInt32>().data;
}
if (op->shape.empty()) return absl::OkStatus();
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,57 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status ResolveSliceAttributes::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto slice_it = model->operators.begin() + op_index;
auto* slice_op = slice_it->get();
if (slice_op->type != OperatorType::kSlice) return absl::OkStatus();
auto* op = static_cast<SliceOperator*>(slice_op);
if (!op->begin.empty()) return absl::OkStatus();
CHECK_EQ(op->inputs.size(), 3);
if (!IsConstantParameterArray(*model, op->inputs[1])) return absl::OkStatus();
if (!IsConstantParameterArray(*model, op->inputs[2])) return absl::OkStatus();
const auto& begin_array = model->GetArray(op->inputs[1]);
if (!begin_array.has_shape()) return absl::OkStatus();
const auto& size_array = model->GetArray(op->inputs[2]);
if (!size_array.has_shape()) return absl::OkStatus();
op->begin = begin_array.GetBuffer<ArrayDataType::kInt32>().data;
op->size = size_array.GetBuffer<ArrayDataType::kInt32>().data;
// TODO(dkalenichenko): Delete the extra inputs?
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,85 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status ResolveSpaceToBatchNDAttributes::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
const auto op_it = model->operators.begin() + op_index;
if (op_it->get()->type != OperatorType::kSpaceToBatchND)
return absl::OkStatus();
auto* op = static_cast<SpaceToBatchNDOperator*>(op_it->get());
// The attributes are resolved only when the 3 attributes (block_shape,
// before_paddings, after_paddings) are all constant.
if (!op->block_shape.empty()) {
return absl::OkStatus();
}
const int block_shape_index = 1;
const int paddings_index = 2;
CHECK_EQ(op->inputs.size(), 3);
if (!IsConstantParameterArray(*model, op->inputs[block_shape_index]) ||
!IsConstantParameterArray(*model, op->inputs[paddings_index]))
return absl::OkStatus();
// Handle paddings.
const auto& paddings_array = model->GetArray(op->inputs[paddings_index]);
if (!paddings_array.has_shape()) return absl::OkStatus();
const std::vector<int>& paddings_dims = paddings_array.shape().dims();
if (paddings_dims.size() != 2) {
// Code only handles padding of 2 dimensions. Perhaps another transformation
// will delete this op.
return absl::OkStatus();
}
const std::vector<int>& paddings_buffer =
paddings_array.GetBuffer<ArrayDataType::kInt32>().data;
for (int i = 0; i < paddings_dims[0]; ++i) {
op->before_paddings.push_back(paddings_buffer[i * 2]);
op->after_paddings.push_back(paddings_buffer[i * 2 + 1]);
}
// Handle block_shape.
const auto& block_shape_array =
model->GetArray(op->inputs[block_shape_index]);
if (!block_shape_array.has_shape()) return absl::OkStatus();
const std::vector<int>& block_shape_dims = block_shape_array.shape().dims();
CHECK_EQ(block_shape_dims.size(), 1);
const std::vector<int>& block_shape_buffer =
block_shape_array.GetBuffer<ArrayDataType::kInt32>().data;
for (int i = 0; i < block_shape_dims[0]; ++i) {
op->block_shape.push_back(block_shape_buffer[i]);
}
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,55 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/graph_transformations/remove_trivial_passthrough.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status ResolveSqueezeAttributes::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto* squeeze_op = model->operators[op_index].get();
if (squeeze_op->type != OperatorType::kSqueeze) {
return absl::OkStatus();
}
DCHECK_EQ(squeeze_op->inputs.size(), 1);
DCHECK_EQ(squeeze_op->outputs.size(), 1);
// If the output is consumed by a reshape op, it's a trivial squeeze.
if (CountOpsWithInput(*model, squeeze_op->outputs[0]) == 1) {
const auto* next_op = GetOpWithInput(*model, squeeze_op->outputs[0]);
if (next_op->type == OperatorType::kReshape) {
AddMessageF(
"%s is trivial because its output is only consumed by a "
"Reshape op",
LogName(*squeeze_op));
*modified = RemoveTrivialPassthroughOp(this, model, op_index);
return absl::OkStatus();
}
}
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,127 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
int PadAttributeArray(Array* attribute_array, std::vector<int> pad_values,
int mask) {
int attribute_dim_count = attribute_array->shape().dims(0);
int dim_count = pad_values.size();
if (attribute_dim_count < dim_count) {
Shape strided_slice_shape = Shape({dim_count});
attribute_array->copy_shape(strided_slice_shape);
Buffer<ArrayDataType::kInt32>* buffer =
&(attribute_array->GetMutableBuffer<ArrayDataType::kInt32>());
buffer->data.resize(RequiredBufferSizeForShape(strided_slice_shape));
for (int i = attribute_dim_count; i < dim_count; i++) {
buffer->data[i] = pad_values[i];
mask |= 1 << i;
}
}
return mask;
}
absl::Status ResolveStridedSliceAttributes::Run(Model* model,
std::size_t op_index,
bool* modified) {
*modified = false;
const auto slice_it = model->operators.begin() + op_index;
auto* slice_op = slice_it->get();
if (slice_op->type != OperatorType::kStridedSlice) return absl::OkStatus();
auto* op = static_cast<StridedSliceOperator*>(slice_op);
if (!op->start_indices.empty()) {
// We have already resolved these attributes
return absl::OkStatus();
}
CHECK_EQ(op->inputs.size(), 4);
const auto& input_array = model->GetArray(op->inputs[0]);
if (!input_array.has_shape()) {
// We require the dimensionality of the input to pad the indices
return absl::OkStatus();
}
auto& start_array = model->GetArray(op->inputs[1]);
if (!start_array.has_shape()) return absl::OkStatus();
if (toco::RequiredBufferSizeForShape(start_array.shape()) > 4) {
// Only 1-4D arrays are supported for now.
return absl::OkStatus();
}
auto& stop_array = model->GetArray(op->inputs[2]);
if (!stop_array.has_shape()) return absl::OkStatus();
auto& stride_array = model->GetArray(op->inputs[3]);
if (!stride_array.has_shape()) return absl::OkStatus();
if (!IsConstantParameterArray(*model, op->inputs[1])) return absl::OkStatus();
if (!IsConstantParameterArray(*model, op->inputs[2])) return absl::OkStatus();
if (!IsConstantParameterArray(*model, op->inputs[3])) return absl::OkStatus();
int num_input_axes = input_array.shape().dimensions_count();
int start_indices_size = start_array.shape().dims(0);
int stop_indices_size = stop_array.shape().dims(0);
int stride_indices_size = stride_array.shape().dims(0);
CHECK_GE(start_indices_size, 1);
CHECK_LE(start_indices_size, 4);
CHECK_LE(stop_indices_size, 4);
CHECK_LE(stride_indices_size, 4);
// The TensorFlow documentation is not explicit on how it handles fewer
// supplied indices than dimensions, but they are accepted. We emulate TF's
// behavior by fully iterating over each omitted dimension.
CHECK_LE(start_indices_size, num_input_axes)
<< "StridedSlice op requires no more than " << num_input_axes
<< " start indices";
CHECK_LE(stop_indices_size, num_input_axes)
<< "StridedSlice op requires no more than " << num_input_axes
<< " stop indices";
CHECK_LE(stride_indices_size, num_input_axes)
<< "StridedSlice op requires no more than " << num_input_axes
<< " strides";
// Ideally, we would remove the input arrays after they have been resolved.
// However, we must then reconstitute these input arrays for all supported
// export formats. For now, leave the arrays so we don't have to modify our
// exporters. Ideally, we wouldn't have op attributes, and would work directly
// with the input arrays.
std::vector<int> begin_pad_values(num_input_axes, 0);
op->begin_mask =
PadAttributeArray(&start_array, begin_pad_values, op->begin_mask);
op->end_mask =
PadAttributeArray(&stop_array, input_array.shape().dims(), op->end_mask);
std::vector<int> stride_pad_values(num_input_axes, 1);
PadAttributeArray(&stride_array, stride_pad_values, 0);
op->start_indices = start_array.GetBuffer<ArrayDataType::kInt32>().data;
op->stop_indices = stop_array.GetBuffer<ArrayDataType::kInt32>().data;
op->strides = stride_array.GetBuffer<ArrayDataType::kInt32>().data;
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,82 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status ResolveTensorFlowConcat::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto concat_it = model->operators.begin() + op_index;
const auto* tf_concat_op = concat_it->get();
if (tf_concat_op->type != OperatorType::kConcat &&
tf_concat_op->type != OperatorType::kConcatV2) {
return absl::OkStatus();
}
CHECK_GE(tf_concat_op->inputs.size(), 2);
// TensorFlow Concat and ConcatV2 nodes only differ by the ordering
// of inputs: in Concat,the axis is the first input, while in
// ConcatV2, it is the last input.
std::size_t axis_pos = 0;
if (tf_concat_op->type == OperatorType::kConcatV2) {
axis_pos = tf_concat_op->inputs.size() - 1;
}
const std::string axis_name = tf_concat_op->inputs[axis_pos];
std::vector<std::string> concat_input_names;
for (std::size_t i = 0; i < tf_concat_op->inputs.size(); i++) {
if (i != axis_pos) {
concat_input_names.push_back(tf_concat_op->inputs[i]);
}
}
// If the axis array hasn't been resolved to a constant yet,
// we need to yield.
const auto& axis_array = model->GetArray(axis_name);
if (!axis_array.buffer) {
AddMessageF("Waiting for the axis of %s to be resolved to a constant",
LogName(*tf_concat_op));
return absl::OkStatus();
}
CHECK(axis_array.data_type == ArrayDataType::kInt32);
const auto& axis_data = axis_array.GetBuffer<ArrayDataType::kInt32>().data;
CHECK_EQ(axis_data.size(), 1);
const int axis = axis_data[0];
// Create the Concatenation op replacing the TensorFlowConcat op.
auto* concatenation_op = new ConcatenationOperator;
concatenation_op->axis = axis;
concatenation_op->inputs = concat_input_names;
concatenation_op->outputs = {tf_concat_op->outputs[0]};
auto depth_concat_it = model->operators.emplace(concat_it, concatenation_op);
CHECK_EQ(depth_concat_it->get(), concatenation_op);
DeleteOpAndArrays(model, tf_concat_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,237 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <algorithm>
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "tensorflow/core/platform/errors.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
namespace {
TransposeOperator* FindTransposeOpWithInput(const Model& model,
const std::string& array_name) {
for (auto it = model.operators.begin(); it != model.operators.end(); ++it) {
Operator* op = it->get();
if (op->type != OperatorType::kTranspose) {
continue;
}
if (op->inputs[0] != array_name) {
continue;
}
const auto& permutation_array = model.GetArray(op->inputs[1]);
if (permutation_array.data_type != ArrayDataType::kInt32) {
continue;
}
const auto& permutation_data =
permutation_array.GetBuffer<ArrayDataType::kInt32>().data;
if (permutation_data.size() != 2) {
continue;
}
if (permutation_data[0] != 1 || permutation_data[1] != 0) {
continue;
}
return static_cast<TransposeOperator*>(op);
}
return nullptr;
}
} // namespace
absl::Status ResolveTensorFlowMatMul::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
auto matmul_it = model->operators.begin() + op_index;
if (matmul_it->get()->type != OperatorType::kMatMul) {
return absl::OkStatus();
}
const auto* matmul_op =
static_cast<const TensorFlowMatMulOperator*>(matmul_it->get());
auto refresh_matmul_iterator = [&model, &matmul_it, &matmul_op]() {
matmul_it = std::find_if(model->operators.begin(), model->operators.end(),
[matmul_op](const std::unique_ptr<Operator>& op) {
return op.get() == matmul_op;
});
DCHECK_EQ(matmul_it->get(), matmul_op);
};
std::string input_lhs = matmul_op->inputs[0];
std::string input_rhs = matmul_op->inputs[1];
// Handle `transpose_a` with best effort: If the dimension of lhs is known,
// insert a `Transpose` op.
if (matmul_op->transpose_a) {
Array& lhs_array = model->GetArray(input_lhs);
if (!lhs_array.has_shape()) {
AddMessageF(
"Not replacing %s by a FullyConnected operator, because it has "
"the transpose_a attribute and LHS has no shape",
LogName(*matmul_op));
return absl::OkStatus();
}
int dimensions_count = lhs_array.shape().dimensions_count();
if (dimensions_count < 2) {
return absl::InvalidArgumentError(absl::StrCat(
"Inputs of MatMul should have dimension >= 2. Got %d dimensions",
dimensions_count));
}
// Create a permutation vector to exchange the last 2 dimensions.
// E.g. For 4D, create [0, 1, 3, 2].
std::vector<int> perm;
perm.reserve(dimensions_count);
for (int i = 0; i < dimensions_count; ++i) {
perm.push_back(i);
}
std::swap(perm[dimensions_count - 1], perm[dimensions_count - 2]);
auto* transpose_op = new TransposeOperator;
transpose_op->inputs = {
input_lhs,
CreateInt32Array(
model, AvailableArrayName(*model, input_lhs + "/transpose/perm"),
perm)};
transpose_op->outputs = {
AvailableArrayName(*model, input_lhs + "/transpose")};
model->GetOrCreateArray(transpose_op->outputs[0]);
model->operators.emplace(matmul_it, transpose_op);
// Sanity check
DCHECK_EQ(transpose_op, FindTransposeOpWithInput(*model, input_lhs));
input_lhs = transpose_op->outputs[0];
refresh_matmul_iterator();
}
// TODO(b/138662017): The following code assumes that RHS is 2D. This isn't
// always true in TensorFlow.
//
// Reorder the axes on the second input. TensorFlow uses row-major ordering
// on both inputs, however this is inefficient for the FullyConnected
// operator. We'll transpose the second input to be in column-major order now
// and let constant propagation optimize things (if possible).
if (!matmul_op->transpose_b) {
// Need to transpose input_rhs, by inserting a TransposeOperator.
// First, check if there already is a TransposeOperator transposing that
// array, so we can just reuse it.
auto* transpose_op = FindTransposeOpWithInput(*model, input_rhs);
if (!transpose_op) {
AddMessageF(
"While replacing %s by a FullyConnected operator, created new "
"Transpose op wrapping RHS input array %s",
LogName(*matmul_op), input_rhs);
// No such TransposeOperator found. Create one now.
transpose_op = new TransposeOperator;
transpose_op->inputs = {
input_rhs,
CreateInt32Array(
model, AvailableArrayName(*model, input_rhs + "/transpose/perm"),
{1, 0})};
transpose_op->outputs = {
AvailableArrayName(*model, input_rhs + "/transpose")};
model->GetOrCreateArray(transpose_op->outputs[0]);
model->operators.emplace(matmul_it, transpose_op);
// Sanity check
DCHECK_EQ(transpose_op, FindTransposeOpWithInput(*model, input_rhs));
refresh_matmul_iterator();
} else {
AddMessageF(
"While replacing %s by a FullyConnected operator, reused existing "
"Transpose op wrapping RHS input array %s",
LogName(*matmul_op), input_rhs);
}
// Re-wire: have the matmul consume the transposed array.
input_rhs = transpose_op->outputs[0];
}
// Construct the new FullyConnectedOperator.
auto* fc_op = new FullyConnectedOperator;
fc_op->inputs = {input_lhs, input_rhs};
fc_op->outputs = matmul_op->outputs;
// Insert the newly constructed FullyConnectedOperator.
model->operators.emplace(matmul_it, fc_op);
// Find the op producing the array passed to this MatMul
auto previous_op_it = model->operators.begin();
bool found = false;
for (; previous_op_it != model->operators.end(); ++previous_op_it) {
for (const auto& output : (*previous_op_it)->outputs) {
if (output == matmul_op->inputs[0]) {
found = true;
break;
}
}
if (found) {
break;
}
}
Operator* previous_op = (found) ? previous_op_it->get() : nullptr;
// Refresh iterator.
matmul_it = model->operators.begin();
for (; matmul_it != model->operators.end(); ++matmul_it) {
if (matmul_it->get() == matmul_op) {
break;
}
}
DCHECK_EQ(matmul_it->get(), matmul_op);
// The way that TensorFlow encodes FullyConnected ops is as a pair
// (Reshape, MatMul), so we want to remove the Reshape op and rewrite the
// MatMul op as a FullyConnected. However, TensorFlow skips the Reshape ops if
// the input doesn't need reshaping, so we can't just match (Reshape, MatMul)
// pairs.
if (previous_op && previous_op->type == OperatorType::kReshape) {
AddMessageF("Combining %s and %s into %s", LogName(*previous_op),
LogName(*matmul_op), LogName(*fc_op));
const auto& previous_op_output = previous_op->outputs[0];
if (CountOpsWithInput(*model, previous_op_output) == 1) {
model->EraseArray(previous_op_output);
}
CHECK_EQ(previous_op->inputs.size(), 2);
input_lhs = previous_op->inputs[0];
fc_op->inputs = {input_lhs, input_rhs};
// Only remove Reshape node if no other node uses its output.
if (CountOpsWithInput(*model, previous_op_output) == 1) {
DeleteOpAndArrays(model, previous_op);
}
// We may have just invalidated matmul_it, so let's refresh it now.
refresh_matmul_iterator();
} else {
AddMessageF("Replacing %s by a FullyConnected operator",
LogName(*matmul_op));
}
// erase the MatMul operator
model->operators.erase(matmul_it);
*modified = true;
return absl::OkStatus();
}
} // namespace toco
@@ -0,0 +1,66 @@
/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/status.h"
#include "tensorflow/lite/toco/graph_transformations/graph_transformations.h"
#include "tensorflow/lite/toco/model.h"
#include "tensorflow/lite/toco/tooling_util.h"
namespace toco {
absl::Status ResolveTensorFlowMerge::Run(Model* model, std::size_t op_index,
bool* modified) {
*modified = false;
const auto merge_it = model->operators.begin() + op_index;
const auto* merge_op = merge_it->get();
if (merge_op->type != OperatorType::kMerge) {
return absl::OkStatus();
}
// We need to yield until this Merge node has only 1 input, which will mean
// that is the selected input. Other graph transformations on other nodes
// such as ResolveTensorFlowSwitch, will take care of trimming the
// non-selected inputs, so that at some point there will be only 1 input left.
if (merge_op->inputs.size() > 1) {
AddMessageF("Waiting for %s to be resolved", LogName(*merge_op));
return absl::OkStatus();
}
// Now that the merge node has 1 input exactly, it is the same as an Identity
// node and can be resolved trivially.
CHECK_EQ(merge_op->inputs.size(), 1);
// Update the edges of the graph ahead of removing the node.
for (const auto& other_op : model->operators) {
for (auto& input : other_op->inputs) {
if (input == merge_op->outputs[0]) {
input = merge_op->inputs[0];
}
}
}
DeleteOpAndArrays(model, merge_op);
*modified = true;
return absl::OkStatus();
}
} // namespace toco

Some files were not shown because too many files have changed in this diff Show More