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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions docs/sphinx/using/backends/cloud/qbraid.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@ qBraid

`qBraid <https://www.qbraid.com/>`__ is a cloud platform that brokers access to
quantum simulators and hardware from multiple vendors through a single API.
CUDA-Q can submit OpenQASM 2 jobs to any device exposed by the qBraid service.
See the `qBraid device catalog <https://account.qbraid.com/devices>`__ for the
set of simulators and QPUs currently available.
CUDA-Q submits OpenQASM 2 jobs to gate-based devices exposed by the qBraid
service. See the `qBraid device catalog <https://account.qbraid.com/devices>`__
for the set of simulators and QPUs currently available.

.. note::

Only gate-based (gate-model) devices are supported through this target.
qBraid also brokers analog devices, such as analog Hamiltonian simulation
(AHS) QPUs, which cannot execute the gate-based kernels CUDA-Q emits.
Selecting such a device (for example, ``aws:quera:qpu:aquila``) is rejected
when the target is configured.

Setting Credentials
```````````````````
Expand Down
22 changes: 22 additions & 0 deletions python/tests/backends/test_qbraid.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,28 @@ def test_qbraid_machine_alternative_device():
assert len(counts) >= 1


def test_qbraid_rejects_analog_device():
"""An analog/AHS device (paradigm != gate_model) is rejected up front.

The helper queries device metadata when the target is configured and
refuses a non-gate-model device, surfacing an actionable error at
set_target time instead of an opaque downstream job failure.
"""
with pytest.raises(RuntimeError, match="gate-model"):
_set_qbraid_target(machine="aws:quera:qpu:aquila")


def test_qbraid_accepts_gate_model_device():
"""A gate-model device passes the paradigm check and executes normally."""
_set_qbraid_target(machine="aws:aws:sim:sv1")
kernel = cudaq.make_kernel()
qubit = kernel.qalloc()
kernel.h(qubit)
kernel.mz(qubit)
counts = cudaq.sample(kernel)
assert len(counts) >= 1


def _arm_result_status(code: int):
"""Force the next /result call on the mock to return the given HTTP code.

Expand Down
43 changes: 43 additions & 0 deletions python/tests/utils/mock_qpu/qbraid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,49 @@ async def resetTestState():
return {"reset": True}


def _paradigm_for_device(device_qrn: str) -> str:
"""Infer a device paradigm from its QRN for testing.

Mirrors qBraid's `paradigm` enum (gate_model | analog | annealing | other).
Analog/AHS devices (e.g. QuEra Aquila, Pasqal Fresnel) report "analog";
everything else defaults to "gate_model".
"""
qrn = device_qrn.lower()
if any(tok in qrn for tok in ("quera", "aquila", "pasqal", "fresnel")):
return "analog"
return "gate_model"


# v2 API: GET /devices/{device_qrn}
@app.get("/devices/{device_qrn}")
async def getDevice(
device_qrn: str = Path(...),
x_api_key: Optional[str] = Header(None, alias="X-API-KEY"),
):
"""Retrieve device metadata (v2 API).

The helper queries this at target-initialization time to confirm the
selected device can run gate-based programs before submitting any job.
"""
if x_api_key is None:
raise HTTPException(status_code=401, detail="API key is required")

paradigm = _paradigm_for_device(device_qrn)
return {
"success": True,
"data": {
"qrn": device_qrn,
"name": device_qrn,
"vendor": "qbraid",
"deviceType": "SIMULATOR" if "sim" in device_qrn.lower() else "QPU",
"paradigm": paradigm,
"status": "ONLINE",
"numberQubits": 30,
"runInputTypes": ["qasm2"],
},
}


# v2 API: GET /jobs/{job_qrn}
@app.get("/jobs/{job_id}")
async def getJob(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ class QbraidServerHelper : public ServerHelper {

parseConfigForCommonParams(config);

// qBraid brokers multiple paradigms under one target, so reject devices
// that can't run gate-based (QASM/QIR) programs. Skipped under emulation.
const bool emulate = getValueOrDefault(config, "emulate", "") == "true";
if (!emulate && !backendConfig["api_key"].empty())
validateDeviceParadigm(backendConfig["device_id"]);

cudaq::info("qBraid configuration initialized:");
for (const auto &[key, value] : backendConfig) {
if (key == "api_key") {
Expand Down Expand Up @@ -362,6 +368,44 @@ class QbraidServerHelper : public ServerHelper {
return headers;
}

/// @brief Throws if @p deviceId is not a gate-model device, per its qBraid
/// metadata (GET /devices/{qrn}). Lookup failures are non-fatal: we log and
/// defer to the backend, which rejects incompatible programs at submission.
void validateDeviceParadigm(const std::string &deviceId) {
nlohmann::json deviceJson;
try {
RestClient client;
auto headers = getHeaders();
const auto devicePath = backendConfig.at("url") + "/devices/" + deviceId;
cudaq::info("Verifying device paradigm via {}", devicePath);
deviceJson = client.get("", devicePath, headers, true);
} catch (const std::exception &e) {
cudaq::info("Could not verify paradigm for device '{}' ({}); deferring "
"compatibility check to job submission.",
deviceId, e.what());
return;
}

// qBraid v2 wraps the device document under a `data` envelope.
if (!deviceJson.contains("data") ||
!deviceJson["data"].contains("paradigm") ||
!deviceJson["data"]["paradigm"].is_string()) {
cudaq::info("Device metadata for '{}' contained no paradigm field; "
"skipping paradigm validation.",
deviceId);
return;
}

const auto paradigm = deviceJson["data"]["paradigm"].get<std::string>();
if (paradigm != "gate_model") {
throw std::runtime_error(
"qBraid device '" + deviceId + "' has paradigm '" + paradigm +
"', which cannot run gate-based CUDA-Q kernels. The 'qbraid' target "
"supports only gate-model devices.");
}
cudaq::info("Device '{}' paradigm is gate_model.", deviceId);
}

/// @brief Helper method to retrieve the value of an environment variable.
std::string getEnvVar(const std::string &key, const std::string &defaultVal,
const bool isRequired) const {
Expand Down
18 changes: 18 additions & 0 deletions unittests/nvqpp/backends/qbraid/QbraidTester.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "CUDAQTestUtils.h"
#include "common/FmtCore.h"
#include "common/RestClient.h"
#include "common/ServerHelper.h"
#include "cudaq/algorithm.h"
#include <fstream>
#include <gtest/gtest.h>
Expand Down Expand Up @@ -302,6 +303,23 @@ CUDAQ_TEST(QbraidTester, checkResultServerErrorRetries) {
EXPECT_GE(counts.size(), 1u);
}

// An analog/AHS device must be rejected at initialize() time, mirroring the
// Python test_qbraid_rejects_analog_device. Driven through the registry so the
// non-default machine can be supplied directly (the shared target config used
// by the other tests pins the default gate-model device).
CUDAQ_TEST(QbraidTester, checkAnalogDeviceRejected) {
auto helper = cudaq::registry::get<cudaq::ServerHelper>("qbraid");
ASSERT_TRUE(helper);
cudaq::BackendConfig config;
config["emulate"] = "false";
config["url"] = "http://localhost:" + mockPort;
config["machine"] = "aws:quera:qpu:aquila";
config["api_key"] = "00000000000000000000000000000000";
EXPECT_TRUE(
throwsWithMessage([&]() { helper->initialize(config); },
"device 'aws:quera:qpu:aquila' has paradigm 'analog'"));
}

int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
auto ret = RUN_ALL_TESTS();
Expand Down