Package & Distribute a Backend Plugin¶
This guide covers how to package a CUDA-Q backend implementation as an
installable Python package, distribute it to end users of the target, and make
it discoverable by both the Python runtime and nvq++ after they install it.
For how to implement a backend (ServerHelper or QPU subclass), see
Extending CUDA-Q with a new Hardware Backend.
Plugin Package Layout¶
Every plugin follows the same directory convention:
my-backend/
├── targets/
│ ├── my-backend.so # Pre-compiled target config (.dylib on macOS)
| | # OR (mutually exclusive)
│ └── my-backend.yml # Raw YAML target config
├── lib/
│ └── libcudaq-serverhelper-my-backend.so # Backend shared library (.dylib on macOS)
├── data/ # Optional auxiliary files
│ └── topology.txt
├── pyproject.toml # Python package metadata
├── __init__.py # Entry-point registration
└── __main__.py # --install-nvqpp hook
The targets/ and lib/ directories are required. The data/ directory
is optional and holds any auxiliary files your backend needs at runtime.
The plugin can choose to ship either a pre-compiled target config library
(my-backend.so) or a raw YAML target config (my-backend.yml). The
pre-compiled format is recommended and required for the plugin to be used in
CUDA-Q C++ programs, whereas the YAML config is for use in Python programs only.
Do not ship the two formats together as the compiled library will always shadow
the YAML config.
Note
This guide writes shared library filenames with the Linux .so
extension. On macOS the corresponding extension is .dylib; substitute
it throughout.
How to pre-compile a target config YAML¶
The target config is always authored as a YAML file. To be used at runtime by compiled C++ programs, it must be distributed as a pre-compiled shared library. This is achieved in two steps:
Use the
cudaq-target-db-gen --plugincommand that is shipped with the CUDA-Q installation to convert the YAML file into a.cppC++ file.Compile the
.cppfile into a shared library and install it astargets/<name>.so(ortargets/<name>.dylib).
# Linux
cudaq-target-db-gen --plugin -o my-backend.gen.cpp my-backend=my-backend.yml
${CXX} -std=c++20 -shared -fPIC -I "${CUDAQ_INSTALL_DIR}/include" \
my-backend.gen.cpp -o targets/my-backend.so
# macOS
cudaq-target-db-gen --plugin -o my-backend.gen.cpp my-backend=my-backend.yml
${CXX} -std=c++20 -shared -fPIC -isysroot "$(xcrun --show-sdk-path)" \
-I "${CUDAQ_INSTALL_DIR}/include" \
my-backend.gen.cpp -o targets/my-backend.dylib
The generated translation unit is self-contained - it only needs CUDA-Q’s
headers, not any CUDA-Q library. The resulting library is platform-specific
and must be built and distributed separately for each supported platform.
On macOS, pass -isysroot explicitly if ${CXX} is the LLVM toolchain
that ships with CUDA-Q rather than Apple’s own clang++: only the latter
infers the SDK location, and without it the C++ standard library headers
are not found.
The generated library exports a single, ABI-versioned symbol
(cudaq::config::kTargetPluginSymbolName); CUDA-Q dlopens it and
dlsyms exactly that symbol name, so a library built against an
incompatible CUDA-Q version fails immediately and unambiguously at load
time rather than being silently misinterpreted.
Target YAML Reference (Plugin Fields)¶
Every target YAML begins with a schema version:
version: 1
name: my-backend
description: "My backend."
The source YAML uses the same schema as in-tree targets, with these plugin-relevant fields:
%PLUGIN_ROOT%¶
A substitution token expanded to the plugin’s root directory when
cudaq-target-db-gen --plugin compiles your YAML (so it must be staged at
its final <pkgRoot>/targets/my-backend.yml location, even temporarily,
when you invoke the generator):
config:
jit-mid-level-pipeline: "qubit-mapping{device=file(%PLUGIN_ROOT%/data/topology.txt)}"
target-arguments:
- key: device
type: string
default: "%PLUGIN_ROOT%/data/topology.txt"
platform-arg: device
Use this to reference files shipped inside the plugin without hard-coding install paths.
target-arguments¶
Declares per-target parameters that surface as CLI flags for nvq++ and
keyword arguments for cudaq.set_target():
target-arguments:
- key: api-key
required: true
type: string
platform-arg: api_key
help-string: "API key for the backend."
- key: shots
required: false
type: integer
platform-arg: shots
help-string: "Default shot count."
These appear as --my-backend-api-key <value> on the command line and
cudaq.set_target("my-backend", api_key=...) in Python.
Building with CUDAQ_EXTERNAL_PROJECTS¶
During development, build your plugin inside the CUDA-Q source tree without modifying any in-tree files:
cmake -B build \
-DCUDAQ_EXTERNAL_PROJECTS="my-backend" \
-DCUDAQ_EXTERNAL_MY_BACKEND_SOURCE_DIR=/path/to/my-backend
ninja -C build cudaq-serverhelper-my-backend # or cudaq-qpu-my-backend
The plugin output lands in build/external/my-backend/ with the standard
targets/, lib/, and packaging files ready to use.
Multiple plugins can be built together:
cmake -B build \
-DCUDAQ_EXTERNAL_PROJECTS="foo;bar" \
-DCUDAQ_EXTERNAL_FOO_SOURCE_DIR=/path/to/foo \
-DCUDAQ_EXTERNAL_BAR_SOURCE_DIR=/path/to/bar
Baking a target into the pre-compiled database instead of shipping a plugin¶
An external project added via CUDAQ_EXTERNAL_PROJECTS is add_subdirectory()’d
into the same CMake configure as the rest of CUDA-Q, so it shares the same
global target-registration state as CUDA-Q’s own in-tree targets. This means
it can call the internal add_target_config(<name>) CMake function (from
cmake/modules/AddCUDAQ.cmake, already include()’d before any
subdirectory is processed) directly from its own CMakeLists.txt:
# my-backend/CMakeLists.txt
add_target_config(my-backend) # expects my-backend.yml next to this file
Doing this bakes my-backend directly into the same pre-compiled
CUDAQTargetDatabase database as every other in-tree target - the target
is resolved with zero YAML parsing and avoids dynamically loading a library
at runtime. In summary, there are two different distribution models:
Compiled plugin library (``targets/<name>.so`` or ``targets/<name>.dylib``): works against an already-built, already-installed CUDA-Q. Can be distributed independently of CUDA-Q’s own release cadence.
``add_target_config`` via ``CUDAQ_EXTERNAL_PROJECTS``: requires rebuilding (this part of) CUDA-Q itself together with your target - there is no pre-built CUDA-Q install this can attach to after the fact. This is the right choice for an organization assembling and shipping its own customized CUDA-Q distribution with extra targets baked in from the start, not for distributing a target independently to users of a stock CUDA-Q install.
Python Packaging¶
A plugin ships as a standard Python package with a cudaq.backends entry
point that makes it discoverable at import cudaq time. The package includes
the compiled target plugin library and shared library, allowing the plugin author to build a wheel
and publish it to a package index or distribute it directly. End users can then
install that package into their own CUDA-Q environments without building the
plugin from source.
Because the package contains a native shared library, it is platform-specific. Plugin authors must build and distribute a separate package for each operating system and architecture required by their users. When distributing wheels, ensure that each wheel has the platform tag matching the platform targeted by its shared library; do not distribute the package as a platform-agnostic wheel.
pyproject.toml¶
[build-system]
requires = ["setuptools>=64", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my-backend-cudaq"
version = "0.1.0"
description = "My Backend plugin for CUDA-Q."
requires-python = ">=3.9"
[project.entry-points."cudaq.backends"]
my-backend = "my_backend_cudaq:register"
[tool.setuptools]
packages = ["my_backend_cudaq"]
[tool.setuptools.package-dir]
"my_backend_cudaq" = "."
[tool.setuptools.package-data]
"my_backend_cudaq" = [
"targets/*.so", "targets/*.dylib", "targets/*.yml", "lib/*",
]
The critical piece is the [project.entry-points."cudaq.backends"] section.
The key (my-backend) is a free-form identifier; the value points to the
register() function in your package.
__init__.py¶
from importlib.resources import files
def register():
import cudaq
cudaq.register_backend_path(str(files(__name__)))
When import cudaq runs, it discovers all cudaq.backends entry points
and calls each one. Your register() function calls
cudaq.register_backend_path() with the package root, which scans
targets/ for compiled plugin libraries (.so, .dylib) and .yml
files and makes those targets available.
If your entry point raises an exception, CUDA-Q logs a warning with the entry-point name and traceback and continues — other plugins still load.
__main__.py (--install-nvqpp hook)¶
PACKAGE_NAME = "my_backend_cudaq"
PLUGIN_NAME = "my-backend"
def main() -> int:
from cudaq.plugins import install_plugin_for_nvqpp_main
return install_plugin_for_nvqpp_main(PACKAGE_NAME, PLUGIN_NAME)
if __name__ == "__main__":
raise SystemExit(main())
This lets users run python -m my_backend_cudaq --install-nvqpp to symlink
the installed package into the nvq++ user plugin scope.
Installing the Plugin for End Users¶
In this section, “installation” means installation by an end user who wants to use the target. It is separate from the preceding build and packaging steps performed by the plugin author. The steps an end user follows depend on how the plugin is distributed and whether they access the target from Python or C++:
pip install (Python — zero config)¶
pip install my-backend-cudaq
This installs the Python package previously published or otherwise distributed
by the plugin author. After installation in the end user’s Python environment,
import cudaq discovers the entry point automatically. No further
configuration is needed — cudaq.set_target("my-backend") works immediately.
--install-nvqpp (make visible to nvq++)¶
The Python entry-point mechanism only fires inside a Python process. To make
the Python package that the end user installed above visible to the nvq++
compiler driver as well:
python -m my_backend_cudaq --install-nvqpp
This symlinks the installed package directory into the user plugin scope.
After this, nvq++ --target=my-backend resolves the target.
cudaq-install-plugin (C++-only workflows)¶
For plugins distributed without Python packaging (built from source, distributed as a tarball):
# Install to user scope (default, no sudo required)
cudaq-install-plugin /path/to/my-backend
# Install to system scope (shared across users)
sudo cudaq-install-plugin --system /path/to/my-backend
# Copy instead of symlink
cudaq-install-plugin --copy /path/to/my-backend
# List installed plugins
cudaq-install-plugin --list
# Remove a plugin
cudaq-install-plugin --uninstall my-backend
Discovery Mechanics¶
nvq++ target resolution¶
When nvq++ --target=my-backend is invoked, cudaq-target-resolve looks
the name up in the unified target registry:
Built-in: the pre-compiled
CUDAQTargetDatabaselinked into CUDA-QUser scope:
${CUDAQ_PLUGIN_ROOT:-${XDG_DATA_HOME:-$HOME/.local/share}/cudaq/plugins}/*/targets/my-backend.{so,dylib}System scope:
${install_dir}/plugins/*/targets/my-backend.{so,dylib}
A compiled plugin library in the same plugin root shadows a same-named
.yml. C++ programs do not support parsing YAML target configuration files
at runtime and expect to find a pre-compiled target config library. Built-in
names cannot be shadowed; an external root that reuses a built-in name is
skipped with a warning. User-scope plugins take precedence over system-scope.
nvq++ --list-targets (and cudaq.get_targets() / has_target())
show targets that are available on this host. Pass --include-unavailable
or include_unavailable=True to include known-but-unavailable targets;
each entry exposes an availability diagnostic (missing GPU, simulator,
platform library, plugin library, or incompatible CUDA-Q version).
When a plugin target is resolved, nvq++ automatically adds the plugin’s
lib/ directory to the linker search path (-L) and runtime path
(-Wl,-rpath).
Python target resolution¶
At import cudaq, the runtime:
Loads built-in targets from the pre-compiled database
Calls each
cudaq.backendsentry point, which registers additional plugin roots viacudaq.register_backend_path()
After this, cudaq.set_target("my-backend") works for any registered target.
Environment variables¶
Variable |
Purpose |
|---|---|
|
Overrides the user-scope plugin directory for |
|
Standard XDG variable. When |
Reference Plugins¶
CUDA-Q ships a reference plugin under docs/sphinx/examples/plugins/
that demonstrates the full plugin lifecycle:
Plugin |
Shape |
What it demonstrates |
|---|---|---|
REST |
|
Use this as a starter template for new plugins. It includes a complete build configuration, Python packaging, lit tests, and documentation.
Quick-Start Checklist¶
□ Implement ServerHelper subclass
□ Author targets/<name>.yml (``version: 1``) and optionally distribute it
as a pre-compiled library
□ Create CMakeLists.txt (build with CUDAQ_EXTERNAL_PROJECTS)
□ Add pyproject.toml with cudaq.backends entry point
□ Add __init__.py with register() function
□ Add __main__.py with --install-nvqpp hook
□ Add tests/ with at least one end-to-end test
□ Verify: pip install . && python -c "import cudaq; assert cudaq.has_target('<name>')"
□ Verify: python -m <pkg> --install-nvqpp && nvq++ --target=<name> ...