diff --git a/Jenkinsfile b/Jenkinsfile index a63eec6d98..f0e0929647 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -16,7 +16,7 @@ pipeline { sh '''#!/bin/bash --login conda activate hls4ml-py310 conda install -y jupyterhub pydot graphviz pytest pytest-cov - pip install pytest-randomly jupyter onnx>=1.4.0 matplotlib pandas seaborn pydigitalwavetools==1.1 pyyaml tensorflow==2.14 qonnx torch git+https://github.com/jmitrevs/qkeras.git@qrecurrent_unstack pyparsing quantizers da4ml + pip install pytest-randomly jupyter onnx>=1.4.0 matplotlib pandas seaborn pydigitalwavetools==1.1 pyyaml tensorflow==2.14 qonnx torch git+https://github.com/jmitrevs/qkeras.git@qrecurrent_unstack pyparsing quantizers alkaid pip install -U ../ --user ./convert-keras-models.sh -x -f keras-models.txt pip uninstall hls4ml -y''' diff --git a/docs/advanced/_static/da4ml-workflow.svg b/docs/advanced/_static/alkaid-workflow.svg similarity index 100% rename from docs/advanced/_static/da4ml-workflow.svg rename to docs/advanced/_static/alkaid-workflow.svg diff --git a/docs/advanced/da.rst b/docs/advanced/da.rst index a52f8a3996..fa8012ad21 100644 --- a/docs/advanced/da.rst +++ b/docs/advanced/da.rst @@ -4,16 +4,16 @@ Distributed Arithmetic .. image:: https://img.shields.io/badge/License-LGPLv3-blue.svg :target: https://www.gnu.org/licenses/lgpl-3.0.en.html -.. image:: https://badge.fury.io/py/da4ml.svg - :target: https://badge.fury.io/py/da4ml +.. image:: https://badge.fury.io/py/alkaid.svg + :target: https://badge.fury.io/py/alkaid .. image:: https://img.shields.io/badge/arXiv-2507.04535-b31b1b.svg :target: https://arxiv.org/abs/2507.04535 -Distributed Arithmetic (DA) is a strategy for constant-matrix-vector multiplication (CMVM) operations used in hls4ml. The implementation is provided by an external library, `da4ml `__, which can be installed with ``pip install hls4ml[da]``. The library transforms the CMVM operations into an adder graph with common subexpression elimations to reduce the overall complexity. As the CMVM operation is fully unrolled, `reuse_factor` **must** be 1 (by default) for the corresponding CMVM operations [*]_. Comparing to the traditional `Latency` strategy CMVM kernels, DA can usually reduce up to 30% of the LUTs and all DSPs used. +Distributed Arithmetic (DA) is a strategy for constant-matrix-vector multiplication (CMVM) operations used in hls4ml. The implementation is provided by an external library, `alkaid `__ (formally da4ml), which can be installed with ``pip install hls4ml[da]``. The library transforms the CMVM operations into an adder graph with common subexpression elimations to reduce the overall complexity. As the CMVM operation is fully unrolled, `reuse_factor` **must** be 1 (by default) for the corresponding CMVM operations [*]_. Comparing to the traditional `Latency` strategy CMVM kernels, DA can usually reduce up to 30% of the LUTs and all DSPs used. .. rst-class:: light -.. image:: _static/da4ml-workflow.svg +.. image:: _static/alkaid-workflow.svg :alt: Workflow of DA in hls4ml :width: 600 @@ -29,4 +29,4 @@ Currently, the DA strategy is only available for the Vivado/Vitis HLS backends. While possible, the RNN layers are not yet supported by the DA strategy. -For more details, please refer to the `da4ml repository `__ or the `paper `__. +For more details, please refer to the `alkaid repository `__ or the `paper `__. diff --git a/docs/intro/status.rst b/docs/intro/status.rst index d3942691ca..cd6f047870 100644 --- a/docs/intro/status.rst +++ b/docs/intro/status.rst @@ -54,37 +54,35 @@ A summary of the on-going status of the ``hls4ml`` tool is in the table below. .. table:: hls4ml Supported Features -+-----------------------+-----+-----+--------------+--------+--------+-----+ -| Frontend/Backend | MLP | CNN | RNN/LSTM/GRU | GarNet | Einsum | MHA | -+=======================+=====+=====+==============+========+========+=====+ -| Keras v2 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | -+-----------------------+-----+-----+--------------+--------+--------+-----+ -| QKeras | ✅ | ✅ | ✅ | ✅ | N/A | N/A | -+-----------------------+-----+-----+--------------+--------+--------+-----+ -| QKeras-v3 | ✅ | ✅ | ✅ | ✅ | ✅ | N/A | -+-----------------------+-----+-----+--------------+--------+--------+-----+ -| HGQ | ✅ | ✅ | N/A | N/A | N/A | N/A | -+-----------------------+-----+-----+--------------+--------+--------+-----+ -| Keras v3 | ✅ | ✅ | ✅ | N/A | ✅ | ❌ | -+-----------------------+-----+-----+--------------+--------+--------+-----+ -| HGQ2 | ✅ | ✅ | N/A | N/A | ✅ | ✅ | -+-----------------------+-----+-----+--------------+--------+--------+-----+ -| Torch | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | -+-----------------------+-----+-----+--------------+--------+--------+-----+ -| ONNX | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -+-----------------------+-----+-----+--------------+--------+--------+-----+ -| QONNX | ✅ | ✅ | ❌ | N/A | N/A | N/A | -+-----------------------+-----+-----+--------------+--------+--------+-----+ -| Vivado/Vitis HLS | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | -+-----------------------+-----+-----+--------------+--------+--------+-----+ -| Intel HLS | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | -+-----------------------+-----+-----+--------------+--------+--------+-----+ -| Catapult HLS | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | -+-----------------------+-----+-----+--------------+--------+--------+-----+ -| oneAPI (experimental) | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | -+-----------------------+-----+-----+--------------+--------+--------+-----+ -| XLS (experimental) | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -+-----------------------+-----+-----+--------------+--------+--------+-----+ ++-----------------------+-----+-----+-----------+--------+--------+--------+-----+ +| Frontend/Backend | MLP | CNN | Recurrent | LUT-NN | GarNet | Einsum | MHA | ++=======================+=====+=====+===========+========+========+========+=====+ +| Keras v2 | ✅ | ✅ | ✅ | N/A | ✅ | ❌ | ❌ | ++-----------------------+-----+-----+-----------+--------+--------+--------+-----+ +| QKeras | ✅ | ✅ | ✅ | N/A | ✅ | N/A | N/A | ++-----------------------+-----+-----+-----------+--------+--------+--------+-----+ +| QKeras-v3 | ✅ | ✅ | ✅ | N/A | ✅ | ✅ | N/A | ++-----------------------+-----+-----+-----------+--------+--------+--------+-----+ +| HGQ | ✅ | ✅ | N/A | N/A | N/A | N/A | N/A | ++-----------------------+-----+-----+-----------+--------+--------+--------+-----+ +| Keras v3 | ✅ | ✅ | ✅ | N/A | N/A | ✅ | ❌ | ++-----------------------+-----+-----+-----------+--------+--------+--------+-----+ +| HGQ2 | ✅ | ✅ | ✅* | ✅* | N/A | ✅ | ✅ | ++-----------------------+-----+-----+-----------+--------+--------+--------+-----+ +| Torch | ✅ | ✅ | ✅ | N/A | ❌ | ✅ | ❌ | ++-----------------------+-----+-----+-----------+--------+--------+--------+-----+ +| ONNX | ✅ | ✅ | ❌ | N/A | ❌ | ❌ | ❌ | ++-----------------------+-----+-----+-----------+--------+--------+--------+-----+ +| QONNX | ✅ | ✅ | ❌ | N/A | N/A | N/A | N/A | ++-----------------------+-----+-----+-----------+--------+--------+--------+-----+ +| Vivado/Vitis HLS | ✅ | ✅ | ✅ | ✅* | ❌ | ✅ | ✅ | ++-----------------------+-----+-----+-----------+--------+--------+--------+-----+ +| Intel HLS | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ++-----------------------+-----+-----+-----------+--------+--------+--------+-----+ +| Catapult HLS | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ++-----------------------+-----+-----+-----------+--------+--------+--------+-----+ +| oneAPI (experimental) | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ++-----------------------+-----+-----+-----------+--------+--------+--------+-----+ Other feature notes: @@ -99,6 +97,7 @@ Other feature notes: * ``hls4ml`` supports Linux [*]_ and requires python >=3.10. hls4ml does not require a specific Linux distribution version and we recommend following the requirements of the HLS tool you are using. * Windows and macOS are not supported. Setting up ``hls4ml`` on these platforms, for example using the Windows Subsystem for Linux (WSL), should be possible, but we do not provide support for such use cases. * BDT support has moved to the `Conifer `__ package +* ``*`` indicates that the support is through symbolic tracing fallback with `Alkaid `__, which is available only when using ``io_parallel`` mode and implements the layer fully unrolled. .. [*] For compiling the projects for simulation or actual HLS. Otherwise, the code **may** be used on other platforms and it will likely to work. However, please note that Windows or other platforms are **not supported** in general and are not tested. diff --git a/hls4ml/backends/vivado/passes/distributed_arithmetic.py b/hls4ml/backends/vivado/passes/distributed_arithmetic.py index d8672104de..0be4ad56d8 100644 --- a/hls4ml/backends/vivado/passes/distributed_arithmetic.py +++ b/hls4ml/backends/vivado/passes/distributed_arithmetic.py @@ -115,8 +115,9 @@ def match(self, node): @requires('da') def transform(self, model: 'ModelGraph', node: Layer): - from da4ml.codegen.hls import hls_logic_and_bridge_gen - from da4ml.trace import FixedVariableArray, HWConfig, comb_trace + from alkaid.cmvm import solver_options_t + from alkaid.codegen.hls import hls_logic_and_bridge_gen + from alkaid.trace import FVArray, trace kernel: np.ndarray = node.attributes['weight'].data kernel = kernel.reshape(-1, kernel.shape[-1]) @@ -125,23 +126,22 @@ def transform(self, model: 'ModelGraph', node: Layer): k, i, f = get_kernel_inp_kif(node) hard_dc = int(os.environ.get('DA_HARD_DC', 2)) - options = {'hard_dc': hard_dc, 'search_all_decompose_dc': True} - inp = FixedVariableArray.from_kif(k, i, f, HWConfig(1, -1, -1), solver_options=options) + options: solver_options_t = {'hard_dc': hard_dc, 'search_all_decompose_dc': True} + inp = FVArray.from_kif(k, i, f, solver_options=options) out = inp @ kernel if node.attributes['bias'] is not None: bias = node.attributes['bias'].data.ravel() assert len(bias) == n_out out += bias - sol = comb_trace(inp, out) - node.attributes['da_kernel_cost'] = sol.cost - backend = model.config.get_config_value('Backend').lower() assert backend in ('vitis', 'vivado') + comb = trace(inp, out) + node.attributes['da_kernel_cost'] = comb.cost flavor = 'vitis' pragmas = ['#pragma HLS INLINE'] if flavor == 'vitis' else None - fn_str, _ = hls_logic_and_bridge_gen(sol, fn_name, flavor, pragmas=pragmas, print_latency=True) + fn_str, _ = hls_logic_and_bridge_gen(comb, fn_name, flavor, pragmas=pragmas, print_latency=True) io_type = node.model.config.get_config_value('IOType') if io_type != 'io_parallel': @@ -361,8 +361,9 @@ def match(self, node): @requires('da') def transform(self, model: 'ModelGraph', node: Layer): - from da4ml.codegen.hls import hls_logic_and_bridge_gen - from da4ml.trace import FixedVariableArray, HWConfig, comb_trace + from alkaid.cmvm import solver_options_t + from alkaid.codegen.hls import hls_logic_and_bridge_gen + from alkaid.trace import FVArray, trace kernel: np.ndarray = node.attributes['weight'].data I, C, L_ker = kernel.shape @@ -382,15 +383,15 @@ def transform(self, model: 'ModelGraph', node: Layer): _k, _i, _f = (v[i] for v in inp_kifs) fn_name = f'einsum_{node.index}_da_{i}_of_{I}' hard_dc = int(os.environ.get('DA_HARD_DC', 2)) - options = {'hard_dc': hard_dc, 'search_all_decompose_dc': True} - inp = FixedVariableArray.from_kif(_k, _i, _f, HWConfig(1, -1, -1), solver_options=options) + options: solver_options_t = {'hard_dc': hard_dc, 'search_all_decompose_dc': True} + inp = FVArray.from_kif(_k, _i, _f, solver_options=options) out = inp @ kernel[i] - sol = comb_trace(inp, out) + comb = trace(inp, out) - node.attributes['da_kernel_cost'] += sol.cost + node.attributes['da_kernel_cost'] += comb.cost pragmas = ['#pragma HLS INLINE'] if flavor == 'vitis' else None - fn_str, _ = hls_logic_and_bridge_gen(sol, fn_name, flavor, pragmas=pragmas, print_latency=True) + fn_str, _ = hls_logic_and_bridge_gen(comb, fn_name, flavor, pragmas=pragmas, print_latency=True) fn_strs.append(fn_str) fn_call = f'{fn_name}(&inp_tpose[({i} * {L_data} + l0) * {C}], &out_tpose[({i} * {L_data} + l0) * {L_ker}]);' @@ -415,8 +416,9 @@ def match(self, node): return isinstance(node, DACombinational) def transform(self, model: 'ModelGraph', node: DACombinational): - from da4ml.codegen.hls import hls_logic_and_bridge_gen - from da4ml.trace import FixedVariableArrayInput, comb_trace + from alkaid.codegen.hls import hls_logic_and_bridge_gen + from alkaid.codegen.hls.hls_codegen import get_io_types + from alkaid.trace import FVArrayInput, trace io_type = model.config.get_config_value('IOType') if io_type != 'io_parallel': @@ -428,14 +430,15 @@ def transform(self, model: 'ModelGraph', node: DACombinational): B, I, s = inp_p.width, inp_p.integer, inp_p.signed i, f = I - s, B - I comb = node.attributes['da_comb_logic'] - inp = FixedVariableArrayInput(comb.shape[0]).quantize(s, i, f) + inp = FVArrayInput(comb.shape[0]).quantize(s, i, f) out = comb(inp) - comb = comb_trace(inp, out) + comb = trace(inp, out) node.attributes['da_comb_logic'] = comb comb = node.attributes['da_comb_logic'] - backend = model.config.get_config_value('Backend').lower() + node.attributes['da_comb_logic'] = comb + if backend in ('vitis', 'vivado'): flavor = 'vitis' elif backend == 'oneapi': @@ -444,16 +447,44 @@ def transform(self, model: 'ModelGraph', node: DACombinational): raise ValueError(f'Unsupported backend {backend} for DACombinational layer.') fn_name = f'da_comblogic_{node.index}' - comb_logic, _ = hls_logic_and_bridge_gen( - comb, fn_name, flavor=flavor, pragmas=['#pragma HLS INLINE'], print_latency=True - ) - namespace = model.config.get_writer_config().get('Namespace', None) or 'nnet' - inp_t: str = node.get_input_variable().type.name out_t: str = node.get_output_variable().type.name - inp_name: str = node.get_input_variable().name + inp_name: str = ', '.join(node.inputs) out_name: str = node.get_output_variable().name + namespace = model.config.get_writer_config().get('Namespace', None) or 'nnet' + + inp_names = node.inputs + inp_types = [model.graph[name].get_output_variable().type.name for name in inp_names] + inp_ts = ', '.join(inp_types) + + fn_name_internal = f'{fn_name}_internal' if len(inp_names) > 1 else fn_name + _name = fn_name_internal if len(inp_names) > 1 else fn_name + comb_logic, _ = hls_logic_and_bridge_gen( + comb, _name, flavor=flavor, pragmas=['#pragma HLS INLINE'], print_latency=True + ) - fn_cpp = f'{namespace}::{fn_name}<{inp_t}, {out_t}>({inp_name}, {out_name});' + # When there's multiple inputs, make a wrapper doing the concatenation and rename the original fn. + if len(inp_names) > 1: + inp_sizes = [prod(model.graph[name].get_output_variable().shape) for name in inp_names] + template_args = ', '.join(f'typename inp{i}_t' for i in range(len(inp_names))) + ', typename out_t' + fn_args = ', '.join(f'inp{i}_t inp{i}[{s}]' for i, s in enumerate(inp_sizes)) + f', out_t out[{comb.shape[1]}]' + _inp_t_str = get_io_types(comb, 'vitis')[0] + forloop = """for (size_t i = {n}; i < {m}; i++) {{ + inp_buf[i] = inp{i}[i-{n}];}} + #pragma HLS UNROLL""" + N = [0] + np.cumsum(inp_sizes).tolist() + forloops = '\n '.join(forloop.format(i=i, n=N[i], m=N[i + 1]) for i in range(len(inp_names))) + wrapper_fn = f"""template <{template_args}> +void {fn_name}({fn_args}) {{ + {_inp_t_str} inp_buf[{comb.shape[0]}]; + #pragma HLS INLINE + + {forloops} + + {fn_name_internal}<{_inp_t_str}, out_t>(inp_buf, out); + }}""" + comb_logic = comb_logic + '\n\n' + wrapper_fn + + fn_cpp = f'{namespace}::{fn_name}<{inp_ts}, {out_t}>({inp_name}, {out_name});' node.attributes['da_codegen'] = Source(comb_logic) node.attributes['function_cpp'] = fn_cpp diff --git a/hls4ml/converters/keras_v3/hgq2/multi_head_attention.py b/hls4ml/converters/keras_v3/hgq2/multi_head_attention.py index 09723f5336..e66a1d8579 100644 --- a/hls4ml/converters/keras_v3/hgq2/multi_head_attention.py +++ b/hls4ml/converters/keras_v3/hgq2/multi_head_attention.py @@ -18,7 +18,7 @@ class QMultiHeadAttentionHandler(QLayerHandler): def handle( self, - layer: 'hgq.layers.QMultiHeadAttention', + layer: 'hgq.layers.attn.mha.QMultiHeadAttention', in_tensors: Sequence['KerasTensor'], out_tensors: Sequence['KerasTensor'], ): @@ -131,7 +131,7 @@ class QLinformerAttentionHandler(QMultiHeadAttentionHandler): def handle( self, - layer: 'hgq.layers.linformer_attention.QLinformerAttention', + layer: 'hgq.layers.attn.linformer.QLinformerAttention', in_tensors: Sequence['KerasTensor'], out_tensors: Sequence['KerasTensor'], ): diff --git a/hls4ml/converters/keras_v3_to_hls.py b/hls4ml/converters/keras_v3_to_hls.py index 9abd031b56..376bc285e9 100644 --- a/hls4ml/converters/keras_v3_to_hls.py +++ b/hls4ml/converters/keras_v3_to_hls.py @@ -179,13 +179,13 @@ def fallback_handler( if self.allow_da_fallback: try: ret = self.da_call(layer, inp_tensors, out_tensors) - print(f'DA handler used for layer {layer.name}') + print(f'DA handler used for layer {layer.name} ({layer.__class__.__module__}.{layer.__class__.__name__}).') return ret except KeyError: pass # missing DA handler except ImportError: - print('da4ml not installed. Set `allow_da_fallback=False` to disable DA fallback.') - pass # da4ml not installed + print('alkaid not installed. Set `allow_da_fallback=False` to disable DA fallback.') + pass # alkaid not installed if self.allow_v2_fallback: ret = self.v2_call(layer, inp_tensors, out_tensors) if ret is not None: @@ -198,8 +198,8 @@ def da_call( self, layer: 'keras.layers.Layer', inp_tensors: Sequence['KerasTensor'], out_tensors: Sequence['KerasTensor'] ): import keras - from da4ml.converter import trace_model - from da4ml.trace import FixedVariableArrayInput, comb_trace + from alkaid.converter import trace_model + from alkaid.trace import FVArrayInput, trace if len(out_tensors) > 1: n_out = len(out_tensors) @@ -207,10 +207,13 @@ def da_call( raise ValueError(f'DA combinational requires n_out=1, got {n_out=} for layer {layer.name} ({cls_name}).') input_shapes: list[list[int]] = [list(t.shape[1:]) for t in inp_tensors] # type: ignore - inp = tuple(FixedVariableArrayInput(tuple(shape)).quantize(1, 32, 32) for shape in input_shapes) _model = keras.Model(inp_tensors, out_tensors) - inp, out = trace_model(_model, inputs=inp) - comb = comb_trace(inp, out) + try: + inp, out = trace_model(_model) # When input bw can be determined automatically + except (AssertionError, ValueError): + inp = tuple(FVArrayInput(tuple(shape)).quantize(1, 32, 32) for shape in input_shapes) + inp, out = trace_model(_model, inputs=inp) + comb = trace(inp, out) input_names = [t.name for t in inp_tensors] output_names = [t.name for t in out_tensors] diff --git a/hls4ml/model/optimizer/passes/bit_exact.py b/hls4ml/model/optimizer/passes/bit_exact.py index e073bd6fa2..cb4251383f 100644 --- a/hls4ml/model/optimizer/passes/bit_exact.py +++ b/hls4ml/model/optimizer/passes/bit_exact.py @@ -263,9 +263,17 @@ def to_in(a): @_request_kif.register def _(layer: DACombinational): - comb = layer.attributes['da_comb_trace'] + comb = layer.attributes['da_comb_logic'] k, i, f = comb.inp_kifs - return k.astype(np.int16), i.astype(np.int16), f.astype(np.int16) + inp_shapes = get_input_shapes(layer) + kk, ii, ff = [], [], [] + bias = 0 + for shape in inp_shapes: + size = prod(shape) + kk.append(k[bias : bias + size].reshape(shape).astype(np.int16)) + ii.append(i[bias : bias + size].reshape(shape).astype(np.int16)) + ff.append(f[bias : bias + size].reshape(shape).astype(np.int16)) + return tuple(zip(kk, ii, ff)) def requested_kif(layer: Layer) -> KIF_t: @@ -738,14 +746,17 @@ def _(layer: UnaryLUT): @_produce_kif.register def _(layer: DACombinational): - from da4ml.trace import FixedVariableArray, comb_trace + from alkaid.trace import FVArray, trace - k_in, i_in, f_in = get_input_kifs(layer)[0] - inp = FixedVariableArray.from_kif(k_in, i_in, f_in) - out = layer.attributes['da_comb_logic'](inp) - comb = comb_trace(inp, out) + kifs = [np.array(kif).reshape(3, -1) for kif in get_input_kifs(layer)] + kif = np.concatenate(kifs, axis=1) + inp = FVArray.from_kif(*kif) + out = layer.attributes['da_comb_logic'](inp.ravel()) + comb = trace(inp, out) k, i, f = comb.out_kifs - return k.astype(np.int16), i.astype(np.int16), f.astype(np.int16) + shape = get_output_shape(layer) + + return (k.astype(np.int16).reshape(shape), i.astype(np.int16).reshape(shape), f.astype(np.int16).reshape(shape)) @_produce_kif.register diff --git a/pyproject.toml b/pyproject.toml index d6e06e75e0..96414258f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ ] dynamic = [ "version" ] dependencies = [ "h5py", "numpy", "pydigitalwavetools==1.1", "pyyaml", "quantizers" ] -optional-dependencies.da = [ "da4ml>=0.5.2,<0.6" ] +optional-dependencies.da = [ "alkaid>=0.8.1,<1" ] optional-dependencies.doc = [ "sphinx", "sphinx-contributors", @@ -35,7 +35,7 @@ optional-dependencies.doc = [ "sphinx-tabs", ] optional-dependencies.hgq = [ "hgq>=0.2.3" ] -optional-dependencies.hgq2 = [ "hgq2>=0.1.7" ] +optional-dependencies.hgq2 = [ "hgq2>=0.2" ] optional-dependencies.keras-v3 = [ "keras>=3.10" ] optional-dependencies.onnx = [ "onnx>=1.4" ] optional-dependencies.optimization = [ @@ -71,8 +71,8 @@ optional-dependencies.testing-keras2 = [ "tensorflow>=2.8,<=2.14.1", ] optional-dependencies.testing-keras3 = [ - "da4ml", - "hgq2>=0.1.7", + "alkaid>=0.8.1,<1", + "hgq2>=0.2", "keras>=3.10", "pquant-ml", "sparsepixels>=0.3", diff --git a/test/pytest/generate_ci_yaml.py b/test/pytest/generate_ci_yaml.py index 91c1c730a0..ca76ed786a 100644 --- a/test/pytest/generate_ci_yaml.py +++ b/test/pytest/generate_ci_yaml.py @@ -38,6 +38,7 @@ KERAS3_LIST = { 'test_keras_v3_api', 'test_hgq2_mha', + 'test_da_custom_layer', 'test_einsum_dense', 'test_qeinsum', 'test_multiout_onnx', diff --git a/test/pytest/test_da_custom_layer.py b/test/pytest/test_da_custom_layer.py new file mode 100644 index 0000000000..4ba97c9b3b --- /dev/null +++ b/test/pytest/test_da_custom_layer.py @@ -0,0 +1,44 @@ +from pathlib import Path + +import keras +import numpy as np +import pytest +from hgq.config import QuantizerConfigScope +from hgq.layers import QGRU, QDenseT + +from hls4ml.converters import convert_from_keras_model + +test_root_path = Path(__file__).parent + + +@pytest.mark.parametrize('return_sequences', [True, False]) +def test_qgru(return_sequences, test_case_id): + with QuantizerConfigScope(b0=4, i0=2): + inp = keras.Input(shape=(8, 4)) + out = QGRU(4, return_sequences=return_sequences, return_state=False)(inp) + model = keras.Model(inputs=inp, outputs=out) + + model_hls = convert_from_keras_model(model, output_dir=str(test_root_path / test_case_id), backend='Vitis') + model_hls.compile() + + data_in = np.random.rand(1000, 8, 4).astype(np.float32) * 32 - 16 + data_out = model.predict(data_in, batch_size=1000) + data_out_hls = model_hls.predict(data_in).reshape(data_out.shape) # type: ignore + + np.testing.assert_equal(data_out, data_out_hls) + + +def test_qdense_t(test_case_id): + with QuantizerConfigScope(b0=4, i0=2): + inp = keras.Input(shape=(8,)) + out = QDenseT(4)(inp) + model = keras.Model(inputs=inp, outputs=out) + + model_hls = convert_from_keras_model(model, output_dir=str(test_root_path / test_case_id), backend='Vitis') + model_hls.compile() + + data_in = np.random.rand(1000, 8).astype(np.float32) * 32 - 16 + data_out = model.predict(data_in, batch_size=1000) + data_out_hls = model_hls.predict(data_in) + + np.testing.assert_equal(data_out, data_out_hls) diff --git a/test/pytest/test_hgq2_mha.py b/test/pytest/test_hgq2_mha.py index 6d8b72f4da..380c345278 100644 --- a/test/pytest/test_hgq2_mha.py +++ b/test/pytest/test_hgq2_mha.py @@ -12,9 +12,6 @@ from hls4ml.converters import convert_from_keras_model -# Current hgq2 release rejects the parallelization_factor kwarg that hls4ml passes; skip until supported. -pytest.skip('Skip until hgq2 supports parallelization_factor in QEinsumDense', allow_module_level=True) - test_path = Path(__file__).parent