Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion example-models
45 changes: 42 additions & 3 deletions hls4ml/model/optimizer/passes/infer_precision.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from collections.abc import Iterable

import numpy as np
from fxpmath import Fxp

from hls4ml.model.optimizer import ConfigurableOptimizerPass
from hls4ml.model.types import (
Expand Down Expand Up @@ -573,9 +574,17 @@ def _infer_par_act_precision(self, node, types_to_infer):
# For threshold relu, set the parameter precision to be the input precision by default;
# for other parametrized activations, just allow the default precision to be used.
# Can override these values in the configuration by explicitly setting them.
if 'param_t' in types_to_infer and node.get_attr('activation').lower() == 'thresholdedrelu':
in_type = node.get_input_variable().type.precision
node.attributes['param_t'].precision = in_type
if 'param_t' in types_to_infer:
if node.get_attr('activation').lower() == 'thresholdedrelu':
# For threshold relu, set the parameter precision to be the input precision by default;
in_type = node.get_input_variable().type.precision
node.attributes['param_t'].precision = in_type
inferred_types.append('param_t')
else:
# find a constant to represent the values
param = node.get_attr('activ_param')
precision = _get_precision_from_constant(param)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand this correctly, we are basically hard-coding the bit width of the parameter to be 8 (9 if signed) and assign the fractional and integer bits based on the value. Is that correct? Because trying to find a way to infer the needed total precision has been something that has stumped me forever when working on the brevitas stuff, but it seems that here as well the only solution is to hardcode some arbitrary value.

Copy link
Contributor

@jmitrevs jmitrevs Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's mainly for those where the cases above don't apply. For 0 values we just use 1 bit. For power of 2 values we use a width of 1 or 2, depending on whether is negative or not. Then comes the attempt to use Fxp from fxpmath, which is logically like struct. It works well for values like 1.25 of things that can be represented exactly. In those cases, the optimizer uses the width from Fxp. But if that produces a width larger than 8 (not including the sign bit), then the size is capped at 8, with the appropriate range being set by the integer size. Note that Fxp would otherwise attempt to use 56 bits to store 1.1. These we cut off at 8 bits.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, thanks. I think I will then implement something similar to the non-power-of-2 cases for brevitas.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The optimizer can set negative integer bitwidths if it needs the precision for smaller values.

node.attributes['param_t'].precision = precision
inferred_types.append('param_t')

return inferred_types
Expand All @@ -594,3 +603,33 @@ def _infer_prelu_act_precision(self, node, types_to_infer):
inferred_types.append('param_t')

return inferred_types


def _get_precision_from_constant(value: int | float, max_width=8):
"""A utility function to find a fixed type to store the constant

Arguments:
value (int or float): the constant value
max_width (int, optional): the maximum fixed width (+ 1 if signed). Defaults to 8

Returns:
FixedPrecisionType: the type to use
"""
if value == 0:
return FixedPrecisionType(width=1, integer=1, signed=False)

signed = value < 0
absval = abs(value)
# check if power of 2
mantissa, exp = np.frexp(absval)
if mantissa == 0.5: # is it a power of 2?
# One could consider returning an ExponentPrecisionType here.
# Decided on FixedPrecisionType everywhere since ExponentPrecisionType is less supported
return FixedPrecisionType(1 + signed, exp, signed)

# now is the general case. First try Fxp
fxpval = Fxp(value, signed=signed)
if isinstance(fxpval.n_word, int) and fxpval.n_word <= max_width:
return FixedPrecisionType(fxpval.n_word, signed + fxpval.n_int, signed)

return FixedPrecisionType(signed + max_width, signed + exp, signed)
2 changes: 1 addition & 1 deletion hls4ml/model/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ def min(self):

@property
def max(self):
return 2.0 ** (self.integer - 1) - 2.0**-self.fractional
return 2.0 ** (self.integer - self.signed) - 2.0**-self.fractional


class XnorPrecisionType(PrecisionType):
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
]
dynamic = [ "version" ]
dependencies = [ "h5py", "numpy", "pydigitalwavetools==1.1", "pyyaml", "quantizers" ]
dependencies = [ "fxpmath", "h5py", "numpy", "pydigitalwavetools==1.1", "pyyaml", "quantizers" ]

optional-dependencies.da = [ "da4ml>=0.2.1,<=0.4" ]
optional-dependencies.doc = [
Expand Down
17 changes: 17 additions & 0 deletions test/pytest/test_auto_precision.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from tensorflow.keras.models import Sequential

import hls4ml
from hls4ml.model.optimizer.passes.infer_precision import _get_precision_from_constant

test_root_path = Path(__file__).parent

Expand Down Expand Up @@ -254,3 +255,19 @@ def test_auto_precision_dense(keras_model_dense, data_1d, io_type, backend):
y_keras = model.predict(data).flatten()
y_hls = hls_model.predict(data).flatten()
np.testing.assert_allclose(y_keras, y_hls, rtol=2e-2, atol=5e-2, verbose=True)


def test_precision_from_constant_unit():
"""unit test on for determining precision needed for a constant"""
testvalues = (0, -1024, 1024, 0.03125, -0.03125, 1.25, -1.25, 1.1, -1.1)
max_width = 8
bit_widths = (1, 2, 1, 1, 2, 3, 4, max_width, max_width + 1)

for val, w in zip(testvalues, bit_widths):
fp = _get_precision_from_constant(val, max_width)
assert fp.min <= val <= fp.max
assert fp.width == w
assert fp.signed == (val < 0)
quantum = 2.0**-fp.fractional
if w < max_width:
assert val % quantum == 0
Loading