diff --git a/docs/sphinx/using/backends/cloud/qbraid.rst b/docs/sphinx/using/backends/cloud/qbraid.rst index dfa72e53913..bfb0d89cd44 100644 --- a/docs/sphinx/using/backends/cloud/qbraid.rst +++ b/docs/sphinx/using/backends/cloud/qbraid.rst @@ -5,9 +5,17 @@ qBraid `qBraid `__ 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 `__ 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 `__ +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 ``````````````````` diff --git a/python/tests/backends/test_qbraid.py b/python/tests/backends/test_qbraid.py index e8e2a6f0ce0..81a2eb457d2 100644 --- a/python/tests/backends/test_qbraid.py +++ b/python/tests/backends/test_qbraid.py @@ -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. diff --git a/python/tests/utils/mock_qpu/qbraid/__init__.py b/python/tests/utils/mock_qpu/qbraid/__init__.py index 7fea100bec5..fa9061c31e6 100644 --- a/python/tests/utils/mock_qpu/qbraid/__init__.py +++ b/python/tests/utils/mock_qpu/qbraid/__init__.py @@ -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( diff --git a/runtime/cudaq/platform/default/rest/helpers/qbraid/QbraidServerHelper.cpp b/runtime/cudaq/platform/default/rest/helpers/qbraid/QbraidServerHelper.cpp index c981c055b4a..7bd53ab889a 100644 --- a/runtime/cudaq/platform/default/rest/helpers/qbraid/QbraidServerHelper.cpp +++ b/runtime/cudaq/platform/default/rest/helpers/qbraid/QbraidServerHelper.cpp @@ -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") { @@ -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(); + 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 { diff --git a/unittests/nvqpp/backends/qbraid/QbraidTester.cpp b/unittests/nvqpp/backends/qbraid/QbraidTester.cpp index 9ce3e6d49c6..08c6f052bb0 100644 --- a/unittests/nvqpp/backends/qbraid/QbraidTester.cpp +++ b/unittests/nvqpp/backends/qbraid/QbraidTester.cpp @@ -9,6 +9,7 @@ #include "CUDAQTestUtils.h" #include "common/FmtCore.h" #include "common/RestClient.h" +#include "common/ServerHelper.h" #include "cudaq/algorithm.h" #include #include @@ -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("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();