Skip to content

Commit ab1c409

Browse files
authored
AI Engine (AIE) Backend support as external plugin (#1390)
* Add initial plugin support for external backends (e.g. aie4ml) * pre-commit fixes * remove unused JSON include * pre-commit fix * make plugin loader consistent with hls4ml * simplify plugin loader
1 parent ade67ab commit ab1c409

File tree

4 files changed

+161
-7
lines changed

4 files changed

+161
-7
lines changed

docs/advanced/plugins.rst

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
=======================================
2+
External Backend and Writer Plugins
3+
=======================================
4+
5+
``hls4ml`` can discover and load backend implementations from
6+
external Python packages. This enables specialised flows, such as the AMD AIE backend, to live in
7+
independent projects that version and iterate at their own cadence while reusing the core
8+
conversion infrastructure.
9+
10+
Discovery
11+
=========
12+
13+
Plugin packages advertise themselves through the ``hls4ml.backends`` Python entry point group. Each
14+
entry exposes a callable that receives ``register_backend`` and ``register_writer`` helpers and performs any setup that is
15+
required. ``hls4ml`` automatically scans for these entry points during ``hls4ml.backends`` import so
16+
third-party backends become available without additional user configuration.
17+
18+
In addition to entry points, modules listed in the ``HLS4ML_BACKEND_PLUGINS`` environment variable
19+
are imported and treated as registration callables. The variable accepts an ``os.pathsep`` separated
20+
list (``:`` on Linux/macOS or ``;`` on Windows):
21+
22+
.. code-block:: bash
23+
24+
export HLS4ML_BACKEND_PLUGINS=aie4ml.plugin:another_pkg.hls4ml_backend
25+
26+
Authoring a Plugin
27+
==================
28+
29+
A minimal plugin registers both a backend and an accompanying writer. The example below
30+
shows how the ``aie4ml`` package exposes its backend via ``pyproject.toml`` and a ``register``
31+
function:
32+
33+
.. code-block:: toml
34+
35+
[project.entry-points."hls4ml.backends"]
36+
AIE = "aie4ml.plugin:register"
37+
38+
.. code-block:: python
39+
40+
# aie4ml/plugin.py
41+
from aie4ml.aie_backend import AIEBackend
42+
from aie4ml.writer import AIEWriter
43+
44+
def register(*, register_backend, register_writer):
45+
register_writer('AIE', AIEWriter)
46+
register_backend('AIE', AIEBackend)
47+
48+
When the plugin is installed, ``hls4ml.backends.get_available_backends()`` will report the new
49+
backend just like the built-in FPGA toolflows.
50+
51+
Packaging Data Files
52+
====================
53+
54+
Backends often rely on firmware templates or device description files. These assets should be
55+
packaged alongside the Python sources using the usual ``setuptools`` mechanisms (``package-data`` or
56+
``include-package-data``) so they are available from the installed distribution.
57+
58+
For an end-to-end example see the companion ``aie4ml`` [https://github.com/dimdano/aie4ml] package that ships alongside this project
59+
as a standalone distribution; it encapsulates the existing AMD AIE backend as an installable plugin
60+
depending on ``hls4ml``.

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
advanced/extension
5454
advanced/model_optimization
5555
advanced/bramfactor
56+
advanced/plugins
5657

5758
.. toctree::
5859
:hidden:

hls4ml/backends/__init__.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from hls4ml.backends.backend import Backend, get_available_backends, get_backend, register_backend # noqa: F401
22
from hls4ml.backends.fpga.fpga_backend import FPGABackend # noqa: F401
33
from hls4ml.backends.oneapi.oneapi_backend import OneAPIBackend
4+
from hls4ml.backends.plugin_loader import load_backend_plugins
45
from hls4ml.backends.quartus.quartus_backend import QuartusBackend
56
from hls4ml.backends.symbolic.symbolic_backend import SymbolicExpressionBackend
67
from hls4ml.backends.vivado.vivado_backend import VivadoBackend
@@ -11,10 +12,16 @@
1112

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

14-
register_backend('Vivado', VivadoBackend)
15-
register_backend('VivadoAccelerator', VivadoAcceleratorBackend)
16-
register_backend('Vitis', VitisBackend)
17-
register_backend('Quartus', QuartusBackend)
18-
register_backend('Catapult', CatapultBackend)
19-
register_backend('SymbolicExpression', SymbolicExpressionBackend)
20-
register_backend('oneAPI', OneAPIBackend)
15+
16+
def _register_builtin_backends():
17+
register_backend('Vivado', VivadoBackend)
18+
register_backend('VivadoAccelerator', VivadoAcceleratorBackend)
19+
register_backend('Vitis', VitisBackend)
20+
register_backend('Quartus', QuartusBackend)
21+
register_backend('Catapult', CatapultBackend)
22+
register_backend('SymbolicExpression', SymbolicExpressionBackend)
23+
register_backend('oneAPI', OneAPIBackend)
24+
25+
26+
_register_builtin_backends()
27+
load_backend_plugins()

hls4ml/backends/plugin_loader.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Utilities for discovering and loading external hls4ml backend plugins."""
2+
3+
import os
4+
from collections.abc import Callable
5+
from importlib import import_module
6+
from importlib.metadata import entry_points
7+
from typing import Any
8+
9+
from hls4ml.backends.backend import register_backend
10+
from hls4ml.writer.writers import register_writer
11+
12+
ENTRY_POINT_GROUP = 'hls4ml.backends'
13+
ENV_PLUGIN_MODULES = 'HLS4ML_BACKEND_PLUGINS'
14+
15+
_plugins_loaded = False
16+
17+
18+
def load_backend_plugins() -> None:
19+
"""Discover and register backend plugins.
20+
21+
This function loads plugins published via Python entry points under the
22+
``hls4ml.backends`` group as well as modules listed in the
23+
``HLS4ML_BACKEND_PLUGINS`` environment variable. The environment variable
24+
accepts a separator compatible with :data:`os.pathsep`.
25+
"""
26+
global _plugins_loaded
27+
if _plugins_loaded:
28+
return
29+
30+
_load_entry_point_plugins()
31+
_load_env_plugins()
32+
33+
_plugins_loaded = True
34+
35+
36+
def _load_entry_point_plugins() -> None:
37+
group_eps = entry_points().select(group=ENTRY_POINT_GROUP)
38+
39+
for ep in group_eps:
40+
try:
41+
obj = ep.load()
42+
except Exception as exc:
43+
print(f'WARNING: failed to load backend plugin entry "{ep.name}": {exc}')
44+
continue
45+
_register_plugin_object(ep.name, obj)
46+
47+
48+
def _load_env_plugins() -> None:
49+
raw_modules = os.environ.get(ENV_PLUGIN_MODULES, '')
50+
if not raw_modules:
51+
return
52+
53+
for module_name in filter(None, raw_modules.split(os.pathsep)):
54+
try:
55+
module = import_module(module_name)
56+
except Exception as exc:
57+
print(f'WARNING: failed to import backend plugin module "{module_name}": {exc}')
58+
continue
59+
60+
register_callable: Any = getattr(module, 'register', module)
61+
_register_plugin_object(module_name, register_callable)
62+
63+
64+
def _register_plugin_object(name: str, obj: Any) -> None:
65+
"""Interpret the plugin object and register provided backends."""
66+
if callable(obj):
67+
_invoke_registration_callable(name, obj)
68+
return
69+
70+
print(f'WARNING: plugin entry "{name}" did not provide a usable backend registration (got {obj!r})')
71+
72+
73+
def _invoke_registration_callable(name: str, func: Callable[..., Any]) -> None:
74+
try:
75+
func(register_backend=register_backend, register_writer=register_writer)
76+
return
77+
except TypeError:
78+
try:
79+
func(register_backend, register_writer)
80+
return
81+
except Exception as exc:
82+
print(f'WARNING: backend plugin callable "{name}" failed: {exc}')
83+
return
84+
except Exception as exc:
85+
print(f'WARNING: backend plugin callable "{name}" failed: {exc}')
86+
return

0 commit comments

Comments
 (0)