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 Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -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'''
Expand Down
10 changes: 5 additions & 5 deletions docs/advanced/da.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/calad0i/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 <https://github.com/calad0i/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

Expand All @@ -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 <https://github.com/calad0i/da4ml>`__ or the `paper <https://arxiv.org/abs/2507.04535>`__.
For more details, please refer to the `alkaid repository <https://github.com/calad0i/alkaid>`__ or the `paper <https://arxiv.org/abs/2507.04535>`__.
61 changes: 30 additions & 31 deletions docs/intro/status.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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 <https://github.com/thesps/conifer>`__ package
* ``*`` indicates that the support is through symbolic tracing fallback with `Alkaid <https://github.com/calad0i/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.

Expand Down
85 changes: 58 additions & 27 deletions hls4ml/backends/vivado/passes/distributed_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here you are dropping the HWConfig argument. I see that the default in alkaid is HWConfig(1, 1, -1), so different from what was passed here before. Just checking if that is an intended change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is intended; the parameter is used for alkaid internal surrogate, and the config changed

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':
Expand Down Expand Up @@ -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
Expand All @@ -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}]);'
Expand All @@ -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':
Expand All @@ -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':
Expand All @@ -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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it would be good to add a test for the multi-input case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@calad0i can you comment why this case is not really reachable at the moment for the record? I'm fine with merging without the test, but would like to have it documented.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It can be reachable by any new layer implementation, like any transformer blocks. But those are already registered in hls4ml,and you need to patch to disable the current path to enable fallback. Or, some new layer is to be proposed and implemented.

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
4 changes: 2 additions & 2 deletions hls4ml/converters/keras_v3/hgq2/multi_head_attention.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
):
Expand Down Expand Up @@ -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'],
):
Expand Down
19 changes: 11 additions & 8 deletions hls4ml/converters/keras_v3_to_hls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -198,19 +198,22 @@ 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)
cls_name = layer.__class__.__name__
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]

Expand Down
Loading
Loading