chore: import upstream snapshot with attribution
Docker Image CI / build-ubuntu2004 (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:36:55 +08:00
commit c8a779b1bb
1887 changed files with 3245738 additions and 0 deletions
@@ -0,0 +1,37 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
#
G_NUM_BITS: int = 8
G_NARROW_RANGE: bool = True
G_SYMMETRIC: bool = True
from tensorflow_quantization.quantize import QuantizationSpec
from tensorflow_quantization.quantize import quantize_model
from tensorflow_quantization.quantize_wrapper_base import BaseQuantizeWrapper
from tensorflow_quantization.quantize_wrappers import WeightedBaseQuantizeWrapper
from tensorflow_quantization.quantize_wrappers import NonWeightedBaseQuantizeWrapper
from tensorflow_quantization.custom_qdq_case_base import CustomQDQInsertionCase
from tensorflow_quantization.utils import CreateAssetsFolders
from tensorflow_quantization.utils import convert_saved_model_to_onnx
from tensorflow_quantization.utils import convert_keras_model_to_onnx
from .version import __version__
@@ -0,0 +1,48 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
#
from abc import ABC
class CustomQDQInsertionCase(ABC):
"""
This class helps user to programatically decide toolkit behavior to quantize specific layers.
Based on the output of this class 'case' function, toolkit deviates from its standard behavior.
"""
def info(self) -> str:
return ""
def case(
self, keras_model: "tf.keras.Model", qspec: "QuantizationSpec"
) -> "QuantizationSpec":
"""
This function is called internally by the framework.
Given keras model is passed as an argument and object of QuantizationSpec class
is expcted in return.
Returned QuantzaionSpec class object should contain information about the layers that needs
to be treated specially/differently from default framework behavior.
Args:
keras_model (tf.keras.Model): Keras functional or sequentail model
qspec (QuantizationSpec): User passed QuantizationSpec object. It is important to note that
new special qdq might or might not use quantizations specs user has provided.
Returns:
A new QuantizationSpec object.
"""
raise NotImplementedError("case method must be overridden by user")
@@ -0,0 +1,363 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
#
from tensorflow_quantization import CustomQDQInsertionCase
from tensorflow_quantization import QuantizationSpec
from tensorflow_quantization import utils
import tensorflow as tf
from typing import List
def is_parent_type(parent_class: str, class_type="Conv") -> bool:
"""
Checks if 'parent_class' is of type 'type'.
Examples of types: Conv, BatchNorm, Dropout, Activation.
"""
return class_type in parent_class
def is_parent_pattern(parent_info: dict, pattern: List = ["BatchNorm", "Conv"]) -> bool:
""" Checks if parent heritage follows a specific 'pattern'.
Args:
parent_info (dict): dictionary with parent's information.
pattern (List): list containing a layer's parental heritage ([parent, grandparent, great-grandparent, ...]).
Returns:
bool: indicating whether a layer's parent heritage follows the given pattern.
"""
grandparent_info = parent_info
for i, p in enumerate(pattern):
if i > 0:
grandparent_info = utils._get_previous_layers_class_and_module_and_name(
grandparent_info["layer"]
)[0]
if not is_parent_type(grandparent_info["class"], class_type=p):
return False
return True
def check_is_quantizable_by_layer_name(
qspec: QuantizationSpec, current_layer_name: str
) -> bool:
"""
Checks if 'current_layer_name' is in 'qspec'. It returns True if 'current_layer_name' is NOT in 'qspec' and
False if it is. This means that the user's request will get prioritized over our automatic methods.
Args:
qspec (QuantizationSpec): quantization specification.
Returns:
is_quantizable_by_layer_name (bool): boolean indicating whether 'current_layer_name' is quantizable by our
method (is NOT in 'qspec'), or not (is in 'qspec', so that configuration should be followed).
"""
def _is_layer_in_user_passed_qspec(layer_name):
for l in qspec.layers:
if l.name == layer_name:
return True
return False
is_quantizable_by_layer_name = qspec is None or (
qspec is not None and not _is_layer_in_user_passed_qspec(current_layer_name)
)
return is_quantizable_by_layer_name
###################################################################
################# General Custom QDQ Cases ########################
###################################################################
class BNQDQCase(CustomQDQInsertionCase):
def __init__(self) -> None:
super().__init__()
def info(self) -> str:
return "Avoids inserting QDQ before BatchNorm in cases where BN is connected to a Conv layer (since that BN " \
"will be fused with previous Conv layer). This case happens in ResNet-v2, where the following pattern " \
"exists: BN-ReLU-Conv blocks (pre-activation function). In that scenario, BN is sometimes connected to " \
"`Add` layer, which doesn't fuse with BN."
def case(
self, keras_model: tf.keras.Model, qspec: QuantizationSpec
) -> QuantizationSpec:
def _check_if_quantizable_bn(layer):
layer_parent = layer.input._keras_history.layer
parent_class_name = layer_parent.__class__.__name__
if not is_parent_type(parent_class_name, class_type="Conv"):
if check_is_quantizable_by_layer_name(qspec, layer.name):
return True
return False
bn_qspec = QuantizationSpec()
for layer in keras_model.layers:
if isinstance(layer, tf.keras.layers.BatchNormalization):
"""
Returns quantizable BatchNorm layers: All BN layers that are not connected to a Conv layer.
In other words, don't add QDQ to BN layers in a Conv-BN sequence (and of course, if it shouldn't be
ignored due to the user's preference)."
"""
if _check_if_quantizable_bn(layer):
bn_qspec.add(
name=layer.name, quantize_input=True, quantize_weight=False
)
return bn_qspec
class ResidualConnectionQDQCase(CustomQDQInsertionCase):
def __init__(self) -> None:
super().__init__()
def info(self) -> str:
info_str = "Goal: To return all quantizable residual inputs. " \
"Rules: Residual connection is represented by the Add layer. The recommendation from the TRT team " \
" is to add QDQ to all of its inputs except when: " \
" - the input is Bias. Note that TF sees MatMul+BiasAdd as a Dense layer, so no need to check " \
" if the input is Bias. " \
" - in the case of one of the inputs being a simple residual branch and the other Conv or " \
" Conv+BN, add QDQ nodes to just the residual branch. This is needed to trigger an INT8 " \
" kernel fusion with Add. " \
" - in the case of more than one input being Conv or Conv+BN, add QDQ to all inputs except 1. " \
" The last 2 cases are needed to trigger an INT8 kernel fusion with Add. " \
" [ResNet-v1]: Note that the connection between Conv2D and Add layer is not direct: " \
" Conv2D -> BatchNormalization -> Add " \
" To get to the layer, we need to access `input._keras_history.layer` " \
" This is the same for EfficientNet-B0. " \
" [ResNet-v2]: Connection is direct ReLU -> Conv2D -> Add " \
" [EfficientNet-B0]: Contains two special patterns: " \
" 1. Conv -> BatchNorm -> Activation -> Add " \
" 2. Conv -> BatchNorm -> Activation -> Dropout -> Add"
return info_str
def case(
self, keras_model: tf.keras.Model, qspec: QuantizationSpec
) -> QuantizationSpec:
res_qspec = QuantizationSpec()
for layer in keras_model.layers:
if isinstance(layer, tf.keras.layers.Add) and check_is_quantizable_by_layer_name(qspec, layer.name):
"""
Returns quantizable inputs to Add layers: all inputs except 1 with 'pattern'.
Patterns checked for: Conv, Conv-BN, Conv-BN-Activation, Conv-BN-Activation-Dropout.
"""
layer_parents = utils.find_my_predecessors(keras_model, layer.name)
# Collect the non-quantizable input (1 branch with Conv pattern)
input_indices_convs = []
for i, l_parent_info in enumerate(layer_parents):
l_parent_class = l_parent_info["class"]
l_parent_layer = l_parent_info["layer"]
# Check that the input is a Conv pattern
if (
is_parent_type(l_parent_class, class_type="Conv")
or is_parent_pattern(l_parent_info, pattern=["BatchNorm", "Conv"])
or is_parent_pattern(l_parent_info, pattern=["Activation", "BatchNorm", "Conv"])
or is_parent_pattern(l_parent_info, pattern=["Dropout", "Activation", "BatchNorm", "Conv"])
):
# Check that it's not a residual branch (input does not have more than 1 outbound node)
if hasattr(l_parent_layer, 'outbound_nodes'):
num_outbound_nodes = len(getattr(l_parent_layer, 'outbound_nodes'))
if num_outbound_nodes == 1:
# Branch without QDQ branch is chosen
input_indices_convs.append(i)
break
# Default behavior: add QDQ in all inputs except 1 with Conv/BN
input_indices = list(range(0, len(layer_parents)))
if len(input_indices_convs) > 0:
# Don't quantize one of the Conv pattern branches.
index_to_delete = input_indices_convs[-1]
del input_indices[index_to_delete]
if len(input_indices) > 0:
res_qspec.add(
layer.name,
quantize_input=True,
quantize_weight=False,
quantization_index=input_indices,
)
return res_qspec
class MaxPoolQDQCase(CustomQDQInsertionCase):
def __init__(self) -> None:
super().__init__()
def info(self) -> str:
return "Enables quantization of MaxPool layers. This is needed in cases where MaxPool is added to a residual " \
"connection and where the other branches are already quantized (needed to trigger a horizontal fusion " \
"in the residual connection. This case happens in ResNet-v2."
def case(
self, keras_model: tf.keras.Model, qspec: QuantizationSpec
) -> QuantizationSpec:
mp_qspec = QuantizationSpec()
for layer in keras_model.layers:
if isinstance(layer, tf.keras.layers.MaxPooling2D):
"""
Returns quantizable MaxPooling2D layers.
"""
if check_is_quantizable_by_layer_name(qspec, layer.name):
mp_qspec.add(
name=layer.name,
quantize_input=True,
quantize_weight=False
)
return mp_qspec
###################################################################
############ Network Specific QDQ Cases ###########################
###################################################################
class ResNetV1QDQCase(CustomQDQInsertionCase):
def __init__(self) -> None:
super().__init__()
def info(self) -> str:
return (
"Returns all quantizable nodes in ResNet-v1: "
" 1. Residual connections."
)
def case(
self, keras_model: tf.keras.Model, qspec: QuantizationSpec
) -> QuantizationSpec:
special_qspec = QuantizationSpec()
# Use Residual connection QDQ
residual_cqdq = ResidualConnectionQDQCase()
residual_cqdq_qspec = residual_cqdq.case(keras_model, qspec)
special_qspec.layers.extend(residual_cqdq_qspec.layers)
return special_qspec
class ResNetV2QDQCase(CustomQDQInsertionCase):
def __init__(self) -> None:
super().__init__()
def info(self) -> str:
return (
"Returns all quantizable nodes in ResNet-v2: "
" 1. Residual connections, "
" 2. BatchNorm not connected to Conv, "
" 3. MaxPool layers."
)
def case(
self, keras_model: tf.keras.Model, qspec: QuantizationSpec
) -> QuantizationSpec:
special_qspec = QuantizationSpec()
# Use Residual connection QDQ
residual_cqdq = ResidualConnectionQDQCase()
residual_cqdq_qspec = residual_cqdq.case(keras_model, qspec)
special_qspec.layers.extend(residual_cqdq_qspec.layers)
# Use BN QDQ Case
bn_cqdq = BNQDQCase()
bn_cqdq_qspec = bn_cqdq.case(keras_model, qspec)
special_qspec.layers.extend(bn_cqdq_qspec.layers)
# Use MaxPool QDQ Case (necessary for ResNet-v2)
mp_cqdq = MaxPoolQDQCase()
mp_cqdq_qspec = mp_cqdq.case(keras_model, qspec)
special_qspec.layers.extend(mp_cqdq_qspec.layers)
return special_qspec
class EfficientNetQDQCase(CustomQDQInsertionCase):
def __init__(self) -> None:
super().__init__()
def info(self) -> str:
return (
"Returns all quantizable nodes in EfficientNet:"
" 1. Residual connections,"
" 2. Quantize inputs (0, 1) of Multiply layers in SE (Squeeze-Excite) block."
)
def case(
self, keras_model: tf.keras.Model, qspec: QuantizationSpec
) -> QuantizationSpec:
special_qspec = QuantizationSpec()
# Use Residual connection QDQ
residual_cqdq = ResidualConnectionQDQCase()
residual_cqdq_qspec = residual_cqdq.case(keras_model, qspec)
special_qspec.layers.extend(residual_cqdq_qspec.layers)
# Implement EfficientNet specific case to trigger horizontal fusion in Mul residual branch.
# Gives preference to the user-specified `qspec`.
for layer in keras_model.layers:
if (
isinstance(layer, tf.keras.layers.Multiply)
and check_is_quantizable_by_layer_name(qspec, layer.name)
):
special_qspec.add(
layer.name,
quantize_input=True,
quantize_weight=False,
quantization_index=[0, 1],
)
return special_qspec
class MobileNetQDQCase(CustomQDQInsertionCase):
def __init__(self) -> None:
super().__init__()
def info(self) -> str:
return (
"Returns all quantizable nodes in MobileNet: "
" 1. Residual connections."
)
def case(
self, keras_model: tf.keras.Model, qspec: QuantizationSpec
) -> QuantizationSpec:
special_qspec = QuantizationSpec()
# Use Residual connection QDQ
residual_cqdq = ResidualConnectionQDQCase()
residual_cqdq_qspec = residual_cqdq.case(keras_model, qspec)
special_qspec.layers.extend(residual_cqdq_qspec.layers)
return special_qspec
class InceptionQDQCase(CustomQDQInsertionCase):
def __init__(self) -> None:
super().__init__()
def info(self) -> str:
return (
"Returns all quantizable nodes in Inception-v3: "
" 1. MaxPool layers to trigger horizontal fusion in the output of Concat."
)
def case(
self, keras_model: tf.keras.Model, qspec: QuantizationSpec
) -> QuantizationSpec:
special_qspec = QuantizationSpec()
# Use MaxPool QDQ Case
mp_cqdq = MaxPoolQDQCase()
mp_cqdq_qspec = mp_cqdq.case(keras_model, qspec)
special_qspec.layers.extend(mp_cqdq_qspec.layers)
return special_qspec
@@ -0,0 +1,64 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
#
"""
This module holds the quantization config class object that is accessed globally by library modules.
DIRECT USE OF THIS MODULE BY USER IS PROHIBITED.
"""
# List that holds quantization config class object, Length is always one!
# Object is added automatically on class creation
G_CONFIG_OBJECT = []
def add_config_object(config_object: "BaseConfig") -> None:
"""
Add instance of quantize config class to the global list.
Args:
config_object : Instance of one of four quantize config class
"""
assert (
len(G_CONFIG_OBJECT) == 0
), "Looks like previous quatize object is alive. Did you call clear() on the object?"
G_CONFIG_OBJECT.append(config_object)
def remove_config_object() -> None:
"""
Remove instance of quantize config class from the global list.
"""
if G_CONFIG_OBJECT:
G_CONFIG_OBJECT.clear()
def get_config_object() -> "BaseConfig":
"""
Return quantize config class object
"""
assert (
len(G_CONFIG_OBJECT) == 1
), "Have you created quantize config object before calling `quantize_model`?"
if G_CONFIG_OBJECT:
return G_CONFIG_OBJECT[0]
def is_config_object_created() -> bool:
"""
Sanity check function for whether quantize config class object is created.
"""
return len(G_CONFIG_OBJECT) == 1
@@ -0,0 +1,445 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
#
# 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.
from typing import List, Union
import tensorflow as tf
import tensorflow_quantization.quantize_wrappers as quantize_wrappers
import tensorflow_quantization.global_config as cfg
import tensorflow_quantization.quantize_config as quantize_config
from dataclasses import dataclass
from tensorflow_quantization.quantize_wrappers import DISABLED_LAYER_QUANTIZATION_DEFAULT
@dataclass
class LayerConfig:
"""
Internal dataclass for a single layer config.
Args:
name (str): Name of the layer. As seen from utilities such as `model.summary()`
is_keras_class (bool) : Set this to True if layer_name passed represents a layer class from Keras.
Default is False.
quantize_input (bool): Set this to True if input to the layers should be quantized. Default is True
since default behavior is following Nvidia quantization recipe.
quantize_weight (bool): Set this to True if weights to the layers should be quantized. Default is True
since default behavior is following Nvidia quantization recipe. For weightless layers, value is
ignored.
quantization_index (List): Indices on inputs to which quantization is applied for the layers with
multiple inputs. E.g Add, Concatenate
Returns:
None
"""
name: str = None
is_keras_class: bool = False
quantize_input: bool = True
quantize_weight: bool = True
quantization_index: list = None
class QuantizationSpec:
"""
Helper class holding config objects for all layers to quantize.
"""
def __init__(self) -> None:
self.layers = []
def __str__(self) -> str:
for l in self.layers:
print(l)
return ""
def add(
self,
name: Union[str, List],
is_keras_class: Union[bool, List] = False,
quantize_input: Union[bool, List] = True,
quantize_weight: Union[bool, List] = True,
quantization_index: Union[List, List[List]] = None,
) -> None:
"""
Takes user parameters and adds LayerConfig object to a list for each add call.
Args:
name (Union[str, List]): Name of the layer. As seen from utilities such as `model.summary()`
is_keras_class (Union[bool, List]): List or a single value. Set this to True if layer_name passed represents a layer class from Keras.
Default is False.
quantize_input (Union[bool, List]): List or a single value. Set this to True if input to the layers should be quantized. Default is True
since default behavior is following Nvidia quantization recipe.
quantize_weight (Union[bool, List]): List or a single value. Set this to True if weights to the layers should be quantized. Default is True
since default behavior is following Nvidia quantization recipe. For weightless layers, value is
ignored.
quantization_index (Union[List, List[List]]): List or List of List. List with indices on inputs to which quantization is applied for the layers with
multiple inputs. E.g Add, Concatenate
Returns:
None
"""
if not isinstance(name, list):
self.layers.append(
LayerConfig(
name=name,
is_keras_class=is_keras_class,
quantize_input=quantize_input,
quantize_weight=quantize_weight,
quantization_index=quantization_index,
)
)
else:
# layer names is passed as a list
if isinstance(is_keras_class, list):
assert len(name) == len(
is_keras_class
), "[E] `is_keras_class` is a list but length is not same as layer `name` list"
if isinstance(quantize_input, list):
assert len(name) == len(
quantize_input
), "[E] `quantize_input` is a list but length is not same as layer `name` list"
if isinstance(quantize_weight, list):
assert len(name) == len(
quantize_weight
), "[E] `quantize_weight` is a list but length is not same as layer `name` list"
if isinstance(quantization_index, list):
assert len(name) == len(
quantization_index
), "[E] `quantization_index` is list but length is not same as layer `name` list"
for i, e in enumerate(name):
cl_name = e
cl_is_keras_class = (
is_keras_class[i]
if isinstance(is_keras_class, list)
else is_keras_class
)
cl_quantize_input = (
quantize_input[i]
if isinstance(quantize_input, list)
else quantize_input
)
cl_quantize_weight = (
quantize_weight[i]
if isinstance(quantize_weight, list)
else quantize_weight
)
cl_quantization_index = (
quantization_index[i]
if isinstance(quantization_index, list)
else quantization_index
)
self.layers.append(
LayerConfig(
name=cl_name,
is_keras_class=cl_is_keras_class,
quantize_input=cl_quantize_input,
quantize_weight=cl_quantize_weight,
quantization_index=cl_quantization_index,
)
)
def _skip_layer(layer: tf.keras.layers.Layer) -> bool:
"""
Decide whether quantization wrapping should be skipped for the given layer.
The decision is made based on an internal quantize config object parameters.
Args:
layer (tf.keras.layers.Layer): Keras model layer
Returns:
bool: True if given layer should not be quantized else False
"""
config_object = cfg.get_config_object()
# Check if any layer with Disabled Quantization by default are in the 'config_object.layer_classes_to_quantize'.
# If so, that layer will be enabled for quantization. Otherwise, skip (return True).
layer_class_name = layer.__class__.__name__
if layer_class_name in DISABLED_LAYER_QUANTIZATION_DEFAULT:
if layer_class_name not in config_object.layer_classes_to_quantize:
if layer.name in config_object.get_layer_config():
# User can enable a single layer even if the default behavior of a Class is to not quantize.
# The decision of whether to quantize this layer or not will be left for later checks, such as when
# quantize_input and quantize_weight = False.
pass
else:
# Default behavior: skip layer
return True
# 1. When quantize_input = False, quantize_weight = False and quantization_index=None, don't even wrap the layer.
if layer.name in config_object.get_layer_config():
current_layer_config = config_object.get_layer_config()[layer.name]
if (
current_layer_config["qbool_list"][0] == False # quantize_input
and current_layer_config["qbool_list"][1] == False # quantize_weight
and "qindex_list" not in current_layer_config
):
print(
"[I] Layer `{layer_name}` is not quantized. There is nothing to quantize since "
"quantize_input = False, quantize_weight = False and quantization_index=None".format(
layer_name=layer.name
)
)
return True
# 2. Called when quantization_mode is `partial`
if config_object.config_class_id == 2:
# A. Skip current `layer class` if current layer class is not in user provided QuantizationSpec class
# object. However, when current layer name is passed by user to quantize, don't skip the layer.
if (
len(config_object.layer_classes_to_quantize) != 0
and layer.__class__.__name__ not in config_object.layer_classes_to_quantize
):
if layer.name in config_object.get_layer_config():
return False
else:
print(
"[I] Layer class `{layer_class_name}` is not quantized. Partial quantization is enabled "
"and layer class is not in user provided QuantizationSpec class object".format(
layer_class_name=layer.__class__.__name__
)
)
return True
# B. Skip current layer if `layer.name` is not in user provided QuantizationSpec class object.
# However, if current layer class is passed by user to quantize, don't skip the layer.
elif layer.name not in config_object.get_layer_config():
if layer.__class__.__name__ in config_object.layer_classes_to_quantize:
return False
else:
print(
"[I] Layer `{layer_name}` is not quantized. Partial quantization is enabled and layer name is not "
"in user provided QuantizationSpec class object".format(
layer_name=layer.name
)
)
return True
return False
def _quantize_model_layer_clone_function(
layer: tf.keras.layers.Layer,
) -> "BaseQuantizeWrapper":
"""
Wrap or leave given layer based on quantize config object parameters.
Args:
layer (tf.keras.layers.Layer): Keras model layer
Returns:
BaseQuantizeWrapper: layer wrapped in BaseQuantizeWrapper class.
"""
layer_wrapper = layer
if _skip_layer(layer):
# Skip the layers not specified by the user.
pass
else:
child_wrappers_dict = quantize_wrappers.BaseQuantizeWrapper.CHILD_WRAPPERS
possible_wrapper_name_for_this_layer = (
layer.__class__.__name__ + "QuantizeWrapper"
)
if possible_wrapper_name_for_this_layer in child_wrappers_dict:
wrapper_function = child_wrappers_dict[possible_wrapper_name_for_this_layer]
layer_wrapper = wrapper_function(layer)
return layer_wrapper
def _execute_quantize_model(
model: tf.keras.Model, class_id: int, qspec: QuantizationSpec = None
) -> tf.keras.Model:
"""
clone the model and apply quantization to specific layers based on quantize config object parameters.
Args:
model (tf.keras.Model): Keras functional or sequential model.
* Currently Subclassed models are not supported
class_id (int): internal quantization class ID
qspec (QuantizationSpec): object of QuantizationSpec class. If few layers or layer classes are to be treated
differently, LayerConfig class objects for that layer/layer class are created internally and
added to QuantizationSpec class.
Returns:
tf.keras.Model: Quantized model with QDQ nodes added.
"""
config_id_class_name_map = {
0: "FullNetworkQuantization",
1: "FullNetworkSpecialQuantization",
2: "PartialNetworkQuantization",
}
# 1. Create quantize config object
q_config_object = getattr(quantize_config, config_id_class_name_map[class_id])()
# 2. Update object attributes
if qspec:
q_config_object.add_quantization_spec_object(qspec, model.layers)
assert (
cfg.is_config_object_created()
), "[E] Have you created the quantization config object before calling `quantize_model`?"
# 3. Ensure that the original model is kept untouched.
# This step is needed as `clone_model` with our custom `clone_function` wraps layers in a destructive manner.
# TODO: delete later if a better solution is found, most likely inside our custom `clone_function`.
cloned_model = tf.keras.models.clone_model(model)
cloned_model.set_weights(model.get_weights())
# 4. Wrap quantizable layers
quant_model = tf.keras.models.clone_model(
cloned_model, input_tensors=None, clone_function=_quantize_model_layer_clone_function
)
# 5. Clean global space afterwards
q_config_object.clean()
return quant_model
def _recognize_config_class_id(
quantization_mode: str = "full", qspec: QuantizationSpec = None
) -> int:
"""
Interpret internal quantize config class based on parameters passed by user to
`quantize_model` function.
Args:
quantization_mode (str): Either 'full' or 'partial' quantization mode
qspec (QuantizationSpec): object of QuantizationSpec class. If few layers or layer classes are to be treated
differently, LayerConfig class objects for that layer/layer class are created internally and
added to QuantizationSpec class.
Returns:
int: ID for quantization category class used internally.
Raises:
Exception: if no class can be interpreted for given parameter combination
"""
if quantization_mode == "full" and qspec is None:
return 0
elif quantization_mode == "full" and qspec is not None:
return 1
elif quantization_mode == "partial" and qspec is not None:
return 2
else:
raise Exception(
"Could not recognize config class ID."
" Are parameters passed to `quantize_model` function correct?"
)
def _validate_config(
quantization_mode: str = "full", qspec: QuantizationSpec = None
) -> None:
"""
Validate if parameters passed to `quantize_model` makes sense.
Args:
quantization_mode (str): quantization mode can be either 'full' or 'partial'
qspec (QuantizationSpec): object of QuantizationSpec class. If few layers or layer classes are to be treated
differently, LayerConfig class objects for that layer/layer class are created internally and
added to QuantizationSpec class.
Returns:
None
Raises:
AssertionError: when configuration is not valid.
"""
def _verify_support_for_all_layer_classes(qspec: QuantizationSpec):
for layer in qspec.layers:
if layer.is_keras_class:
# Layer class name is provided.
child_wrappers_dict = (
quantize_wrappers.BaseQuantizeWrapper.CHILD_WRAPPERS
)
possible_wrapper_name_for_this_layer = layer.name + "QuantizeWrapper"
assert possible_wrapper_name_for_this_layer in child_wrappers_dict, (
"[E] layer class `{layer_name}` is not supported yet! Either there is no native wrapper or user "
"provided wrapper registration failed.".format(
layer_name=layer.name
)
)
if qspec:
_verify_support_for_all_layer_classes(qspec)
if quantization_mode == "partial":
assert (
qspec is not None
), "[E] `QuantizationSpec` class object must be passed when `quantization_mode=partial`."
def quantize_model(
model,
quantization_mode: str = "full",
quantization_spec: QuantizationSpec = None,
custom_qdq_cases: List["CustomQDQInsertionCase"] = None,
) -> tf.keras.Model:
"""
Insert Q/DQ nodes in Keras model and return a copy. Weights are preserved unlike native keras clone.
Args:
model(tf.keras.Model): Keras Functional or Sequential model.subclassed models are not yet supported.
quantization_mode(str): quantization mode can be either 'full' or 'partial'
quantization_spec(QuantizationSpec) : object of QuantizationSpec class. If few layers or layer classes are to
be treated differently, LayerConfig class objects for that layer/layer class are created internally and
added to QuantizationSpec class.
custom_qdq_cases(List[CustomQDQInsertionCase]) : `Case` method on every object in this list is called by passing
model and user passed quantization_spec as arguments. Each member of this list is an object of a class
inherited from CustomQDQInsertionCase class.
Raises:
AssertionError: When passed model is subclassed.
AssertionError: When CustomQDQInsertionCase does not return QuantizationSpec object.
AssertionError: When quantization mode is `partial` but QuantizationSpec object is not passed.
AssertionError: When quantization wrapper is not found for desired layer class.
ExceptionError: When internal quantization class ID can't be detected. This happens when passed parameters
do not make sense.
Returns:
tf.keras.Model: Quantized model with QDQ nodes inserted according to NVIDIA quantization recipe.
"""
supported_model_classes = {"Functional", "Sequential"}
assert (
model.__class__.__name__ in supported_model_classes
), "[E] Currently only `Functional` or `Sequential` model quantization is supported."
# Update quantization_spec object based on output of special QDQ cases.
custom_quantization_spec = QuantizationSpec()
if custom_qdq_cases:
for custom_qdq_case in custom_qdq_cases:
qspec_case_object = custom_qdq_case.case(model, quantization_spec)
if qspec_case_object:
assert isinstance(
qspec_case_object, QuantizationSpec
), "[E] {} \
does not return an object of QuantizationSpec.".format(
qspec_case_object.__class__.__name__
)
custom_quantization_spec.layers.extend(qspec_case_object.layers)
# if user has passed quantization_spec then extend it with custom_quantization_spec
# else use just custom_quantization_spec
if quantization_spec:
quantization_spec.layers.extend(custom_quantization_spec.layers)
else:
if len(custom_quantization_spec.layers) != 0:
quantization_spec = custom_quantization_spec
# Check if config is valid and quantize model
_validate_config(quantization_mode, quantization_spec)
cid = _recognize_config_class_id(quantization_mode, quantization_spec)
return _execute_quantize_model(model, cid, quantization_spec)
@@ -0,0 +1,273 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
#
"""
This module implements classes to configure three supported quantization modes:
1. Full: quantize all layers with standard protocol based NVIDIA quantization scheme.
2. Full special: quantize few layers in a specific way and remaining with standard protocol based on NVIDIA
quantization scheme.
3. Partial: quantize ONLY few layers.
Each quantization mode can quantize all supported Keras layer classes or only subset of it.
"""
from abc import ABC
import tensorflow_quantization.global_config as global_config
import warnings
from typing import List, Dict
class BaseConfig(ABC):
"""
Base class from which four quantize config classes are derived.
Default quantization recipe is Nvidia's recommendation.
"""
def __new__(cls):
instance = super().__new__(cls)
# Add instance to global list
global_config.add_config_object(instance)
return instance
def __init__(self) -> None:
self.quantization_mode: str = "full"
self.layerwise_config: dict = {} # holds special layers information.
self.layer_classes_to_quantize: set = set()
self.config_class_id: int = 0
def __str__(self) -> str:
return (
" quantization_mode: {quant_mode} \n "
"layerwise_config: {layerwise_config} \n "
"specific_layer_class: {specific_layer_class} \n "
"config_class_id: {config_class_id} \n".format(
quant_mode=self.quantization_mode,
layerwise_config=self.layerwise_config,
specific_layer_class=self.specific_layer_class,
config_class_id=self.config_class_id,
)
)
@staticmethod
def _validate_layer_names(
user_passed_layer_names: List, model_layers: List
) -> None:
"""
Check whether user passed layer names exists in Keras model being quantized.
Args:
user_passed_layer_names (List): Layer names passed by user to treat specially.
model_layers (List): Keras model layers passed as a list.
Returns:
None
Raise:
Warning : when specific layer name is not found. Such layers are simply ignored.
"""
model_layer_name_set = set()
for l in model_layers:
model_layer_name_set.add(l.name)
for ul in user_passed_layer_names:
if ul not in model_layer_name_set:
warnings.warn(
"layer name {} is passed by user but could not find layer with this name in model.".format(
ul
)
)
def add_quantization_spec_object(
self, qspec: "QuantizationSpec", original_model_layers: List
) -> None:
"""
This method parses object of QuantizationSpec class and fill in `layerwise_config` dictionary
holding information about layers that need to be treated specially.
Specific layer classes that need to be treated specially are also here.
Args:
qspec (QuantizationSpec): object of QuantizationSpec class. If few layers or layer classes are to be treated
differently, LayerConfig class objects for that layer/layer class are created internally and
added to QuantizationSpec class.
original_model_layers (List): Keras model layers passed as a list.
Returns:
None
"""
for layer in qspec.layers:
if layer.is_keras_class:
self.add_special_layer_class(layer.name)
else:
layer_config_dict = {"qbool_list": [False, False]}
layer_config_dict["qbool_list"][0] = layer.quantize_input
layer_config_dict["qbool_list"][1] = layer.quantize_weight
if layer.quantization_index:
layer_config_dict["qindex_list"] = layer.quantization_index
self.add_special_layer(
layer_name=layer.name, config_dict=layer_config_dict
)
# Validate whether added layers exist in the model
self._validate_layer_names(
list(self.layerwise_config.keys()), original_model_layers
)
def add_special_layer(self, layer_name: str, config_dict: Dict) -> None:
"""
Add layer specific quantization information to quantize config object.
Args:
layer_name (str): layer name
config_dict (Dict): Layer specific quantization parameter dictionary in the
following format.
There are only two accepted keys `qbool_list` and `qindex_list`.
`qbool_list` is list of length two where each value is
[<True/False quantize inputs>, <True/False quantize weights>]
e.g.
To quantize inputs and weights, `qbool_list`=[True, True]
`qindex_list` is a list of specific indices to quaintize for layers such as Add, Concatenate
where more than two inputs are present.
Based on above information,
1. config_dict for weighted layer with name `dense_2`, to quantize inputs and weights will be
{'qbool_list':[True, True]} with laye_name=`dense_2`
2. config_dict for non weighted layer with name `add_3` to quantize input at index 1 will be
{'qbool_list':[True, False], 'qindex_list':[1]} with layer_name=`add_3`
Returns:
None
Raises:
Exception: When invalid keys are detected.
"""
self.layerwise_config[layer_name] = config_dict
def remove_layer(self, layer_name: str) -> None:
"""
Remove specific layer based on name from quantize config object.
Args:
layer_name (str): layer name
Returns:
None
"""
if layer_name in self.layerwise_config:
del self.layerwise_config[layer_name]
def remove_layers(self, layers_name: List) -> None:
"""
Bulk remove specific layers based on names from quantize config object.
Args:
layers_name (List): layers names, list of strings
Returns:
None
"""
for layer_name in layers_name:
self.remove_layer(layer_name=layer_name)
def get_layer_config(self) -> Dict:
"""
Return dictionary with information about layers to quantize for quantize
config object.
Args:
None
Returns:
Dict: a dictionary with layerwise configuration parameters.
"""
return self.layerwise_config
def is_empty(self) -> bool:
"""
Return True if no layer specific quantization information is available in quantize
config object.
Args:
None
Returns:
bool: True if no special layers are passed else return False
"""
return not self.layerwise_config
def clear_layer_config(self) -> None:
"""
Clear layer config information from quanize config object
Args:
None
Returns:
None
"""
self.layerwise_config.clear()
def add_special_layer_class(self, layer_class_name: str) -> None:
"""
Add class name to quantize config object so that only layers with specific class are quantized.
Args:
layer_class_name : String that represents keras class
Returns:
None
"""
self.layer_classes_to_quantize.add(layer_class_name)
def clean(self):
"""
Clean quantize config object from global space. Calling this is important to use `quantize_model` multiple times
within a single module.
Args:
None
Returns:
None
"""
global_config.remove_config_object()
class FullNetworkQuantization(BaseConfig):
"""
Quantize all layers based on NV scheme.
Nvidia recommended recipe for quantization is using Q/DQ only wth inputs/weights.
Q/DQ output support is just to compare engine performance/accuracy when other quantization
scheme is used.
NV: Add Q/DQ at input and weights
TF: Add Q/DQ at output and weights
This is config class with index `0` which is default.
"""
def __init__(self) -> None:
super().__init__()
self.config_class_id = 0
class FullNetworkSpecialQuantization(BaseConfig):
"""
Quantize few layers in specific way and remaining network in standard way based on NV scheme.
Layers are selected based on 'names' which can be via 'model.summary()' for functional
and sequential models.
Subclassed model layer information can be found using `KerasModelTraveller` class from utils.
This is config class with index 1.
"""
def __init__(self) -> None:
super().__init__()
self.config_class_id = 1
class PartialNetworkQuantization(BaseConfig):
"""
Quantize only specific layers and not the entire network.
Layers are selected based on name.
This is config class with index 2.
"""
def __init__(self) -> None:
super().__init__()
self.quantization_mode = "partial"
self.config_class_id = 2
@@ -0,0 +1,236 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
#
# 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.
import tensorflow as tf
import tensorflow_quantization.quantizers as quantizers
import tensorflow_quantization.global_config as cfg
from abc import abstractmethod
deserialize_keras_object = tf.keras.utils.deserialize_keras_object
serialize_keras_object = tf.keras.utils.serialize_keras_object
NO_WEIGHT_LAYERS = {
"Concatenate",
"Add",
"AveragePooling2D",
"GlobalAveragePooling2D",
"MaxPooling2D",
"BatchNormalization",
}
class BaseQuantizeWrapper(tf.keras.layers.Wrapper):
"""Base wrapper class which all layer wrappers inherit"""
CHILD_WRAPPERS = {}
def __init_subclass__(cls, **kwargs) -> None:
super().__init_subclass__(**kwargs)
cls.CHILD_WRAPPERS[cls.__name__] = cls
def __init__(self, layer: tf.keras.layers.Layer, **kwargs):
"""Create a quantize emulate wrapper for a keras layer.
This wrapper provides options to quantize inputs and weights of the layer.
Args:
layer (tf.keras.layers.Layer): The keras layer to be quantized.
**kwargs: Additional keyword arguments to be passed to the keras layer.
"""
if layer is None:
raise ValueError("`layer` cannot be None.")
# Check against keras.Model since it is an instance of keras.layers.Layer.
if not isinstance(layer, tf.keras.layers.Layer) or isinstance(
layer, tf.keras.Model
):
raise ValueError(
"`layer` can only be a `tf.keras.layers.Layer` instance. "
"You passed an instance of type: {input}.".format(
input=layer.__class__.__name__
)
)
if "name" not in kwargs:
kwargs["name"] = self._make_layer_name(layer)
super(BaseQuantizeWrapper, self).__init__(layer, **kwargs)
# get quantize config object that holds all the information about how quantization should be performed.
quantize_config_object = cfg.get_config_object()
# set all initial quantization parameters to False/None
self.quantize_inputs = False
self.quantize_weights = False
self.quantize_specific_input_indices = None
layer_class_name_t = layer.__class__.__name__ # Layer class name
layer_name_t = layer.name # Actual layer name
def _configure_singular_quantize():
self.quantize_inputs = True
if layer_class_name_t in NO_WEIGHT_LAYERS:
self.quantize_weights = False
else:
self.quantize_weights = True
def _configure_special_quantize(
quantize_bool_list: list, layer_name_t: str, index_list_if_any: list = None
):
assert (len(quantize_bool_list)) == 2, (
"Three boolean values (representing whether to quantize [inputs, weights]) must be provided in "
"quantize_config for layer: {layer_name_t}. If quantization does not apply for specific part, "
"pass None. e.g. For layer ( e.g. Concatenate, Add) with no weights, `qbool_list` to quantize "
"input can be [True, False]".format(layer_name_t=layer_name_t)
)
self.quantize_inputs = quantize_bool_list[0]
if layer_class_name_t in NO_WEIGHT_LAYERS:
self.quantize_weights = False
else:
self.quantize_weights = quantize_bool_list[1]
if index_list_if_any:
self.quantize_specific_input_indices = index_list_if_any
if quantize_config_object.config_class_id == 0:
# This is straight forward full network quantization
_configure_singular_quantize()
else:
# Config class id 1 or 2.
# User has provided layer (name) specific quantization information
quantize_config_dict = quantize_config_object.get_layer_config()
if layer_name_t in quantize_config_dict:
# This layer needs to be quantized in specific way
if "qindex_list" in quantize_config_dict[layer_name_t]:
_configure_special_quantize(
quantize_config_dict[layer_name_t]["qbool_list"],
layer_name_t,
quantize_config_dict[layer_name_t]["qindex_list"],
)
else:
_configure_special_quantize(
quantize_config_dict[layer_name_t]["qbool_list"], layer_name_t
)
else:
_configure_singular_quantize()
self._track_trackable(layer, name="layer")
@staticmethod
def _make_layer_name(layer):
return "{}_{}".format("quant", layer.name)
@staticmethod
def _weight_name(name):
"""Extracts the weight name from the full TensorFlow variable name.
For example, returns 'kernel' for 'dense_2/kernel:0'.
Args:
name: TensorFlow variable name.
Returns:
Extracted weight name.
"""
return name.split(":")[0].split("/")[-1]
def build(self, input_shape):
super(BaseQuantizeWrapper, self).build(input_shape)
self.optimizer_step = self.add_weight(
"optimizer_step",
initializer=tf.keras.initializers.Constant(-1),
dtype=tf.dtypes.int32,
trainable=False,
)
def compute_output_shape(self, input_shape):
return self.layer.compute_output_shape(self.layer.input_shape)
def _last_value_quantizer(
self, x, training, quantizer_vars, per_channel=False, channel_axis=-1
):
"""Use currying to return True/False specialized fns to the cond."""
from tensorflow_quantization import G_NUM_BITS, G_SYMMETRIC, G_NARROW_RANGE
return quantizers.LastValueQuantize(
x,
quantizer_vars["min_var"],
quantizer_vars["max_var"],
per_channel=per_channel,
channel_axis=channel_axis,
is_training=training,
num_bits=G_NUM_BITS,
narrow_range=G_NARROW_RANGE,
symmetric=G_SYMMETRIC,
)
@abstractmethod
def call(self, inputs, training=None):
raise NotImplementedError
def get_config(self):
base_config = super(BaseQuantizeWrapper, self).get_config()
config = {"quantize_config": None}
return dict(list(base_config.items()) + list(config.items()))
@classmethod
def from_config(cls, config):
config = config.copy()
# BaseQuantizeWrapper may be constructed with any QuantizeConfig and the
# wrapper itself cannot know all the possible config classes.
# The deserialization code should ensure the QuantizeConfig is in keras
# serialization scope.
quantize_config = deserialize_keras_object(
config.pop("quantize_config"), module_objects=globals(), custom_objects=None
)
layer = tf.keras.layers.deserialize(config.pop("layer"))
return cls(layer=layer, quantize_config=quantize_config, **config)
@property
def trainable(self):
return self.layer.trainable
@trainable.setter
def trainable(self, value):
self.layer.trainable = value
@property
def trainable_weights(self):
return self.layer.trainable_weights + self._trainable_weights
@property
def non_trainable_weights(self):
return self.layer.non_trainable_weights + self._non_trainable_weights
@property
def updates(self):
return self.layer.updates + self._updates
@property
def losses(self):
return self.layer.losses + self._losses
@@ -0,0 +1,556 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
#
import tensorflow as tf
from tensorflow.python.util import tf_inspect
from tensorflow_quantization.quantize_wrapper_base import BaseQuantizeWrapper
import warnings
"""
Naming convention for keras `layer` quantize wrapper is
<layer.__class__.__name__>QuantizeWrapper
"""
DISABLED_LAYER_QUANTIZATION_DEFAULT = [
"MaxPooling2D",
"BatchNormalization",
"Add",
"Multiply",
"Concatenate"
]
# ##############################################
# ############# Weighted Layers ################
# ##############################################
class WeightedBaseQuantizeWrapper(BaseQuantizeWrapper):
"""
BaseQuantizeWrapper for weighted layers: Conv2D, DepthwiseConv2D, and Dense layer.
These layers share a lot of the same code except for a few modifications. Conv2D and Dense share the same code.
Layers that inherit this class support weight and input QDQ nodes.
TRT Rule:
One Q/DQ pair should be attached to the input activation, and another Q/DQ pair should be attached to weights.
Weights tensor is per-channel quantized:
For the Q/DQ attached to weight tensor, set axis=0 and axis=1 for Conv and ConvTransposed respectively.
Input tensor is per-tensor quantized.
"""
def __init__(
self, layer: tf.keras.layers.Layer, kernel_type: str = "kernel", **kwargs
):
"""
Creates a wrapper to emulate quantization for a keras layer.
Args:
layer (tf.keras.layers.Layer): The keras layer to be quantized.
kernel_type (str): Options=['kernel' for Conv2D/Dense, 'depthwise_kernel' for DepthwiseConv2D]
**kwargs: Additional keyword arguments to be passed to the keras layer.
"""
self.kernel_type = kernel_type
self.channel_axis = kwargs.get("axis", -1)
super().__init__(layer, **kwargs)
def build(self, input_shape):
super().build(input_shape)
self._weight_vars = []
self.input_vars = {}
self.output_vars = {}
self.channel_axis = -1
if self.kernel_type == "depthwise_kernel":
self.channel_axis = 2
# quantize weights only applicable for weighted ops.
# By default weights is per channel quantization
if self.quantize_weights:
# get kernel weights dims.
kernel_weights = getattr(self.layer, self.kernel_type)
min_weight = self.layer.add_weight(
kernel_weights.name.split(":")[0] + "_min",
shape=(kernel_weights.shape[self.channel_axis]),
initializer=tf.keras.initializers.Constant(-6.0),
trainable=False,
)
max_weight = self.layer.add_weight(
kernel_weights.name.split(":")[0] + "_max",
shape=(kernel_weights.shape[self.channel_axis]),
initializer=tf.keras.initializers.Constant(6.0),
trainable=False,
)
quantizer_vars = {"min_var": min_weight, "max_var": max_weight}
self._weight_vars.append((kernel_weights, quantizer_vars))
# Needed to ensure unquantized weights get trained as part of the wrapper.
self._trainable_weights.append(kernel_weights)
# By default input is per tensor quantization
if self.quantize_inputs:
input_min_weight = self.layer.add_weight(
self.layer.name + "_ip_min",
shape=None,
initializer=tf.keras.initializers.Constant(-6.0),
trainable=False,
)
input_max_weight = self.layer.add_weight(
self.layer.name + "_ip_max",
shape=None,
initializer=tf.keras.initializers.Constant(6.0),
trainable=False,
)
self.input_vars["min_var"] = input_min_weight
self.input_vars["max_var"] = input_max_weight
def call(self, inputs, training=None):
if training is None:
training = tf.keras.backend.learning_phase()
# Quantize all weights, and replace them in the underlying layer.
if self.quantize_weights:
quantized_weights = []
quantized_weight = self._last_value_quantizer(
self._weight_vars[0][0],
training,
self._weight_vars[0][1],
per_channel=True,
channel_axis=self.channel_axis,
)
quantized_weights.append(quantized_weight)
# Replace the original weights with QDQ weights
setattr(self.layer, self.kernel_type, quantized_weights[0])
# Quantize inputs to the conv layer
if self.quantize_inputs:
quantized_inputs = self._last_value_quantizer(
inputs, training, self.input_vars, per_channel=False
)
else:
quantized_inputs = inputs
args = tf_inspect.getfullargspec(self.layer.call).args
if "training" in args:
outputs = self.layer.call(quantized_inputs, training=training)
else:
outputs = self.layer.call(quantized_inputs)
return outputs
class Conv2DQuantizeWrapper(WeightedBaseQuantizeWrapper):
def __init__(self, layer: tf.keras.layers.Layer, **kwargs):
"""
Creates a wrapper to emulate quantization for the Conv2D keras layer.
Args:
layer (tf.keras.layers.Layer): The keras layer to be quantized.
**kwargs: Additional keyword arguments to be passed to the keras layer.
"""
self.kernel_type = "kernel"
super().__init__(layer, kernel_type=self.kernel_type, **kwargs)
def build(self, input_shape):
super().build(input_shape)
def call(self, inputs, training=None):
return super().call(inputs, training=training)
class DenseQuantizeWrapper(WeightedBaseQuantizeWrapper):
def __init__(self, layer: tf.keras.layers.Layer, **kwargs):
"""
Creates a wrapper to emulate quantization for the Dense keras layer.
Args:
layer (tf.keras.layers.Layer): The keras layer to be quantized.
**kwargs: Additional keyword arguments to be passed to the keras layer.
"""
self.kernel_type = "kernel"
super().__init__(layer, kernel_type=self.kernel_type, **kwargs)
def build(self, input_shape):
super().build(input_shape)
def call(self, inputs, training=None):
return super().call(inputs, training=training)
class DepthwiseConv2DQuantizeWrapper(WeightedBaseQuantizeWrapper):
"""Requires TF >= 2.8.0"""
def __init__(self, layer: tf.keras.layers.Layer, **kwargs):
"""
Creates a wrapper to emulate quantization for the DepthwiseConv2D keras layer.
Args:
layer (tf.keras.layers.Layer): The keras layer to be quantized.
**kwargs: Additional keyword arguments to be passed to the keras layer.
"""
self.kernel_type = "depthwise_kernel"
super().__init__(layer, kernel_type=self.kernel_type, **kwargs)
def build(self, input_shape):
super().build(input_shape)
def call(self, inputs, training=None):
return super().call(inputs, training=training)
# ##############################################
# ########### Non-Weighted Layers ##############
# ######### with Single Input/Output ###########
# ##############################################
class NonWeightedBaseQuantizeWrapper(BaseQuantizeWrapper):
"""
BaseQuantizeWrapper for non-weighted layers with Single Input/Output: AveragePooling2D, GlobalAveragePooling,
MaxPooling2D and BatchNormalization.
Supports 1 input and 1 output QDQ. Similar to Concat, except that Concat supports multiple inputs.
NonWeightedBaseQuantizeWrapper can use WeightedBaseQuantizeWrapper by giving quantize_weigths=False.
"""
def __init__(self, layer: tf.keras.layers.Layer, **kwargs):
"""
Creates a wrapper to emulate quantization for non-weighted keras layers.
Args:
layer (tf.keras.layers.Layer): The keras layer to be quantized.
**kwargs: Additional keyword arguments to be passed to the keras layer.
"""
super().__init__(layer, **kwargs)
def build(self, input_shape):
super().build(input_shape)
self.input_vars = {}
# By default input is per tensor quantization
if self.quantize_inputs:
input_min_weight = self.layer.add_weight(
self.layer.name + "_ip_min",
shape=None,
initializer=tf.keras.initializers.Constant(-6.0),
trainable=False,
)
input_max_weight = self.layer.add_weight(
self.layer.name + "_ip_max",
shape=None,
initializer=tf.keras.initializers.Constant(6.0),
trainable=False,
)
self.input_vars["min_var"] = input_min_weight
self.input_vars["max_var"] = input_max_weight
def call(self, inputs, training=None):
if training is None:
training = tf.keras.backend.learning_phase()
# Quantize inputs to the conv layer
if self.quantize_inputs:
quantized_inputs = self._last_value_quantizer(
inputs, training, self.input_vars, per_channel=False
)
else:
quantized_inputs = inputs
args = tf_inspect.getfullargspec(self.layer.call).args
if "training" in args:
outputs = self.layer.call(quantized_inputs, training=training)
else:
outputs = self.layer.call(quantized_inputs)
return outputs
class AveragePooling2DQuantizeWrapper(NonWeightedBaseQuantizeWrapper):
"""
TRT Rule:
Add Q/DQ to its input if the ops follows is quantized.
Quantize average pooling will introduce small variance compared to float because of the rounding change.
TensorRT doesnt have Int8 in and fp32 out average pool support.
If the op follows average pooling is not quantized, it is users choice between running average pooling
in int8 then convert to fp32 for the following op and run average pooling in fp32.
Currently, we're adding QDQ to all AveragePooling2D layers.
"""
def __init__(self, layer: tf.keras.layers.Layer, **kwargs):
"""
Creates a wrapper to emulate quantization for the AveragePooling2D keras layer.
Args:
layer (tf.keras.layers.Layer): The keras layer to be quantized.
**kwargs: Additional keyword arguments to be passed to the keras layer.
"""
super().__init__(layer, **kwargs)
def build(self, input_shape):
super().build(input_shape)
def call(self, inputs, training=None):
return super().call(inputs, training=training)
class GlobalAveragePooling2DQuantizeWrapper(NonWeightedBaseQuantizeWrapper):
"""
TRT Rule:
No explicit rule from the TRT team. Following the same as AveragePooling2D.
Residual block v2: Add to MaxPool (branch1) and BN (branch2).
Supports 1 input and 1 output QDQ. Same as AveragePooling2DQuantizeWrapper.
"""
def __init__(self, layer: tf.keras.layers.Layer, **kwargs):
"""
Creates a wrapper to emulate quantization for the GlobalAveragePooling2D keras layer.
Args:
layer (tf.keras.layers.Layer): The keras layer to be quantized.
**kwargs: Additional keyword arguments to be passed to the keras layer.
"""
super().__init__(layer, **kwargs)
def build(self, input_shape):
super().build(input_shape)
def call(self, inputs, training=None):
return super().call(inputs, training=training)
class MaxPooling2DQuantizeWrapper(NonWeightedBaseQuantizeWrapper):
"""
TRT Rule:
Max pooling is precision-neutral. But unlike ReLU, input and output of max pooling will have different
histograms which will lead to different calibration results.
The recommendation is to let TensorRT optimize precision neutral ops.
There are cases where adding Q/DQ before maxpool can enable additional optimization.
Residual block v2: Add to MaxPool (branch1) and BN (branch2).
Supports 1 input and 1 output QDQ. Same as AveragePooling2DQuantizeWrapper.
"""
def __init__(self, layer: tf.keras.layers.Layer, **kwargs):
"""
Creates a wrapper to emulate quantization for the MaxPooling2D keras layer.
Args:
layer (tf.keras.layers.Layer): The keras layer to be quantized.
**kwargs: Additional keyword arguments to be passed to the keras layer.
"""
super().__init__(layer, **kwargs)
def build(self, input_shape):
super().build(input_shape)
def call(self, inputs, training=None):
return super().call(inputs, training=training)
class BatchNormalizationQuantizeWrapper(NonWeightedBaseQuantizeWrapper):
"""
TRT Rule:
Keep batch normalization untouched, don't add Q/DQ to its input and not necessary to fold it before exporting
graph. TensorRT supports Batch normalization folding. It can take a graph with batch normalization, fold it
into previous convolution and create a new graph.
If batch normalization is folded before exporting the graph, TensorRT can still import and execute the graph as
it becomes regular convolutions.
Exception for Residual block v2:
BN-ReLU-Conv2D -> need to add Q/DQ before BN in order to run in INT8.
In order to do that, we add a check in 'quantize_model()' to check if BN's parent is a Conv layer. If it is, set
quantize_inputs to False. The reason why we don't add this check here is to allow the user to add QDQ nodes
before BN if they so wish.
Supports 1 input and 1 output QDQ. Same as AveragePooling2DQuantizeWrapper.
"""
def __init__(self, layer: tf.keras.layers.Layer, **kwargs):
"""
Creates a wrapper to emulate quantization for the BatchNormalization keras layer.
Args:
layer (tf.keras.layers.Layer): The keras layer to be quantized.
**kwargs: Additional keyword arguments to be passed to the keras layer.
"""
super().__init__(layer, **kwargs)
def build(self, input_shape):
super().build(input_shape)
def call(self, inputs, training=None):
return super().call(inputs, training=training)
# ##############################################
# ########### Non-Weighted Layers ##############
# #### with Multiple Inputs, Single Output #####
# ##############################################
class NonWeightedBaseQuantizeWrapperForMultipleInputs(BaseQuantizeWrapper):
"""
BaseQuantizeWrapper for non-weighted layers with Multiple Inputs: Concat, Add, and Multiply.
Supports multiple inputs and 1 output QDQ. Similar to AveragePooling2D, except pooling supports only a single input.
TRT Rule:
Add Q/DQ to all inputs of the layer.
"""
def __init__(self, layer: tf.keras.layers.Layer, **kwargs):
"""
Creates a wrapper to emulate quantization for the keras layer.
Args:
layer (tf.keras.layers.Layer): The keras layer to be quantized.
**kwargs: Additional keyword arguments to be passed to the keras layer.
"""
super().__init__(layer, **kwargs)
def _should_quantization_this_index(self, i):
if not self.quantize_specific_input_indices:
return True
else:
# This is a small list so iterating makes sense
for e in self.quantize_specific_input_indices:
if e == i:
return True
elif e >= self.num_inputs:
warnings.warn(
"{layer_name} has {num_inputs} inputs but quantization index {e} is passed.".format(
layer_name=self.layer.name, num_inputs=self.num_inputs, e=e
)
)
return False
def build(self, input_shape):
super().build(input_shape)
self.input_vars = [] # list of dictionaries
self.num_inputs = len(input_shape)
# By default input is per tensor quantization
if self.quantize_inputs:
# for concat input is list of Tensors
layer_name_key_idx = 0
for i in range(self.num_inputs):
if self._should_quantization_this_index(i):
input_min_weight = self.layer.add_weight(
self.layer.name + "_ip{}_min".format(layer_name_key_idx),
shape=None,
initializer=tf.keras.initializers.Constant(-6.0),
trainable=False,
)
input_max_weight = self.layer.add_weight(
self.layer.name + "_ip{}_max".format(layer_name_key_idx),
shape=None,
initializer=tf.keras.initializers.Constant(6.0),
trainable=False,
)
self.input_vars.append(
{"min_var": input_min_weight, "max_var": input_max_weight}
)
layer_name_key_idx += 1
def call(self, inputs, training=None):
if training is None:
training = tf.keras.backend.learning_phase()
# Quantize inputs to the conv layer
quantized_inputs = inputs[:]
if self.quantize_inputs:
input_vars_idx = 0
for i in range(len(inputs)):
if self._should_quantization_this_index(i):
quantized_inputs[i] = self._last_value_quantizer(
inputs[i],
training,
self.input_vars[input_vars_idx],
per_channel=False,
)
input_vars_idx += 1
args = tf_inspect.getfullargspec(self.layer.call).args
if "training" in args:
outputs = self.layer.call(quantized_inputs, training=training)
else:
outputs = self.layer.call(quantized_inputs)
return outputs
class MultiplyQuantizeWrapper(NonWeightedBaseQuantizeWrapperForMultipleInputs):
"""
TRT Rule:
Add Q/DQ to all inputs of Multiply layer in SE block.
"""
def __init__(self, layer: tf.keras.layers.Layer, **kwargs):
"""
Creates a wrapper to emulate quantization for the Multiply keras layer.
Args:
layer (tf.keras.layers.Layer): The keras layer to be quantized.
**kwargs: Additional keyword arguments to be passed to the keras layer.
"""
super().__init__(layer, **kwargs)
def build(self, input_shape):
super().build(input_shape)
def call(self, inputs, training=None):
return super().call(inputs, training=training)
class ConcatenateQuantizeWrapper(NonWeightedBaseQuantizeWrapperForMultipleInputs):
"""
TRT Rule:
Add Q/DQ to all inputs.
Alternative: If there is Q/DQ attached to the input of the op after concat, don't add Q/DQ to input to concat,
let TensorRT pull from the next op.
"""
def __init__(self, layer: tf.keras.layers.Layer, **kwargs):
"""
Creates a wrapper to emulate quantization for the Concatenate keras layer.
Args:
layer (tf.keras.layers.Layer): The keras layer to be quantized.
**kwargs: Additional keyword arguments to be passed to the keras layer.
"""
super().__init__(layer, **kwargs)
def build(self, input_shape):
super().build(input_shape)
def call(self, inputs, training=None):
return super().call(inputs, training=training)
class AddQuantizeWrapper(NonWeightedBaseQuantizeWrapperForMultipleInputs):
"""
TRT Rule:
If the add is NOT bias. Attach Q/DQ to all of its input.
Exception: add in residual block. To trigger fusion, Attach Q/DQ to the residual being added to output of
convolution.
"""
def __init__(self, layer: tf.keras.layers.Layer, **kwargs):
"""
Creates a wrapper to emulate quantization for the Add keras layer.
Args:
layer (tf.keras.layers.Layer): The keras layer to be quantized.
**kwargs: Additional keyword arguments to be passed to the keras layer.
"""
super().__init__(layer, **kwargs)
def build(self, input_shape):
super().build(input_shape)
def call(self, inputs, training=None):
return super().call(inputs, training=training)
@@ -0,0 +1,185 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
#
# 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.
"""
This module is borrowed from TFMOT repository and updated.
It implements QDQ insertion based on "Last Value Quantization".
"""
import tensorflow as tf
def LastValueQuantize(
inputs,
min_var,
max_var,
per_channel=False,
channel_axis=-1,
name_prefix="LastValueQuant",
is_training=True,
num_bits=8,
narrow_range=False,
symmetric=False,
):
"""Adds a layer that collects quantization ranges as last input ranges.
LastValueQuantize creates variables called 'min' and 'max', representing the
interval used for quantization and clamping.
Args:
inputs: a tensor containing values to be quantized.
per_channel: (Optional) a boolean specifying whether to use different
quantization ranges per output channel.
init_min: a float scalar, the initial value for variable min.
init_max: a float scalar, the initial value for variable max.
name_prefix: name_prefix for created nodes.
is_training: Whether the op is applied to a training or eval graph.
num_bits: Number of bits to use for quantization, must be between 2 and 8.
narrow_range: Whether to use the narrow quantization range
[1; 2^num_bits - 1] or wide range [0; 2^num_bits - 1].
symmetric: If true, use symmetric quantization limits instead of training
the minimum and maximum of each quantization range separately.
Returns:
a tensor containing quantized values.
"""
with tf.name_scope(name_prefix):
input_shape = inputs.get_shape()
input_dim = len(input_shape)
if channel_axis == -1:
channel_axis += input_dim
if not is_training:
return _QuantizeAndDequantize(
inputs,
min_var,
max_var,
per_channel=per_channel,
channel_axis=channel_axis,
num_bits=num_bits,
narrow_range=narrow_range,
)
if per_channel:
if input_dim == 2:
reduce_dims = [0]
elif input_dim == 4:
reduce_dims = [i for i in range(input_dim) if i != channel_axis]
if per_channel:
if input_dim >= 2:
batch_min = tf.math.reduce_min(
inputs, axis=reduce_dims, name="BatchMin"
)
else:
batch_min = inputs
else:
batch_min = tf.math.reduce_min(inputs, name="BatchMin")
if per_channel:
if input_dim >= 2:
batch_max = tf.math.reduce_max(
inputs, axis=reduce_dims, name="BatchMax"
)
else:
batch_max = inputs
else:
batch_max = tf.math.reduce_max(inputs, name="BatchMax")
if symmetric:
if narrow_range:
min_max_ratio = -1
else:
# In two's complement notation, the negative range is slightly larger
# than the positive range.
min_max_ratio = -((1 << num_bits) - 2) / (1 << num_bits)
# TFLite requires that 0.0 if always in the [min; max] range. Because
# batch_min <= batch_max, it follows that range_min <= 0 <= range_max.
range_min = tf.math.minimum(batch_min, batch_max / min_max_ratio)
range_max = tf.math.maximum(batch_max, batch_min * min_max_ratio)
else:
# TFLite requires that 0.0 if always in the [min; max] range.
range_min = tf.math.minimum(batch_min, 0.0)
range_max = tf.math.maximum(batch_max, 0.0)
assign_min = min_var.assign(range_min, name="AssignMinLast")
assign_max = max_var.assign(range_max, name="AssignMaxLast")
return _QuantizeAndDequantize(
inputs,
assign_min,
assign_max,
per_channel=per_channel,
channel_axis=channel_axis,
num_bits=num_bits,
narrow_range=narrow_range,
)
def _QuantizeAndDequantize(
inputs, min_var, max_var, per_channel, channel_axis, num_bits, narrow_range
):
"""Adds a fake quantization operation.
Depending on value of per_channel, this operation may do global quantization
or per channel quantization. min_var and max_var should have corresponding
shapes: [1] when per_channel == False and [d] when per_channel == True.
Args:
inputs: a tensor containing values to be quantized.
min_var: a variable containing quantization range lower end(s).
max_var: a variable containing quantization range upper end(s).
per_channel: a boolean specifying whether to use per-channel quantization.
num_bits: Number of bits to use for quantization, must be between 2 and 8.
narrow_range: Whether to use the narrow quantization range
[1; 2^num_bits - 1] or wide range [0; 2^num_bits - 1].
Returns:
a tensor containing quantized values.
"""
if per_channel:
return tf.quantization.quantize_and_dequantize_v2(
inputs,
min_var,
max_var,
num_bits=num_bits,
narrow_range=narrow_range,
axis=channel_axis,
range_given=True,
)
else:
assert min_var.get_shape() == [] # pylint: disable=g-explicit-bool-comparison
assert max_var.get_shape() == [] # pylint: disable=g-explicit-bool-comparison
return tf.quantization.quantize_and_dequantize_v2(
inputs,
min_var,
max_var,
num_bits=num_bits,
narrow_range=narrow_range,
range_given=True,
)
@@ -0,0 +1,364 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
#
import tensorflow as tf
from collections import deque
from typing import List
import os
import shutil
from tf2onnx import tf_loader, utils, convert
import copy
def ensure_and_clean_dir(dir_path, do_clean_dir=True) -> None:
"""Create a directory to save test logs
Args:
dir_path (str): directory to create / clean.
do_clean_dir (bool): boolean indicating whether to clean the directory if it already exists (remove+create).
Returns:
None
"""
if not os.path.exists(dir_path):
os.makedirs(dir_path)
elif do_clean_dir:
shutil.rmtree(dir_path)
os.makedirs(dir_path)
class Folder:
"""
Folder class that tracks all files for a single experiment.
"""
def __init__(self, folder_name) -> None:
self.base = folder_name
ensure_and_clean_dir(self.base)
self.fp32 = os.path.join(self.base, "fp32")
ensure_and_clean_dir(self.fp32)
self.fp32_saved_model = os.path.join(
self.fp32, "saved_model"
) # location of fp32 saved keras model
self.fp32_onnx_model = os.path.join(
self.fp32, "original.onnx"
) # location of fp32 onnx model
self.int8 = os.path.join(self.base, "int8")
ensure_and_clean_dir(self.int8)
self.int8_saved_model = os.path.join(
self.int8, "saved_model"
) # location of int8 saved keras model
self.int8_onnx_model = os.path.join(
self.int8, "quantized.onnx"
) # location of int8 onnx model
class CreateAssetsFolders:
"""Create empty folders to save the original and quantized TensorFlow models and their respective ONNX
models for each experiment.
The following directory structure is created: base_directory -> experiment_directory (created by `add_folder` method) -> (fp32 [saved_model, .onnx model]),
(int8 [saved_model, .onnx model]).
"""
def __init__(self, base_experiment_directory) -> None:
self.base = base_experiment_directory
if not os.path.exists(self.base):
os.mkdir(self.base)
def add_folder(self, folder_name: str) -> None:
"""
Create the experiment directory (sub-folder in the base directory passed to this class).
Args:
folder_name (str): name of folder
Returns:
None
"""
setattr(self, folder_name, Folder(os.path.join(self.base, folder_name)))
def convert_saved_model_to_onnx(
saved_model_dir: str, onnx_model_path: str, opset=13
) -> None:
"""Convert Keras saved model into ONNX format.
Works directly with CreateAssetsFolder object path.
Args:
saved_model_dir (str): Path to keras saved model.
onnx_model_path (str): Full path to ONNX model file.
Returns:
None
"""
# 1. Let TensorRT optimize QDQ nodes instead of TF
from tf2onnx.optimizer import _optimizers
updated_optimizers = copy.deepcopy(_optimizers)
del updated_optimizers["q_dq_optimizer"]
del updated_optimizers["const_dequantize_optimizer"]
# 2. Extract graph definition from SavedModel
graph_def, inputs, outputs = tf_loader.from_saved_model(
model_path=saved_model_dir,
input_names=None,
output_names=None,
tag="serve",
signatures=["serving_default"],
)
# 3. Convert tf2onnx and save onnx file
model_proto, _ = convert._convert_common(
graph_def,
opset=opset,
input_names=inputs,
output_names=outputs,
output_path=onnx_model_path,
optimizers=updated_optimizers,
)
utils.save_protobuf(onnx_model_path, model_proto)
print("ONNX conversion Done!")
def convert_keras_model_to_onnx(
keras_model: tf.keras.Model, onnx_model_path: str, opset=13
) -> None:
"""Convert in-memory Keras model into ONNX format.
Works directly with CreateAssetsFolder object path.
Args:
keras_model (tf.keras.Model): Keras model.
onnx_model_path (str): Full path to ONNX model file.
Returns:
None
"""
# 1. Let TensorRT optimize QDQ nodes instead of TF
from tf2onnx.optimizer import _optimizers
updated_optimizers = copy.deepcopy(_optimizers)
del updated_optimizers["q_dq_optimizer"]
del updated_optimizers["const_dequantize_optimizer"]
# 2. Convert keras model directly and save onnx file.
onnx_model_proto, _ = convert.from_keras(keras_model, opset=opset, optimizers=updated_optimizers)
utils.save_protobuf(onnx_model_path, onnx_model_proto)
class KerasModelTraveller:
"""
Utility class to travel Keras model and print out detailed layer information.
"""
def __init__(self, print_layer_config=False) -> None:
self._pc = print_layer_config
self.model_list = deque([])
# Used to filter which classes you want printed, by layer.__class__
self._filter_by_class = None
self._layer_names = []
self._print_basic_info = None
def _print_layer_info(self, layer):
assert isinstance(layer, tf.keras.layers.Layer)
if self._filter_by_class is None or layer.__class__ in self._filter_by_class:
self._layer_names.append(layer.name)
if self._print_basic_info:
print(
"layer name:{layer_name}, layer class:{layer_class}".format(
layer_name=layer.name, layer_class=layer.__class__
)
)
if self._pc:
print(layer.get_config())
if self._print_basic_info:
print("-----------------")
def _dissect(self):
if not self.model_list:
return
number_of_models = len(self.model_list)
for _ in range(number_of_models):
# Get a subclassed model
current_model = self.model_list.pop()
print("Keras Subclassed Model: {}".format(current_model.__class__.__name__))
assert isinstance(current_model, tf.keras.Model)
for l in current_model.layers:
if isinstance(l, tf.keras.Model):
# This is another subclassed model inside
# Add this model to model queue for further analysis
self.model_list.appendleft(l)
self._dissect()
else:
# This is a layer
self._print_layer_info(l)
def _travel(
self, keras_model: tf.keras.Model, filter_by_class=None, print_basic_info=False
):
"""Gets layer info by dissecting the model (need for multi-layered models)
Args:
keras_model (tf.keras.Model): Keras model
filter_by_class (str): None or array of layer.__class__ to print
Returns:
None
"""
self.filter_by_class = filter_by_class
self._print_basic_info = print_basic_info
assert isinstance(
keras_model, tf.keras.Model
), "Model passed is not Keras model"
self.model_list.appendleft(keras_model)
self._dissect()
self.filter_by_class = None
def get_layer_names(self, keras_model: tf.keras.Model, filter_by_class=None):
"""Get name of all layers in the model.
Args:
keras_model (tf.keras.Model): Keras model
filter_by_class (str): None or array of layer.__class__ to print
Returns:
None
"""
self._travel(keras_model=keras_model, filter_by_class=filter_by_class)
return self._layer_names
def get_layer_information(self, keras_model: tf.keras.Model, filter_by_class=None):
"""Print information about all layers.
Args:
keras_model (tf.keras.Model): Keras model
filter_by_class (str): None or array of layer.__class__ to print
Returns:
None
"""
self._travel(
keras_model=keras_model,
filter_by_class=filter_by_class,
print_basic_info=True,
)
def _get_layer_info(layer: tf.keras.layers.Layer) -> dict:
"""
Returns the layer's class, module, and name
"""
return {
"class": layer.__class__.__name__,
"module": layer.__class__.__module__,
"name": layer.name,
"layer": layer,
}
def _get_previous_layers_class_and_module_and_name(
layer: tf.keras.layers.Layer,
) -> List[dict]:
"""
For a given layer return a dictionary with name, module and class information of all previous layers.
"""
r = []
if isinstance(layer.input, list):
for layer_input_tensor in layer.input:
ip_tensor_parent_layer = layer_input_tensor._keras_history.layer
r.append(_get_layer_info(ip_tensor_parent_layer))
else:
ip_tensor_parent_layer = layer.input._keras_history.layer
r.append(_get_layer_info(ip_tensor_parent_layer))
return r
def find_my_predecessors(model: tf.keras.Model, current_layer_name: str) -> List[dict]:
"""
Given a layer name, find all predecessors of that layer.
Args:
model (tf.keras.Model): Keras functional model
current_layer_name (str): name of a model layer for which predecessors has to be found.
Returns:
List[dict]: List of predecessors. Each dictionary has three keys as follows,
::
{'class':<pred_layer_class>, 'module':<pred_layer_module>, 'name':<pred_layer_name>}
Raises:
AssertionError: If model is subclassed or current_layer_name is not string.
"""
supported_model_classes = {"Functional", "Sequential"}
assert isinstance(current_layer_name, str), "current layer name should be passed."
assert (
model.__class__.__name__ in supported_model_classes
), "model should be Functional or Sequential."
for layer in model.layers:
if layer.name == current_layer_name:
return _get_previous_layers_class_and_module_and_name(layer)
def find_my_successors(model: tf.keras.Model, current_layer_name: str) -> List[dict]:
"""
Given a layer name, find all successors of that layer.
Args:
model (tf.keras.Model): Keras functional model
current_layer_name (str): name of a model layer for which successors has to be found.
Returns:
List[dict]: List of predecessors. Each dictionary has three keys as follows,
::
{'class':<pred_layer_class>, 'module':<pred_layer_module>, 'name':<pred_layer_name>}
Raises:
AssertionError: If model is subclassed or current_layer_name is not string.
"""
supported_model_classes = {"Functional", "Sequential"}
assert isinstance(current_layer_name, str), "current layer name should be passed."
assert (
model.__class__.__name__ in supported_model_classes
), "model should be Functional or Sequential."
def _check_all_next_layers_with_connection_to_current(
next_layers: List[tf.keras.layers.Layer],
current_layer_name: str,
current_layer_class: str,
):
successors = []
for layer in next_layers:
p_layers = _get_previous_layers_class_and_module_and_name(layer)
for p_layer in p_layers:
if (
p_layer["class"] == current_layer_class
and p_layer["name"] == current_layer_name
):
successors.append(_get_layer_info(layer))
return successors
all_layers = model.layers
for i, layer in enumerate(all_layers):
if layer.name == current_layer_name:
next_layers = all_layers[i + 1 :]
layer_info = _get_layer_info(layer)
return _check_all_next_layers_with_connection_to_current(
next_layers, layer_info["name"], layer_info["class"]
)