274 lines
9.8 KiB
Python
274 lines
9.8 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.
|
|
#
|
|
|
|
|
|
"""
|
|
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
|