quantizers 1.0.1__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.0.1 → quantizers-1.2.0}/.pre-commit-config.yaml +5 -14
- {quantizers-1.0.1/src/quantizers.egg-info → quantizers-1.2.0}/PKG-INFO +3 -2
- {quantizers-1.0.1 → quantizers-1.2.0}/pyproject.toml +26 -1
- {quantizers-1.0.1 → quantizers-1.2.0}/src/quantizers/__init__.py +2 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/src/quantizers/_version.py +9 -4
- quantizers-1.2.0/src/quantizers/binary/__init__.py +4 -0
- quantizers-1.0.1/src/quantizers/binary/binary_ops.py → quantizers-1.2.0/src/quantizers/binary/_binary_ops.py +6 -7
- quantizers-1.2.0/src/quantizers/binary/binary_ops.py +10 -0
- quantizers-1.2.0/src/quantizers/binary/binary_ops_np.py +9 -0
- quantizers-1.2.0/src/quantizers/fixed_point/__init__.py +4 -0
- quantizers-1.2.0/src/quantizers/fixed_point/_fixed_point_ops.py +215 -0
- quantizers-1.2.0/src/quantizers/fixed_point/fixed_point_ops.py +19 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/src/quantizers/fixed_point/fixed_point_ops_np.py +18 -18
- quantizers-1.2.0/src/quantizers/minifloat/__init__.py +4 -0
- quantizers-1.0.1/src/quantizers/minifloat/float_point_ops.py → quantizers-1.2.0/src/quantizers/minifloat/_float_point_ops.py +17 -17
- quantizers-1.2.0/src/quantizers/minifloat/float_point_ops.py +15 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/src/quantizers/minifloat/float_point_ops_np.py +12 -12
- {quantizers-1.0.1 → quantizers-1.2.0}/src/quantizers/stateful_quantizers.py +14 -16
- {quantizers-1.0.1 → quantizers-1.2.0/src/quantizers.egg-info}/PKG-INFO +3 -2
- {quantizers-1.0.1 → quantizers-1.2.0}/src/quantizers.egg-info/SOURCES.txt +3 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/test_quantizer_match.py +35 -22
- {quantizers-1.0.1 → quantizers-1.2.0}/test/test_stateless_quantizer.py +56 -55
- quantizers-1.0.1/src/quantizers/binary/__init__.py +0 -9
- quantizers-1.0.1/src/quantizers/binary/binary_ops_np.py +0 -9
- quantizers-1.0.1/src/quantizers/fixed_point/__init__.py +0 -8
- quantizers-1.0.1/src/quantizers/fixed_point/fixed_point_ops.py +0 -279
- quantizers-1.0.1/src/quantizers/minifloat/__init__.py +0 -9
- {quantizers-1.0.1 → quantizers-1.2.0}/.clang-format +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/.github/workflows/python-publish.yml +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/.github/workflows/python-test.yml +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/.gitignore +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/LICENSE +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/README.md +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/setup.cfg +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/src/quantizers.egg-info/dependency_links.txt +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/src/quantizers.egg-info/requires.txt +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/src/quantizers.egg-info/top_level.txt +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_binary.h +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_common.h +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_decl.h +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_fixed.h +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_fixed_base.h +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_fixed_ref.h +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_fixed_special.h +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_float.h +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_int.h +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_int_base.h +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_int_ref.h +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_int_special.h +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/ap_types/ap_shift_reg.h +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/ap_types/etc/ap_private.h +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/ap_types/hls_math.h +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/ap_types/hls_stream.h +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/ap_types/test.cpp +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/ap_types/utils/x_hls_utils.h +0 -0
- {quantizers-1.0.1 → quantizers-1.2.0}/test/cpp_source/quantizers.h +0 -0
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
exclude: '\.svg$|ap_types/|_version.py'
|
|
2
2
|
|
|
3
3
|
repos:
|
|
4
|
-
- repo: https://github.com/
|
|
5
|
-
rev:
|
|
4
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
5
|
+
rev: v0.8.2
|
|
6
6
|
hooks:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
'--ignore=E731,E402',
|
|
11
|
-
'--in-place',
|
|
12
|
-
]
|
|
7
|
+
- id: ruff
|
|
8
|
+
args: [--fix]
|
|
9
|
+
- id: ruff-format
|
|
13
10
|
|
|
14
11
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
15
12
|
rev: v4.6.0
|
|
@@ -36,12 +33,6 @@ repos:
|
|
|
36
33
|
- id: pyupgrade
|
|
37
34
|
args: ["--py310-plus"]
|
|
38
35
|
|
|
39
|
-
- repo: https://github.com/PyCQA/isort
|
|
40
|
-
rev: 5.13.2
|
|
41
|
-
hooks:
|
|
42
|
-
- id: isort
|
|
43
|
-
args: ['--line-length=65535']
|
|
44
|
-
|
|
45
36
|
- repo: https://github.com/pre-commit/mirrors-clang-format
|
|
46
37
|
rev: v18.1.8
|
|
47
38
|
hooks:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: quantizers
|
|
3
|
-
Version: 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
|
[](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.0
|
|
8
|
+
version = "1.2.0"
|
|
9
9
|
authors = [{ name = "Chang Sun", email = "chsun@cern.ch" }]
|
|
10
10
|
description = ""
|
|
11
11
|
readme = "README.md"
|
|
@@ -70,3 +70,28 @@ addopts = "--import-mode=importlib --cov=src/quantizers --cov-report=xml --cov-r
|
|
|
70
70
|
|
|
71
71
|
[tool.coverage.run]
|
|
72
72
|
omit = ["*/_version.py"]
|
|
73
|
+
|
|
74
|
+
[tool.coverage.report]
|
|
75
|
+
exclude_lines = ['pragma: no cover', 'if TYPE_CHECKING:']
|
|
76
|
+
|
|
77
|
+
[tool.ruff]
|
|
78
|
+
include = ["src/**/*.py"]
|
|
79
|
+
|
|
80
|
+
line-length = 130
|
|
81
|
+
indent-width = 4
|
|
82
|
+
|
|
83
|
+
target-version = "py310"
|
|
84
|
+
|
|
85
|
+
[tool.ruff.format]
|
|
86
|
+
quote-style = "single"
|
|
87
|
+
indent-style = "space"
|
|
88
|
+
skip-magic-trailing-comma = false
|
|
89
|
+
docstring-code-format = true
|
|
90
|
+
docstring-code-line-length = 130
|
|
91
|
+
|
|
92
|
+
[tool.ruff.lint]
|
|
93
|
+
select = ["E", "W", "F", "I", "F401"]
|
|
94
|
+
ignore = ["E501", "F403", "F405", "E741"]
|
|
95
|
+
explicit-preview-rules = true
|
|
96
|
+
fixable = ["ALL"]
|
|
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.0
|
|
16
|
-
__version_tuple__ = version_tuple = (1,
|
|
20
|
+
__version__ = version = '1.2.0'
|
|
21
|
+
__version_tuple__ = version_tuple = (1, 2, 0)
|
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
from keras import ops
|
|
2
|
-
from keras.api.random import SeedGenerator
|
|
3
2
|
|
|
4
3
|
|
|
5
4
|
@ops.custom_gradient
|
|
6
|
-
def
|
|
7
|
-
r = ops.where(x > 0, 1
|
|
5
|
+
def _binary_quantize(x):
|
|
6
|
+
r = ops.where(x > 0, 1.0, -1.0)
|
|
8
7
|
|
|
9
8
|
def grad(*args, upstream=None):
|
|
10
9
|
if upstream is None:
|
|
11
10
|
(upstream,) = args
|
|
12
11
|
dy = upstream
|
|
13
|
-
return dy * (1. - ops.tanh(x)**2) # type: ignore
|
|
12
|
+
return dy * (1.0 - ops.tanh(x) ** 2) # type: ignore
|
|
14
13
|
|
|
15
14
|
return ops.stop_gradient(r), grad
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
@ops.custom_gradient
|
|
19
|
-
def
|
|
20
|
-
r = ops.where(x > 0.5, 1
|
|
18
|
+
def _ternary_quantize(x):
|
|
19
|
+
r = ops.where(x > 0.5, 1.0, ops.where(x < -0.5, -1.0, 0.0))
|
|
21
20
|
|
|
22
21
|
def grad(*args, upstream=None):
|
|
23
22
|
if upstream is None:
|
|
24
23
|
(upstream,) = args
|
|
25
24
|
dy = upstream
|
|
26
|
-
return dy * (1. - ops.tanh(x)**2) # type: ignore
|
|
25
|
+
return dy * (1.0 - ops.tanh(x) ** 2) # type: ignore
|
|
27
26
|
|
|
28
27
|
return ops.stop_gradient(r), grad
|
|
@@ -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
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
2
|
+
|
|
3
|
+
from numpy.typing import ArrayLike
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
pass
|
|
7
|
+
T = TypeVar('T', bound=ArrayLike)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_fixed_quantizer(round_mode: str = 'TRN', overflow_mode: str = 'WRAP'):
|
|
11
|
+
"""Get a stateless fixed-point quantizer given the round and overflow mode.
|
|
12
|
+
The quantizer is differentiable w.r.t. to the input and f, also i if using saturation overflow mode.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
round_mode: round mode, one of
|
|
16
|
+
"""
|
|
17
|
+
from ._fixed_point_ops import _get_fixed_quantizer
|
|
18
|
+
|
|
19
|
+
return _get_fixed_quantizer(round_mode, overflow_mode)
|
|
@@ -2,7 +2,6 @@ from collections.abc import Callable
|
|
|
2
2
|
from typing import Any, TypeVar
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
|
-
from numpy.typing import ArrayLike
|
|
6
5
|
|
|
7
6
|
round_mode_registry_np: dict[str, Callable[[Any, bool | None], Any]] = {}
|
|
8
7
|
round_mode_registry_scaled_np: dict[str, Callable[[Any, Any, bool | None], Any]] = {}
|
|
@@ -16,7 +15,7 @@ def rnd_mode_np(names: str | list[str] | tuple[str, ...]):
|
|
|
16
15
|
|
|
17
16
|
def inner(func):
|
|
18
17
|
def wrapper(x, f, training=None, seed_gen=None):
|
|
19
|
-
scale = 2
|
|
18
|
+
scale = 2.0**f
|
|
20
19
|
sx = x * scale
|
|
21
20
|
sxq = func(sx)
|
|
22
21
|
xq = sxq / scale
|
|
@@ -33,6 +32,7 @@ def rnd_mode_np(names: str | list[str] | tuple[str, ...]):
|
|
|
33
32
|
round_mode_registry_np[func.__name__.upper()] = func
|
|
34
33
|
|
|
35
34
|
return func
|
|
35
|
+
|
|
36
36
|
return inner
|
|
37
37
|
|
|
38
38
|
|
|
@@ -83,6 +83,7 @@ def sat_mode_np(name: str | list | tuple):
|
|
|
83
83
|
saturation_mode_registry_np[name] = func
|
|
84
84
|
saturation_mode_registry_np[func.__name__.upper()] = func
|
|
85
85
|
return func
|
|
86
|
+
|
|
86
87
|
return inner
|
|
87
88
|
|
|
88
89
|
|
|
@@ -90,14 +91,14 @@ def sat_mode_np(name: str | list | tuple):
|
|
|
90
91
|
def wrap(x, k, i, f, training=None):
|
|
91
92
|
xs = x
|
|
92
93
|
bk = i + k
|
|
93
|
-
bias = k * 2
|
|
94
|
-
return (
|
|
94
|
+
bias = k * 2.0 ** (bk - 1)
|
|
95
|
+
return (xs + bias) % (2.0**bk) - bias
|
|
95
96
|
|
|
96
97
|
|
|
97
98
|
@sat_mode_np('SAT')
|
|
98
99
|
def sat(x, k, i, f, training=None):
|
|
99
|
-
f_eps = 2
|
|
100
|
-
__max = 2
|
|
100
|
+
f_eps = 2.0 ** (-f)
|
|
101
|
+
__max = 2.0**i
|
|
101
102
|
_max = __max - f_eps
|
|
102
103
|
_min = -__max * k
|
|
103
104
|
return np.clip(x, _min, _max)
|
|
@@ -105,16 +106,16 @@ def sat(x, k, i, f, training=None):
|
|
|
105
106
|
|
|
106
107
|
@sat_mode_np('SAT_SYM')
|
|
107
108
|
def sat_sym(x, k, i, f, training=None):
|
|
108
|
-
f_eps = 2
|
|
109
|
-
_max = 2
|
|
109
|
+
f_eps = 2.0 ** (-f)
|
|
110
|
+
_max = 2.0**i - f_eps
|
|
110
111
|
_min = -_max * k
|
|
111
112
|
return np.clip(x, _min, _max)
|
|
112
113
|
|
|
113
114
|
|
|
114
115
|
@sat_mode_np('WRAP_SM')
|
|
115
116
|
def wrap_sm_fn(x, k, i, f, training=None, quant_fn: Callable = lambda x: x):
|
|
116
|
-
eps = 2
|
|
117
|
-
high = 2
|
|
117
|
+
eps = 2.0**-f
|
|
118
|
+
high = 2.0**i - eps
|
|
118
119
|
low = -(high + eps) * k
|
|
119
120
|
interval = 2 ** (i + k)
|
|
120
121
|
c1 = ((x) / interval) % 2 >= 1
|
|
@@ -134,24 +135,22 @@ def get_fixed_quantizer_np(round_mode: str = 'TRN', overflow_mode: str = 'WRAP')
|
|
|
134
135
|
overflow_mode = overflow_mode.upper()
|
|
135
136
|
round_mode = round_mode[2:] if round_mode.startswith('S_') else round_mode
|
|
136
137
|
|
|
137
|
-
round_fn_scaled = round_mode_registry_scaled_np
|
|
138
|
-
|
|
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
|
|
138
|
+
round_fn_scaled = round_mode_registry_scaled_np[round_mode]
|
|
139
|
+
sat_fn = saturation_mode_registry_np[overflow_mode]
|
|
143
140
|
|
|
144
141
|
if overflow_mode == 'WRAP_SM':
|
|
145
|
-
assert round_mode in ('RND', 'RND_CONV'),
|
|
142
|
+
assert round_mode in ('RND', 'RND_CONV'), 'WRAP_SM only supports RND and RND_CONV rounding modes in this implementation.'
|
|
146
143
|
|
|
147
144
|
def quantizer(x: T, k: T, i: T, f: T, training: bool | None = False, seed_gen: None = None) -> np.ndarray:
|
|
148
|
-
assert not training,
|
|
145
|
+
assert not training, 'Training mode not supported in numpy implementation.'
|
|
149
146
|
|
|
150
147
|
_i = np.maximum(i, -f)
|
|
151
148
|
|
|
152
149
|
if overflow_mode == 'WRAP_SM':
|
|
150
|
+
|
|
153
151
|
def rnd_fn_wrapped(x):
|
|
154
152
|
return round_fn_scaled(x, f, training)
|
|
153
|
+
|
|
155
154
|
return wrap_sm_fn(x, k, _i, f, training, rnd_fn_wrapped)
|
|
156
155
|
|
|
157
156
|
if overflow_mode != 'WRAP':
|
|
@@ -160,4 +159,5 @@ def get_fixed_quantizer_np(round_mode: str = 'TRN', overflow_mode: str = 'WRAP')
|
|
|
160
159
|
if overflow_mode == 'WRAP':
|
|
161
160
|
x = sat_fn(x, k, _i, f, training)
|
|
162
161
|
return x
|
|
162
|
+
|
|
163
163
|
return quantizer
|
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
import numpy as np
|
|
2
1
|
from keras import ops
|
|
3
2
|
|
|
4
3
|
|
|
5
4
|
@ops.custom_gradient
|
|
6
|
-
def
|
|
7
|
-
|
|
5
|
+
def _float_quantize(x, m, e, e0=0.0):
|
|
6
|
+
"""Quantize an array to floatlet (m mantissa bits, excl. sign bit, e exponent bits) format. Tentative gradient impl."""
|
|
8
7
|
m = m + 1
|
|
9
8
|
eps = 1e-7
|
|
10
9
|
|
|
11
10
|
e_req = ops.floor(ops.log2(ops.abs(x) + eps)) # type: ignore
|
|
12
|
-
_e_high = ops.maximum(2
|
|
11
|
+
_e_high = ops.maximum(2.0 ** (e - 1), 1.0)
|
|
13
12
|
e_low, e_high = -_e_high + e0 + 1, _e_high + e0 - 1 # type: ignore
|
|
14
13
|
e_act = ops.clip(e_req, e_low, e_high)
|
|
15
|
-
scale = 2
|
|
14
|
+
scale = 2.0 ** (e_act - m + 1)
|
|
16
15
|
sig = x / scale
|
|
17
16
|
qsig = ops.round(sig)
|
|
18
|
-
_sig_high = 2
|
|
17
|
+
_sig_high = 2.0**m
|
|
19
18
|
_sig_high = ops.where(e_high != e_act, _sig_high, _sig_high - 1)
|
|
20
19
|
clip_sig = ops.clip(qsig, -_sig_high, _sig_high) # type: ignore
|
|
21
20
|
qx = clip_sig * scale
|
|
@@ -26,31 +25,32 @@ def float_quantize(x, m, e, e0=0.):
|
|
|
26
25
|
dy = upstream
|
|
27
26
|
mask_e = e_req != e_act
|
|
28
27
|
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
|
|
28
|
+
dm = ops.where(mask_m, scale * (sig - qsig) * dy * ops.log(2.0), 0.0)
|
|
29
|
+
d_exp = ops.where(mask_e, (x - qx) * dy * ops.log(2.0), 0.0)
|
|
30
|
+
de = d_exp * ops.log(2.0) * _e_high # type: ignore
|
|
32
31
|
de0 = ops.where(e_req > e_high, d_exp, -d_exp) # type: ignore
|
|
33
32
|
return dy, dm, de, de0
|
|
34
33
|
|
|
35
34
|
return ops.stop_gradient(qx), grad
|
|
36
35
|
|
|
37
36
|
|
|
38
|
-
def
|
|
39
|
-
|
|
37
|
+
def _float_decompose(x, m, e, e0=0):
|
|
38
|
+
"""Quantize an array to floatlet (m mantissa bits, excl. sign bit, e exponent bits) format. Tentative gradient impl."""
|
|
39
|
+
|
|
40
40
|
m = m + 1
|
|
41
41
|
eps = 1e-7
|
|
42
42
|
|
|
43
43
|
e_req = ops.floor(ops.log2(ops.abs(x) + eps)) # type: ignore
|
|
44
|
-
_e_high = 2
|
|
44
|
+
_e_high = 2.0 ** (e - 1)
|
|
45
45
|
e_low, e_high = -_e_high + e0, _e_high + e0 - 1
|
|
46
46
|
e_act = ops.clip(e_req, e_low + 1, e_high)
|
|
47
|
-
scale = 2
|
|
47
|
+
scale = 2.0 ** (e_act - m + 1)
|
|
48
48
|
sig = x / scale
|
|
49
49
|
qsig = ops.round(sig)
|
|
50
|
-
cond = (e_act != e_high) & (ops.abs(qsig) == 2
|
|
50
|
+
cond = (e_act != e_high) & (ops.abs(qsig) == 2.0**m)
|
|
51
51
|
e_act = ops.where(cond, e_act + 1, e_act) # type: ignore
|
|
52
|
-
qsig = ops.where(cond, qsig / 2
|
|
53
|
-
r_mantissa_bound = 2 - 2
|
|
54
|
-
r_mantissa = ops.clip(qsig * 2
|
|
52
|
+
qsig = ops.where(cond, qsig / 2.0, qsig) # type: ignore
|
|
53
|
+
r_mantissa_bound = 2 - 2.0 ** (-m + 1)
|
|
54
|
+
r_mantissa = ops.clip(qsig * 2.0 ** (-m + 1), -r_mantissa_bound, r_mantissa_bound)
|
|
55
55
|
r_exponent = e_act
|
|
56
56
|
return r_mantissa, r_exponent
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def float_quantize(x, m, e, e0: Any = 0.0):
|
|
5
|
+
"""Quantize an array to floatlet (m mantissa bits, excl. sign bit, e exponent bits) format. Tentative gradient impl."""
|
|
6
|
+
from ._float_point_ops import _float_quantize
|
|
7
|
+
|
|
8
|
+
return _float_quantize(x, m, e, e0)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def float_decompose(x, m, e, e0=0):
|
|
12
|
+
"""Quantize an array to floatlet (m mantissa bits, excl. sign bit, e exponent bits) format. Tentative gradient impl."""
|
|
13
|
+
from ._float_point_ops import _float_decompose
|
|
14
|
+
|
|
15
|
+
return _float_decompose(x, m, e, e0)
|