diff --git a/CHANGELOG.md b/CHANGELOG.md index 4994358b81..f29facf2b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ releases may include breaking changes. ### Added +- ✨ Add support for reopening existing QDMI jobs in C++, Python, and the QDMI + driver ([#2008]) ([**@burgholzer**]) - ✨ Add PennyLane support for gate-based QDMI devices ([#2005]) ([**@burgholzer**]) - ✨ Integrate QDMI devices as MLIR compiler targets across C++, Python, and @@ -720,6 +722,7 @@ for previous changelogs._ +[#2008]: https://github.com/munich-quantum-toolkit/core/pull/2008 [#2007]: https://github.com/munich-quantum-toolkit/core/pull/2007 [#2006]: https://github.com/munich-quantum-toolkit/core/pull/2006 [#2005]: https://github.com/munich-quantum-toolkit/core/pull/2005 diff --git a/bindings/fomac/fomac.cpp b/bindings/fomac/fomac.cpp index bad0b9681c..d2e7330e62 100644 --- a/bindings/fomac/fomac.cpp +++ b/bindings/fomac/fomac.cpp @@ -470,6 +470,14 @@ when the custom slot is unsupported.)pb"); "custom5"_a = nb::none(), nb::rv_policy::reference_internal, "Submits an exact byte payload to the device."); + device.def( + "open_job", + [](const fomac::Device& self, const std::string& jobId) { + return self.openJob(jobId); + }, + "job_id"_a, nb::rv_policy::reference_internal, + "Opens an existing job by its device-provided ID."); + device.def("__repr__", [](const fomac::Device& dev) { return ""; }); diff --git a/cmake/ExternalDependencies.cmake b/cmake/ExternalDependencies.cmake index c7f695d2b0..008d0b344d 100644 --- a/cmake/ExternalDependencies.cmake +++ b/cmake/ExternalDependencies.cmake @@ -89,11 +89,11 @@ if(BUILD_MQT_CORE_TESTS) endif() # cmake-format: off -set(QDMI_MINIMUM_VERSION 1.3.2 +set(QDMI_MINIMUM_VERSION 1.3.3 CACHE STRING "Minimum QDMI version") -set(QDMI_VERSION 1.3.2 +set(QDMI_VERSION 1.3.3 CACHE STRING "QDMI version") -set(QDMI_REV "d05a0b418f42e54e9585d2e00af8ce23e745fd83" # v1.3.2 +set(QDMI_REV "3716948044e23aa50c084732aae2cc3ee913014e" # QDMI PR #485 CACHE STRING "QDMI identifier (tag, branch or commit hash)") set(QDMI_REPO_OWNER "Munich-Quantum-Software-Stack" CACHE STRING "QDMI repository owner (change when using a fork)") diff --git a/include/mqt-core/fomac/FoMaC.hpp b/include/mqt-core/fomac/FoMaC.hpp index 652fffca3e..f275d6b0a0 100644 --- a/include/mqt-core/fomac/FoMaC.hpp +++ b/include/mqt-core/fomac/FoMaC.hpp @@ -551,6 +551,16 @@ class Device { const std::optional& custom4 = std::nullopt, const std::optional& custom5 = std::nullopt) const; + /** + * @brief Opens an existing job by its device-provided ID. + * @details Opening a job does not submit, clone, or modify the remote job. + * The returned handle can be used to query its state and retrieve results. + * @param jobId The nonempty opaque ID returned by @ref Job::getId. + * @throws std::runtime_error If the driver or device cannot open the job. + * @see QDMI_device_open_job + */ + [[nodiscard]] Job openJob(std::string_view jobId) const; + auto operator<=>(const Device&) const noexcept = default; private: diff --git a/include/mqt-core/qdmi/driver/Driver.hpp b/include/mqt-core/qdmi/driver/Driver.hpp index 974c754fd9..48b86ce4b0 100644 --- a/include/mqt-core/qdmi/driver/Driver.hpp +++ b/include/mqt-core/qdmi/driver/Driver.hpp @@ -123,6 +123,9 @@ struct DeviceLibrary { /// Function pointer to @ref QDMI_device_session_create_device_job. decltype(QDMI_device_session_create_device_job)* device_session_create_device_job{}; + /// Optional function pointer to @ref QDMI_device_session_open_device_job. + decltype(QDMI_device_session_open_device_job)* + device_session_open_device_job{}; /// Function pointer to @ref QDMI_device_job_free. decltype(QDMI_device_job_free)* device_job_free{}; /// Function pointer to @ref QDMI_device_job_set_parameter. @@ -269,6 +272,12 @@ struct QDMI_Device_impl_d { */ auto createJob(QDMI_Job* job) -> int; + /** + * @brief Opens an existing job for the device. + * @see QDMI_device_open_job + */ + auto openJob(const char* jobId, QDMI_Job* job) -> int; + /** * @brief Frees the job associated with the device. * @see QDMI_job_free diff --git a/python/mqt/core/fomac.pyi b/python/mqt/core/fomac.pyi index b240309e9c..8b2b3e26e9 100644 --- a/python/mqt/core/fomac.pyi +++ b/python/mqt/core/fomac.pyi @@ -369,6 +369,9 @@ class Device: ) -> Job: """Submits an exact byte payload to the device.""" + def open_job(self, job_id: str) -> Job: + """Opens an existing job by its device-provided ID.""" + def __eq__(self, arg: object, /) -> bool: ... def __ne__(self, arg: object, /) -> bool: ... diff --git a/src/fomac/FoMaC.cpp b/src/fomac/FoMaC.cpp index fb3e7156a6..dee784c54f 100644 --- a/src/fomac/FoMaC.cpp +++ b/src/fomac/FoMaC.cpp @@ -407,6 +407,14 @@ Job Device::submitJob(const std::span program, return jobWrapper; } +Job Device::openJob(const std::string_view jobId) const { + const std::string id{jobId}; + QDMI_Job job = nullptr; + qdmi::throwIfError(QDMI_device_open_job(device_.get(), id.c_str(), &job), + "Opening job"); + return Job{job, device_}; +} + void Device::setCustomJobParam(QDMI_Job job, const QDMI_Job_Parameter param, const CustomJobParameter& value) { std::visit( diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index 7a85a8038c..fee8130420 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -844,6 +844,13 @@ int MQT_DDSIM_QDMI_device_session_create_device_job( return session->createDeviceJob(job); } +int MQT_DDSIM_QDMI_device_session_open_device_job( + [[maybe_unused]] MQT_DDSIM_QDMI_Device_Session session, + [[maybe_unused]] const char* jobId, + [[maybe_unused]] MQT_DDSIM_QDMI_Device_Job* job) { + return QDMI_ERROR_NOTSUPPORTED; +} + void MQT_DDSIM_QDMI_device_job_free(MQT_DDSIM_QDMI_Device_Job job) { job->free(); } diff --git a/src/qdmi/devices/na/Device.cpp b/src/qdmi/devices/na/Device.cpp index a6a68d2204..6b049c5b55 100644 --- a/src/qdmi/devices/na/Device.cpp +++ b/src/qdmi/devices/na/Device.cpp @@ -684,6 +684,13 @@ int MQT_NA_QDMI_device_session_create_device_job( return session->createDeviceJob(job); } +int MQT_NA_QDMI_device_session_open_device_job( + [[maybe_unused]] MQT_NA_QDMI_Device_Session session, + [[maybe_unused]] const char* jobId, + [[maybe_unused]] MQT_NA_QDMI_Device_Job* job) { + return QDMI_ERROR_NOTSUPPORTED; +} + void MQT_NA_QDMI_device_job_free(MQT_NA_QDMI_Device_Job job) { if (job != nullptr) { job->free(); diff --git a/src/qdmi/devices/sc/Device.cpp b/src/qdmi/devices/sc/Device.cpp index a62a7955d9..5fb9faede4 100644 --- a/src/qdmi/devices/sc/Device.cpp +++ b/src/qdmi/devices/sc/Device.cpp @@ -470,6 +470,12 @@ int MQT_SC_QDMI_device_session_create_device_job( return session == nullptr ? QDMI_ERROR_INVALIDARGUMENT : session->createDeviceJob(job); } +int MQT_SC_QDMI_device_session_open_device_job( + [[maybe_unused]] MQT_SC_QDMI_Device_Session session, + [[maybe_unused]] const char* jobId, + [[maybe_unused]] MQT_SC_QDMI_Device_Job* job) { + return QDMI_ERROR_NOTSUPPORTED; +} void MQT_SC_QDMI_device_job_free(MQT_SC_QDMI_Device_Job job) { if (job != nullptr) { job->free(); diff --git a/src/qdmi/driver/Driver.cpp b/src/qdmi/driver/Driver.cpp index 0e7be02538..fa8fc32322 100644 --- a/src/qdmi/driver/Driver.cpp +++ b/src/qdmi/driver/Driver.cpp @@ -122,6 +122,12 @@ DynamicDeviceLibrary::DynamicDeviceLibrary(const std::string& libName, throw std::runtime_error("Failed to load symbol: " + symbolName); \ } \ } +#define LOAD_OPTIONAL_DYNAMIC_SYMBOL(symbol) \ + { \ + const std::string symbolName = std::string(prefix) + "_QDMI_" + #symbol; \ + (symbol) = reinterpret_cast( \ + DL_SYM(libHandle_, symbolName.c_str())); \ + } //===----------------------------------------------------------------------===// try { @@ -136,6 +142,7 @@ DynamicDeviceLibrary::DynamicDeviceLibrary(const std::string& libName, LOAD_DYNAMIC_SYMBOL(device_session_set_parameter) // device job interface LOAD_DYNAMIC_SYMBOL(device_session_create_device_job) + LOAD_OPTIONAL_DYNAMIC_SYMBOL(device_session_open_device_job) LOAD_DYNAMIC_SYMBOL(device_job_free) LOAD_DYNAMIC_SYMBOL(device_job_set_parameter) LOAD_DYNAMIC_SYMBOL(device_job_query_property) @@ -234,6 +241,7 @@ void applyOverride(std::optional& value, #undef DL_OPEN #undef DL_SYM #undef DL_CLOSE +#undef LOAD_OPTIONAL_DYNAMIC_SYMBOL } // namespace qdmi QDMI_Device_impl_d::QDMI_Device_impl_d( @@ -397,6 +405,26 @@ auto QDMI_Device_impl_d::createJob(QDMI_Job* job) -> int { return QDMI_SUCCESS; } +auto QDMI_Device_impl_d::openJob(const char* const jobId, QDMI_Job* job) + -> int { + if (jobId == nullptr || *jobId == '\0' || job == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (library_->device_session_open_device_job == nullptr) { + return QDMI_ERROR_NOTSUPPORTED; + } + QDMI_Device_Job deviceJob = nullptr; + const auto result = library_->device_session_open_device_job( + deviceSession_, jobId, &deviceJob); + if (result != QDMI_SUCCESS) { + return result; + } + auto uniqueJob = std::make_unique(deviceJob, this); + const auto it = jobs_.emplace(uniqueJob.get(), std::move(uniqueJob)).first; + *job = it->first; + return QDMI_SUCCESS; +} + auto QDMI_Device_impl_d::freeJob(QDMI_Job job) -> void { if (job != nullptr) { jobs_.erase(job); @@ -798,6 +826,13 @@ int QDMI_device_create_job(QDMI_Device dev, QDMI_Job* job) { return dev->createJob(job); } +int QDMI_device_open_job(QDMI_Device dev, const char* jobId, QDMI_Job* job) { + if (dev == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + return dev->openJob(jobId, job); +} + void QDMI_job_free(QDMI_Job job) { if (job != nullptr) { job->free(); diff --git a/test/python/fomac/test_fomac.py b/test/python/fomac/test_fomac.py index 631c3ab4c2..4dd49cb73b 100644 --- a/test/python/fomac/test_fomac.py +++ b/test/python/fomac/test_fomac.py @@ -586,6 +586,12 @@ def test_device_submit_job_preserves_num_shots(ddsim_device: Device) -> None: assert job3.num_shots == 1000 +def test_device_open_job_reports_unsupported_provider(ddsim_device: Device) -> None: + """Expose job reopening through Python without requiring DDSIM support.""" + with pytest.raises(RuntimeError, match=r"Opening job: Not supported\."): + ddsim_device.open_job("unknown") + + @pytest.fixture def submitted_job(ddsim_device: Device) -> Job: """Fixture that provides a submitted job for testing. diff --git a/test/qdmi/driver/session_device.cpp b/test/qdmi/driver/session_device.cpp index 5f91e81268..08fd379b77 100644 --- a/test/qdmi/driver/session_device.cpp +++ b/test/qdmi/driver/session_device.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include struct QDMI_Child_Device_impl_d {}; @@ -27,6 +28,8 @@ struct QDMI_Device_Session_impl_d { struct QDMI_Device_Job_impl_d { QDMI_Device_Session session = nullptr; + std::string id = "session-job"; + bool opened = false; }; namespace { @@ -205,14 +208,33 @@ TEST_SESSION_QDMI_device_session_create_device_job(QDMI_Device_Session session, } // The QDMI C API transfers this allocation through an opaque raw handle. // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - *job = new (std::nothrow) QDMI_Device_Job_impl_d{session}; + *job = new (std::nothrow) QDMI_Device_Job_impl_d{.session = session}; + return *job == nullptr ? QDMI_ERROR_OUTOFMEM : QDMI_SUCCESS; +} + +extern "C" int TEST_SESSION_QDMI_device_session_open_device_job( + QDMI_Device_Session session, const char* jobId, QDMI_Device_Job* job) { + if (session == nullptr || !session->initialized || jobId == nullptr || + *jobId == '\0' || job == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (std::string_view{jobId} != "session-job") { + return QDMI_ERROR_NOTFOUND; + } + // The QDMI C API transfers this allocation through an opaque raw handle. + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + *job = new (std::nothrow) + QDMI_Device_Job_impl_d{.session = session, .opened = true}; return *job == nullptr ? QDMI_ERROR_OUTOFMEM : QDMI_SUCCESS; } extern "C" int TEST_SESSION_QDMI_device_job_set_parameter( QDMI_Device_Job job, QDMI_Device_Job_Parameter /*parameter*/, size_t /*size*/, const void* /*value*/) { - return job == nullptr ? QDMI_ERROR_INVALIDARGUMENT : QDMI_SUCCESS; + if (job == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + return job->opened ? QDMI_ERROR_BADSTATE : QDMI_SUCCESS; } extern "C" int TEST_SESSION_QDMI_device_job_query_property( @@ -222,14 +244,14 @@ extern "C" int TEST_SESSION_QDMI_device_job_query_property( prop != QDMI_DEVICE_JOB_PROPERTY_ID) { return QDMI_ERROR_INVALIDARGUMENT; } - return queryString("session-job", size, value, sizeRet); + return queryString(job->id, size, value, sizeRet); } extern "C" int TEST_SESSION_QDMI_device_job_submit(QDMI_Device_Job job) { if (job == nullptr || job->session == nullptr) { return QDMI_ERROR_INVALIDARGUMENT; } - return QDMI_SUCCESS; + return job->opened ? QDMI_ERROR_BADSTATE : QDMI_SUCCESS; } extern "C" int TEST_SESSION_QDMI_device_job_cancel(QDMI_Device_Job /*job*/) { diff --git a/test/qdmi/driver/test_driver.cpp b/test/qdmi/driver/test_driver.cpp index 3b982cb172..2e28c60f08 100644 --- a/test/qdmi/driver/test_driver.cpp +++ b/test/qdmi/driver/test_driver.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -265,6 +266,15 @@ class ChildDeviceLibrary final : public qdmi::DeviceLibrary { return driver.open(id); } +[[nodiscard]] auto openOwnedSessionTestDevice(const std::string_view id) + -> fomac::Device { + static_cast(qdmi::Driver::get().registerDeviceIfAbsent( + {.id = std::string{id}, + .library = MQT_CORE_QDMI_SESSION_DEVICE, + .prefix = "TEST_SESSION"})); + return fomac::Session::openDevice(id); +} + class DriverTest : public testing::TestWithParam { protected: QDMI_Session session = nullptr; @@ -455,6 +465,58 @@ TEST_P(DriverTest, JobCreate) { QDMI_ERROR_INVALIDARGUMENT); } +TEST_P(DriverTest, JobOpen) { + QDMI_Job job = nullptr; + const auto result = QDMI_device_open_job(device, "session-job", &job); + if (result == QDMI_ERROR_NOTSUPPORTED) { + return; + } + ASSERT_EQ(result, QDMI_SUCCESS); + ASSERT_NE(job, nullptr); + QDMI_job_free(job); + EXPECT_EQ(QDMI_device_open_job(device, "", &job), QDMI_ERROR_INVALIDARGUMENT); + EXPECT_EQ(QDMI_device_open_job(device, "unknown", &job), QDMI_ERROR_NOTFOUND); +} + +TEST(JobOpenTest, OpensExistingJobThroughClientApi) { + const auto ownedDevice = openOwnedSessionTestDevice("test.open-job-client"); + QDMI_Device device = ownedDevice; + QDMI_Job job = nullptr; + ASSERT_EQ(QDMI_device_open_job(device, "session-job", &job), QDMI_SUCCESS); + ASSERT_NE(job, nullptr); + + size_t size = 0; + ASSERT_EQ( + QDMI_job_query_property(job, QDMI_JOB_PROPERTY_ID, 0, nullptr, &size), + QDMI_SUCCESS); + std::string id(size - 1, '\0'); + EXPECT_EQ(QDMI_job_query_property(job, QDMI_JOB_PROPERTY_ID, size, id.data(), + nullptr), + QDMI_SUCCESS); + EXPECT_EQ(id, "session-job"); + const size_t numShots = 1; + EXPECT_EQ(QDMI_job_set_parameter(job, QDMI_JOB_PARAMETER_SHOTSNUM, + sizeof(numShots), &numShots), + QDMI_ERROR_BADSTATE); + EXPECT_EQ(QDMI_job_submit(job), QDMI_ERROR_BADSTATE); + QDMI_job_free(job); + + EXPECT_EQ(QDMI_device_open_job(nullptr, "session-job", &job), + QDMI_ERROR_INVALIDARGUMENT); + EXPECT_EQ(QDMI_device_open_job(device, nullptr, &job), + QDMI_ERROR_INVALIDARGUMENT); + EXPECT_EQ(QDMI_device_open_job(device, "", &job), QDMI_ERROR_INVALIDARGUMENT); + EXPECT_EQ(QDMI_device_open_job(device, "session-job", nullptr), + QDMI_ERROR_INVALIDARGUMENT); + EXPECT_EQ(QDMI_device_open_job(device, "unknown", &job), QDMI_ERROR_NOTFOUND); +} + +TEST(FoMaCJobTest, OpensExistingJobs) { + const auto device = openOwnedSessionTestDevice("test.open-job-fomac"); + const auto job = device.openJob("session-job"); + EXPECT_EQ(job.getId(), "session-job"); +} + TEST_P(DriverTest, JobSetParameter) { EXPECT_EQ(QDMI_job_set_parameter(nullptr, QDMI_JOB_PARAMETER_MAX, 0, nullptr), QDMI_ERROR_INVALIDARGUMENT);