From 3fa392460adbd5acfb624358bf6a477b547dccb1 Mon Sep 17 00:00:00 2001 From: Joseph Lee Date: Mon, 19 Oct 2026 10:19:12 -0400 Subject: [PATCH 1/3] [Backline] Accept transports from Catalyst's backend registry get_transport accepts transports implemented by backends in Catalyst's registry (lazy import; unchecked without Catalyst). Remove the Backline.backline alias and update the executor port docstring. Co-Authored-By: Claude Opus 5.5 --- doc/releases/changelog-dev.md | 7 +++++++ pennylane/backline/device.py | 5 ----- pennylane/backline/placement.py | 3 +-- pennylane/backline/transports.py | 33 ++++++++++++++++++++++++-------- tests/backline/test_device.py | 5 ----- tests/backline/test_placement.py | 23 ++++++++++++++++++++-- 6 files changed, 54 insertions(+), 22 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index c53bb43e7f7..8fb0bc1d8c3 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -759,6 +759,10 @@

Improvements 🛠

+* A Backline placement accepts any transport that a backend in Catalyst's transport backend + registry implements, so a backend package can add a transport without a PennyLane change. + [(#XXXXX)](https://github.com/PennyLaneAI/pennylane/pull/XXXXX) + * The `decompose_mcx_with_no_worker` decomposition rule for :class:`~.MultiControlledX` now only applies when no work wires are provided, matching `MultiControlledX.compute_decomposition`. This removes controlled `GlobalPhase` and `PhaseShift` rules from the decomposition graph and @@ -1253,6 +1257,9 @@

Breaking changes 💔

+* The `qp.Backline.backline` alias of `qp.Backline.placement` has been removed. Use `placement`. + [(#XXXXX)](https://github.com/PennyLaneAI/pennylane/pull/XXXXX) + * :func:`~.transforms.decompose` is now a pass-only transform that always dispatches to Catalyst's ``graph_decomposition`` under :func:`~.qjit`. The previous tape-based implementation is available as :func:`~.transforms._tape_decompose`. diff --git a/pennylane/backline/device.py b/pennylane/backline/device.py index b33e2eed1fb..dc9ea94a067 100644 --- a/pennylane/backline/device.py +++ b/pennylane/backline/device.py @@ -121,11 +121,6 @@ def placement(self): """Placement: The :class:`~.Placement` the device was configured with.""" return self._placement - @property - def backline(self): - """Placement: Alias of :attr:`placement` for Catalyst.""" - return self._placement - @property def transport(self): """Transport: The :class:`~.Transport` carrying data between nodes.""" diff --git a/pennylane/backline/placement.py b/pennylane/backline/placement.py index 5c345a4e636..ba2cd1e82bc 100644 --- a/pennylane/backline/placement.py +++ b/pennylane/backline/placement.py @@ -142,8 +142,7 @@ class Node: * ``"address"`` (str) - ``"host:port"`` of an executor whose lifetime is managed elsewhere. Attach to it rather than launching one. * ``"port"`` (int) - the port the executor binds on the target, and the local end of the ssh - tunnel reaching it. Omit it for a local executor and a free port is chosen at launch, so the - address is only known afterwards. + tunnel reaching it. Omit it for a local executor and the compiler picks a free port. * ``"user"`` (str) - the ssh account on the remote host. Defaults to the local username. Naming neither ``"host"`` nor ``"address"`` runs the executor as a subprocess on this machine. diff --git a/pennylane/backline/transports.py b/pennylane/backline/transports.py index b645f30259c..5a80eb9f8cf 100644 --- a/pennylane/backline/transports.py +++ b/pennylane/backline/transports.py @@ -32,13 +32,16 @@ class Transport: :attr:`~pennylane.Backline.transport`. The compiler combines the transport with each node's hardware to select a concrete runtime backend. - The registered transports are: + The built-in transports are: * ``"rdma"``: messages travel over RDMA, so every coprocessor needs an :class:`~.Endpoint`. * ``"memcpy"``: messages are copied in memory, with no network endpoint. * ``"none"``: no messages move. The placement is a :class:`~.Controller` alone, and passing any coprocessor raises an error. + Any other transport a backend in Catalyst's transport backend registry implements is accepted + as well, so a backend package adds a transport without a PennyLane change. + .. warning:: :mod:`Backline <.backline>` is experimental and only usable through the Catalyst @@ -94,17 +97,31 @@ def get_transport(name): Transport: The :class:`~.Transport` produced by the registered factory. Raises: - ValueError: If ``name`` is not registered. + ValueError: If ``name`` is neither registered here nor implemented by a backend in + Catalyst's transport backend registry. Without Catalyst installed, any name is + accepted and checked when the program is compiled. .. seealso:: :func:`~.register_transport`, :class:`~.Transport` """ + if name in _TRANSPORTS: + return _TRANSPORTS[name]() + known = _catalyst_transports() + if known is None or name in known: + return Transport(name) + raise ValueError( + f"unknown transport {name!r}; registered transports: {sorted({*_TRANSPORTS, *known})}" + ) + + +def _catalyst_transports(): + """The transports Catalyst's backend registry implements, or ``None`` without Catalyst.""" try: - factory = _TRANSPORTS[name] - except KeyError: - raise ValueError( - f"unknown transport {name!r}; registered transports: {sorted(_TRANSPORTS)}" - ) from None - return factory() + from catalyst.backline import ( # pylint: disable=import-outside-toplevel + backend_transports, + ) + except ImportError: + return None + return backend_transports() @register_transport("rdma") diff --git a/tests/backline/test_device.py b/tests/backline/test_device.py index 638b3fd1add..2710937c561 100644 --- a/tests/backline/test_device.py +++ b/tests/backline/test_device.py @@ -50,11 +50,6 @@ def test_holds_placement_and_transport(self): assert isinstance(dev.placement, Placement) assert dev.transport.name == "rdma" - def test_backline_property_is_an_alias_of_placement(self): - """``dev.backline`` is the Catalyst-facing alias for :attr:`placement`.""" - dev = _device() - assert dev.backline is dev.placement - def test_all_arguments_are_keyword_only(self): """No argument may be passed positionally, so ordering can never be confused, and a controller is never mistaken for wires.""" diff --git a/tests/backline/test_placement.py b/tests/backline/test_placement.py index 0471388d3fc..53a10e9014c 100644 --- a/tests/backline/test_placement.py +++ b/tests/backline/test_placement.py @@ -471,12 +471,31 @@ def test_coprocessors_coerced_to_tuple(self): placement = Placement(controller=ctrl, coprocessors=[cop], transport="rdma") assert isinstance(placement.coprocessors, tuple) - def test_unknown_transport_raises(self): - """An unregistered transport name is rejected at construction.""" + def test_unknown_transport_raises(self, monkeypatch): + """A transport neither built in nor known to Catalyst's registry is rejected.""" + monkeypatch.setattr("pennylane.backline.transports._catalyst_transports", set) ctrl = qp.Controller(name="fpga") with pytest.raises(ValueError, match="unknown transport"): Placement(controller=ctrl, transport="does-not-exist") + def test_a_transport_from_catalysts_registry_is_accepted(self, monkeypatch): + """A backend package's transport needs no PennyLane change.""" + monkeypatch.setattr( + "pennylane.backline.transports._catalyst_transports", lambda: {"example"} + ) + assert ( + Placement(controller=qp.Controller(name="c"), transport="example").transport.name + == "example" + ) + + def test_without_catalyst_any_transport_is_left_to_the_compiler(self, monkeypatch): + """With no registry to ask, the name is checked when the program is compiled.""" + monkeypatch.setattr("pennylane.backline.transports._catalyst_transports", lambda: None) + assert ( + Placement(controller=qp.Controller(name="c"), transport="example").transport.name + == "example" + ) + def test_not_exported_at_top_level(self): """Placement is reached via pennylane.backline, not the top-level namespace.""" assert not hasattr(qp, "Placement") From 41bfa713691f1e32809a86d817750c6c5354e298 Mon Sep 17 00:00:00 2001 From: Joseph Lee Date: Mon, 19 Oct 2026 10:26:25 -0400 Subject: [PATCH 2/3] update changelog --- doc/releases/changelog-dev.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 8fb0bc1d8c3..c72f5486598 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -761,7 +761,7 @@ * A Backline placement accepts any transport that a backend in Catalyst's transport backend registry implements, so a backend package can add a transport without a PennyLane change. - [(#XXXXX)](https://github.com/PennyLaneAI/pennylane/pull/XXXXX) + [(#10353)](https://github.com/PennyLaneAI/pennylane/pull/10353) * The `decompose_mcx_with_no_worker` decomposition rule for :class:`~.MultiControlledX` now only applies when no work wires are provided, matching `MultiControlledX.compute_decomposition`. @@ -1258,7 +1258,7 @@

Breaking changes 💔

* The `qp.Backline.backline` alias of `qp.Backline.placement` has been removed. Use `placement`. - [(#XXXXX)](https://github.com/PennyLaneAI/pennylane/pull/XXXXX) + [(#10353)](https://github.com/PennyLaneAI/pennylane/pull/10353) * :func:`~.transforms.decompose` is now a pass-only transform that always dispatches to Catalyst's ``graph_decomposition`` under :func:`~.qjit`. The previous tape-based implementation is From 88d9f6e142d4e02bff49e8720f745bd76d010311 Mon Sep 17 00:00:00 2001 From: Joseph Lee Date: Tue, 20 Oct 2026 16:06:26 -0400 Subject: [PATCH 3/3] hongsheng comments --- doc/releases/changelog-dev.md | 3 ++- pennylane/backline/placement.py | 15 +++++++++++++-- tests/backline/test_placement.py | 6 ++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 495d8fd71b7..22f4df74797 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -839,7 +839,8 @@

Improvements 🛠

* A Backline placement accepts any transport that a backend in Catalyst's transport backend - registry implements, so a backend package can add a transport without a PennyLane change. + registry implements, so a backend package can add a transport without a PennyLane change. A + :class:`~.Controller` or :class:`~.Coprocessor` names the backend to run on with ``backend=``. [(#10353)](https://github.com/PennyLaneAI/pennylane/pull/10353) * The `decompose_mcx_with_no_worker` decomposition rule for :class:`~.MultiControlledX` now only diff --git a/pennylane/backline/placement.py b/pennylane/backline/placement.py index 3ef5ee1b044..d1ebf715d68 100644 --- a/pennylane/backline/placement.py +++ b/pennylane/backline/placement.py @@ -97,6 +97,8 @@ class Node: hardware (str): The hardware this node executes on. Defaults to ``"cpu"``; other allowed values are ``gpu`` and ``fpga`. The compiler combines this with the placement's :class:`~.Transport` to select the runtime backend. + backend (str, None): The name of the transport backend to run this node on. Defaults to + ``None``, letting the compiler pick the default backend for the transport and hardware. remote (bool): Whether this node runs on another machine. Defaults to ``False``. executor_options (dict, None): Options for the executor to launch for this node. Defaults to ``None``, which runs the node in this process. See the @@ -116,6 +118,12 @@ class Node: """The hardware this node executes on. The compiler combines this with the placement's :class:`~.Transport` to select the runtime backend.""" + backend: str | None = None + """The name of the transport backend to run this node on, out of those in Catalyst's transport + backend registry that implement the placement's transport on this node's hardware. Defaults to + ``None``, letting the compiler pick the default one. Naming one selects a backend that is not a + default, such as ``"fpga_verbs"``, or one installed by a backend package.""" + remote: bool = False """Whether this node runs on another machine, so that the libraries it loads are the ones installed beside it there rather than the ones in this installation. Defaults to ``False``. @@ -206,8 +214,7 @@ class Node: index; the remaining keys are backend-specific (a GPU backend takes ``gpu=``, an FPGA engine takes ``sq_mem=``/``data_mem=``/``reply_mem=``). - - Keys outside this set are dropped rather than rejected, so a misspelling is silent. + Keys outside this set are rejected when the program is compiled. """ def __post_init__(self): @@ -236,6 +243,8 @@ class Controller(Node): hardware (str): The hardware the controller executes on. Defaults to ``"cpu"``; other allowed values are ``"fpga"``. ``"gpu"`` is not accepted as a controller. The compiler combines this with the placement's :class:`~.Transport` to select the runtime backend. + backend (str, None): The name of the transport backend to run this node on. Defaults to + ``None``, letting the compiler pick the default backend for the transport and hardware. remote (bool): Whether the controller runs on another machine. Defaults to ``False``. executor_options (dict, None): Options for the executor to launch for this controller. Defaults to ``None``, which runs the node in this process. See the @@ -342,6 +351,8 @@ class Coprocessor(Node): hardware (str): The hardware the coprocessing function executes on. Defaults to ``"cpu"``; other allowed values are ``gpu``. ``fpga`` is not currently accepted for a coprocessor. The compiler combines this with the placement's :class:`~.Transport` to select the runtime backend. + backend (str, None): The name of the transport backend to run this node on. Defaults to + ``None``, letting the compiler pick the default backend for the transport and hardware. remote (bool): Whether the controller runs on another machine. Defaults to ``False``. coprocessor_fn (str, CoprocessorFunction): The function that processes each received message. A string is wrapped in a :class:`~.CoprocessorFunction` naming that symbol, so diff --git a/tests/backline/test_placement.py b/tests/backline/test_placement.py index 53a10e9014c..d9aaddaf255 100644 --- a/tests/backline/test_placement.py +++ b/tests/backline/test_placement.py @@ -496,6 +496,12 @@ def test_without_catalyst_any_transport_is_left_to_the_compiler(self, monkeypatc == "example" ) + def test_a_node_names_its_backend(self): + """``backend`` defaults to letting the compiler choose, and keeps a name it is given.""" + assert qp.Controller().backend is None + assert qp.Controller(backend="fpga_verbs").backend == "fpga_verbs" + assert qp.Coprocessor(coprocessor_fn="decoder", backend="example").backend == "example" + def test_not_exported_at_top_level(self): """Placement is reached via pennylane.backline, not the top-level namespace.""" assert not hasattr(qp, "Placement")