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.
Files changed (52) hide show
  1. {quantizers-1.1.0/src/quantizers.egg-info → quantizers-1.2.0}/PKG-INFO +3 -2
  2. {quantizers-1.1.0 → quantizers-1.2.0}/pyproject.toml +2 -2
  3. {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/_version.py +9 -4
  4. quantizers-1.2.0/src/quantizers/fixed_point/_fixed_point_ops.py +215 -0
  5. {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/fixed_point/fixed_point_ops.py +3 -6
  6. {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/fixed_point/fixed_point_ops_np.py +2 -6
  7. {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/minifloat/float_point_ops.py +4 -1
  8. {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/stateful_quantizers.py +6 -0
  9. {quantizers-1.1.0 → quantizers-1.2.0/src/quantizers.egg-info}/PKG-INFO +3 -2
  10. {quantizers-1.1.0 → quantizers-1.2.0}/test/test_quantizer_match.py +26 -20
  11. {quantizers-1.1.0 → quantizers-1.2.0}/test/test_stateless_quantizer.py +56 -55
  12. quantizers-1.1.0/src/quantizers/fixed_point/_fixed_point_ops.py +0 -289
  13. {quantizers-1.1.0 → quantizers-1.2.0}/.clang-format +0 -0
  14. {quantizers-1.1.0 → quantizers-1.2.0}/.github/workflows/python-publish.yml +0 -0
  15. {quantizers-1.1.0 → quantizers-1.2.0}/.github/workflows/python-test.yml +0 -0
  16. {quantizers-1.1.0 → quantizers-1.2.0}/.gitignore +0 -0
  17. {quantizers-1.1.0 → quantizers-1.2.0}/.pre-commit-config.yaml +0 -0
  18. {quantizers-1.1.0 → quantizers-1.2.0}/LICENSE +0 -0
  19. {quantizers-1.1.0 → quantizers-1.2.0}/README.md +0 -0
  20. {quantizers-1.1.0 → quantizers-1.2.0}/setup.cfg +0 -0
  21. {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/__init__.py +0 -0
  22. {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/binary/__init__.py +0 -0
  23. {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/binary/_binary_ops.py +0 -0
  24. {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/binary/binary_ops.py +0 -0
  25. {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/binary/binary_ops_np.py +0 -0
  26. {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/fixed_point/__init__.py +0 -0
  27. {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/minifloat/__init__.py +0 -0
  28. {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/minifloat/_float_point_ops.py +0 -0
  29. {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers/minifloat/float_point_ops_np.py +0 -0
  30. {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers.egg-info/SOURCES.txt +0 -0
  31. {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers.egg-info/dependency_links.txt +0 -0
  32. {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers.egg-info/requires.txt +0 -0
  33. {quantizers-1.1.0 → quantizers-1.2.0}/src/quantizers.egg-info/top_level.txt +0 -0
  34. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_binary.h +0 -0
  35. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_common.h +0 -0
  36. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_decl.h +0 -0
  37. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_fixed.h +0 -0
  38. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_fixed_base.h +0 -0
  39. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_fixed_ref.h +0 -0
  40. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_fixed_special.h +0 -0
  41. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_float.h +0 -0
  42. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_int.h +0 -0
  43. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_int_base.h +0 -0
  44. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_int_ref.h +0 -0
  45. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_int_special.h +0 -0
  46. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_shift_reg.h +0 -0
  47. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/etc/ap_private.h +0 -0
  48. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/hls_math.h +0 -0
  49. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/hls_stream.h +0 -0
  50. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/test.cpp +0 -0
  51. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/ap_types/utils/x_hls_utils.h +0 -0
  52. {quantizers-1.1.0 → quantizers-1.2.0}/test/cpp_source/quantizers.h +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: quantizers
3
- Version: 1.1.0
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
  [![PyPI version](https://badge.fury.io/py/quantizers.svg)](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.1.0"
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 setuptools_scm
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, Union
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.1.0'
16
- __version_tuple__ = version_tuple = (1, 1, 0)
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 collections.abc import Callable
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
- from keras.api.random import SeedGenerator
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.get(round_mode, None)
139
- if round_fn_scaled is None:
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
- def float_quantize(x, m, e, e0=0.0):
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
1
+ Metadata-Version: 2.4
2
2
  Name: quantizers
3
- Version: 1.1.0
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
  [![PyPI version](https://badge.fury.io/py/quantizers.svg)](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.**-8
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('fixed_round_mode', ['TRN', 'TRN_ZERO', 'RND', 'S_RND', 'S_RND_CONV', 'RND_CONV', 'RND_ZERO', 'RND_INF', 'RND_MIN_INF'])
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'''Fixed quantizer has inconsistent behavior with C++ implementation:
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'''Float quantizer has inconsistent behavior with C++ implementation:
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'''Multiplication has inconsistent behavior with C++ implementation:
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 = np.array(q(np.array(data)))
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'''Binary quantizer has inconsistent behavior with C++ implementation:
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 = np.array(q(np.array(data)))
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'''Ternary quantizer has inconsistent behavior with C++ implementation:
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 binary_quantize, float_decompose, float_decompose_np, float_quantize, get_fixed_quantizer, ternary_quantize
9
- from quantizers.fixed_point._fixed_point_ops import round_mode_registry
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., 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.**-M
38
- _max = (2 - m_eps) * 2.**(2.**(E - 1) - 1 + E0)
39
- _min_pos_normal = 2.**(-2.**(E - 1) + E0 + 1)
40
- _min_pos_subnormal = m_eps * 2.**(-2.**(E - 1) + E0)
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.**(x * (log_overflow - log_normal) + log_normal) * sign
46
- x_subnormal = 2.**(x * (log_normal - log_subnormal) + log_subnormal - 1) * sign
47
- x_overflow = 2.**(x * 2 + log_overflow + 1) * sign
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('round_mode', ['TRN', 'TRN_ZERO', 'RND', 'S_RND', 'RND_CONV', 'S_RND_CONV', 'RND_ZERO', 'RND_INF', 'RND_MIN_INF'])
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_') and 'WRAP' not in overflow_mode:
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), f'X grad Error'
77
- assert jnp.all(di == 0), f'I grad Error'
78
- assert jnp.all(df < 0), f'F grad Error'
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), f'X grad Error'
82
- assert jnp.all(df <= 0), f'F grad Error'
83
- assert jnp.all(di <= 0), f'I grad Error'
84
- assert jnp.all((df < 0) | (di < 0) | (x == xq)), f'Grad Error for sat mode'
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.**i - 2.**f
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), f'Normal Number X grad Error'
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), f'Normal Number E grad Error'
113
- assert jnp.all(de0 == 0), f'Normal Number E0 grad Error'
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), f'Subnormal Number X grad Error'
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), f'Overflow Number X grad Error'
125
- assert jnp.all(dm == 0), f'Overflow Number M grad Error'
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_), f'Float Decompose Error @ Normal Number'
138
+ assert np.all(mm == mm_) and np.all(ee == ee_), 'Float Decompose Error @ Normal Number'
138
139
 
139
- xq_ = mm * 2.**ee # type: ignore
140
+ xq_ = mm * 2.0**ee # type: ignore
140
141
  xq = float_quantize(x_normal, M, E, E0)
141
- assert jnp.all(xq == xq_), f'Float Decompose Error @ Normal Number'
142
- assert jnp.all(jnp.abs(mm) < 2), f'Mantissa Error @ Normal Number' # type: ignore
143
- assert jnp.all(jnp.abs(mm) >= 1), f'Mantissa Error @ Normal Number' # type: ignore
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_), f'Float Decompose Error @ Subnormal Number'
148
+ assert np.all(mm == mm_) and np.all(ee == ee_), 'Float Decompose Error @ Subnormal Number'
148
149
 
149
- xq_ = mm * 2.**ee # type: ignore
150
+ xq_ = mm * 2.0**ee # type: ignore
150
151
  xq = float_quantize(x_subnormal, M, E, E0)
151
- assert jnp.all(xq == xq_), f'Float Decompose Error @ Subnormal Number'
152
- assert jnp.all(jnp.abs(mm) < 1), f'Mantissa Error @ Subnormal Number' # type: ignore
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_), f'Float Decompose Error'
157
+ assert np.all(mm == mm_) and np.all(ee == ee_), 'Float Decompose Error'
157
158
 
158
- xq_ = mm * 2.**ee # type: ignore
159
+ xq_ = mm * 2.0**ee # type: ignore
159
160
  xq = float_quantize(x_overflow, M, E, E0)
160
- assert jnp.all(xq == xq_), f'Float Decompose Error'
161
- assert jnp.all(jnp.abs(mm) < 2), f'Mantissa Error @ Overflow Number' # type: ignore
162
- assert jnp.all(jnp.abs(mm) >= 1), f'Mantissa Error @ Overflow Number' # type: ignore
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)), f'Binary Quantizer Error'
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), f'Gradient Error'
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)), f'Ternary Quantizer Error'
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), f'Gradient Error'
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