quantizers 1.1.0__tar.gz → 1.2.0__tar.gz
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.
- {quantizers-1.1.0/src/quantizers.egg-info → quantizers-1.2.0}/PKG-INFO +3 -2
- {quantizers-1.1.0 → quantizers-1.2.0}/pyproject.toml +2 -2
- {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/_version.py +9 -4
- quantizers-1.2.0/src/quantizers/fixed_point/_fixed_point_ops.py +215 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/fixed_point/fixed_point_ops.py +3 -6
- {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/fixed_point/fixed_point_ops_np.py +2 -6
- {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/minifloat/float_point_ops.py +4 -1
- {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/stateful_quantizers.py +6 -0
- {quantizers-1.1.0 → quantizers-1.2.0/src/quantizers.egg-info}/PKG-INFO +3 -2
- {quantizers-1.1.0 → quantizers-1.2.0}/test/test_quantizer_match.py +26 -20
- {quantizers-1.1.0 → quantizers-1.2.0}/test/test_stateless_quantizer.py +56 -55
- quantizers-1.1.0/src/quantizers/fixed_point/_fixed_point_ops.py +0 -289
- {quantizers-1.1.0 → quantizers-1.2.0}/.clang-format +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/.github/workflows/python-publish.yml +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/.github/workflows/python-test.yml +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/.gitignore +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/.pre-commit-config.yaml +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/LICENSE +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/README.md +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/setup.cfg +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/__init__.py +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/binary/__init__.py +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/binary/_binary_ops.py +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/binary/binary_ops.py +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/binary/binary_ops_np.py +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/fixed_point/__init__.py +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/minifloat/__init__.py +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/minifloat/_float_point_ops.py +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/minifloat/float_point_ops_np.py +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers.egg-info/SOURCES.txt +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers.egg-info/dependency_links.txt +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers.egg-info/requires.txt +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers.egg-info/top_level.txt +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_binary.h +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_common.h +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_decl.h +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_fixed.h +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_fixed_base.h +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_fixed_ref.h +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_fixed_special.h +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_float.h +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_int.h +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_int_base.h +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_int_ref.h +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_int_special.h +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_shift_reg.h +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/etc/ap_private.h +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/hls_math.h +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/hls_stream.h +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/test.cpp +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/utils/x_hls_utils.h +0 -0
- {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/quantizers.h +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: quantizers
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Author-email: Chang Sun <chsun@cern.ch>
|
|
5
5
|
License: GNU Lesser General Public License v3 (LGPLv3)
|
|
6
6
|
Project-URL: repository, https://github.com/calad0i/quantizers
|
|
@@ -24,6 +24,7 @@ Requires-Dist: pytest-env; extra == "test"
|
|
|
24
24
|
Requires-Dist: keras>=3.0; extra == "test"
|
|
25
25
|
Requires-Dist: cppyy; extra == "test"
|
|
26
26
|
Requires-Dist: jax; extra == "test"
|
|
27
|
+
Dynamic: license-file
|
|
27
28
|
|
|
28
29
|
# Quantizers
|
|
29
30
|
[](https://badge.fury.io/py/quantizers)
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "quantizers"
|
|
8
|
-
version = "1.
|
|
8
|
+
version = "1.2.0"
|
|
9
9
|
authors = [{ name = "Chang Sun", email = "chsun@cern.ch" }]
|
|
10
10
|
description = ""
|
|
11
11
|
readme = "README.md"
|
|
@@ -91,7 +91,7 @@ docstring-code-line-length = 130
|
|
|
91
91
|
|
|
92
92
|
[tool.ruff.lint]
|
|
93
93
|
select = ["E", "W", "F", "I", "F401"]
|
|
94
|
-
ignore = ["E501", "F403", "F405"]
|
|
94
|
+
ignore = ["E501", "F403", "F405", "E741"]
|
|
95
95
|
explicit-preview-rules = true
|
|
96
96
|
fixable = ["ALL"]
|
|
97
97
|
unfixable = []
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
# file generated by
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
+
|
|
3
6
|
TYPE_CHECKING = False
|
|
4
7
|
if TYPE_CHECKING:
|
|
5
|
-
from typing import Tuple
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
6
11
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
12
|
else:
|
|
8
13
|
VERSION_TUPLE = object
|
|
@@ -12,5 +17,5 @@ __version__: str
|
|
|
12
17
|
__version_tuple__: VERSION_TUPLE
|
|
13
18
|
version_tuple: VERSION_TUPLE
|
|
14
19
|
|
|
15
|
-
__version__ = version = '1.
|
|
16
|
-
__version_tuple__ = version_tuple = (1,
|
|
20
|
+
__version__ = version = '1.2.0'
|
|
21
|
+
__version_tuple__ = version_tuple = (1, 2, 0)
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Any, TypeVar
|
|
4
|
+
|
|
5
|
+
from keras import ops
|
|
6
|
+
from keras.api.random import SeedGenerator
|
|
7
|
+
from numpy.typing import ArrayLike
|
|
8
|
+
|
|
9
|
+
round_mode_registry: dict[str, Callable[[Any], Any]] = {}
|
|
10
|
+
saturation_mode_registry: dict[str, Callable[[Any, Any, Any, Any], Any]] = {}
|
|
11
|
+
|
|
12
|
+
T = TypeVar('T', bound=ArrayLike)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _clip(x, min_value, max_value):
|
|
16
|
+
mask_overflow = x > max_value
|
|
17
|
+
mask_underflow = x < min_value
|
|
18
|
+
return ops.where(mask_overflow, max_value, ops.where(mask_underflow, min_value, x))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def rnd_mode(name: str):
|
|
22
|
+
def inner(func):
|
|
23
|
+
assert name not in round_mode_registry, f"Round mode '{name}' already exists."
|
|
24
|
+
|
|
25
|
+
@wraps(func)
|
|
26
|
+
def wrapper(x):
|
|
27
|
+
xq = func(x)
|
|
28
|
+
return ops.stop_gradient(xq) + (x - ops.stop_gradient(x))
|
|
29
|
+
|
|
30
|
+
round_mode_registry[name] = wrapper
|
|
31
|
+
return wrapper
|
|
32
|
+
|
|
33
|
+
return inner
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@rnd_mode('TRN')
|
|
37
|
+
def floor(x):
|
|
38
|
+
return ops.floor(x)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@rnd_mode('RND')
|
|
42
|
+
def round(x):
|
|
43
|
+
# Round to nearest, ties positive infinity.
|
|
44
|
+
return ops.floor(x + 0.5)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@rnd_mode('RND_CONV')
|
|
48
|
+
def round_conv(x):
|
|
49
|
+
# Round to nearest, ties to even.
|
|
50
|
+
return ops.round(x)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@rnd_mode('TRN_ZERO')
|
|
54
|
+
def floor_zero(x):
|
|
55
|
+
# Truncate towards zero.
|
|
56
|
+
sign = ops.sign(x)
|
|
57
|
+
return ops.floor(ops.abs(x)) * sign # type: ignore
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@rnd_mode('RND_ZERO')
|
|
61
|
+
def round_zero(x):
|
|
62
|
+
# Round to nearest, ties towards zero.
|
|
63
|
+
sign = ops.sign(x)
|
|
64
|
+
return -ops.floor(-ops.abs(x) + 0.5) * sign # type:ignore
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@rnd_mode('RND_MIN_INF')
|
|
68
|
+
def round_min_inf(x):
|
|
69
|
+
# Round to nearest, ties towards negative infinity.
|
|
70
|
+
return -ops.floor(-x + 0.5) # type:ignore
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@rnd_mode('RND_INF')
|
|
74
|
+
def round_inf(x):
|
|
75
|
+
# Round to nearest, ties away from zero.
|
|
76
|
+
sign = ops.sign(x)
|
|
77
|
+
return ops.floor(ops.abs(x) + 0.5) * sign # type: ignore
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def sat_mode(name: str | list | tuple):
|
|
81
|
+
names = (name,) if isinstance(name, str) else name
|
|
82
|
+
|
|
83
|
+
def inner(func):
|
|
84
|
+
for name in names:
|
|
85
|
+
assert name not in saturation_mode_registry, f"Saturation mode '{name}' already exists."
|
|
86
|
+
saturation_mode_registry[name] = func
|
|
87
|
+
saturation_mode_registry[func.__name__.upper()] = func
|
|
88
|
+
return func
|
|
89
|
+
|
|
90
|
+
return inner
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@sat_mode('WRAP')
|
|
94
|
+
def wrap(x, k, i, f):
|
|
95
|
+
xs = x
|
|
96
|
+
bk = i + k
|
|
97
|
+
bias = k * 2.0 ** (bk - 1)
|
|
98
|
+
return (xs + bias) % (2.0**bk) - bias
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@sat_mode('SAT')
|
|
102
|
+
def sat(x, k, i, f):
|
|
103
|
+
f_eps = 2.0 ** (-f)
|
|
104
|
+
__max = 2.0**i
|
|
105
|
+
_max = __max - f_eps
|
|
106
|
+
_min = -__max * k
|
|
107
|
+
r = _clip(x, _min, _max)
|
|
108
|
+
return r
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@sat_mode('SAT_SYM')
|
|
112
|
+
def sat_sym(x, k, i, f):
|
|
113
|
+
f_eps = 2.0 ** (-f)
|
|
114
|
+
_max = 2.0**i - f_eps
|
|
115
|
+
_min = -_max * k
|
|
116
|
+
r = _clip(x, _min, _max)
|
|
117
|
+
return r
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@sat_mode('WRAP_SM')
|
|
121
|
+
def wrap_sm_fn(x, k, i, f, training=None, quant_fn: Callable = lambda x: x):
|
|
122
|
+
# x=ops.round(x*2.**f)
|
|
123
|
+
# High and low bounds are reflective. When overflows, can be less trash than WARP but still more trash than SAT.
|
|
124
|
+
eps = 2.0**-f
|
|
125
|
+
high = 2.0**i - eps
|
|
126
|
+
low = -(high + eps) * k
|
|
127
|
+
interval = 2 ** (i + k)
|
|
128
|
+
c1 = ((x) / interval) % 2 >= 1 # type: ignore
|
|
129
|
+
c1 = c1 & (ops.abs(x) > eps / 2)
|
|
130
|
+
c2 = ((x + eps / 2) / (interval / 2)) % 2 >= 1 # type: ignore
|
|
131
|
+
qx = quant_fn(x)
|
|
132
|
+
mapped = ((qx - high - eps) % interval) + low
|
|
133
|
+
|
|
134
|
+
mapped = ops.where(c2, -mapped - eps, mapped) # type: ignore
|
|
135
|
+
mapped = ops.where(c1, -mapped - eps, mapped) # type: ignore
|
|
136
|
+
|
|
137
|
+
return mapped
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class FixedPointQuantizer:
|
|
141
|
+
def round(self, x, f: Any = 1.0, stochastic: bool | None = None, seed_gen: SeedGenerator | None = None):
|
|
142
|
+
scale = 2.0**f
|
|
143
|
+
x = x * scale
|
|
144
|
+
xq = self.round_fn(x)
|
|
145
|
+
# if stochastic:
|
|
146
|
+
# noise = keras.random.uniform(ops.shape(x), -0.49, 0.49, seed=seed_gen)
|
|
147
|
+
# x = x + noise
|
|
148
|
+
# xq2 = self.round_fn(x)
|
|
149
|
+
# xq = xq + ops.stop_gradient(xq2 - xq)
|
|
150
|
+
xq = xq / scale
|
|
151
|
+
return xq
|
|
152
|
+
|
|
153
|
+
def saturate(self, x, k, i, f):
|
|
154
|
+
return self.sat_fn(x, k, i, f)
|
|
155
|
+
|
|
156
|
+
def __init__(self, round_mode: str = 'TRN', overflow_mode: str = 'WRAP'):
|
|
157
|
+
round_mode = round_mode.upper()
|
|
158
|
+
overflow_mode = overflow_mode.upper()
|
|
159
|
+
self.stochastic = False
|
|
160
|
+
|
|
161
|
+
if round_mode.startswith('S_'):
|
|
162
|
+
round_mode = round_mode[2:]
|
|
163
|
+
self.stochastic = True
|
|
164
|
+
|
|
165
|
+
if overflow_mode == 'WRAP_SM':
|
|
166
|
+
assert round_mode in (
|
|
167
|
+
'RND',
|
|
168
|
+
'RND_CONV',
|
|
169
|
+
), 'WRAP_SM only supports RND and RND_CONV rounding modes in this implementation.'
|
|
170
|
+
|
|
171
|
+
self.round_mode = round_mode
|
|
172
|
+
self.overflow_mode = overflow_mode
|
|
173
|
+
round_fn = round_mode_registry[round_mode]
|
|
174
|
+
sat_fn = saturation_mode_registry[overflow_mode]
|
|
175
|
+
self.sat_fn = sat_fn
|
|
176
|
+
self.round_fn = round_fn
|
|
177
|
+
|
|
178
|
+
def forward(self, x, k, i, f, training=False, seed_gen=None):
|
|
179
|
+
# Workaround for gradient computation around 0.
|
|
180
|
+
# When have values outside boundary rounded to boundary, grad on f presents despite the value will be clipped off anyway.
|
|
181
|
+
# Thus have saturation before rounding, except for wrap mode, which doesn't round during training.
|
|
182
|
+
|
|
183
|
+
if self.overflow_mode != 'WRAP':
|
|
184
|
+
x = self.saturate(x, k, i, f)
|
|
185
|
+
x = self.round(x, f, self.stochastic and training, seed_gen)
|
|
186
|
+
if self.overflow_mode == 'WRAP' and not training:
|
|
187
|
+
x = self.saturate(x, k, i, f)
|
|
188
|
+
return x
|
|
189
|
+
|
|
190
|
+
def forward_wrap_sm(self, x, k, i, f, training=False, seed_gen: SeedGenerator | None = None):
|
|
191
|
+
def quant_fn(x):
|
|
192
|
+
return self.round(x, f, training and self.stochastic, seed_gen)
|
|
193
|
+
|
|
194
|
+
x = wrap_sm_fn(x, k, i, f, training, quant_fn)
|
|
195
|
+
return x
|
|
196
|
+
|
|
197
|
+
def __call__(self, x, k, i, f, training=False, seed_gen=None):
|
|
198
|
+
i = ops.stop_gradient(ops.maximum(i, -f)) + (i - ops.stop_gradient(i)) # type: ignore
|
|
199
|
+
if self.stochastic and training:
|
|
200
|
+
assert seed_gen is not None, 'Seed generator must be provided for stochastic rounding.'
|
|
201
|
+
if self.overflow_mode != 'WRAP_SM':
|
|
202
|
+
return self.forward(x, k, i, f, training, seed_gen)
|
|
203
|
+
else:
|
|
204
|
+
return self.forward_wrap_sm(x, k, i, f, training, seed_gen)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _get_fixed_quantizer(round_mode: str = 'TRN', overflow_mode: str = 'WRAP'):
|
|
208
|
+
"""Get a stateless fixed-point quantizer given the round and overflow mode.
|
|
209
|
+
The quantizer is differentiable w.r.t. to the input and f, also i if using saturation overflow mode.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
round_mode: round mode, one of
|
|
213
|
+
"""
|
|
214
|
+
quantizer = FixedPointQuantizer(round_mode, overflow_mode)
|
|
215
|
+
return quantizer # type: ignore
|
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
from
|
|
2
|
-
from typing import TYPE_CHECKING, Any, TypeVar
|
|
1
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
3
2
|
|
|
4
3
|
from numpy.typing import ArrayLike
|
|
5
4
|
|
|
6
5
|
if TYPE_CHECKING:
|
|
7
|
-
|
|
6
|
+
pass
|
|
8
7
|
T = TypeVar('T', bound=ArrayLike)
|
|
9
8
|
|
|
10
9
|
|
|
11
|
-
def get_fixed_quantizer(
|
|
12
|
-
round_mode: str = 'TRN', overflow_mode: str = 'WRAP'
|
|
13
|
-
) -> Callable[[T, Any, Any, Any, bool | None, 'SeedGenerator | None'], T]:
|
|
10
|
+
def get_fixed_quantizer(round_mode: str = 'TRN', overflow_mode: str = 'WRAP'):
|
|
14
11
|
"""Get a stateless fixed-point quantizer given the round and overflow mode.
|
|
15
12
|
The quantizer is differentiable w.r.t. to the input and f, also i if using saturation overflow mode.
|
|
16
13
|
|
|
@@ -135,12 +135,8 @@ def get_fixed_quantizer_np(round_mode: str = 'TRN', overflow_mode: str = 'WRAP')
|
|
|
135
135
|
overflow_mode = overflow_mode.upper()
|
|
136
136
|
round_mode = round_mode[2:] if round_mode.startswith('S_') else round_mode
|
|
137
137
|
|
|
138
|
-
round_fn_scaled = round_mode_registry_scaled_np
|
|
139
|
-
|
|
140
|
-
raise ValueError(f'Unknown rounding mode: {round_mode}')
|
|
141
|
-
sat_fn = saturation_mode_registry_np.get(overflow_mode, None)
|
|
142
|
-
if sat_fn is None:
|
|
143
|
-
raise ValueError(f'Unknown saturation mode: {overflow_mode}')
|
|
138
|
+
round_fn_scaled = round_mode_registry_scaled_np[round_mode]
|
|
139
|
+
sat_fn = saturation_mode_registry_np[overflow_mode]
|
|
144
140
|
|
|
145
141
|
if overflow_mode == 'WRAP_SM':
|
|
146
142
|
assert round_mode in ('RND', 'RND_CONV'), 'WRAP_SM only supports RND and RND_CONV rounding modes in this implementation.'
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def float_quantize(x, m, e, e0: Any = 0.0):
|
|
2
5
|
"""Quantize an array to floatlet (m mantissa bits, excl. sign bit, e exponent bits) format. Tentative gradient impl."""
|
|
3
6
|
from ._float_point_ops import _float_quantize
|
|
4
7
|
|
|
@@ -91,6 +91,9 @@ class FixedQ:
|
|
|
91
91
|
if use_numpy:
|
|
92
92
|
q = get_fixed_quantizer_np(self.rounding, self.overflow)
|
|
93
93
|
else:
|
|
94
|
+
from keras.api import ops
|
|
95
|
+
|
|
96
|
+
k, i, f = ops.convert_to_tensor(k), ops.convert_to_tensor(i), ops.convert_to_tensor(f)
|
|
94
97
|
q = get_fixed_quantizer(self.rounding, self.overflow)
|
|
95
98
|
return q(x, k, i, f, False, None) # type: ignore
|
|
96
99
|
|
|
@@ -128,6 +131,9 @@ class MinifloatQ:
|
|
|
128
131
|
if use_numpy:
|
|
129
132
|
return float_quantize_np(x, s, e, e0)
|
|
130
133
|
else:
|
|
134
|
+
from keras.api import ops
|
|
135
|
+
|
|
136
|
+
s, e, e0 = ops.convert_to_tensor(s), ops.convert_to_tensor(e), ops.convert_to_tensor(e0)
|
|
131
137
|
return float_quantize(x, s, e, e0)
|
|
132
138
|
|
|
133
139
|
def __repr__(self):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: quantizers
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Author-email: Chang Sun <chsun@cern.ch>
|
|
5
5
|
License: GNU Lesser General Public License v3 (LGPLv3)
|
|
6
6
|
Project-URL: repository, https://github.com/calad0i/quantizers
|
|
@@ -24,6 +24,7 @@ Requires-Dist: pytest-env; extra == "test"
|
|
|
24
24
|
Requires-Dist: keras>=3.0; extra == "test"
|
|
25
25
|
Requires-Dist: cppyy; extra == "test"
|
|
26
26
|
Requires-Dist: jax; extra == "test"
|
|
27
|
+
Dynamic: license-file
|
|
27
28
|
|
|
28
29
|
# Quantizers
|
|
29
30
|
[](https://badge.fury.io/py/quantizers)
|
|
@@ -18,7 +18,7 @@ def register_cpp():
|
|
|
18
18
|
|
|
19
19
|
@pytest.fixture(scope='session')
|
|
20
20
|
def data():
|
|
21
|
-
arr = np.random.randint(-2**15, 2**15, 1000) * 2
|
|
21
|
+
arr = np.random.randint(-(2**15), 2**15, 1000) * 2.0**-8
|
|
22
22
|
return ops.array(arr.astype(np.float32))
|
|
23
23
|
|
|
24
24
|
|
|
@@ -53,19 +53,20 @@ def c_quantize_ternary(x):
|
|
|
53
53
|
return r
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
@pytest.mark.parametrize(
|
|
56
|
+
@pytest.mark.parametrize(
|
|
57
|
+
'fixed_round_mode', ['TRN', 'TRN_ZERO', 'RND', 'S_RND', 'S_RND_CONV', 'RND_CONV', 'RND_ZERO', 'RND_INF', 'RND_MIN_INF']
|
|
58
|
+
)
|
|
57
59
|
@pytest.mark.parametrize('fixed_overflow_mode', ['WRAP', 'WRAP_SM', 'SAT', 'SAT_SYM'])
|
|
58
60
|
@pytest.mark.parametrize('k', [0, 1])
|
|
59
61
|
@pytest.mark.parametrize('b', [2, 7, 12])
|
|
60
62
|
@pytest.mark.parametrize('i', [-5, 0, 8])
|
|
61
63
|
def test_fixed_quantizer_forward(fixed_round_mode, fixed_overflow_mode, k, b, i, data, register_cpp):
|
|
62
|
-
|
|
63
64
|
k, i, f = k, i, b - i
|
|
64
65
|
W, I = k + b, k + i
|
|
65
66
|
if fixed_overflow_mode == 'WRAP_SM':
|
|
66
67
|
if k == 0:
|
|
67
68
|
pytest.skip('WRAP_SM does not support k=0')
|
|
68
|
-
if fixed_round_mode not in ('RND_CONV', 'RND'):
|
|
69
|
+
if fixed_round_mode not in ('RND_CONV', 'RND', 'S_RND', 'S_RND_CONV'):
|
|
69
70
|
pytest.skip('WRAP_SM only supports RND-like rounding')
|
|
70
71
|
|
|
71
72
|
fixed_q = FixedQ(W, I, k, fixed_round_mode, fixed_overflow_mode)
|
|
@@ -73,25 +74,31 @@ def test_fixed_quantizer_forward(fixed_round_mode, fixed_overflow_mode, k, b, i,
|
|
|
73
74
|
|
|
74
75
|
arr_py_fixed_np = np.array(fixed_q(np.array(data)))
|
|
75
76
|
arr_py_fixed = np.array(fixed_q(data))
|
|
77
|
+
mismatch = np.where(arr_py_fixed_np != arr_py_fixed)[0]
|
|
78
|
+
assert len(mismatch) == 0, f"""Fixed quantizer has inconsistent behavior with numpy implementation:
|
|
79
|
+
[* {len(mismatch)} mismatches, {min(len(mismatch), 5)} shown *]
|
|
80
|
+
np: {arr_py_fixed_np[mismatch][:5]}
|
|
81
|
+
keras: {arr_py_fixed[mismatch][:5]}
|
|
82
|
+
in: {data[mismatch][:5] * 2.0**8}
|
|
83
|
+
"""
|
|
76
84
|
assert np.all(arr_py_fixed_np == arr_py_fixed), 'numpy / keras implementation inconsistent'
|
|
77
85
|
|
|
78
86
|
arr_c_fixed = c_quantize_fixed(data, k, i, f, fixed_round_mode, fixed_overflow_mode)
|
|
79
87
|
arr_c_fixed_np = np.array([float(x) for x in arr_c_fixed])
|
|
80
88
|
|
|
81
89
|
mismatch = np.where(arr_py_fixed != arr_c_fixed_np)[0]
|
|
82
|
-
assert len(mismatch) == 0, f
|
|
90
|
+
assert len(mismatch) == 0, f"""Fixed quantizer has inconsistent behavior with C++ implementation:
|
|
83
91
|
[* {len(mismatch)} mismatches, {min(len(mismatch), 5)} shown *]
|
|
84
92
|
C++: {arr_c_fixed_np[mismatch][:5]}
|
|
85
93
|
Py: {arr_py_fixed[mismatch][:5]}
|
|
86
94
|
in: {data[mismatch][:5]}
|
|
87
|
-
|
|
95
|
+
"""
|
|
88
96
|
|
|
89
97
|
|
|
90
98
|
@pytest.mark.parametrize('M', [2, 6])
|
|
91
99
|
@pytest.mark.parametrize('E', [2, 6])
|
|
92
100
|
@pytest.mark.parametrize('E0', [0, 3, 5])
|
|
93
101
|
def test_minifloat_quantizer_forward(M, E, E0, data, register_cpp):
|
|
94
|
-
|
|
95
102
|
q = MinifloatQ(M, E, E0)
|
|
96
103
|
print(q)
|
|
97
104
|
arr_py_float = np.array(q(data))
|
|
@@ -102,13 +109,13 @@ def test_minifloat_quantizer_forward(M, E, E0, data, register_cpp):
|
|
|
102
109
|
arr_c_float_np = np.array([float(x) for x in arr_c_float])
|
|
103
110
|
|
|
104
111
|
mismatch = np.where(arr_py_float != arr_c_float_np)[0]
|
|
105
|
-
assert len(mismatch) == 0, f
|
|
112
|
+
assert len(mismatch) == 0, f"""Float quantizer has inconsistent behavior with C++ implementation:
|
|
106
113
|
[* {len(mismatch)} mismatches, {min(len(mismatch), 5)} shown *]
|
|
107
114
|
[* Up to 5 shown]
|
|
108
115
|
C++: {arr_c_float_np[mismatch][:5]}
|
|
109
116
|
Py: {arr_py_float[mismatch][:5]}
|
|
110
117
|
in: {data[mismatch][:5]}
|
|
111
|
-
|
|
118
|
+
"""
|
|
112
119
|
|
|
113
120
|
|
|
114
121
|
@pytest.mark.parametrize('fixed_round_mode', ['RND'])
|
|
@@ -120,7 +127,6 @@ def test_minifloat_quantizer_forward(M, E, E0, data, register_cpp):
|
|
|
120
127
|
@pytest.mark.parametrize('E', [2, 4])
|
|
121
128
|
@pytest.mark.parametrize('E0', [1, 8])
|
|
122
129
|
def test_fixed_float_mult(fixed_round_mode, fixed_overflow_mode, k, i, f, M, E, E0, data, register_cpp):
|
|
123
|
-
|
|
124
130
|
if fixed_overflow_mode == 'WRAP_SM':
|
|
125
131
|
if k == 0:
|
|
126
132
|
pytest.skip('WRAP_SM does not support k=0')
|
|
@@ -137,47 +143,47 @@ def test_fixed_float_mult(fixed_round_mode, fixed_overflow_mode, k, i, f, M, E,
|
|
|
137
143
|
|
|
138
144
|
mismatch = np.where(mult_py != mult_c)[0]
|
|
139
145
|
print(len(mismatch))
|
|
140
|
-
assert len(mismatch) == 0, f
|
|
146
|
+
assert len(mismatch) == 0, f"""Multiplication has inconsistent behavior with C++ implementation:
|
|
141
147
|
[* {len(mismatch)} mismatches, {min(len(mismatch), 5)} shown *]
|
|
142
148
|
C++: {mult_c[mismatch][:5]}
|
|
143
149
|
Py: {mult_py[mismatch][:5]}
|
|
144
150
|
fx in: {arr_c_fixed_np[mismatch][:5]}
|
|
145
151
|
fp in: {arr_c_float_np[mismatch][:5]}
|
|
146
|
-
|
|
152
|
+
"""
|
|
147
153
|
|
|
148
154
|
|
|
149
155
|
def test_binary(data, register_cpp):
|
|
150
156
|
q = BinaryQ()
|
|
151
157
|
print(q)
|
|
152
|
-
arr_py_binary = q(data)
|
|
153
|
-
arr_py_binary_np =
|
|
158
|
+
arr_py_binary = np.array(q(data))
|
|
159
|
+
arr_py_binary_np = q(np.array(data))
|
|
154
160
|
assert np.all(arr_py_binary_np == arr_py_binary), 'numpy / keras implementation inconsistent'
|
|
155
161
|
|
|
156
162
|
arr_c_binary = c_quantize_binary(data)
|
|
157
163
|
arr_c_binary_np = np.array([float(x) for x in arr_c_binary])
|
|
158
164
|
|
|
159
165
|
mismatch = np.where(arr_py_binary != arr_c_binary_np)[0]
|
|
160
|
-
assert len(mismatch) == 0, f
|
|
166
|
+
assert len(mismatch) == 0, f"""Binary quantizer has inconsistent behavior with C++ implementation:
|
|
161
167
|
[* {len(mismatch)} mismatches, {min(len(mismatch), 5)} shown *]
|
|
162
168
|
C++: {arr_c_binary_np[mismatch][:5]}
|
|
163
169
|
Py: {arr_py_binary[mismatch][:5]}
|
|
164
170
|
in: {data[mismatch][:5]}
|
|
165
|
-
|
|
171
|
+
"""
|
|
166
172
|
|
|
167
173
|
|
|
168
174
|
def test_ternary(data, register_cpp):
|
|
169
175
|
q = TernaryQ()
|
|
170
176
|
print(q)
|
|
171
|
-
arr_py_ternary = q(data)
|
|
172
|
-
arr_py_ternary_np =
|
|
177
|
+
arr_py_ternary = np.array(q(data))
|
|
178
|
+
arr_py_ternary_np = q(np.array(data))
|
|
173
179
|
assert np.all(arr_py_ternary_np == arr_py_ternary), 'numpy / keras implementation inconsistent'
|
|
174
180
|
|
|
175
181
|
arr_c_ternary = c_quantize_ternary(data)
|
|
176
182
|
arr_c_ternary_np = np.array([float(x) for x in arr_c_ternary])
|
|
177
183
|
mismatch = np.where(arr_py_ternary != arr_c_ternary_np)[0]
|
|
178
|
-
assert len(mismatch) == 0, f
|
|
184
|
+
assert len(mismatch) == 0, f"""Ternary quantizer has inconsistent behavior with C++ implementation:
|
|
179
185
|
[* {len(mismatch)} mismatches, {min(len(mismatch), 5)} shown *]
|
|
180
186
|
C++: {arr_c_ternary_np[mismatch][:5]}
|
|
181
187
|
Py: {arr_py_ternary[mismatch][:5]}
|
|
182
188
|
in: {data[mismatch][:5]}
|
|
183
|
-
|
|
189
|
+
"""
|
|
@@ -4,9 +4,16 @@ import jax
|
|
|
4
4
|
import jax.numpy as jnp
|
|
5
5
|
import numpy as np
|
|
6
6
|
import pytest
|
|
7
|
+
from keras.api.random import SeedGenerator
|
|
7
8
|
|
|
8
|
-
from quantizers import
|
|
9
|
-
|
|
9
|
+
from quantizers import (
|
|
10
|
+
binary_quantize,
|
|
11
|
+
float_decompose,
|
|
12
|
+
float_decompose_np,
|
|
13
|
+
float_quantize,
|
|
14
|
+
get_fixed_quantizer,
|
|
15
|
+
ternary_quantize,
|
|
16
|
+
)
|
|
10
17
|
|
|
11
18
|
|
|
12
19
|
@pytest.fixture(scope='module')
|
|
@@ -30,26 +37,28 @@ def xxx_mee0():
|
|
|
30
37
|
M = jax.random.randint(k1, N, 1, 9).astype(jnp.float32)
|
|
31
38
|
E = jax.random.randint(k2, N, 1, 4).astype(jnp.float32)
|
|
32
39
|
E0 = jax.random.randint(k3, N, -8, 8).astype(jnp.float32)
|
|
33
|
-
x = jax.random.uniform(k4, N, jnp.float32, -1
|
|
40
|
+
x = jax.random.uniform(k4, N, jnp.float32, -1.0, 1.0)
|
|
34
41
|
|
|
35
42
|
sign = jnp.sign(x)
|
|
36
43
|
x = jnp.abs(x)
|
|
37
|
-
m_eps = 2
|
|
38
|
-
_max = (2 - m_eps) * 2
|
|
39
|
-
_min_pos_normal = 2
|
|
40
|
-
_min_pos_subnormal = m_eps * 2
|
|
44
|
+
m_eps = 2.0**-M
|
|
45
|
+
_max = (2 - m_eps) * 2.0 ** (2.0 ** (E - 1) - 1 + E0)
|
|
46
|
+
_min_pos_normal = 2.0 ** (-(2.0 ** (E - 1)) + E0 + 1)
|
|
47
|
+
_min_pos_subnormal = m_eps * 2.0 ** (-(2.0 ** (E - 1)) + E0)
|
|
41
48
|
log_subnormal = jnp.log2(_min_pos_subnormal)
|
|
42
49
|
log_normal = jnp.log2(_min_pos_normal)
|
|
43
50
|
log_overflow = jnp.log2(_max)
|
|
44
51
|
|
|
45
|
-
x_normal = 2
|
|
46
|
-
x_subnormal = 2
|
|
47
|
-
x_overflow = 2
|
|
52
|
+
x_normal = 2.0 ** (x * (log_overflow - log_normal) + log_normal) * sign
|
|
53
|
+
x_subnormal = 2.0 ** (x * (log_normal - log_subnormal) + log_subnormal - 1) * sign
|
|
54
|
+
x_overflow = 2.0 ** (x * 2 + log_overflow + 1) * sign
|
|
48
55
|
xxx = (x_normal, x_subnormal, x_overflow)
|
|
49
56
|
return xxx, M, E, E0
|
|
50
57
|
|
|
51
58
|
|
|
52
|
-
@pytest.mark.parametrize(
|
|
59
|
+
@pytest.mark.parametrize(
|
|
60
|
+
'round_mode', ['TRN', 'TRN_ZERO', 'RND', 'S_RND', 'RND_CONV', 'S_RND_CONV', 'RND_ZERO', 'RND_INF', 'RND_MIN_INF']
|
|
61
|
+
)
|
|
53
62
|
@pytest.mark.parametrize('overflow_mode', ['WRAP', 'SAT', 'SAT_SYM', 'WRAP_SM'])
|
|
54
63
|
def test_fixed_quantizer_grad(round_mode, overflow_mode, x_kif):
|
|
55
64
|
if round_mode not in ('RND', 'RND_CONV') and overflow_mode == 'WRAP_SM':
|
|
@@ -57,44 +66,37 @@ def test_fixed_quantizer_grad(round_mode, overflow_mode, x_kif):
|
|
|
57
66
|
|
|
58
67
|
quantizer = get_fixed_quantizer(round_mode, overflow_mode)
|
|
59
68
|
assert callable(quantizer)
|
|
69
|
+
seed = SeedGenerator(42)
|
|
60
70
|
|
|
61
71
|
x, k, i, f = x_kif
|
|
62
72
|
|
|
63
|
-
xq = quantizer(x, k, i, f, True) # type: ignore
|
|
64
|
-
if not round_mode.startswith('S_')
|
|
73
|
+
xq = quantizer(x, k, i, f, True, seed) # type: ignore
|
|
74
|
+
if 'WRAP' not in overflow_mode and not round_mode.startswith('S_'):
|
|
65
75
|
xq1 = quantizer(x, k, i, f, False) # type: ignore
|
|
66
76
|
assert jnp.all(xq == xq1)
|
|
67
77
|
|
|
68
78
|
def abs_quantization_err(x, k, i, f):
|
|
69
|
-
xq = quantizer(x, k, i, f, True) # type: ignore
|
|
79
|
+
xq = quantizer(x, k, i, f, True, seed) # type: ignore
|
|
70
80
|
err = jnp.abs(x - xq)
|
|
71
81
|
return jnp.sum(err)
|
|
72
82
|
|
|
73
83
|
dx, di, df = jax.grad(abs_quantization_err, (0, 2, 3))(x, 1, i, f)
|
|
74
84
|
|
|
75
85
|
if overflow_mode == 'WRAP':
|
|
76
|
-
assert jnp.all(dx == 0),
|
|
77
|
-
assert jnp.all(di == 0),
|
|
78
|
-
assert jnp.all(df < 0),
|
|
86
|
+
assert jnp.all(dx == 0), 'X grad Error'
|
|
87
|
+
assert jnp.all(di == 0), 'I grad Error'
|
|
88
|
+
assert jnp.all(df < 0), 'F grad Error'
|
|
79
89
|
elif overflow_mode in ('SAT', 'SAT_SYM'):
|
|
80
90
|
for _dx in jnp.unique(dx):
|
|
81
|
-
assert _dx in (-1, 0, 1),
|
|
82
|
-
assert jnp.all(df <= 0),
|
|
83
|
-
assert jnp.all(di <= 0),
|
|
84
|
-
assert jnp.all((df < 0) | (di < 0) | (x == xq)),
|
|
91
|
+
assert _dx in (-1, 0, 1), 'X grad Error'
|
|
92
|
+
assert jnp.all(df <= 0), 'F grad Error'
|
|
93
|
+
assert jnp.all(di <= 0), 'I grad Error'
|
|
94
|
+
assert jnp.all((df < 0) | (di < 0) | (x == xq)), 'Grad Error for sat mode'
|
|
85
95
|
elif overflow_mode == 'WRAP_SM':
|
|
86
|
-
mask = jnp.abs(x) < 2
|
|
96
|
+
mask = jnp.abs(x) < 2.0**i - 2.0**f
|
|
87
97
|
assert jnp.all(df[mask] < 0), f'F grad Error {jnp.mean(df <= 0)}'
|
|
88
98
|
|
|
89
99
|
|
|
90
|
-
@pytest.mark.parametrize('round_mode', ['TRN', 'TRN_ZERO', 'RND', 'RND_CONV', 'RND_ZERO', 'RND_INF', 'RND_MIN_INF'])
|
|
91
|
-
def test_round_ste(round_mode, x_kif):
|
|
92
|
-
x = x_kif[0]
|
|
93
|
-
rnd_fn = round_mode_registry[round_mode]
|
|
94
|
-
grad_x = jax.grad(lambda x: jnp.sum(rnd_fn(x, True)))(x)
|
|
95
|
-
assert jnp.all(grad_x == 1), f'Gradient Error for {round_mode}'
|
|
96
|
-
|
|
97
|
-
|
|
98
100
|
def test_float_quantizer_grad(xxx_mee0):
|
|
99
101
|
xxx, M, E, E0 = xxx_mee0
|
|
100
102
|
x_normal, x_subnormal, x_overflow = xxx
|
|
@@ -107,72 +109,71 @@ def test_float_quantizer_grad(xxx_mee0):
|
|
|
107
109
|
dx, dm, de, de0 = jax.grad(abs_quantization_err, range(4))(x_normal, M, E, E0)
|
|
108
110
|
xq = float_quantize(x_normal, M, E, E0)
|
|
109
111
|
mask = x_normal != xq
|
|
110
|
-
assert jnp.all(dx == 0),
|
|
112
|
+
assert jnp.all(dx == 0), 'Normal Number X grad Error'
|
|
111
113
|
assert jnp.all(dm[mask] < 0), f'Normal Number M grad Error: max={jnp.max(dm[mask])}>0'
|
|
112
|
-
assert jnp.all(de == 0),
|
|
113
|
-
assert jnp.all(de0 == 0),
|
|
114
|
+
assert jnp.all(de == 0), 'Normal Number E grad Error'
|
|
115
|
+
assert jnp.all(de0 == 0), 'Normal Number E0 grad Error'
|
|
114
116
|
|
|
115
117
|
dx, dm, de, de0 = jax.grad(abs_quantization_err, range(4))(x_subnormal, M, E, E0)
|
|
116
118
|
xq = float_quantize(x_subnormal, M, E, E0)
|
|
117
119
|
mask = x_subnormal != xq
|
|
118
|
-
assert jnp.all(dx == 0),
|
|
120
|
+
assert jnp.all(dx == 0), 'Subnormal Number X grad Error'
|
|
119
121
|
assert jnp.all(dm[mask] < 0), f'Subnormal Number M grad Error: max={jnp.max(dm[mask])}>0'
|
|
120
122
|
assert jnp.all(de[mask] < 0), f'Subnormal Number E grad Error: max={jnp.max(de[mask])}>0'
|
|
121
123
|
assert jnp.all(de0[mask] > 0), f'Subnormal Number E0 grad Error: max={jnp.max(de0[mask])}>0'
|
|
122
124
|
|
|
123
125
|
dx, dm, de, de0 = jax.grad(abs_quantization_err, range(4))(x_overflow, M, E, E0)
|
|
124
|
-
assert jnp.all(dx == 0),
|
|
125
|
-
assert jnp.all(dm == 0),
|
|
126
|
+
assert jnp.all(dx == 0), 'Overflow Number X grad Error'
|
|
127
|
+
assert jnp.all(dm == 0), 'Overflow Number M grad Error'
|
|
126
128
|
assert jnp.all(de < 0), f'Overflow Number E grad Error: max={jnp.max(de)}>0'
|
|
127
129
|
assert jnp.all(de0 < 0), f'Overflow Number E0 grad Error: max={jnp.max(de0)}>0'
|
|
128
130
|
|
|
129
131
|
|
|
130
132
|
def test_float_decompose(xxx_mee0):
|
|
131
|
-
|
|
132
133
|
xxx, M, E, E0 = xxx_mee0
|
|
133
134
|
x_normal, x_subnormal, x_overflow = xxx
|
|
134
135
|
|
|
135
136
|
mm, ee = float_decompose(x_normal, M, E, E0)
|
|
136
137
|
mm_, ee_ = float_decompose_np(x_normal, M, E, E0)
|
|
137
|
-
assert np.all(mm == mm_) and np.all(ee == ee_),
|
|
138
|
+
assert np.all(mm == mm_) and np.all(ee == ee_), 'Float Decompose Error @ Normal Number'
|
|
138
139
|
|
|
139
|
-
xq_ = mm * 2
|
|
140
|
+
xq_ = mm * 2.0**ee # type: ignore
|
|
140
141
|
xq = float_quantize(x_normal, M, E, E0)
|
|
141
|
-
assert jnp.all(xq == xq_),
|
|
142
|
-
assert jnp.all(jnp.abs(mm) < 2),
|
|
143
|
-
assert jnp.all(jnp.abs(mm) >= 1),
|
|
142
|
+
assert jnp.all(xq == xq_), 'Float Decompose Error @ Normal Number'
|
|
143
|
+
assert jnp.all(jnp.abs(mm) < 2), 'Mantissa Error @ Normal Number' # type: ignore
|
|
144
|
+
assert jnp.all(jnp.abs(mm) >= 1), 'Mantissa Error @ Normal Number' # type: ignore
|
|
144
145
|
|
|
145
146
|
mm, ee = float_decompose(x_subnormal, M, E, E0)
|
|
146
147
|
mm_, ee_ = float_decompose_np(x_subnormal, M, E, E0)
|
|
147
|
-
assert np.all(mm == mm_) and np.all(ee == ee_),
|
|
148
|
+
assert np.all(mm == mm_) and np.all(ee == ee_), 'Float Decompose Error @ Subnormal Number'
|
|
148
149
|
|
|
149
|
-
xq_ = mm * 2
|
|
150
|
+
xq_ = mm * 2.0**ee # type: ignore
|
|
150
151
|
xq = float_quantize(x_subnormal, M, E, E0)
|
|
151
|
-
assert jnp.all(xq == xq_),
|
|
152
|
-
assert jnp.all(jnp.abs(mm) < 1),
|
|
152
|
+
assert jnp.all(xq == xq_), 'Float Decompose Error @ Subnormal Number'
|
|
153
|
+
assert jnp.all(jnp.abs(mm) < 1), 'Mantissa Error @ Subnormal Number' # type: ignore
|
|
153
154
|
|
|
154
155
|
mm, ee = float_decompose(x_overflow, M, E, E0)
|
|
155
156
|
mm_, ee_ = float_decompose_np(x_overflow, M, E, E0)
|
|
156
|
-
assert np.all(mm == mm_) and np.all(ee == ee_),
|
|
157
|
+
assert np.all(mm == mm_) and np.all(ee == ee_), 'Float Decompose Error'
|
|
157
158
|
|
|
158
|
-
xq_ = mm * 2
|
|
159
|
+
xq_ = mm * 2.0**ee # type: ignore
|
|
159
160
|
xq = float_quantize(x_overflow, M, E, E0)
|
|
160
|
-
assert jnp.all(xq == xq_),
|
|
161
|
-
assert jnp.all(jnp.abs(mm) < 2),
|
|
162
|
-
assert jnp.all(jnp.abs(mm) >= 1),
|
|
161
|
+
assert jnp.all(xq == xq_), 'Float Decompose Error'
|
|
162
|
+
assert jnp.all(jnp.abs(mm) < 2), 'Mantissa Error @ Overflow Number' # type: ignore
|
|
163
|
+
assert jnp.all(jnp.abs(mm) >= 1), 'Mantissa Error @ Overflow Number' # type: ignore
|
|
163
164
|
|
|
164
165
|
|
|
165
166
|
def test_binary_quantizer_grad(x_kif):
|
|
166
167
|
x, *_ = x_kif
|
|
167
168
|
xq = binary_quantize(x)
|
|
168
|
-
assert jnp.all((xq == -1) | (xq == 1)),
|
|
169
|
+
assert jnp.all((xq == -1) | (xq == 1)), 'Binary Quantizer Error'
|
|
169
170
|
grad = jax.grad(lambda x: jnp.sum(binary_quantize(x)))(x)
|
|
170
|
-
assert jnp.all(grad > 0),
|
|
171
|
+
assert jnp.all(grad > 0), 'Gradient Error'
|
|
171
172
|
|
|
172
173
|
|
|
173
174
|
def test_ternary_quantizer_grad(x_kif):
|
|
174
175
|
x, *_ = x_kif
|
|
175
176
|
xq = ternary_quantize(x)
|
|
176
|
-
assert jnp.all((xq == -1) | (xq == 0) | (xq == 1)),
|
|
177
|
+
assert jnp.all((xq == -1) | (xq == 0) | (xq == 1)), 'Ternary Quantizer Error'
|
|
177
178
|
grad = jax.grad(lambda x: jnp.sum(ternary_quantize(x)))(x)
|
|
178
|
-
assert jnp.all(grad > 0),
|
|
179
|
+
assert jnp.all(grad > 0), 'Gradient Error'
|
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
from collections.abc import Callable
|
|
2
|
-
from typing import Any, TypeVar
|
|
3
|
-
|
|
4
|
-
import keras
|
|
5
|
-
from keras import ops
|
|
6
|
-
from keras.api.random import SeedGenerator
|
|
7
|
-
from numpy.typing import ArrayLike
|
|
8
|
-
|
|
9
|
-
round_mode_registry: dict[str, Callable[[Any, bool | None], Any]] = {}
|
|
10
|
-
round_mode_registry_scaled: dict[str, Callable[[Any, Any, bool | None, SeedGenerator | None], Any]] = {}
|
|
11
|
-
saturation_mode_registry: dict[str, Callable[[Any, Any, Any, Any, bool | None], Any]] = {}
|
|
12
|
-
|
|
13
|
-
T = TypeVar('T', bound=ArrayLike)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@ops.custom_gradient
|
|
17
|
-
def _clip(x, min_value, max_value):
|
|
18
|
-
r = ops.clip(x, min_value, max_value)
|
|
19
|
-
|
|
20
|
-
def grad(*args, upstream=None):
|
|
21
|
-
mask1 = x > max_value
|
|
22
|
-
mask2 = x < min_value
|
|
23
|
-
if upstream is None:
|
|
24
|
-
(upstream,) = args
|
|
25
|
-
dy = upstream
|
|
26
|
-
dx = ops.where(~(mask1 | mask2), dy, 0.0)
|
|
27
|
-
dmax = ops.where(mask1, dy, 0.0)
|
|
28
|
-
dmin = ops.where(mask2, dy, 0.0)
|
|
29
|
-
return dx, dmin, dmax
|
|
30
|
-
|
|
31
|
-
return r, grad
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def rnd_mode(names: str | list[str] | tuple[str, ...]):
|
|
35
|
-
names = (names,) if isinstance(names, str) else names
|
|
36
|
-
|
|
37
|
-
def inner(func):
|
|
38
|
-
@keras.ops.custom_gradient
|
|
39
|
-
def inner_wrapper(x, f):
|
|
40
|
-
scale = 2.0**f
|
|
41
|
-
sx = x * scale
|
|
42
|
-
sxq = func(sx)
|
|
43
|
-
xq = sxq / scale
|
|
44
|
-
delta = xq - x
|
|
45
|
-
|
|
46
|
-
def grad(*args, upstream=None):
|
|
47
|
-
if upstream is None:
|
|
48
|
-
(upstream,) = args
|
|
49
|
-
dy = upstream
|
|
50
|
-
dx = dy
|
|
51
|
-
df = -ops.log(2.0) * delta * dy # type: ignore
|
|
52
|
-
return dx, df
|
|
53
|
-
|
|
54
|
-
return ops.stop_gradient(xq), grad
|
|
55
|
-
|
|
56
|
-
@keras.ops.custom_gradient
|
|
57
|
-
def inner_ste_wrapper(x):
|
|
58
|
-
r = func(x)
|
|
59
|
-
|
|
60
|
-
def grad(*args, upstream=None):
|
|
61
|
-
if upstream is None:
|
|
62
|
-
(upstream,) = args
|
|
63
|
-
dy = upstream
|
|
64
|
-
return dy
|
|
65
|
-
|
|
66
|
-
return ops.stop_gradient(r), grad
|
|
67
|
-
|
|
68
|
-
def wrapper(x, f, training=None, seed_gen=None): # type: ignore
|
|
69
|
-
return inner_wrapper(x, f)
|
|
70
|
-
|
|
71
|
-
def ste_wrapper(x, training=None): # type: ignore
|
|
72
|
-
return inner_ste_wrapper(x)
|
|
73
|
-
|
|
74
|
-
for name in names:
|
|
75
|
-
assert name not in round_mode_registry_scaled, f"Rounding mode '{name}' already exists."
|
|
76
|
-
round_mode_registry_scaled[name] = wrapper
|
|
77
|
-
round_mode_registry_scaled[func.__name__.upper()] = wrapper
|
|
78
|
-
|
|
79
|
-
for name in names:
|
|
80
|
-
assert name not in round_mode_registry, f"Rounding mode '{name}' already exists."
|
|
81
|
-
round_mode_registry[name] = ste_wrapper
|
|
82
|
-
round_mode_registry[func.__name__.upper()] = ste_wrapper
|
|
83
|
-
|
|
84
|
-
return ste_wrapper
|
|
85
|
-
|
|
86
|
-
return inner
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
@rnd_mode('TRN')
|
|
90
|
-
def floor(x):
|
|
91
|
-
return ops.floor(x)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
@rnd_mode('RND')
|
|
95
|
-
def round(x):
|
|
96
|
-
# Round to nearest, ties positive infinity.
|
|
97
|
-
return ops.floor(x + 0.5)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
@rnd_mode('RND_CONV')
|
|
101
|
-
def round_conv(x):
|
|
102
|
-
# Round to nearest, ties to even.
|
|
103
|
-
return ops.round(x)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
@rnd_mode('TRN_ZERO')
|
|
107
|
-
def floor_zero(x):
|
|
108
|
-
# Truncate towards zero.
|
|
109
|
-
sign = ops.sign(x)
|
|
110
|
-
return ops.floor(ops.abs(x)) * sign # type: ignore
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
@rnd_mode('RND_ZERO')
|
|
114
|
-
def round_zero(x):
|
|
115
|
-
# Round to nearest, ties towards zero.
|
|
116
|
-
sign = ops.sign(x)
|
|
117
|
-
return -ops.floor(-ops.abs(x) + 0.5) * sign # type:ignore
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
@rnd_mode('RND_MIN_INF')
|
|
121
|
-
def round_min_inf(x):
|
|
122
|
-
# Round to nearest, ties towards negative infinity.
|
|
123
|
-
return -ops.floor(-x + 0.5) # type:ignore
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
@rnd_mode('RND_INF')
|
|
127
|
-
def round_inf(x):
|
|
128
|
-
# Round to nearest, ties away from zero.
|
|
129
|
-
sign = ops.sign(x)
|
|
130
|
-
return ops.floor(ops.abs(x) + 0.5) * sign # type: ignore
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def sat_mode(name: str | list | tuple):
|
|
134
|
-
names = (name,) if isinstance(name, str) else name
|
|
135
|
-
|
|
136
|
-
def inner(func):
|
|
137
|
-
for name in names:
|
|
138
|
-
assert name not in saturation_mode_registry, f"Saturation mode '{name}' already exists."
|
|
139
|
-
saturation_mode_registry[name] = func
|
|
140
|
-
saturation_mode_registry[func.__name__.upper()] = func
|
|
141
|
-
return func
|
|
142
|
-
|
|
143
|
-
return inner
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
@sat_mode('WRAP')
|
|
147
|
-
def wrap(x, k, i, f, training=None):
|
|
148
|
-
if training:
|
|
149
|
-
return x
|
|
150
|
-
|
|
151
|
-
xs = x
|
|
152
|
-
bk = i + k
|
|
153
|
-
bias = k * 2.0 ** (bk - 1)
|
|
154
|
-
return (xs + bias) % (2.0**bk) - bias
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
@sat_mode('SAT')
|
|
158
|
-
def sat(x, k, i, f, training=None):
|
|
159
|
-
f_eps = 2.0 ** (-f)
|
|
160
|
-
__max = 2.0**i
|
|
161
|
-
_max = __max - f_eps
|
|
162
|
-
_min = -__max * k
|
|
163
|
-
r = _clip(x, _min, _max)
|
|
164
|
-
return r
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
@sat_mode('SAT_SYM')
|
|
168
|
-
def sat_sym(x, k, i, f, training=None):
|
|
169
|
-
f_eps = 2.0 ** (-f)
|
|
170
|
-
_max = 2.0**i - f_eps
|
|
171
|
-
_min = -_max * k
|
|
172
|
-
r = _clip(x, _min, _max)
|
|
173
|
-
return r
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
@sat_mode('WRAP_SM')
|
|
177
|
-
def wrap_sm_fn(x, k, i, f, training=None, quant_fn: Callable = lambda x: x):
|
|
178
|
-
# x=ops.round(x*2.**f)
|
|
179
|
-
# High and low bounds are reflective. When overflows, can be less trash than WARP but still more trash than SAT.
|
|
180
|
-
eps = 2.0**-f
|
|
181
|
-
high = 2.0**i - eps
|
|
182
|
-
low = -(high + eps) * k
|
|
183
|
-
interval = 2 ** (i + k)
|
|
184
|
-
c1 = ((x) / interval) % 2 >= 1 # type: ignore
|
|
185
|
-
c1 = c1 & (ops.abs(x) > eps / 2)
|
|
186
|
-
c2 = ((x + eps / 2) / (interval / 2)) % 2 >= 1 # type: ignore
|
|
187
|
-
qx = quant_fn(x)
|
|
188
|
-
mapped = ((qx - high - eps) % interval) + low
|
|
189
|
-
|
|
190
|
-
mapped = ops.where(c2, -mapped - eps, mapped) # type: ignore
|
|
191
|
-
mapped = ops.where(c1, -mapped - eps, mapped) # type: ignore
|
|
192
|
-
|
|
193
|
-
return mapped
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def _get_fixed_quantizer(
|
|
197
|
-
round_mode: str = 'TRN', overflow_mode: str = 'WRAP'
|
|
198
|
-
) -> Callable[[T, Any, Any, Any, bool | None, SeedGenerator | None], T]:
|
|
199
|
-
"""Get a stateless fixed-point quantizer given the round and overflow mode.
|
|
200
|
-
The quantizer is differentiable w.r.t. to the input and f, also i if using saturation overflow mode.
|
|
201
|
-
|
|
202
|
-
Args:
|
|
203
|
-
round_mode: round mode, one of
|
|
204
|
-
"""
|
|
205
|
-
round_mode = round_mode.upper()
|
|
206
|
-
overflow_mode = overflow_mode.upper()
|
|
207
|
-
round_fn_scaled = round_mode_registry_scaled.get(round_mode, None)
|
|
208
|
-
if round_fn_scaled is None:
|
|
209
|
-
raise ValueError(f'Unknown rounding mode: {round_mode}')
|
|
210
|
-
sat_fn = saturation_mode_registry.get(overflow_mode, None)
|
|
211
|
-
if sat_fn is None:
|
|
212
|
-
raise ValueError(f'Unknown saturation mode: {overflow_mode}')
|
|
213
|
-
|
|
214
|
-
if overflow_mode == 'WRAP_SM':
|
|
215
|
-
assert round_mode in ('RND', 'RND_CONV'), 'WRAP_SM only supports RND and RND_CONV rounding modes in this implementation.'
|
|
216
|
-
|
|
217
|
-
def quantizer(x: T, k: T, i: T, f: T, training: bool | None = None, seed_gen: SeedGenerator | None = None) -> T:
|
|
218
|
-
"""Stateless fixed-point quantizer.
|
|
219
|
-
Args:
|
|
220
|
-
x: input tensor
|
|
221
|
-
k: number of fractional bits
|
|
222
|
-
i: number of integer bits
|
|
223
|
-
f: number of fractional bits
|
|
224
|
-
training: training mode
|
|
225
|
-
"""
|
|
226
|
-
i = ops.stop_gradient(ops.maximum(i, -f)) + (i - ops.stop_gradient(i)) # type: ignore
|
|
227
|
-
|
|
228
|
-
if overflow_mode == 'WRAP_SM':
|
|
229
|
-
|
|
230
|
-
def rnd_fn_wrapped(x):
|
|
231
|
-
return round_fn_scaled(x, f, training, seed_gen)
|
|
232
|
-
|
|
233
|
-
return wrap_sm_fn(x, k, i, f, training, rnd_fn_wrapped) # type: ignore
|
|
234
|
-
|
|
235
|
-
# Workaround for gradient computation around 0.
|
|
236
|
-
# When have small rounded to boundary, grad on f presents despite the value will be clipped off anyway.
|
|
237
|
-
# Thus have saturation before rounding, except for wrap mode, which doesn't round during training.
|
|
238
|
-
if overflow_mode != 'WRAP':
|
|
239
|
-
x = sat_fn(x, k, i, f, training)
|
|
240
|
-
x = round_fn_scaled(x, f, training, seed_gen)
|
|
241
|
-
if overflow_mode == 'WRAP':
|
|
242
|
-
x = sat_fn(x, k, i, f, training)
|
|
243
|
-
return x
|
|
244
|
-
|
|
245
|
-
return quantizer
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
# ======================= STOCHASTIC ROUNDING =======================
|
|
249
|
-
def register_stochastic_rounding():
|
|
250
|
-
@keras.ops.custom_gradient
|
|
251
|
-
def stochastic_scaled(x, f, noise):
|
|
252
|
-
scale = 2.0**f
|
|
253
|
-
sx = x * scale
|
|
254
|
-
sxq = ops.floor(sx + noise)
|
|
255
|
-
xq = sxq / scale
|
|
256
|
-
delta = xq - x
|
|
257
|
-
|
|
258
|
-
def grad(*args, upstream=None):
|
|
259
|
-
if upstream is None:
|
|
260
|
-
(upstream,) = args
|
|
261
|
-
dy = upstream
|
|
262
|
-
dx = dy
|
|
263
|
-
df = -ops.log(2.0) * delta * dy # type: ignore
|
|
264
|
-
return dx, df, None
|
|
265
|
-
|
|
266
|
-
return ops.stop_gradient(xq), grad
|
|
267
|
-
|
|
268
|
-
rnd_scaled = round_mode_registry_scaled['RND']
|
|
269
|
-
rnd_conv_scaled = round_mode_registry_scaled['RND_CONV']
|
|
270
|
-
|
|
271
|
-
def outer_rnd(x, f, training=None, seed_gen=None):
|
|
272
|
-
if training:
|
|
273
|
-
noise = keras.random.uniform(ops.shape(x), seed=seed_gen)
|
|
274
|
-
return stochastic_scaled(x, f, noise)
|
|
275
|
-
else:
|
|
276
|
-
return rnd_scaled(x, f, training, seed_gen)
|
|
277
|
-
|
|
278
|
-
def outer_rnd_conv(x, f, training=None, seed_gen=None):
|
|
279
|
-
if training:
|
|
280
|
-
noise = keras.random.uniform(ops.shape(x), seed=seed_gen)
|
|
281
|
-
return stochastic_scaled(x, f, noise)
|
|
282
|
-
else:
|
|
283
|
-
return rnd_conv_scaled(x, f, training, seed_gen)
|
|
284
|
-
|
|
285
|
-
round_mode_registry_scaled['S_RND'] = outer_rnd
|
|
286
|
-
round_mode_registry_scaled['S_RND_CONV'] = outer_rnd_conv
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
register_stochastic_rounding()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|