446 lines
19 KiB
Python
446 lines
19 KiB
Python
#
|
|
# 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)
|