quantizers 1.0.0__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.
quantizers/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .binary import *
2
+ from .fixed_point import *
3
+ from .minifloat import *
4
+ from .stateful_quantizers import BinaryQ, FixedQ, MinifloatQ, TernaryQ
quantizers/_version.py ADDED
@@ -0,0 +1,16 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ from typing import Tuple, Union
6
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
7
+ else:
8
+ VERSION_TUPLE = object
9
+
10
+ version: str
11
+ __version__: str
12
+ __version_tuple__: VERSION_TUPLE
13
+ version_tuple: VERSION_TUPLE
14
+
15
+ __version__ = version = '1.0.0'
16
+ __version_tuple__ = version_tuple = (1, 0, 0)
@@ -0,0 +1,9 @@
1
+ from .binary_ops_np import binary_quantize_np, ternary_quantize_np
2
+
3
+ try:
4
+ from .binary_ops import binary_quantize, ternary_quantize
5
+ except ImportError:
6
+ binary_quantize = binary_quantize_np
7
+ ternary_quantize = ternary_quantize_np
8
+
9
+ __all__ = ['binary_quantize', 'ternary_quantize', 'binary_quantize_np', 'ternary_quantize_np']
@@ -0,0 +1,28 @@
1
+ from keras import ops
2
+ from keras.api.random import SeedGenerator
3
+
4
+
5
+ @ops.custom_gradient
6
+ def binary_quantize(x):
7
+ r = ops.where(x > 0, 1., -1.)
8
+
9
+ def grad(*args, upstream=None):
10
+ if upstream is None:
11
+ (upstream,) = args
12
+ dy = upstream
13
+ return dy * (1. - ops.tanh(x)**2) # type: ignore
14
+
15
+ return ops.stop_gradient(r), grad
16
+
17
+
18
+ @ops.custom_gradient
19
+ def ternary_quantize(x):
20
+ r = ops.where(x > 0.5, 1., ops.where(x < -0.5, -1., 0.))
21
+
22
+ def grad(*args, upstream=None):
23
+ if upstream is None:
24
+ (upstream,) = args
25
+ dy = upstream
26
+ return dy * (1. - ops.tanh(x)**2) # type: ignore
27
+
28
+ return ops.stop_gradient(r), grad
@@ -0,0 +1,9 @@
1
+ import numpy as np
2
+
3
+
4
+ def binary_quantize_np(x):
5
+ return np.where(x > 0, 1., -1.)
6
+
7
+
8
+ def ternary_quantize_np(x):
9
+ return np.where(x > 0.5, 1., np.where(x < -0.5, -1., 0.))
@@ -0,0 +1,8 @@
1
+ from .fixed_point_ops_np import get_fixed_quantizer_np
2
+
3
+ try:
4
+ from .fixed_point_ops import get_fixed_quantizer
5
+ except ImportError:
6
+ get_fixed_quantizer = get_fixed_quantizer_np
7
+
8
+ __all__ = ['get_fixed_quantizer', 'get_fixed_quantizer_np']
@@ -0,0 +1,279 @@
1
+ from collections.abc import Callable
2
+ from functools import singledispatch
3
+ from typing import Any, TypeVar
4
+
5
+ import keras
6
+ from keras import ops
7
+ from keras.api.random import SeedGenerator
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
+ from numpy.typing import ArrayLike
13
+
14
+ T = TypeVar('T', bound=ArrayLike)
15
+
16
+
17
+ @ops.custom_gradient
18
+ def _clip(x, min_value, max_value):
19
+ r = ops.clip(x, min_value, max_value)
20
+
21
+ def grad(*args, upstream=None):
22
+ mask1 = x > max_value
23
+ mask2 = x < min_value
24
+ if upstream is None:
25
+ (upstream,) = args
26
+ dy = upstream
27
+ dx = ops.where(~(mask1 | mask2), dy, 0.)
28
+ dmax = ops.where(mask1, dy, 0.)
29
+ dmin = ops.where(mask2, dy, 0.)
30
+ return dx, dmin, dmax
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.**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.) * delta * dy # type: ignore
52
+ return dx, df
53
+ return ops.stop_gradient(xq), grad
54
+
55
+ @keras.ops.custom_gradient
56
+ def inner_ste_wrapper(x):
57
+ r = func(x)
58
+
59
+ def grad(*args, upstream=None):
60
+ if upstream is None:
61
+ (upstream,) = args
62
+ dy = upstream
63
+ return dy
64
+ return ops.stop_gradient(r), grad
65
+
66
+ def wrapper(x, f, training=None, seed_gen=None): # type: ignore
67
+ return inner_wrapper(x, f)
68
+
69
+ def ste_wrapper(x, training=None): # type: ignore
70
+ return inner_ste_wrapper(x)
71
+
72
+ for name in names:
73
+ assert name not in round_mode_registry_scaled, f"Rounding mode '{name}' already exists."
74
+ round_mode_registry_scaled[name] = wrapper
75
+ round_mode_registry_scaled[func.__name__.upper()] = wrapper
76
+
77
+ for name in names:
78
+ assert name not in round_mode_registry, f"Rounding mode '{name}' already exists."
79
+ round_mode_registry[name] = ste_wrapper
80
+ round_mode_registry[func.__name__.upper()] = ste_wrapper
81
+
82
+ return ste_wrapper
83
+ return inner
84
+
85
+
86
+ @rnd_mode('TRN')
87
+ def floor(x):
88
+ return ops.floor(x)
89
+
90
+
91
+ @rnd_mode('RND')
92
+ def round(x):
93
+ # Round to nearest, ties positive infinity.
94
+ return ops.floor(x + 0.5)
95
+
96
+
97
+ @rnd_mode('RND_CONV')
98
+ def round_conv(x):
99
+ # Round to nearest, ties to even.
100
+ return ops.round(x)
101
+
102
+
103
+ @rnd_mode('TRN_ZERO')
104
+ def floor_zero(x):
105
+ # Truncate towards zero.
106
+ sign = ops.sign(x)
107
+ return ops.floor(ops.abs(x)) * sign # type: ignore
108
+
109
+
110
+ @rnd_mode('RND_ZERO')
111
+ def round_zero(x):
112
+ # Round to nearest, ties towards zero.
113
+ sign = ops.sign(x)
114
+ return -ops.floor(-ops.abs(x) + 0.5) * sign # type:ignore
115
+
116
+
117
+ @rnd_mode('RND_MIN_INF')
118
+ def round_min_inf(x):
119
+ # Round to nearest, ties towards negative infinity.
120
+ return -ops.floor(-x + 0.5) # type:ignore
121
+
122
+
123
+ @rnd_mode('RND_INF')
124
+ def round_inf(x):
125
+ # Round to nearest, ties away from zero.
126
+ sign = ops.sign(x)
127
+ return ops.floor(ops.abs(x) + 0.5) * sign # type: ignore
128
+
129
+
130
+ def sat_mode(name: str | list | tuple):
131
+ names = (name,) if isinstance(name, str) else name
132
+
133
+ def inner(func):
134
+ for name in names:
135
+ assert name not in saturation_mode_registry, f"Saturation mode '{name}' already exists."
136
+ saturation_mode_registry[name] = func
137
+ saturation_mode_registry[func.__name__.upper()] = func
138
+ return func
139
+ return inner
140
+
141
+
142
+ @sat_mode('WRAP')
143
+ def wrap(x, k, i, f, training=None):
144
+ if training:
145
+ return x
146
+
147
+ xs = x
148
+ bk = i + k
149
+ bias = k * 2**(bk - 1)
150
+ return ((xs + bias) % (2**bk) - bias)
151
+
152
+
153
+ @sat_mode('SAT')
154
+ def sat(x, k, i, f, training=None):
155
+ f_eps = 2**(-f)
156
+ __max = 2**i
157
+ _max = __max - f_eps
158
+ _min = -__max * k
159
+ r = _clip(x, _min, _max)
160
+ return r
161
+
162
+
163
+ @sat_mode('SAT_SYM')
164
+ def sat_sym(x, k, i, f, training=None):
165
+ f_eps = 2**(-f)
166
+ _max = 2**i - f_eps
167
+ _min = -_max * k
168
+ r = _clip(x, _min, _max)
169
+ return r
170
+
171
+
172
+ @sat_mode('WRAP_SM')
173
+ def wrap_sm_fn(x, k, i, f, training=None, quant_fn: Callable = lambda x: x):
174
+ # x=ops.round(x*2**f)
175
+ # High and low bounds are reflective. When overflows, can be less trash than WARP but still more trash than SAT.
176
+ eps = 2**-f
177
+ high = 2**i - eps
178
+ low = -(high + eps) * k
179
+ interval = 2 ** (i + k)
180
+ c1 = ((x) / interval) % 2 >= 1 # type: ignore
181
+ c1 = c1 & (ops.abs(x) > eps / 2)
182
+ c2 = ((x + eps / 2) / (interval / 2)) % 2 >= 1 # type: ignore
183
+ qx = quant_fn(x)
184
+ mapped = ((qx - high - eps) % interval) + low
185
+
186
+ mapped = ops.where(c2, -mapped - eps, mapped) # type: ignore
187
+ mapped = ops.where(c1, -mapped - eps, mapped) # type: ignore
188
+
189
+ return mapped
190
+
191
+
192
+ def get_fixed_quantizer(round_mode: str = 'TRN', overflow_mode: str = 'WRAP') -> Callable[[T, Any, Any, Any, bool | None, SeedGenerator | None], T]:
193
+ """Get a stateless fixed-point quantizer given the round and overflow mode.
194
+ The quantizer is differentiable w.r.t. to the input and f, also i if using saturation overflow mode.
195
+
196
+ Args:
197
+ round_mode: round mode, one of
198
+ """
199
+ round_mode = round_mode.upper()
200
+ overflow_mode = overflow_mode.upper()
201
+ round_fn_scaled = round_mode_registry_scaled.get(round_mode, None)
202
+ if round_fn_scaled is None:
203
+ raise ValueError(f"Unknown rounding mode: {round_mode}") # pragma: no cover
204
+ sat_fn = saturation_mode_registry.get(overflow_mode, None)
205
+ if sat_fn is None:
206
+ raise ValueError(f"Unknown saturation mode: {overflow_mode}") # pragma: no cover
207
+
208
+ if overflow_mode == 'WRAP_SM':
209
+ assert round_mode in ('RND', 'RND_CONV'), "WRAP_SM only supports RND and RND_CONV rounding modes in this implementation."
210
+
211
+ def quantizer(x: T, k: T, i: T, f: T, training: bool | None = None, seed_gen: SeedGenerator | None = None) -> T:
212
+ """Stateless fixed-point quantizer.
213
+ Args:
214
+ x: input tensor
215
+ k: number of fractional bits
216
+ i: number of integer bits
217
+ f: number of fractional bits
218
+ training: training mode
219
+ """
220
+ i = ops.stop_gradient(ops.maximum(i, -f)) + (i - ops.stop_gradient(i)) # type: ignore
221
+
222
+ if overflow_mode == 'WRAP_SM':
223
+ def rnd_fn_wrapped(x):
224
+ return round_fn_scaled(x, f, training, seed_gen)
225
+ return wrap_sm_fn(x, k, i, f, training, rnd_fn_wrapped) # type: ignore
226
+
227
+ # Workaround for gradient computation around 0.
228
+ # When have small rounded to boundary, grad on f presents despite the value will be clipped off anyway.
229
+ # Thus have saturation before rounding, except for wrap mode, which doesn't round during training.
230
+ if overflow_mode != 'WRAP':
231
+ x = sat_fn(x, k, i, f, training)
232
+ x = round_fn_scaled(x, f, training, seed_gen)
233
+ if overflow_mode == 'WRAP':
234
+ x = sat_fn(x, k, i, f, training)
235
+ return x
236
+ return quantizer
237
+
238
+
239
+ # ======================= STOCHASTIC ROUNDING =======================
240
+ def register_stochastic_rounding():
241
+ @keras.ops.custom_gradient
242
+ def stochastic_scaled(x, f, noise):
243
+ scale = 2.**f
244
+ sx = x * scale
245
+ sxq = ops.floor(sx + noise)
246
+ xq = sxq / scale
247
+ delta = xq - x
248
+
249
+ def grad(*args, upstream=None):
250
+ if upstream is None:
251
+ (upstream,) = args
252
+ dy = upstream
253
+ dx = dy
254
+ df = - ops.log(2.) * delta * dy # type: ignore
255
+ return dx, df, None
256
+ return ops.stop_gradient(xq), grad
257
+
258
+ rnd_scaled = round_mode_registry_scaled['RND']
259
+ rnd_conv_scaled = round_mode_registry_scaled['RND_CONV']
260
+
261
+ def outer_rnd(x, f, training=None, seed_gen=None):
262
+ if training:
263
+ noise = keras.random.uniform(ops.shape(x), seed=seed_gen)
264
+ return stochastic_scaled(x, f, noise)
265
+ else:
266
+ return rnd_scaled(x, f, training, seed_gen)
267
+
268
+ def outer_rnd_conv(x, f, training=None, seed_gen=None):
269
+ if training:
270
+ noise = keras.random.uniform(ops.shape(x), seed=seed_gen)
271
+ return stochastic_scaled(x, f, noise)
272
+ else:
273
+ return rnd_conv_scaled(x, f, training, seed_gen)
274
+
275
+ round_mode_registry_scaled['S_RND'] = outer_rnd
276
+ round_mode_registry_scaled['S_RND_CONV'] = outer_rnd_conv
277
+
278
+
279
+ register_stochastic_rounding()
@@ -0,0 +1,163 @@
1
+ from collections.abc import Callable
2
+ from typing import Any, TypeVar
3
+
4
+ import numpy as np
5
+ from numpy.typing import ArrayLike
6
+
7
+ round_mode_registry_np: dict[str, Callable[[Any, bool | None], Any]] = {}
8
+ round_mode_registry_scaled_np: dict[str, Callable[[Any, Any, bool | None], Any]] = {}
9
+ saturation_mode_registry_np: dict[str, Callable[[Any, Any, Any, Any, bool | None], Any]] = {}
10
+
11
+ T = TypeVar('T', bound=np.ndarray)
12
+
13
+
14
+ def rnd_mode_np(names: str | list[str] | tuple[str, ...]):
15
+ names = (names,) if isinstance(names, str) else names
16
+
17
+ def inner(func):
18
+ def wrapper(x, f, training=None, seed_gen=None):
19
+ scale = 2.**f
20
+ sx = x * scale
21
+ sxq = func(sx)
22
+ xq = sxq / scale
23
+ return xq
24
+
25
+ for name in names:
26
+ assert name not in round_mode_registry_scaled_np, f"Rounding mode '{name}' already exists."
27
+ round_mode_registry_scaled_np[name] = wrapper
28
+ round_mode_registry_scaled_np[func.__name__.upper()] = wrapper
29
+
30
+ for name in names:
31
+ assert name not in round_mode_registry_np, f"Rounding mode '{name}' already exists."
32
+ round_mode_registry_np[name] = func
33
+ round_mode_registry_np[func.__name__.upper()] = func
34
+
35
+ return func
36
+ return inner
37
+
38
+
39
+ @rnd_mode_np('TRN')
40
+ def floor(x):
41
+ return np.floor(x)
42
+
43
+
44
+ @rnd_mode_np('RND')
45
+ def round(x):
46
+ return np.floor(x + 0.5)
47
+
48
+
49
+ @rnd_mode_np('RND_CONV')
50
+ def round_conv(x):
51
+ return np.round(x)
52
+
53
+
54
+ @rnd_mode_np('TRN_ZERO')
55
+ def floor_zero(x):
56
+ sign = np.sign(x)
57
+ return np.floor(np.abs(x)) * sign
58
+
59
+
60
+ @rnd_mode_np('RND_ZERO')
61
+ def round_zero(x):
62
+ sign = np.sign(x)
63
+ return -np.floor(-np.abs(x) + 0.5) * sign
64
+
65
+
66
+ @rnd_mode_np('RND_MIN_INF')
67
+ def round_min_inf(x):
68
+ return -np.floor(-x + 0.5)
69
+
70
+
71
+ @rnd_mode_np('RND_INF')
72
+ def round_inf(x):
73
+ sign = np.sign(x)
74
+ return np.floor(np.abs(x) + 0.5) * sign
75
+
76
+
77
+ def sat_mode_np(name: str | list | tuple):
78
+ names = (name,) if isinstance(name, str) else name
79
+
80
+ def inner(func):
81
+ for name in names:
82
+ assert name not in saturation_mode_registry_np, f"Saturation mode '{name}' already exists."
83
+ saturation_mode_registry_np[name] = func
84
+ saturation_mode_registry_np[func.__name__.upper()] = func
85
+ return func
86
+ return inner
87
+
88
+
89
+ @sat_mode_np('WRAP')
90
+ def wrap(x, k, i, f, training=None):
91
+ xs = x
92
+ bk = i + k
93
+ bias = k * 2**(bk - 1)
94
+ return ((xs + bias) % (2**bk) - bias)
95
+
96
+
97
+ @sat_mode_np('SAT')
98
+ def sat(x, k, i, f, training=None):
99
+ f_eps = 2**(-f)
100
+ __max = 2**i
101
+ _max = __max - f_eps
102
+ _min = -__max * k
103
+ return np.clip(x, _min, _max)
104
+
105
+
106
+ @sat_mode_np('SAT_SYM')
107
+ def sat_sym(x, k, i, f, training=None):
108
+ f_eps = 2**(-f)
109
+ _max = 2**i - f_eps
110
+ _min = -_max * k
111
+ return np.clip(x, _min, _max)
112
+
113
+
114
+ @sat_mode_np('WRAP_SM')
115
+ def wrap_sm_fn(x, k, i, f, training=None, quant_fn: Callable = lambda x: x):
116
+ eps = 2**-f
117
+ high = 2**i - eps
118
+ low = -(high + eps) * k
119
+ interval = 2 ** (i + k)
120
+ c1 = ((x) / interval) % 2 >= 1
121
+ c1 = c1 & (np.abs(x) > eps / 2)
122
+ c2 = ((x + eps / 2) / (interval / 2)) % 2 >= 1
123
+ qx = quant_fn(x)
124
+ mapped = ((qx - high - eps) % interval) + low
125
+
126
+ mapped = np.where(c2, -mapped - eps, mapped)
127
+ mapped = np.where(c1, -mapped - eps, mapped)
128
+
129
+ return mapped
130
+
131
+
132
+ def get_fixed_quantizer_np(round_mode: str = 'TRN', overflow_mode: str = 'WRAP'):
133
+ round_mode = round_mode.upper()
134
+ overflow_mode = overflow_mode.upper()
135
+ round_mode = round_mode[2:] if round_mode.startswith('S_') else round_mode
136
+
137
+ round_fn_scaled = round_mode_registry_scaled_np.get(round_mode, None)
138
+ if round_fn_scaled is None:
139
+ raise ValueError(f"Unknown rounding mode: {round_mode}") # pragma: no cover
140
+ sat_fn = saturation_mode_registry_np.get(overflow_mode, None)
141
+ if sat_fn is None:
142
+ raise ValueError(f"Unknown saturation mode: {overflow_mode}") # pragma: no cover
143
+
144
+ if overflow_mode == 'WRAP_SM':
145
+ assert round_mode in ('RND', 'RND_CONV'), "WRAP_SM only supports RND and RND_CONV rounding modes in this implementation."
146
+
147
+ def quantizer(x: T, k: T, i: T, f: T, training: bool | None = False, seed_gen: None = None) -> np.ndarray:
148
+ assert not training, "Training mode not supported in numpy implementation."
149
+
150
+ _i = np.maximum(i, -f)
151
+
152
+ if overflow_mode == 'WRAP_SM':
153
+ def rnd_fn_wrapped(x):
154
+ return round_fn_scaled(x, f, training)
155
+ return wrap_sm_fn(x, k, _i, f, training, rnd_fn_wrapped)
156
+
157
+ if overflow_mode != 'WRAP':
158
+ x = sat_fn(x, k, _i, f, training)
159
+ x = round_fn_scaled(x, f, training)
160
+ if overflow_mode == 'WRAP':
161
+ x = sat_fn(x, k, _i, f, training)
162
+ return x
163
+ return quantizer
@@ -0,0 +1,9 @@
1
+ from .float_point_ops_np import float_decompose_np, float_quantize_np
2
+
3
+ try:
4
+ from .float_point_ops import float_decompose, float_quantize
5
+ except ImportError:
6
+ float_quantize = float_quantize_np
7
+ float_decompose = float_decompose_np
8
+
9
+ __all__ = ['float_quantize', 'float_decompose', 'float_quantize_np', 'float_decompose_np']
@@ -0,0 +1,56 @@
1
+ import numpy as np
2
+ from keras import ops
3
+
4
+
5
+ @ops.custom_gradient
6
+ def float_quantize(x, m, e, e0=0.):
7
+ '''Quantize an array to floatlet (m mantissa bits, excl. sign bit, e exponent bits) format. Tentative gradient impl.'''
8
+ m = m + 1
9
+ eps = 1e-7
10
+
11
+ e_req = ops.floor(ops.log2(ops.abs(x) + eps)) # type: ignore
12
+ _e_high = ops.maximum(2.**(e - 1), 1.)
13
+ e_low, e_high = -_e_high + e0 + 1, _e_high + e0 - 1 # type: ignore
14
+ e_act = ops.clip(e_req, e_low, e_high)
15
+ scale = 2.**(e_act - m + 1)
16
+ sig = x / scale
17
+ qsig = ops.round(sig)
18
+ _sig_high = 2.**m
19
+ _sig_high = ops.where(e_high != e_act, _sig_high, _sig_high - 1)
20
+ clip_sig = ops.clip(qsig, -_sig_high, _sig_high) # type: ignore
21
+ qx = clip_sig * scale
22
+
23
+ def grad(*args, upstream=None):
24
+ if upstream is None:
25
+ (upstream,) = args
26
+ dy = upstream
27
+ mask_e = e_req != e_act
28
+ mask_m = e_req <= e_high # type: ignore
29
+ dm = ops.where(mask_m, scale * (sig - qsig) * dy * ops.log(2.), 0.)
30
+ d_exp = ops.where(mask_e, (x - qx) * dy * ops.log(2.), 0.)
31
+ de = d_exp * ops.log(2.) * _e_high # type: ignore
32
+ de0 = ops.where(e_req > e_high, d_exp, -d_exp) # type: ignore
33
+ return dy, dm, de, de0
34
+
35
+ return ops.stop_gradient(qx), grad
36
+
37
+
38
+ def float_decompose(x, m, e, e0=0):
39
+ '''Quantize an array to floatlet (m mantissa bits, excl. sign bit, e exponent bits) format. Tentative gradient impl.'''
40
+ m = m + 1
41
+ eps = 1e-7
42
+
43
+ e_req = ops.floor(ops.log2(ops.abs(x) + eps)) # type: ignore
44
+ _e_high = 2.**(e - 1)
45
+ e_low, e_high = -_e_high + e0, _e_high + e0 - 1
46
+ e_act = ops.clip(e_req, e_low + 1, e_high)
47
+ scale = 2.**(e_act - m + 1)
48
+ sig = x / scale
49
+ qsig = ops.round(sig)
50
+ cond = (e_act != e_high) & (ops.abs(qsig) == 2**m)
51
+ e_act = ops.where(cond, e_act + 1, e_act) # type: ignore
52
+ qsig = ops.where(cond, qsig / 2., qsig) # type: ignore
53
+ r_mantissa_bound = 2 - 2.**(-m + 1)
54
+ r_mantissa = ops.clip(qsig * 2.**(-m + 1), -r_mantissa_bound, r_mantissa_bound)
55
+ r_exponent = e_act
56
+ return r_mantissa, r_exponent
@@ -0,0 +1,42 @@
1
+ import numpy as np
2
+
3
+
4
+ def float_quantize_np(x, m, e, e0=0.):
5
+ '''Quantize an array to floatlet (m mantissa bits, excl. sign bit, e exponent bits) format. Tentative gradient impl.'''
6
+ m = m + 1
7
+ eps = 1e-7
8
+
9
+ e_req = np.floor(np.log2(np.abs(x) + eps)) # type: ignore
10
+ _e_high = np.maximum(2.**(e - 1), 1.)
11
+ e_low, e_high = -_e_high + e0 + 1, _e_high + e0 - 1 # type: ignore
12
+ e_act = np.clip(e_req, e_low, e_high)
13
+ scale = 2.**(e_act - m + 1)
14
+ sig = x / scale
15
+ qsig = np.round(sig)
16
+ _sig_high = 2.**m
17
+ _sig_high = np.where(e_high != e_act, _sig_high, _sig_high - 1)
18
+ clip_sig = np.clip(qsig, -_sig_high, _sig_high) # type: ignore
19
+ qx = clip_sig * scale
20
+
21
+ return qx
22
+
23
+
24
+ def float_decompose_np(x, m, e, e0=0):
25
+ '''Quantize an array to floatlet (m mantissa bits, excl. sign bit, e exponent bits) format. Tentative gradient impl.'''
26
+ m = m + 1
27
+ eps = 1e-7
28
+
29
+ e_req = np.floor(np.log2(np.abs(x) + eps)) # type: ignore
30
+ _e_high = 2.**(e - 1)
31
+ e_low, e_high = -_e_high + e0, _e_high + e0 - 1
32
+ e_act = np.clip(e_req, e_low + 1, e_high)
33
+ scale = 2.**(e_act - m + 1)
34
+ sig = x / scale
35
+ qsig = np.round(sig)
36
+ cond = (e_act != e_high) & (np.abs(qsig) == 2**m)
37
+ e_act = np.where(cond, e_act + 1, e_act) # type: ignore
38
+ qsig = np.where(cond, qsig / 2., qsig) # type: ignore
39
+ r_mantissa_bound = 2 - 2.**(-m + 1)
40
+ r_mantissa = np.clip(qsig * 2.**(-m + 1), -r_mantissa_bound, r_mantissa_bound)
41
+ r_exponent = e_act
42
+ return r_mantissa, r_exponent
@@ -0,0 +1,142 @@
1
+ from typing import Any, TypeVar, overload
2
+
3
+ import numpy as np
4
+
5
+ from .binary import *
6
+ from .fixed_point import *
7
+ from .minifloat import *
8
+
9
+ T = TypeVar('T', np.ndarray, float, int)
10
+
11
+
12
+ def is_tensor(x):
13
+ base_module = x.__class__.__module__.split('.', 1)[0]
14
+ return base_module in ('tensorflow', 'torch', 'jaxlib')
15
+
16
+
17
+ class BinaryQ:
18
+ """Stateful binary quantizer."""
19
+
20
+ @overload
21
+ def __call__(self, x: T) -> np.ndarray:
22
+ ... # pragma: no cover
23
+
24
+ @overload
25
+ def __call__(self, x: Any) -> Any:
26
+ ... # pragma: no cover
27
+
28
+ def __call__(self, x):
29
+ use_numpy = not is_tensor(x)
30
+ if use_numpy:
31
+ return binary_quantize_np(x)
32
+ else:
33
+ return binary_quantize(x)
34
+
35
+ def __repr__(self):
36
+ return 'Binary()'
37
+
38
+
39
+ class TernaryQ:
40
+ """Stateful ternary quantizer."""
41
+
42
+ @overload
43
+ def __call__(self, x: T) -> np.ndarray:
44
+ ... # pragma: no cover
45
+
46
+ @overload
47
+ def __call__(self, x: Any) -> Any:
48
+ ... # pragma: no cover
49
+
50
+ def __call__(self, x):
51
+ use_numpy = not is_tensor(x)
52
+ if use_numpy:
53
+ return ternary_quantize_np(x)
54
+ else:
55
+ return ternary_quantize(x)
56
+
57
+ def __repr__(self):
58
+ return 'Ternary()'
59
+
60
+
61
+ class FixedQ:
62
+ """Stateful fixed-point quantizer.
63
+
64
+ Parameters
65
+ ----------
66
+ width : int
67
+ The total bit-width of the quantized number.
68
+ integer : int
69
+ The number of bits used for the integer part. INCLUDES the sign bit.
70
+ signed : bool|int
71
+ Whether the quantized number is signed.
72
+ rounding : str, optional
73
+ The rounding method to use, by default 'TRN'. Available options are 'TRN', 'TRN_ZERO', 'RND', 'S_RND', 'S_RND_CONV', 'RND_CONV', 'RND_ZERO', 'RND_INF', 'RND_MIN_INF'.
74
+ overflow : str, optional
75
+ The overflow handling method to use, by default 'WRAP'. Available options are 'WRAP', 'SAT', 'SAT_SYM', 'WRAP_SM'.
76
+ """
77
+
78
+ def __init__(self, width: int, integer: int, signed: bool | int = True, rounding: str = 'TRN', overflow: str = 'WRAP'):
79
+ self.width = width
80
+ self.integer = integer
81
+ self.signed = signed
82
+ self.rounding = rounding
83
+ self.overflow = overflow
84
+
85
+ @overload
86
+ def __call__(self, x: T) -> np.ndarray:
87
+ ... # pragma: no cover
88
+
89
+ @overload
90
+ def __call__(self, x: Any) -> Any:
91
+ ... # pragma: no cover
92
+
93
+ def __call__(self, x):
94
+ use_numpy = not is_tensor(x)
95
+ k, i, f = self.signed, self.integer - self.signed, self.width - self.integer
96
+ k, i, f = float(k), float(i), float(f)
97
+ if use_numpy:
98
+ q = get_fixed_quantizer_np(self.rounding, self.overflow)
99
+ else:
100
+ q = get_fixed_quantizer(self.rounding, self.overflow)
101
+ return q(x, k, i, f, False, None) # type: ignore
102
+
103
+ def __repr__(self):
104
+ return f'Fixed(w={self.width}, i={self.integer}, s={self.signed}, r={self.rounding}, o={self.overflow})'
105
+
106
+
107
+ class MinifloatQ:
108
+ """Stateful minifloat quantizer.
109
+
110
+ Parameters
111
+ ----------
112
+ significant : int
113
+ The number of significant bits.
114
+ exponent : int
115
+ The number of exponent bits.
116
+ exponent_bias : int
117
+ The bias applied to the exponent.
118
+ """
119
+
120
+ def __init__(self, significant: int, exponent: int, exponent_bias: int):
121
+ self.significant = significant
122
+ self.exponent = exponent
123
+ self.exponent_bias = exponent_bias
124
+
125
+ @overload
126
+ def __call__(self, x: T) -> np.ndarray:
127
+ ... # pragma: no cover
128
+
129
+ @overload
130
+ def __call__(self, x: Any) -> Any:
131
+ ... # pragma: no cover
132
+
133
+ def __call__(self, x):
134
+ s, e, e0 = float(self.significant), float(self.exponent), float(self.exponent_bias)
135
+ use_numpy = not is_tensor(x)
136
+ if use_numpy:
137
+ return float_quantize_np(x, s, e, e0)
138
+ else:
139
+ return float_quantize(x, s, e, e0)
140
+
141
+ def __repr__(self):
142
+ return f'Minifloat(s={self.significant}, e={self.exponent}, b={self.exponent_bias})'
@@ -0,0 +1,165 @@
1
+ GNU LESSER GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+
9
+ This version of the GNU Lesser General Public License incorporates
10
+ the terms and conditions of version 3 of the GNU General Public
11
+ License, supplemented by the additional permissions listed below.
12
+
13
+ 0. Additional Definitions.
14
+
15
+ As used herein, "this License" refers to version 3 of the GNU Lesser
16
+ General Public License, and the "GNU GPL" refers to version 3 of the GNU
17
+ General Public License.
18
+
19
+ "The Library" refers to a covered work governed by this License,
20
+ other than an Application or a Combined Work as defined below.
21
+
22
+ An "Application" is any work that makes use of an interface provided
23
+ by the Library, but which is not otherwise based on the Library.
24
+ Defining a subclass of a class defined by the Library is deemed a mode
25
+ of using an interface provided by the Library.
26
+
27
+ A "Combined Work" is a work produced by combining or linking an
28
+ Application with the Library. The particular version of the Library
29
+ with which the Combined Work was made is also called the "Linked
30
+ Version".
31
+
32
+ The "Minimal Corresponding Source" for a Combined Work means the
33
+ Corresponding Source for the Combined Work, excluding any source code
34
+ for portions of the Combined Work that, considered in isolation, are
35
+ based on the Application, and not on the Linked Version.
36
+
37
+ The "Corresponding Application Code" for a Combined Work means the
38
+ object code and/or source code for the Application, including any data
39
+ and utility programs needed for reproducing the Combined Work from the
40
+ Application, but excluding the System Libraries of the Combined Work.
41
+
42
+ 1. Exception to Section 3 of the GNU GPL.
43
+
44
+ You may convey a covered work under sections 3 and 4 of this License
45
+ without being bound by section 3 of the GNU GPL.
46
+
47
+ 2. Conveying Modified Versions.
48
+
49
+ If you modify a copy of the Library, and, in your modifications, a
50
+ facility refers to a function or data to be supplied by an Application
51
+ that uses the facility (other than as an argument passed when the
52
+ facility is invoked), then you may convey a copy of the modified
53
+ version:
54
+
55
+ a) under this License, provided that you make a good faith effort to
56
+ ensure that, in the event an Application does not supply the
57
+ function or data, the facility still operates, and performs
58
+ whatever part of its purpose remains meaningful, or
59
+
60
+ b) under the GNU GPL, with none of the additional permissions of
61
+ this License applicable to that copy.
62
+
63
+ 3. Object Code Incorporating Material from Library Header Files.
64
+
65
+ The object code form of an Application may incorporate material from
66
+ a header file that is part of the Library. You may convey such object
67
+ code under terms of your choice, provided that, if the incorporated
68
+ material is not limited to numerical parameters, data structure
69
+ layouts and accessors, or small macros, inline functions and templates
70
+ (ten or fewer lines in length), you do both of the following:
71
+
72
+ a) Give prominent notice with each copy of the object code that the
73
+ Library is used in it and that the Library and its use are
74
+ covered by this License.
75
+
76
+ b) Accompany the object code with a copy of the GNU GPL and this license
77
+ document.
78
+
79
+ 4. Combined Works.
80
+
81
+ You may convey a Combined Work under terms of your choice that,
82
+ taken together, effectively do not restrict modification of the
83
+ portions of the Library contained in the Combined Work and reverse
84
+ engineering for debugging such modifications, if you also do each of
85
+ the following:
86
+
87
+ a) Give prominent notice with each copy of the Combined Work that
88
+ the Library is used in it and that the Library and its use are
89
+ covered by this License.
90
+
91
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
92
+ document.
93
+
94
+ c) For a Combined Work that displays copyright notices during
95
+ execution, include the copyright notice for the Library among
96
+ these notices, as well as a reference directing the user to the
97
+ copies of the GNU GPL and this license document.
98
+
99
+ d) Do one of the following:
100
+
101
+ 0) Convey the Minimal Corresponding Source under the terms of this
102
+ License, and the Corresponding Application Code in a form
103
+ suitable for, and under terms that permit, the user to
104
+ recombine or relink the Application with a modified version of
105
+ the Linked Version to produce a modified Combined Work, in the
106
+ manner specified by section 6 of the GNU GPL for conveying
107
+ Corresponding Source.
108
+
109
+ 1) Use a suitable shared library mechanism for linking with the
110
+ Library. A suitable mechanism is one that (a) uses at run time
111
+ a copy of the Library already present on the user's computer
112
+ system, and (b) will operate properly with a modified version
113
+ of the Library that is interface-compatible with the Linked
114
+ Version.
115
+
116
+ e) Provide Installation Information, but only if you would otherwise
117
+ be required to provide such information under section 6 of the
118
+ GNU GPL, and only to the extent that such information is
119
+ necessary to install and execute a modified version of the
120
+ Combined Work produced by recombining or relinking the
121
+ Application with a modified version of the Linked Version. (If
122
+ you use option 4d0, the Installation Information must accompany
123
+ the Minimal Corresponding Source and Corresponding Application
124
+ Code. If you use option 4d1, you must provide the Installation
125
+ Information in the manner specified by section 6 of the GNU GPL
126
+ for conveying Corresponding Source.)
127
+
128
+ 5. Combined Libraries.
129
+
130
+ You may place library facilities that are a work based on the
131
+ Library side by side in a single library together with other library
132
+ facilities that are not Applications and are not covered by this
133
+ License, and convey such a combined library under terms of your
134
+ choice, if you do both of the following:
135
+
136
+ a) Accompany the combined library with a copy of the same work based
137
+ on the Library, uncombined with any other library facilities,
138
+ conveyed under the terms of this License.
139
+
140
+ b) Give prominent notice with the combined library that part of it
141
+ is a work based on the Library, and explaining where to find the
142
+ accompanying uncombined form of the same work.
143
+
144
+ 6. Revised Versions of the GNU Lesser General Public License.
145
+
146
+ The Free Software Foundation may publish revised and/or new versions
147
+ of the GNU Lesser General Public License from time to time. Such new
148
+ versions will be similar in spirit to the present version, but may
149
+ differ in detail to address new problems or concerns.
150
+
151
+ Each version is given a distinguishing version number. If the
152
+ Library as you received it specifies that a certain numbered version
153
+ of the GNU Lesser General Public License "or any later version"
154
+ applies to it, you have the option of following the terms and
155
+ conditions either of that published version or of any later version
156
+ published by the Free Software Foundation. If the Library as you
157
+ received it does not specify a version number of the GNU Lesser
158
+ General Public License, you may choose any version of the GNU Lesser
159
+ General Public License ever published by the Free Software Foundation.
160
+
161
+ If the Library as you received it specifies that a proxy can decide
162
+ whether future versions of the GNU Lesser General Public License shall
163
+ apply, that proxy's public statement of acceptance of any version is
164
+ permanent authorization for you to choose that version for the
165
+ Library.
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.1
2
+ Name: quantizers
3
+ Version: 1.0.0
4
+ Author-email: Chang Sun <chsun@cern.ch>
5
+ License: GNU Lesser General Public License v3 (LGPLv3)
6
+ Project-URL: repository, https://github.com/calad0i/quantizers
7
+ Keywords: quantization
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Provides-Extra: dependencies
16
+ Requires-Dist: numpy ; extra == 'dependencies'
17
+ Provides-Extra: grad
18
+ Requires-Dist: keras >=3 ; extra == 'grad'
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest ; extra == 'test'
21
+ Requires-Dist: pytest-cov ; extra == 'test'
22
+ Requires-Dist: pytest-xdist ; extra == 'test'
23
+ Requires-Dist: keras >=3 ; extra == 'test'
24
+
25
+ # Quantizers
26
+ [![PyPI version](https://badge.fury.io/py/quantizers.svg)](https://badge.fury.io/py/quantizers)
27
+ [![License](https://img.shields.io/badge/License-LGPL-blue)](LICENSE)
28
+
29
+ Hardware-oriented numerical quantizers for deep learning models, implemented in Keras v3 and NumPy. Provides bit-accurate precision matching with Vivado/Vitis HLS implementations.
30
+
31
+ ## Features
32
+
33
+ - Bit-accurate to the HLS implementation up to 32/64-bit floating point precision
34
+ - Support for fixed-point and minifloat number formats
35
+ - Differentiable Keras v3 implementations with gradients on inputs
36
+ - With surrogate gradients for bit-width optimization as described in *[Gradient-based Automatic Mixed Precision Quantization for Neural Networks On-Chip](https://arxiv.org/abs/2405.00645)*
37
+ - Supports stochastic rounding for training
38
+
39
+ ## Supported Quantizers
40
+
41
+ ### Fixed-Point Quantizer
42
+
43
+ Parameters:
44
+ - `k` (keep_negative): Enable negative numbers
45
+ - `i` (integer_bits): Number of bits before decimal point (excludes sign bit)
46
+ - `f` (fractional_bits): Number of bits after decimal point
47
+ - For C++: `W = k + i + f`, `I = k + i`, `S = k`
48
+
49
+ Supported modes:
50
+ - Rounding: `TRN`, `RND`, `RND_CONV`, `TRN_ZERO`, `RND_ZERO`, `RND_MIN_INF`, `RND_INF`
51
+ - `S_RND` and `S_RND_CONV` for stochastic rounding; Not available in NumPy implementation as it is for training only
52
+ - Overflow: `WRAP`, `SAT`, `SAT_SYM`, `WRAP_SM`
53
+
54
+ Limitations:
55
+ - `WRAP_SM` only works with `RND` or `RND_CONV` rounding
56
+ - `WRAP*` modes don't provide surrogate gradients for integer bits
57
+ - Saturation bit forced to zero for `WRAP` and `WRAP_SM`
58
+
59
+ ### Minifloat Quantizer
60
+
61
+ Parameters:
62
+ - `m` (mantissa_bits): Mantissa width
63
+ - `e` (exponent_bits): Exponent width
64
+ - `e0` (exponent_zero): Exponent bias (default: 0)
65
+ - Range: `[-2^(e-1) + e0, 2^(e-1) - 1 + e0]`
66
+
67
+ Features:
68
+ - Supports subnormal numbers
69
+ - Uses `RND_CONV` rounding and `SAT` overflow
70
+ - HLS-synthesizable implementation in `test/cpp_source/ap_types/ap_float.h`
71
+
72
+ ### Simplified Quantizers
73
+
74
+ - **Binary**: Maps to {-1,1} with 0 to -1. (preliminary implementation)
75
+ - **Ternary**: Shorthand for fixed-point `fixed<2, 1, RND_CONV, SAT_SYM>`
76
+
77
+
78
+ ## Installation
79
+
80
+ **requires python>=3.10**
81
+
82
+ ```bash
83
+ pip install quantizers
84
+ ```
85
+ `keras>=3.0` and at least one compatible backend (`pytorch`, `jax`, or `tensorflow`) is required for training.
86
+
87
+ ## Usage
88
+
89
+ ### Stateless Quantizers
90
+ ```python
91
+ from quantizers import (
92
+ float_quantize(_np), # add _np for NumPy implementation
93
+ get_fixed_quantizer(_np),
94
+ binary_quantize(_np),
95
+ ternary_quantize(_np),
96
+ )
97
+
98
+ # Fixed-point quantizer
99
+ fixed_quantizer = get_fixed_quantizer(round_mode, overflow_mode)
100
+ fixedp_qtensor = fixed_quantizer(
101
+ x,
102
+ integer_bits,
103
+ fractional_bits,
104
+ keep_negative,
105
+ training, # For stochastic rounding, and WRAP does not happen during training
106
+ seed, # For stochastic rounding only
107
+ )
108
+
109
+ # Minifloat quantizer
110
+ floatp_qtensor = float_quantize(x, mantissa_bits, exponent_bits, exponent_zero)
111
+
112
+ # Simplified quantizers
113
+ binary_qtensor = binary_quantize(x)
114
+ ternary_qtensor = ternary_quantize(x)
115
+ ```
116
+
117
+ ### Stateful Quantizers
118
+ ```python
119
+ # Can be used for, but not intended for training
120
+ fixed_q = FixedQ(
121
+ width,
122
+ integer_bits, # including the sign bit)
123
+ keep_negative,
124
+ fixed_round_mode, # No stochastic rounding
125
+ fixed_overflow_mode
126
+ )
127
+ quantized = fixed_q(x)
128
+
129
+ mfloat_q = MinifloatQ(mantissa_bits, exponent_bits, exponent_zero)
130
+ quantized = mfloat_q(x)
131
+ ```
@@ -0,0 +1,17 @@
1
+ quantizers/__init__.py,sha256=o6yFBm_3iSB2Lz2oRTQHYA5o7s49i5La7Ib66P0bxXU,145
2
+ quantizers/_version.py,sha256=DGJ4pj32xs3_DRJhSzQwCiRNnAQrMgo09USYpyMZsKc,411
3
+ quantizers/stateful_quantizers.py,sha256=B7VuDJnpMR7Pp9bPGoq_FGyx8ykLx9B-_6dDKLnRdhA,4007
4
+ quantizers/binary/__init__.py,sha256=tguWtPkpGhAVzAo28fdh1SGc7Bk3Ww33FBU0K829MHo,335
5
+ quantizers/binary/binary_ops.py,sha256=xYFHN-3aENv3swwn7YQIsW9xsLbXXbJXZuCXPYEV07o,687
6
+ quantizers/binary/binary_ops_np.py,sha256=GMjT9BUmkN0ykkLvQGvyh8k6Lj5x-IQRmy92qm0phvo,176
7
+ quantizers/fixed_point/__init__.py,sha256=X7zxaHzTG7uFfW0warVxef2WMvVYtiqNH-Lo3HlcPPo,244
8
+ quantizers/fixed_point/fixed_point_ops.py,sha256=QDsyQYCCuk-FK9nk51Sa1GOuLEidQ8LFwKanLK4a9Xo,8900
9
+ quantizers/fixed_point/fixed_point_ops_np.py,sha256=3oHsEB_R_20u6VrvoPH3Ahx6xf234Xs9hX2S4jQha9A,4748
10
+ quantizers/minifloat/__init__.py,sha256=6AZVKSRESofllv0joQR6KAs4w9Eay8f6UAkE89ieW5s,333
11
+ quantizers/minifloat/float_point_ops.py,sha256=xOrqoHtPCBb2dJnbiMX3mMyf2JsPUKXGs96JGJeIezg,2105
12
+ quantizers/minifloat/float_point_ops_np.py,sha256=e2t_cOjuzwd_JGKEmHZj-POY-SLiXOtPKbswzJfkndc,1522
13
+ quantizers-1.0.0.dist-info/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
14
+ quantizers-1.0.0.dist-info/METADATA,sha256=gg-4sGI-3RtD53Tr1APALE774MXO1BtKJSK07KxEZyA,4306
15
+ quantizers-1.0.0.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
16
+ quantizers-1.0.0.dist-info/top_level.txt,sha256=K9nZlz267J8tCfY-mMGqqb8umqII1BxWSIXgC4J-vn0,11
17
+ quantizers-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.5.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ quantizers