Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,11 @@

<h3>Improvements 🛠</h3>

* 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. 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
applies when no work wires are provided, matching `MultiControlledX.compute_decomposition`.
This removes controlled `GlobalPhase` and `PhaseShift` rules from the decomposition graph and
Expand Down Expand Up @@ -1336,6 +1341,9 @@

<h3>Breaking changes 💔</h3>

* The `qp.Backline.backline` alias of `qp.Backline.placement` has been removed. Use `placement`.
[(#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
available as :func:`~.transforms._tape_decompose`.
Expand Down
5 changes: 0 additions & 5 deletions pennylane/backline/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
18 changes: 14 additions & 4 deletions pennylane/backline/placement.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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``.
Expand All @@ -142,8 +150,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.
Expand Down Expand Up @@ -207,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):
Expand Down Expand Up @@ -237,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
Expand Down Expand Up @@ -343,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
Expand Down
33 changes: 25 additions & 8 deletions pennylane/backline/transports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
5 changes: 0 additions & 5 deletions tests/backline/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
29 changes: 27 additions & 2 deletions tests/backline/test_placement.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,12 +471,37 @@ 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_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")
Expand Down
Loading