diff --git a/.github/workflows/config/spelling_allowlist.txt b/.github/workflows/config/spelling_allowlist.txt index 1cc87b42d38..c7f8833aebd 100644 --- a/.github/workflows/config/spelling_allowlist.txt +++ b/.github/workflows/config/spelling_allowlist.txt @@ -108,6 +108,7 @@ QPUs QPU’s QTX QX +Qilimanjaro Qiskit QuEra QuTiP diff --git a/CMakeLists.txt b/CMakeLists.txt index 414aeb92cdc..327788a3c35 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,6 +103,11 @@ if (NOT DEFINED CUDAQ_ENABLE_PASQAL_BACKEND) set(CUDAQ_ENABLE_PASQAL_BACKEND ON CACHE BOOL "Enable building the Pasqal target.") endif() +# Enable Qilimanjaro target by default. +if (NOT DEFINED CUDAQ_ENABLE_QILIMANJARO_BACKEND) + set(CUDAQ_ENABLE_QILIMANJARO_BACKEND ON CACHE BOOL "Enable building the Qilimanjaro target.") +endif() + # Enable Quantum Circuits, Inc. (QCI) target by default. if (NOT DEFINED CUDAQ_ENABLE_QCI_BACKEND) set(CUDAQ_ENABLE_QCI_BACKEND ON CACHE BOOL "Enable building the Quantum Circuits, Inc. target.") diff --git a/python/cudaq/dynamics/evolution.py b/python/cudaq/dynamics/evolution.py index 974f2b5035a..eaec1cb38f1 100644 --- a/python/cudaq/dynamics/evolution.py +++ b/python/cudaq/dynamics/evolution.py @@ -24,7 +24,7 @@ from .integrator import BaseIntegrator from .schedule import Schedule -analog_targets = ["pasqal", "quera"] +analog_targets = ["pasqal", "qilimanjaro", "quera"] def _taylor_series_expm(op_matrix: NDArray[numpy.complexfloating], diff --git a/python/extension/CMakeLists.txt b/python/extension/CMakeLists.txt index 9f366ae908e..0c2b718a69f 100644 --- a/python/extension/CMakeLists.txt +++ b/python/extension/CMakeLists.txt @@ -96,6 +96,8 @@ declare_mlir_python_extension(CUDAQuantumPythonSources.Extension ../../runtime/cudaq/platform/orca/OrcaServerHelper.cpp ../../runtime/cudaq/platform/pasqal/PasqalRemoteRESTQPU.cpp ../../runtime/cudaq/platform/pasqal/PasqalServerHelper.cpp + ../../runtime/cudaq/platform/qilimanjaro/QilimanjaroRemoteRESTQPU.cpp + ../../runtime/cudaq/platform/qilimanjaro/QilimanjaroServerHelper.cpp ../../runtime/cudaq/platform/quera/QuEraRemoteRESTQPU.cpp EMBED_CAPI_LINK_LIBS diff --git a/python/tests/backends/test_Qilimanjaro.py b/python/tests/backends/test_Qilimanjaro.py new file mode 100644 index 00000000000..07761d3b160 --- /dev/null +++ b/python/tests/backends/test_Qilimanjaro.py @@ -0,0 +1,30 @@ +# ============================================================================ # +# Copyright (c) 2025 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # + +import cudaq +import json +import os +import pytest + +skipIfQilimanjaroNotInstalled = pytest.mark.skipif( + not cudaq.has_target("qilimanjaro"), + reason='Could not find `qilimanjaro` in installation' +) + + +@pytest.fixture(scope="session", autouse=True) +def do_something(): + # NOTE: Credentials can be set with environment variables + cudaq.set_target("qilimanjaro") + yield "Running the tests." + cudaq.reset_target() + +# leave for gdb debugging +if __name__ == "__main__": + loc = os.path.abspath(__file__) + pytest.main([loc, "-rP"]) \ No newline at end of file diff --git a/runtime/cudaq/platform/CMakeLists.txt b/runtime/cudaq/platform/CMakeLists.txt index c376a21e858..031b651e243 100644 --- a/runtime/cudaq/platform/CMakeLists.txt +++ b/runtime/cudaq/platform/CMakeLists.txt @@ -24,3 +24,7 @@ endif() if (CUDAQ_ENABLE_PASQAL_BACKEND) add_subdirectory(pasqal) endif() + +if (CUDAQ_ENABLE_QILIMANJARO_BACKEND) + add_subdirectory(qilimanjaro) +endif() diff --git a/runtime/cudaq/platform/qilimanjaro/CMakeLists.txt b/runtime/cudaq/platform/qilimanjaro/CMakeLists.txt new file mode 100644 index 00000000000..422dc53921b --- /dev/null +++ b/runtime/cudaq/platform/qilimanjaro/CMakeLists.txt @@ -0,0 +1,38 @@ +# ============================================================================ # +# Copyright (c) 2022 - 2025 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # + +set(LIBRARY_NAME cudaq-qilimanjaro-qpu) +message(STATUS "Building Qilimanjaro REST QPU.") + +add_library(${LIBRARY_NAME} + SHARED + QilimanjaroRemoteRESTQPU.cpp + QilimanjaroServerHelper.cpp +) + +target_include_directories(${LIBRARY_NAME} PRIVATE . + PUBLIC + $ + $ +) + +target_link_libraries(${LIBRARY_NAME} + PUBLIC + cudaq-operator + cudaq-common + PRIVATE + pthread + cudaq-mlir-runtime + fmt::fmt-header-only + cudaq + cudaq-platform-default +) + +install(TARGETS ${LIBRARY_NAME} DESTINATION lib) + +add_target_config(qilimanjaro) diff --git a/runtime/cudaq/platform/qilimanjaro/QilimanjaroRemoteRESTQPU.cpp b/runtime/cudaq/platform/qilimanjaro/QilimanjaroRemoteRESTQPU.cpp new file mode 100644 index 00000000000..1d1596f66e9 --- /dev/null +++ b/runtime/cudaq/platform/qilimanjaro/QilimanjaroRemoteRESTQPU.cpp @@ -0,0 +1,23 @@ +/******************************************************************************* + * Copyright (c) 2025 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +#include "common/AnalogRemoteRESTQPU.h" + +namespace { + +/// @brief The `QilimanjaroRemoteRESTQPU` is a subtype of QPU that enables the +/// execution of Analog Hamiltonian Program via a REST Client. +class QilimanjaroRemoteRESTQPU : public cudaq::AnalogRemoteRESTQPU { +public: + QilimanjaroRemoteRESTQPU() : AnalogRemoteRESTQPU() {} + QilimanjaroRemoteRESTQPU(QilimanjaroRemoteRESTQPU &&) = delete; + virtual ~QilimanjaroRemoteRESTQPU() = default; +}; +} // namespace + +CUDAQ_REGISTER_TYPE(cudaq::QPU, QilimanjaroRemoteRESTQPU, qilimanjaro) \ No newline at end of file diff --git a/runtime/cudaq/platform/qilimanjaro/QilimanjaroServerHelper.cpp b/runtime/cudaq/platform/qilimanjaro/QilimanjaroServerHelper.cpp new file mode 100644 index 00000000000..674a2846c3d --- /dev/null +++ b/runtime/cudaq/platform/qilimanjaro/QilimanjaroServerHelper.cpp @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2025 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +#include "QilimanjaroServerHelper.h" +#include "common/AnalogHamiltonian.h" +#include "common/Logger.h" + +#include +#include + +namespace cudaq { + +void QilimanjaroServerHelper::initialize(BackendConfig config) { + cudaq::info("Initialize Qilimanjaro's SpeQtrum."); + + // Hard-coded for now. + const std::string MACHINE = "radagast"; + + cudaq::info("Running on device {}", MACHINE); + + if (!config.contains("machine")) + config["machine"] = MACHINE; + + if (!config["nshots"].empty()) + setShots(std::stoul(config["nshots"])); + + parseConfigForCommonParams(config); + + backendConfig = std::move(config); +} + +RestHeaders QilimanjaroServerHelper::getHeaders() { + std::string token; + + if (auto auth_token = std::getenv("QILIMANJARO_AUTH_TOKEN")) + token = "Bearer " + std::string(auth_token); + else + token = "Bearer "; + + std::map headers{ + {"Authorization", token}, + {"Content-Type", "application/json"}, + {"User-Agent", "cudaq/0.12.0"}, // TODO: How to get version dynamically? + {"Connection", "keep-alive"}, + {"Accept", "*/*"}}; + + return headers; +} + +ServerJobPayload +QilimanjaroServerHelper::createJob(std::vector &circuitCodes) { + std::vector tasks; + // TODO: circuitCodes needs to change to the Time Evolution JSON + for (auto &circuitCode : circuitCodes) { + ServerMessage message; + message["device_code"] = backendConfig.at("machine"); + message["shots"] = shots; + message["job_type"] = "analog"; + message["payload"] = nlohmann::json::parse(circuitCode.code); + tasks.push_back(message); + } + + cudaq::info("Created job payload for Qilimanjaro, targeting device {}", + backendConfig.at("machine")); + + // Return a tuple containing the job path, headers, and the job message + return std::make_tuple(speqtrumApiUrl + "/execute", getHeaders(), tasks); +} + +std::string QilimanjaroServerHelper::extractJobId(ServerMessage &postResponse) { + return postResponse["id"].get(); +} + +std::string QilimanjaroServerHelper::constructGetJobPath(std::string &jobId) { + return speqtrumApiUrl + "/jobs/" + jobId + "?result=true"; +} + +std::string +QilimanjaroServerHelper::constructGetJobPath(ServerMessage &postResponse) { + auto jobId = extractJobId(postResponse); + return constructGetJobPath(jobId); +} + +bool QilimanjaroServerHelper::jobIsDone(ServerMessage &getJobResponse) { + std::unordered_set terminal_states = {"completed", "error", "canceled", "timeout"}; + auto jobStatus = getJobResponse["status"].get(); + return terminal_states.find(jobStatus) != terminal_states.end(); +} + +sample_result QilimanjaroServerHelper::processResults(ServerMessage &postJobResponse, + std::string &jobId) { + auto jobStatus = postJobResponse["status"].get(); + if (jobStatus != "completed") + throw std::runtime_error("Job status: " + jobStatus); + + auto job_result = postJobResponse["result"]; + + // TODO: We need a new method signature with EvolveResults instead. + std::vector results; + + return sample_result(results); +} + +} // namespace cudaq + +// Register the Qilimanjaro server helper in the CUDA-Q server helper factory +CUDAQ_REGISTER_TYPE(cudaq::ServerHelper, cudaq::QilimanjaroServerHelper, qilimanjaro) \ No newline at end of file diff --git a/runtime/cudaq/platform/qilimanjaro/QilimanjaroServerHelper.h b/runtime/cudaq/platform/qilimanjaro/QilimanjaroServerHelper.h new file mode 100644 index 00000000000..460b46edbc5 --- /dev/null +++ b/runtime/cudaq/platform/qilimanjaro/QilimanjaroServerHelper.h @@ -0,0 +1,67 @@ +/****************************************************************-*- C++ -*-**** + * Copyright (c) 2025 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +#include "common/ServerHelper.h" +#include "nlohmann/json.hpp" + +namespace cudaq { + +class QilimanjaroServerHelper : public ServerHelper { +protected: + /// @brief Server helper implementation for communicating with the REST API of + /// Qilimanjaro's SpeQtrum platform. + const std::string speqtrumApiUrl = "https://qilimanjaro.ddns.net/public-api/api/v1"; + +public: + /// @brief Returns the name of the server helper. + const std::string name() const override { return "qilimanjaro"; } + + /// @brief Initializes the server helper with the provided backend + /// configuration. + void initialize(BackendConfig config) override; + + /// @brief Return the POST/GET required headers. + /// @return + RestHeaders getHeaders() override; + + /// @brief Creates a quantum computation job using the provided kernel + /// executions and returns the corresponding payload. + ServerJobPayload + createJob(std::vector &circuitCodes) override; + + /// @brief Extract the job id from the server response from posting the job. + std::string extractJobId(ServerMessage &postResponse) override; + + /// @brief Get the specific path required to retrieve job results. Construct + /// specifically from the job id. + std::string constructGetJobPath(std::string &jobId) override; + + /// @brief Get the specific path required to retrieve job results. Construct + /// from the full server response message. + std::string constructGetJobPath(ServerMessage &postResponse) override; + + /// @brief Get the jobs results polling interval. + /// @return + std::chrono::microseconds + nextResultPollingInterval(ServerMessage &postResponse) override { + return std::chrono::seconds(1); + } + + /// @brief Return true if the job is done. + bool jobIsDone(ServerMessage &getJobResponse) override; + + /// @brief Given a successful job and the success response, + /// retrieve the results and map them to a sample_result. + /// @param postJobResponse + /// @param jobId + /// @return + sample_result processResults(ServerMessage &postJobResponse, + std::string &jobId) override; +}; + +} // namespace cudaq diff --git a/runtime/cudaq/platform/qilimanjaro/qilimanjaro.yml b/runtime/cudaq/platform/qilimanjaro/qilimanjaro.yml new file mode 100644 index 00000000000..b7e06cc9d4c --- /dev/null +++ b/runtime/cudaq/platform/qilimanjaro/qilimanjaro.yml @@ -0,0 +1,28 @@ +# ============================================================================ # +# Copyright (c) 2022 - 2025 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # + +name: qilimanjaro +description: "CUDA-Q target for qilimanjaro." +config: + # Tell DefaultQuantumPlatform what QPU subtype to use + platform-qpu: qilimanjaro + # Add the qilimanjaro-qpu library to the link list + link-libs: ["-lcudaq-qilimanjaro-qpu"] + # Allow evolve API in C++ + preprocessor-defines: ["-D CUDAQ_ANALOG_TARGET"] + # Library mode is only for simulators, physical backends must turn this off + library-mode: false + # Tell NVQ++ to generate glue code to set the target backend name + gen-target-backend: true + +target-arguments: + - key: machine + required: false + type: string + platform-arg: machine + help-string: "Specify the Qilimanjaro machine." diff --git a/scripts/validate_container.sh b/scripts/validate_container.sh index 18569bfd991..59024b8a251 100644 --- a/scripts/validate_container.sh +++ b/scripts/validate_container.sh @@ -84,6 +84,7 @@ available_backends=`\ if [ "${qpu}" != "remote_rest" ] && [ "${qpu}" != "NvcfSimulatorQPU" ] \ && [ "${qpu}" != "fermioniq" ] && [ "${qpu}" != "orca" ] \ && [ "${qpu}" != "pasqal" ] && [ "${qpu}" != "quera" ] \ + && [ "${qpu}" != "qilimanjaro" ] \ && ($gpu_available || [ -z "$gpus" ] || [ "${gpus,,}" == "false" ]); then \ basename $file | cut -d "." -f 1; \ fi; \ diff --git a/targettests/lit.site.cfg.py.in b/targettests/lit.site.cfg.py.in index 3526ec984e7..1265abd5c27 100644 --- a/targettests/lit.site.cfg.py.in +++ b/targettests/lit.site.cfg.py.in @@ -57,6 +57,13 @@ if cmake_boolvar_to_bool(config.cudaq_backends_pasqal): else: config.substitutions.append(('%pasqal_avail', 'false')) +config.cudaq_backends_qilimanjaro = "@CUDAQ_ENABLE_QILIMANJARO_BACKEND@" +if cmake_boolvar_to_bool(config.cudaq_backends_qilimanjaro): + config.available_features.add('qilimanjaro') + config.substitutions.append(('%qilimanjaro_avail', 'true')) +else: + config.substitutions.append(('%qilimanjaro_avail', 'false')) + config.cudaq_backends_qci = "@CUDAQ_ENABLE_QCI_BACKEND@" if cmake_boolvar_to_bool(config.cudaq_backends_qci): config.available_features.add('qci') diff --git a/unittests/backends/CMakeLists.txt b/unittests/backends/CMakeLists.txt index 2868a780618..0e46b0a1652 100644 --- a/unittests/backends/CMakeLists.txt +++ b/unittests/backends/CMakeLists.txt @@ -28,3 +28,4 @@ endif() add_subdirectory(pasqal) add_subdirectory(qpp_observe) add_subdirectory(quera) +add_subdirectory(qilimanjaro) diff --git a/unittests/backends/qilimanjaro/CMakeLists.txt b/unittests/backends/qilimanjaro/CMakeLists.txt new file mode 100644 index 00000000000..940851cb1fc --- /dev/null +++ b/unittests/backends/qilimanjaro/CMakeLists.txt @@ -0,0 +1,25 @@ +# ============================================================================ # +# Copyright (c) 2022 - 2025 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # + +add_executable(test_qilimanjaro QilimanjaroTester.cpp) + +if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND NOT APPLE) + target_link_options(test_qilimanjaro PRIVATE -Wl,--no-as-needed) +endif() + +target_include_directories(test_qilimanjaro PRIVATE ../..) + +target_link_libraries(test_qilimanjaro + PRIVATE + fmt::fmt-header-only + cudaq + cudaq-common + gtest_main +) + +gtest_discover_tests(test_qilimanjaro) diff --git a/unittests/backends/qilimanjaro/QilimanjaroTester.cpp b/unittests/backends/qilimanjaro/QilimanjaroTester.cpp new file mode 100644 index 00000000000..d4e1aae9b97 --- /dev/null +++ b/unittests/backends/qilimanjaro/QilimanjaroTester.cpp @@ -0,0 +1,102 @@ +/******************************************************************************* + * Copyright (c) 2025 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +#include "CUDAQTestUtils.h" +#include "common/AnalogHamiltonian.h" + +const std::string sampleTimeEvolutionPayload = R"( + { + "type": "time_evolution", + "payload": { + "hamiltonians": { + "Hinit": { + "X0": 0.8, + "X1": 0.6, + "X0X1": 0.4 + }, + "Hinter": { + "Z0": 1.0, + "Z1": 0.9, + "Z0Z1": 0.5 + }, + "Hfinal": { + "Z0": 1.2, + "Z1": 1.0, + "Z0Z1": 0.7 + } + }, + "schedules": [ + { + "Hinit": [1.0, 0.75, 0.5, 0.25, 0.0], + "Hinter": [0.0, 0.25, 0.5, 0.75, 1.0], + "Hfinal": [0.0, 0.0, 0.0, 0.0, 0.0] + }, + { + "Hinit": [0.0, 0.0, 0.0, 0.0, 0.0], + "Hinter": [1.0, 0.75, 0.5, 0.25, 0.0], + "Hfinal": [0.0, 0.25, 0.5, 0.75, 1.0] + } + ], + "observables": [ + { + "Z0": 1.0, + "Z1": 0.8, + "Z0Z1": 0.6 + }, + { + "X0": 0.9, + "X1": 0.7, + "X0X1": 0.5 + } + ], + "initial_state": [ + {"real": 0.7071, "imag": 0.0}, + {"real": 0.0, "imag": 0.0}, + {"real": 0.0, "imag": 0.0}, + {"real": 0.7071, "imag": 0.0} + ] + } + } +)"; + +CUDAQ_TEST(QilimanjaroTester, checkTimeEvolutionJson) { + // Define Hamiltonians + cudaq::spin_op Hinit = 0.8 * cudaq::spin::x(0) + 0.6 * cudaq::spin::x(1) + 0.4 * cudaq::spin::x(0) * cudaq::spin::x(1); + cudaq::spin_op Hinter = 1.0 * cudaq::spin::z(0) + 0.9 * cudaq::spin::z(1) + 0.5 * cudaq::spin::z(0) * cudaq::spin::z(1); + cudaq::spin_op Hfinal = 1.2 * cudaq::spin::z(0) + 1.0 * cudaq::spin::z(1) + 0.7 * cudaq::spin::z(0) * cudaq::spin::z(1); + + // Define initial state + std::vector> initial_state_data = { + {0.7071, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {0.7071, 0.0} + }; + cudaq::state initial_state = cudaq::state::from_data(initial_state_data); + + // Define observables + std::vector observables = { + 1.0 * cudaq::spin::z(0) + 0.7 * cudaq::spin::z(1) + 0.6 * cudaq::spin::z(0) * cudaq::spin::z(1), + 0.9 * cudaq::spin::x(0) + 0.7 * cudaq::spin::x(1) + 0.5 * cudaq::spin::x(0) * cudaq::spin::x(1) + }; + + // Define schedules (Is there any cudaq class?) + std::vector>> schedules = { + { + {"Hinit", {1.0, 0.75, 0.5, 0.25, 0.0}}, + {"Hinter", {0.0, 0.25, 0.5, 0.75, 1.0}}, + {"Hfinal", {0.0, 0.0, 0.0, 0.0, 0.0}} + }, + { + {"Hinit", {0.0, 0.0, 0.0, 0.0, 0.0}}, + {"Hinter", {1.0, 0.75, 0.5, 0.25, 0.0}}, + {"Hfinal", {0.0, 0.25, 0.5, 0.75, 1.0}} + } + }; + + // TODO: How will we construct the payload and which is the class we should parse into? + + auto refPayload = nlohmann::json::parse(sampleTimeEvolutionPayload); +} \ No newline at end of file