diff --git a/.github/workflows/check-transport.yaml b/.github/workflows/check-transport.yaml new file mode 100644 index 0000000000..9728597066 --- /dev/null +++ b/.github/workflows/check-transport.yaml @@ -0,0 +1,189 @@ +name: Check Transport Backends + +on: + pull_request: + types: + - opened + - reopened + - synchronize + - ready_for_review + paths: + - 'runtime/lib/transport/**' + - 'runtime/tests/Test_Transport*.cpp' + - 'runtime/tests/stubs/stub_transport_backend*' + - 'runtime/tests/CMakeLists.txt' + - 'runtime/CMakeLists.txt' + - 'runtime/lib/CMakeLists.txt' + - 'runtime/Makefile' + - 'Makefile' + - '.github/workflows/check-transport.yaml' + push: + branches: [ main ] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + determine_runner: + if: github.event.pull_request.draft == false + name: Determine runner type to use + uses: ./.github/workflows/determine-workflow-runner.yml + with: + default_runner: ubuntu-24.04 + + constants: + name: "Set build matrix" + uses: ./.github/workflows/constants.yaml + needs: [determine_runner] + with: + multiple_compilers: false + runs_on: ${{ needs.determine_runner.outputs.runner_group }} + + transport-tests: + name: Transport Backend Tests (Soft-RoCE) + needs: [constants, determine_runner] + runs-on: ${{ needs.determine_runner.outputs.runner_group }} + + steps: + - name: Checkout Catalyst repo + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ needs.constants.outputs.primary_python_version }} + + - name: Install build and RDMA userland dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + cmake ninja-build clang make \ + libibverbs-dev libibverbs1 ibverbs-providers ibverbs-utils \ + rdma-core iproute2 + python3 -m pip install "nanobind<2.13" pybind11 + + - name: Ensure rdma_rxe.ko is available for the running kernel + run: | + # Try linux-modules-extra first; if the .ko isn't there, build + # rdma_rxe out-of-tree from the matching upstream stable branch. + set -euo pipefail + + KVER=$(uname -r) + echo "Running kernel: $KVER" + + have_rxe() { + find /lib/modules/"$KVER" -name 'rdma_rxe*' -print -quit | grep -q . + } + + # --- Path 1: standard Ubuntu extras package ---------------------- + sudo apt-get install -y "linux-modules-extra-$KVER" 2>/dev/null || true + sudo depmod -a + if have_rxe; then + echo "rdma_rxe.ko provided by linux-modules-extra-$KVER" + find /lib/modules/"$KVER" -name 'rdma_rxe*' -print + exit 0 + fi + + echo "linux-modules-extra unavailable or missing rdma_rxe;" \ + "falling back to out-of-tree build." + + # --- Path 2: build rdma_rxe out-of-tree -------------------------- + sudo apt-get install -y \ + build-essential bc bison flex libssl-dev libelf-dev git \ + "linux-headers-$KVER" + + # Upstream stable branch matching the running kernel's major.minor. + KVER_MAJMIN=$(echo "$KVER" | cut -d- -f1 | cut -d. -f1-2) + SRC=/tmp/linux-src + echo "Cloning linux-${KVER_MAJMIN}.y stable branch..." + git clone --depth 1 \ + --branch "linux-${KVER_MAJMIN}.y" \ + https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git \ + "$SRC" + + # Force obj-m; Azure kernel has CONFIG_RDMA_RXE unset. + sed -i 's|obj-\$(CONFIG_RDMA_RXE)|obj-m|' \ + "$SRC/drivers/infiniband/sw/rxe/Makefile" + + make -C "/lib/modules/$KVER/build" \ + M="$SRC/drivers/infiniband/sw/rxe" modules + + KO="$SRC/drivers/infiniband/sw/rxe/rdma_rxe.ko" + if [ ! -f "$KO" ]; then + echo "::error::rdma_rxe.ko not produced by build" + ls -la "$SRC/drivers/infiniband/sw/rxe/" + exit 1 + fi + + sudo install -D -m 644 "$KO" \ + "/lib/modules/$KVER/kernel/drivers/infiniband/sw/rxe/rdma_rxe.ko" + sudo depmod -a + + if ! have_rxe; then + echo "::error::rdma_rxe.ko not resolvable after install" + exit 1 + fi + echo "Built and installed rdma_rxe.ko from linux-${KVER_MAJMIN}.y" + modinfo rdma_rxe | head -20 + + - name: Configure Soft-RoCE (rxe0 on loopback) + run: | + # Attach rdma_rxe to `lo` so the tests find an ibverbs device named rxe0. + sudo modprobe rdma_rxe + sudo rdma link add rxe0 type rxe netdev lo + rdma link show + ibv_devices + ibv_devinfo -d rxe0 + # Fail fast if rxe0 didn't come up - otherwise Catch2 tests would + # silently SKIP and the loopback would be the only thing exercising it. + ibv_devices | awk '{print $1}' | grep -q '^rxe0$' + + - name: Get Cached LLVM Source + id: cache-llvm-source + uses: actions/cache/restore@v4 + with: + path: mlir/llvm-project + key: llvm-${{ needs.constants.outputs.llvm_version }}-default-source + enableCrossOsArchive: true + + - name: Clone LLVM Submodule + if: steps.cache-llvm-source.outputs.cache-hit != 'true' + uses: actions/checkout@v4 + with: + repository: llvm/llvm-project + ref: ${{ needs.constants.outputs.llvm_version }} + path: mlir/llvm-project + + - name: Build Catalyst-Runtime with ENABLE_TRANSPORT=ON + run: | + COMPILER_LAUNCHER="" \ + C_COMPILER=$(which clang) \ + CXX_COMPILER=$(which clang++) \ + LLVM_DIR="$(pwd)/mlir/llvm-project" \ + ENABLE_ASAN=OFF \ + ENABLE_TRANSPORT=ON \ + make runtime + + - name: Run transport test suite (Catch2 + loopback) + run: | + COMPILER_LAUNCHER="" \ + C_COMPILER=$(which clang) \ + CXX_COMPILER=$(which clang++) \ + LLVM_DIR="$(pwd)/mlir/llvm-project" \ + make test-runtime-transport + + - name: Upload loopback logs on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: transport-loopback-logs + path: | + /tmp/cvl_coproc.log + /tmp/cvl_ctrl.log + if-no-files-found: ignore + retention-days: 7 diff --git a/Makefile b/Makefile index 7f7062b5b2..5462dc4215 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ ENZYME_BUILD_DIR ?= $(MK_DIR)/mlir/Enzyme/build COVERAGE_REPORT ?= term-missing ENABLE_OPENQASM ?= ON ENABLE_OQD ?= OFF +ENABLE_TRANSPORT ?= OFF TEST_BACKEND ?= "lightning.qubit" TEST_BRAKET ?= NONE ENABLE_ASAN ?= OFF @@ -150,12 +151,12 @@ dialect-docs: $(MAKE) -C mlir dialect-docs runtime: - $(MAKE) -C runtime runtime ENABLE_OQD=$(ENABLE_OQD) + $(MAKE) -C runtime runtime ENABLE_OQD=$(ENABLE_OQD) ENABLE_TRANSPORT=$(ENABLE_TRANSPORT) oqc: $(MAKE) -C frontend/catalyst/third_party/oqc/src oqc -.PHONY: test test-runtime test-frontend lit pytest test-demos test-oqc test-toml-spec +.PHONY: test test-runtime test-runtime-transport test-frontend lit pytest test-demos test-oqc test-toml-spec test: test-runtime test-frontend test-demos test-toml-spec: @@ -164,6 +165,9 @@ test-toml-spec: test-runtime: $(MAKE) -C runtime test +test-runtime-transport: + $(MAKE) -C runtime test-transport + test-mlir: $(MAKE) -C mlir test diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 8f07c91366..c8d9f7809e 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -14,6 +14,15 @@ * A new runtime transport layer for remote/local executors is introduced. [(#3043)](https://github.com/PennyLaneAI/catalyst/pull/3043) + [(#3045)](https://github.com/PennyLaneAI/catalyst/pull/3045) + +* A new `Transport` MLIR dialect is added, providing typed ops for driving a transport session's + lifecycle at the IR level. + [(#3047)](https://github.com/PennyLaneAI/catalyst/pull/3047) + +* A `convert-transport-to-llvm` pass is added, lowering the `Transport` dialect ops to the + transport runtime CAPI. + [(#3048)](https://github.com/PennyLaneAI/catalyst/pull/3048) * A new remote/local executor infrastructure has been added to Catalyst, enabling qnode kernels to be dispatched to a separate executor process. `executor` dialect models the session lifecycle @@ -306,6 +315,9 @@ * The `/benchmark` GitHub comment trigger can now accept additional arguments and has been renamed to `!benchmark`. [(#2947)](https://github.com/PennyLaneAI/catalyst/pull/2947) +* Added CI checks for the runtime `cpu_verbs` transport backend via Soft-RoCE. + [(#3074)](https://github.com/PennyLaneAI/catalyst/pull/3074) + * The frontend now generates MLIR in reference semantics when capture is enabled. [(#2663)](https://github.com/PennyLaneAI/catalyst/pull/2663) [(#2664)](https://github.com/PennyLaneAI/catalyst/pull/2664) diff --git a/mlir/include/CMakeLists.txt b/mlir/include/CMakeLists.txt index dc6069430e..828da1274a 100644 --- a/mlir/include/CMakeLists.txt +++ b/mlir/include/CMakeLists.txt @@ -12,4 +12,5 @@ add_subdirectory(Quantum) add_subdirectory(QRef) add_subdirectory(Executor) add_subdirectory(RTIO) +add_subdirectory(Transport) add_subdirectory(Test) diff --git a/mlir/include/RegisterAllPasses.h b/mlir/include/RegisterAllPasses.h index 6fa7c5d7f6..71879efeb9 100644 --- a/mlir/include/RegisterAllPasses.h +++ b/mlir/include/RegisterAllPasses.h @@ -29,6 +29,7 @@ #include "hlo-extensions/Transforms/Passes.h" #include "Executor/Transforms/Passes.h" +#include "Transport/Transforms/Passes.h" namespace catalyst { @@ -47,6 +48,7 @@ inline void registerAllPasses() quantum::registerQuantumPasses(); executor::registerExecutorPasses(); rtio::registerRTIOPasses(); + transport::registerTransportPasses(); test::registerTestPasses(); } diff --git a/mlir/include/Transport/CMakeLists.txt b/mlir/include/Transport/CMakeLists.txt new file mode 100644 index 0000000000..9f57627c32 --- /dev/null +++ b/mlir/include/Transport/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(IR) +add_subdirectory(Transforms) diff --git a/mlir/include/Transport/IR/CMakeLists.txt b/mlir/include/Transport/IR/CMakeLists.txt new file mode 100644 index 0000000000..d2b9081dc0 --- /dev/null +++ b/mlir/include/Transport/IR/CMakeLists.txt @@ -0,0 +1,8 @@ +add_mlir_dialect(TransportOps transport) +add_mlir_doc(TransportDialect TransportDialect Transport/ -gen-dialect-doc) +add_mlir_doc(TransportOps TransportOps Transport/ -gen-op-doc) + +set(LLVM_TARGET_DEFINITIONS TransportOps.td) +mlir_tablegen(TransportEnums.h.inc -gen-enum-decls) +mlir_tablegen(TransportEnums.cpp.inc -gen-enum-defs) +add_public_tablegen_target(MLIRTransportEnumsIncGen) diff --git a/mlir/include/Transport/IR/TransportDialect.h b/mlir/include/Transport/IR/TransportDialect.h new file mode 100644 index 0000000000..675578a74c --- /dev/null +++ b/mlir/include/Transport/IR/TransportDialect.h @@ -0,0 +1,25 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "mlir/Bytecode/BytecodeOpInterface.h" +#include "mlir/IR/Dialect.h" +#include "mlir/IR/OpDefinition.h" + +#include "Transport/IR/TransportEnums.h.inc" +#include "Transport/IR/TransportOpsDialect.h.inc" + +#define GET_TYPEDEF_CLASSES +#include "Transport/IR/TransportOpsTypes.h.inc" diff --git a/mlir/include/Transport/IR/TransportDialect.td b/mlir/include/Transport/IR/TransportDialect.td new file mode 100644 index 0000000000..bf090f81dd --- /dev/null +++ b/mlir/include/Transport/IR/TransportDialect.td @@ -0,0 +1,97 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef TRANSPORT_DIALECT +#define TRANSPORT_DIALECT + +include "mlir/IR/OpBase.td" +include "mlir/IR/DialectBase.td" +include "mlir/IR/AttrTypeBase.td" +include "mlir/IR/EnumAttr.td" +include "mlir/Interfaces/SideEffectInterfaces.td" + +//===----------------------------------------------------------------------===// +// Transport dialect definition. +//===----------------------------------------------------------------------===// + +def Transport_Dialect : Dialect { + let summary = "Typed ops for setting up and driving a transport session."; + let description = [{ + The transport dialect models a connection-oriented data-movement session + between two endpoints: creating a session, bringing up the connection, + exchanging memory handles, establishing a data path, and running rounds + of request/reply traffic until teardown. + }]; + + let name = "transport"; + let cppNamespace = "::catalyst::transport"; + let useDefaultTypePrinterParser = 1; + let usePropertiesForAttributes = 1; +} + +//===----------------------------------------------------------------------===// +// Enums. +//===----------------------------------------------------------------------===// + +def Transport_Role : I32EnumAttr<"Role", "transport session role", [ + I32EnumAttrCase<"Controller", 0, "controller">, + I32EnumAttrCase<"Coprocessor", 1, "coprocessor"> + ]> { + let cppNamespace = "::catalyst::transport"; +} + + +//===----------------------------------------------------------------------===// +// Types. +//===----------------------------------------------------------------------===// + +class Transport_Type traits = []> + : TypeDef { + let mnemonic = typeMnemonic; +} + +// Opaque session handle, parameterized by its role. The role is a compile-time tag +// that selects which role-specific ops the session may take part in. +def Transport_SessionType : Transport_Type<"Session", "session"> { + let summary = "An opaque transport session handle, tagged with its role."; + let parameters = (ins EnumParameter:$role); + let assemblyFormat = "`<` $role `>`"; +} + +def Transport_TokenType : Transport_Type<"Token", "token"> { + let summary = "A handle to an in-flight asynchronous step, awaited with transport.barrier."; +} + +// Role-constrained session types used by the role-specific ops. +def Transport_ControllerSession : Type< + CPred<"::llvm::isa<::catalyst::transport::SessionType>($_self) && " + "::llvm::cast<::catalyst::transport::SessionType>($_self).getRole() == " + "::catalyst::transport::Role::Controller">, + "controller transport session">; + +def Transport_CoprocessorSession : Type< + CPred<"::llvm::isa<::catalyst::transport::SessionType>($_self) && " + "::llvm::cast<::catalyst::transport::SessionType>($_self).getRole() == " + "::catalyst::transport::Role::Coprocessor">, + "coprocessor transport session">; + +//===----------------------------------------------------------------------===// +// Operation base. +//===----------------------------------------------------------------------===// + +// All transport ops perform side-effecting I/O +class Transport_Op traits = []> : + Op])>; + +#endif // TRANSPORT_DIALECT diff --git a/mlir/include/Transport/IR/TransportOps.h b/mlir/include/Transport/IR/TransportOps.h new file mode 100644 index 0000000000..0cc3624b7b --- /dev/null +++ b/mlir/include/Transport/IR/TransportOps.h @@ -0,0 +1,26 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "mlir/Bytecode/BytecodeOpInterface.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" + +#include "Transport/IR/TransportDialect.h" + +#define GET_OP_CLASSES +#include "Transport/IR/TransportOps.h.inc" diff --git a/mlir/include/Transport/IR/TransportOps.td b/mlir/include/Transport/IR/TransportOps.td new file mode 100644 index 0000000000..21dda4b077 --- /dev/null +++ b/mlir/include/Transport/IR/TransportOps.td @@ -0,0 +1,186 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef TRANSPORT_OPS +#define TRANSPORT_OPS + +include "mlir/IR/OpBase.td" +include "mlir/IR/BuiltinAttributes.td" +include "mlir/IR/CommonTypeConstraints.td" +include "mlir/Interfaces/SideEffectInterfaces.td" +include "Transport/IR/TransportDialect.td" + +// A round's payload or reply buffer: a 1-D memref (buffer form) or tensor (value form). +def Transport_Buffer : MemRefRankOf<[I1, I8, I16, I32, I64, Index], [1]>; +def Transport_TensorBuffer : 1DTensorOf<[I1, I8, I16, I32, I64, Index]>; +def Transport_AnyBuffer : AnyTypeOf<[Transport_Buffer, Transport_TensorBuffer]>; + +//===----------------------------------------------------------------------===// +// Session creation +//===----------------------------------------------------------------------===// + +def Transport_CreateOp : Transport_Op<"create"> { + let summary = "Create a transport session for a role."; + let description = [{ + Creates a session on the backend selected by `backend_lib` and `config`; the + result type's role picks controller or coprocessor. When `key` is set, the + session is registered under it so `transport.get_session` can resolve it from + another function. + }]; + let arguments = (ins StrAttr:$backend_lib, StrAttr:$config, + DefaultValuedStrAttr:$key); + let results = (outs Transport_SessionType:$session); + let assemblyFormat = "attr-dict `->` qualified(type($session))"; +} + +//===----------------------------------------------------------------------===// +// Connection bring-up +//===----------------------------------------------------------------------===// + +def Transport_ConnectOp : Transport_Op<"connect"> { + let summary = "Connect to the peer (blocking)."; + let arguments = (ins Transport_SessionType:$session, StrAttr:$peer, I16Attr:$oob_port); + let assemblyFormat = "$session attr-dict `:` qualified(type($session))"; +} + +def Transport_ConnectAsyncOp : Transport_Op<"connect_async"> { + let summary = "Non-blocking connect; await the token with transport.barrier."; + let arguments = (ins Transport_SessionType:$session, StrAttr:$peer, I16Attr:$oob_port); + let results = (outs Transport_TokenType:$token); + let assemblyFormat = "$session attr-dict `:` qualified(type($session)) `->` type($token)"; +} + +def Transport_ExchangeKeysOp : Transport_Op<"exchange_keys"> { + let summary = "Exchange memory-region handles with the peer (blocking)."; + let arguments = (ins Transport_SessionType:$session); + let assemblyFormat = "$session attr-dict `:` qualified(type($session))"; +} + +def Transport_ExchangeKeysAsyncOp : Transport_Op<"exchange_keys_async"> { + let summary = "Non-blocking exchange_keys; await the token with transport.barrier."; + let arguments = (ins Transport_SessionType:$session); + let results = (outs Transport_TokenType:$token); + let assemblyFormat = "$session attr-dict `:` qualified(type($session)) `->` type($token)"; +} + +def Transport_BarrierOp : Transport_Op<"barrier"> { + let summary = "Await an asynchronous bring-up step."; + let arguments = (ins Transport_TokenType:$token); + let assemblyFormat = "$token attr-dict `:` type($token)"; +} + +def Transport_EstablishChannelOp : Transport_Op<"establish_channel"> { + let summary = "Arm the data channel for the given data path."; + let description = [{ + Prepares the data channel selected by `data_path` for the rounds that follow. + Runs after `exchange_keys` and before `kick`/`collect`. + }]; + let arguments = (ins Transport_SessionType:$session, StrAttr:$data_path); + let assemblyFormat = "$session $data_path attr-dict `:` qualified(type($session))"; +} + +//===----------------------------------------------------------------------===// +// Round setup +//===----------------------------------------------------------------------===// + +def Transport_CommitWorkItemOp : Transport_Op<"commit_work_item"> { + let summary = "Declare a round's request/reply byte sizes (controller)."; + let arguments = (ins Transport_ControllerSession:$session, I32Attr:$work_item_idx, + I64Attr:$in_bytes, I64Attr:$out_bytes); + let assemblyFormat = "$session attr-dict `:` qualified(type($session))"; +} + +def Transport_SetCoprocessorFnOp : Transport_Op<"set_coprocessor_fn"> { + let summary = "Bind the function run per received message (coprocessor)."; + let description = [{ + Binds the function the coprocessor applies to each received message before + replying. `symbol` is the name the coprocessor resolves and invokes. + }]; + let arguments = (ins Transport_CoprocessorSession:$session, + DefaultValuedStrAttr:$symbol); + let assemblyFormat = "$session attr-dict `:` qualified(type($session))"; +} + +//===----------------------------------------------------------------------===// +// Execution +//===----------------------------------------------------------------------===// + +def Transport_StartOp : Transport_Op<"start"> { + let summary = "Start the session; runs until stop (non-blocking)."; + let arguments = (ins Transport_SessionType:$session); + let assemblyFormat = "$session attr-dict `:` qualified(type($session))"; +} + +def Transport_KickOp : Transport_Op<"kick"> { + let summary = "Send a payload as one round's request (controller)."; + let arguments = (ins Transport_ControllerSession:$session, Transport_AnyBuffer:$payload, + I32Attr:$work_item_idx); + let assemblyFormat = "$session `,` $payload attr-dict `:` qualified(type($session)) `,` type($payload)"; +} + +def Transport_CollectOp : Transport_Op<"collect"> { + let summary = "Receive the current round's reply."; + let description = [{ + Receives the current round's reply in one of two forms: a value form that + returns it as a tensor `$result`, or a destination-passing form that writes + it into the `$dest` buffer. The buffer shape gives the expected reply size. + }]; + let arguments = (ins Transport_SessionType:$session, Optional:$dest); + let results = (outs Optional:$result); + let assemblyFormat = [{ + $session (`,` $dest^)? attr-dict `:` qualified(type($session)) + (`,` type($dest)^)? (`->` type($result)^)? + }]; +} + +def Transport_LastRttNsOp : Transport_Op<"last_rtt_ns"> { + let summary = "Round-trip time of the last round, in nanoseconds."; + let arguments = (ins Transport_SessionType:$session); + let results = (outs I64:$rtt_ns); + let assemblyFormat = "$session attr-dict `:` qualified(type($session)) `->` type($rtt_ns)"; +} + +//===----------------------------------------------------------------------===// +// Teardown +//===----------------------------------------------------------------------===// + +def Transport_StopOp : Transport_Op<"stop"> { + let summary = "Stop the session (idempotent)."; + let arguments = (ins Transport_SessionType:$session); + let assemblyFormat = "$session attr-dict `:` qualified(type($session))"; +} + +def Transport_DestroyOp : Transport_Op<"destroy"> { + let summary = "Destroy the session and release it."; + let arguments = (ins Transport_SessionType:$session); + let assemblyFormat = "$session attr-dict `:` qualified(type($session))"; +} + +//===----------------------------------------------------------------------===// +// Session resolution +//===----------------------------------------------------------------------===// + +def Transport_GetSessionOp : Transport_Op<"get_session"> { + let summary = "Resolve a session by role and key."; + let description = [{ + Returns the session that `transport.create` registered under (role, `key`), + where the role is taken from the result type. Lets a session created in one + function be used in another without passing it as a value. + }]; + let arguments = (ins DefaultValuedStrAttr:$key); + let results = (outs Transport_SessionType:$session); + let assemblyFormat = "attr-dict `:` qualified(type($session))"; +} + +#endif // TRANSPORT_OPS diff --git a/mlir/include/Transport/Transforms/CMakeLists.txt b/mlir/include/Transport/Transforms/CMakeLists.txt new file mode 100644 index 0000000000..fb92bac94b --- /dev/null +++ b/mlir/include/Transport/Transforms/CMakeLists.txt @@ -0,0 +1,4 @@ +set(LLVM_TARGET_DEFINITIONS Passes.td) +mlir_tablegen(Passes.h.inc -gen-pass-decls -name Transport) +add_public_tablegen_target(MLIRTransportPassIncGen) +add_mlir_doc(Passes TransportPasses ./ -gen-pass-doc) diff --git a/mlir/include/Transport/Transforms/Passes.h b/mlir/include/Transport/Transforms/Passes.h new file mode 100644 index 0000000000..78d1d1c464 --- /dev/null +++ b/mlir/include/Transport/Transforms/Passes.h @@ -0,0 +1,30 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "mlir/Pass/Pass.h" + +#include "Transport/IR/TransportDialect.h" +#include "Transport/IR/TransportOps.h" + +namespace catalyst { +namespace transport { + +#define GEN_PASS_DECL +#define GEN_PASS_REGISTRATION +#include "Transport/Transforms/Passes.h.inc" + +} // namespace transport +} // namespace catalyst diff --git a/mlir/include/Transport/Transforms/Passes.td b/mlir/include/Transport/Transforms/Passes.td new file mode 100644 index 0000000000..a33e9bd80e --- /dev/null +++ b/mlir/include/Transport/Transforms/Passes.td @@ -0,0 +1,32 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef TRANSPORT_PASSES +#define TRANSPORT_PASSES + +include "mlir/Pass/PassBase.td" + +def ConvertTransportToLLVMPass : Pass<"convert-transport-to-llvm", "mlir::ModuleOp"> { + let summary = "Lower the transport dialect to LLVM dialect with runtime calls."; + let description = [{ + Lowers each `transport` op to an `llvm.call` on the matching + `__catalyst__transport__*` symbol. + }]; + + let dependentDialects = [ + "mlir::LLVM::LLVMDialect" + ]; +} + +#endif // TRANSPORT_PASSES diff --git a/mlir/lib/CMakeLists.txt b/mlir/lib/CMakeLists.txt index eb4eecbef8..162c23c90f 100644 --- a/mlir/lib/CMakeLists.txt +++ b/mlir/lib/CMakeLists.txt @@ -14,4 +14,5 @@ add_subdirectory(QRef) add_subdirectory(Quantum) add_subdirectory(Executor) add_subdirectory(RTIO) +add_subdirectory(Transport) add_subdirectory(Test) diff --git a/mlir/lib/Driver/CMakeLists.txt b/mlir/lib/Driver/CMakeLists.txt index ef518d378e..7334e6f218 100644 --- a/mlir/lib/Driver/CMakeLists.txt +++ b/mlir/lib/Driver/CMakeLists.txt @@ -76,6 +76,8 @@ set(LIBS ion-transforms MLIRRTIO rtio-transforms + MLIRTransport + transport-transforms MLIRExecutor executor-transforms MLIRCatalystTest diff --git a/mlir/lib/Driver/CompilerDriver.cpp b/mlir/lib/Driver/CompilerDriver.cpp index f781e5bfb5..373c8edc57 100644 --- a/mlir/lib/Driver/CompilerDriver.cpp +++ b/mlir/lib/Driver/CompilerDriver.cpp @@ -90,6 +90,7 @@ #include "RegisterAllPasses.h" #include "Executor/IR/ExecutorDialect.h" +#include "Transport/IR/TransportDialect.h" using namespace mlir; using namespace catalyst; @@ -183,6 +184,7 @@ void registerAllCatalystDialects(DialectRegistry ®istry) registry.insert(); registry.insert(); registry.insert(); + registry.insert(); registry.insert(); registry.insert(); registry.insert(); diff --git a/mlir/lib/Transport/CMakeLists.txt b/mlir/lib/Transport/CMakeLists.txt new file mode 100644 index 0000000000..9f57627c32 --- /dev/null +++ b/mlir/lib/Transport/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(IR) +add_subdirectory(Transforms) diff --git a/mlir/lib/Transport/IR/CMakeLists.txt b/mlir/lib/Transport/IR/CMakeLists.txt new file mode 100644 index 0000000000..1dbeed05e2 --- /dev/null +++ b/mlir/lib/Transport/IR/CMakeLists.txt @@ -0,0 +1,14 @@ +add_mlir_library(MLIRTransport + TransportDialect.cpp + TransportOps.cpp + + ADDITIONAL_HEADER_DIRS + ${PROJECT_SOURCE_DIR}/include/Transport + + DEPENDS + MLIRTransportOpsIncGen + MLIRTransportEnumsIncGen + + LINK_LIBS PRIVATE + MLIRLLVMDialect +) diff --git a/mlir/lib/Transport/IR/TransportDialect.cpp b/mlir/lib/Transport/IR/TransportDialect.cpp new file mode 100644 index 0000000000..c2c41d2800 --- /dev/null +++ b/mlir/lib/Transport/IR/TransportDialect.cpp @@ -0,0 +1,51 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "Transport/IR/TransportDialect.h" + +#include "llvm/ADT/TypeSwitch.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/DialectImplementation.h" + +#include "Transport/IR/TransportOps.h" + +using namespace mlir; +using namespace catalyst::transport; + +//===----------------------------------------------------------------------===// +// Transport dialect definitions. +//===----------------------------------------------------------------------===// + +#include "Transport/IR/TransportEnums.cpp.inc" +#include "Transport/IR/TransportOpsDialect.cpp.inc" + +//===----------------------------------------------------------------------===// +// Transport type definitions. +//===----------------------------------------------------------------------===// + +#define GET_TYPEDEF_CLASSES +#include "Transport/IR/TransportOpsTypes.cpp.inc" + +void TransportDialect::initialize() +{ + addTypes< +#define GET_TYPEDEF_LIST +#include "Transport/IR/TransportOpsTypes.cpp.inc" + >(); + + addOperations< +#define GET_OP_LIST +#include "Transport/IR/TransportOps.cpp.inc" + >(); +} diff --git a/mlir/lib/Transport/IR/TransportOps.cpp b/mlir/lib/Transport/IR/TransportOps.cpp new file mode 100644 index 0000000000..8d894ef69d --- /dev/null +++ b/mlir/lib/Transport/IR/TransportOps.cpp @@ -0,0 +1,24 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "Transport/IR/TransportOps.h" + +#include "mlir/IR/Builders.h" +#include "mlir/IR/OpImplementation.h" + +using namespace mlir; +using namespace catalyst::transport; + +#define GET_OP_CLASSES +#include "Transport/IR/TransportOps.cpp.inc" diff --git a/mlir/lib/Transport/Transforms/CMakeLists.txt b/mlir/lib/Transport/Transforms/CMakeLists.txt new file mode 100644 index 0000000000..5497e20f8c --- /dev/null +++ b/mlir/lib/Transport/Transforms/CMakeLists.txt @@ -0,0 +1,27 @@ +set(LIBRARY_NAME transport-transforms) + +file(GLOB SRC + TransportToLLVM.cpp +) + +get_property(dialect_libs GLOBAL PROPERTY MLIR_DIALECT_LIBS) +get_property(conversion_libs GLOBAL PROPERTY MLIR_CONVERSION_LIBS) +set(LIBS + ${dialect_libs} + ${conversion_libs} + MLIRTransport +) + +set(DEPENDS + MLIRTransportPassIncGen + MLIRTransportEnumsIncGen +) + +add_mlir_library(${LIBRARY_NAME} STATIC ${SRC} LINK_LIBS PRIVATE ${LIBS} DEPENDS ${DEPENDS}) +target_compile_features(${LIBRARY_NAME} PUBLIC cxx_std_20) +target_include_directories(${LIBRARY_NAME} PUBLIC + $ + $ + $ + $ +) diff --git a/mlir/lib/Transport/Transforms/TransportToLLVM.cpp b/mlir/lib/Transport/Transforms/TransportToLLVM.cpp new file mode 100644 index 0000000000..ce5e1698a2 --- /dev/null +++ b/mlir/lib/Transport/Transforms/TransportToLLVM.cpp @@ -0,0 +1,345 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Lower the `transport` dialect to `llvm.call`s on the __catalyst__transport__* +// CAPI (runtime/include/TransportCAPI.h). + +#include "llvm/ADT/Twine.h" +#include "mlir/Conversion/LLVMCommon/TypeConverter.h" +#include "mlir/Dialect/LLVMIR/FunctionCallUtils.h" +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Transforms/DialectConversion.h" + +#include "Transport/IR/TransportOps.h" +#include "Transport/Transforms/Passes.h" + +using namespace mlir; +using namespace catalyst::transport; + +namespace catalyst { +namespace transport { + +#define GEN_PASS_DEF_CONVERTTRANSPORTTOLLVMPASS +#include "Transport/Transforms/Passes.h.inc" + +namespace { + +LLVM::LLVMPointerType ptrTy(MLIRContext *ctx) { return LLVM::LLVMPointerType::get(ctx); } +IntegerType i32Ty(MLIRContext *ctx) { return IntegerType::get(ctx, 32); } +IntegerType i64Ty(MLIRContext *ctx) { return IntegerType::get(ctx, 64); } + +ModuleOp moduleOf(Operation *op) { return op->getParentOfType(); } + +Value emitCall(ConversionPatternRewriter &rewriter, Location loc, ModuleOp mod, StringRef name, + ArrayRef paramTys, Type resultTy, ValueRange args) +{ + Type rty = resultTy ? resultTy : LLVM::LLVMVoidType::get(rewriter.getContext()); + auto fn = LLVM::lookupOrCreateFn(rewriter, mod, name, paramTys, rty); + assert(succeeded(fn) && "failed to declare transport CAPI function"); + auto call = LLVM::CallOp::create(rewriter, loc, *fn, args); + return call.getNumResults() ? call.getResult() : Value(); +} + +Value globalStr(ConversionPatternRewriter &rewriter, Location loc, ModuleOp mod, StringRef prefix, + StringRef value) +{ + static int counter = 0; + std::string symName = (prefix + Twine(counter++)).str(); + return LLVM::createGlobalString(loc, rewriter, symName, Twine(value).concat(Twine('\0')).str(), + LLVM::Linkage::Internal); +} + +Value constInt(ConversionPatternRewriter &rewriter, Location loc, Type ty, int64_t v) +{ + return LLVM::ConstantOp::create(rewriter, loc, ty, rewriter.getIntegerAttr(ty, v)); +} + +// From a lowered 1-D memref descriptor (an LLVM struct), extract the aligned data +// pointer and the buffer's size in bytes (num elements * element byte width). +std::pair memrefPtrAndBytes(ConversionPatternRewriter &rewriter, Location loc, + Value descriptor, MemRefType memTy) +{ + Value ptr = LLVM::ExtractValueOp::create(rewriter, loc, descriptor, ArrayRef{1}); + Value nelem = LLVM::ExtractValueOp::create(rewriter, loc, descriptor, ArrayRef{3, 0}); + Type elemTy = memTy.getElementType(); + int64_t elemBytes = isa(elemTy) ? 8 : (elemTy.getIntOrFloatBitWidth() + 7) / 8; + Value ebytes = constInt(rewriter, loc, i64Ty(rewriter.getContext()), elemBytes); + Value bytes = LLVM::MulOp::create(rewriter, loc, nelem, ebytes); + return {ptr, bytes}; +} + +//===----------------------------------------------------------------------===// +// Patterns +//===----------------------------------------------------------------------===// + +struct CreateLowering : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + LogicalResult matchAndRewrite(CreateOp op, OpAdaptor, + ConversionPatternRewriter &rewriter) const override + { + auto *ctx = op.getContext(); + ModuleOp mod = moduleOf(op); + auto sessTy = cast(op.getSession().getType()); + Value lib = globalStr(rewriter, op.getLoc(), mod, "transport_backend_", op.getBackendLib()); + Value cfg = globalStr(rewriter, op.getLoc(), mod, "transport_config_", op.getConfig()); + Value key = globalStr(rewriter, op.getLoc(), mod, "transport_key_", op.getKey()); + Value role = + constInt(rewriter, op.getLoc(), i32Ty(ctx), static_cast(sessTy.getRole())); + Value s = emitCall(rewriter, op.getLoc(), mod, "__catalyst__transport__create", + {ptrTy(ctx), ptrTy(ctx), i32Ty(ctx), ptrTy(ctx)}, ptrTy(ctx), + {lib, cfg, role, key}); + rewriter.replaceOp(op, s); + return success(); + } +}; + +template struct ConnectLoweringBase : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + LogicalResult matchAndRewrite(OpT op, typename OpT::Adaptor adaptor, + ConversionPatternRewriter &rewriter) const override + { + auto *ctx = op.getContext(); + ModuleOp mod = op->template getParentOfType(); + Value peer = globalStr(rewriter, op.getLoc(), mod, "transport_peer_", op.getPeer()); + Value port = constInt(rewriter, op.getLoc(), IntegerType::get(ctx, 16), op.getOobPort()); + if (Async) { + Value r = emitCall(rewriter, op.getLoc(), mod, "__catalyst__transport__connect_async", + {ptrTy(ctx), ptrTy(ctx), IntegerType::get(ctx, 16)}, i64Ty(ctx), + {adaptor.getSession(), peer, port}); + rewriter.replaceOp(op, r); + } + else { + emitCall(rewriter, op.getLoc(), mod, "__catalyst__transport__connect", + {ptrTy(ctx), ptrTy(ctx), IntegerType::get(ctx, 16)}, i32Ty(ctx), + {adaptor.getSession(), peer, port}); + rewriter.eraseOp(op); + } + return success(); + } +}; +using ConnectLowering = ConnectLoweringBase; +using ConnectAsyncLowering = ConnectLoweringBase; + +template +struct ExchangeKeysLoweringBase : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + LogicalResult matchAndRewrite(OpT op, typename OpT::Adaptor adaptor, + ConversionPatternRewriter &rewriter) const override + { + auto *ctx = op.getContext(); + ModuleOp mod = op->template getParentOfType(); + if (Async) { + Value r = + emitCall(rewriter, op.getLoc(), mod, "__catalyst__transport__exchange_keys_async", + {ptrTy(ctx)}, i64Ty(ctx), {adaptor.getSession()}); + rewriter.replaceOp(op, r); + } + else { + emitCall(rewriter, op.getLoc(), mod, "__catalyst__transport__exchange_keys", + {ptrTy(ctx)}, i32Ty(ctx), {adaptor.getSession()}); + rewriter.eraseOp(op); + } + return success(); + } +}; +using ExchangeKeysLowering = ExchangeKeysLoweringBase; +using ExchangeKeysAsyncLowering = ExchangeKeysLoweringBase; + +struct BarrierLowering : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + LogicalResult matchAndRewrite(BarrierOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override + { + auto *ctx = op.getContext(); + emitCall(rewriter, op.getLoc(), moduleOf(op), "__catalyst__transport__barrier", + {i64Ty(ctx)}, i32Ty(ctx), {adaptor.getToken()}); + rewriter.eraseOp(op); + return success(); + } +}; + +struct EstablishChannelLowering : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + LogicalResult matchAndRewrite(EstablishChannelOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override + { + auto *ctx = op.getContext(); + Value dp = globalStr(rewriter, op.getLoc(), moduleOf(op), "transport_data_path_", + op.getDataPath()); + emitCall(rewriter, op.getLoc(), moduleOf(op), "__catalyst__transport__establish_channel", + {ptrTy(ctx), ptrTy(ctx)}, i32Ty(ctx), {adaptor.getSession(), dp}); + rewriter.eraseOp(op); + return success(); + } +}; + +struct SetCoprocessorFnLowering : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + LogicalResult matchAndRewrite(SetCoprocessorFnOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override + { + auto *ctx = op.getContext(); + ModuleOp mod = moduleOf(op); + Value sym = globalStr(rewriter, op.getLoc(), mod, "transport_coproc_fn_", op.getSymbol()); + emitCall(rewriter, op.getLoc(), mod, "__catalyst__transport__set_coprocessor_fn", + {ptrTy(ctx), ptrTy(ctx)}, i32Ty(ctx), {adaptor.getSession(), sym}); + rewriter.eraseOp(op); + return success(); + } +}; + +struct CommitWorkItemLowering : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + LogicalResult matchAndRewrite(CommitWorkItemOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override + { + auto *ctx = op.getContext(); + Value idx = constInt(rewriter, op.getLoc(), i32Ty(ctx), op.getWorkItemIdx()); + Value inB = constInt(rewriter, op.getLoc(), i64Ty(ctx), op.getInBytes()); + Value outB = constInt(rewriter, op.getLoc(), i64Ty(ctx), op.getOutBytes()); + emitCall(rewriter, op.getLoc(), moduleOf(op), "__catalyst__transport__commit_work_item", + {ptrTy(ctx), i32Ty(ctx), i64Ty(ctx), i64Ty(ctx)}, i32Ty(ctx), + {adaptor.getSession(), idx, inB, outB}); + rewriter.eraseOp(op); + return success(); + } +}; + +struct KickLowering : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + LogicalResult matchAndRewrite(KickOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override + { + auto *ctx = op.getContext(); + ModuleOp mod = moduleOf(op); + auto memTy = dyn_cast(op.getPayload().getType()); + if (!memTy) + return rewriter.notifyMatchFailure(op, "kick payload must be bufferized (memref)"); + auto [srcPtr, bytes] = + memrefPtrAndBytes(rewriter, op.getLoc(), adaptor.getPayload(), memTy); + Value slot = emitCall(rewriter, op.getLoc(), mod, "__catalyst__transport__data_slot", + {ptrTy(ctx)}, ptrTy(ctx), {adaptor.getSession()}); + LLVM::MemcpyOp::create(rewriter, op.getLoc(), slot, srcPtr, bytes, /*isVolatile=*/false); + Value idx = constInt(rewriter, op.getLoc(), i32Ty(ctx), op.getWorkItemIdx()); + emitCall(rewriter, op.getLoc(), mod, "__catalyst__transport__kick", + {ptrTy(ctx), i32Ty(ctx)}, i32Ty(ctx), {adaptor.getSession(), idx}); + rewriter.eraseOp(op); + return success(); + } +}; + +struct CollectLowering : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + LogicalResult matchAndRewrite(CollectOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override + { + auto *ctx = op.getContext(); + ModuleOp mod = moduleOf(op); + if (!op.getDest()) + return rewriter.notifyMatchFailure(op, + "collect must be bufferized (dest-passing form)"); + auto memTy = cast(op.getDest().getType()); + auto [dstPtr, bytes] = memrefPtrAndBytes(rewriter, op.getLoc(), adaptor.getDest(), memTy); + emitCall(rewriter, op.getLoc(), mod, "__catalyst__transport__collect", + {ptrTy(ctx), ptrTy(ctx), i64Ty(ctx)}, i32Ty(ctx), + {adaptor.getSession(), dstPtr, bytes}); + rewriter.eraseOp(op); + return success(); + } +}; + +struct LastRttLowering : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + LogicalResult matchAndRewrite(LastRttNsOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override + { + Value r = + emitCall(rewriter, op.getLoc(), moduleOf(op), "__catalyst__transport__last_rtt_ns", + {ptrTy(op.getContext())}, i64Ty(op.getContext()), {adaptor.getSession()}); + rewriter.replaceOp(op, r); + return success(); + } +}; + +// Void-returning single-session ops: start / stop / close / destroy. +template struct VoidSessionLowering : public OpConversionPattern { + VoidSessionLowering(const TypeConverter &tc, MLIRContext *ctx, StringRef sym) + : OpConversionPattern(tc, ctx), symbol(sym) + { + } + LogicalResult matchAndRewrite(OpT op, typename OpT::Adaptor adaptor, + ConversionPatternRewriter &rewriter) const override + { + emitCall(rewriter, op.getLoc(), op->template getParentOfType(), symbol, + {ptrTy(op.getContext())}, Type(), {adaptor.getSession()}); + rewriter.eraseOp(op); + return success(); + } + std::string symbol; +}; + +// get_session: look the session up by role from the runtime registry (populated at create). +struct GetSessionLowering : public OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + LogicalResult matchAndRewrite(GetSessionOp op, OpAdaptor, + ConversionPatternRewriter &rewriter) const override + { + auto *ctx = op.getContext(); + ModuleOp mod = moduleOf(op); + auto sessTy = cast(op.getSession().getType()); + Value role = + constInt(rewriter, op.getLoc(), i32Ty(ctx), static_cast(sessTy.getRole())); + Value key = globalStr(rewriter, op.getLoc(), mod, "transport_key_", op.getKey()); + Value s = emitCall(rewriter, op.getLoc(), mod, "__catalyst__transport__get_session", + {i32Ty(ctx), ptrTy(ctx)}, ptrTy(ctx), {role, key}); + rewriter.replaceOp(op, s); + return success(); + } +}; + +} // namespace + +struct ConvertTransportToLLVMPass + : public impl::ConvertTransportToLLVMPassBase { + using ConvertTransportToLLVMPassBase::ConvertTransportToLLVMPassBase; + + void runOnOperation() override + { + MLIRContext *ctx = &getContext(); + LLVMTypeConverter tc(ctx); + tc.addConversion([ctx](SessionType) -> Type { return LLVM::LLVMPointerType::get(ctx); }); + tc.addConversion([ctx](TokenType) -> Type { return IntegerType::get(ctx, 64); }); + + RewritePatternSet patterns(ctx); + patterns.add(tc, ctx); + patterns.add>(tc, ctx, "__catalyst__transport__start"); + patterns.add>(tc, ctx, "__catalyst__transport__stop"); + patterns.add>(tc, ctx, "__catalyst__transport__destroy"); + + ConversionTarget target(*ctx); + target.addLegalDialect(); + target.addIllegalDialect(); + + if (failed(applyPartialConversion(getOperation(), target, std::move(patterns)))) + signalPassFailure(); + } +}; + +} // namespace transport +} // namespace catalyst diff --git a/mlir/test/Transport/ConvertTransportToLLVM.mlir b/mlir/test/Transport/ConvertTransportToLLVM.mlir new file mode 100644 index 0000000000..e2590bb88d --- /dev/null +++ b/mlir/test/Transport/ConvertTransportToLLVM.mlir @@ -0,0 +1,89 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// RUN: quantum-opt %s --convert-transport-to-llvm --split-input-file | FileCheck %s + +// CHECK-DAG: llvm.func @__catalyst__transport__create(!llvm.ptr, !llvm.ptr, i32, !llvm.ptr) -> !llvm.ptr +// CHECK-DAG: llvm.func @__catalyst__transport__connect(!llvm.ptr, !llvm.ptr, i16) -> i32 +// CHECK-DAG: llvm.func @__catalyst__transport__exchange_keys(!llvm.ptr) -> i32 +// CHECK-DAG: llvm.func @__catalyst__transport__establish_channel(!llvm.ptr, !llvm.ptr) -> i32 +// CHECK-DAG: llvm.func @__catalyst__transport__commit_work_item(!llvm.ptr, i32, i64, i64) -> i32 +// CHECK-DAG: llvm.func @__catalyst__transport__data_slot(!llvm.ptr) -> !llvm.ptr +// CHECK-DAG: llvm.func @__catalyst__transport__kick(!llvm.ptr, i32) -> i32 +// CHECK-DAG: llvm.func @__catalyst__transport__collect(!llvm.ptr, !llvm.ptr, i64) -> i32 +// CHECK-DAG: llvm.func @__catalyst__transport__start(!llvm.ptr) +// CHECK-DAG: llvm.func @__catalyst__transport__stop(!llvm.ptr) +// CHECK-DAG: llvm.func @__catalyst__transport__destroy(!llvm.ptr) + +// Controller: create (role in the result type) -> bring-up -> kick/collect over buffers. +// CHECK-LABEL: func.func @controller +func.func @controller(%syndrome: memref, %correction: memref) { + // CHECK: %[[S:.*]] = llvm.call @__catalyst__transport__create({{.*}}) : (!llvm.ptr, !llvm.ptr, i32, !llvm.ptr) -> !llvm.ptr + %s = transport.create {backend_lib = "libbackend.so", config = "cfg"} -> !transport.session + // CHECK: llvm.call @__catalyst__transport__connect(%[[S]] + transport.connect %s {peer = "127.0.0.1", oob_port = 18560 : i16} : !transport.session + // CHECK: llvm.call @__catalyst__transport__exchange_keys(%[[S]]) + transport.exchange_keys %s : !transport.session + // CHECK: llvm.call @__catalyst__transport__establish_channel(%[[S]] + transport.establish_channel %s "cpu_verbs" : !transport.session + // CHECK: llvm.call @__catalyst__transport__commit_work_item(%[[S]] + transport.commit_work_item %s {work_item_idx = 0 : i32, in_bytes = 8 : i64, out_bytes = 8 : i64} : !transport.session + // CHECK: llvm.call @__catalyst__transport__start(%[[S]]) + transport.start %s : !transport.session + // CHECK: %[[SLOT:.*]] = llvm.call @__catalyst__transport__data_slot(%[[S]]) + // CHECK: "llvm.intr.memcpy"(%[[SLOT]] + // CHECK: llvm.call @__catalyst__transport__kick(%[[S]] + transport.kick %s, %syndrome {work_item_idx = 0 : i32} : !transport.session, memref + // CHECK: llvm.call @__catalyst__transport__collect(%[[S]] + transport.collect %s, %correction : !transport.session, memref + // CHECK: llvm.call @__catalyst__transport__stop(%[[S]]) + transport.stop %s : !transport.session + // CHECK: llvm.call @__catalyst__transport__destroy(%[[S]]) + transport.destroy %s : !transport.session + return +} + +// ----- + +// Coprocessor: create + bind the coprocessor function symbol + async bring-up. +// CHECK-LABEL: func.func @coprocessor +func.func @coprocessor() { + // CHECK: %[[C:.*]] = llvm.call @__catalyst__transport__create({{.*}}) : (!llvm.ptr, !llvm.ptr, i32, !llvm.ptr) -> !llvm.ptr + %c = transport.create {backend_lib = "libbackend.so", config = "cfg"} -> !transport.session + // CHECK: llvm.call @__catalyst__transport__connect_async(%[[C]] + %t = transport.connect_async %c {peer = "127.0.0.1", oob_port = 18560 : i16} : !transport.session -> !transport.token + // CHECK: llvm.call @__catalyst__transport__barrier + transport.barrier %t : !transport.token + // CHECK: llvm.call @__catalyst__transport__set_coprocessor_fn(%[[C]], {{.*}}) : (!llvm.ptr, !llvm.ptr) -> i32 + transport.set_coprocessor_fn %c {symbol = "foo"} : !transport.session + // CHECK: llvm.call @__catalyst__transport__destroy(%[[C]]) + transport.destroy %c : !transport.session + return +} + +// ----- + +// get_session resolves a session by (role from result type, key) via the runtime registry. +// CHECK-DAG: llvm.func @__catalyst__transport__get_session(i32, !llvm.ptr) -> !llvm.ptr +// CHECK-LABEL: func.func @resolve +func.func @resolve(%syndrome: memref, %correction: memref) { + // CHECK: %[[R:.*]] = llvm.mlir.constant(0 : i32) : i32 + // CHECK: %[[S:.*]] = llvm.call @__catalyst__transport__get_session(%[[R]], {{.*}}) : (i32, !llvm.ptr) -> !llvm.ptr + %s = transport.get_session {key = "cop0"} : !transport.session + // CHECK: llvm.call @__catalyst__transport__kick(%[[S]] + transport.kick %s, %syndrome {work_item_idx = 0 : i32} : !transport.session, memref + // CHECK: llvm.call @__catalyst__transport__collect(%[[S]] + transport.collect %s, %correction : !transport.session, memref + return +} diff --git a/mlir/test/Transport/RoleVerify.mlir b/mlir/test/Transport/RoleVerify.mlir new file mode 100644 index 0000000000..a1ea8f2669 --- /dev/null +++ b/mlir/test/Transport/RoleVerify.mlir @@ -0,0 +1,43 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// RUN: quantum-opt --split-input-file --verify-diagnostics %s + +// Role safety: controller-only ops reject a coprocessor session and vice versa. + +func.func @kick_requires_controller() { + %c = transport.create {backend_lib = "x", config = "c"} -> !transport.session + %buf = memref.alloc() : memref<1xi8> + // expected-error @+1 {{operand #0 must be}} + transport.kick %c, %buf {work_item_idx = 0 : i32} : !transport.session, memref<1xi8> + return +} + +// ----- + +func.func @commit_requires_controller() { + %c = transport.create {backend_lib = "x", config = "c"} -> !transport.session + // expected-error @+1 {{operand #0 must be}} + transport.commit_work_item %c {work_item_idx = 0 : i32, in_bytes = 8 : i64, out_bytes = 8 : i64} : !transport.session + return +} + +// ----- + +func.func @set_coprocessor_fn_requires_coprocessor() { + %c = transport.create {backend_lib = "x", config = "c"} -> !transport.session + // expected-error @+1 {{operand #0 must be}} + transport.set_coprocessor_fn %c {symbol = "decode"} : !transport.session + return +} diff --git a/mlir/test/Transport/SmokeTest.mlir b/mlir/test/Transport/SmokeTest.mlir new file mode 100644 index 0000000000..daf2048c19 --- /dev/null +++ b/mlir/test/Transport/SmokeTest.mlir @@ -0,0 +1,65 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// RUN: quantum-opt %s | quantum-opt | FileCheck %s + +// Smoke test for the transport dialect + +// CHECK-LABEL: func.func @transport_smoketest +func.func @transport_smoketest(%payload: memref, %reply: memref) { + // CHECK: transport.create {{.*}} -> !transport.session + %ct = transport.create {backend_lib = "libbackend.so", config = "cfg"} -> !transport.session + // CHECK: transport.create {{.*}} -> !transport.session + %co = transport.create {backend_lib = "libbackend.so", config = "cfg"} -> !transport.session + + // CHECK: transport.connect_async %{{.*}} : !transport.session -> !transport.token + %t1 = transport.connect_async %co {peer = "127.0.0.1", oob_port = 18590 : i16} : !transport.session -> !transport.token + // CHECK: transport.connect %{{.*}} : !transport.session + transport.connect %ct {peer = "127.0.0.1", oob_port = 18590 : i16} : !transport.session + // CHECK: transport.barrier %{{.*}} : !transport.token + transport.barrier %t1 : !transport.token + // CHECK: transport.exchange_keys_async %{{.*}} : !transport.session -> !transport.token + %t2 = transport.exchange_keys_async %co : !transport.session -> !transport.token + transport.barrier %t2 : !transport.token + // CHECK: transport.exchange_keys %{{.*}} : !transport.session + transport.exchange_keys %ct : !transport.session + + // CHECK: transport.establish_channel %{{.*}} "cpu_verbs" : !transport.session + transport.establish_channel %ct "cpu_verbs" : !transport.session + // CHECK: transport.establish_channel %{{.*}} "gpu_engine" : !transport.session + transport.establish_channel %co "gpu_engine" : !transport.session + + // CHECK: transport.set_coprocessor_fn %{{.*}} {symbol = "foo"} : !transport.session + transport.set_coprocessor_fn %co {symbol = "foo"} : !transport.session + // CHECK: transport.commit_work_item %{{.*}} : !transport.session + transport.commit_work_item %ct {work_item_idx = 0 : i32, in_bytes = 8 : i64, out_bytes = 8 : i64} : !transport.session + + transport.start %co : !transport.session + transport.start %ct : !transport.session + + // CHECK: transport.get_session : !transport.session + %ct2 = transport.get_session : !transport.session + + // CHECK: transport.kick %{{.*}}, %{{.*}} {work_item_idx = 0 : i32} : !transport.session, memref + transport.kick %ct2, %payload {work_item_idx = 0 : i32} : !transport.session, memref + // CHECK: transport.collect %{{.*}}, %{{.*}} : !transport.session, memref + transport.collect %ct2, %reply : !transport.session, memref + // CHECK: transport.last_rtt_ns %{{.*}} : !transport.session -> i64 + %rtt = transport.last_rtt_ns %ct2 : !transport.session -> i64 + + transport.stop %ct : !transport.session + transport.destroy %ct : !transport.session + transport.destroy %co : !transport.session + return +} diff --git a/mlir/tools/quantum-lsp-server/CMakeLists.txt b/mlir/tools/quantum-lsp-server/CMakeLists.txt index c1f20f5abe..15403e0ef6 100644 --- a/mlir/tools/quantum-lsp-server/CMakeLists.txt +++ b/mlir/tools/quantum-lsp-server/CMakeLists.txt @@ -19,6 +19,7 @@ set(LIBS MLIRExecutor MLIRQecLogical MLIRQecPhysical + MLIRTransport ) add_llvm_executable(quantum-lsp-server quantum-lsp-server.cpp) diff --git a/mlir/tools/quantum-lsp-server/quantum-lsp-server.cpp b/mlir/tools/quantum-lsp-server/quantum-lsp-server.cpp index 22fea5f042..a6714ad20b 100644 --- a/mlir/tools/quantum-lsp-server/quantum-lsp-server.cpp +++ b/mlir/tools/quantum-lsp-server/quantum-lsp-server.cpp @@ -31,6 +31,7 @@ #include "RTIO/IR/RTIODialect.h" #include "Executor/IR/ExecutorDialect.h" +#include "Transport/IR/TransportDialect.h" int main(int argc, char **argv) { @@ -46,6 +47,7 @@ int main(int argc, char **argv) registry.insert(); registry.insert(); registry.insert(); + registry.insert(); registry.insert(); registry.insert(); registry.insert(); diff --git a/mlir/tools/quantum-opt/CMakeLists.txt b/mlir/tools/quantum-opt/CMakeLists.txt index eb90358640..f6fa524c1e 100644 --- a/mlir/tools/quantum-opt/CMakeLists.txt +++ b/mlir/tools/quantum-opt/CMakeLists.txt @@ -33,6 +33,8 @@ set(LIBS ion-transforms MLIRRTIO rtio-transforms + MLIRTransport + transport-transforms MLIRExecutor executor-transforms MLIRQecLogical diff --git a/mlir/tools/quantum-opt/quantum-opt.cpp b/mlir/tools/quantum-opt/quantum-opt.cpp index 809b556b6e..12fad60737 100644 --- a/mlir/tools/quantum-opt/quantum-opt.cpp +++ b/mlir/tools/quantum-opt/quantum-opt.cpp @@ -51,6 +51,7 @@ #include "RegisterAllPasses.h" #include "Executor/IR/ExecutorDialect.h" +#include "Transport/IR/TransportDialect.h" namespace test { void registerTestDialect(mlir::DialectRegistry &); @@ -79,6 +80,7 @@ int main(int argc, char **argv) registry.insert(); registry.insert(); registry.insert(); + registry.insert(); registry.insert(); registry.insert(); registry.insert(); diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 9fe951ed2a..8cf8c3a201 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -15,6 +15,7 @@ option(RUNTIME_ENABLE_WARNINGS "Enable -Wall and -Werror" ON) option(ENABLE_OPENQASM "Build OpenQasm backend device" OFF) option(ENABLE_OQD "Build OQD backend device" OFF) +option(ENABLE_TRANSPORT "Build the backend-agnostic transport loader (rt_transport)" OFF) set(CMAKE_VERBOSE_MAKEFILE ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) diff --git a/runtime/Makefile b/runtime/Makefile index 84c9ba328e..4370e10e07 100644 --- a/runtime/Makefile +++ b/runtime/Makefile @@ -13,6 +13,7 @@ CODE_COVERAGE ?= OFF BUILD_TYPE ?= RelWithDebInfo ENABLE_OPENQASM ?= ON ENABLE_OQD ?= OFF +ENABLE_TRANSPORT ?= OFF ENABLE_ASAN ?= OFF STRICT_WARNINGS ?= ON LLVM_DIR ?= $(MK_DIR)/../mlir/llvm-project/ @@ -53,6 +54,16 @@ ifeq ($(ENABLE_OQD), ON) TEST_TARGETS += runner_tests_oqd endif +ifeq ($(ENABLE_TRANSPORT), ON) + BUILD_TARGETS += rt_transport +endif + +TRANSPORT_TEST_TARGETS := \ + runner_tests_transport \ + runner_tests_transport_common \ + runner_tests_transport_cpu \ + cpu_verbs_selftest + .PHONY: help help: @echo "Please use \`make ' where is one of" @@ -78,6 +89,7 @@ configure: -DCMAKE_CXX_COMPILER_LAUNCHER=$(COMPILER_LAUNCHER) \ -DENABLE_OPENQASM=$(ENABLE_OPENQASM) \ -DENABLE_OQD=$(ENABLE_OQD) \ + -DENABLE_TRANSPORT=$(ENABLE_TRANSPORT) \ -DENABLE_CODE_COVERAGE=$(CODE_COVERAGE) \ -DPython_EXECUTABLE=$(PYTHON) \ -DENABLE_ADDRESS_SANITIZER=$(ENABLE_ASAN) \ @@ -112,6 +124,22 @@ ifeq ($(ENABLE_OQD), ON) $(ASAN_COMMAND) $(RT_BUILD_DIR)/tests/runner_tests_oqd endif +.PHONY: test-transport +test-transport: ENABLE_TRANSPORT=ON +test-transport: CODE_COVERAGE=OFF +test-transport: BUILD_TYPE?=RelWithDebInfo +test-transport: configure + cmake --build $(RT_BUILD_DIR) --target $(TRANSPORT_TEST_TARGETS) -j$(NPROC) + @echo "Catalyst transport CAPI/loader test suite (stub backend)" + $(ASAN_COMMAND) $(RT_BUILD_DIR)/tests/runner_tests_transport + @echo "Catalyst transport common primitives (SKIPs without rxe0)" + $(ASAN_COMMAND) $(RT_BUILD_DIR)/tests/runner_tests_transport_common + @echo "Catalyst transport cpu_verbs backend (SKIPs without rxe0)" + $(ASAN_COMMAND) $(RT_BUILD_DIR)/tests/runner_tests_transport_cpu + @echo "Catalyst transport cpu_verbs two-process loopback (requires rxe0)" + BIN=$(RT_BUILD_DIR)/lib/transport/cpu_verbs/cpu_verbs_selftest \ + $(MK_DIR)/lib/transport/cpu_verbs/run_loopback.sh + .PHONY: coverage coverage: RT_BUILD_DIR := $(RT_BUILD_DIR)_cov coverage: CODE_COVERAGE=ON diff --git a/runtime/include/Transport.hpp b/runtime/include/Transport.hpp index 7f8f7b07c0..8cb21da9e5 100644 --- a/runtime/include/Transport.hpp +++ b/runtime/include/Transport.hpp @@ -19,15 +19,6 @@ namespace catalyst::transport { -/** - * @brief Data-plane strategy: which engine issues the transfer. - */ -enum class DataPath : std::uint8_t { - CpuVerbs, // Plain ibverbs on CPU. - GpuEngine, // Gpu-initiated comms. - Other, -}; - /** * @brief Memory kind: selects the allocation and registration path. */ @@ -70,7 +61,7 @@ struct PeerRef { * @brief Configuration for the data-movement channel a session uses. */ struct ChannelDesc { - DataPath data_path = DataPath::CpuVerbs; + std::string data_path = "cpu_verbs"; }; /** @@ -102,11 +93,10 @@ class TransportSession { * * @param size Size of the region in bytes. * @param kind Memory kind selecting the allocation and registration path. - * @param access Access flags for the registration. * * @return `MemRegion` The allocated and registered region. */ - virtual MemRegion alloc_memory(std::size_t size, MemKind kind, std::uint32_t access) = 0; + virtual MemRegion alloc_memory(std::size_t size, MemKind kind) = 0; /** * @brief Advertise a local region and receive the peer's region over the out-of-band channel. @@ -133,14 +123,16 @@ class TransportSession { virtual void start() = 0; /** - * @brief Wait for a result and write it out. + * @brief Wait for a result and scatter it into the reply buffers. * - * @param replies Output buffer to write the result into. - * @param bytes Capacity of the output buffer, in bytes. + * @param replies Array of `n` buffers to write the results into. + * @param replies_bytes Array of `n` capacities (bytes), one per reply buffer. + * @param n Number of reply buffers. * * @return `int` */ - virtual int collect(void *replies, std::uint64_t bytes) = 0; + virtual int collect(void *const *replies, const std::uint64_t *replies_bytes, + std::size_t n) = 0; /** * @brief Stop the engine and join. Idempotent. diff --git a/runtime/include/TransportBackend.h b/runtime/include/TransportBackend.h new file mode 100644 index 0000000000..734362510c --- /dev/null +++ b/runtime/include/TransportBackend.h @@ -0,0 +1,76 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The plugin ABI for out-of-tree transport backends. +// +// A transport backend is a shared library that implements a TransportSession role (controller or +// coprocessor) and exports the matching factory symbol. + +#pragma once +#ifndef TRANSPORTBACKEND_H +#define TRANSPORTBACKEND_H + +#include +#include + +#include "Transport.hpp" + +#define CATALYST_TRANSPORT_CONTROLLER_FACTORY_SYMBOL "CatalystTransportControllerFactory" +#define CATALYST_TRANSPORT_COPROCESSOR_FACTORY_SYMBOL "CatalystTransportCoprocessorFactory" + +// The factory signatures backends must export with C linkage. +extern "C" { +using CatalystTransportControllerFactoryFn = catalyst::transport::ControllerSession *(const char *); +using CatalystTransportCoprocessorFactoryFn = + catalyst::transport::CoprocessorSession *(const char *); +} + +// A helper template macro to generate the Factory function. +// e.g. GENERATE_TRANSPORT_CONTROLLER_FACTORY(CatalystTransportController, make_controller) +// where `make_controller(const std::string &config) -> ControllerSession*`. +#define GENERATE_TRANSPORT_CONTROLLER_FACTORY(IDENTIFIER, BUILDER) \ + extern "C" catalyst::transport::ControllerSession *IDENTIFIER##Factory(const char *config) \ + { \ + try { \ + return (BUILDER)(config ? std::string(config) : std::string()); \ + } \ + catch (const std::exception &e) { \ + std::fprintf(stderr, "[transport] controller factory failed: %s\n", e.what()); \ + return nullptr; \ + } \ + catch (...) { \ + std::fprintf(stderr, "[transport] controller factory failed: unknown exception\n"); \ + return nullptr; \ + } \ + } + +// e.g. GENERATE_TRANSPORT_COPROCESSOR_FACTORY(CatalystTransportCoprocessor, make_coprocessor) +// where `make_coprocessor(const std::string &config) -> CoprocessorSession*`. +#define GENERATE_TRANSPORT_COPROCESSOR_FACTORY(IDENTIFIER, BUILDER) \ + extern "C" catalyst::transport::CoprocessorSession *IDENTIFIER##Factory(const char *config) \ + { \ + try { \ + return (BUILDER)(config ? std::string(config) : std::string()); \ + } \ + catch (const std::exception &e) { \ + std::fprintf(stderr, "[transport] coprocessor factory failed: %s\n", e.what()); \ + return nullptr; \ + } \ + catch (...) { \ + std::fprintf(stderr, "[transport] coprocessor factory failed: unknown exception\n"); \ + return nullptr; \ + } \ + } + +#endif // TRANSPORTBACKEND_H diff --git a/runtime/include/TransportCAPI.h b/runtime/include/TransportCAPI.h new file mode 100644 index 0000000000..08a5138c78 --- /dev/null +++ b/runtime/include/TransportCAPI.h @@ -0,0 +1,92 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// TransportCAPI.h - C entry points the Catalyst compiler emits to drive a transport session. +// +// The backend is a separate plugin `.so` the runtime dlopen's at session create (see +// TransportBackend.h) + +#pragma once +#ifndef TRANSPORTCAPI_H +#define TRANSPORTCAPI_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Opaque transport session handle +typedef struct CatalystTransportSession CatalystTransportSession; + +// Return codes: 0 == success; negative == error +enum { + CATALYST_TRANSPORT_OK = 0, + CATALYST_TRANSPORT_ERR = -1, // Generic exception + CATALYST_TRANSPORT_ERR_MEMORY = -2, // Memory error + CATALYST_TRANSPORT_ERR_TIMEOUT = -3, // Timeout error + CATALYST_TRANSPORT_ERR_STUCK = -4, // Something got stuck +}; + +// Session role (mirrors catalyst::transport::Role in the dialect). +enum { + CATALYST_TRANSPORT_ROLE_CONTROLLER = 0, + CATALYST_TRANSPORT_ROLE_COPROCESSOR = 1, +}; + +// Create a session from a named backend plugin `.so` (dlopen'd by the runtime). `role` selects +// which factory symbol is looked up (controller vs coprocessor). `config` is the backend's string. +// Returns NULL on failure. +// `key` registers the session under (role, key) for later get_session; empty = not registered. +CatalystTransportSession *__catalyst__transport__create(const char *backend_lib, const char *config, + int32_t role, const char *key); + +// Resolve the live session registered at create under (`role`, `key`). +// Lets a session brought up in one function be used in another. +CatalystTransportSession *__catalyst__transport__get_session(int32_t role, const char *key); + +// Bring-up. The peer region learned in exchange_keys is kept inside the session, so +// establish_channel takes no peer handle. The *_async variants run on a worker thread and return a +// token to await with barrier. +int __catalyst__transport__connect(CatalystTransportSession *s, const char *peer, + uint16_t oob_port); +int64_t __catalyst__transport__connect_async(CatalystTransportSession *s, const char *peer, + uint16_t oob_port); +int __catalyst__transport__exchange_keys(CatalystTransportSession *s); +int64_t __catalyst__transport__exchange_keys_async(CatalystTransportSession *s); +int __catalyst__transport__barrier(int64_t token); +int __catalyst__transport__establish_channel(CatalystTransportSession *s, const char *data_path); + +// Coprocessor-only: bind the function run per received message, resolved by runtime symbol name. +int __catalyst__transport__set_coprocessor_fn(CatalystTransportSession *s, const char *symbol); + +// Controller-only: work items + kick. +int __catalyst__transport__commit_work_item(CatalystTransportSession *s, uint32_t work_item_idx, + uint64_t in_bytes, uint64_t out_bytes); +void *__catalyst__transport__data_slot(CatalystTransportSession *s); +int __catalyst__transport__kick(CatalystTransportSession *s, uint32_t work_item_idx); + +// Run / collect / teardown. +void __catalyst__transport__start(CatalystTransportSession *s); +int __catalyst__transport__collect(CatalystTransportSession *s, void *reply, uint64_t reply_bytes); +uint64_t __catalyst__transport__last_rtt_ns(CatalystTransportSession *s); +void __catalyst__transport__stop(CatalystTransportSession *s); +void __catalyst__transport__destroy(CatalystTransportSession *s); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // TRANSPORTCAPI_H diff --git a/runtime/lib/CMakeLists.txt b/runtime/lib/CMakeLists.txt index eb7a77b63d..64d07dcd95 100644 --- a/runtime/lib/CMakeLists.txt +++ b/runtime/lib/CMakeLists.txt @@ -68,3 +68,7 @@ add_subdirectory(QEC) if(ENABLE_OQD) add_subdirectory(OQDcapi) endif() + +if(ENABLE_TRANSPORT) +add_subdirectory(transport) +endif() diff --git a/runtime/lib/transport/CMakeLists.txt b/runtime/lib/transport/CMakeLists.txt new file mode 100644 index 0000000000..e1b1a521c9 --- /dev/null +++ b/runtime/lib/transport/CMakeLists.txt @@ -0,0 +1,37 @@ +############################################### +# transport backends: shared dependencies # +############################################### + +find_package(Threads REQUIRED) +find_library(IBVERBS_LIB ibverbs) +if(NOT IBVERBS_LIB) + message(FATAL_ERROR + "ENABLE_TRANSPORT requires libibverbs (e.g. libibverbs-dev / rdma-core), but it was not found.") +endif() + +############################################### +# library rt_transport # +############################################### + +add_library(rt_transport SHARED TransportCAPI.cpp) + +target_include_directories(rt_transport + PUBLIC + ${runtime_includes} + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${PROJECT_SOURCE_DIR}/lib/backend/common # DynamicLibraryLoader.hpp +) + +target_link_libraries(rt_transport PRIVATE + ${CMAKE_DL_LIBS} # dlopen/dlsym for backend plugins +) + +set_property(TARGET rt_transport PROPERTY POSITION_INDEPENDENT_CODE ON) + +############################################### +# transport backends # +############################################### + +add_subdirectory(common) +add_subdirectory(cpu_verbs) diff --git a/runtime/lib/transport/TransportCAPI.cpp b/runtime/lib/transport/TransportCAPI.cpp new file mode 100644 index 0000000000..1ec26a5c32 --- /dev/null +++ b/runtime/lib/transport/TransportCAPI.cpp @@ -0,0 +1,372 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "TransportCAPI.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "DynamicLibraryLoader.hpp" +#include "Transport.hpp" +#include "TransportBackend.h" + +using catalyst::transport::ChannelDesc; +using catalyst::transport::ConnectInfo; +using catalyst::transport::ControllerSession; +using catalyst::transport::CoprocessorFn; +using catalyst::transport::CoprocessorSession; +using catalyst::transport::MemKind; +using catalyst::transport::MemRegion; +using catalyst::transport::PeerRef; +using catalyst::transport::TransportSession; + +// The opaque handle. `sess` is the base type; the concrete role is chosen by the factory at +// create. The peer region learned in exchange_keys is kept here so establish_channel needs no peer +// argument (matches the dialect, where the peer is session-held). +struct CatalystTransportSession { + std::unique_ptr backend; + TransportSession *sess = nullptr; // heap-allocated by the backend factory + MemRegion reply; // local region advertised in exchange_keys + bool reply_ready = false; + PeerRef peer; // peer region learned in exchange_keys + bool peer_ready = false; +}; + +namespace { + +// (role, key) -> live session registry. Populated at create, read by get_session so a session +// brought up in one function can be resolved in another. +std::unordered_map g_registry; + +std::string registry_key(std::int32_t role, const char *key) +{ + return std::to_string(role) + "/" + (key ? key : ""); +} + +// Local reply region size, provisioned automatically at exchange_keys. +constexpr std::uint64_t kReplyBytes = 16 * 1024; + +// Run fn, logging and swallowing any exception. Returns fn()'s result, or CATALYST_TRANSPORT_ERR +// if it threw; when fn() returns void there is nothing to return. +template auto guard(Fn &&fn) -> decltype(fn()) +{ + try { + return fn(); + } + catch (const std::exception &e) { + std::cerr << "[transport] " << e.what() << "\n"; + } + catch (...) { + } + if constexpr (!std::is_void_v) { + return CATALYST_TRANSPORT_ERR; + } +} + +// Provision the local reply region on first use (idempotent). +void ensure_reply(CatalystTransportSession *s) +{ + if (!s->reply_ready) { + s->reply = s->sess->alloc_memory(kReplyBytes, MemKind::CpuRam); + s->reply_ready = true; + } +} + +ControllerSession *as_controller(CatalystTransportSession *s) +{ + return s ? dynamic_cast(s->sess) : nullptr; +} + +// Bring-up bodies shared by the blocking and async (worker-thread) entry points. +int do_connect(CatalystTransportSession *s, std::string peer, std::uint16_t oob_port) +{ + ConnectInfo info; + info.peer = std::move(peer); + info.oob_port = oob_port; + return s->sess->connect(info); +} + +int do_exchange_keys(CatalystTransportSession *s) +{ + ensure_reply(s); + s->peer = s->sess->exchange_keys(s->reply); + s->peer_ready = true; + return CATALYST_TRANSPORT_OK; +} + +// Built-in fallback coprocessor function: echo the input back. +std::size_t echo_fn(const void *in, std::size_t in_len, void *out, std::size_t out_cap, void *) +{ + std::size_t n = std::min(in_len, out_cap); + if (n && in && out) { + std::memcpy(out, in, n); + } + return n; +} + +// Async task registry: connect_async / exchange_keys_async run on a worker thread and return a +// token; barrier awaits it. Tokens start at 1 so a 0 return can signal a dispatch failure. +std::mutex g_async_mtx; +std::int64_t g_next_token = 1; +std::unordered_map> g_async_tasks; + +std::int64_t dispatch_async(std::function fn) +{ + std::lock_guard lk(g_async_mtx); + std::int64_t token = g_next_token++; + g_async_tasks.emplace(token, std::async(std::launch::async, std::move(fn))); + return token; +} + +int await_token(std::int64_t token) +{ + std::future fut; + { + std::lock_guard lk(g_async_mtx); + auto it = g_async_tasks.find(token); + if (it == g_async_tasks.end()) { + return CATALYST_TRANSPORT_ERR; + } + fut = std::move(it->second); + g_async_tasks.erase(it); + } + return guard([&] { return fut.get(); }); +} + +} // namespace + +extern "C" { + +CatalystTransportSession *__catalyst__transport__create(const char *backend_lib, const char *config, + std::int32_t role, const char *key) +{ + try { + if (!backend_lib || !*backend_lib) { + std::cerr << "[transport] no backend library given\n"; + return nullptr; + } + auto h = std::make_unique(); + h->backend = std::make_unique(backend_lib); + const char *cfg = config ? config : ""; + if (role == CATALYST_TRANSPORT_ROLE_COPROCESSOR) { + auto *factory = h->backend->getSymbol( + CATALYST_TRANSPORT_COPROCESSOR_FACTORY_SYMBOL); + h->sess = factory(cfg); + } + else { + auto *factory = h->backend->getSymbol( + CATALYST_TRANSPORT_CONTROLLER_FACTORY_SYMBOL); + h->sess = factory(cfg); + } + if (!h->sess) { + std::cerr << "[transport] backend factory returned null for config: " << cfg << "\n"; + return nullptr; + } + auto *raw = h.release(); + if (key && *key) { + g_registry[registry_key(role, key)] = raw; // resolved later via get_session + } + return raw; + } + catch (const std::exception &e) { + std::cerr << "[transport] create: " << e.what() << "\n"; + return nullptr; + } + catch (...) { + return nullptr; + } +} + +int __catalyst__transport__connect(CatalystTransportSession *s, const char *peer, + std::uint16_t oob_port) +{ + if (!s || !s->sess) { + return CATALYST_TRANSPORT_ERR; + } + return guard([&] { return do_connect(s, peer ? peer : "", oob_port); }); +} + +std::int64_t __catalyst__transport__connect_async(CatalystTransportSession *s, const char *peer, + std::uint16_t oob_port) +{ + if (!s || !s->sess) { + return 0; + } + return dispatch_async( + [s, p = std::string(peer ? peer : ""), oob_port] { return do_connect(s, p, oob_port); }); +} + +int __catalyst__transport__exchange_keys(CatalystTransportSession *s) +{ + if (!s || !s->sess) { + return CATALYST_TRANSPORT_ERR; + } + return guard([&] { return do_exchange_keys(s); }); +} + +std::int64_t __catalyst__transport__exchange_keys_async(CatalystTransportSession *s) +{ + if (!s || !s->sess) { + return 0; + } + return dispatch_async([s] { return do_exchange_keys(s); }); +} + +int __catalyst__transport__barrier(std::int64_t token) { return await_token(token); } + +int __catalyst__transport__establish_channel(CatalystTransportSession *s, const char *data_path) +{ + if (!s || !s->sess) { + return CATALYST_TRANSPORT_ERR; + } + return guard([&] { + ChannelDesc desc; + desc.data_path = data_path ? data_path : ""; // opaque; the backend interprets it + s->sess->establish_channel(desc, s->reply, s->peer); + return CATALYST_TRANSPORT_OK; + }); +} + +int __catalyst__transport__set_coprocessor_fn(CatalystTransportSession *s, const char *symbol) +{ + if (!s || !s->sess) { + return CATALYST_TRANSPORT_ERR; + } + return guard([&] { + auto *co = dynamic_cast(s->sess); + if (!co) { + std::cerr << "[transport] set_coprocessor_fn on a non-coprocessor session\n"; + return CATALYST_TRANSPORT_ERR; + } + // Empty symbol selects the built-in echo; a named-but-unresolved symbol is a hard error + CoprocessorFn fn = &echo_fn; + if (symbol && *symbol) { + fn = reinterpret_cast(dlsym(RTLD_DEFAULT, symbol)); + if (!fn) { + std::cerr << "[transport] set_coprocessor_fn: symbol not found: " << symbol << "\n"; + return CATALYST_TRANSPORT_ERR; + } + } + co->set_coprocessor_fn(fn, nullptr); + return CATALYST_TRANSPORT_OK; + }); +} + +int __catalyst__transport__commit_work_item(CatalystTransportSession *s, + std::uint32_t work_item_idx, std::uint64_t in_bytes, + std::uint64_t out_bytes) +{ + auto *c = as_controller(s); + if (!c) { + return CATALYST_TRANSPORT_ERR; + } + return guard([&] { + if (s->reply_ready && out_bytes > s->reply.size) { + std::cerr << "[transport] commit_work_item: out_bytes (" << out_bytes + << ") exceeds the reply region (" << s->reply.size << ")\n"; + return CATALYST_TRANSPORT_ERR; + } + c->commit_work_item(work_item_idx, in_bytes, out_bytes); + return CATALYST_TRANSPORT_OK; + }); +} + +void *__catalyst__transport__data_slot(CatalystTransportSession *s) +{ + auto *c = as_controller(s); + void *slot = nullptr; + if (c) { + guard([&] { slot = c->data_slot(); }); + } + return slot; +} + +int __catalyst__transport__kick(CatalystTransportSession *s, std::uint32_t work_item_idx) +{ + auto *c = as_controller(s); + if (!c) { + return CATALYST_TRANSPORT_ERR; + } + return guard([&] { return c->kick(work_item_idx); }); +} + +int __catalyst__transport__collect(CatalystTransportSession *s, void *reply, + std::uint64_t reply_bytes) +{ + if (!s || !s->sess) { + return CATALYST_TRANSPORT_ERR; + } + return guard([&] { + void *replies[1] = {reply}; + std::uint64_t replies_bytes[1] = {reply_bytes}; + return s->sess->collect(replies, replies_bytes, 1); + }); +} + +std::uint64_t __catalyst__transport__last_rtt_ns(CatalystTransportSession *s) +{ + if (!s || !s->sess) { + return 0; + } + return s->sess->last_rtt_ns(); +} + +void __catalyst__transport__start(CatalystTransportSession *s) +{ + if (s && s->sess) { + guard([&] { s->sess->start(); }); + } +} + +void __catalyst__transport__stop(CatalystTransportSession *s) +{ + if (s && s->sess) { + guard([&] { s->sess->stop(); }); + } +} + +CatalystTransportSession *__catalyst__transport__get_session(std::int32_t role, const char *key) +{ + auto it = g_registry.find(registry_key(role, key)); + if (it == g_registry.end()) { + std::cerr << "[transport] get_session: no session registered for role " << role << " key '" + << (key ? key : "") << "'\n"; + return nullptr; + } + return it->second; +} + +void __catalyst__transport__destroy(CatalystTransportSession *s) +{ + if (!s) { + return; + } + for (auto it = g_registry.begin(); it != g_registry.end();) + it = (it->second == s) ? g_registry.erase(it) : std::next(it); + delete s->sess; // owned by the backend factory + s->backend.reset(); + delete s; +} + +} // extern "C" diff --git a/runtime/lib/transport/common/CMakeLists.txt b/runtime/lib/transport/common/CMakeLists.txt new file mode 100644 index 0000000000..f4dfb8d961 --- /dev/null +++ b/runtime/lib/transport/common/CMakeLists.txt @@ -0,0 +1,20 @@ +############################################### +# library transport_common +# Device-agnostic RDMA verbs primitives +# shared by all transport backends +############################################### + +add_library(transport_common STATIC + CompletionQueue.cpp + Context.cpp + MemoryRegion.cpp + OobSocket.cpp + ProtectionDomain.cpp + QueuePair.cpp +) + +target_include_directories(transport_common PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(transport_common PUBLIC ${IBVERBS_LIB} Threads::Threads) + +set_property(TARGET transport_common PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/runtime/lib/transport/common/CompletionQueue.cpp b/runtime/lib/transport/common/CompletionQueue.cpp new file mode 100644 index 0000000000..6cf2b09982 --- /dev/null +++ b/runtime/lib/transport/common/CompletionQueue.cpp @@ -0,0 +1,33 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "CompletionQueue.hpp" + +#include + +#include "Error.hpp" + +namespace catalyst::transport::common { +CompletionQueue::CompletionQueue(std::shared_ptr ctx, int depth) : ctx_(std::move(ctx)) +{ + cq_ = ibv_create_cq(ctx_->get(), depth, nullptr, nullptr, 0); + RDMA_CHECK(cq_, "ibv_create_cq"); +} +CompletionQueue::~CompletionQueue() +{ + if (cq_) + ibv_destroy_cq(cq_); +} +ibv_cq *CompletionQueue::get() const { return cq_; } +} // namespace catalyst::transport::common diff --git a/runtime/lib/transport/common/CompletionQueue.hpp b/runtime/lib/transport/common/CompletionQueue.hpp new file mode 100644 index 0000000000..1788ff5e95 --- /dev/null +++ b/runtime/lib/transport/common/CompletionQueue.hpp @@ -0,0 +1,42 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include + +#include "Context.hpp" + +#include + +namespace catalyst::transport::common { + +/** + * @class CompletionQueue class. + * + * @brief An RAII wrapper for an RDMA Completion Queue (`ibv_cq`). + */ +class CompletionQueue { + public: + CompletionQueue() = delete; + CompletionQueue(std::shared_ptr ctx, int depth); + ~CompletionQueue(); + CompletionQueue(const CompletionQueue &) = delete; + CompletionQueue &operator=(const CompletionQueue &) = delete; + ibv_cq *get() const; + + private: + std::shared_ptr ctx_; + ibv_cq *cq_ = nullptr; +}; +} // namespace catalyst::transport::common diff --git a/runtime/lib/transport/common/Context.cpp b/runtime/lib/transport/common/Context.cpp new file mode 100644 index 0000000000..dceb8c73ae --- /dev/null +++ b/runtime/lib/transport/common/Context.cpp @@ -0,0 +1,62 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "Context.hpp" + +#include +#include + +#include "Error.hpp" +namespace catalyst::transport::common { +Context::Context(const std::string &dev_name) +{ + int n = 0; + ibv_device **devs = ibv_get_device_list(&n); + RDMA_CHECK(devs && n > 0, "ibv_get_device_list"); + auto it = std::find_if(devs, devs + n, + [&](ibv_device *d) { return dev_name == ibv_get_device_name(d); }); + ibv_device *dev = (it != devs + n) ? *it : nullptr; + if (!dev) { + std::string avail; + for (int i = 0; i < n; ++i) { + avail += (i ? ", " : ""); + avail += ibv_get_device_name(devs[i]); + } + ibv_free_device_list(devs); + RDMA_FAIL("device %s not found (available: %s)", dev_name.c_str(), + avail.empty() ? "" : avail.c_str()); + } + ctx_ = ibv_open_device(dev); + ibv_free_device_list(devs); + RDMA_CHECK(ctx_, "ibv_open_device(%s)", dev_name.c_str()); +} +Context::~Context() +{ + if (ctx_) + ibv_close_device(ctx_); +} +ibv_context *Context::get() const { return ctx_; } +ibv_port_attr Context::port_attr(std::uint8_t port) const +{ + ibv_port_attr attr{}; + RDMA_CHECK(ibv_query_port(ctx_, port, &attr) == 0, "ibv_query_port(%u)", port); + return attr; +} +ibv_gid Context::gid(std::uint8_t port, int idx) const +{ + ibv_gid gid{}; + RDMA_CHECK(ibv_query_gid(ctx_, port, idx, &gid) == 0, "ibv_query_gid(%u,%d)", port, idx); + return gid; +} +} // namespace catalyst::transport::common diff --git a/runtime/lib/transport/common/Context.hpp b/runtime/lib/transport/common/Context.hpp new file mode 100644 index 0000000000..34494e1c2b --- /dev/null +++ b/runtime/lib/transport/common/Context.hpp @@ -0,0 +1,61 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include + +#include + +namespace catalyst::transport::common { +/** + * @class Context + * @brief RAII wrapper for an RDMA device context (`ibv_context`). + * + * Manages unique ownership of a physical NIC handle. Exposes network + * attributes, port status, and GID table properties. + */ +class Context { + public: + /** + * @brief Opens an RDMA device by its system name (e.g., "mlx5_0"). + */ + explicit Context(const std::string &dev_name); // open by name + + /** + * @brief Closes the physical RDMA device handle. + */ + ~Context(); + Context(const Context &) = delete; + Context &operator=(const Context &) = delete; + + /** + * @brief Returns the raw underlying device context pointer. + */ + ibv_context *get() const; + + /** + * @brief Accesses hardware attributes for a specific physical port. + */ + ibv_port_attr port_attr(std::uint8_t port) const; + + /** + * @brief Retrieves a Global Identifier (GID) from the device table. + */ + ibv_gid gid(std::uint8_t port, int idx) const; + + private: + ibv_context *ctx_ = nullptr; +}; +} // namespace catalyst::transport::common diff --git a/runtime/lib/transport/common/Error.hpp b/runtime/lib/transport/common/Error.hpp new file mode 100644 index 0000000000..8fbc797095 --- /dev/null +++ b/runtime/lib/transport/common/Error.hpp @@ -0,0 +1,50 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include +#include +#include + +namespace catalyst::transport::common { +class RdmaError : public std::runtime_error { + public: + using std::runtime_error::runtime_error; +}; + +/** + * Throw RdmaError with a preformatted message. + */ +[[noreturn]] inline void rdma_throw(const char *msg) { throw RdmaError(msg); } +} // namespace catalyst::transport::common + +// Unconditionally fail with "file:line: msg (errno=..)" context. +#define RDMA_FAIL(...) \ + do { \ + char rdma_msg_[256]; \ + std::snprintf(rdma_msg_, sizeof(rdma_msg_), __VA_ARGS__); \ + char rdma_full_[512]; \ + std::snprintf(rdma_full_, sizeof(rdma_full_), "%s:%d: %s (errno=%d: %s)", __FILE__, \ + __LINE__, rdma_msg_, errno, std::strerror(errno)); \ + ::catalyst::transport::common::rdma_throw(rdma_full_); \ + } while (0) + +// Throw RdmaError with file:line + errno when cond is false. +#define RDMA_CHECK(cond, ...) \ + do { \ + if (!(cond)) { \ + RDMA_FAIL(__VA_ARGS__); \ + } \ + } while (0) diff --git a/runtime/lib/transport/common/Handshake.hpp b/runtime/lib/transport/common/Handshake.hpp new file mode 100644 index 0000000000..bb448b3483 --- /dev/null +++ b/runtime/lib/transport/common/Handshake.hpp @@ -0,0 +1,44 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include + +namespace catalyst::transport::common { + +/** + * @struct QpInfo + * @brief Connection metadata exchanged out-of-band to connect remote Queue + * Pairs. + */ +struct QpInfo { + std::uint32_t qpn; // Unique Queue Pair Number hardware identifier. + std::uint32_t psn; // Starting Packet Sequence Number for flow validation. + std::uint8_t gid[16]; // 128-bit Global Identifier routing address. +}; + +/** + * @struct HandshakeMsg + * @brief Message exchanged once over the OOB TCP socket, after the MR exists, + * so QP identity and MR handle are swapped together. + */ +struct HandshakeMsg { + QpInfo fwd; // forward QP (controller -> coprocessor) + QpInfo bwd; // backward QP (coprocessor -> controller) + std::uint64_t mr_vaddr; // where the peer writes into us + std::uint32_t mr_rkey; + std::uint32_t mtu_enum; // ibv_mtu enum +}; + +} // namespace catalyst::transport::common diff --git a/runtime/lib/transport/common/MemoryRegion.cpp b/runtime/lib/transport/common/MemoryRegion.cpp new file mode 100644 index 0000000000..c5c5c586ed --- /dev/null +++ b/runtime/lib/transport/common/MemoryRegion.cpp @@ -0,0 +1,109 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "MemoryRegion.hpp" + +#include +#include +#include +#include + +#include "Error.hpp" + +namespace catalyst::transport::common { + +/** + * @brief Register caller-owned host memory (borrowed; the region does not own + * it). + */ +MemoryRegion::MemoryRegion(std::shared_ptr pd, void *addr, std::size_t length, + MemAccess access) + : MemoryRegion(std::move(pd), addr, length, access, nullptr) +{ +} + +/** + * @brief Register caller-provided memory, keeping `backing` alive for the MR's + * lifetime. + */ +MemoryRegion::MemoryRegion(std::shared_ptr pd, void *addr, std::size_t length, + MemAccess access, std::shared_ptr backing) + : pd_(std::move(pd)), backing_buffer_(std::move(backing)) +{ + mr_ = ibv_reg_mr(pd_->get(), addr, length, static_cast(access)); + RDMA_CHECK(mr_, "ibv_reg_mr"); +} + +/** + * @brief Register a dma-buf region (e.g. exported GPU memory); does not own the + * buffer. + */ +MemoryRegion::MemoryRegion(std::shared_ptr pd, std::uint64_t offset, + std::size_t length, std::uint64_t iova, int fd, MemAccess access) + : pd_(std::move(pd)) +{ + mr_ = ibv_reg_dmabuf_mr(pd_->get(), offset, length, iova, fd, static_cast(access)); + RDMA_CHECK(mr_, "ibv_reg_dmabuf_mr"); +} + +/** + * @brief Allocate + own an aligned host buffer, then register it. + */ +MemoryRegion MemoryRegion::alloc_host(std::shared_ptr pd, std::size_t length, + std::size_t alignment, MemAccess access) +{ + // aligned_alloc requires a power-of-two alignment and a size that is a + // multiple of it; guard alignment first so a bad value throws rather than + // dividing by zero. + RDMA_CHECK(std::has_single_bit(alignment), "alignment must be a power of two, got %zu", + alignment); + std::size_t rounded = ((length + alignment - 1) / alignment) * alignment; + void *buf = std::aligned_alloc(alignment, rounded); + RDMA_CHECK(buf, "aligned_alloc(%zu)", rounded); + std::memset(buf, 0, rounded); + return MemoryRegion(std::move(pd), buf, length, access, std::shared_ptr(buf, std::free)); +} + +MemoryRegion::~MemoryRegion() +{ + if (mr_) + ibv_dereg_mr(mr_); +} + +MemoryRegion::MemoryRegion(MemoryRegion &&other) noexcept + : pd_(std::move(other.pd_)), mr_(std::exchange(other.mr_, nullptr)), + backing_buffer_(std::move(other.backing_buffer_)) +{ +} + +MemoryRegion &MemoryRegion::operator=(MemoryRegion &&other) noexcept +{ + if (this != &other) { + if (mr_) { + ibv_dereg_mr(mr_); // release our current MR before taking other's + } + pd_ = std::move(other.pd_); + mr_ = std::exchange(other.mr_, nullptr); + backing_buffer_ = std::move(other.backing_buffer_); + } + return *this; +} + +ibv_mr *MemoryRegion::get() const noexcept { return mr_; } +void *MemoryRegion::addr() const noexcept { return mr_ ? mr_->addr : nullptr; } +std::size_t MemoryRegion::length() const noexcept { return mr_ ? mr_->length : 0; } +std::uint32_t MemoryRegion::lkey() const noexcept { return mr_ ? mr_->lkey : 0; } +std::uint32_t MemoryRegion::rkey() const noexcept { return mr_ ? mr_->rkey : 0; } + +} // namespace catalyst::transport::common diff --git a/runtime/lib/transport/common/MemoryRegion.hpp b/runtime/lib/transport/common/MemoryRegion.hpp new file mode 100644 index 0000000000..67aaedfecb --- /dev/null +++ b/runtime/lib/transport/common/MemoryRegion.hpp @@ -0,0 +1,92 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include +#include + +#include "ProtectionDomain.hpp" + +#include + +namespace catalyst::transport::common { + +/** + * @enum MemAccess flag. + * @brief Type-safe hardware access permissions for registered Memory Regions. + */ +enum class MemAccess : int { + LOCAL_WRITE = IBV_ACCESS_LOCAL_WRITE, + REMOTE_WRITE = IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_LOCAL_WRITE, + REMOTE_READ = IBV_ACCESS_REMOTE_READ, +}; + +/** + * @brief Bitwise OR operator for combining MemAccess flags. + */ +constexpr MemAccess operator|(MemAccess a, MemAccess b) +{ + return static_cast(static_cast(a) | static_cast(b)); +} + +/** + * @class MemoryRegion + * @brief RAII wrapper for an `ibv_mr`, managing hardware registration and + * backing storage. + */ +class MemoryRegion { + public: + // register caller-provided memory; region does not own the buffer. + MemoryRegion(std::shared_ptr pd, void *addr, std::size_t length, + MemAccess access); + // register caller-provided memory and keep a shared_ptr for lifetime. + MemoryRegion(std::shared_ptr pd, void *addr, std::size_t length, + MemAccess access, std::shared_ptr backing); + // register a dma-buf (does not own the buffer). + MemoryRegion(std::shared_ptr pd, std::uint64_t offset, std::size_t length, + std::uint64_t iova, int fd, MemAccess access); + // allocate + own an aligned host buffer, then register it. + static MemoryRegion alloc_host(std::shared_ptr pd, std::size_t length, + std::size_t alignment, MemAccess access); + ~MemoryRegion(); + + MemoryRegion(const MemoryRegion &) = delete; + MemoryRegion &operator=(const MemoryRegion &) = delete; + MemoryRegion(MemoryRegion &&o) noexcept; + MemoryRegion &operator=(MemoryRegion &&o) noexcept; + + ibv_mr *get() const noexcept; + + /// @brief Returns the base virtual address of the registered region. + void *addr() const noexcept; + + /// @brief Returns the total capacity of the memory region in bytes. + std::size_t length() const noexcept; + + /// @brief Returns the local key required for posting local work requests. + std::uint32_t lkey() const noexcept; + + /// @brief Returns the remote key required by peers for one-sided RDMA + /// operations. + std::uint32_t rkey() const noexcept; + + private: + std::shared_ptr pd_; // keeps PD alive + ibv_mr *mr_ = nullptr; + // Keeps the backing storage alive until after the MR is deregistered; + // null for borrowed / dma-buf regions that own nothing. + std::shared_ptr backing_buffer_; +}; +} // namespace catalyst::transport::common diff --git a/runtime/lib/transport/common/OobSocket.cpp b/runtime/lib/transport/common/OobSocket.cpp new file mode 100644 index 0000000000..2f9f5e8ff1 --- /dev/null +++ b/runtime/lib/transport/common/OobSocket.cpp @@ -0,0 +1,119 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "OobSocket.hpp" + +#include +#include +#include +#include +#include +#include + +#include "Error.hpp" + +#include +#include +#include + +namespace catalyst::transport::common { + +namespace { +void set_tcp_nodelay(int fd) +{ + int one = 1; + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)); +} +} // namespace + +FdGuard tcp_listen_accept(std::uint16_t port) +{ + FdGuard listener(socket(AF_INET, SOCK_STREAM, 0)); + RDMA_CHECK(listener.valid(), "socket"); + int one = 1; + setsockopt(listener.get(), SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + sockaddr_in sa{ + .sin_family = AF_INET, + .sin_port = htons(port), + .sin_addr = {.s_addr = INADDR_ANY}, + }; + RDMA_CHECK(bind(listener.get(), reinterpret_cast(&sa), sizeof(sa)) == 0, "bind(%u)", + port); + RDMA_CHECK(listen(listener.get(), 1) == 0, "listen"); + FdGuard client(accept(listener.get(), nullptr, nullptr)); + RDMA_CHECK(client.valid(), "accept"); + set_tcp_nodelay(client.get()); + return client; // listener closed by its FdGuard on return +} + +FdGuard tcp_connect(const char *host, std::uint16_t port) +{ + // getaddrinfo resolves both numeric IPs ("127.0.0.1") and hostnames + // ("localhost", "node01"). IPv4-only. + addrinfo hints{ + .ai_family = AF_INET, + .ai_socktype = SOCK_STREAM, + }; + char port_str[6]; + std::snprintf(port_str, sizeof(port_str), "%u", port); + addrinfo *res = nullptr; + int rc = getaddrinfo(host, port_str, &hints, &res); + RDMA_CHECK(rc == 0, "getaddrinfo(%s:%s): %s", host, port_str, gai_strerror(rc)); + std::unique_ptr res_guard(res, freeaddrinfo); + + for (int attempt = 0; attempt < 200; ++attempt) { + FdGuard s(socket(res->ai_family, res->ai_socktype, res->ai_protocol)); + RDMA_CHECK(s.valid(), "socket"); + if (connect(s.get(), res->ai_addr, res->ai_addrlen) == 0) { + set_tcp_nodelay(s.get()); + return s; + } + using namespace std::chrono_literals; + std::this_thread::sleep_for(50ms); + } + RDMA_FAIL("tcp_connect(%s:%u) failed after 200 attempts", host, port); +} + +void send_exact(int fd, const void *buf, std::size_t n) +{ + std::size_t done = 0; + const char *p = static_cast(buf); + while (done < n) { + ssize_t r = ::send(fd, p + done, n - done, 0); + if (r < 0) { + if (errno == EINTR) + continue; // interrupted by signal; retry + RDMA_FAIL("send: %s", std::strerror(errno)); + } + done += static_cast(r); + } +} + +void recv_exact(int fd, void *buf, std::size_t n) +{ + std::size_t done = 0; + char *p = static_cast(buf); + while (done < n) { + ssize_t r = ::recv(fd, p + done, n - done, 0); + if (r < 0) { + if (errno == EINTR) + continue; // interrupted by signal; retry + RDMA_FAIL("recv: %s", std::strerror(errno)); + } + RDMA_CHECK(r > 0, "recv: peer closed connection (%zu/%zu bytes)", done, n); + done += static_cast(r); + } +} + +} // namespace catalyst::transport::common diff --git a/runtime/lib/transport/common/OobSocket.hpp b/runtime/lib/transport/common/OobSocket.hpp new file mode 100644 index 0000000000..dced857c60 --- /dev/null +++ b/runtime/lib/transport/common/OobSocket.hpp @@ -0,0 +1,74 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include +#include + +#include + +namespace catalyst::transport::common { + +// RAII handle for a socket file descriptor. +class FdGuard { + public: + FdGuard() noexcept = default; + explicit FdGuard(int fd) noexcept : fd_(fd) {} + ~FdGuard() { reset(); } + + FdGuard(FdGuard &&other) noexcept : fd_(other.release()) {} + FdGuard &operator=(FdGuard &&o) noexcept + { + if (this != &o) + reset(o.release()); + return *this; + } + FdGuard(const FdGuard &) = delete; + FdGuard &operator=(const FdGuard &) = delete; + + [[nodiscard]] int get() const noexcept { return fd_; } + [[nodiscard]] bool valid() const noexcept { return fd_ >= 0; } + explicit operator bool() const noexcept { return valid(); } + + /** + * @brief Closes the current socket and takes ownership of a new one. + * @param new_fd The new socket descriptor to manage. + */ + void reset(int new_fd = -1) noexcept + { + if (fd_ >= 0) + ::close(fd_); + fd_ = new_fd; + } + + /** + * @brief Releases ownership of the socket without closing it. + * @return The raw socket file descriptor. + */ + [[nodiscard]] int release() noexcept { return std::exchange(fd_, -1); } + + private: + int fd_ = -1; +}; + +// OOB TCP handshake helpers. +FdGuard tcp_listen_accept(std::uint16_t port); +FdGuard tcp_connect(const char *host, std::uint16_t port); + +// Blocking exact-length IO over an OOB socket. +void send_exact(int fd, const void *buf, std::size_t n); +void recv_exact(int fd, void *buf, std::size_t n); + +} // namespace catalyst::transport::common diff --git a/runtime/lib/transport/common/ProtectionDomain.cpp b/runtime/lib/transport/common/ProtectionDomain.cpp new file mode 100644 index 0000000000..89b2cc44e6 --- /dev/null +++ b/runtime/lib/transport/common/ProtectionDomain.cpp @@ -0,0 +1,33 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ProtectionDomain.hpp" + +#include + +#include "Error.hpp" + +namespace catalyst::transport::common { +ProtectionDomain::ProtectionDomain(std::shared_ptr ctx) : ctx_(std::move(ctx)) +{ + pd_ = ibv_alloc_pd(ctx_->get()); + RDMA_CHECK(pd_, "ibv_alloc_pd"); +} +ProtectionDomain::~ProtectionDomain() +{ + if (pd_) + ibv_dealloc_pd(pd_); +} +ibv_pd *ProtectionDomain::get() const { return pd_; } +} // namespace catalyst::transport::common diff --git a/runtime/lib/transport/common/ProtectionDomain.hpp b/runtime/lib/transport/common/ProtectionDomain.hpp new file mode 100644 index 0000000000..795c0e81fc --- /dev/null +++ b/runtime/lib/transport/common/ProtectionDomain.hpp @@ -0,0 +1,55 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include + +#include "Context.hpp" + +#include + +namespace catalyst::transport::common { +/** + * @class ProtectionDomain + * @brief RAII wrapper for an `ibv_pd` resource, managing memory protection + * domains. + */ +class ProtectionDomain { + public: + /** + * @brief Allocates an InfiniBand protection domain tied to the lifecycle of + * the context. + * @param ctx Shared pointer to the underlying hardware context. + */ + explicit ProtectionDomain(std::shared_ptr ctx); + ProtectionDomain() = delete; + + /** + * @brief Automatically releases the underlying `ibv_pd` resource. + */ + ~ProtectionDomain(); + ProtectionDomain(const ProtectionDomain &) = delete; + ProtectionDomain &operator=(const ProtectionDomain &) = delete; + + /** + * @brief Returns a raw pointer to the underlying verbs protection domain. + * @return Raw pointer to `ibv_pd`. + */ + ibv_pd *get() const; + + private: + std::shared_ptr ctx_; // keeps the Context alive + ibv_pd *pd_ = nullptr; // Low-level verbs protection domain handle. +}; +} // namespace catalyst::transport::common diff --git a/runtime/lib/transport/common/QpState.hpp b/runtime/lib/transport/common/QpState.hpp new file mode 100644 index 0000000000..0270c040ab --- /dev/null +++ b/runtime/lib/transport/common/QpState.hpp @@ -0,0 +1,61 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include "Error.hpp" + +namespace catalyst::transport::common { + +enum class QpState { RESET, INIT, RTR, RTS, ERROR }; + +inline const char *to_string(QpState s) +{ + switch (s) { + case QpState::RESET: + return "Reset"; + case QpState::INIT: + return "Init"; + case QpState::RTR: + return "Rtr"; + case QpState::RTS: + return "Rts"; + case QpState::ERROR: + return "Error"; + } + return "?"; +} + +// Checks whether a given transition is valid. +constexpr bool is_valid_transition(QpState from, QpState to) +{ + if (to == QpState::ERROR || to == QpState::RESET) + return true; + switch (from) { + case QpState::RESET: + return to == QpState::INIT; + case QpState::INIT: + return to == QpState::RTR; + case QpState::RTR: + return to == QpState::RTS; + default: + return false; + } +} + +class BadTransition : public RdmaError { + public: + using RdmaError::RdmaError; +}; + +} // namespace catalyst::transport::common diff --git a/runtime/lib/transport/common/QueuePair.cpp b/runtime/lib/transport/common/QueuePair.cpp new file mode 100644 index 0000000000..d7ec088292 --- /dev/null +++ b/runtime/lib/transport/common/QueuePair.cpp @@ -0,0 +1,144 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "QueuePair.hpp" + +#include +#include +#include + +#include "Error.hpp" + +namespace catalyst::transport::common { + +namespace { +// RC QP tuning attributes for ibv_modify_qp (RTR/RTS). Encodings per the IB +// spec; see ibv_modify_qp(3). +constexpr std::uint8_t MIN_RNR_TIMER = 12; // RNR NAK timer code +constexpr std::uint8_t HOP_LIMIT = 64; // GRH hop limit +constexpr std::uint8_t QP_TIMEOUT = 14; // ACK timeout code +constexpr std::uint8_t RETRY_CNT = 7; // transport retry count +constexpr std::uint8_t RNR_RETRY = 7; // RNR retry count (7 = infinite) +constexpr std::uint8_t MAX_RD_ATOMIC = 1; // outstanding RDMA read/atomic ops +} // namespace + +QueuePair::QueuePair(std::shared_ptr pd, std::shared_ptr send_cq, + std::shared_ptr recv_cq, int max_send_wr, int max_inline) + : pd_(std::move(pd)), send_cq_(std::move(send_cq)), recv_cq_(std::move(recv_cq)) +{ + ibv_qp_init_attr a{ + .send_cq = send_cq_->get(), + .recv_cq = recv_cq_->get(), + .cap = + { + .max_send_wr = static_cast(max_send_wr), + .max_recv_wr = 4, + .max_send_sge = 1, + .max_recv_sge = 1, + .max_inline_data = static_cast(max_inline), + }, + .qp_type = IBV_QPT_RC, + .sq_sig_all = 0, + }; + qp_ = ibv_create_qp(pd_->get(), &a); + RDMA_CHECK(qp_, "ibv_create_qp"); +} + +QueuePair::~QueuePair() +{ + if (qp_) + ibv_destroy_qp(qp_); +} + +ibv_qp *QueuePair::get() const { return qp_; } +std::uint32_t QueuePair::qpn() const { return qp_->qp_num; } +QpState QueuePair::state() const { return state_; } + +void QueuePair::check_transition(QpState to) const +{ + if (!is_valid_transition(state_, to)) { + char m[128]; + std::snprintf(m, sizeof(m), "invalid QP transition %s -> %s", to_string(state_), + to_string(to)); + throw BadTransition(m); + } +} + +void QueuePair::modify(QpState to, ibv_qp_attr &attr, int mask, const char *what) +{ + check_transition(to); + // ibv_modify_qp returns the error code directly; errno is unreliable for + // it. + int rc = ibv_modify_qp(qp_, &attr, mask); + // ibv_modify_qp returns an errno-style code directly (errno itself is + // unreliable for it), so translate rc, not errno, to a readable message. + RDMA_CHECK(rc == 0, "%s rc=%d (%s)", what, rc, std::strerror(rc)); + state_ = to; // advance only after a successful modify +} + +void QueuePair::to_init(std::uint8_t port) +{ + ibv_qp_attr a{ + .qp_state = IBV_QPS_INIT, + .qp_access_flags = + IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ, + .pkey_index = 0, + .port_num = port, + }; + modify(QpState::INIT, a, IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_ACCESS_FLAGS, + "modify_to_init"); +} + +void QueuePair::to_rtr(std::uint32_t dest_qpn, std::uint32_t dest_psn, + const std::uint8_t dest_gid[16], int sgid_idx, std::uint8_t port, + std::uint32_t mtu_enum) +{ + ibv_qp_attr a{ + .qp_state = IBV_QPS_RTR, + .path_mtu = static_cast(mtu_enum), + .rq_psn = dest_psn, + .dest_qp_num = dest_qpn, + .ah_attr = + { + .grh = {.sgid_index = static_cast(sgid_idx), .hop_limit = HOP_LIMIT}, + .is_global = 1, + .port_num = port, + }, + .max_dest_rd_atomic = MAX_RD_ATOMIC, + .min_rnr_timer = MIN_RNR_TIMER, + }; + std::memcpy(&a.ah_attr.grh.dgid, dest_gid, 16); + modify(QpState::RTR, a, + IBV_QP_STATE | IBV_QP_AV | IBV_QP_PATH_MTU | IBV_QP_DEST_QPN | IBV_QP_RQ_PSN | + IBV_QP_MAX_DEST_RD_ATOMIC | IBV_QP_MIN_RNR_TIMER, + "modify_to_rtr"); +} + +void QueuePair::to_rts(std::uint32_t sq_psn) +{ + ibv_qp_attr a{ + .qp_state = IBV_QPS_RTS, + .sq_psn = sq_psn, + .max_rd_atomic = MAX_RD_ATOMIC, + .timeout = QP_TIMEOUT, + .retry_cnt = RETRY_CNT, + .rnr_retry = RNR_RETRY, + }; + modify(QpState::RTS, a, + IBV_QP_STATE | IBV_QP_TIMEOUT | IBV_QP_RETRY_CNT | IBV_QP_RNR_RETRY | IBV_QP_SQ_PSN | + IBV_QP_MAX_QP_RD_ATOMIC, + "modify_to_rts"); +} + +} // namespace catalyst::transport::common diff --git a/runtime/lib/transport/common/QueuePair.hpp b/runtime/lib/transport/common/QueuePair.hpp new file mode 100644 index 0000000000..7bbffcdf2e --- /dev/null +++ b/runtime/lib/transport/common/QueuePair.hpp @@ -0,0 +1,52 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include + +#include "CompletionQueue.hpp" +#include "ProtectionDomain.hpp" +#include "QpState.hpp" + +#include + +namespace catalyst::transport::common { +class QueuePair { + public: + QueuePair(std::shared_ptr pd, std::shared_ptr send_cq, + std::shared_ptr recv_cq, int max_send_wr, int max_inline = 0); + ~QueuePair(); + QueuePair(const QueuePair &) = delete; + QueuePair &operator=(const QueuePair &) = delete; + + ibv_qp *get() const; + std::uint32_t qpn() const; + QpState state() const; + + void to_init(std::uint8_t port); + void to_rtr(std::uint32_t dest_qpn, std::uint32_t dest_psn, const std::uint8_t dest_gid[16], + int sgid_idx, std::uint8_t port, std::uint32_t mtu_enum); + void to_rts(std::uint32_t sq_psn); + + private: + void check_transition(QpState to) const; + void modify(QpState to, ibv_qp_attr &attr, int mask, const char *what); + std::shared_ptr pd_; // keeps PD + (transitively) Context alive + std::shared_ptr send_cq_; // keeps the CQ alive + std::shared_ptr recv_cq_; + ibv_qp *qp_ = nullptr; + QpState state_ = QpState::RESET; +}; +} // namespace catalyst::transport::common diff --git a/runtime/lib/transport/common/WireProtocol.hpp b/runtime/lib/transport/common/WireProtocol.hpp new file mode 100644 index 0000000000..5e68b045b6 --- /dev/null +++ b/runtime/lib/transport/common/WireProtocol.hpp @@ -0,0 +1,60 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include + +namespace catalyst::transport::common { + +// Ring size, identical on both sides; power of two so index is a mask. +inline constexpr std::size_t K_RING_SLOTS = 256; + +// Selective-signalling stride (flow control on the pipelined send paths). +inline constexpr std::uint32_t SIGNAL_EVERY = 64; + +// Validation salt: packed into Payload.value low bits in the RTT self-test. +inline constexpr std::uint32_t SALT = 0xC0DE1515u; + +// Demo/loopback payload the controller ships each shot (stand-in for a real +// measurement outcome). Its (echo) decode is what the self-test checks. +inline constexpr std::uint64_t DEMO_SYNDROME = 0x0123456789ABCDEFull; + +// 16 B wire frame. +// Note: this application payload size is unrelated to the network MTU (the QP's +// max packet size, negotiated at RTR). The size here is far below any MTU, so +// each transfer is always a single packet. +#pragma pack(push, 1) +struct Payload { + std::uint64_t value; + std::uint32_t seq_num; + std::uint32_t pad; +}; +#pragma pack(pop) +static_assert(sizeof(Payload) == 16, "Payload must be exactly 16 B"); + +// Some controller DMA engine requires 64-B aligned. Rings are +// 64-B-strided slots; only the leading Payload (16 B) is transferred per slot. +struct alignas(64) PayloadSlot { + Payload p; + std::uint8_t pad_[48]; +}; +static_assert(sizeof(PayloadSlot) == 64, "PayloadSlot must be exactly 64 B"); +static_assert(alignof(PayloadSlot) == 64, "PayloadSlot must be 64-B aligned"); + +// Receive ring is K_RING_SLOTS PayloadSlots; the peer writes slot[cursor % +// K_RING_SLOTS]. K_RING_SLOTS must be a power of two. +inline constexpr std::size_t REGION_BYTES = K_RING_SLOTS * sizeof(PayloadSlot); + +} // namespace catalyst::transport::common diff --git a/runtime/lib/transport/cpu_verbs/CMakeLists.txt b/runtime/lib/transport/cpu_verbs/CMakeLists.txt new file mode 100644 index 0000000000..56cf7e8d50 --- /dev/null +++ b/runtime/lib/transport/cpu_verbs/CMakeLists.txt @@ -0,0 +1,35 @@ +############################################### +# CPU-verbs transport backend # +############################################### + +add_library(cpu_verbs_impl STATIC + base/CpuSessionBase.cpp + controller/CpuControllerSession.cpp + coprocessor/CpuCoprocessorSession.cpp +) +target_include_directories(cpu_verbs_impl PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} # CpuBackendConfig.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/base + ${CMAKE_CURRENT_SOURCE_DIR}/controller + ${CMAKE_CURRENT_SOURCE_DIR}/coprocessor + ${runtime_includes} # Transport.hpp, TransportBackend.h +) +target_link_libraries(cpu_verbs_impl PUBLIC transport_common) +set_property(TARGET cpu_verbs_impl PROPERTY POSITION_INDEPENDENT_CODE ON) + +# Controller library +add_library(catalyst_transport_cpu_controller SHARED controller/CpuControllerFactory.cpp) +target_link_libraries(catalyst_transport_cpu_controller PRIVATE cpu_verbs_impl) + +# Coprocessor library +add_library(catalyst_transport_cpu_coprocessor SHARED coprocessor/CpuCoprocessorFactory.cpp) +target_link_libraries(catalyst_transport_cpu_coprocessor PRIVATE cpu_verbs_impl) + +# Reference coprocessor function (Steane decode), built as a standalone shared lib +# exporting a CoprocessorFn symbol. +add_library(steane_coprocessor SHARED coprocessor/coprocessor_functions/steane_decoder_fn.cpp) +set_property(TARGET steane_coprocessor PROPERTY POSITION_INDEPENDENT_CODE ON) + +# Self-contained loopback application for testing +add_executable(cpu_verbs_selftest cpu_verbs_selftest.cpp) +target_link_libraries(cpu_verbs_selftest PRIVATE cpu_verbs_impl) diff --git a/runtime/lib/transport/cpu_verbs/CpuBackendConfig.hpp b/runtime/lib/transport/cpu_verbs/CpuBackendConfig.hpp new file mode 100644 index 0000000000..a50369a354 --- /dev/null +++ b/runtime/lib/transport/cpu_verbs/CpuBackendConfig.hpp @@ -0,0 +1,52 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include +#include + +namespace catalyst::transport::cpu_verbs { + +// Construction parameters parsed from a backend factory `config` string +// ("key=value;..."). Connection parameters (peer/oob_port) are not here — they +// arrive later via connect(). Recognised keys: `dev`, `gid`. +struct CpuConfig { + std::string dev = "rxe0"; + int gid = 1; +}; + +inline CpuConfig parse_cpu_config(const std::string &config) +{ + CpuConfig cfg; + for (std::size_t pos = 0; pos < config.size();) { + const std::size_t sep = config.find(';', pos); + const std::size_t end = (sep == std::string::npos) ? config.size() : sep; + const std::string_view tok(config.data() + pos, end - pos); + if (const std::size_t eq = tok.find('='); eq != std::string_view::npos) { + const std::string_view key = tok.substr(0, eq); + const std::string val(tok.substr(eq + 1)); + if (key == "dev") + cfg.dev = val; + else if (key == "gid") + cfg.gid = std::atoi(val.c_str()); + } + if (sep == std::string::npos) + break; + pos = sep + 1; + } + return cfg; +} + +} // namespace catalyst::transport::cpu_verbs diff --git a/runtime/lib/transport/cpu_verbs/base/CpuSessionBase.cpp b/runtime/lib/transport/cpu_verbs/base/CpuSessionBase.cpp new file mode 100644 index 0000000000..2efdbdb674 --- /dev/null +++ b/runtime/lib/transport/cpu_verbs/base/CpuSessionBase.cpp @@ -0,0 +1,253 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "CpuSessionBase.hpp" + +#include +#include +#include +#include +#include +#include + +#include "Error.hpp" +#include "Handshake.hpp" +#include "WireProtocol.hpp" + +namespace catalyst::transport::cpu_verbs { +using namespace catalyst::transport; +using namespace catalyst::transport::common; + +namespace { +// RDMA device port; rxe0 is single-port -> 1. +constexpr std::uint8_t PORT = 1; +// Max CQEs taken per non-blocking batch reap. +constexpr int REAP_BATCH = 16; +// QP inline capacity for inline sends (>= sizeof(Payload), 16 B). +constexpr int INLINE_MAX = 256; +constexpr int SQ_DEPTH = 4096; +constexpr int CQ_DEPTH = 4096; +} // namespace + +CpuSessionBase::CpuSessionBase(std::string dev, int gid_idx) + : dev_name_(std::move(dev)), gid_idx_(gid_idx) +{ +} + +int CpuSessionBase::connect(const ConnectInfo &info) +{ + ctx_ = std::make_shared(dev_name_); + pd_ = std::make_shared(ctx_); + fwd_cq_ = std::make_shared(ctx_, CQ_DEPTH); + bwd_cq_ = std::make_shared(ctx_, CQ_DEPTH); + fwd_qp_ = std::make_shared(pd_, fwd_cq_, fwd_cq_, SQ_DEPTH, INLINE_MAX); + bwd_qp_ = std::make_shared(pd_, bwd_cq_, bwd_cq_, SQ_DEPTH, INLINE_MAX); + fwd_qp_->to_init(PORT); + bwd_qp_->to_init(PORT); + active_mtu_ = static_cast(ctx_->port_attr(PORT).active_mtu); + mygid_ = ctx_->gid(PORT, gid_idx_); + const bool listen = oob_listens(); + oob_fd_ = + listen ? tcp_listen_accept(info.oob_port) : tcp_connect(info.peer.c_str(), info.oob_port); + return 0; +} + +MemRegion CpuSessionBase::alloc_memory(std::size_t size, MemKind kind) +{ + RDMA_CHECK(kind == MemKind::CpuRam, "cpu_libibverbs: only MemKind::CpuRam supported"); + caller_memory_regions_.push_back(MemoryRegion::alloc_host( + pd_, size, 4096, MemAccess::LOCAL_WRITE | MemAccess::REMOTE_WRITE)); + const MemoryRegion &mr = caller_memory_regions_.back(); + return MemRegion{ + .addr = mr.addr(), + .size = size, + .lkey = mr.lkey(), + .rkey = mr.rkey(), + .kind = MemKind::CpuRam, + }; +} + +PeerRef CpuSessionBase::exchange_keys(const MemRegion &local) +{ + HandshakeMsg my{ + .fwd = {.qpn = fwd_qp_->qpn(), .psn = 0}, + .bwd = {.qpn = bwd_qp_->qpn(), .psn = 0}, + .mr_vaddr = reinterpret_cast(local.addr), + .mr_rkey = local.rkey, + .mtu_enum = active_mtu_, + }; + // gid is a 16-byte array copied from the local GID after the aggregate + // init. + std::memcpy(my.fwd.gid, &mygid_, sizeof(mygid_)); + std::memcpy(my.bwd.gid, &mygid_, sizeof(mygid_)); + HandshakeMsg peer{}; + const int fd = oob_fd_.get(); + const bool listen = oob_listens(); + if (listen) { + send_exact(fd, &my, sizeof(my)); + recv_exact(fd, &peer, sizeof(peer)); + } + else { + recv_exact(fd, &peer, sizeof(peer)); + send_exact(fd, &my, sizeof(my)); + } + const std::uint32_t mtu = std::min(my.mtu_enum, peer.mtu_enum); + fwd_qp_->to_rtr(peer.fwd.qpn, peer.fwd.psn, peer.fwd.gid, gid_idx_, PORT, mtu); + bwd_qp_->to_rtr(peer.bwd.qpn, peer.bwd.psn, peer.bwd.gid, gid_idx_, PORT, mtu); + fwd_qp_->to_rts(my.fwd.psn); + bwd_qp_->to_rts(my.bwd.psn); + return PeerRef{ + .rkey = peer.mr_rkey, + .remote_addr = peer.mr_vaddr, + .size = REGION_BYTES, + }; +} + +void CpuSessionBase::establish_channel(const ChannelDesc &desc, const MemRegion &local, + const PeerRef &peer) +{ + RDMA_CHECK(local.size >= REGION_BYTES, "region too small for ring: %zu < %zu", local.size, + REGION_BYTES); + desc_ = desc; + local_ = local; + peer_ = peer; + send_buf_ = + common::MemoryRegion::alloc_host(pd_, sizeof(Payload), 64, common::MemAccess::LOCAL_WRITE); +} + +void CpuSessionBase::post_write(ibv_qp *qp, std::uint64_t cursor, bool inline_data, bool signaled) +{ + auto *send = send_payload(); // value already written by the caller + send->seq_num = static_cast(cursor + 1); + send->pad = 0; + ibv_sge sge{ + .addr = reinterpret_cast(send), + .length = sizeof(Payload), // 16 B on the wire, into slot's first 16 B + .lkey = send_buf_->lkey(), + }; + ibv_send_wr wr{ + .sg_list = &sge, + .num_sge = 1, + .opcode = IBV_WR_RDMA_WRITE, + .send_flags = static_cast((inline_data ? IBV_SEND_INLINE : 0) | + (signaled ? IBV_SEND_SIGNALED : 0)), + }; + wr.wr.rdma.remote_addr = + peer_.remote_addr + (cursor & (K_RING_SLOTS - 1)) * sizeof(PayloadSlot); + wr.wr.rdma.rkey = peer_.rkey; + ibv_send_wr *bad = nullptr; + RDMA_CHECK(ibv_post_send(qp, &wr, &bad) == 0, "ibv_post_send"); +} + +// Non-blocking batch reap of `cq`: take whatever completions are ready (up to +// REAP_BATCH) and decrement `outstanding`. With drain=true, keep polling until +// every signaled send has completed (teardown), guarded so a lost CQE can't +// hang the join. +void CpuSessionBase::reap(ibv_cq *cq, int &outstanding, bool drain) +{ + std::array wc{}; + int empty = 0; + constexpr int DRAIN_MAX_EMPTY = 1000000; + do { + int n = ibv_poll_cq(cq, static_cast(wc.size()), wc.data()); + if (n == 0) { + if (!drain) + return; + if (++empty >= DRAIN_MAX_EMPTY) + return; + continue; + } + empty = 0; + for (int k = 0; k < n; ++k) { + RDMA_CHECK(wc[k].status == IBV_WC_SUCCESS, "CQE status=%d", wc[k].status); + --outstanding; + } + } while (drain && outstanding > 0); +} + +Payload *CpuSessionBase::poll_message_arrival(std::uint64_t cursor, std::stop_token st) +{ + // Slots are reused (K_RING_SLOTS is a power of two). The ring contains + // 64 B PayloadSlots; the peer writes the 16 B Payload into each slot's + // head. + auto *ring = reinterpret_cast(local_.addr); + Payload *slot = &ring[cursor & (K_RING_SLOTS - 1)].p; + // Poll seq_num with acquire ordering: once it updates, value (written + // before it in the single RDMA_WRITE) is present, and the acquire keeps its + // read from being hoisted ahead. + std::atomic_ref seq_ref(slot->seq_num); + const auto expected = static_cast(cursor + 1); + while (seq_ref.load(std::memory_order_acquire) != expected) { + if (st.stop_requested()) + return nullptr; + std::this_thread::yield(); + } + return slot; +} + +void CpuSessionBase::start() +{ + stop(); + failed_.store(false, std::memory_order_relaxed); + error_ = nullptr; + completed_.store(0, std::memory_order_relaxed); + last_word_.store(0, std::memory_order_relaxed); + // jthread injects the stop_token. A data-path RDMA_CHECK throws RdmaError; + // it must not escape the thread function (that would std::terminate). + // Capture it into error_ and publish via failed_ (release) so collect() + // can rethrow the real exception. + auto body = [this](std::stop_token st) { + try { + run(st); + } + catch (...) { + error_ = std::current_exception(); + failed_.store(true, std::memory_order_release); + } + }; + engine_ = std::jthread(body); +} + +int CpuSessionBase::collect(void *const *replies, const std::uint64_t *replies_bytes, std::size_t n) +{ + while (completed_.load(std::memory_order_acquire) == 0) { + if (failed_.load(std::memory_order_acquire)) + std::rethrow_exception(error_); // surface the engine's real error + if (!engine_.joinable() || engine_.get_stop_token().stop_requested()) + break; + std::this_thread::yield(); + } + if (failed_.load(std::memory_order_acquire)) + std::rethrow_exception(error_); + // Stopped before any round completed -> no data (non-exceptional). + if (completed_.load(std::memory_order_acquire) == 0) + return -1; + if (n > 0 && replies && replies[0]) { + const std::uint64_t w = last_word_.load(std::memory_order_relaxed); + const std::size_t nb = + replies_bytes ? std::min(replies_bytes[0], sizeof(w)) : sizeof(w); + std::memcpy(replies[0], &w, nb); + } + return 0; +} + +void CpuSessionBase::stop() +{ + if (engine_.joinable()) { + engine_.request_stop(); + engine_.join(); + } +} + +} // namespace catalyst::transport::cpu_verbs diff --git a/runtime/lib/transport/cpu_verbs/base/CpuSessionBase.hpp b/runtime/lib/transport/cpu_verbs/base/CpuSessionBase.hpp new file mode 100644 index 0000000000..c0725121d6 --- /dev/null +++ b/runtime/lib/transport/cpu_verbs/base/CpuSessionBase.hpp @@ -0,0 +1,90 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CompletionQueue.hpp" +#include "Context.hpp" +#include "MemoryRegion.hpp" +#include "OobSocket.hpp" +#include "ProtectionDomain.hpp" +#include "QueuePair.hpp" +#include "Transport.hpp" +#include "WireProtocol.hpp" + +namespace catalyst::transport::cpu_verbs { +using namespace catalyst::transport; + +// Shared lifecycle for coprocessor and controller roles. +class CpuSessionBase : public TransportSession { + public: + explicit CpuSessionBase(std::string dev = "rxe0", int gid_idx = 1); + ~CpuSessionBase() override { stop(); } + + int connect(const ConnectInfo &info) override; + MemRegion alloc_memory(std::size_t size, MemKind kind) override; + PeerRef exchange_keys(const MemRegion &local) override; + void establish_channel(const ChannelDesc &desc, const MemRegion &local, + const PeerRef &peer) override; + void start() override; + int collect(void *const *replies, const std::uint64_t *replies_bytes, std::size_t n) override; + void stop() override; + + protected: + // The role-specific engine loop (runs on engine_). + virtual void run(std::stop_token st) = 0; + // True for the coprocessor (listens/sends first on the OOB socket). + virtual bool oob_listens() const = 0; + + void post_write(ibv_qp *qp, std::uint64_t cursor, bool inline_data, bool signaled); + void reap(ibv_cq *cq, int &outstanding, bool drain); + common::Payload *poll_message_arrival(std::uint64_t cursor, std::stop_token st); + common::Payload *send_payload() + { + return reinterpret_cast(send_buf_->addr()); + } + + std::string dev_name_; + int gid_idx_; + ibv_gid mygid_{}; // local GID, cached in connect() for exchange_keys() + std::uint32_t active_mtu_ = 0; // local active_mtu enum, cached in connect() + std::shared_ptr ctx_; + std::shared_ptr pd_; + std::shared_ptr fwd_cq_, bwd_cq_; + std::shared_ptr fwd_qp_, bwd_qp_; + common::FdGuard oob_fd_; + std::vector caller_memory_regions_; + std::optional send_buf_; // local send source (one Payload) + MemRegion local_{}; + PeerRef peer_{}; + ChannelDesc desc_{}; + // failed_ (release) publishes error_; collect() acquire-loads it and + // rethrows. + std::atomic failed_{false}; + std::exception_ptr error_; + std::atomic completed_{0}; + std::atomic last_word_{0}; + std::jthread engine_; +}; + +} // namespace catalyst::transport::cpu_verbs diff --git a/runtime/lib/transport/cpu_verbs/controller/CpuControllerFactory.cpp b/runtime/lib/transport/cpu_verbs/controller/CpuControllerFactory.cpp new file mode 100644 index 0000000000..afa787f159 --- /dev/null +++ b/runtime/lib/transport/cpu_verbs/controller/CpuControllerFactory.cpp @@ -0,0 +1,32 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Plugin entry point for the CPU-verbs controller backend. The runtime dlopen's +// this .so and resolves CatalystTransportControllerFactory (see TransportBackend.h). + +#include + +#include "CpuBackendConfig.hpp" +#include "CpuControllerSession.hpp" +#include "TransportBackend.h" + +namespace { +catalyst::transport::ControllerSession *make_cpu_controller(const std::string &config) +{ + const auto cfg = catalyst::transport::cpu_verbs::parse_cpu_config(config); + return new catalyst::transport::cpu_verbs::CpuControllerSession(cfg.dev, cfg.gid); +} +} // namespace + +GENERATE_TRANSPORT_CONTROLLER_FACTORY(CatalystTransportController, make_cpu_controller) diff --git a/runtime/lib/transport/cpu_verbs/controller/CpuControllerSession.cpp b/runtime/lib/transport/cpu_verbs/controller/CpuControllerSession.cpp new file mode 100644 index 0000000000..68b0b7fcfb --- /dev/null +++ b/runtime/lib/transport/cpu_verbs/controller/CpuControllerSession.cpp @@ -0,0 +1,98 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "CpuControllerSession.hpp" + +#include +#include +#include +#include + +#include "WireProtocol.hpp" + +namespace catalyst::transport::cpu_verbs { +using namespace catalyst::transport::common; + +namespace { +std::uint64_t now_ns() +{ + return static_cast(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count()); +} +} // namespace + +void CpuControllerSession::Impl::start() +{ + stop(); // drain any leftovers + reset (idempotent) + next_send_ = 0; + next_recv_ = 0; + signaled_outstanding_ = 0; + rtt_ns_ = 0; +} + +void CpuControllerSession::Impl::stop() +{ + if (fwd_cq_) + reap(fwd_cq_->get(), signaled_outstanding_, /*drain=*/true); + CpuSessionBase::stop(); // no engine thread runs for the controller; harmless join +} + +// Single work item, fixed-size frame: work_item_idx is ignored; the sizes are +// just recorded (out_bytes_ caps the reply in collect()). +void CpuControllerSession::Impl::commit_work_item(std::uint32_t /*work_item_idx*/, + std::uint64_t in_bytes, std::uint64_t out_bytes) +{ + in_bytes_ = in_bytes; + out_bytes_ = out_bytes; +} + +void *CpuControllerSession::Impl::data_slot() +{ + // Current round's outbound slot: the caller writes up to in_bytes_ here, then kick()s. + return &send_payload()->value; +} + +int CpuControllerSession::Impl::kick(std::uint32_t /*work_item_idx*/) +{ + // The payload was written into data_slot() by the caller; fire one round. + kick_ns_ = now_ns(); + const bool sig = (next_send_ % SIGNAL_EVERY == 0); + post_write(fwd_qp_->get(), next_send_, /*inline_data=*/true, sig); + if (sig) { + ++signaled_outstanding_; + reap(fwd_cq_->get(), signaled_outstanding_, /*drain=*/false); // lazy: free the SQ + } + ++next_send_; + return 0; +} + +int CpuControllerSession::Impl::collect(void *const *replies, const std::uint64_t *replies_bytes, + std::size_t n) +{ + std::stop_token none; // blocking wait for this round's reply + Payload *r = poll_message_arrival(next_recv_, none); + if (!r) + return -1; + rtt_ns_ = now_ns() - kick_ns_; + ++next_recv_; + if (n > 0 && replies && replies[0]) { + const std::size_t cap = replies_bytes ? replies_bytes[0] : out_bytes_; + const std::size_t nb = std::min(cap, sizeof(r->value)); + std::memcpy(replies[0], &r->value, nb); + } + return 0; +} + +} // namespace catalyst::transport::cpu_verbs diff --git a/runtime/lib/transport/cpu_verbs/controller/CpuControllerSession.hpp b/runtime/lib/transport/cpu_verbs/controller/CpuControllerSession.hpp new file mode 100644 index 0000000000..d35b527bcb --- /dev/null +++ b/runtime/lib/transport/cpu_verbs/controller/CpuControllerSession.hpp @@ -0,0 +1,95 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include + +#include "CpuSessionBase.hpp" + +namespace catalyst::transport::cpu_verbs { + +// Controller role: caller-driven. The caller commits a work item +// (I/O sizes), writes the outbound payload into data_slot(), kick()s one round, +// then collect()s the reply. No internal engine thread. +class CpuControllerSession : public ControllerSession { + public: + explicit CpuControllerSession(std::string dev = "rxe0", int gid_idx = 1) + : base_(std::move(dev), gid_idx) + { + } + + int connect(const ConnectInfo &info) override { return base_.connect(info); } + MemRegion alloc_memory(std::size_t size, MemKind kind) override + { + return base_.alloc_memory(size, kind); + } + PeerRef exchange_keys(const MemRegion &local) override { return base_.exchange_keys(local); } + void establish_channel(const ChannelDesc &desc, const MemRegion &local, + const PeerRef &peer) override + { + base_.establish_channel(desc, local, peer); + } + void start() override { base_.start(); } + int collect(void *const *replies, const std::uint64_t *replies_bytes, std::size_t n) override + { + return base_.collect(replies, replies_bytes, n); + } + void stop() override { base_.stop(); } + std::uint64_t last_rtt_ns() const override { return base_.last_rtt_ns(); } + + // ControllerSession interface. + void commit_work_item(std::uint32_t work_item_idx, std::uint64_t in_bytes, + std::uint64_t out_bytes) override + { + base_.commit_work_item(work_item_idx, in_bytes, out_bytes); + } + int kick(std::uint32_t work_item_idx = 0) override { return base_.kick(work_item_idx); } + void *data_slot() override { return base_.data_slot(); } + + private: + // Caller-driven controller over the shared session primitives. run() is unused + // (no engine thread); start()/stop()/collect() are overridden for the + // synchronous kick model. + class Impl : public CpuSessionBase { + public: + using CpuSessionBase::CpuSessionBase; + ~Impl() { stop(); } + + void start() override; + void stop() override; + int collect(void *const *replies, const std::uint64_t *replies_bytes, + std::size_t n) override; + std::uint64_t last_rtt_ns() const override { return rtt_ns_; } + + void commit_work_item(std::uint32_t work_item_idx, std::uint64_t in_bytes, + std::uint64_t out_bytes); + int kick(std::uint32_t work_item_idx); + void *data_slot(); + + protected: + void run(std::stop_token) override {} // unused: controller is caller-driven + bool oob_listens() const override { return false; } + + private: + // Sizes from commit_work_item are fixed in CPU controller. + std::uint64_t in_bytes_ = sizeof(common::Payload::value); + std::uint64_t out_bytes_ = sizeof(common::Payload::value); + std::uint64_t next_send_ = 0, next_recv_ = 0; + int signaled_outstanding_ = 0; + std::uint64_t kick_ns_ = 0, rtt_ns_ = 0; + }; + Impl base_; +}; + +} // namespace catalyst::transport::cpu_verbs diff --git a/runtime/lib/transport/cpu_verbs/coprocessor/CpuCoprocessorFactory.cpp b/runtime/lib/transport/cpu_verbs/coprocessor/CpuCoprocessorFactory.cpp new file mode 100644 index 0000000000..50fe36cfbc --- /dev/null +++ b/runtime/lib/transport/cpu_verbs/coprocessor/CpuCoprocessorFactory.cpp @@ -0,0 +1,35 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Plugin entry point for the CPU-verbs coprocessor backend. A loader dlopen's +// this .so and resolves CatalystTransportCoprocessorFactory (see TransportBackend.h). +// The coprocessor function is bound after construction via set_coprocessor_fn() +// (by the coprocessor-side harness); a freshly built session defaults to the +// built-in echo. + +#include + +#include "CpuBackendConfig.hpp" +#include "CpuCoprocessorSession.hpp" +#include "TransportBackend.h" + +namespace { +catalyst::transport::CoprocessorSession *make_cpu_coprocessor(const std::string &config) +{ + const auto cfg = catalyst::transport::cpu_verbs::parse_cpu_config(config); + return new catalyst::transport::cpu_verbs::CpuCoprocessorSession(cfg.dev, cfg.gid); +} +} // namespace + +GENERATE_TRANSPORT_COPROCESSOR_FACTORY(CatalystTransportCoprocessor, make_cpu_coprocessor) diff --git a/runtime/lib/transport/cpu_verbs/coprocessor/CpuCoprocessorSession.cpp b/runtime/lib/transport/cpu_verbs/coprocessor/CpuCoprocessorSession.cpp new file mode 100644 index 0000000000..c31e97f3a5 --- /dev/null +++ b/runtime/lib/transport/cpu_verbs/coprocessor/CpuCoprocessorSession.cpp @@ -0,0 +1,62 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "CpuCoprocessorSession.hpp" + +#include +#include + +#include "WireProtocol.hpp" + +namespace catalyst::transport::cpu_verbs { +using namespace catalyst::transport::common; + +void CpuCoprocessorSession::set_coprocessor_fn(CoprocessorFn fn, void *ctx) +{ + base_.coproc_fn_ = fn; + base_.coproc_ctx_ = ctx; +} + +// Coprocessor: wait for a message, run the coprocessor function into the send +// buffer in place, then send the result. A null fn is the built-in echo +// (passthrough). Replies are inline + selectively signaled; the bwd CQ is +// reaped in batches at signal points. +void CpuCoprocessorSession::Impl::run(std::stop_token st) +{ + int signaled_outstanding = 0; + for (std::uint64_t c = 0; !st.stop_requested(); c++) { + Payload *r = poll_message_arrival(c, st); // the incoming message + if (!r) { + reap(bwd_cq_->get(), signaled_outstanding, /*drain=*/true); + return; + } + last_word_.store(r->value, std::memory_order_relaxed); + completed_.fetch_add(1, std::memory_order_release); + Payload *send = send_payload(); + send->value = 0; // deterministic high bytes when the result is shorter + if (coproc_fn_) + coproc_fn_(&r->value, sizeof(r->value), &send->value, sizeof(send->value), coproc_ctx_); + else + send->value = r->value; // built-in echo + const bool sig = (c % SIGNAL_EVERY == 0); + post_write(bwd_qp_->get(), c, /*inline_data=*/true, sig); + if (sig) { + ++signaled_outstanding; + reap(bwd_cq_->get(), signaled_outstanding, /*drain=*/false); + } + } + reap(bwd_cq_->get(), signaled_outstanding, /*drain=*/true); +} + +} // namespace catalyst::transport::cpu_verbs diff --git a/runtime/lib/transport/cpu_verbs/coprocessor/CpuCoprocessorSession.hpp b/runtime/lib/transport/cpu_verbs/coprocessor/CpuCoprocessorSession.hpp new file mode 100644 index 0000000000..c0422825c0 --- /dev/null +++ b/runtime/lib/transport/cpu_verbs/coprocessor/CpuCoprocessorSession.hpp @@ -0,0 +1,68 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include + +#include "CpuSessionBase.hpp" + +namespace catalyst::transport::cpu_verbs { + +// Coprocessor role: receives messages, runs the coprocessor function, and +// returns the result. The function is bound via set_coprocessor_fn; nullptr +// selects the built-in echo (passthrough self-test). +class CpuCoprocessorSession : public CoprocessorSession { + public: + explicit CpuCoprocessorSession(std::string dev = "rxe0", int gid_idx = 1) + : base_(std::move(dev), gid_idx) + { + } + + int connect(const ConnectInfo &info) override { return base_.connect(info); } + MemRegion alloc_memory(std::size_t size, MemKind kind) override + { + return base_.alloc_memory(size, kind); + } + PeerRef exchange_keys(const MemRegion &local) override { return base_.exchange_keys(local); } + void establish_channel(const ChannelDesc &desc, const MemRegion &local, + const PeerRef &peer) override + { + base_.establish_channel(desc, local, peer); + } + void start() override { base_.start(); } + int collect(void *const *replies, const std::uint64_t *replies_bytes, std::size_t n) override + { + return base_.collect(replies, replies_bytes, n); + } + void stop() override { base_.stop(); } + + void set_coprocessor_fn(CoprocessorFn fn, void *ctx) override; + + private: + class Impl : public CpuSessionBase { + public: + using CpuSessionBase::CpuSessionBase; + ~Impl() { stop(); } + CoprocessorFn coproc_fn_ = nullptr; // nullptr -> built-in echo + void *coproc_ctx_ = nullptr; + + protected: + void run(std::stop_token st) override; + bool oob_listens() const override { return true; } + }; + Impl base_; +}; + +} // namespace catalyst::transport::cpu_verbs diff --git a/runtime/lib/transport/cpu_verbs/coprocessor/coprocessor_functions/steane_decoder_fn.cpp b/runtime/lib/transport/cpu_verbs/coprocessor/coprocessor_functions/steane_decoder_fn.cpp new file mode 100644 index 0000000000..383b416f10 --- /dev/null +++ b/runtime/lib/transport/cpu_verbs/coprocessor/coprocessor_functions/steane_decoder_fn.cpp @@ -0,0 +1,52 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +/** + * @file + * A reference CoprocessorFn implementing a [[7,1,3]] Steane-code decode. + */ + +#include +#include +#include + +/** + * @brief A hard-coded [[7,1,3]] Steane-code decode, exposed as a CoprocessorFn + * (see Transport.hpp) — the general "run this on the coprocessor" contract that + * supersedes the old decoder-plugin ABI. + * + * @note The FTQC (Fault-Tolerant Quantum Computing) compilation pipeline + * dispatches either X-check or Z-check syndromes independently per call, so each + * call carries a single 3-bit check. This may be unified in future iterations. + * + * @param in Pointer to the inbound syndrome measurements. + * @param in_len Length of the inbound syndrome, in bytes. + * @param out Pointer to the outbound correction buffer. + * @param out_cap Capacity of the outbound buffer, in bytes. + * @param ctx Opaque context (unused). + * @return Number of bytes written to @p out. + */ +extern "C" std::size_t steane_coprocessor(const void *in, std::size_t in_len, void *out, + std::size_t out_cap, void * /*ctx*/) +{ + std::uint64_t syndrome = 0; + std::memcpy(&syndrome, in, in_len < sizeof(syndrome) ? in_len : sizeof(syndrome)); + // One 3-bit check (X or Z) per call; the nonzero index selects the single + // corrected qubit (0 => no error). + const std::uint32_t check = syndrome & 0x7u; + const std::uint64_t correction = static_cast(check ? (1u << (check - 1)) : 0u); + const std::size_t nb = out_cap < sizeof(correction) ? out_cap : sizeof(correction); + std::memset(out, 0, out_cap); + std::memcpy(out, &correction, nb); + return nb; +} diff --git a/runtime/lib/transport/cpu_verbs/cpu_verbs_selftest.cpp b/runtime/lib/transport/cpu_verbs/cpu_verbs_selftest.cpp new file mode 100644 index 0000000000..646e98fc29 --- /dev/null +++ b/runtime/lib/transport/cpu_verbs/cpu_verbs_selftest.cpp @@ -0,0 +1,122 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CpuControllerSession.hpp" +#include "CpuCoprocessorSession.hpp" +#include "WireProtocol.hpp" + +using namespace catalyst::transport; +using namespace catalyst::transport::cpu_verbs; +using namespace catalyst::transport::common; // REGION_BYTES, DEMO_SYNDROME, Payload + +int main(int argc, char **argv) +{ + std::string role = "coprocessor", dev = "rxe0", peer = "127.0.0.1"; + int gid = 1; + std::uint16_t port = 18560; + // Message sizes (bytes). The controller role feeds these to commit_work_item; + // both roles size the collect() buffer by --correction-bytes. + std::uint32_t syndrome_bytes = sizeof(std::uint64_t); + std::uint32_t correction_bytes = sizeof(std::uint64_t); + for (int i = 1; i + 1 < argc; i += 2) { + std::string k = argv[i], v = argv[i + 1]; + if (k == "--role") + role = v; + else if (k == "--dev") + dev = v; + else if (k == "--gid") + gid = std::atoi(v.c_str()); + else if (k == "--peer") + peer = v; + else if (k == "--port") + port = static_cast(std::atoi(v.c_str())); + else if (k == "--syndrome-bytes") + syndrome_bytes = static_cast(std::strtoul(v.c_str(), nullptr, 0)); + else if (k == "--correction-bytes") + correction_bytes = static_cast(std::strtoul(v.c_str(), nullptr, 0)); + } + const bool is_coprocessor = (role == "coprocessor"); + + std::unique_ptr s; + CpuCoprocessorSession *coproc = nullptr; + CpuControllerSession *controller = nullptr; + if (is_coprocessor) { + auto up = std::make_unique(dev, gid); + coproc = up.get(); + s = std::move(up); + } + else { + auto up = std::make_unique(dev, gid); + controller = up.get(); + s = std::move(up); + } + + ConnectInfo ci{ + .peer = peer, + .oob_port = port, + }; + s->connect(ci); + MemRegion m = s->alloc_memory(REGION_BYTES, MemKind::CpuRam); + PeerRef p = s->exchange_keys(m); + ChannelDesc desc{ + .data_path = "cpu_verbs", + }; + s->establish_channel(desc, m, p); + + // Reply buffer that collect() fills with up to --correction-bytes. Sized to at + // least 8 B so the echo check below can always read a full 64-bit word: `got` + // copies the leading 8 bytes of the reply and compares them to DEMO_SYNDROME. + std::vector corr(std::max(correction_bytes, sizeof(std::uint64_t)), + 0); + void *outs[1] = {corr.data()}; + std::uint64_t obytes[1] = {correction_bytes}; + if (coproc) { + coproc->set_coprocessor_fn(nullptr, nullptr); // built-in echo + coproc->start(); + std::this_thread::sleep_for(std::chrono::seconds(3)); // serve ~3 s + coproc->collect(outs, obytes, 1); + coproc->stop(); + } + else { + // Controller: commit a work item sized by --syndrome-bytes/--correction-bytes, + // write the syndrome into data_slot(), kick one round, collect the correction. + controller->commit_work_item(/*work_item_idx=*/0, syndrome_bytes, correction_bytes); + controller->start(); + const std::uint64_t syndrome = DEMO_SYNDROME; + std::memcpy(controller->data_slot(), &syndrome, sizeof(syndrome)); + controller->kick(0); + controller->collect(outs, obytes, 1); + controller->stop(); + } + + std::uint64_t got = 0; + std::memcpy(&got, corr.data(), sizeof(got)); + + // Built-in echo coprocessor: both roles observe the demo syndrome. + const bool pass = (got == DEMO_SYNDROME); + std::fprintf(stderr, "[%s] got=0x%llx expect=0x%llx -> %s\n", role.c_str(), + static_cast(got), + static_cast(DEMO_SYNDROME), pass ? "PASS" : "FAIL"); + return pass ? 0 : 1; +} diff --git a/runtime/lib/transport/cpu_verbs/run_loopback.sh b/runtime/lib/transport/cpu_verbs/run_loopback.sh new file mode 100755 index 0000000000..81cb6a452f --- /dev/null +++ b/runtime/lib/transport/cpu_verbs/run_loopback.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# +# Manual two-process loopback test for the CPU-verbs transport. Launches +# the cpu_verbs_selftest binary twice on this host, talking to each other over a +# local RDMA device (default rxe0 / SoftRoCE): +# - coprocessor: background; listens on the OOB port and runs the built-in echo +# - controller: foreground; connects, sends one syndrome, collects the reply +# The echo bounces the syndrome back, so both roles observe DEMO_SYNDROME and exit +# 0, and the script prints "LOOPBACK: PASS". +# +# Requires a working RDMA device (e.g. rxe0 SoftRoCE). +# Env overrides: DEV (rxe0), GID (1), PORT (18560), BIN (built selftest path). +set -u +DEV=${DEV:-rxe0}; GID=${GID:-1}; PORT=${PORT:-18560} +BIN=${BIN:-"$(cd "$(dirname "$0")" && pwd)/../../../build/lib/transport/cpu_verbs/cpu_verbs_selftest"} +[ -x "$BIN" ] || { echo "binary not found: $BIN (build the cpu_verbs_selftest target first)"; exit 2; } + +timeout 30 "$BIN" --role coprocessor --dev "$DEV" --gid "$GID" --port "$PORT" \ + >/tmp/cvl_coproc.log 2>&1 & +SPID=$! +sleep 1 +timeout 30 "$BIN" --role controller --dev "$DEV" --gid "$GID" --peer 127.0.0.1 --port "$PORT" \ + >/tmp/cvl_ctrl.log 2>&1 +CRC=$? +wait $SPID; SRC=$? +echo "=== coprocessor ==="; cat /tmp/cvl_coproc.log +echo "=== controller ==="; cat /tmp/cvl_ctrl.log +echo "coprocessor rc=$SRC controller rc=$CRC" +[ "$SRC" = 0 ] && [ "$CRC" = 0 ] && echo "LOOPBACK: PASS" || { echo "LOOPBACK: FAIL"; exit 1; } diff --git a/runtime/tests/CMakeLists.txt b/runtime/tests/CMakeLists.txt index bd9f9ba4d0..cc3ab42951 100644 --- a/runtime/tests/CMakeLists.txt +++ b/runtime/tests/CMakeLists.txt @@ -166,3 +166,48 @@ target_link_libraries(runner_tests_rsdecomp_runtime PRIVATE ) catch_discover_tests(runner_tests_rsdecomp_runtime) + +# Transport test suites +if(ENABLE_TRANSPORT) + # CAPI + loader, exercised against a stub backend plugin. + add_library(stub_transport_backend SHARED stubs/stub_transport_backend.cpp) + target_include_directories(stub_transport_backend PRIVATE ${runtime_includes}) + set_property(TARGET stub_transport_backend PROPERTY POSITION_INDEPENDENT_CODE ON) + + add_executable(runner_tests_transport) + target_sources(runner_tests_transport PRIVATE Test_Transport.cpp) + target_link_libraries(runner_tests_transport PRIVATE + Catch2WithMain + catalyst_runtime_testing + rt_transport + ) + target_compile_definitions(runner_tests_transport PRIVATE + STUB_BACKEND_PATH="$") + add_dependencies(runner_tests_transport stub_transport_backend) + catch_discover_tests(runner_tests_transport) + + # CPU-verbs backend: device-agnostic common primitives (RDMA-backed; SKIP without rxe0). + add_executable(runner_tests_transport_common) + target_sources(runner_tests_transport_common PRIVATE + Test_TransportCommon.cpp + Test_TransportWireProtocol.cpp + ) + target_link_libraries(runner_tests_transport_common PRIVATE + Catch2WithMain + catalyst_runtime_testing + transport_common + ) + catch_discover_tests(runner_tests_transport_common) + + # CPU-verbs backend: both controller and coprocessor (requires RDMA - skip without rxe0). + add_executable(runner_tests_transport_cpu) + target_sources(runner_tests_transport_cpu PRIVATE + Test_TransportCpuVerbs.cpp + ) + target_link_libraries(runner_tests_transport_cpu PRIVATE + Catch2WithMain + catalyst_runtime_testing + cpu_verbs_impl + ) + catch_discover_tests(runner_tests_transport_cpu) +endif() diff --git a/runtime/tests/Test_Transport.cpp b/runtime/tests/Test_Transport.cpp new file mode 100644 index 0000000000..512fc3350e --- /dev/null +++ b/runtime/tests/Test_Transport.cpp @@ -0,0 +1,140 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Unit tests for the transport CAPI session registry and per-call behavior + +#include + +#include "catch2/catch_test_macros.hpp" + +#include "TransportCAPI.h" + +namespace { +constexpr std::int32_t kController = CATALYST_TRANSPORT_ROLE_CONTROLLER; +constexpr std::int32_t kCoprocessor = CATALYST_TRANSPORT_ROLE_COPROCESSOR; +constexpr const char *kStub = STUB_BACKEND_PATH; + +CatalystTransportSession *make(std::int32_t role, const char *key) +{ + return __catalyst__transport__create(kStub, "cfg", role, key); +} +} // namespace + +TEST_CASE("create registers a session resolvable by (role, key)", "[transport]") +{ + auto *s = make(kController, "reg_ctrl"); + REQUIRE(s != nullptr); + CHECK(__catalyst__transport__get_session(kController, "reg_ctrl") == s); + // Unknown key and mismatched role both miss. + CHECK(__catalyst__transport__get_session(kController, "reg_absent") == nullptr); + CHECK(__catalyst__transport__get_session(kCoprocessor, "reg_ctrl") == nullptr); + __catalyst__transport__destroy(s); + // destroy unregisters. + CHECK(__catalyst__transport__get_session(kController, "reg_ctrl") == nullptr); +} + +TEST_CASE("role disambiguates the same key", "[transport]") +{ + auto *ct = make(kController, "dis_key"); + auto *co = make(kCoprocessor, "dis_key"); + REQUIRE(ct != nullptr); + REQUIRE(co != nullptr); + REQUIRE(ct != co); + CHECK(__catalyst__transport__get_session(kController, "dis_key") == ct); + CHECK(__catalyst__transport__get_session(kCoprocessor, "dis_key") == co); + __catalyst__transport__destroy(ct); + __catalyst__transport__destroy(co); +} + +TEST_CASE("an empty key is not registered", "[transport]") +{ + auto *s = make(kCoprocessor, ""); + REQUIRE(s != nullptr); + CHECK(__catalyst__transport__get_session(kCoprocessor, "") == nullptr); + __catalyst__transport__destroy(s); +} + +TEST_CASE("re-create under the same key overwrites", "[transport]") +{ + auto *s1 = make(kController, "ovr_key"); + auto *s2 = make(kController, "ovr_key"); + REQUIRE(s1 != s2); + CHECK(__catalyst__transport__get_session(kController, "ovr_key") == s2); + __catalyst__transport__destroy(s1); + // s1 was already overwritten, so s2 remains resolvable until its own destroy. + CHECK(__catalyst__transport__get_session(kController, "ovr_key") == s2); + __catalyst__transport__destroy(s2); + CHECK(__catalyst__transport__get_session(kController, "ovr_key") == nullptr); +} + +TEST_CASE("get_session on an unregistered role/key returns null", "[transport]") +{ + CHECK(__catalyst__transport__get_session(kController, "never_created") == nullptr); +} + +TEST_CASE("set_coprocessor_fn: an empty symbol binds the built-in echo", "[transport]") +{ + auto *s = make(kCoprocessor, ""); + REQUIRE(s != nullptr); + CHECK(__catalyst__transport__set_coprocessor_fn(s, "") == CATALYST_TRANSPORT_OK); + CHECK(__catalyst__transport__set_coprocessor_fn(s, nullptr) == CATALYST_TRANSPORT_OK); + __catalyst__transport__destroy(s); +} + +TEST_CASE("set_coprocessor_fn: an unresolved symbol is an error", "[transport]") +{ + auto *s = make(kCoprocessor, ""); + REQUIRE(s != nullptr); + CHECK(__catalyst__transport__set_coprocessor_fn(s, "catalyst_no_such_symbol_xyz") == + CATALYST_TRANSPORT_ERR); + __catalyst__transport__destroy(s); +} + +TEST_CASE("set_coprocessor_fn on a controller session is an error", "[transport]") +{ + auto *s = make(kController, ""); + REQUIRE(s != nullptr); + CHECK(__catalyst__transport__set_coprocessor_fn(s, "") == CATALYST_TRANSPORT_ERR); + __catalyst__transport__destroy(s); +} + +TEST_CASE("null session arguments are rejected without crashing", "[transport]") +{ + CHECK(__catalyst__transport__connect(nullptr, "127.0.0.1", 0) == CATALYST_TRANSPORT_ERR); + CHECK(__catalyst__transport__exchange_keys(nullptr) == CATALYST_TRANSPORT_ERR); + CHECK(__catalyst__transport__establish_channel(nullptr, "cpu_verbs") == CATALYST_TRANSPORT_ERR); + CHECK(__catalyst__transport__set_coprocessor_fn(nullptr, "") == CATALYST_TRANSPORT_ERR); + CHECK(__catalyst__transport__commit_work_item(nullptr, 0, 0, 0) == CATALYST_TRANSPORT_ERR); + CHECK(__catalyst__transport__kick(nullptr, 0) == CATALYST_TRANSPORT_ERR); + std::uint8_t buf[4] = {}; + CHECK(__catalyst__transport__collect(nullptr, buf, sizeof(buf)) == CATALYST_TRANSPORT_ERR); + CHECK(__catalyst__transport__data_slot(nullptr) == nullptr); + CHECK(__catalyst__transport__last_rtt_ns(nullptr) == 0); + // The void entry points must simply not crash on null. + __catalyst__transport__start(nullptr); + __catalyst__transport__stop(nullptr); + __catalyst__transport__destroy(nullptr); + SUCCEED(); +} + +TEST_CASE("commit_work_item rejects a reply larger than the provisioned region", "[transport]") +{ + auto *s = make(kController, ""); + REQUIRE(s != nullptr); + // exchange_keys provisions the local reply region (the stub reports a zero-size region). + REQUIRE(__catalyst__transport__exchange_keys(s) == CATALYST_TRANSPORT_OK); + CHECK(__catalyst__transport__commit_work_item(s, 0, 0, 1) == CATALYST_TRANSPORT_ERR); + CHECK(__catalyst__transport__commit_work_item(s, 0, 0, 0) == CATALYST_TRANSPORT_OK); + __catalyst__transport__destroy(s); +} diff --git a/runtime/tests/Test_TransportCommon.cpp b/runtime/tests/Test_TransportCommon.cpp new file mode 100644 index 0000000000..8fdf54dfb7 --- /dev/null +++ b/runtime/tests/Test_TransportCommon.cpp @@ -0,0 +1,58 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "Context.hpp" +#include "QpState.hpp" + +#include +#include + +using namespace catalyst::transport::common; + +static bool have_rxe() +{ + int n = 0; + ibv_device **devs = ibv_get_device_list(&n); + bool found = false; + for (int i = 0; i < n; ++i) + if (std::string(ibv_get_device_name(devs[i])) == "rxe0") + found = true; + if (devs) + ibv_free_device_list(devs); + return found; +} + +TEST_CASE("QpState transitions gate the RC bring-up edges", "[common]") +{ + REQUIRE(is_valid_transition(QpState::RESET, QpState::INIT)); + REQUIRE(is_valid_transition(QpState::INIT, QpState::RTR)); + REQUIRE(is_valid_transition(QpState::RTR, QpState::RTS)); + REQUIRE(is_valid_transition(QpState::RTS, QpState::ERROR)); + REQUIRE(is_valid_transition(QpState::RTS, QpState::RESET)); + REQUIRE_FALSE(is_valid_transition(QpState::RESET, QpState::RTR)); + REQUIRE_FALSE(is_valid_transition(QpState::INIT, QpState::RTS)); +} + +TEST_CASE("Context opens rxe0 with an active port", "[common]") +{ + if (!have_rxe()) + SKIP("no rxe0 RDMA device"); + Context ctx("rxe0"); + REQUIRE(ctx.get() != nullptr); + ibv_port_attr pa = ctx.port_attr(1); + REQUIRE(pa.state == IBV_PORT_ACTIVE); + REQUIRE(pa.active_mtu > 0); +} diff --git a/runtime/tests/Test_TransportCpuVerbs.cpp b/runtime/tests/Test_TransportCpuVerbs.cpp new file mode 100644 index 0000000000..8cb69d0fcb --- /dev/null +++ b/runtime/tests/Test_TransportCpuVerbs.cpp @@ -0,0 +1,235 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include + +#include "CpuControllerSession.hpp" +#include "CpuCoprocessorSession.hpp" +#include "WireProtocol.hpp" + +#include +#include + +using namespace catalyst::transport; +using namespace catalyst::transport::cpu_verbs; +using namespace catalyst::transport::common; // DEMO_SYNDROME, REGION_BYTES + +static bool have_rxe() +{ + int n = 0; + ibv_device **devs = ibv_get_device_list(&n); + bool found = false; + for (int i = 0; i < n; ++i) + if (std::string(ibv_get_device_name(devs[i])) == "rxe0") + found = true; + if (devs) + ibv_free_device_list(devs); + return found; +} + +// A custom coprocessor function (bitwise-invert) to exercise the set_coprocessor_fn +// path with a non-null, non-echo function. +static std::size_t invert_fn(const void *in, std::size_t in_len, void *out, std::size_t out_cap, + void * /*ctx*/) +{ + std::uint64_t v = 0; + std::memcpy(&v, in, std::min(in_len, sizeof(v))); + v = ~v; + const std::size_t n = std::min(out_cap, sizeof(v)); + std::memcpy(out, &v, n); + return n; +} + +TEST_CASE("controller and coprocessor connect: both reach INIT and open the " + "OOB channel", + "[cpu_libibverbs]") +{ + if (!have_rxe()) + SKIP("no rxe0 RDMA device"); + const std::uint16_t port = 18590; + int coproc_rc = -99; + std::thread t([&] { + CpuCoprocessorSession coproc("rxe0", 1); + ConnectInfo ci{ + .peer = "127.0.0.1", + .oob_port = port, + }; + coproc_rc = coproc.connect(ci); + }); + CpuControllerSession controller("rxe0", 1); + ConnectInfo ci{ + .peer = "127.0.0.1", + .oob_port = port, + }; + int controller_rc = controller.connect(ci); + t.join(); + REQUIRE(controller_rc == 0); + REQUIRE(coproc_rc == 0); +} + +TEST_CASE("alloc_memory registers host RAM and exchange_keys swaps regions", "[cpu_libibverbs]") +{ + if (!have_rxe()) + SKIP("no rxe0 RDMA device"); + const std::uint16_t port = 18591; + const std::size_t SIZE = REGION_BYTES; + std::uint32_t coproc_rkey = 0; + std::uint64_t coproc_peer_addr = 0; + std::uint64_t coproc_peer_size = 0; + std::thread t([&] { + CpuCoprocessorSession coproc("rxe0", 1); + ConnectInfo ci{ + .peer = "127.0.0.1", + .oob_port = port, + }; + coproc.connect(ci); + MemRegion m = coproc.alloc_memory(SIZE, MemKind::CpuRam); + coproc_rkey = m.rkey; + PeerRef p = coproc.exchange_keys(m); + coproc_peer_addr = p.remote_addr; + coproc_peer_size = p.size; + }); + CpuControllerSession controller("rxe0", 1); + ConnectInfo ci{ + .peer = "127.0.0.1", + .oob_port = port, + }; + controller.connect(ci); + MemRegion mine = controller.alloc_memory(SIZE, MemKind::CpuRam); + PeerRef peer = controller.exchange_keys(mine); + t.join(); + + REQUIRE(mine.addr != nullptr); + REQUIRE(mine.lkey != 0); + REQUIRE(mine.rkey != 0); + REQUIRE(peer.size == SIZE); + REQUIRE(peer.rkey == coproc_rkey); + REQUIRE(coproc_peer_addr == reinterpret_cast(mine.addr)); + REQUIRE(coproc_peer_size == SIZE); +} + +TEST_CASE("round-trip: coprocessor gets request, controller gets bounced reply", "[cpu_libibverbs]") +{ + if (!have_rxe()) + SKIP("no rxe0 RDMA device"); + const std::uint16_t port = 18593; + const std::size_t SIZE = REGION_BYTES; + std::uint64_t coproc_got = 0; + std::thread t([&] { + CpuCoprocessorSession coproc("rxe0", 1); + ConnectInfo ci{ + .peer = "127.0.0.1", + .oob_port = port, + }; + coproc.connect(ci); + MemRegion m = coproc.alloc_memory(SIZE, MemKind::CpuRam); + PeerRef p = coproc.exchange_keys(m); + ChannelDesc desc{ + .data_path = "cpu_verbs", + }; + coproc.establish_channel(desc, m, p); + coproc.set_coprocessor_fn(nullptr, nullptr); // built-in echo + coproc.start(); + void *outs[1] = {&coproc_got}; + std::uint64_t obytes[1] = {sizeof(coproc_got)}; + coproc.collect(outs, obytes, 1); + coproc.stop(); + }); + CpuControllerSession controller("rxe0", 1); + ConnectInfo ci{ + .peer = "127.0.0.1", + .oob_port = port, + }; + controller.connect(ci); + MemRegion m = controller.alloc_memory(SIZE, MemKind::CpuRam); + PeerRef p = controller.exchange_keys(m); + ChannelDesc desc{ + .data_path = "cpu_verbs", + }; + controller.establish_channel(desc, m, p); + controller.commit_work_item(0, sizeof(std::uint64_t), sizeof(std::uint64_t)); + controller.start(); + const std::uint64_t syndrome = DEMO_SYNDROME; + std::memcpy(controller.data_slot(), &syndrome, sizeof(syndrome)); + controller.kick(0); + std::uint64_t controller_got = 0; + void *outs[1] = {&controller_got}; + std::uint64_t obytes[1] = {sizeof(controller_got)}; + controller.collect(outs, obytes, 1); + controller.stop(); + t.join(); + + REQUIRE(coproc_got == DEMO_SYNDROME); + REQUIRE(controller_got == DEMO_SYNDROME); // echoed back unchanged +} + +TEST_CASE("round-trip with a custom coprocessor function runs on the coprocessor", + "[cpu_libibverbs]") +{ + if (!have_rxe()) + SKIP("no rxe0 RDMA device"); + const std::uint16_t port = 18595; + const std::size_t SIZE = REGION_BYTES; + std::thread t([&] { + CpuCoprocessorSession coproc("rxe0", 1); + ConnectInfo ci{ + .peer = "127.0.0.1", + .oob_port = port, + }; + coproc.connect(ci); + MemRegion m = coproc.alloc_memory(SIZE, MemKind::CpuRam); + PeerRef p = coproc.exchange_keys(m); + ChannelDesc desc{ + .data_path = "cpu_verbs", + }; + coproc.establish_channel(desc, m, p); + coproc.set_coprocessor_fn(invert_fn, nullptr); + coproc.start(); + std::uint64_t got = 0; + void *outs[1] = {&got}; + std::uint64_t obytes[1] = {sizeof(got)}; + coproc.collect(outs, obytes, 1); + coproc.stop(); + }); + CpuControllerSession controller("rxe0", 1); + ConnectInfo ci{ + .peer = "127.0.0.1", + .oob_port = port, + }; + controller.connect(ci); + MemRegion m = controller.alloc_memory(SIZE, MemKind::CpuRam); + PeerRef p = controller.exchange_keys(m); + ChannelDesc desc{ + .data_path = "cpu_verbs", + }; + controller.establish_channel(desc, m, p); + controller.commit_work_item(0, sizeof(std::uint64_t), sizeof(std::uint64_t)); + controller.start(); + const std::uint64_t syndrome = DEMO_SYNDROME; + std::memcpy(controller.data_slot(), &syndrome, sizeof(syndrome)); + controller.kick(0); + std::uint64_t got = 0; + void *outs[1] = {&got}; + std::uint64_t obytes[1] = {sizeof(got)}; + controller.collect(outs, obytes, 1); + controller.stop(); + t.join(); + + REQUIRE(got == ~DEMO_SYNDROME); // controller received the coprocessor's result + REQUIRE(got != DEMO_SYNDROME); // and it is not a mere echo +} diff --git a/runtime/tests/Test_TransportWireProtocol.cpp b/runtime/tests/Test_TransportWireProtocol.cpp new file mode 100644 index 0000000000..bbcdd65b6d --- /dev/null +++ b/runtime/tests/Test_TransportWireProtocol.cpp @@ -0,0 +1,46 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "WireProtocol.hpp" + +#include + +using namespace catalyst::transport::common; + +TEST_CASE("Payload is the 16 B wire frame") +{ + STATIC_REQUIRE(sizeof(Payload) == 16); + STATIC_REQUIRE(offsetof(Payload, value) == 0); + STATIC_REQUIRE(offsetof(Payload, seq_num) == 8); + STATIC_REQUIRE(offsetof(Payload, pad) == 12); +} + +TEST_CASE("PayloadSlot is a 64 B slot") +{ + STATIC_REQUIRE(sizeof(PayloadSlot) == 64); + STATIC_REQUIRE(alignof(PayloadSlot) == 64); + STATIC_REQUIRE(offsetof(PayloadSlot, p) == 0); +} + +TEST_CASE("Ring geometry and constants match the trampoline") +{ + STATIC_REQUIRE(K_RING_SLOTS == 256); + STATIC_REQUIRE((K_RING_SLOTS & (K_RING_SLOTS - 1)) == 0); + STATIC_REQUIRE(REGION_BYTES == K_RING_SLOTS * sizeof(PayloadSlot)); + STATIC_REQUIRE(SIGNAL_EVERY == 64); + STATIC_REQUIRE(SALT == 0xC0DE1515u); + STATIC_REQUIRE(DEMO_SYNDROME == 0x0123456789ABCDEFull); +} diff --git a/runtime/tests/stubs/stub_transport_backend.cpp b/runtime/tests/stubs/stub_transport_backend.cpp new file mode 100644 index 0000000000..bea1840743 --- /dev/null +++ b/runtime/tests/stubs/stub_transport_backend.cpp @@ -0,0 +1,64 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A no-op transport backend for Test_Transport: implements the controller/coprocessor factory ABI +// with sessions whose methods do nothing, so the transport CAPI (create/get_session/registry, +// argument plumbing) can be unit-tested without a NIC. + +#include +#include + +#include "Transport.hpp" +#include "TransportBackend.h" + +using namespace catalyst::transport; + +namespace { + +struct StubController : ControllerSession { + std::uint64_t slot = 0; + int connect(const ConnectInfo &) override { return 0; } + MemRegion alloc_memory(std::size_t, MemKind) override { return {}; } + PeerRef exchange_keys(const MemRegion &) override { return {}; } + void establish_channel(const ChannelDesc &, const MemRegion &, const PeerRef &) override {} + void start() override {} + int collect(void *const *, const std::uint64_t *, std::size_t) override { return 0; } + void stop() override {} + void commit_work_item(std::uint32_t, std::uint64_t, std::uint64_t) override {} + int kick(std::uint32_t) override { return 0; } + void *data_slot() override { return &slot; } +}; + +struct StubCoprocessor : CoprocessorSession { + int connect(const ConnectInfo &) override { return 0; } + MemRegion alloc_memory(std::size_t, MemKind) override { return {}; } + PeerRef exchange_keys(const MemRegion &) override { return {}; } + void establish_channel(const ChannelDesc &, const MemRegion &, const PeerRef &) override {} + void start() override {} + int collect(void *const *, const std::uint64_t *, std::size_t) override { return 0; } + void stop() override {} + void set_coprocessor_fn(CoprocessorFn, void *) override {} +}; + +} // namespace + +extern "C" ControllerSession *CatalystTransportControllerFactory(const char *) +{ + return new StubController(); +} + +extern "C" CoprocessorSession *CatalystTransportCoprocessorFactory(const char *) +{ + return new StubCoprocessor(); +}