Extending CUDA-Q with a new Hardware Backend¶
This guide explains how to create a new quantum hardware backend for CUDA-Q. All external backends are developed as external plugins — self-contained packages that register targets with the CUDA-Q runtime without modifying the core repository. Plugin authors can distribute these plugins as Python packages so that end users of the target can install them into their own CUDA-Q environments.
This guide covers the most common backend shape: a REST-style backend that
subclasses ServerHelper to communicate with a provider’s REST API, reusing
the built-in remote_rest QPU.
All backends use the same plugin package layout and distribution mechanism described in Package & Distribute a Backend Plugin.
Plugin Directory Structure¶
Every backend plugin follows this layout:
my-backend/
├── targets/
│ └── my-backend.yml # Target configuration
├── lib/
│ └── libcudaq-serverhelper-my-backend.so
└── data/ # Optional auxiliary files
└── topology.txt
The targets/ directory contains one or more YAML target configurations.
The lib/ directory contains the shared libraries that implement the backend.
The optional data/ directory holds auxiliary files (device topologies, noise
models, calibration data, etc.).
REST-Style Backends (Server Helper)¶
A REST-style backend communicates with a provider’s HTTP API. You implement a
ServerHelper subclass that handles authentication, job submission, polling,
and result processing. The built-in remote_rest QPU handles the execution
lifecycle.
Server Helper Class¶
The server helper is the core component that handles communication with the
quantum hardware provider’s API. It extends the ServerHelper base class and
implements methods for job submission, result retrieval, and other
provider-specific functionality. The base class definition can be found in the
CUDA-Q repository.
Here’s a template for implementing a server helper class:
// ProviderNameServerHelper.cpp
#include "common/ServerHelper.h"
#include "nlohmann/json.hpp"
namespace cudaq {
/// @brief Handles job submission and result retrieval for Provider Name.
class ProviderNameServerHelper : public ServerHelper {
static constexpr const char *DEFAULT_URL = "https://api.provider-name.com";
public:
const std::string name() const override { return "<provider_name>"; }
void initialize(BackendConfig config) override {
backendConfig = config;
parseConfigForCommonParams(backendConfig);
if (!backendConfig.count("url"))
backendConfig["url"] = DEFAULT_URL;
if (auto it = config.find("shots"); it != config.end())
setShots(std::stoul(it->second));
}
RestHeaders getHeaders() override {
RestHeaders headers;
headers["Content-Type"] = "application/json";
if (backendConfig.count("api_key"))
headers["Authorization"] = "Bearer " + backendConfig["api_key"];
return headers;
}
/// @brief Build one task JSON per compiled kernel and POST them together.
ServerJobPayload createJob(std::vector<KernelExecution> &circuitCodes) override {
std::vector<ServerMessage> tasks;
tasks.reserve(circuitCodes.size());
for (const auto &circuit : circuitCodes) {
ServerMessage task;
task["content"] = circuit.code;
task["shots"] = shots;
tasks.push_back(std::move(task));
}
return {backendConfig["url"] + "/jobs", getHeaders(), std::move(tasks)};
}
std::string extractJobId(ServerMessage &postResponse) override {
if (!postResponse.contains("id"))
return "";
return postResponse.at("id");
}
/// @brief Both overloads must return a full URL, not just the job ID.
std::string constructGetJobPath(std::string &jobId) override {
return backendConfig["url"] + "/jobs/" + jobId;
}
std::string constructGetJobPath(ServerMessage &postResponse) override {
auto jobId = extractJobId(postResponse);
return constructGetJobPath(jobId);
}
bool jobIsDone(ServerMessage &getJobResponse) override {
if (!getJobResponse.contains("status"))
return false;
std::string status = getJobResponse["status"];
return status == "COMPLETED" || status == "FAILED";
}
/// @brief Map provider result counts to a CUDA-Q sample_result.
///
/// Raw results from quantum hardware often need post-processing (bit
/// reordering, normalization, etc.) to match CUDA-Q's expectations.
cudaq::sample_result processResults(ServerMessage &postJobResponse,
std::string &jobId) override {
auto samplesJson = postJobResponse["results"]["counts"];
cudaq::CountsDictionary counts;
for (auto &[bitstring, count] : samplesJson.items())
counts[bitstring] = count;
return cudaq::sample_result{cudaq::ExecutionResult{counts}};
}
std::chrono::microseconds
nextResultPollingInterval(ServerMessage &postResponse) override {
return std::chrono::seconds(5);
}
};
} // namespace cudaq
CUDAQ_REGISTER_TYPE(cudaq::ServerHelper, cudaq::ProviderNameServerHelper, <provider_name>)
The CUDAQ_REGISTER_TYPE macro at the bottom registers the helper so that
the runtime can find it by name when the target is activated.
Target YAML Configuration¶
Create a YAML file that tells CUDA-Q how to activate your target:
# <provider_name>.yml
name: "<provider_name>"
description: "CUDA-Q target for Provider Name."
cudaq-version: "@CUDA_QUANTUM_VERSION@"
config:
platform-qpu: remote_rest
link-libs: ["-lcudaq-rest-qpu"]
gen-target-backend: true
preprocessor-defines: ["-D CUDAQ_QUANTUM_DEVICE"]
# Optional: customize the JIT lowering pipeline for your gate set.
# Contact the CUDA-Q team for help setting this up.
jit-mid-level-pipeline: "lower-to-cfg,func.func(canonicalize,multicontrol-decomposition),decomposition{enable-patterns=U3ToRotations},symbol-dce,<provider_name>-gate-set-mapping"
# Supported values: qir-base, qir-adaptive, qasm2
codegen-emission: qir-base
library-mode: false
# target-arguments are optional; omit the section if your backend needs none.
target-arguments:
- key: api-key
required: true
type: string
platform-arg: api_key
help-string: "API key for Provider Name."
- key: url
required: false
type: string
platform-arg: url
help-string: "Specify Provider Name API server URL."
- key: device
required: false
type: string
platform-arg: device
help-string: "Specify the Provider Name quantum device to use."
Key fields:
cudaq-version— the CUDA-Q version this plugin was built against. Set via CMake’s@CUDA_QUANTUM_VERSION@substitution; checked at target-load time for compatibility.platform-qpu: remote_rest— use the built-in REST QPU (no custom QPU subclass needed).link-libs— libraries to link when compiling withnvq++.codegen-emission— the IR format sent to the provider (qir-base,qir-adaptive, orqasm2).target-arguments— declares parameters that surface as--my-backend-api-key <value>on thenvq++command line and as keyword arguments tocudaq.set_target("my-backend", api_key=...)in Python.
For the full list of recognized YAML fields see the mapping traits in TargetConfigYaml.cpp. For a complete working example of a REST-style plugin, see the mock_rest reference plugin.
CMake Build File¶
A minimal CMakeLists.txt for a REST-style plugin:
# No cmake_minimum_required / project() — this file runs as an
# add_subdirectory() child of the CUDA-Q build via CUDAQ_EXTERNAL_PROJECTS,
# so it inherits the CUDA-Q project scope and all its CMake targets.
set(plugin_root ${CMAKE_CURRENT_BINARY_DIR})
set(plugin_lib_dir ${plugin_root}/lib)
set(plugin_target_dir ${plugin_root}/targets)
file(MAKE_DIRECTORY ${plugin_lib_dir} ${plugin_target_dir})
configure_file(targets/my-backend.yml.in
${plugin_target_dir}/my-backend.yml @ONLY)
add_library(cudaq-serverhelper-my-backend SHARED
MyBackendServerHelper.cpp)
set_target_properties(cudaq-serverhelper-my-backend PROPERTIES
LIBRARY_OUTPUT_DIRECTORY ${plugin_lib_dir})
target_include_directories(cudaq-serverhelper-my-backend
PRIVATE ${PROJECT_SOURCE_DIR}/runtime)
target_link_libraries(cudaq-serverhelper-my-backend
PRIVATE cudaq-common cudaq-logger)
When developing inside the CUDA-Q source tree, build your plugin with
CUDAQ_EXTERNAL_PROJECTS:
cmake -B build \
-DCUDAQ_EXTERNAL_PROJECTS="my-backend" \
-DCUDAQ_EXTERNAL_MY_BACKEND_SOURCE_DIR=$PWD/my-backend
ninja -C build cudaq-serverhelper-my-backend
See Package & Distribute a Backend Plugin for how to build standalone against an installed CUDA-Q.
Auxiliary Files and %PLUGIN_ROOT%¶
Plugins that ship auxiliary files (device topologies, calibration data, noise
models) place them under their package root — typically in a data/
subdirectory. To reference these files portably in the YAML, use the
%PLUGIN_ROOT% substitution token:
config:
jit-mid-level-pipeline: "qubit-mapping{device=file(%PLUGIN_ROOT%/data/topology.txt)}"
target-arguments:
- key: device
type: string
default: "%PLUGIN_ROOT%/data/topology.txt"
platform-arg: device
When the runtime loads the YAML, every %PLUGIN_ROOT% is replaced with the
absolute path of the plugin package root. This works regardless of where the
package is installed.
Testing Your Backend¶
Create a tests/ directory in your plugin with lit tests or standalone test
programs that exercise the full lifecycle:
Plugin builds successfully
Target YAML is valid and discoverable
nvq++ --target=my-backendcompiles a programPython can set the target and run a kernel
For REST-style backends, CUDA-Q provides a mock QPU server framework under
python/tests/utils/ that you can use to test without real hardware.
See the reference plugins’ tests/ directories for concrete examples:
Example Usage¶
After an end user installs the distributed plugin package, they interact with its target like any built-in target. Here, “installed” refers to installing the plugin in the target user’s CUDA-Q environment, not to the plugin author’s build process. See Package & Distribute a Backend Plugin for how to create and distribute the Python package and for the commands end users run to install it.
import cudaq
cudaq.set_target('my-backend',
api_key='your_api_key',
device='your_device')
@cudaq.kernel
def bell():
qubits = cudaq.qvector(2)
h(qubits[0])
x.ctrl(qubits[0], qubits[1])
mz(qubits)
counts = cudaq.sample(bell)
print(counts)
nvq++ --target=my-backend --my-backend-api-key=... bell.cpp -o bell
./bell
Next Steps¶
Once you have a working backend implementation, see Package & Distribute a Backend Plugin to learn
how to build platform-specific, installable Python wheels, make the plugin
discoverable by nvq++, and distribute it for the operating systems and
architectures required by your users.