Wiring a host library#
A host library is one that defines a public scientific API and wants to let backends optionally accelerate it.
1. Instantiate a BackendDispatcher#
# example_host/_backends/__init__.py
from scverse_backends import BackendDispatcher
_dispatcher = BackendDispatcher(
entrypoint_group="example_host.backends",
host_name="example_host",
trusted_backends={
"example_accel": {
"aliases": ["example", "accelerated"],
"package": "example-host-accel",
},
},
reserved_backends={
"generic": "Use a concrete backend alias instead.",
},
)
backend_dispatch = _dispatcher.backend_dispatch
settings = _dispatcher.settings
get_backend = _dispatcher.get_backend
available_backend_names = _dispatcher.available_backend_names
BackendDispatcher is fully instance-scoped — every host owns its
own registry, settings, and ContextVar. Multiple hosts can dispatch
independently in the same process.
Constructor parameters#
entrypoint_groupThe Python entrypoint group your backends register against. Convention is
"<host_name>.backends".host_nameUsed in error messages and as the prefix of the settings
ContextVar.trusted_backends(optional)Mapping of canonical backend name to
{"aliases": [...], "package": "<pip-name>"}. Backends listed here skip the “untrusted” warning on first use, and the host’s error messages can suggest apip installfor known-but-not-installed backends. Any package can still plug in without being listed.Trusted aliases are reserved. If
acceleratedis listed as an alias forexample_accel, an unrelated backend cannot claimname="accelerated"oraliases=["accelerated"].Trusted canonical names are also provider-verified during entrypoint discovery. By default,
packageis treated as the expected Python distribution name. If the same trusted backend can be shipped by multiple distributions, usedistributions:trusted_backends={ "example_accel": { "aliases": ["example", "accelerated"], "package": "example-host-accel", "distributions": [ "example-host-accel", "example-host-accel-cu12", ], "entrypoints": ["example_accel"], "module_prefixes": ["example_host_accel"], }, }
This prevents another installed package from registering an entrypoint whose adapter claims
name="example_accel"unless the entrypoint is provided by one of the configured distributions.entrypoints,object_refs, andmodule_prefixesare optional extra checks for hosts that want a narrower handshake.reserved_backends(optional)Mapping of host-reserved backend names or aliases to human-readable reasons. No backend may claim these names. Use this for names that are too generic or intentionally left undefined by the host.
2. Decorate your public functions#
Use @backend_dispatch on module-level public functions. Instance methods and
class methods are outside the dispatch contract because their self/cls
binding does not map cleanly onto backend adapter callables.
# example_host/analysis.py
from example_host._backends import backend_dispatch
@backend_dispatch
def compute_score(data, *, method="fast", n_jobs=None, copy=False):
"""Compute a score.
Parameters
----------
data
Input data.
method
Scoring method.
n_jobs
Number of CPU jobs (host-only — silently dropped on backends).
copy
Return a copy instead of writing to ``adata``.
"""
...
@backend_dispatch injects a backend=None keyword and (after backends are
discovered) merges any backend-specific parameters into the function’s
signature and numpydoc. None means “use the active setting”;
backend="cpu" explicitly forces the host implementation for that call.
The injected backend selector is documented with the host-owned
Parameters, while backend-specific parameters are placed under
Other Parameters and marked on the parameter line with the backend that
provided them. That
keeps host-owned knobs visually separate from optional backend package
knobs.
For example, a backend-only parameter from example_accel is rendered
as solver (example_accel) under Other Parameters.
3. Re-export settings for users#
# example_host/__init__.py
from example_host._backends import settings # noqa: F401
4. Trigger eager discovery where signatures matter#
Discovery is lazy by default — backend entrypoints aren’t loaded
until something actually queries the dispatcher (settings setter,
get_backend, a non-CPU dispatched call). That keeps host import
fast.
But help(my_func), IDE tooltips, and Sphinx autodoc introspect the
function statically — they never trigger lazy discovery. Without an
explicit nudge, those tools see only the host’s CPU-only signature.
BackendDispatcher.discover() is the explicit nudge:
# docs/conf.py
import example_host._backends as _b
_b._dispatcher.discover() # so autodoc sees merged signatures
# in IDE startup, test setup, or anywhere else that introspects:
from example_host._backends import _dispatcher
_dispatcher.discover()
discover() is idempotent and safe to call repeatedly — internally it
reuses the cached “original docstring” of every decorated function so
re-running the merge never double-injects backend params.
Discovery is intentionally one-shot for installed entry points. It is not a runtime plugin reload mechanism; if a backend package is installed into an already-running Python process, restart the process before expecting the host to discover it. Functions decorated after discovery still get merged against the already-discovered backends.
Don’t call discover() from your top-level __init__.py. That
forces every backend’s heavy imports (CUDA runtimes, JAX, …) to load on
every host import — wiping out the lazy-discovery benefit. Save it for
the few places that actually need merged signatures.
That’s all the host needs to do. Backend authors take it from here.