From 84a07efcdf499d1d3115c72cbf1e01773f24e797 Mon Sep 17 00:00:00 2001 From: dimdano Date: Thu, 25 Sep 2025 13:54:49 +0200 Subject: [PATCH 1/4] Add initial plugin support for external backends (e.g. aie4ml) --- MANIFEST.in | 1 + README.md | 3 + docs/advanced/plugins.rst | 61 +++++++++++++++ docs/index.rst | 1 + hls4ml/backends/__init__.py | 21 ++++-- hls4ml/backends/plugin_loader.py | 124 +++++++++++++++++++++++++++++++ 6 files changed, 204 insertions(+), 7 deletions(-) create mode 100644 docs/advanced/plugins.rst create mode 100644 hls4ml/backends/plugin_loader.py diff --git a/MANIFEST.in b/MANIFEST.in index e3ee5ded3c..4a6bc93b2f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,6 +4,7 @@ graft test graft contrib recursive-include hls4ml/templates * recursive-include hls4ml *.py +recursive-include hls4ml/backends *.json recursive-include hls4ml/contrib * global-exclude .git .gitmodules .gitlab-ci.yml *.pyc include hls4ml/backends/vivado_accelerator/supported_boards.json diff --git a/README.md b/README.md index a6f4a080cb..162f1a9236 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,9 @@ Detailed tutorials on how to use `hls4ml`'s various functionalities can be found pip install hls4ml ``` +Specialised backends are now distributed as optional plugins. Install them alongside hls4ml via +``pip install ``, for example ``pip install aie4ml`` for the AMD AIE flow. + To install the extra dependencies for profiling: ```bash diff --git a/docs/advanced/plugins.rst b/docs/advanced/plugins.rst new file mode 100644 index 0000000000..e1949b8f9d --- /dev/null +++ b/docs/advanced/plugins.rst @@ -0,0 +1,61 @@ +======================================= +External Backend and Writer Plugins +======================================= + +Starting with this release ``hls4ml`` can discover and load backend implementations from +external Python packages. This enables specialised flows—such as the AMD AIE backend—to live in +independent projects that version and iterate at their own cadence while reusing the core +conversion infrastructure. + +Discovery +========= + +Plugin packages advertise themselves through the ``hls4ml.backends`` Python entry point group. Each +entry either exposes a subclass of :class:`hls4ml.backends.backend.Backend` or a callable that +receives ``register_backend`` and ``register_writer`` helpers and performs any setup that is +required. ``hls4ml`` automatically scans for these entry points during ``hls4ml.backends`` import so +third-party backends become available without additional user configuration. + +In addition to entry points, modules listed in the ``HLS4ML_BACKEND_PLUGINS`` environment variable +are imported and treated as registration callables. The variable accepts an ``os.pathsep`` separated +list (``:`` on Linux/macOS or ``;`` on Windows): + +.. code-block:: bash + + export HLS4ML_BACKEND_PLUGINS=aie4ml.plugin:another_pkg.hls4ml_backend + +Authoring a Plugin +================== + +A minimal plugin registers both a backend and an accompanying writer. The example below +shows how the ``aie4ml`` package exposes its backend via ``pyproject.toml`` and a ``register`` +function: + +.. code-block:: toml + + [project.entry-points."hls4ml.backends"] + AIE = "aie4ml.plugin:register" + +.. code-block:: python + + # aie4ml/plugin.py + from aie4ml.aie_backend import AIEBackend + from aie4ml.writer import AIEWriter + + def register(*, register_backend, register_writer): + register_writer('AIE', AIEWriter) + register_backend('AIE', AIEBackend) + +When the plugin is installed, ``hls4ml.backends.get_available_backends()`` will report the new +backend just like the built-in FPGA toolflows. + +Packaging Data Files +==================== + +Backends often rely on firmware templates or device description files. These assets should be +packaged alongside the Python sources using the usual ``setuptools`` mechanisms (``package-data`` or +``include-package-data``) so they are available from the installed distribution. + +For an end-to-end example see the companion ``aie4ml`` [https://github.com/dimdano/aie4ml] package that ships alongside this project +as a standalone distribution; it encapsulates the existing AMD AIE backend as an installable plugin +depending on ``hls4ml``. diff --git a/docs/index.rst b/docs/index.rst index ed617a4537..f170ca6858 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -53,6 +53,7 @@ advanced/extension advanced/model_optimization advanced/bramfactor + advanced/plugins .. toctree:: :hidden: diff --git a/hls4ml/backends/__init__.py b/hls4ml/backends/__init__.py index 4a48f072cd..7b31770c84 100644 --- a/hls4ml/backends/__init__.py +++ b/hls4ml/backends/__init__.py @@ -10,11 +10,18 @@ from hls4ml.backends.catapult.catapult_backend import CatapultBackend # isort: skip from hls4ml.backends.vitis.vitis_backend import VitisBackend # isort: skip +from hls4ml.backends.plugin_loader import load_backend_plugins -register_backend('Vivado', VivadoBackend) -register_backend('VivadoAccelerator', VivadoAcceleratorBackend) -register_backend('Vitis', VitisBackend) -register_backend('Quartus', QuartusBackend) -register_backend('Catapult', CatapultBackend) -register_backend('SymbolicExpression', SymbolicExpressionBackend) -register_backend('oneAPI', OneAPIBackend) + +def _register_builtin_backends(): + register_backend('Vivado', VivadoBackend) + register_backend('VivadoAccelerator', VivadoAcceleratorBackend) + register_backend('Vitis', VitisBackend) + register_backend('Quartus', QuartusBackend) + register_backend('Catapult', CatapultBackend) + register_backend('SymbolicExpression', SymbolicExpressionBackend) + register_backend('oneAPI', OneAPIBackend) + + +_register_builtin_backends() +load_backend_plugins() diff --git a/hls4ml/backends/plugin_loader.py b/hls4ml/backends/plugin_loader.py new file mode 100644 index 0000000000..05edb3ef40 --- /dev/null +++ b/hls4ml/backends/plugin_loader.py @@ -0,0 +1,124 @@ +"""Utilities for discovering and loading external hls4ml backend plugins.""" + +from __future__ import annotations + +import inspect +import logging +import os +from collections.abc import Iterable +from importlib import import_module +from typing import Any, Callable + +try: # pragma: no cover - fall back for older Python versions + from importlib.metadata import entry_points +except ImportError: # pragma: no cover + from importlib_metadata import entry_points # type: ignore + +from hls4ml.backends.backend import Backend, register_backend +from hls4ml.writer.writers import register_writer + +ENTRY_POINT_GROUP = 'hls4ml.backends' +ENV_PLUGIN_MODULES = 'HLS4ML_BACKEND_PLUGINS' + +_plugins_loaded = False + + +def load_backend_plugins(logger: logging.Logger | None = None) -> None: + """Discover and register backend plugins. + + This function loads plugins published via Python entry points under the + ``hls4ml.backends`` group as well as modules listed in the + ``HLS4ML_BACKEND_PLUGINS`` environment variable. The environment variable + accepts a separator compatible with :data:`os.pathsep`. + + Args: + logger (logging.Logger, optional): Optional logger used for diagnostics. + When omitted, a module-local logger will be used. + """ + + global _plugins_loaded + if _plugins_loaded: + return + + logger = logger or logging.getLogger(__name__) + + _load_entry_point_plugins(logger) + _load_env_plugins(logger) + + _plugins_loaded = True + + +def _load_entry_point_plugins(logger: logging.Logger) -> None: + eps = entry_points() + + if hasattr(eps, 'select'): + group_eps = eps.select(group=ENTRY_POINT_GROUP) + else: # pragma: no cover - legacy importlib_metadata API + group_eps = eps.get(ENTRY_POINT_GROUP, []) + + for ep in group_eps: + try: + obj = ep.load() + except Exception as exc: # pragma: no cover - defensive + logger.warning('Failed to load backend plugin entry %s: %s', ep.name, exc, exc_info=logger.isEnabledFor(logging.DEBUG)) + continue + _register_plugin_object(ep.name, obj, logger) + + +def _load_env_plugins(logger: logging.Logger) -> None: + raw_modules = os.environ.get(ENV_PLUGIN_MODULES, '') + if not raw_modules: + return + + for module_name in filter(None, raw_modules.split(os.pathsep)): + try: + module = import_module(module_name) + except Exception as exc: # pragma: no cover - defensive + logger.warning('Failed to import backend plugin module %s: %s', module_name, exc, exc_info=logger.isEnabledFor(logging.DEBUG)) + continue + + register_callable: Any = getattr(module, 'register', module) + _register_plugin_object(module_name, register_callable, logger) + + +def _register_plugin_object(name: str, obj: Any, logger: logging.Logger) -> None: + """Interpret the plugin object and register provided backends.""" + + if inspect.isclass(obj) and issubclass(obj, Backend): + _safe_register_backend(name, obj, logger) + return + + if isinstance(obj, Iterable) and not isinstance(obj, (str, bytes)): + for item in obj: + _register_plugin_object(name, item, logger) + return + + if callable(obj): + _invoke_registration_callable(name, obj, logger) + return + + logger.warning('Plugin entry %s did not provide a usable backend registration (got %r)', name, obj) + + +def _invoke_registration_callable(name: str, func: Callable[..., Any], logger: logging.Logger) -> None: + try: + func(register_backend=register_backend, register_writer=register_writer) + except TypeError: + try: + func(register_backend, register_writer) + except Exception as exc: # pragma: no cover - defensive + logger.warning('Backend plugin callable %s failed: %s', name, exc, exc_info=logger.isEnabledFor(logging.DEBUG)) + else: + return + except Exception as exc: # pragma: no cover - defensive + logger.warning('Backend plugin callable %s failed: %s', name, exc, exc_info=logger.isEnabledFor(logging.DEBUG)) + return + else: + return + + +def _safe_register_backend(name: str, backend_cls: type[Backend], logger: logging.Logger) -> None: + try: + register_backend(name, backend_cls) + except Exception as exc: # pragma: no cover - defensive + logger.warning('Failed to register backend %s from plugin: %s', name, exc, exc_info=logger.isEnabledFor(logging.DEBUG)) From 0c61bcace578f777f443ba9c87604eccae1d5058 Mon Sep 17 00:00:00 2001 From: dimdano Date: Fri, 26 Sep 2025 10:56:17 +0200 Subject: [PATCH 2/4] pre-commit fixes --- README.md | 3 --- docs/advanced/plugins.rst | 4 ++-- hls4ml/backends/__init__.py | 2 +- hls4ml/backends/plugin_loader.py | 19 ++++++++++++++----- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 162f1a9236..a6f4a080cb 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,6 @@ Detailed tutorials on how to use `hls4ml`'s various functionalities can be found pip install hls4ml ``` -Specialised backends are now distributed as optional plugins. Install them alongside hls4ml via -``pip install ``, for example ``pip install aie4ml`` for the AMD AIE flow. - To install the extra dependencies for profiling: ```bash diff --git a/docs/advanced/plugins.rst b/docs/advanced/plugins.rst index e1949b8f9d..ddb7373bfd 100644 --- a/docs/advanced/plugins.rst +++ b/docs/advanced/plugins.rst @@ -2,8 +2,8 @@ External Backend and Writer Plugins ======================================= -Starting with this release ``hls4ml`` can discover and load backend implementations from -external Python packages. This enables specialised flows—such as the AMD AIE backend—to live in +``hls4ml`` can discover and load backend implementations from +external Python packages. This enables specialised flows, such as the AMD AIE backend, to live in independent projects that version and iterate at their own cadence while reusing the core conversion infrastructure. diff --git a/hls4ml/backends/__init__.py b/hls4ml/backends/__init__.py index 7b31770c84..54a047646a 100644 --- a/hls4ml/backends/__init__.py +++ b/hls4ml/backends/__init__.py @@ -1,6 +1,7 @@ from hls4ml.backends.backend import Backend, get_available_backends, get_backend, register_backend # noqa: F401 from hls4ml.backends.fpga.fpga_backend import FPGABackend # noqa: F401 from hls4ml.backends.oneapi.oneapi_backend import OneAPIBackend +from hls4ml.backends.plugin_loader import load_backend_plugins from hls4ml.backends.quartus.quartus_backend import QuartusBackend from hls4ml.backends.symbolic.symbolic_backend import SymbolicExpressionBackend from hls4ml.backends.vivado.vivado_backend import VivadoBackend @@ -10,7 +11,6 @@ from hls4ml.backends.catapult.catapult_backend import CatapultBackend # isort: skip from hls4ml.backends.vitis.vitis_backend import VitisBackend # isort: skip -from hls4ml.backends.plugin_loader import load_backend_plugins def _register_builtin_backends(): diff --git a/hls4ml/backends/plugin_loader.py b/hls4ml/backends/plugin_loader.py index 05edb3ef40..a51ccd7e38 100644 --- a/hls4ml/backends/plugin_loader.py +++ b/hls4ml/backends/plugin_loader.py @@ -5,9 +5,9 @@ import inspect import logging import os -from collections.abc import Iterable +from collections.abc import Callable, Iterable from importlib import import_module -from typing import Any, Callable +from typing import Any try: # pragma: no cover - fall back for older Python versions from importlib.metadata import entry_points @@ -60,7 +60,9 @@ def _load_entry_point_plugins(logger: logging.Logger) -> None: try: obj = ep.load() except Exception as exc: # pragma: no cover - defensive - logger.warning('Failed to load backend plugin entry %s: %s', ep.name, exc, exc_info=logger.isEnabledFor(logging.DEBUG)) + logger.warning( + 'Failed to load backend plugin entry %s: %s', ep.name, exc, exc_info=logger.isEnabledFor(logging.DEBUG) + ) continue _register_plugin_object(ep.name, obj, logger) @@ -74,7 +76,12 @@ def _load_env_plugins(logger: logging.Logger) -> None: try: module = import_module(module_name) except Exception as exc: # pragma: no cover - defensive - logger.warning('Failed to import backend plugin module %s: %s', module_name, exc, exc_info=logger.isEnabledFor(logging.DEBUG)) + logger.warning( + 'Failed to import backend plugin module %s: %s', + module_name, + exc, + exc_info=logger.isEnabledFor(logging.DEBUG), + ) continue register_callable: Any = getattr(module, 'register', module) @@ -121,4 +128,6 @@ def _safe_register_backend(name: str, backend_cls: type[Backend], logger: loggin try: register_backend(name, backend_cls) except Exception as exc: # pragma: no cover - defensive - logger.warning('Failed to register backend %s from plugin: %s', name, exc, exc_info=logger.isEnabledFor(logging.DEBUG)) + logger.warning( + 'Failed to register backend %s from plugin: %s', name, exc, exc_info=logger.isEnabledFor(logging.DEBUG) + ) From f0f09e94ae391e86593539181f8d32dba89550c9 Mon Sep 17 00:00:00 2001 From: dimdano Date: Fri, 26 Sep 2025 13:42:26 +0200 Subject: [PATCH 3/4] remove unused JSON include --- MANIFEST.in | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 4a6bc93b2f..656a95dd71 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,7 +4,6 @@ graft test graft contrib recursive-include hls4ml/templates * recursive-include hls4ml *.py -recursive-include hls4ml/backends *.json recursive-include hls4ml/contrib * global-exclude .git .gitmodules .gitlab-ci.yml *.pyc -include hls4ml/backends/vivado_accelerator/supported_boards.json +include hls4ml/backends/vivado_accelerator/supported_boards.json \ No newline at end of file From c5c7c127e83dd2532d1bf92a16084cd163a9aa9d Mon Sep 17 00:00:00 2001 From: dimdano Date: Fri, 26 Sep 2025 13:44:22 +0200 Subject: [PATCH 4/4] pre-commit fix --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 656a95dd71..e3ee5ded3c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,4 +6,4 @@ recursive-include hls4ml/templates * recursive-include hls4ml *.py recursive-include hls4ml/contrib * global-exclude .git .gitmodules .gitlab-ci.yml *.pyc -include hls4ml/backends/vivado_accelerator/supported_boards.json \ No newline at end of file +include hls4ml/backends/vivado_accelerator/supported_boards.json