Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
72aa3ea
Initialize CoyoteAccelerator Backend
bo3z Jul 25, 2025
896665b
CoyoteAccelerator backend hardware modules
bo3z Jul 25, 2025
4be45d1
Init Coyote submodule
bo3z Jul 25, 2025
ecde593
CoyoteAccelerator backend software modules
bo3z Jul 25, 2025
be28016
CoyoteAccelerator backend writer + build scripts
bo3z Jul 25, 2025
2c600a6
CoyoteAccelerator backend Python Overlay for neural network inference
bo3z Jul 25, 2025
3055a0b
Minor fixes and documentation updates
bo3z Jul 28, 2025
9554271
Remove unnecessary sleep when polling which reduces performance
bo3z Aug 1, 2025
e0714b7
Merge branch 'main' into coyote-accelerator
JanFSchulte Jan 21, 2026
d58bf73
FIFO Optimization fix
LordGash Feb 24, 2026
789e093
Update coyote_accelerator_writer.py
LordGash Feb 25, 2026
9a6b0fe
Update coyote_accelerator_backend.py
LordGash Feb 25, 2026
d35820f
Update FIFO depth optimization
LordGash Mar 2, 2026
d4a6a2f
Merge pull request #6 from LordGash/coyote
bo3z Mar 3, 2026
14c0ecf
CoyoteAccelerator: fix overlay floating dtype check
lorenzo-as Mar 12, 2026
f73f399
Merge pull request #8 from lorenzo-as/fix/coyote-overlay-dtype-check
bo3z Jul 5, 2026
77be539
Rename CoyoteAccelerator to Coyote & minor doc fixes
bo3z Mar 6, 2026
9630158
Address PR comments
bo3z Jul 5, 2026
657cef2
Merge branch 'main' into coyote-accelerator
bo3z Jul 5, 2026
fea86b0
Update submodule
bo3z Jul 5, 2026
770d3dc
Correctly parse HLS clock uncertainty
bo3z Jul 5, 2026
2dd6984
Fix SW compile errors for new version of Coyote
bo3z Jul 5, 2026
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
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@
[submodule "hls4ml/templates/catapult/ac_math"]
path = hls4ml/templates/catapult/ac_math
url = https://github.com/hlslibs/ac_math.git
[submodule "hls4ml/contrib/Coyote"]
path = hls4ml/contrib/Coyote
url = https://github.com/fpgasystems/Coyote.git
branch = integrations/hls4ml
70 changes: 70 additions & 0 deletions docs/backend/accelerator.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,73 @@ The ``predict`` method will send the input data to the PL and return the output

nn = NeuralNetworkOverlay('hls4ml_nn.bit', X_test.shape, y_test.shape)
y_hw, latency, throughput = nn.predict(X_test, profile=True)


=================
Coyote
=================

The **Coyote** backend of ``hls4ml`` leverages the `Coyote shell <https://github.com/fpgasystems/Coyote>`_ to easily deploy models on PCIe-attached Alveo FPGAs.
Coyote is an open-source, research shell that facilitates the deployment of applications on FPGAs, as well as the integration of FPGAs into larger computer systems.
Some of its features include:
- Multi-tenancy
- Virtualized memory
- Optimized data movement
- Dynamic reconfiguration
- Automatic work scheduling and memory striping
- Networking for distributed applications

The list of supported boards is available in the `Coyote documentation. <https://fpgasystems.github.io/Coyote/intro/quick-start.html>`_
The current Coyote backend can be used to deploy hls4ml models from both Python and C++. While the focus of the current backend is on the inference,
it can easily be extended to support dynamic reconfiguration of models, as well as distributed inference across multiple FPGAs.

CoyoteOverlay
================================

Similar to the VivadoAccelerator backend, the Coyote backend creates a custom **neural network overlay** that interacts with the FPGA.
This overlay can be used to provide inputs, run inference and retrieve the predictions. Additionally, the overlay provides a utility
functon to load the model bitstream and driver for some clusters. On others, the users need to manually load the bitstream and driver.
For guidance, see the `Coyote documentation. <https://fpgasystems.github.io/Coyote/intro/quick-start.html#deploying-coyote>`_.

.. note:: To use the Coyote backend, hls4ml must be cloned with submodules using ``git clone --recurse-submodules``.
Additionally, a full Vivado/Vitis installation is required to synthesize the hardware and compile the host software.

C++ binary
================================

Additionally, the Coyote backend generates and compiles a C++ program that can be used to run inference on the FPGA.
The binary can be found in ``<hls4ml-output-dir>/build/<project-name>_cyt_sw/bin/test`` and when launched, it will
run inference using the inputs from ``tb_data``. Similar to the Python overlay, the bitstream and driver must be loaded before running the inference.

Example
======================

Similar to the ``VivadoAccelerator``backend, we first generate a bitstream from a Keras model ``model`` and a config.
Comment thread
bo3z marked this conversation as resolved.

.. code-block:: Python

import hls4ml
config = hls4ml.utils.config_from_keras_model(model, granularity='name')
hls_model = hls4ml.converters.convert_from_keras_model(model,
hls_config=config,
output_dir='hls4ml_prj_coyote',
backend='Coyote',
board='u55c')
hls_model.build(bitfile=True)

After this command completes, the FPGA must be programmed with the bistream. Additionally, the Coyote driver must be loaded.
For some platforms, Coyote provides utility functions to load the bitstream and driver. For others, this can be achieved using
the Vivado hardware manager and Linux commands. More detail can be found in the `Coyote documentation. <https://fpgasystems.github.io/Coyote/intro/quick-start.html#deploying-coyote>`_.

Finally, we can create a ``CoyoteOverlay`` object, which can be used to run inference on the FPGA. Additionally, the overlay provides a utility
functon to load the model bitstream and driver for some clusters.
When running inference, we must provide the input tensor and the shape of the output tensor (to allocate the buffers for the data transfer).
Optionally, batch size can be specified..
The ``predict`` method will send the input data to the FPGA and return the output data ``y_hw``.

.. code-block:: Python

from hls4ml.backends.coyote import CoyoteOverlay

overlay = CoyoteOverlay('hls4ml_prj_coyote')
y_hw = overlay.predict(x, (1, ), BATCH_SIZE)
2 changes: 2 additions & 0 deletions hls4ml/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from hls4ml.backends.vitis.vitis_backend import VitisBackend # isort: skip

from hls4ml.backends.coyote.coyote_backend import CoyoteBackend

def _register_builtin_backends():
register_backend('Vivado', VivadoBackend)
Expand All @@ -22,6 +23,7 @@ def _register_builtin_backends():
register_backend('Catapult', CatapultBackend)
register_backend('SymbolicExpression', SymbolicExpressionBackend)
register_backend('oneAPI', OneAPIBackend)
register_backend('Coyote', CoyoteBackend)
register_backend('Libero', LiberoBackend)


Expand Down
1 change: 1 addition & 0 deletions hls4ml/backends/coyote/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from hls4ml.backends.coyote.coyote_overlay import CoyoteOverlay # noqa: F401
155 changes: 155 additions & 0 deletions hls4ml/backends/coyote/coyote_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import os
import subprocess
from hls4ml.model.flow import get_flow, register_flow
from hls4ml.backends import VitisBackend, VivadoBackend

class CoyoteBackend(VitisBackend):
"""
The Coyote backend, which deploys hls4ml models on a PCIe-attached Alveo FPGA
Underneath it uses the Coyote shell: https://github.com/fpgasystems/Coyote,
which offers high-performance data movement, networking capabilities, multi-tenancy,
partial reconfiguration etc. This backend has some similarities with the VitisAccelerator
backend, but the underlying platforms are different. The implementation of this backend
remains mostly simple, inheriting most of the functionality from the Vitis backend and
providing the necessary infrastructure to run model inference on Alveo boards.

Currently, this backend supports batched inference of a single model on hardware.
In the future, it can easily be extended with the following capabilities, leveraging
Coyote's features:
- Distributed inference
- Multiple parallel instances of hls4ml models (same or distinct models)
- Dynamic, run-time reconfiguration of models

Generic examples of Coyote can be found at the above-mentioned repository, under examples/
"""

def __init__(self):
super(VivadoBackend, self).__init__(name='Coyote')
self._register_layer_attributes()
self._register_flows()

def _register_flows(self):
writer_passes = ['make_stamp', 'coyote:write_hls']
self._writer_flow = register_flow('write', writer_passes, requires=['vitis:ip'], backend=self.name)

ip_flow_requirements = get_flow('vitis:ip').requires.copy()
self._default_flow = register_flow('ip', None, requires=ip_flow_requirements, backend=self.name)

###
# Register the fifo depth optimization flow which is different from the one for vivado
fifo_depth_opt_passes = [
'coyote:fifo_depth_optimization'
] + writer_passes # After optimization, a new project will be written

register_flow('fifo_depth_optimization', fifo_depth_opt_passes, requires=['vitis:ip'], backend=self.name)
###

def compile(self, model):
"""
Compiles the hls4ml model for software emulation

Args:
model (ModelGraph): hls4ml model to synthesize

Return:
lib_name (str): The name of the compiled library
"""
lib_name = None
ret_val = subprocess.run(
['./build_lib.sh'],
shell=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
cwd=model.config.get_output_dir(),
)
if ret_val.returncode != 0:
print(ret_val.stdout)
raise Exception(f'Failed to compile project "{model.config.get_project_name()}"')
lib_name = '{}/build/{}-{}.so'.format(
model.config.get_output_dir(), model.config.get_project_name(), model.config.get_config_value('Stamp')
)

return lib_name

def build(
self,
model,
device: str = 'u55c',
aclk_freq: float = 250,
reset: bool = False,
csim: bool = True,
synth: bool = True,
cosim: bool = False,
validation: bool = False,
csynth: bool = False,
bitfile: bool = False,
timing_opt: bool = False,
hls_clock_period: float = 4,
hls_clock_uncertainty: float = 27
):
"""
Synthesizes the hls4ml model bitstream as part of the Coyote shell
and compiles the host-side software to control the FPGA and run model inference

Args:
model (ModelGraph): hls4ml model to synthesize
device (str, optional): Target Alveo FPGA card; currently supported u55c, u280 and u250
aclk_freq (float, optional): System/shell clock frequency
reset (bool, optional): Reset HLS project, if a previous one is found
csim (bool, optional): Run C-Simulation of the HLS project
synth (bool, optional): Run HLS synthesis
cosim (bool, optional): Run HLS co-simulation
validation (bool, optional): Validate results between C-Sim and Co-Sim
csynth (bool, optional): Run Coyote synthesis using Vivado, which will synthesize the model in a vFPGA
bitfile (bool, optional): Generate Coyote bitstream
timing_opt (bool, optional): Run additional optimizations when running PnR during bitstream generation
hls_clock_period (float, optional): Clock period to be used for HLS synthesis
hls_clock_uncertainty (float, optional): Clock uncertainty to be used for HLS synthesis

NOTE: Currently, the hardware will synthesize with a default clock period of 4ns / 250 MHz frequency,
since this is the default frequency of Coyote (since the XDMA core defaults to 250 MHz). Coyote allows
one to specify a different clock period for the model and use a clock-domain crossing (CDC) between the
XDMA region and the model. This option is currently not exposed as part of the hls4ml backend, but advanced
users can easily set in the the CMake configuration of Coyote.

NOTE: While the hardware will synthesize at 250 MHz, users can optionally pass a different HLS clock period
This is primarily a work-around when HLS synthesize a kernel that doesn't meet timing during PnR.
The "trick" is to run HLS synthesis at a higher clock frequency then (or provide higher uncertainty)

TODO: Add functionality to parse synthesis reports
"""
hw_build_dir = f'{model.config.get_output_dir()}/build/{model.config.get_project_name()}_cyt_hw'
sw_build_dir = f'{model.config.get_output_dir()}/build/{model.config.get_project_name()}_cyt_sw'

# Synthesize hardware
cmake_cmd = (
f'cmake ../../ '
f'-DFLOW=hw '
f'-DFDEV_NAME={device} '
f'-DACLK_F={aclk_freq} '
f'-DBUILD_OPT={int(timing_opt)} '
f'-DEN_HLS_RESET={int(reset)} '
f'-DEN_HLS_CSIM={int(csim)} '
f'-DEN_HLS_SYNTH={int(synth)} '
f'-DEN_HLS_COSIM={int(cosim)} '
f'-DEN_HLS_VALIDATION={int(validation)} '
f'-DHLS_CLOCK_PERIOD={hls_clock_period} '
f'-DHLS_CLOCK_UNCERTAINTY={int(hls_clock_uncertainty)}'
)

os.makedirs(hw_build_dir, exist_ok=True)
subprocess.run(cmake_cmd, shell=True, cwd=hw_build_dir, check=True)

if bitfile:
subprocess.run('make project && make bitgen', shell=True, cwd=hw_build_dir, check=True)
elif csynth:
subprocess.run('make project && make synth', shell=True, cwd=hw_build_dir, check=True)
else:
subprocess.run('make project', shell=True, cwd=hw_build_dir, check=True)

# Compile host software
os.makedirs(sw_build_dir, exist_ok=True)
subprocess.run('cmake ../../ -DFLOW=sw', shell=True, cwd=sw_build_dir, check=True)
subprocess.run('make', shell=True, cwd=sw_build_dir, check=True)

105 changes: 105 additions & 0 deletions hls4ml/backends/coyote/coyote_overlay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import os
import time
import ctypes
import logging
import numpy as np

class CoyoteOverlay:
"""
CoyoteOverlay class, similar to NeuralNetworkOverlay for the VivadoAccelerator backend
This class can be used to run model inference on the FPGA with the Coyote backend
"""
def __init__(self, path: str, project_name: str = 'myproject'):
"""
Default constructor

Args:
path (str): Path to the hls4ml folder, as specified in convert_model(...)
project_name (str, optional): hls4ml model name, if different than myproject
"""

self.path = path
self.project_name = project_name

# Set up dynamic C library
self.coyote_lib = ctypes.cdll.LoadLibrary(
f'{self.path}/build/{self.project_name}_cyt_sw/libCoyoteInference.so'
)

self.coyote_lib.init_model_inference.argtypes = [ctypes.c_uint, ctypes.c_uint, ctypes.c_uint]
self.coyote_lib.init_model_inference.restype = ctypes.POINTER(ctypes.c_void_p)

self.coyote_lib.flush.argtypes = [ctypes.POINTER(ctypes.c_void_p)]
self.coyote_lib.predict.argtypes = [ctypes.POINTER(ctypes.c_void_p)]

self.coyote_lib.get_inference_predictions.argtypes = [ctypes.POINTER(ctypes.c_void_p), ctypes.c_uint]
self.coyote_lib.get_inference_predictions.restype = ctypes.POINTER(ctypes.c_float)

self.coyote_lib.free_model_inference.argtypes = [ctypes.POINTER(ctypes.c_void_p)]

def program_ethz_hacc_fpga(self):
"""
Utility function for loading the Coyote-hls4ml bitstream and driver
on the ETH Zurich Heteregenous Accelerate Compute Cluster (HACC)
On other clusters, users would need to manually load the bitstream and driver
Gudance on this is specified in Coyote docs.
"""
os.system(
f'cd {self.path}/Coyote/driver && '
f'make && '
f'cd ../util && '
f'bash program_hacc_local.sh ../../build/{self.project_name}_cyt_hw/bitstreams/cyt_top.bit ../driver/build/coyote_driver.ko'
)

def predict(self, X: np.array, y_shape: tuple, batch_size: int = 1, verbose: bool = True):
"""
Run model inference

Args:
X (np.array): Input data
y_shape (tuple): Shape of the output; used for allocating sufficient memory for the output
batch_size (int, optional): Inference batch size
"""
if len(X.shape) == 1:
X = np.array([X])
if not np.issubdtype(X.dtype, np.floating):
logging.warning('CoyoteOverlay only supports (for now) floating-point inputs; casting input data to float')

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.

Have we completely switched to logging module or we still use warnings. Do these two play along nicely?

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.

I am not sure.

I see three instances in the code:

  • In some cases the function warn(...) is used.
  • In some other cases, a normal print is used, e.g., print('WARNING:...')
  • Some code (mostly mine), uses logging.warning.

I've never had issues with logging.warning but happy to change as needed.

X = X.astype(np.float32)
y = np.empty((len(X), *y_shape))
np_pointer_nd = np.ctypeslib.ndpointer(dtype=np.float32, ndim=len(X[0].shape), flags='C')
self.coyote_lib.set_inference_data.argtypes = [ctypes.POINTER(ctypes.c_void_p), np_pointer_nd, ctypes.c_uint]

model = self.coyote_lib.init_model_inference(batch_size, int(np.prod(X[0].shape)), int(np.prod(y_shape)))

cnt = 0
avg_latency = 0
avg_throughput = 0
total_batches = 0
for x in X:
self.coyote_lib.set_inference_data(model, x, cnt)
cnt += 1
if cnt == batch_size:
self.coyote_lib.flush(model)

ts = time.time_ns()
self.coyote_lib.predict(model)
te = time.time_ns()

time_taken = te - ts
avg_latency += (time_taken / 1e3)
avg_throughput += (batch_size / (time_taken * 1e-9))

for j in range(batch_size):
tmp = self.coyote_lib.get_inference_predictions(model, j)
y[total_batches * batch_size + j] = np.ctypeslib.as_array(tmp, shape=y_shape)

cnt = 0
total_batches += 1

self.coyote_lib.free_model_inference(model)
if verbose:
print(f'Batch size: {batch_size}; batches processed: {total_batches}')
print(f'Mean latency: {round(avg_latency / total_batches, 3)}us (inference only)')
print(f'Mean throughput: {round(avg_throughput / total_batches, 1)} samples/s (inference only)')

return y
Empty file.
Loading
Loading