/* 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/tools/optimize/quantize_model.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "flatbuffers/flexbuffers.h" #include "absl/strings/str_cat.h" #include "flatbuffers/flatbuffer_builder.h" // from @flatbuffers #include "tensorflow/compiler/mlir/lite/tools/optimize/operator_property.h" #include "tensorflow/lite/context.h" #include "tensorflow/lite/core/api/error_reporter.h" #include "tensorflow/lite/core/model.h" #include "tensorflow/lite/kernels/internal/cppmath.h" #include "tensorflow/lite/schema/schema_generated.h" #include "tensorflow/lite/schema/schema_utils.h" #include "tensorflow/lite/tools/optimize/model_utils.h" #include "tensorflow/lite/tools/optimize/quantization_utils.h" namespace tflite { namespace optimize { namespace { // Bias tensors must be duplicated if it is used as a non-bias input in another // op (quantized to 8 bit), in order to quantize to 32 bit. TfLiteStatus DuplicateBiasesWithMultipleUses(ModelT* model, ErrorReporter* error_reporter) { std::set input_uses; // Get all input uses for constant tensors. for (size_t subgraph_idx = 0; subgraph_idx < model->subgraphs.size(); subgraph_idx++) { SubGraphT* subgraph = model->subgraphs.at(subgraph_idx).get(); for (size_t op_idx = 0; op_idx < subgraph->operators.size(); op_idx++) { operator_property::OperatorProperty property = operator_property::GetOperatorProperty(model, subgraph_idx, op_idx); auto* op = subgraph->operators[op_idx].get(); for (const auto& idx_pair : property.inputs) { const int idx = idx_pair.first; if (op->inputs[idx] < 0 || idx >= op->inputs.size()) { continue; } const TensorT* input_tensor = subgraph->tensors[op->inputs[idx]].get(); if (!input_tensor || (input_tensor->buffer < 0) || (input_tensor->buffer >= model->buffers.size())) { continue; } const BufferT* buffer = model->buffers[input_tensor->buffer].get(); if (buffer && !buffer->data.empty()) { input_uses.insert({op->inputs[idx]}); } } } } std::map bias_uses; for (size_t subgraph_idx = 0; subgraph_idx < model->subgraphs.size(); subgraph_idx++) { SubGraphT* subgraph = model->subgraphs.at(subgraph_idx).get(); for (size_t op_idx = 0; op_idx < subgraph->operators.size(); op_idx++) { operator_property::OperatorProperty property = operator_property::GetOperatorProperty(model, subgraph_idx, op_idx); OperatorT* op = subgraph->operators[op_idx].get(); for (const int bias_idx : property.biases) { if (bias_idx >= op->inputs.size() || op->inputs[bias_idx] < 0) { continue; } const TensorT* bias_tensor = subgraph->tensors[op->inputs[bias_idx]].get(); if (!bias_tensor || (bias_tensor->buffer < 0) || (bias_tensor->buffer >= model->buffers.size())) { continue; } const BufferT* bias_buffer = model->buffers[bias_tensor->buffer].get(); if (!bias_buffer || bias_buffer->data.empty()) { continue; } if (input_uses.find(op->inputs[bias_idx]) != input_uses.end()) { // If used as input, duplicate the tensor and insert into bias uses. int bias_use_count = 1; auto inserted = bias_uses.insert({op->inputs[bias_idx], bias_use_count}); if (!inserted.second) { bias_use_count = ++inserted.first->second; } std::unique_ptr new_tensor(new TensorT); new_tensor->name = absl::StrCat(bias_tensor->name, "_duplicate_", bias_use_count); new_tensor->shape = bias_tensor->shape; new_tensor->type = bias_tensor->type; if (bias_tensor->quantization) { new_tensor->quantization = std::make_unique(); new_tensor->quantization->scale.assign( bias_tensor->quantization->scale.begin(), bias_tensor->quantization->scale.end()); new_tensor->quantization->zero_point.assign( bias_tensor->quantization->zero_point.begin(), bias_tensor->quantization->zero_point.end()); } std::unique_ptr new_buffer(new BufferT); new_buffer->data.assign(bias_buffer->data.begin(), bias_buffer->data.end()); model->buffers.push_back(std::move(new_buffer)); new_tensor->buffer = model->buffers.size() - 1; subgraph->tensors.push_back(std::move(new_tensor)); op->inputs[bias_idx] = subgraph->tensors.size() - 1; } } } } return kTfLiteOk; } bool IsFloatTensor(const SubGraphT* subgraph, int32_t tensor_idx) { TensorT* tensor = subgraph->tensors[tensor_idx].get(); if (tensor->type != TensorType_FLOAT32) { // Skip non-real-valued tensor. return false; } return true; } // Gets the operator property from the operator_property list and additionally // modifies the quantizable parameter based on the user's specified // operator_names. operator_property::OperatorProperty GetOperatorProperty( const std::unordered_set& operator_names, const ModelT* model, int subgraph_index, int op_idx, const string& operator_name, const TensorType& activations_type, bool disable_per_channel = false, bool disable_per_channel_quantization_for_dense_layers = false) { operator_property::OperatorProperty property = operator_property::GetOperatorProperty(model, subgraph_index, op_idx); const SubGraphT* subgraph = model->subgraphs[subgraph_index].get(); const OperatorT* op = subgraph->operators[op_idx].get(); const BuiltinOperator op_code = GetBuiltinCode(model->operator_codes[op->opcode_index].get()); if (activations_type == TensorType_INT16 && !property.quantizable_int16) { property.quantizable = false; } // The algorithm adds Dequantize and Quantize, so we don't require them to be // in the operator_names. if (op_code != BuiltinOperator_DEQUANTIZE && op_code != BuiltinOperator_QUANTIZE) { property.quantizable = property.quantizable && (operator_names.find(operator_name) != operator_names.end()); } if (disable_per_channel) { for (auto& input : property.inputs) { if (input.second.per_axis) { input.second.per_axis = false; } } } if (disable_per_channel_quantization_for_dense_layers && op_code == BuiltinOperator_FULLY_CONNECTED) { for (auto& input : property.inputs) { if (input.second.per_axis) { input.second.per_axis = false; } } } return property; } bool IsRealValueOp(const std::unordered_set& real_value_op_set, const string& operator_name) { return real_value_op_set.find(operator_name) != real_value_op_set.end(); } // Utility function to determine if tensor is constant and only has one use. bool IsConstantWithOneUse(const ModelT* model, const SubGraphT* subgraph, const int tensor_id) { if (!subgraph || (tensor_id >= subgraph->tensors.size())) { return false; } const auto& tensor = subgraph->tensors[tensor_id]; if (!tensor || !model || (tensor->buffer < 0) || (tensor->buffer >= model->buffers.size()) || (!model->buffers[tensor->buffer]) || (model->buffers[tensor->buffer]->data.empty())) { return false; } int uses = 0; for (size_t op_idx = 0; op_idx < subgraph->operators.size(); op_idx++) { const auto& op = subgraph->operators[op_idx]; if (!op) { continue; } const std::vector& inputs = op->inputs; if ((std::find(inputs.begin(), inputs.end(), tensor_id) != inputs.end()) && (++uses > 1)) { return false; } } return true; } // Creates a set that contains all quantizable ops that happen to take a // non-float type in the source graph. std::unordered_set PopulateRealValueOpSet( ModelT* model, const std::unordered_set& operator_names, const TensorType& activations_type) { std::unordered_set real_value_op_set; for (size_t subgraph_idx = 0; subgraph_idx < model->subgraphs.size(); subgraph_idx++) { SubGraphT* subgraph = model->subgraphs.at(subgraph_idx).get(); for (size_t op_idx = 0; op_idx < subgraph->operators.size(); op_idx++) { OperatorT* op = subgraph->operators[op_idx].get(); const BuiltinOperator op_code = GetBuiltinCode(model->operator_codes[op->opcode_index].get()); if (op->outputs.empty() && op_code != BuiltinOperator_ASSIGN_VARIABLE) { continue; } const string operator_name = op_code != BuiltinOperator_ASSIGN_VARIABLE ? subgraph->tensors[op->outputs[0]]->name : subgraph->tensors[op->inputs[0]]->name; operator_property::OperatorProperty property = GetOperatorProperty(operator_names, model, subgraph_idx, op_idx, operator_name, activations_type); if (!property.quantizable) { real_value_op_set.insert(operator_name); continue; } for (const std::pair& input : property.inputs) { const int32_t input_idx = input.first; const int32_t tensor_idx = op->inputs[input_idx]; if (IsFloatTensor(subgraph, tensor_idx)) { real_value_op_set.insert(operator_name); break; } } for (const std::pair& output : property.outputs) { const int32_t output_idx = output.first; const int32_t tensor_idx = op->outputs[output_idx]; if (IsFloatTensor(subgraph, tensor_idx)) { real_value_op_set.insert(operator_name); break; } } if (property.arbitrary_inputs) { const int32_t tensor_idx = op->inputs[0]; if (IsFloatTensor(subgraph, tensor_idx)) { real_value_op_set.insert(operator_name); } } if (property.arbitrary_outputs) { const int32_t tensor_idx = op->outputs[0]; if (IsFloatTensor(subgraph, tensor_idx)) { real_value_op_set.insert(operator_name); } } } } return real_value_op_set; } // Set quantized_bias_type for CONV_2D/FULLY_CONNECTED/TRANSPOSE_CONV so that // the accumulator is initialized to the appropriate default value when the bias // is NULL. void SetOperatorPropertyBiasType(ModelT* model, const TensorType& bias_type) { for (int subgraph_idx = 0, end = model->subgraphs.size(); subgraph_idx < end; subgraph_idx++) { SubGraphT* subgraph = model->subgraphs.at(subgraph_idx).get(); // Iterate backward to avoid messing with index. for (int op_idx = subgraph->operators.size() - 1; op_idx >= 0; op_idx--) { OperatorT* op = subgraph->operators[op_idx].get(); OperatorCodeT* op_code = model->operator_codes[op->opcode_index].get(); if (op_code && op_code->builtin_code == BuiltinOperator_FULLY_CONNECTED) { auto* options = op->builtin_options.AsFullyConnectedOptions(); if (options) { options->quantized_bias_type = bias_type; } } else if (op_code && op_code->builtin_code == BuiltinOperator_CONV_2D) { auto* options = op->builtin_options.AsConv2DOptions(); if (options) { options->quantized_bias_type = bias_type; } } else if (op_code && op_code->builtin_code == BuiltinOperator_TRANSPOSE_CONV) { auto* options = op->builtin_options.AsTransposeConvOptions(); if (options) { options->quantized_bias_type = bias_type; } } } } } TfLiteStatus QuantizeBias(ModelT* model, const TensorT* input_tensor, const TensorT* weight_tensor, TensorT* bias_tensor, bool is_per_channel, int channel_dim_index, const TensorType& bias_type, ErrorReporter* error_reporter) { if (bias_tensor->shape.size() != 1) { TF_LITE_REPORT_ERROR(error_reporter, "Expected bias tensor shape to be 1."); return kTfLiteError; } if (input_tensor->type == tflite::TensorType_INT8 && bias_type != tflite::TensorType_INT32) { TF_LITE_REPORT_ERROR( error_reporter, "Expected bias type to be TensorType_INT32 for Int8Quant."); return kTfLiteError; } if (input_tensor->type == tflite::TensorType_INT16 && bias_type != tflite::TensorType_INT32 && bias_type != tflite::TensorType_INT64) { TF_LITE_REPORT_ERROR(error_reporter, "Expected bias type to be TensorType_INT32 or " "TensorType_INT64 for Int16Quant."); return kTfLiteError; } int32_t channel_dim_size = bias_tensor->shape[0]; TF_LITE_ENSURE(error_reporter, weight_tensor->quantization); std::vector weight_scales = weight_tensor->quantization->scale; if (is_per_channel) { if (bias_tensor->shape[0] != weight_tensor->shape[channel_dim_index]) { TF_LITE_REPORT_ERROR( error_reporter, "Channel mismatch between bias and weight tensors %d vs %d", bias_tensor->shape[0], weight_tensor->shape[channel_dim_index]); return kTfLiteError; } if (!input_tensor->quantization || input_tensor->quantization->scale.size() != 1) { TF_LITE_REPORT_ERROR(error_reporter, "Input tensor missing quantization information"); return kTfLiteError; } if (weight_scales.size() != channel_dim_size) { TF_LITE_REPORT_ERROR(error_reporter, "Mismatch weight scale dimension: %d", weight_scales.size()); return kTfLiteError; } if (bias_type == tflite::TensorType_INT64) { return utils::SymmetricPerChannelBiasQuantize( model, bias_tensor, input_tensor->quantization->scale[0], weight_scales.data(), channel_dim_size, error_reporter); } else { return utils::SymmetricPerChannelBiasQuantize( model, bias_tensor, input_tensor->quantization->scale[0], weight_scales.data(), channel_dim_size, error_reporter); } } else { if (weight_scales.size() != 1) { TF_LITE_REPORT_ERROR( error_reporter, "Expected per-layer weight scale dimension size 1, got %d", weight_scales.size()); return kTfLiteError; } if (bias_type == tflite::TensorType_INT64) { return utils::SymmetricPerLayerBiasQuantize( model, bias_tensor, input_tensor->quantization->scale[0] * weight_scales[0], error_reporter); } else { return utils::SymmetricPerLayerBiasQuantize( model, bias_tensor, input_tensor->quantization->scale[0] * weight_scales[0], error_reporter); } } return kTfLiteError; } // True if the tensor type has to be modified. bool TensorTypeChangeRequired(const TensorT* tensor, const TensorType& type) { // The quantized model is type INT8/INT16, so if the user provided type is // INT8/INT16, we do not have to do any custom logic. Additionally, if the // current tensor isn't INT8/INT16 quantized, the custom type doesn't apply. bool int8check = type != TensorType_INT8 && tensor->type == TensorType_INT8 && !tensor->quantization->scale.empty(); bool int16check = type != TensorType_INT16 && tensor->type == TensorType_INT16 && !tensor->quantization->scale.empty(); return (int8check || int16check); } // Check if input is consumed by quantize, which means we don't need to // requantize if the output scale is the same as the input tensor's. bool InputQuantizeRequired(const ModelT* model, const SubGraphT* subgraph, int32_t input_idx) { std::vector quantize_ops; for (size_t op_idx = 0; op_idx < subgraph->operators.size(); op_idx++) { OperatorT* op = subgraph->operators[op_idx].get(); if (std::find(op->inputs.begin(), op->inputs.end(), input_idx) != op->inputs.end()) { const BuiltinOperator op_code = GetBuiltinCode(model->operator_codes[op->opcode_index].get()); if (op_code != BuiltinOperator_QUANTIZE) { return true; } quantize_ops.push_back(op); } } if (quantize_ops.size() == 1) { const auto* tensor = subgraph->tensors[input_idx].get(); const auto* op = quantize_ops[0]; const int32_t output_idx = op->outputs[0]; const auto output_type = subgraph->tensors[output_idx]->type; const float output_scale = subgraph->tensors[output_idx]->quantization->scale[0]; const int64_t output_zero_point = subgraph->tensors[output_idx]->quantization->zero_point[0]; if (output_type == tensor->type && output_scale == tensor->quantization->scale[0] && output_zero_point == tensor->quantization->zero_point[0]) { return false; } } return true; } // Sets the input type, adding a Leading Op node at the start of the model if // necessary. // Returns the new input tensor index. int32_t SetInputType(ModelT* model, SubGraphT* subgraph, const int32_t tensor_idx, const TensorType& input_type, const TensorType& activations_type) { TensorT* tensor = subgraph->tensors[tensor_idx].get(); if (!TensorTypeChangeRequired(tensor, input_type)) { return -1; } if (input_type == TensorType_FLOAT32 || input_type == TensorType_UINT8) { std::string type_string = activations_type == TensorType_INT16 ? "int16" : "int8"; // Create a new tensor to be the input of the leading Op. std::unique_ptr leading_op_input; if (input_type == TensorType_FLOAT32) { // Add tensor for quantize operator. Scales and zero points are not // needed. const string leading_op_name = tensor->name; const string new_name_original_input = tensor->name + "_" + type_string; tensor->name = new_name_original_input; utils::MakeTensor(leading_op_name, tensor->shape, tensor->shape_signature, input_type, &leading_op_input); } else { // Get scale and zero point from the first tensor. const float scale = subgraph->tensors[tensor_idx]->quantization->scale[0]; const int64_t zero_point = subgraph->tensors[tensor_idx]->quantization->zero_point[0]; // Add tensor for requantize operator. Scale is the existing scale and // zero point is shifted by +128. TFLITE_DCHECK_GE(zero_point, -128); TFLITE_DCHECK_LE(zero_point, 127); const string leading_op_name = tensor->name; const string new_name_original_input = tensor->name + "_" + type_string; tensor->name = new_name_original_input; utils::MakeTensorWithQuantParam( leading_op_name, tensor->shape, tensor->shape_signature, input_type, scale, zero_point + 128, &leading_op_input); } // Check if quantize op already exists. if (!InputQuantizeRequired(model, subgraph, tensor_idx)) { subgraph->tensors[tensor_idx] = std::move(leading_op_input); return tensor_idx; } const int32_t leading_op_input_idx = subgraph->tensors.size(); subgraph->tensors.push_back(std::move(leading_op_input)); // Create the leading op, which is Quantize Op that quantize or requantize // the input. std::unique_ptr leading_op; utils::MakeQuantizeOperator(model, &leading_op, leading_op_input_idx, tensor_idx); // Insert the new op at the start of the model. subgraph->operators.insert(subgraph->operators.begin(), std::move(leading_op)); return leading_op_input_idx; } return -1; } // Sets the output type, adding a Tailing Op node at the end of the model if // necessary. // Returns the new output tensor index. int32_t SetOutputType(ModelT* model, SubGraphT* subgraph, const int32_t tensor_idx, const TensorType& output_type, const TensorType& activations_type) { TensorT* tensor = subgraph->tensors[tensor_idx].get(); if (!TensorTypeChangeRequired(tensor, output_type)) { return -1; } if (output_type == TensorType_FLOAT32 || output_type == TensorType_UINT8) { std::string type_string = activations_type == TensorType_INT16 ? "int16" : "int8"; // Create a new tensor to be the output of the tailing op. std::unique_ptr tailing_op_output; if (output_type == TensorType_FLOAT32) { const string tailing_op_name = tensor->name; const string new_name_original_output = tensor->name + "_" + type_string; tensor->name = new_name_original_output; utils::MakeTensor(tailing_op_name, tensor->shape, tensor->shape_signature, output_type, &tailing_op_output); } else { // Get scale and zero point from the last tensor. const float scale = subgraph->tensors[tensor_idx]->quantization->scale[0]; const int64_t zero_point = subgraph->tensors[tensor_idx]->quantization->zero_point[0]; // Add tensor for requantize operator. Scale is the existing scale and // zero point is shifted by +128. TFLITE_DCHECK_GE(zero_point, -128); TFLITE_DCHECK_LE(zero_point, 127); const string tailing_op_name = tensor->name; const string new_name_original_output = tensor->name + "_" + type_string; tensor->name = new_name_original_output; utils::MakeTensorWithQuantParam( tailing_op_name, tensor->shape, tensor->shape_signature, output_type, scale, zero_point + 128, &tailing_op_output); } const int32_t tailing_op_output_idx = subgraph->tensors.size(); subgraph->tensors.push_back(std::move(tailing_op_output)); // Create the tailing operation. std::unique_ptr tailing_op; if (output_type == TensorType_FLOAT32) { // Tailing Op is Dequantize Op. utils::MakeDequantizeOperator(model, &tailing_op, tensor_idx, tailing_op_output_idx); } else { // Tailing Op is Quantize Op that does requantization. utils::MakeQuantizeOperator(model, &tailing_op, tensor_idx, tailing_op_output_idx); } // Add the operator at the end of the model. subgraph->operators.push_back(std::move(tailing_op)); return tailing_op_output_idx; } return -1; } // Sets the input and output types to the provided types. Leading and // tailing operations will be added if needed. // For Float input and output, leading op is Quantize and tailing op is // Dequantize. // For Uint8 input and output, leading op is Quantize (uint8 to // int8, can be thought as "requant") and tailing op is also Quantize (int8 to // uint8, can be thought as "requant"). TfLiteStatus SetInputAndOutputTypes(ModelT* model, const TensorType& input_type, const TensorType& output_type, const TensorType& activations_type, bool handle_external_state, ErrorReporter* error_reporter) { for (int subgraph_idx = 0; subgraph_idx < model->subgraphs.size(); subgraph_idx++) { SubGraphT* subgraph = model->subgraphs.at(subgraph_idx).get(); SignatureDefT* signature_def = nullptr; for (const auto& sig_def : model->signature_defs) { if (sig_def->subgraph_index == subgraph_idx) { signature_def = sig_def.get(); break; } } for (int i = 0; i < subgraph->inputs.size(); ++i) { TensorT* tensor = subgraph->tensors[subgraph->inputs[i]].get(); // TODO(suharshs): Add support for this case if it ever comes up. if (tensor->type == TensorType_FLOAT32 && input_type != tensor->type) { TF_LITE_REPORT_ERROR( error_reporter, "Unsupported input type %s for input tensor %d of type %s.", EnumNameTensorType(input_type), subgraph->inputs[i], EnumNameTensorType(tensor->type)); return kTfLiteError; } // Keeps the state input tensors in their original type(same as activation // type). But changes the input type of input data/features. const int32_t input_idx = SetInputType( model, subgraph, subgraph->inputs[i], ((i > 0 && handle_external_state) ? activations_type : input_type), activations_type); if (input_idx < 0) { continue; } if (signature_def != nullptr) { for (const auto& input : signature_def->inputs) { if (input->tensor_index == subgraph->inputs[i]) { input->tensor_index = input_idx; break; } } } subgraph->inputs[i] = input_idx; } for (int i = 0; i < subgraph->outputs.size(); ++i) { TensorT* tensor = subgraph->tensors[subgraph->outputs[i]].get(); // TODO(suharshs): Add support for this case if it ever comes up. if (tensor->type == TensorType_FLOAT32 && output_type != tensor->type) { TF_LITE_REPORT_ERROR( error_reporter, "Unsupported output type %s for output tensor '%s' of type %s.", EnumNameTensorType(output_type), tensor->name.c_str(), EnumNameTensorType(tensor->type)); return kTfLiteError; } // Keeps the state output tensors in their original type. Avoids extra OP. const int32_t output_idx = SetOutputType( model, subgraph, subgraph->outputs[i], ((i > 0 && handle_external_state) ? activations_type : output_type), activations_type); if (output_idx < 0) { continue; } if (signature_def != nullptr) { for (const auto& output : signature_def->outputs) { if (output->tensor_index == subgraph->outputs[i]) { output->tensor_index = output_idx; break; } } } subgraph->outputs[i] = output_idx; } } return kTfLiteOk; } // Requantize a constant quantized tensor. template TfLiteStatus RequantizeConstant( const std::vector& buffer_data, const TensorT* tensor, const std::unique_ptr& new_quantization, std::vector& new_buffer_data) { if (new_buffer_data.size() != buffer_data.size()) { new_buffer_data.resize(buffer_data.size()); } const auto& quantization = tensor->quantization; const std::vector& scales = quantization->scale; if (scales.empty()) { // No existing quantization, assumes that new quantization parameters // are correct. new_buffer_data.assign(buffer_data.begin(), buffer_data.end()); return kTfLiteOk; } const std::vector& zero_points = quantization->zero_point; const int num_elements = buffer_data.size() / sizeof(TensorDataType); std::vector float_values(num_elements); const TensorDataType* buffer_values = reinterpret_cast(buffer_data.data()); // This logic is for per-channel quantization, but works for per-tensor. const int kPerChannelMaxDim = 4; const std::vector& tensor_shape = tensor->shape; RuntimeShape unextended_tensor_dims(tensor_shape.size(), tensor_shape.data()); RuntimeShape tensor_dims = RuntimeShape::ExtendedShape(kPerChannelMaxDim, unextended_tensor_dims); const int channel_dim_index = quantization->quantized_dimension + kPerChannelMaxDim - unextended_tensor_dims.DimensionsCount(); int indices[kPerChannelMaxDim]; for (indices[0] = 0; indices[0] < tensor_dims.Dims(0); indices[0]++) { for (indices[1] = 0; indices[1] < tensor_dims.Dims(1); indices[1]++) { for (indices[2] = 0; indices[2] < tensor_dims.Dims(2); indices[2]++) { for (indices[3] = 0; indices[3] < tensor_dims.Dims(3); indices[3]++) { const float scale = scales.size() > 1 ? scales[indices[channel_dim_index]] : scales[0]; const int64_t zp = zero_points.size() > 1 ? zero_points[indices[channel_dim_index]] : zero_points[0]; const int index = Offset(tensor_dims, indices); float_values[index] = scale * (buffer_values[index] - zp); } } } } // Only have to deal with per-tensor for new parameters. if (tensor->type == TensorType_INT16) { std::vector requant_int16 = utils::SymmetricQuantizeFloatsToInt16( float_values.data(), float_values.size(), new_quantization->scale[0]); uint8_t* uint8_buffer = reinterpret_cast(requant_int16.data()); new_buffer_data.assign(uint8_buffer, uint8_buffer + buffer_data.size()); return kTfLiteOk; } else if (tensor->type == TensorType_INT8) { const int32_t q_min = std::numeric_limits::min(); const int32_t q_max = std::numeric_limits::max(); const float scaling_factor = new_quantization->scale[0]; const int32_t zp = new_quantization->zero_point[0]; const auto& rescale = [&scaling_factor, &zp, &q_min, &q_max](float f) -> uint8_t { const float scaling_factor_inv = (scaling_factor == 0) ? 0 : 1.0 / scaling_factor; int32_t q_i32 = TfLiteRound(f * scaling_factor_inv) + zp; int8_t q = std::min(std::max(q_i32, q_min), q_max); return *(reinterpret_cast(&q)); }; std::transform(float_values.begin(), float_values.end(), new_buffer_data.begin(), rescale); return kTfLiteOk; } return kTfLiteError; } // Apply constraints to ops if they have any. // We have made the restriction that for int8 quantized concat, minimum, and // maximum, the inputs and outputs must have the same scale and zero point. // The other ones with constraints are handled in QuantizeWeightsAndInput. TfLiteStatus ApplyConstraints( ModelT* model, const std::unordered_set& operator_names, const std::unordered_set& real_value_op_set, TensorType activations_type, ErrorReporter* error_reporter) { for (int subgraph_idx = 0; subgraph_idx < model->subgraphs.size(); subgraph_idx++) { SubGraphT* subgraph = model->subgraphs.at(subgraph_idx).get(); // Iterate backward to avoid messing with index. for (int op_idx = subgraph->operators.size() - 1; op_idx >= 0; op_idx--) { OperatorT* op = subgraph->operators[op_idx].get(); if (op->outputs.empty()) { continue; } const string operator_name = subgraph->tensors[op->outputs[0]]->name; operator_property::OperatorProperty property = GetOperatorProperty(operator_names, model, subgraph_idx, op_idx, operator_name, activations_type); if (!property.quantizable || !IsRealValueOp(real_value_op_set, operator_name)) { continue; } TensorT* output_tensor = subgraph->tensors[op->outputs[0]].get(); if (!property.arbitrary_inputs || !property.restrict_same_input_output_scale(output_tensor->type)) { continue; } // If ApplyConstraints and requant is needed, use the min of min and max // of max, which means using the scale and zero point of output. if (!utils::QuantizationParametersExist(output_tensor)) { TF_LITE_REPORT_ERROR( error_reporter, "Unable to get scale or zero point from the tensor at %d.", op->outputs[0]); return kTfLiteError; } const float output_scale = output_tensor->quantization->scale[0]; const float output_zp = output_tensor->quantization->zero_point[0]; for (size_t input_idx = 0; input_idx < op->inputs.size(); ++input_idx) { TensorT* input_tensor = subgraph->tensors[op->inputs[input_idx]].get(); if (!utils::QuantizationParametersExist(input_tensor)) { TF_LITE_REPORT_ERROR( error_reporter, "Unable to get scale or zero point from tensor at %d.", op->inputs[input_idx]); return kTfLiteError; } if (input_tensor->quantization->scale[0] == output_scale && input_tensor->quantization->zero_point[0] == output_zp) { // This input does not need to be requantized. continue; } if (IsConstantWithOneUse(model, subgraph, op->inputs[input_idx])) { auto quantization = std::make_unique(); quantization->scale.push_back(output_scale); quantization->zero_point.push_back(output_zp); const std::vector& buffer_data = model->buffers[input_tensor->buffer]->data; std::vector new_buffer_data; TfLiteStatus requant_status = kTfLiteError; if (input_tensor->type == TensorType_INT8) { requant_status = RequantizeConstant( buffer_data, input_tensor, quantization, new_buffer_data); } else if (input_tensor->type == TensorType_INT16) { requant_status = RequantizeConstant( buffer_data, input_tensor, quantization, new_buffer_data); } if (requant_status == kTfLiteOk) { model->buffers[input_tensor->buffer]->data = new_buffer_data; input_tensor->quantization = std::move(quantization); continue; } else { quantization.release(); } } std::unique_ptr additional_tensor; const string requant_tensor_name = input_tensor->name + "_requantized"; utils::MakeTensorWithQuantParam( requant_tensor_name, input_tensor->shape, input_tensor->shape_signature, activations_type, output_scale, output_zp, &additional_tensor); const int32_t additional_tensor_idx = subgraph->tensors.size(); subgraph->tensors.push_back(std::move(additional_tensor)); // Add requant op before this input. // There are better ways to handle this, which is to try to push the // rescale upwards recursively and hope all upstream ops can absort // this rescale.and only add requant when there is no other way. std::unique_ptr requant_op; utils::MakeQuantizeOperator(model, &requant_op, op->inputs[input_idx], additional_tensor_idx); op->inputs[input_idx] = additional_tensor_idx; subgraph->operators.insert(subgraph->operators.begin() + op_idx, std::move(requant_op)); } } } return kTfLiteOk; } // In case of int16 activations, there are two implementations of kernels for // ADD/SUB operators. We set the builtin option pot_scale_int16 // during quantization so that from now only the general case implementation is // used. void SetOperatorPropertyADDSUBOperator(ModelT* model, const TensorType& activations_type) { if (activations_type != TensorType_INT16) { // This is needed only in case of int16 activations. return; } for (int subgraph_idx = 0, end = model->subgraphs.size(); subgraph_idx < end; subgraph_idx++) { SubGraphT* subgraph = model->subgraphs.at(subgraph_idx).get(); // Iterate backward to avoid messing with index. for (int op_idx = subgraph->operators.size() - 1; op_idx >= 0; op_idx--) { OperatorT* op = subgraph->operators[op_idx].get(); OperatorCodeT* op_code = model->operator_codes[op->opcode_index].get(); if (op_code && op_code->builtin_code == BuiltinOperator_ADD) { { auto* options = op->builtin_options.AsAddOptions(); if (options) { options->pot_scale_int16 = false; } } } if (op_code && op_code->builtin_code == BuiltinOperator_SUB) { { auto* options = op->builtin_options.AsSubOptions(); if (options) { options->pot_scale_int16 = false; } } } } } } std::vector> GetInputs( const OperatorT* op, operator_property::OperatorProperty property) { std::vector> inputs; if (property.arbitrary_inputs || !property.quantizable) { for (int i = 0; i < op->inputs.size(); ++i) { inputs.push_back({i, {}}); } } else { inputs = property.inputs; } return inputs; } std::vector> GetOutputs( const OperatorT* op, operator_property::OperatorProperty property) { std::vector> outputs; if (property.arbitrary_outputs) { for (int i = 0; i < op->outputs.size(); ++i) { outputs.push_back({i, {}}); } } else { outputs = property.outputs; } return outputs; } bool ShouldRestrictSameInputOutputScale( operator_property::OperatorProperty property, TensorType tensor_type) { // Ops with multiple inputs (i.e. concat, max and min) gets restricted in // ApplyConstraints. return (!property.arbitrary_inputs && property.restrict_same_input_output_scale(tensor_type)); } bool IsSubgraphInput(SubGraphT* subgraph, int32_t index) { for (const int32_t input_idx : subgraph->inputs) { if (index == input_idx) { return true; } } return false; } // Quantize the op input. Will increment op_idx if ops are added. TfLiteStatus QuantizeOpInput( ModelT* model, int32_t subgraph_idx, size_t* op_idx, operator_property::OperatorProperty property, const std::pair& input, const TensorType& activations_type, ErrorReporter* error_reporter) { int32_t input_idx = input.first; operator_property::TensorProperty tensor_property = input.second; SubGraphT* subgraph = model->subgraphs.at(subgraph_idx).get(); OperatorT* op = subgraph->operators[*op_idx].get(); const BuiltinOperator op_code = GetBuiltinCode(model->operator_codes[op->opcode_index].get()); if (input_idx >= op->inputs.size()) { TF_LITE_REPORT_ERROR( error_reporter, "Required input index %d is larger than the input length of op " "%s at index %d in subgraph %d", input_idx, op->inputs.size(), EnumNameBuiltinOperator(op_code), *op_idx, subgraph_idx); return kTfLiteError; } const int32_t tensor_idx = op->inputs[input_idx]; if (tensor_idx == -1) { // Skip optional tensor. return kTfLiteOk; } TensorT* tensor = subgraph->tensors[tensor_idx].get(); // Assumes if tensor is quantized, then it is a weight and quantized to 8 bit. const bool is_input_quantized = utils::QuantizationParametersExist(tensor); if (property.quantizable && !is_input_quantized) { // The operation is quantizable, but the input isn't yet quantized. if (utils::HasBuffer(model, subgraph, tensor_idx)) { // TODO(suharshs): Look at consumers, throw error if one consumer is // per-channel and one per-layer. bool quantize_const_input = false; if (activations_type == TensorType_INT16 && (property.restrict_same_input_output_scale(tensor->type) || property.quantize_input_as_activations)) { quantize_const_input = true; } if (tensor_property.number_of_bits == 8 && !quantize_const_input) { if (tensor_property.use_derived_scale) { // Currently 8bit tensors in input do not accept derived scale. return kTfLiteError; } if (utils::QuantizeWeight(model, tensor, tensor_property.per_axis, tensor_property.per_axis_index, error_reporter) != kTfLiteOk) { TF_LITE_REPORT_ERROR( error_reporter, "Unable to quantize buffer or min/max value for input %d " "in op %s in subgraph %d, node: %d", input_idx, EnumNameBuiltinOperator(op_code), subgraph_idx, *op_idx); return kTfLiteError; } } else if (tensor_property.number_of_bits == 16 || quantize_const_input) { if (tensor_property.use_derived_scale) { // Currently 16bit tensors in input do not accept derived scale. return kTfLiteError; } TensorT* tensor = subgraph->tensors[tensor_idx].get(); int total_size = 1; for (int i = 0; i < tensor->shape.size(); ++i) { total_size *= tensor->shape[i]; } BufferT* buffer = model->buffers[tensor->buffer].get(); float* float_data = reinterpret_cast(buffer->data.data()); auto minmax = std::minmax_element(float_data, float_data + total_size); const float min = *minmax.first; const float max = *minmax.second; const float range = std::max(std::abs(min), std::abs(max)); // The narrow range quantized value for int16. const float quantize_range = 32767.0; const float scale = range / quantize_range; return utils::SymmetricQuantizeFloatsToInt16(model, tensor, scale, error_reporter); } else if (tensor_property.number_of_bits == 32) { if (!tensor_property.use_derived_scale) { // Currently 32 bit tensors in input only accept derived scale. return kTfLiteError; } TensorT* tensor = subgraph->tensors[tensor_idx].get(); const float scale = utils::GetEffectiveScale( model, subgraph, *op_idx, tensor_property.derived_scale.input_tensors, tensor_property.derived_scale.intermediate_tensors, tensor_property.derived_scale.factors); return utils::SymmetricPerLayerBiasQuantize( model, tensor, scale, error_reporter); } else if (tensor_property.number_of_bits == 10) { // When the number of bits is 10 (instead of 16), quantize the tensor to // [-512, 512], instead of [-32767, 32767]. TensorT* tensor = subgraph->tensors[tensor_idx].get(); int total_size = 1; for (int i = 0; i < tensor->shape.size(); ++i) { total_size *= tensor->shape[i]; } BufferT* buffer = model->buffers[tensor->buffer].get(); float* buffer_data = reinterpret_cast(buffer->data.data()); auto minmax = std::minmax_element(buffer_data, buffer_data + total_size); const float range = std::max(std::abs(*minmax.first), std::abs(*minmax.second)); const float quantized_range = 512.0; const float scale = range / quantized_range; return utils::SymmetricQuantizeFloatsToInt16(model, tensor, scale, error_reporter); } else { // Currently supports only 8, 16, 32, 10 bits. TF_LITE_REPORT_ERROR( error_reporter, "Unable to quantize buffer or min/max value for input %d " "in op %s in subgraph %d, node: %d", input_idx, EnumNameBuiltinOperator(op_code), subgraph_idx, *op_idx); return kTfLiteError; } } else if (utils::HasMinMax(tensor)) { if (IsSubgraphInput(subgraph, tensor_idx) || tensor_property.state_tensor) { if (tensor_property.number_of_bits == 8) { if (tensor_property.use_derived_scale) { // Currently 8bit tensors in input do not accept derived scale. return kTfLiteError; } TF_LITE_ENSURE_STATUS(utils::QuantizeActivation( tensor, activations_type, error_reporter)); } else if (tensor_property.number_of_bits == 16) { TensorT* tensor = subgraph->tensors[tensor_idx].get(); float quantized_range = 32767.0; float range = std::max(std::abs(tensor->quantization->min[0]), std::abs(tensor->quantization->max[0])); if (tensor_property.extend_to_power_of_two) { const int power_of_two_scale = utils::GetPowerOfTwoScale( tensor->quantization->min[0], tensor->quantization->max[0]); range = std::pow(2, power_of_two_scale); // NOLINT quantized_range = 32768.0; } const float scale = range / quantized_range; utils::QuantizeActivationToInt16(tensor, scale); } } else { // If the tensor is not a model input, we need to add a Quantize // operation since the preceding op may require a float output. std::string type_string = activations_type == TensorType_INT16 ? "int16" : "int8"; std::unique_ptr op_output; utils::MakeTensor(tensor->name + "_" + type_string, tensor->shape, tensor->shape_signature, activations_type, &op_output); op_output->quantization = std::make_unique(); op_output->quantization->min.push_back(tensor->quantization->min[0]); op_output->quantization->max.push_back(tensor->quantization->max[0]); TF_LITE_ENSURE_STATUS(utils::QuantizeActivation( op_output.get(), activations_type, error_reporter)); const int32_t quant_op_output_idx = subgraph->tensors.size(); subgraph->tensors.push_back(std::move(op_output)); std::unique_ptr quant_op; utils::MakeQuantizeOperator(model, &quant_op, tensor_idx, quant_op_output_idx); subgraph->operators.insert(subgraph->operators.begin() + *op_idx, std::move(quant_op)); op->inputs[input_idx] = quant_op_output_idx; *op_idx += 1; } } else { TF_LITE_REPORT_ERROR(error_reporter, "Unable to find buffer or min/max value for input " "%d in %s in subgraph %d, node: %d", input_idx, EnumNameBuiltinOperator(op_code), subgraph_idx, *op_idx); return kTfLiteError; } } else if (!property.quantizable && is_input_quantized) { // If the tensor is quantized, we have to add a Dequantize op after // since this op is not quantizable. std::unique_ptr op_output; utils::MakeTensor(tensor->name + "_float", tensor->shape, tensor->shape_signature, TensorType_FLOAT32, &op_output); const int32_t dequant_op_output_idx = subgraph->tensors.size(); subgraph->tensors.push_back(std::move(op_output)); std::unique_ptr dequant_op; utils::MakeDequantizeOperator(model, &dequant_op, tensor_idx, dequant_op_output_idx); subgraph->operators.insert(subgraph->operators.begin() + *op_idx, std::move(dequant_op)); op->inputs[input_idx] = dequant_op_output_idx; *op_idx += 1; } return kTfLiteOk; } // Quantize the op output. TfLiteStatus QuantizeOpOutput( ModelT* model, int32_t subgraph_idx, int32_t op_idx, operator_property::OperatorProperty property, const std::pair& output, TensorType activations_type, ErrorReporter* error_reporter) { int32_t output_idx = output.first; operator_property::TensorProperty tensor_property = output.second; // If the operator is not quantizable, we don't need to do anything for the // output. if (!property.quantizable) { return kTfLiteOk; } SubGraphT* subgraph = model->subgraphs.at(subgraph_idx).get(); OperatorT* op = subgraph->operators[op_idx].get(); const BuiltinOperator op_code = GetBuiltinCode(model->operator_codes[op->opcode_index].get()); if (output_idx >= op->outputs.size()) { TF_LITE_REPORT_ERROR( error_reporter, "Required output index %d is larger than the output length of " "op %s at index %d in subgraph %d", output_idx, op->outputs.size(), EnumNameBuiltinOperator(op_code), op_idx, subgraph_idx); return kTfLiteError; } TensorT* output_tensor = subgraph->tensors[op->outputs[output_idx]].get(); if (utils::QuantizationParametersExist(output_tensor)) { // Skip output if it has been quantized. return kTfLiteOk; } if (ShouldRestrictSameInputOutputScale(property, output_tensor->type)) { // Copy quantization parameter. For average pool, max pool, etc // min/max can be different but we want them to be the same. // Get scale and zero point of input. if (property.inputs[0].first >= op->inputs.size()) { TF_LITE_REPORT_ERROR( error_reporter, "Required input index %d is larger than the input length of " "op %s at index %d in subgraph %d", property.inputs[0].first, op->inputs.size(), EnumNameBuiltinOperator(op_code), op_idx, subgraph_idx); return kTfLiteError; } const int input_tensor_idx = op->inputs[property.inputs[0].first]; TensorT* input_tensor = subgraph->tensors[input_tensor_idx].get(); if (input_tensor->quantization->scale.size() != 1 || input_tensor->quantization->zero_point.size() != 1) { TF_LITE_REPORT_ERROR(error_reporter, "Invalid quantization params for op %s at index %d " "in subgraph %d", EnumNameBuiltinOperator(op_code), op_idx, subgraph_idx); return kTfLiteError; } const float input_scale = input_tensor->quantization->scale[0]; const int32_t input_zero_point = input_tensor->quantization->zero_point[0]; // Apply to output. output_tensor->quantization = std::make_unique(); output_tensor->quantization->scale.push_back(input_scale); output_tensor->quantization->zero_point.push_back(input_zero_point); if (!input_tensor->quantization->min.empty()) { const float min = input_tensor->quantization->min[0]; output_tensor->quantization->min = {min}; } if (!input_tensor->quantization->max.empty()) { const float max = input_tensor->quantization->max[0]; output_tensor->quantization->max = {max}; } output_tensor->type = activations_type; } else if (tensor_property.restriction) { const auto scale_and_zp = activations_type == TensorType_INT16 ? tensor_property.restricted_value_int16 : tensor_property.restricted_value_int8; // Apply to output. output_tensor->quantization = std::make_unique(); output_tensor->quantization->scale.push_back(scale_and_zp.first); output_tensor->quantization->zero_point.push_back(scale_and_zp.second); output_tensor->type = activations_type; } else { // Process regular output that doesn't have any restrictions. if (utils::HasMinMax(output_tensor)) { utils::QuantizeActivation(output_tensor, activations_type, error_reporter); } else { TF_LITE_REPORT_ERROR( error_reporter, "Unable to find min/max value for output %d in %s in " "subgraph %d, node: %d", output_idx, EnumNameBuiltinOperator(op_code), subgraph_idx, op_idx); return kTfLiteError; } } return kTfLiteOk; } TfLiteStatus QuantizeIntermediateTensors(ModelT* model, TensorType activations_type, ErrorReporter* error_reporter) { for (size_t subgraph_idx = 0; subgraph_idx < model->subgraphs.size(); subgraph_idx++) { SubGraphT* subgraph = model->subgraphs.at(subgraph_idx).get(); for (size_t op_idx = 0; op_idx < subgraph->operators.size(); op_idx++) { operator_property::OperatorProperty property = operator_property::GetOperatorProperty(model, subgraph_idx, op_idx); if (!property.intermediates.empty()) { OperatorT* op = subgraph->operators[op_idx].get(); const BuiltinOperator op_code = GetBuiltinCode(model->operator_codes[op->opcode_index].get()); for (const std::pair& input : property.intermediates) { const int index_local = input.first; const int index_global = op->intermediates[index_local]; if (index_global == -1) { // Skip optional tensor. continue; } if (input.second.number_of_bits == 8 && input.second.symmetric == false) { TensorT* tensor = subgraph->tensors[index_global].get(); if (tensor->quantization == nullptr) { continue; } if (utils::HasMinMax(tensor)) { utils::QuantizeActivation(tensor, activations_type, error_reporter); } else { TF_LITE_REPORT_ERROR(error_reporter, "Unable to find min/max value for " "intermediate tensor %d in %s in " "subgraph %d, node: %d", index_local, EnumNameBuiltinOperator(op_code), subgraph_idx, op_idx); return kTfLiteError; } } else if (input.second.number_of_bits == 16 && input.second.symmetric == true) { TensorT* tensor = subgraph->tensors[index_global].get(); if (tensor->quantization == nullptr) { continue; } const float min = tensor->quantization->min[0]; const float max = tensor->quantization->max[0]; const float range = std::max(std::abs(min), std::abs(max)); if (range < 1e-8) { return kTfLiteError; } // Get scale and zero point. const float quantized_range = 32767.0; const float scale = range / quantized_range; utils::QuantizeActivationToInt16(tensor, scale); } else { return kTfLiteError; } } } } } return kTfLiteOk; } // Quantize tensors that have shared range. For example, in LSTM, the output // tensor and input state tensor should share the same range because they are // using the same scale and zero point. // We have to model this explicitly because the output is modeled as an extra // tensor in LSTM. In calibrator, state tensors are logged both before and after // the inference so the range is fully captured. But output, although it is // identical to activation, is not a state tensor the input value (range) of the // very first inference is not captured. TfLiteStatus QuantizeSharedRange(ModelT* model, ErrorReporter* error_reporter) { for (size_t subgraph_idx = 0; subgraph_idx < model->subgraphs.size(); subgraph_idx++) { SubGraphT* subgraph = model->subgraphs.at(subgraph_idx).get(); for (size_t op_idx = 0; op_idx < subgraph->operators.size(); op_idx++) { operator_property::OperatorProperty property = operator_property::GetOperatorProperty(model, subgraph_idx, op_idx); if (!property.intermediates.empty()) { OperatorT* op = subgraph->operators[op_idx].get(); for (const std::vector& input : property.restrict_scale) { if (input.empty()) { continue; } // Currently only support two values. The first one for input and // the second one for output. if (input.size() != 2) { return kTfLiteError; } const int index_1 = input[0]; const int index_2 = input[1]; TensorT* tensor_1 = subgraph->tensors[op->inputs[index_1]].get(); TensorT* tensor_2 = subgraph->tensors[op->outputs[index_2]].get(); const float min_of_min = std::min(tensor_1->quantization->min[0], tensor_2->quantization->min[0]); const float max_of_max = std::max(tensor_1->quantization->max[0], tensor_2->quantization->max[0]); if (min_of_min == 0.0 && max_of_max == 0.0) { return kTfLiteError; } // Asmmetric quantization to 8 bit. auto quantization_params = std::make_unique(); utils::GetAsymmetricQuantizationParams( min_of_min, max_of_max, -128, 127, quantization_params.get()); // Populate both tensors with the same parameters. const float scale = quantization_params->scale[0]; const int32 zero_point = quantization_params->zero_point[0]; for (TensorT* tensor : {tensor_1, tensor_2}) { tensor->quantization = std::make_unique(); tensor->quantization->scale.push_back(scale); tensor->quantization->zero_point.push_back(zero_point); tensor->type = TensorType_INT8; } } } } } return kTfLiteOk; } // Quantize a constant based on min/max quantization parameters for // resource assignments during initialization. Constant buffers should // have the same quantization parameters as assignments. TfLiteStatus QuantizeConstantVariable(ModelT* model, const TensorType& activations_type, TensorT* var_tensor, ErrorReporter* error_reporter) { if (activations_type == TensorType_INT16) { const float min = var_tensor->quantization->min[0]; const float max = var_tensor->quantization->max[0]; const float range = std::max(std::abs(min), std::abs(max)); const float quantize_range = 32767.0; const float scale = range / quantize_range; return utils::SymmetricQuantizeFloatsToInt16(model, var_tensor, scale, error_reporter); } else if (activations_type == TensorType_INT8) { TF_LITE_ENSURE_STATUS(utils::QuantizeActivation( var_tensor, activations_type, error_reporter)); QuantizationParametersT* quantization_params = var_tensor->quantization.get(); const float scaling_factor = quantization_params->scale[0]; const int zero_point = quantization_params->zero_point[0]; const BufferT* buffer = model->buffers[var_tensor->buffer].get(); const float* float_data = reinterpret_cast(buffer->data.data()); uint64_t num_elements; TF_LITE_ENSURE_STATUS(utils::NumElements(*var_tensor, &num_elements)); const float scaling_factor_inv = (scaling_factor == 0) ? 0 : 1.0 / scaling_factor; std::vector quantized(num_elements); const int32_t kMinScale = std::numeric_limits::min(); const int32_t kMaxScale = std::numeric_limits::max(); for (size_t i = 0; i < num_elements; i++) { const int32_t quantized_value = static_cast( TfLiteRound(float_data[i] * scaling_factor_inv) + zero_point); quantized[i] = std::min(kMaxScale, std::max(kMinScale, quantized_value)); } uint8_t* uint8_buffer = reinterpret_cast(quantized.data()); const size_t buffer_size = num_elements * sizeof(int8_t); model->buffers[var_tensor->buffer]->data.assign(uint8_buffer, uint8_buffer + buffer_size); return kTfLiteOk; } return kTfLiteError; } using TensorResourceMap = std::map, std::string>; using ResourceMinMaxMap = std::map>; // Find min of mins, max of maxes for each variable read or assignment. void PopulateResourceMinMaxMap(ModelT* model, TensorResourceMap& tensor_resource_map, ResourceMinMaxMap& resource_min_max_map) { for (size_t subgraph_idx = 0; subgraph_idx < model->subgraphs.size(); subgraph_idx++) { SubGraphT* subgraph = model->subgraphs.at(subgraph_idx).get(); for (size_t op_idx = 0; op_idx < subgraph->operators.size(); op_idx++) { OperatorT* op = subgraph->operators[op_idx].get(); const BuiltinOperator op_code = GetBuiltinCode(model->operator_codes[op->opcode_index].get()); if (op_code == BuiltinOperator_VAR_HANDLE) { const std::string& name = op->builtin_options.AsVarHandleOptions()->shared_name; resource_min_max_map.insert({name, {0.0, 0.0}}); tensor_resource_map.insert({{subgraph_idx, op->outputs[0]}, name}); } if ((op_code == BuiltinOperator_ASSIGN_VARIABLE) || (op_code == BuiltinOperator_READ_VARIABLE)) { if (tensor_resource_map.find({subgraph_idx, op->inputs[0]}) == tensor_resource_map.end()) { continue; } const std::string& name = tensor_resource_map[{subgraph_idx, op->inputs[0]}]; TensorT* var_tensor; if (op_code == BuiltinOperator_ASSIGN_VARIABLE) { var_tensor = subgraph->tensors[op->inputs[1]].get(); } else if (op_code == BuiltinOperator_READ_VARIABLE) { var_tensor = subgraph->tensors[op->outputs[0]].get(); } else { continue; } if (!var_tensor->quantization || var_tensor->quantization->min.empty() || var_tensor->quantization->max.empty()) { continue; } // resources are quantized per tensor. const float current_min = var_tensor->quantization->min[0]; const float current_max = var_tensor->quantization->max[0]; auto inserted = resource_min_max_map.insert({name, {current_min, current_max}}); if (!inserted.second) { resource_min_max_map[name] = { std::min(inserted.first->second.first, current_min), std::max(inserted.first->second.second, current_max)}; } } } } } // Quantize resource variables. Each resource read and assign should have // identical quantization parameters. TfLiteStatus QuantizeResources(ModelT* model, const TensorType& activations_type, ErrorReporter* error_reporter) { // Shared name is only stored in the var handle operator, use resoure name map // to map tensors to resource names. TensorResourceMap tensor_resource_map; ResourceMinMaxMap resource_min_max_map; PopulateResourceMinMaxMap(model, tensor_resource_map, resource_min_max_map); if (resource_min_max_map.empty()) { // No resources found, so this is OK. return kTfLiteOk; } // Update quantization parameters. for (size_t subgraph_idx = 0; subgraph_idx < model->subgraphs.size(); subgraph_idx++) { SubGraphT* subgraph = model->subgraphs.at(subgraph_idx).get(); for (size_t op_idx = 0; op_idx < subgraph->operators.size(); op_idx++) { OperatorT* op = subgraph->operators[op_idx].get(); const BuiltinOperator op_code = GetBuiltinCode(model->operator_codes[op->opcode_index].get()); if (op_code == BuiltinOperator_ASSIGN_VARIABLE || op_code == BuiltinOperator_READ_VARIABLE) { if (tensor_resource_map.find({subgraph_idx, op->inputs[0]}) == tensor_resource_map.end()) { continue; } const std::string& name = tensor_resource_map[{subgraph_idx, op->inputs[0]}]; TensorT* var_tensor = nullptr; bool is_constant_assign = false; if (op_code == BuiltinOperator_ASSIGN_VARIABLE) { var_tensor = subgraph->tensors[op->inputs[1]].get(); is_constant_assign = utils::HasBuffer(model, subgraph, op->inputs[1]); } else if (op_code == BuiltinOperator_READ_VARIABLE) { var_tensor = subgraph->tensors[op->outputs[0]].get(); } else { continue; } if (resource_min_max_map.find(name) == resource_min_max_map.end()) { continue; } if (!var_tensor->quantization) { var_tensor->quantization = std::make_unique(); var_tensor->quantization->min.push_back( resource_min_max_map[name].first); var_tensor->quantization->max.push_back( resource_min_max_map[name].second); } else { var_tensor->quantization->min[0] = resource_min_max_map[name].first; var_tensor->quantization->max[0] = resource_min_max_map[name].second; } if (!is_constant_assign) { continue; } if (QuantizeConstantVariable(model, activations_type, var_tensor, error_reporter) != kTfLiteOk) { TF_LITE_REPORT_ERROR( error_reporter, "Unable to quantize buffer or min/max value for assignment " "in op %s in subgraph %d, node: %d", EnumNameBuiltinOperator(op_code), subgraph_idx, op_idx); return kTfLiteError; } } } } return kTfLiteOk; } // Quantize inputs and weights. // Because of ops such as lstm, still need to do per op, instead of weights. TfLiteStatus QuantizeWeightsInputOutput( ModelT* model, bool allow_float, const std::unordered_set& operator_names, const std::unordered_set& real_value_op_set, const TensorType& activations_type, bool disable_per_channel, bool disable_per_channel_quantization_for_dense_layers, ErrorReporter* error_reporter) { // Flag to track unsupported ops. bool quantization_not_supported = false; // Loop over the graph and quantize ops. for (size_t subgraph_idx = 0; subgraph_idx < model->subgraphs.size(); subgraph_idx++) { SubGraphT* subgraph = model->subgraphs.at(subgraph_idx).get(); for (size_t op_idx = 0; op_idx < subgraph->operators.size(); op_idx++) { OperatorT* op = subgraph->operators[op_idx].get(); const BuiltinOperator op_code = GetBuiltinCode(model->operator_codes[op->opcode_index].get()); if (op->outputs.empty() && op_code != BuiltinOperator_ASSIGN_VARIABLE) { continue; } const string operator_name = op_code != BuiltinOperator_ASSIGN_VARIABLE ? subgraph->tensors[op->outputs[0]]->name : subgraph->tensors[op->inputs[0]]->name; operator_property::OperatorProperty property = GetOperatorProperty( operator_names, model, subgraph_idx, op_idx, operator_name, activations_type, disable_per_channel, disable_per_channel_quantization_for_dense_layers); if (!IsRealValueOp(real_value_op_set, operator_name)) { continue; } if (activations_type == TensorType_INT16 && !property.quantizable && !allow_float) { TF_LITE_REPORT_ERROR( error_reporter, "Quantization to 16x8-bit not yet supported for op: '%s'.\n", EnumNameBuiltinOperator(op_code)); quantization_not_supported = true; } else if (!property.quantizable && !allow_float) { if (op_code == BuiltinOperator_DEQUANTIZE && std::find(subgraph->outputs.begin(), subgraph->outputs.end(), op->outputs[0]) != subgraph->outputs.end()) { continue; } TF_LITE_REPORT_ERROR(error_reporter, "Quantization not yet supported for op: '%s'.\n", EnumNameBuiltinOperator(op_code)); quantization_not_supported = true; } // Quantize operator inputs/weights. for (const std::pair& input : GetInputs(op, property)) { TF_LITE_ENSURE_STATUS(QuantizeOpInput(model, subgraph_idx, &op_idx, property, input, activations_type, error_reporter)); } // Quantize operator outputs. for (const std::pair& output : GetOutputs(op, property)) { TF_LITE_ENSURE_STATUS( QuantizeOpOutput(model, subgraph_idx, op_idx, property, output, activations_type, error_reporter)); } } } // Return; emit errors if there are any. if (quantization_not_supported) { return kTfLiteError; } return kTfLiteOk; } // Quantize bias. TfLiteStatus QuantizeBiases( ModelT* model, const std::unordered_set& operator_names, const std::unordered_set& real_value_op_set, const TensorType& activations_type, const TensorType& bias_type, bool disable_per_channel, bool disable_per_channel_quantization_for_dense_layers, ErrorReporter* error_reporter) { for (size_t subgraph_idx = 0; subgraph_idx < model->subgraphs.size(); subgraph_idx++) { SubGraphT* subgraph = model->subgraphs.at(subgraph_idx).get(); for (size_t op_idx = 0; op_idx < subgraph->operators.size(); op_idx++) { OperatorT* op = subgraph->operators[op_idx].get(); const BuiltinOperator op_code = GetBuiltinCode(model->operator_codes[op->opcode_index].get()); if (op->outputs.empty()) { continue; } const string operator_name = subgraph->tensors[op->outputs[0]]->name; operator_property::OperatorProperty property = GetOperatorProperty( operator_names, model, subgraph_idx, op_idx, operator_name, activations_type, disable_per_channel, disable_per_channel_quantization_for_dense_layers); if (!property.quantizable || !IsRealValueOp(real_value_op_set, operator_name)) { continue; } for (const int bias_idx : property.biases) { if (bias_idx >= op->inputs.size() || op->inputs[bias_idx] == kTfLiteOptionalTensor) { continue; } // Quantize if it is not quantized already as the // output of another op or input of another op. TensorT* bias_tensor = subgraph->tensors[op->inputs[bias_idx]].get(); if (!utils::QuantizationParametersExist(bias_tensor)) { if (utils::HasBuffer(model, subgraph, op->inputs[bias_idx])) { if (property.inputs.size() != 2) { TF_LITE_REPORT_ERROR(error_reporter, "Expect the input length of " "op %s at index %d in subgraph %d to be 2", bias_idx, op->inputs.size(), EnumNameBuiltinOperator(op_code), op_idx, subgraph_idx); return kTfLiteError; } const int32_t input_tensor_idx = op->inputs[property.inputs[0].first]; TensorT* input_tensor = subgraph->tensors[input_tensor_idx].get(); const int32_t weight_tensor_idx = op->inputs[property.inputs[1].first]; TensorT* weight_tensor = subgraph->tensors[weight_tensor_idx].get(); operator_property::TensorProperty weight_property = property.inputs[1].second; const bool per_axis = weight_property.per_axis && utils::HasBuffer(model, subgraph, weight_tensor_idx); TF_LITE_ENSURE_STATUS(QuantizeBias( model, input_tensor, weight_tensor, bias_tensor, per_axis, weight_property.per_axis_index, bias_type, error_reporter)); } } else { // If bias is already quantized, make sure it is quantized to 32 bit. if (bias_tensor->type != TensorType_INT32) { TF_LITE_REPORT_ERROR( error_reporter, "Bias (\"%s\" at global index %d) of op \"%s\" at op_index %d " "in subgraph %d is expected to be quantized to INT32 but it is " "already quantized to %s.\n", bias_tensor->name.c_str(), op->inputs[bias_idx], operator_name.c_str(), op_idx, subgraph_idx, EnumNameTensorType(bias_tensor->type)); } } } } } return kTfLiteOk; } std::unordered_set GetAllOperatorOutputs(ModelT* model) { std::unordered_set operator_names; for (int32_t subgraph_idx = 0; subgraph_idx < model->subgraphs.size(); subgraph_idx++) { SubGraphT* subgraph = model->subgraphs.at(subgraph_idx).get(); for (int32_t tensor_idx = 0; tensor_idx < subgraph->tensors.size(); tensor_idx++) { operator_names.insert(subgraph->tensors[tensor_idx]->name); } } return operator_names; } // Populate the quantization parameters max and min for input tensors. // Assumes that dynamic tensors already have stored min, max values and throw // an error if a tensor does not have min, max quantization parameter or a // buffer. // If any static tensors are not inputs to an operation, their max, min values // will not be filled by this function. TfLiteStatus FillQuantizationParams( ModelT* model, const std::unordered_set& operator_names, const std::unordered_set& real_value_op_set, const TensorType& activations_type, bool disable_per_channel, bool disable_per_channel_quantization_for_dense_layers, ErrorReporter* error_reporter) { for (size_t subgraph_idx = 0; subgraph_idx < model->subgraphs.size(); subgraph_idx++) { SubGraphT* subgraph = model->subgraphs.at(subgraph_idx).get(); for (size_t op_idx = 0; op_idx < subgraph->operators.size(); op_idx++) { OperatorT* op = subgraph->operators[op_idx].get(); operator_property::OperatorProperty property = operator_property::GetOperatorProperty(model, subgraph_idx, op_idx); if (!property.quantizable) { continue; } if (!op->outputs.empty()) { const string operator_name = subgraph->tensors[op->outputs[0]]->name; property = GetOperatorProperty( operator_names, model, subgraph_idx, op_idx, operator_name, activations_type, disable_per_channel, disable_per_channel_quantization_for_dense_layers); if (!IsRealValueOp(real_value_op_set, operator_name)) { continue; } } // Populate max, min for each input tensor. for (const std::pair& input : property.inputs) { // Get tensor. const int32_t input_idx = input.first; const int32_t tensor_idx = op->inputs[input_idx]; if (tensor_idx == -1) { // Skip optional tensor. continue; } TensorT* tensor = subgraph->tensors[tensor_idx].get(); // Static tensor. if (!utils::HasMinMax(tensor) && utils::HasBuffer(model, subgraph, tensor_idx)) { // Get input float data and tensor dimensions. const BufferT* buffer = model->buffers[tensor->buffer].get(); const float* float_input_data = reinterpret_cast(buffer->data.data()); if (tensor->quantization == nullptr) { tensor->quantization = std::make_unique(); } // Fill per channel max and min with respect to channel_dim_index. if (input.second.per_axis) { int32_t channel_dim_index = input.second.per_axis_index; TF_LITE_ENSURE_STATUS(utils::FillPerChannelMinMax( float_input_data, tensor->shape, channel_dim_index, tensor->quantization.get(), error_reporter)); // Fill per layer max and min. } else if (!utils::HasMinMax(tensor) && !input.second.per_axis && utils::HasBuffer(model, subgraph, tensor_idx)) { uint64_t input_size; TF_LITE_ENSURE_STATUS(utils::NumElements(*tensor, &input_size)); utils::FillSingleMinMax(float_input_data, input_size, tensor->quantization.get()); } if (tensor->quantization->quantized_dimension != input.second.per_axis_index) { TF_LITE_REPORT_ERROR( error_reporter, "Quantized dimension for tensor property and quantization " "parameters do not match. Got %d and %d respectively.", input.second.per_axis_index, tensor->quantization->quantized_dimension); return kTfLiteError; } // Dynamic tensor. } else if (!utils::HasMinMax(tensor) && !utils::HasBuffer(model, subgraph, tensor_idx)) { TF_LITE_REPORT_ERROR( error_reporter, "Max and min for dynamic tensors should be" " recorded during calibration: Failed for tensor %s\n", tensor->name.c_str()); if (tensor->quantization == nullptr) { TF_LITE_REPORT_ERROR(error_reporter, "No quantization params for tensor %s", tensor->name.c_str()); } else if (tensor->quantization->min.empty() || tensor->quantization->max.empty()) { TF_LITE_REPORT_ERROR(error_reporter, "Empty min/max for tensor %s", tensor->name.c_str()); } return kTfLiteError; } if (utils::QuantizationParametersExist(tensor)) { TF_LITE_REPORT_ERROR( error_reporter, "Scale and zero points should not be recorded before " "quantization."); return kTfLiteError; } } // loop over op inputs } // loop over ops } // loop over subgraphs return kTfLiteOk; } // Check compatibility of activation, weight and bias scales. Adjust if needed. TfLiteStatus EnsureBiasScaleCompatibility( ModelT* model, const std::unordered_set& operator_names, const std::unordered_set& real_value_op_set, const TensorType& activations_type, bool disable_per_channel, bool disable_per_channel_quantization_for_dense_layers, ErrorReporter* error_reporter) { for (size_t subgraph_idx = 0; subgraph_idx < model->subgraphs.size(); subgraph_idx++) { SubGraphT* subgraph = model->subgraphs.at(subgraph_idx).get(); for (size_t op_idx = 0; op_idx < subgraph->operators.size(); op_idx++) { OperatorT* op = subgraph->operators[op_idx].get(); if (op->outputs.empty()) { continue; } const string operator_name = subgraph->tensors[op->outputs[0]]->name; operator_property::OperatorProperty property = GetOperatorProperty( operator_names, model, subgraph_idx, op_idx, operator_name, activations_type, disable_per_channel, disable_per_channel_quantization_for_dense_layers); if (!IsRealValueOp(real_value_op_set, operator_name)) { continue; } // Loop over all bias tensors. for (const int bias_idx : property.biases) { if (bias_idx >= op->inputs.size() || op->inputs[bias_idx] == kTfLiteOptionalTensor) { continue; } TensorT* bias_tensor = subgraph->tensors[op->inputs[bias_idx]].get(); int32_t channel_dim_size = bias_tensor->shape[0]; if (bias_tensor->shape.size() != 1) { TF_LITE_REPORT_ERROR(error_reporter, "Expected bias tensor to be a vector."); return kTfLiteError; } if (property.inputs.size() != 2) { // Only works for two input tensors. TF_LITE_REPORT_ERROR( error_reporter, "Expect %d inputs for op %s at index %d in subgraph %d to be 2", property.inputs.size(), op_idx, subgraph_idx); return kTfLiteError; } if (!property.arbitrary_inputs && property.quantizable) { // Get input and weight tensors. const int32_t input_tensor_idx = op->inputs[property.inputs[0].first]; TensorT* input_tensor = subgraph->tensors[input_tensor_idx].get(); const int32_t weight_tensor_idx = op->inputs[property.inputs[1].first]; TensorT* weight_tensor = subgraph->tensors[weight_tensor_idx].get(); operator_property::TensorProperty weight_property = property.inputs[1].second; TF_LITE_ENSURE(error_reporter, input_tensor->quantization); // Check quantization parameters exist for input. if (!utils::HasMinMax(input_tensor)) { TF_LITE_REPORT_ERROR( error_reporter, "Input tensor missing quantization information. Should be " "populated during calibration."); return kTfLiteError; } // Get input scale for asymmetric quantization. QuantizationParametersT temp_quant_params = QuantizationParametersT(); TF_LITE_ENSURE_STATUS( utils::GetQuantizationParams(input_tensor, activations_type, &temp_quant_params, error_reporter)); if (temp_quant_params.scale.size() != 1) { TF_LITE_REPORT_ERROR(error_reporter, "Unexpected input quantization scale size."); return kTfLiteError; } float input_scale = temp_quant_params.scale[0]; // Check that max/min values have been filled for weights. if (!utils::HasMinMax(weight_tensor)) { TF_LITE_REPORT_ERROR( error_reporter, "Min and/or max values have not been recorded for weight " "tensor. This should have happened in FillQuantizationParams."); return kTfLiteError; } // Ensure the tensor dimensions are compatible. if (weight_property.per_axis && utils::HasBuffer(model, subgraph, weight_tensor_idx)) { if (bias_tensor->shape[0] != weight_tensor->shape[weight_property.per_axis_index]) { TF_LITE_REPORT_ERROR( error_reporter, "Channel mismatch between bias and weight tensors %d vs %d", bias_tensor->shape[0], weight_tensor->shape[weight_property.per_axis_index]); return kTfLiteError; } // Ensure that the number of max/mins matches the channel_dim_size. if (weight_tensor->quantization->max.size() != channel_dim_size) { TF_LITE_REPORT_ERROR( error_reporter, "Mismatch between number of weight maxs and channels: %d vs " "%d", weight_tensor->quantization->max.size(), channel_dim_size); return kTfLiteError; } if (weight_tensor->quantization->min.size() != channel_dim_size) { TF_LITE_REPORT_ERROR( error_reporter, "Mismatch between number of weight mins and channels: %d", weight_tensor->quantization->min.size()); return kTfLiteError; } } // Get data and size of bias tensor. const BufferT* buffer = model->buffers[bias_tensor->buffer].get(); const float* bias_data = reinterpret_cast(buffer->data.data()); uint64_t bias_size; TF_LITE_ENSURE_STATUS(utils::NumElements(*bias_tensor, &bias_size)); // Adjust weight scales if needed. TF_LITE_ENSURE_STATUS(utils::AdjustWeightsForBiasScale( weight_tensor->quantization.get(), bias_data, bias_size, input_scale, error_reporter)); if (utils::QuantizationParametersExist(weight_tensor)) { TF_LITE_REPORT_ERROR( error_reporter, "Scale and zero points should not be recorded for the weight " "tensor before quantization."); return kTfLiteError; } if (utils::QuantizationParametersExist(input_tensor)) { TF_LITE_REPORT_ERROR( error_reporter, "Scale and zero points should not be recorded for the input " "tensor before quantization."); return kTfLiteError; } } } } } return kTfLiteOk; } } // namespace // Assumes that the operators in the model have been topologically sorted. TfLiteStatus QuantizeModel( flatbuffers::FlatBufferBuilder* builder, ModelT* model, const TensorType& input_type, const TensorType& output_type, bool allow_float, const std::unordered_set& operator_names, const TensorType& activations_type, const TensorType& bias_type, bool disable_per_channel, bool disable_per_channel_quantization_for_dense_layers, ErrorReporter* error_reporter, bool handle_external_state = false) { auto real_value_op_set = PopulateRealValueOpSet(model, operator_names, activations_type); TF_LITE_ENSURE_STATUS(DuplicateBiasesWithMultipleUses(model, error_reporter)); TF_LITE_ENSURE_STATUS(FillQuantizationParams( model, operator_names, real_value_op_set, activations_type, disable_per_channel, disable_per_channel_quantization_for_dense_layers, error_reporter)); TF_LITE_ENSURE_STATUS(EnsureBiasScaleCompatibility( model, operator_names, real_value_op_set, activations_type, disable_per_channel, disable_per_channel_quantization_for_dense_layers, error_reporter)); TF_LITE_ENSURE_STATUS( QuantizeIntermediateTensors(model, activations_type, error_reporter)); TF_LITE_ENSURE_STATUS(QuantizeSharedRange(model, error_reporter)); TF_LITE_ENSURE_STATUS( QuantizeResources(model, activations_type, error_reporter)); TF_LITE_ENSURE_STATUS(QuantizeWeightsInputOutput( model, allow_float, operator_names, real_value_op_set, activations_type, disable_per_channel, disable_per_channel_quantization_for_dense_layers, error_reporter)); TF_LITE_ENSURE_STATUS(ApplyConstraints(model, operator_names, real_value_op_set, activations_type, error_reporter)); SetOperatorPropertyBiasType(model, bias_type); TF_LITE_ENSURE_STATUS(QuantizeBiases( model, operator_names, real_value_op_set, activations_type, bias_type, disable_per_channel, disable_per_channel_quantization_for_dense_layers, error_reporter)); utils::SetOperatorCodeVersion(model); TF_LITE_ENSURE_STATUS( SetInputAndOutputTypes(model, input_type, output_type, activations_type, handle_external_state, error_reporter)); SetOperatorPropertyADDSUBOperator(model, activations_type); flatbuffers::Offset output_model_location = Model::Pack(*builder, model); FinishModelBuffer(*builder, output_model_location); return kTfLiteOk; } // Assumes that the operators in the model have been topologically sorted. TfLiteStatus QuantizeModel(flatbuffers::FlatBufferBuilder* builder, ModelT* model, const TensorType& input_type, const TensorType& output_type, bool allow_float, const std::unordered_set& operator_names, const TensorType& activations_type, const TensorType& bias_type, ErrorReporter* error_reporter) { return QuantizeModel( builder, model, input_type, output_type, allow_float, operator_names, activations_type, /*bias_type=*/bias_type, /*disable_per_channel=*/false, /*disable_per_channel_quantization_for_dense_layers=*/false, error_reporter); } TfLiteStatus QuantizeModelAllOperators( flatbuffers::FlatBufferBuilder* builder, ModelT* model, const TensorType& input_type, const TensorType& output_type, bool allow_float, const TensorType& activations_type, const TensorType& bias_type, ErrorReporter* error_reporter) { return QuantizeModel( builder, model, input_type, output_type, allow_float, GetAllOperatorOutputs(model), activations_type, bias_type, /*disable_per_channel=*/false, /*disable_per_channel_quantization_for_dense_layers=*/false, error_reporter); } TfLiteStatus QuantizeModelAllOperators( flatbuffers::FlatBufferBuilder* builder, ModelT* model, const TensorType& input_type, const TensorType& output_type, bool allow_float, const TensorType& activations_type, const TensorType& bias_type, bool disable_per_channel, bool disable_per_channel_quantization_for_dense_layers, ErrorReporter* error_reporter) { return QuantizeModel(builder, model, input_type, output_type, allow_float, GetAllOperatorOutputs(model), activations_type, bias_type, disable_per_channel, disable_per_channel_quantization_for_dense_layers, error_reporter); } TfLiteStatus QuantizeModel(flatbuffers::FlatBufferBuilder* builder, ModelT* model, const TensorType& input_type, const TensorType& output_type, bool allow_float, ErrorReporter* error_reporter) { return QuantizeModel(builder, model, input_type, output_type, allow_float, GetAllOperatorOutputs(model), /*activations_type=*/TensorType_INT8, /*bias_type=*/TensorType_INT32, error_reporter); } TfLiteStatus QuantizeModel( flatbuffers::FlatBufferBuilder* builder, ModelT* model, const TensorType& input_type, const TensorType& output_type, bool allow_float, bool disable_per_channel, bool disable_per_channel_quantization_for_dense_layers, ErrorReporter* error_reporter) { return QuantizeModel(builder, model, input_type, output_type, allow_float, GetAllOperatorOutputs(model), /*activations_type=*/TensorType_INT8, /*bias_type=*/TensorType_INT32, /*disable_per_channel=*/disable_per_channel, /*disable_per_channel_quantization_for_dense_layers=*/ disable_per_channel_quantization_for_dense_layers, error_reporter); } TfLiteStatus QuantizeModel( flatbuffers::FlatBufferBuilder* builder, ModelT* model, const TensorType& input_type, const TensorType& output_type, bool allow_float, bool disable_per_channel, bool disable_per_channel_quantization_for_dense_layers, ErrorReporter* error_reporter, bool handle_external_state) { return QuantizeModel(builder, model, input_type, output_type, allow_float, GetAllOperatorOutputs(model), /*activations_type=*/TensorType_INT8, /*bias_type=*/TensorType_INT32, /*disable_per_channel=*/disable_per_channel, /*disable_per_channel_quantization_for_dense_layers=*/ disable_per_channel_quantization_for_dense_layers, error_reporter, handle_external_state); } TfLiteStatus QuantizeModel(flatbuffers::FlatBufferBuilder* builder, ModelT* model, const TensorType& input_type, const TensorType& output_type, ErrorReporter* error_reporter) { return QuantizeModel(builder, model, input_type, output_type, /*allow_float=*/false, error_reporter); } TfLiteStatus QuantizeModel(flatbuffers::FlatBufferBuilder* builder, ModelT* model, ErrorReporter* error_reporter) { return QuantizeModel(builder, model, TensorType_FLOAT32, TensorType_FLOAT32, /*allow_float=*/false, error_reporter); } } // namespace optimize } // namespace tflite