ai-edge-quantizer-nightly 0.0.1.dev20250302__py3-none-any.whl → 0.5.0.dev20260103__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai_edge_quantizer/algorithm_manager.py +224 -0
- ai_edge_quantizer/algorithm_manager_api_test.py +7 -0
- ai_edge_quantizer/algorithms/nonlinear_quantize/float_casting_test.py +2 -2
- ai_edge_quantizer/algorithms/uniform_quantize/common_quantize.py +643 -20
- ai_edge_quantizer/algorithms/uniform_quantize/common_quantize_test.py +29 -2
- ai_edge_quantizer/algorithms/uniform_quantize/dequantized_weight_recovery.py +29 -35
- ai_edge_quantizer/algorithms/uniform_quantize/dequantized_weight_recovery_test.py +35 -12
- ai_edge_quantizer/algorithms/uniform_quantize/hadamard_rotation.py +414 -0
- ai_edge_quantizer/algorithms/uniform_quantize/hadamard_rotation_test.py +440 -0
- ai_edge_quantizer/algorithms/uniform_quantize/mse.py +127 -0
- ai_edge_quantizer/algorithms/uniform_quantize/mse_test.py +195 -0
- ai_edge_quantizer/algorithms/uniform_quantize/naive_min_max_quantize.py +54 -168
- ai_edge_quantizer/algorithms/uniform_quantize/naive_min_max_quantize_test.py +54 -17
- ai_edge_quantizer/algorithms/uniform_quantize/octav.py +188 -0
- ai_edge_quantizer/algorithms/uniform_quantize/octav_test.py +240 -0
- ai_edge_quantizer/algorithms/uniform_quantize/uniform_quantize_tensor.py +260 -13
- ai_edge_quantizer/algorithms/uniform_quantize/uniform_quantize_tensor_test.py +152 -5
- ai_edge_quantizer/algorithms/utils/common_utils.py +142 -54
- ai_edge_quantizer/calibrator.py +58 -94
- ai_edge_quantizer/calibrator_test.py +5 -74
- ai_edge_quantizer/default_policy.py +108 -16
- ai_edge_quantizer/model_modifier.py +132 -8
- ai_edge_quantizer/model_modifier_test.py +81 -1
- ai_edge_quantizer/model_validator.py +38 -10
- ai_edge_quantizer/model_validator_test.py +2 -1
- ai_edge_quantizer/params_generator.py +230 -47
- ai_edge_quantizer/params_generator_test.py +366 -261
- ai_edge_quantizer/qtyping.py +92 -6
- ai_edge_quantizer/quantizer.py +167 -23
- ai_edge_quantizer/quantizer_test.py +288 -26
- ai_edge_quantizer/recipe.py +156 -21
- ai_edge_quantizer/recipe_manager.py +158 -1
- ai_edge_quantizer/recipe_manager_test.py +146 -32
- ai_edge_quantizer/recipe_test.py +93 -17
- ai_edge_quantizer/transformation_instruction_generator.py +313 -46
- ai_edge_quantizer/transformation_instruction_generator_test.py +449 -27
- ai_edge_quantizer/transformation_performer.py +112 -58
- ai_edge_quantizer/transformation_performer_test.py +176 -4
- ai_edge_quantizer/transformations/duplicate_buffer.py +46 -0
- ai_edge_quantizer/transformations/duplicate_buffer_test.py +106 -0
- ai_edge_quantizer/transformations/duplicate_tensor.py +62 -0
- ai_edge_quantizer/transformations/duplicate_tensor_test.py +131 -0
- ai_edge_quantizer/transformations/insert_decomposed_hadamard_rotation.py +299 -0
- ai_edge_quantizer/transformations/insert_decomposed_hadamard_rotation_test.py +244 -0
- ai_edge_quantizer/transformations/insert_hadamard_rotation.py +186 -0
- ai_edge_quantizer/transformations/insert_hadamard_rotation_test.py +200 -0
- ai_edge_quantizer/transformations/quantize_tensor.py +24 -44
- ai_edge_quantizer/transformations/quantize_tensor_test.py +3 -2
- ai_edge_quantizer/transformations/transformation_utils.py +157 -11
- ai_edge_quantizer/transformations/transformation_utils_test.py +96 -2
- ai_edge_quantizer/utils/calibration_utils.py +263 -1
- ai_edge_quantizer/utils/calibration_utils_test.py +173 -3
- ai_edge_quantizer/utils/constrained_ops_utils.py +111 -0
- ai_edge_quantizer/utils/constrained_ops_utils_test.py +50 -0
- ai_edge_quantizer/utils/test_utils.py +191 -58
- ai_edge_quantizer/utils/tfl_flatbuffer_utils.py +96 -50
- ai_edge_quantizer/utils/tfl_flatbuffer_utils_test.py +20 -0
- ai_edge_quantizer/utils/tfl_interpreter_utils.py +138 -5
- ai_edge_quantizer/utils/tfl_interpreter_utils_test.py +29 -2
- ai_edge_quantizer/utils/validation_utils.py +114 -4
- ai_edge_quantizer/utils/validation_utils_test.py +80 -0
- {ai_edge_quantizer_nightly-0.0.1.dev20250302.dist-info → ai_edge_quantizer_nightly-0.5.0.dev20260103.dist-info}/METADATA +13 -3
- ai_edge_quantizer_nightly-0.5.0.dev20260103.dist-info/RECORD +81 -0
- {ai_edge_quantizer_nightly-0.0.1.dev20250302.dist-info → ai_edge_quantizer_nightly-0.5.0.dev20260103.dist-info}/WHEEL +1 -1
- ai_edge_quantizer/transformations/emulated_subchannel.py +0 -363
- ai_edge_quantizer/transformations/emulated_subchannel_test.py +0 -212
- ai_edge_quantizer_nightly-0.0.1.dev20250302.dist-info/RECORD +0 -67
- {ai_edge_quantizer_nightly-0.0.1.dev20250302.dist-info → ai_edge_quantizer_nightly-0.5.0.dev20260103.dist-info/licenses}/LICENSE +0 -0
- {ai_edge_quantizer_nightly-0.0.1.dev20250302.dist-info → ai_edge_quantizer_nightly-0.5.0.dev20260103.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Copyright 2024 The AI Edge Quantizer Authors.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from typing import cast
|
|
18
|
+
|
|
19
|
+
from absl.testing import parameterized
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
from tensorflow.python.platform import googletest
|
|
23
|
+
from ai_edge_quantizer import qtyping
|
|
24
|
+
from ai_edge_quantizer.algorithms.uniform_quantize import mse
|
|
25
|
+
from ai_edge_quantizer.utils import test_utils
|
|
26
|
+
from ai_edge_quantizer.utils import tfl_flatbuffer_utils
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MseQuantizeTest(parameterized.TestCase):
|
|
30
|
+
"""Tests for general functions for MSE."""
|
|
31
|
+
|
|
32
|
+
def setUp(self):
|
|
33
|
+
super().setUp()
|
|
34
|
+
np.random.seed(666)
|
|
35
|
+
self._test_model_path = os.path.join(
|
|
36
|
+
test_utils.get_path_to_datafile("../../tests/models"),
|
|
37
|
+
"conv_fc_mnist.tflite",
|
|
38
|
+
)
|
|
39
|
+
self._test_model = tfl_flatbuffer_utils.read_model(self._test_model_path)
|
|
40
|
+
# The test model has one subgraph for now.
|
|
41
|
+
self._graph_info = qtyping.GraphInfo(
|
|
42
|
+
subgraph_tensors=self._test_model.subgraphs[0].tensors,
|
|
43
|
+
buffers=self._test_model.buffers,
|
|
44
|
+
)
|
|
45
|
+
self._tensor_name_to_qsv = {}
|
|
46
|
+
subgraph0 = self._test_model.subgraphs[0]
|
|
47
|
+
self._subgraph_op_index = 3
|
|
48
|
+
self._fc_op = subgraph0.operators[self._subgraph_op_index]
|
|
49
|
+
self._fc_op_info = qtyping.OpInfo(
|
|
50
|
+
op=self._fc_op,
|
|
51
|
+
op_name=qtyping.TFLOperationName.FULLY_CONNECTED,
|
|
52
|
+
subgraph_op_index=self._subgraph_op_index,
|
|
53
|
+
op_quant_config=qtyping.OpQuantizationConfig(
|
|
54
|
+
weight_tensor_config=None,
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def test_get_tensor_quant_params_raises_error_with_unsupported_symmetry(self):
|
|
59
|
+
err_msg = "Unsupported symmetry"
|
|
60
|
+
test_data = np.array([[-7, 7], [4, -4], [4, -4], [7, 7]])
|
|
61
|
+
with self.assertRaisesWithPredicateMatch(
|
|
62
|
+
ValueError, lambda err: err_msg in str(err)
|
|
63
|
+
):
|
|
64
|
+
_ = mse.get_tensor_quant_params(
|
|
65
|
+
op_info=self._fc_op_info,
|
|
66
|
+
tensor_quant_config=qtyping.TensorQuantizationConfig(
|
|
67
|
+
num_bits=4,
|
|
68
|
+
symmetric=False,
|
|
69
|
+
granularity=qtyping.QuantGranularity.CHANNELWISE,
|
|
70
|
+
),
|
|
71
|
+
tensor_content=test_data,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def test_get_tensor_quant_params_raises_error_with_unsupported_granularity(
|
|
75
|
+
self,
|
|
76
|
+
):
|
|
77
|
+
err_msg = "Blockwise quantization is not supported"
|
|
78
|
+
test_data = np.array([[-7, 7], [4, -4], [4, -4], [7, 7]])
|
|
79
|
+
with self.assertRaisesWithPredicateMatch(
|
|
80
|
+
ValueError, lambda err: err_msg in str(err)
|
|
81
|
+
):
|
|
82
|
+
_ = mse.get_tensor_quant_params(
|
|
83
|
+
op_info=self._fc_op_info,
|
|
84
|
+
tensor_quant_config=qtyping.TensorQuantizationConfig(
|
|
85
|
+
num_bits=4,
|
|
86
|
+
symmetric=True,
|
|
87
|
+
granularity=qtyping.QuantGranularity.BLOCKWISE_32,
|
|
88
|
+
),
|
|
89
|
+
tensor_content=test_data,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def test_get_tensor_quant_params_succeeds_with_qsv(self):
|
|
93
|
+
# Fall back to naive_min_max_quantize.py for non-weight tensors.
|
|
94
|
+
tensor_quant_params = mse.get_tensor_quant_params(
|
|
95
|
+
op_info=self._fc_op_info,
|
|
96
|
+
tensor_quant_config=qtyping.TensorQuantizationConfig(
|
|
97
|
+
num_bits=8,
|
|
98
|
+
granularity=qtyping.QuantGranularity.TENSORWISE,
|
|
99
|
+
),
|
|
100
|
+
tensor_qsv={
|
|
101
|
+
"min": np.array([-1]),
|
|
102
|
+
"max": np.array([1]),
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
self.assertIsNone(tensor_quant_params.quantized_dimension)
|
|
107
|
+
scale = tensor_quant_params.scale
|
|
108
|
+
self.assertEqual(scale.shape, (1,))
|
|
109
|
+
self.assertSequenceAlmostEqual(scale.flatten(), [1 / 127])
|
|
110
|
+
|
|
111
|
+
# Zero point should be zero for symmetric quantization.
|
|
112
|
+
zp = tensor_quant_params.zero_point
|
|
113
|
+
self.assertEqual(np.sum(zp), 0)
|
|
114
|
+
self.assertEqual(zp.shape, (1,))
|
|
115
|
+
|
|
116
|
+
def test_get_tensor_quant_params_succeeds_with_tensorwise_granularity(self):
|
|
117
|
+
test_data = np.array([
|
|
118
|
+
[-1e5, 25, -50, 75, -100, 125],
|
|
119
|
+
[25, -30, 50, -75, 1e5, -125],
|
|
120
|
+
[50, -60, 70, -80, 90, -100],
|
|
121
|
+
])
|
|
122
|
+
tensor_config = qtyping.TensorQuantizationConfig(
|
|
123
|
+
num_bits=4,
|
|
124
|
+
symmetric=True,
|
|
125
|
+
granularity=qtyping.QuantGranularity.TENSORWISE,
|
|
126
|
+
)
|
|
127
|
+
fc_op_info = qtyping.OpInfo(
|
|
128
|
+
op=self._fc_op,
|
|
129
|
+
op_name=qtyping.TFLOperationName.FULLY_CONNECTED,
|
|
130
|
+
subgraph_op_index=self._subgraph_op_index,
|
|
131
|
+
op_quant_config=qtyping.OpQuantizationConfig(
|
|
132
|
+
weight_tensor_config=tensor_config,
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
quant_params = mse.get_tensor_quant_params(
|
|
136
|
+
op_info=fc_op_info,
|
|
137
|
+
tensor_quant_config=tensor_config,
|
|
138
|
+
tensor_content=test_data,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
with self.subTest(name="CheckQuantParamsShapes"):
|
|
142
|
+
self.assertEqual(quant_params.zero_point.shape, (1, 1))
|
|
143
|
+
self.assertEqual(quant_params.scale.shape, (1, 1))
|
|
144
|
+
self.assertIsNone(quant_params.quantized_dimension)
|
|
145
|
+
self.assertIsNotNone(quant_params.quantized_data)
|
|
146
|
+
self.assertTupleEqual(
|
|
147
|
+
cast(np.ndarray, quant_params.quantized_data).shape, test_data.shape
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
with self.subTest(name="CheckQuantParamsValues"):
|
|
151
|
+
self.assertTrue(np.all(quant_params.zero_point == 0))
|
|
152
|
+
|
|
153
|
+
def test_get_tensor_quant_params_succeeds_with_channelwise_granularity(self):
|
|
154
|
+
# Test that the call generates quant params that are appropriately shaped,
|
|
155
|
+
# have some clipping, and correct config values without checking the
|
|
156
|
+
# actual values numerically.
|
|
157
|
+
test_data = np.array([
|
|
158
|
+
[-1e5, 25, -50, 75, -100, 125],
|
|
159
|
+
[25, -30, 50, -75, 1e5, -125],
|
|
160
|
+
[50, -60, 70, -80, 90, -100],
|
|
161
|
+
])
|
|
162
|
+
tensor_config = qtyping.TensorQuantizationConfig(
|
|
163
|
+
num_bits=4,
|
|
164
|
+
symmetric=True,
|
|
165
|
+
granularity=qtyping.QuantGranularity.CHANNELWISE,
|
|
166
|
+
)
|
|
167
|
+
fc_op_info = qtyping.OpInfo(
|
|
168
|
+
op=self._fc_op,
|
|
169
|
+
op_name=qtyping.TFLOperationName.FULLY_CONNECTED,
|
|
170
|
+
subgraph_op_index=self._subgraph_op_index,
|
|
171
|
+
op_quant_config=qtyping.OpQuantizationConfig(
|
|
172
|
+
weight_tensor_config=tensor_config,
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
quant_params = mse.get_tensor_quant_params(
|
|
176
|
+
op_info=fc_op_info,
|
|
177
|
+
tensor_quant_config=tensor_config,
|
|
178
|
+
tensor_content=test_data,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
with self.subTest(name="CheckQuantParamsShapes"):
|
|
182
|
+
self.assertEqual(quant_params.zero_point.shape, (test_data.shape[0], 1))
|
|
183
|
+
self.assertEqual(quant_params.scale.shape, (test_data.shape[0], 1))
|
|
184
|
+
self.assertIsNotNone(quant_params.quantized_data)
|
|
185
|
+
self.assertTupleEqual(
|
|
186
|
+
cast(np.ndarray, quant_params.quantized_data).shape, test_data.shape
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
with self.subTest(name="CheckQuantParamsValues"):
|
|
190
|
+
self.assertTrue(np.all(quant_params.zero_point == 0))
|
|
191
|
+
self.assertEqual(quant_params.quantized_dimension, 0)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if __name__ == "__main__":
|
|
195
|
+
googletest.main()
|
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
|
|
16
16
|
"""Performs naive min/max uniform quantization."""
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
import dataclasses
|
|
19
19
|
from typing import Any, Optional
|
|
20
20
|
import numpy as np
|
|
21
21
|
from ai_edge_quantizer import qtyping
|
|
22
|
+
from ai_edge_quantizer.algorithms.uniform_quantize import common_quantize
|
|
22
23
|
from ai_edge_quantizer.algorithms.uniform_quantize import uniform_quantize_tensor
|
|
23
24
|
from ai_edge_quantizer.algorithms.utils import common_utils
|
|
24
25
|
from ai_edge_quantizer.utils import tfl_flatbuffer_utils
|
|
@@ -29,143 +30,6 @@ _QuantTransformation = qtyping.QuantTransformation
|
|
|
29
30
|
_IntType = uniform_quantize_tensor.IntType
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
def _init_tensor_min_max(
|
|
33
|
-
tensor_data: Optional[np.ndarray],
|
|
34
|
-
op_info: qtyping.OpInfo,
|
|
35
|
-
) -> qtyping.QSV:
|
|
36
|
-
"""Initialize the min/max for a tensor."""
|
|
37
|
-
if tensor_data is None:
|
|
38
|
-
return {}
|
|
39
|
-
else:
|
|
40
|
-
weight_tensor_config = op_info.op_quant_config.weight_tensor_config
|
|
41
|
-
quantized_dim = None
|
|
42
|
-
if weight_tensor_config is not None and (
|
|
43
|
-
weight_tensor_config.granularity == qtyping.QuantGranularity.CHANNELWISE
|
|
44
|
-
or weight_tensor_config.granularity
|
|
45
|
-
== qtyping.QuantGranularity.BLOCKWISE
|
|
46
|
-
):
|
|
47
|
-
quantized_dim = common_utils.get_weight_quantized_dim(
|
|
48
|
-
op_info, tensor_data
|
|
49
|
-
)
|
|
50
|
-
if (
|
|
51
|
-
weight_tensor_config is not None
|
|
52
|
-
and weight_tensor_config.granularity
|
|
53
|
-
== qtyping.QuantGranularity.BLOCKWISE
|
|
54
|
-
):
|
|
55
|
-
reshaped_data, reduce_dims = _reshape_data_for_blockwise(
|
|
56
|
-
tensor_data,
|
|
57
|
-
quantized_dim,
|
|
58
|
-
weight_tensor_config.block_size,
|
|
59
|
-
)
|
|
60
|
-
return {
|
|
61
|
-
"min": np.min(reshaped_data, axis=reduce_dims, keepdims=False),
|
|
62
|
-
"max": np.max(reshaped_data, axis=reduce_dims, keepdims=False),
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
else:
|
|
66
|
-
reduce_dims = common_utils.get_reduce_dims(
|
|
67
|
-
quantized_dim, tensor_data.shape
|
|
68
|
-
)
|
|
69
|
-
return {
|
|
70
|
-
"min": np.min(tensor_data, axis=reduce_dims, keepdims=True),
|
|
71
|
-
"max": np.max(tensor_data, axis=reduce_dims, keepdims=True),
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _get_tensor_shape_for_blockwise(
|
|
76
|
-
tensor_shape: Sequence[int], quantized_dim: int, block_size: int
|
|
77
|
-
) -> list[int]:
|
|
78
|
-
"""Get the tensor shape for blockwise quantization.
|
|
79
|
-
|
|
80
|
-
This function splits the quantize dimension of the tensor into blocks and the
|
|
81
|
-
dim/blocks. Hence, min/max of the tensor can be calculated for each block
|
|
82
|
-
using existing functions.
|
|
83
|
-
|
|
84
|
-
Args:
|
|
85
|
-
tensor_shape: The original shape of the tensor.
|
|
86
|
-
quantized_dim: The dimension to be quantized blockwise.
|
|
87
|
-
block_size: The size of the block.
|
|
88
|
-
|
|
89
|
-
Returns:
|
|
90
|
-
The new tensor shape for calculating scale and zp for blockwise
|
|
91
|
-
quantization.
|
|
92
|
-
"""
|
|
93
|
-
new_shape = []
|
|
94
|
-
for index, val in enumerate(tensor_shape):
|
|
95
|
-
if index == quantized_dim:
|
|
96
|
-
new_shape.append(int(val / block_size))
|
|
97
|
-
new_shape.append(block_size)
|
|
98
|
-
else:
|
|
99
|
-
new_shape.append(val)
|
|
100
|
-
return new_shape
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _reshape_data_for_blockwise(
|
|
104
|
-
tensor_data: np.ndarray, quantized_dim: int, block_size: int
|
|
105
|
-
) -> tuple[np.ndarray, int]:
|
|
106
|
-
"""Reshapes data for blockwise quantization.
|
|
107
|
-
|
|
108
|
-
Args:
|
|
109
|
-
tensor_data: The original tensor data.
|
|
110
|
-
quantized_dim: The dimension to be quantized blockwise.
|
|
111
|
-
block_size: The size of the block.
|
|
112
|
-
|
|
113
|
-
Returns:
|
|
114
|
-
A tuple containing the reshaped tensor data and the new reduce dimension.
|
|
115
|
-
"""
|
|
116
|
-
new_shape = _get_tensor_shape_for_blockwise(
|
|
117
|
-
tensor_data.shape, quantized_dim, block_size
|
|
118
|
-
)
|
|
119
|
-
reshaped_data = tensor_data.reshape(new_shape)
|
|
120
|
-
return reshaped_data, quantized_dim + 1
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def _broadcast_scale_zp_for_blockwise(
|
|
124
|
-
tensor_content: np.ndarray,
|
|
125
|
-
quant_params: qtyping.UniformQuantParams,
|
|
126
|
-
) -> qtyping.UniformQuantParams:
|
|
127
|
-
"""Broadcasts scale and zp for blockwise quantization.
|
|
128
|
-
|
|
129
|
-
Args:
|
|
130
|
-
tensor_content: The original tensor data.
|
|
131
|
-
quant_params: The quantization parameters.
|
|
132
|
-
|
|
133
|
-
Returns:
|
|
134
|
-
The updated quantization parameters with broadcasted scale and zp for
|
|
135
|
-
correct constant quantization.
|
|
136
|
-
"""
|
|
137
|
-
if quant_params.quantized_dimension is None:
|
|
138
|
-
raise ValueError("Quantized dimension must be specified.")
|
|
139
|
-
if quant_params.block_size is None or quant_params.block_size <= 0:
|
|
140
|
-
raise ValueError("Block size must be specified and positive.")
|
|
141
|
-
quantized_dim = quant_params.quantized_dimension
|
|
142
|
-
expanded_tensor_shape = _get_tensor_shape_for_blockwise(
|
|
143
|
-
tensor_content.shape, quantized_dim, quant_params.block_size
|
|
144
|
-
)
|
|
145
|
-
expanded_scale = np.reshape(
|
|
146
|
-
np.broadcast_to(
|
|
147
|
-
np.expand_dims(quant_params.scale, quantized_dim + 1),
|
|
148
|
-
expanded_tensor_shape,
|
|
149
|
-
),
|
|
150
|
-
tensor_content.shape,
|
|
151
|
-
)
|
|
152
|
-
expanded_zp = np.reshape(
|
|
153
|
-
np.broadcast_to(
|
|
154
|
-
np.expand_dims(quant_params.zero_point, quantized_dim + 1),
|
|
155
|
-
expanded_tensor_shape,
|
|
156
|
-
),
|
|
157
|
-
tensor_content.shape,
|
|
158
|
-
)
|
|
159
|
-
return qtyping.UniformQuantParams(
|
|
160
|
-
scale=expanded_scale,
|
|
161
|
-
zero_point=expanded_zp,
|
|
162
|
-
num_bits=quant_params.num_bits,
|
|
163
|
-
symmetric=quant_params.symmetric,
|
|
164
|
-
quantized_dimension=quantized_dim,
|
|
165
|
-
block_size=quant_params.block_size,
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
|
|
169
33
|
def get_tensor_quant_params(
|
|
170
34
|
op_info: qtyping.OpInfo,
|
|
171
35
|
tensor_quant_config: qtyping.TensorQuantizationConfig,
|
|
@@ -191,7 +55,7 @@ def get_tensor_quant_params(
|
|
|
191
55
|
# weight-only and DRQ do not require calibration, thus it is
|
|
192
56
|
# possible that this information is missing here. In that case we
|
|
193
57
|
# collect min/max on the spot.
|
|
194
|
-
tensor_min_max =
|
|
58
|
+
tensor_min_max = common_quantize.init_tensor_min_max(
|
|
195
59
|
tensor_content,
|
|
196
60
|
op_info,
|
|
197
61
|
)
|
|
@@ -210,50 +74,44 @@ def get_tensor_quant_params(
|
|
|
210
74
|
" parameters. Check if the correct calibration results are passed into"
|
|
211
75
|
" the ParamsGenerator."
|
|
212
76
|
)
|
|
77
|
+
clipping_values = None
|
|
213
78
|
zp, scale = uniform_quantize_tensor.tensor_zp_scale_from_min_max(
|
|
214
79
|
tensor_min_max["min"],
|
|
215
80
|
tensor_min_max["max"],
|
|
216
81
|
tensor_quant_config.num_bits,
|
|
217
82
|
tensor_quant_config.symmetric,
|
|
83
|
+
tensor_quant_config.granularity,
|
|
84
|
+
clipping_values,
|
|
85
|
+
)
|
|
86
|
+
quantized_dim = common_utils.get_weight_quantized_dim(
|
|
87
|
+
op_info, tensor_content, tensor_quant_config.granularity
|
|
218
88
|
)
|
|
219
|
-
quantized_dim = None
|
|
220
|
-
if (
|
|
221
|
-
tensor_quant_config.granularity == qtyping.QuantGranularity.CHANNELWISE
|
|
222
|
-
or tensor_quant_config.granularity == qtyping.QuantGranularity.BLOCKWISE
|
|
223
|
-
):
|
|
224
|
-
quantized_dim = common_utils.get_weight_quantized_dim(
|
|
225
|
-
op_info, tensor_content
|
|
226
|
-
)
|
|
227
89
|
quant_params = qtyping.UniformQuantParams(
|
|
228
90
|
scale=scale,
|
|
229
91
|
zero_point=zp,
|
|
230
92
|
num_bits=tensor_quant_config.num_bits,
|
|
231
93
|
symmetric=tensor_quant_config.symmetric,
|
|
232
94
|
quantized_dimension=quantized_dim,
|
|
233
|
-
block_size=
|
|
95
|
+
block_size=uniform_quantize_tensor.extract_block_size_from_granularity(
|
|
96
|
+
tensor_quant_config.granularity
|
|
97
|
+
),
|
|
234
98
|
)
|
|
235
99
|
if tensor_content is None:
|
|
236
100
|
return quant_params
|
|
237
101
|
|
|
238
|
-
# The reshaping for blockwise quantization is unique hence we do this here
|
|
239
|
-
# to avoid unexpected broadcast behavior downstream.
|
|
240
|
-
if tensor_quant_config.granularity == qtyping.QuantGranularity.BLOCKWISE:
|
|
241
|
-
quant_params = _broadcast_scale_zp_for_blockwise(
|
|
242
|
-
tensor_content, quant_params
|
|
243
|
-
)
|
|
244
|
-
|
|
245
102
|
quantized_vars = uniform_quantize_tensor.uniform_quantize(
|
|
246
|
-
tensor_content,
|
|
103
|
+
tensor_content,
|
|
104
|
+
quant_params,
|
|
105
|
+
uniform_quantize_tensor.is_blockwise(tensor_quant_config.granularity),
|
|
247
106
|
)
|
|
248
107
|
# Update with quantized values.
|
|
249
|
-
return
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
block_size=tensor_quant_config.block_size,
|
|
108
|
+
return dataclasses.replace(quant_params, quantized_data=quantized_vars)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def check_if_quantized(tensor: Any) -> bool:
|
|
112
|
+
"""Checks if the tensor is quantized."""
|
|
113
|
+
return (
|
|
114
|
+
tensor.quantization is not None and tensor.quantization.scale is not None
|
|
257
115
|
)
|
|
258
116
|
|
|
259
117
|
|
|
@@ -278,6 +136,13 @@ def init_qsvs(
|
|
|
278
136
|
op_qsvs = {}
|
|
279
137
|
|
|
280
138
|
inputs_to_ignore = inputs_to_ignore or []
|
|
139
|
+
quantized_inputs_to_ignore = [
|
|
140
|
+
opr_idx
|
|
141
|
+
for opr_idx, tensor_idx in enumerate(op_info.op.inputs)
|
|
142
|
+
if check_if_quantized(graph_info.subgraph_tensors[tensor_idx])
|
|
143
|
+
]
|
|
144
|
+
inputs_to_ignore.extend(quantized_inputs_to_ignore)
|
|
145
|
+
|
|
281
146
|
outputs_to_ignore = outputs_to_ignore or []
|
|
282
147
|
for opr_idx, tensor_idx in enumerate(op_info.op.inputs):
|
|
283
148
|
if tensor_idx != -1 and opr_idx not in inputs_to_ignore:
|
|
@@ -286,7 +151,7 @@ def init_qsvs(
|
|
|
286
151
|
tensor_data = tfl_flatbuffer_utils.get_tensor_data(
|
|
287
152
|
tensor, graph_info.buffers
|
|
288
153
|
)
|
|
289
|
-
op_qsvs[tensor_name] =
|
|
154
|
+
op_qsvs[tensor_name] = common_quantize.init_tensor_min_max(
|
|
290
155
|
tensor_data,
|
|
291
156
|
op_info,
|
|
292
157
|
)
|
|
@@ -297,7 +162,7 @@ def init_qsvs(
|
|
|
297
162
|
tensor_data = tfl_flatbuffer_utils.get_tensor_data(
|
|
298
163
|
tensor, graph_info.buffers
|
|
299
164
|
)
|
|
300
|
-
op_qsvs[tensor_name] =
|
|
165
|
+
op_qsvs[tensor_name] = common_quantize.init_tensor_min_max(
|
|
301
166
|
tensor_data,
|
|
302
167
|
op_info,
|
|
303
168
|
)
|
|
@@ -310,6 +175,7 @@ def min_max_calibrate(
|
|
|
310
175
|
tensor_content_map: dict[str, np.ndarray],
|
|
311
176
|
inputs_to_ignore: Optional[list[int]] = None,
|
|
312
177
|
outputs_to_ignore: Optional[list[int]] = None,
|
|
178
|
+
valid_range: tuple[float, float] = (-3e38, 3e38),
|
|
313
179
|
) -> dict[str, qtyping.QSV]:
|
|
314
180
|
"""Collect quantization statistics variable (QSV, e.g., min/max) for the op.
|
|
315
181
|
|
|
@@ -319,11 +185,18 @@ def min_max_calibrate(
|
|
|
319
185
|
tensor_content_map: A map of tensor name to tensor content.
|
|
320
186
|
inputs_to_ignore: Input tensor indices to ignore.
|
|
321
187
|
outputs_to_ignore: Output tensor indices to ignore.
|
|
188
|
+
valid_range: The valid range for tensor content, excluding the boundaries.
|
|
189
|
+
Tensor values outside this range are ignored during calibration. Defaults
|
|
190
|
+
to an approximate bfloat16 range. This range is chosen to address issues
|
|
191
|
+
with `padv2` where a bfloat16 -inf padding constant can cause problems.
|
|
192
|
+
Values exceeding this range can lead to quantization issues and are
|
|
193
|
+
therefore excluded from min/max calibration.
|
|
322
194
|
|
|
323
195
|
Returns:
|
|
324
196
|
A dictionary with key as tensor name and value as the collected QSV.
|
|
325
197
|
"""
|
|
326
198
|
op_qsvs = {}
|
|
199
|
+
min_val, max_val = valid_range
|
|
327
200
|
|
|
328
201
|
def _collect_activation_tensor_min_max(tensor_idx):
|
|
329
202
|
tensor = graph_info.subgraph_tensors[tensor_idx]
|
|
@@ -335,12 +208,25 @@ def min_max_calibrate(
|
|
|
335
208
|
return
|
|
336
209
|
tensor_name = tfl_flatbuffer_utils.get_tensor_name(tensor)
|
|
337
210
|
tensor_content = tensor_content_map[tensor_name]
|
|
211
|
+
qsv_shape = (1,) * tensor_content.ndim
|
|
212
|
+
filter_mask = (tensor_content > min_val) & (tensor_content < max_val)
|
|
213
|
+
if np.any(filter_mask):
|
|
214
|
+
tensor_content = tensor_content[filter_mask]
|
|
215
|
+
# Reshape is needed to ensure the scalar min/max have the same number of
|
|
216
|
+
# dimensions as the input tensor array, for compatibility with subsequent
|
|
217
|
+
# operations.
|
|
338
218
|
op_qsvs[tensor_name] = {
|
|
339
|
-
"min": np.min(tensor_content, axis=None
|
|
340
|
-
"max": np.max(tensor_content, axis=None
|
|
219
|
+
"min": np.min(tensor_content, axis=None).reshape(qsv_shape),
|
|
220
|
+
"max": np.max(tensor_content, axis=None).reshape(qsv_shape),
|
|
341
221
|
}
|
|
342
222
|
|
|
343
223
|
inputs_to_ignore = inputs_to_ignore or []
|
|
224
|
+
quantized_inputs_to_ignore = [
|
|
225
|
+
opr_idx
|
|
226
|
+
for opr_idx, tensor_idx in enumerate(tfl_op.inputs)
|
|
227
|
+
if check_if_quantized(graph_info.subgraph_tensors[tensor_idx])
|
|
228
|
+
]
|
|
229
|
+
inputs_to_ignore.extend(quantized_inputs_to_ignore)
|
|
344
230
|
outputs_to_ignore = outputs_to_ignore or []
|
|
345
231
|
for i, tensor_idx in enumerate(tfl_op.inputs):
|
|
346
232
|
if tensor_idx != -1 and i not in inputs_to_ignore:
|
|
@@ -17,12 +17,12 @@ import os
|
|
|
17
17
|
from typing import cast
|
|
18
18
|
|
|
19
19
|
from absl.testing import parameterized
|
|
20
|
+
import ml_dtypes
|
|
20
21
|
import numpy as np
|
|
21
22
|
|
|
22
23
|
from tensorflow.python.platform import googletest
|
|
23
24
|
from ai_edge_quantizer import qtyping
|
|
24
25
|
from ai_edge_quantizer.algorithms.uniform_quantize import naive_min_max_quantize
|
|
25
|
-
from ai_edge_quantizer.algorithms.uniform_quantize import uniform_quantize_tensor
|
|
26
26
|
from ai_edge_quantizer.utils import test_utils
|
|
27
27
|
from ai_edge_quantizer.utils import tfl_flatbuffer_utils
|
|
28
28
|
|
|
@@ -166,8 +166,7 @@ class NaiveMinMaxQuantizeTest(parameterized.TestCase):
|
|
|
166
166
|
weight_tensor_config = _TensorQuantConfig(
|
|
167
167
|
num_bits=4,
|
|
168
168
|
symmetric=True,
|
|
169
|
-
granularity=qtyping.QuantGranularity.
|
|
170
|
-
block_size=2,
|
|
169
|
+
granularity=qtyping.QuantGranularity.BLOCKWISE_32,
|
|
171
170
|
)
|
|
172
171
|
op_info = qtyping.OpInfo(
|
|
173
172
|
op=fc_op,
|
|
@@ -177,30 +176,68 @@ class NaiveMinMaxQuantizeTest(parameterized.TestCase):
|
|
|
177
176
|
weight_tensor_config=weight_tensor_config,
|
|
178
177
|
),
|
|
179
178
|
)
|
|
180
|
-
test_data = np.
|
|
179
|
+
test_data = np.random.uniform(low=-10, high=10, size=(4, 32)).astype(
|
|
180
|
+
np.float32
|
|
181
|
+
)
|
|
181
182
|
quant_params = naive_min_max_quantize.get_tensor_quant_params(
|
|
182
183
|
op_info=op_info,
|
|
183
184
|
tensor_quant_config=weight_tensor_config,
|
|
184
185
|
tensor_content=test_data,
|
|
185
186
|
)
|
|
186
|
-
scale = quant_params.scale
|
|
187
187
|
zp = quant_params.zero_point
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
188
|
+
self.assertEqual(zp.shape, (4, 1))
|
|
189
|
+
self.assertTrue(np.array_equal(zp, np.zeros([4, 1])))
|
|
190
|
+
|
|
191
|
+
self.assertEqual(quant_params.scale.shape, (4, 1))
|
|
192
|
+
expected_scales = np.max(np.abs(test_data), axis=1, keepdims=True) / 7.0
|
|
193
|
+
expected_scales = (
|
|
194
|
+
expected_scales.astype(ml_dtypes.bfloat16)
|
|
195
|
+
.astype(np.float16)
|
|
196
|
+
.astype(np.float32)
|
|
197
|
+
)
|
|
198
|
+
self.assertTrue(np.allclose(quant_params.scale, expected_scales, atol=1e-5))
|
|
199
|
+
|
|
198
200
|
self.assertIsNotNone(quant_params.quantized_data)
|
|
199
201
|
self.assertTupleEqual(
|
|
200
202
|
cast(np.ndarray, quant_params.quantized_data).shape, test_data.shape
|
|
201
203
|
)
|
|
202
|
-
self.assertEqual(quant_params.block_size,
|
|
203
|
-
self.assertEqual(quant_params.quantized_dimension,
|
|
204
|
+
self.assertEqual(quant_params.block_size, 32)
|
|
205
|
+
self.assertEqual(quant_params.quantized_dimension, 1)
|
|
206
|
+
|
|
207
|
+
def test_calibrate_ignores_inf_min_max(self):
|
|
208
|
+
"""Tests that calibration ignores infinity values."""
|
|
209
|
+
# Sample input/output data for the fc op.
|
|
210
|
+
input_tensor_name = "sequential/flatten/Reshape"
|
|
211
|
+
output_tensor_name = (
|
|
212
|
+
"sequential/dense/MatMul;sequential/dense/Relu;sequential/dense/BiasAdd"
|
|
213
|
+
)
|
|
214
|
+
bloat16_inf = 3.39e38
|
|
215
|
+
tensor_content_map = {
|
|
216
|
+
input_tensor_name: np.array(
|
|
217
|
+
[[-np.inf, 1.0, 5.0, np.inf, bloat16_inf]], dtype=np.float32
|
|
218
|
+
),
|
|
219
|
+
output_tensor_name: np.array(
|
|
220
|
+
[[6.0, 7.0, -bloat16_inf, 9.0, np.inf]], dtype=np.float32
|
|
221
|
+
),
|
|
222
|
+
}
|
|
223
|
+
# Read from Model Explorer.
|
|
224
|
+
subgraph0 = self._test_model.subgraphs[0]
|
|
225
|
+
fc_op = subgraph0.operators[3]
|
|
226
|
+
op_qsvs = naive_min_max_quantize.min_max_calibrate(
|
|
227
|
+
fc_op,
|
|
228
|
+
self._graph_info,
|
|
229
|
+
tensor_content_map,
|
|
230
|
+
inputs_to_ignore=[1, 2], # Ignore weight and bias.
|
|
231
|
+
outputs_to_ignore=[],
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
self.assertIn(input_tensor_name, op_qsvs)
|
|
235
|
+
self.assertEqual(op_qsvs[input_tensor_name]["min"], 1.0)
|
|
236
|
+
self.assertEqual(op_qsvs[input_tensor_name]["max"], 5.0)
|
|
237
|
+
|
|
238
|
+
self.assertIn(output_tensor_name, op_qsvs)
|
|
239
|
+
self.assertEqual(op_qsvs[output_tensor_name]["min"], 6.0)
|
|
240
|
+
self.assertEqual(op_qsvs[output_tensor_name]["max"], 9.0)
|
|
204
241
|
|
|
205
242
|
|
|
206
243
|
if __name__ == "__main__":
|