diff --git a/python/cudaq/runtime/sample.py b/python/cudaq/runtime/sample.py index 37f606f3e72..1de5739c602 100644 --- a/python/cudaq/runtime/sample.py +++ b/python/cudaq/runtime/sample.py @@ -21,10 +21,11 @@ def __broadcastSample(kernel, N = len(argSet) results = [] for i, a in enumerate(argSet): - ctx = cudaq_runtime.ExecutionContext('sample', shots_count) + ctx = cudaq_runtime.ExecutionContext( + "sample_explicit" if explicit_measurements else "sample", + shots_count) ctx.totalIterations = N ctx.batchIteration = i - ctx.explicitMeasurements = explicit_measurements cudaq_runtime.setExecutionContext(ctx) kernel(*a) res = ctx.result @@ -111,9 +112,9 @@ def sample(kernel, cudaq_runtime.unset_noise() return res - ctx = cudaq_runtime.ExecutionContext("sample", shots_count) + ctx = cudaq_runtime.ExecutionContext( + "sample_explicit" if explicit_measurements else "sample", shots_count) ctx.hasConditionalsOnMeasureResults = has_conditionals_on_measure_result - ctx.explicitMeasurements = explicit_measurements cudaq_runtime.setExecutionContext(ctx) counts = cudaq_runtime.SampleResult() diff --git a/python/runtime/common/py_ExecutionContext.cpp b/python/runtime/common/py_ExecutionContext.cpp index 70d83dd52f8..05640228bed 100644 --- a/python/runtime/common/py_ExecutionContext.cpp +++ b/python/runtime/common/py_ExecutionContext.cpp @@ -36,8 +36,6 @@ void bindExecutionContext(py::module &mod) { .def_readwrite("batchIteration", &cudaq::ExecutionContext::batchIteration) .def_readwrite("numberTrajectories", &cudaq::ExecutionContext::numberTrajectories) - .def_readwrite("explicitMeasurements", - &cudaq::ExecutionContext::explicitMeasurements) .def_readonly("invocationResultBuffer", &cudaq::ExecutionContext::invocationResultBuffer) .def("setSpinOperator", diff --git a/runtime/common/BaseRemoteRESTQPU.h b/runtime/common/BaseRemoteRESTQPU.h index 3ba10daae3c..103d5bf6b49 100644 --- a/runtime/common/BaseRemoteRESTQPU.h +++ b/runtime/common/BaseRemoteRESTQPU.h @@ -19,6 +19,7 @@ #include "common/Resources.h" #include "common/RestClient.h" #include "common/RuntimeMLIR.h" +#include "common/SamplingMode.h" #include "cudaq.h" #include "cudaq/Frontend/nvqpp/AttributeNames.h" #include "cudaq/Optimizer/Builder/Intrinsics.h" @@ -711,7 +712,7 @@ class BaseRemoteRESTQPU : public QPU { } if (executionContext) { - if (executionContext->name == "sample") { + if (cudaq::details::isSamplingContext(executionContext)) { executionContext->reorderIdx = mapping_reorder_idx; // No need to add measurements only to remove them eventually if (postCodeGenPasses.find("remove-measurements") == std::string::npos) diff --git a/runtime/common/ExecutionContext.h b/runtime/common/ExecutionContext.h index 69776848a96..674b74d1443 100644 --- a/runtime/common/ExecutionContext.h +++ b/runtime/common/ExecutionContext.h @@ -124,10 +124,6 @@ class ExecutionContext { /// calculation on simulation backends that support trajectory simulation. std::optional numberTrajectories = std::nullopt; - /// @brief Whether or not to simply concatenate measurements in execution - /// order. - bool explicitMeasurements = false; - /// @brief Probability of occurrence of each error mechanism (column) in /// Measurement Syndrome Matrix (0-1 range). std::optional> msm_probabilities; diff --git a/runtime/common/SamplingMode.h b/runtime/common/SamplingMode.h new file mode 100644 index 00000000000..2787b450cd8 --- /dev/null +++ b/runtime/common/SamplingMode.h @@ -0,0 +1,56 @@ +/****************************************************************-*- 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 "ExecutionContext.h" + +namespace cudaq::details { + +enum class SamplingMode { + // Standard sampling mode, measurements are ordered by the qubit allocation + // order, no duplicates + Default, + // Explicit measurement mode, measurements are concatenated by the execution + // order, duplicates allowed + Explicit, +}; + +/// Derive sampling mode from execution context +inline SamplingMode getSamplingMode(const ExecutionContext *ctx) { + if (!ctx) + throw std::runtime_error("ExecutionContext is null."); + if (ctx->name == "sample") + return SamplingMode::Default; + if (ctx->name == "sample_explicit") + return SamplingMode::Explicit; + throw std::runtime_error("Unknown sampling mode - " + ctx->name); +} + +/// Check if this is any sampling context +inline bool isSamplingContext(const ExecutionContext *ctx) { + if (!ctx) + throw std::runtime_error("ExecutionContext is null."); + return ctx->name.find("sample") != std::string::npos; +} + +/// Check if this is explicit measurements sampling mode +inline bool isExplicitSamplingMode(const ExecutionContext *ctx) { + if (!ctx) + throw std::runtime_error("ExecutionContext is null."); + return getSamplingMode(ctx) == SamplingMode::Explicit; +} + +/// Derive sampling mode from execution context name +inline SamplingMode getSamplingMode(const std::string &contextName) { + if (contextName == "sample") + return SamplingMode::Default; + if (contextName == "sample_explicit") + return SamplingMode::Explicit; + throw std::runtime_error("Unknown sampling mode - " + contextName); +} + +} // namespace cudaq::details diff --git a/runtime/cudaq/algorithms/sample.h b/runtime/cudaq/algorithms/sample.h index 60cb1ef8355..e01fc7d983c 100644 --- a/runtime/cudaq/algorithms/sample.h +++ b/runtime/cudaq/algorithms/sample.h @@ -54,12 +54,12 @@ runSampling(KernelFunctor &&wrappedKernel, quantum_platform &platform, "kernel with conditional logic on a measurement result."); } // Create the execution context. - auto ctx = std::make_unique("sample", shots); + auto ctx = std::make_unique( + explicitMeasurements ? "sample_explicit" : "sample", shots); ctx->kernelName = kernelName; ctx->batchIteration = batchIteration; ctx->totalIterations = totalBatchIters; ctx->hasConditionalsOnMeasureResults = hasConditionalFeebdback; - ctx->explicitMeasurements = explicitMeasurements; #ifdef CUDAQ_LIBRARY_MODE // If we have a kernel that has its quake code registered, we diff --git a/runtime/nvqir/CircuitSimulator.h b/runtime/nvqir/CircuitSimulator.h index 057109336ce..85dce9f2b0b 100644 --- a/runtime/nvqir/CircuitSimulator.h +++ b/runtime/nvqir/CircuitSimulator.h @@ -16,6 +16,7 @@ #include "common/NoiseModel.h" #include "common/QuditIdTracker.h" #include "common/SampleResult.h" +#include "common/SamplingMode.h" #include "common/Timing.h" #include "cudaq/host_config.h" #include @@ -101,6 +102,10 @@ class CircuitSimulator { /// sample() function. bool supportsBufferedSample = false; + /// @brief The current sampling mode, derived from execution context + cudaq::details::SamplingMode currentSamplingMode = + cudaq::details::SamplingMode::Default; + public: /// @brief The constructor CircuitSimulator() = default; @@ -500,7 +505,8 @@ class CircuitSimulatorBase : public CircuitSimulator { return 1; if (executionContext->hasConditionalsOnMeasureResults) return 1; - if (executionContext->explicitMeasurements && !supportsBufferedSample) + if (cudaq::details::isExplicitSamplingMode(executionContext) && + !supportsBufferedSample) return 1; return static_cast(executionContext->shots); } @@ -542,48 +548,58 @@ class CircuitSimulatorBase : public CircuitSimulator { "Simulation data not available for this simulator backend."); } - /// @brief Handle basic sampling tasks by storing the qubit index for - /// processing in resetExecutionContext. Return true to indicate this is - /// sampling and to exit early. False otherwise. + /// @brief Handle measurement recording for default sampling mode. Builds + /// register mappings, allows only unique measurements per qubit. + bool handleBasicSampling_Default(const std::size_t qubitIdx, + const std::string ®Name) { + // Add the qubit to the sampling list + sampleQubits.push_back(qubitIdx); + auto processForRegName = [&](const std::string ®Str) { + // Insert the sample qubit into the register name map + auto iter = registerNameToMeasuredQubit.find(regStr); + if (iter == registerNameToMeasuredQubit.end()) + registerNameToMeasuredQubit.emplace(regStr, + std::vector{qubitIdx}); + else if (std::find(iter->second.begin(), iter->second.end(), qubitIdx) == + iter->second.end()) + iter->second.push_back(qubitIdx); + }; + // Insert into global register and named register (if it exists) + processForRegName(cudaq::GlobalRegisterName); + if (!regName.empty()) + processForRegName(regName); + return true; + } + + /// @brief Handle measurement recording for explicit sampling mode. Preserves + /// execution order, allows duplicate measurements per qubit. + bool handleBasicSampling_Explicit(const std::size_t qubitIdx, + const std::string ®Name) { + // Handle duplicate measurements in explicit measurements mode + auto iter = std::find(sampleQubits.begin(), sampleQubits.end(), qubitIdx); + if (iter != sampleQubits.end()) + flushAnySamplingTasks(/*force this*/ true); + // Add the qubit to the sampling list + sampleQubits.push_back(qubitIdx); + return true; + } + + /// @brief Handle basic sampling tasks by dispatching to the sampling + /// mode-specific implementation. Return true to indicate this is sampling and + /// to exit early. False otherwise. bool handleBasicSampling(const std::size_t qubitIdx, const std::string ®Name) { - if (executionContext && executionContext->name == "sample" && + if (executionContext && + cudaq::details::isSamplingContext(executionContext) && !executionContext->hasConditionalsOnMeasureResults) { - - // Handle duplicate measurements in explicit measurements mode - if (executionContext->explicitMeasurements) { - auto iter = - std::find(sampleQubits.begin(), sampleQubits.end(), qubitIdx); - if (iter != sampleQubits.end()) - flushAnySamplingTasks(/*force this*/ true); + switch (currentSamplingMode) { + case cudaq::details::SamplingMode::Explicit: + return handleBasicSampling_Explicit(qubitIdx, regName); + case cudaq::details::SamplingMode::Default: + default: + return handleBasicSampling_Default(qubitIdx, regName); } - // Add the qubit to the sampling list - sampleQubits.push_back(qubitIdx); - - // If we're using explicit measurements (an optimized sampling mode), then - // don't populate registerNameToMeasuredQubit. - if (executionContext->explicitMeasurements) - return true; - - auto processForRegName = [&](const std::string ®Str) { - // Insert the sample qubit into the register name map - auto iter = registerNameToMeasuredQubit.find(regStr); - if (iter == registerNameToMeasuredQubit.end()) - registerNameToMeasuredQubit.emplace( - regStr, std::vector{qubitIdx}); - else if (std::find(iter->second.begin(), iter->second.end(), - qubitIdx) == iter->second.end()) - iter->second.push_back(qubitIdx); - }; - - // Insert into global register and named register (if it exists) - processForRegName(cudaq::GlobalRegisterName); - if (!regName.empty()) - processForRegName(regName); - - return true; } - return false; } @@ -596,7 +612,8 @@ class CircuitSimulatorBase : public CircuitSimulator { const std::string ®isterName) { // We still care about what qubit we are measuring if in the // sample-conditional context - if (executionContext && executionContext->name == "sample" && + if (executionContext && + cudaq::details::isSamplingContext(executionContext) && executionContext->hasConditionalsOnMeasureResults) { std::string mutableRegisterName = registerName; @@ -710,55 +727,29 @@ class CircuitSimulatorBase : public CircuitSimulator { "subclasses, override addQubitsToState."); } - /// @brief Execute a sampling task with the current set of sample qubits. - void flushAnySamplingTasks(bool force = false) { - if (force && supportsBufferedSample && - executionContext->explicitMeasurements) { - int nShots = getNumShotsToExec(); - if (!sampleQubits.empty()) { - // We have a few more qubits to be sampled. Call sample on the subclass, - // but there is no need to save the results this time. - sample(sampleQubits, nShots); - sampleQubits.clear(); - } - // OK, now we're ready to grab the buffered sample results for the entire - // execution context. - auto execResult = sample(sampleQubits, nShots); - executionContext->result.append(execResult); - return; - } - + /// @brief Execute a sampling task with the current set of sample qubits in + /// default sampling mode. + void flushAnySamplingTasks_Default(bool force) { if (sampleQubits.empty()) return; - if (executionContext->hasConditionalsOnMeasureResults && !force) return; - - // Sort the qubit indices (unless we're in the optimized sampling mode that - // simply concatenates sequential measurements) - if (!executionContext->explicitMeasurements) { - std::sort(sampleQubits.begin(), sampleQubits.end()); - auto last = std::unique(sampleQubits.begin(), sampleQubits.end()); - sampleQubits.erase(last, sampleQubits.end()); - } - + // Sort and deduplicate + std::sort(sampleQubits.begin(), sampleQubits.end()); + auto last = std::unique(sampleQubits.begin(), sampleQubits.end()); + sampleQubits.erase(last, sampleQubits.end()); CUDAQ_INFO("Sampling the current state, with measure qubits = {}", sampleQubits); - // Ask the subtype to sample the current state auto execResult = sample(sampleQubits, getNumShotsToExec()); - if (registerNameToMeasuredQubit.empty()) { - executionContext->result.append(execResult, - executionContext->explicitMeasurements); + executionContext->result.append(execResult, /*explicitMode=*/false); } else { - for (auto &[regName, qubits] : registerNameToMeasuredQubit) { // Measurements are sorted according to qubit allocation order std::sort(qubits.begin(), qubits.end()); auto last = std::unique(qubits.begin(), qubits.end()); qubits.erase(last, qubits.end()); - // Find the position of the qubits we have in the result bit string // Create a map of qubit to bit string location std::unordered_map qubitLocMap; @@ -768,7 +759,6 @@ class CircuitSimulatorBase : public CircuitSimulator { auto idx = std::distance(sampleQubits.begin(), iter); qubitLocMap.insert({qubits[i], idx}); } - cudaq::ExecutionResult tmp(regName); for (auto &[bits, count] : execResult.counts) { std::string b = ""; @@ -777,15 +767,52 @@ class CircuitSimulatorBase : public CircuitSimulator { b += bits[qubitLocMap[qb]]; tmp.appendResult(b, count); } - executionContext->result.append(tmp); } } - sampleQubits.clear(); registerNameToMeasuredQubit.clear(); } + /// @brief Execute sampling task with explicit measurements sampling mode + void flushAnySamplingTasks_Explicit(bool force) { + if (force && supportsBufferedSample) { + int nShots = getNumShotsToExec(); + if (!sampleQubits.empty()) { + // We have a few more qubits to be sampled. Call sample on the subclass, + // but there is no need to save the results this time. + sample(sampleQubits, nShots); + sampleQubits.clear(); + } + // OK, now we're ready to grab the buffered sample results for the entire + // execution context. + auto execResult = sample(sampleQubits, nShots); + executionContext->result.append(execResult); + return; + } + if (sampleQubits.empty()) + return; + CUDAQ_INFO("Sampling the current state, with measure qubits = {}", + sampleQubits); + // Ask the subtype to sample the current state + auto execResult = sample(sampleQubits, getNumShotsToExec()); + executionContext->result.append(execResult, /*explicitMode=*/true); + sampleQubits.clear(); + } + + /// @brief Execute a sampling task with the current set of sample qubits. + void flushAnySamplingTasks(bool force = false) { + switch (currentSamplingMode) { + case cudaq::details::SamplingMode::Explicit: + flushAnySamplingTasks_Explicit(force); + return; + case cudaq::details::SamplingMode::Default: + default: + flushAnySamplingTasks_Default(force); + return; + } + } + /// @brief Add a new gate application task to the queue void enqueueGate(const std::string name, const std::vector> &matrix, @@ -1129,13 +1156,11 @@ class CircuitSimulatorBase : public CircuitSimulator { if (!executionContext) return; - // Get the ExecutionContext name - auto execContextName = executionContext->name; - // If we are sampling... - if (execContextName.find("sample") != std::string::npos) { + if (cudaq::details::isSamplingContext(executionContext)) { // Sample the state over the specified number of shots - if (sampleQubits.empty() && !executionContext->explicitMeasurements) { + if (sampleQubits.empty() && + !cudaq::details::isExplicitSamplingMode(executionContext)) { if (isInBatchMode()) sampleQubits.resize(batchModeCurrentNumQubits); else @@ -1216,6 +1241,7 @@ class CircuitSimulatorBase : public CircuitSimulator { bool shouldSetToZero = isInBatchMode() && !isLastBatch(); executionContext = nullptr; + currentSamplingMode = cudaq::details::SamplingMode::Default; // Reset the state if we've deallocated all qubits. if (tracker.allDeallocated()) { @@ -1237,6 +1263,8 @@ class CircuitSimulatorBase : public CircuitSimulator { /// @brief Set the execution context void setExecutionContext(cudaq::ExecutionContext *context) override { executionContext = context; + if (cudaq::details::isSamplingContext(executionContext)) + currentSamplingMode = cudaq::details::getSamplingMode(context->name); executionContext->canHandleObserve = canHandleObserve(); currentCircuitName = context->kernelName; CUDAQ_INFO("Setting current circuit name to {}", currentCircuitName); diff --git a/runtime/nvqir/stim/StimCircuitSimulator.cpp b/runtime/nvqir/stim/StimCircuitSimulator.cpp index 3e7390a43a2..613baa67a97 100644 --- a/runtime/nvqir/stim/StimCircuitSimulator.cpp +++ b/runtime/nvqir/stim/StimCircuitSimulator.cpp @@ -155,7 +155,8 @@ class StimCircuitSimulator : public nvqir::CircuitSimulatorBase { // Default to single shot std::size_t batch_size = 1; auto *executionContext = getExecutionContext(); - if (executionContext && executionContext->name == "sample" && + if (executionContext && + cudaq::details::isSamplingContext(executionContext) && !executionContext->hasConditionalsOnMeasureResults) batch_size = executionContext->shots; else if (executionContext && executionContext->name == "msm") @@ -479,18 +480,18 @@ class StimCircuitSimulator : public nvqir::CircuitSimulatorBase { } /// @brief Sample the multi-qubit state. If \p qubits is empty and - /// explicitMeasurements is set, this returns all previously saved - /// measurements. + /// performing the explicit measurements sampling mode, this returns all + /// previously saved measurements. cudaq::ExecutionResult sample(const std::vector &qubits, const int shots) override { - if (executionContext->explicitMeasurements && qubits.empty() && - num_measurements == 0) + if (cudaq::details::isExplicitSamplingMode(executionContext) && + qubits.empty() && num_measurements == 0) throw std::runtime_error( "The sampling option `explicit_measurements` is not supported on a " "kernel without any measurement operation."); bool populateResult = [&]() { - if (executionContext->explicitMeasurements) + if (cudaq::details::isExplicitSamplingMode(executionContext)) return qubits.empty(); return true; }(); @@ -527,9 +528,10 @@ class StimCircuitSimulator : public nvqir::CircuitSimulatorBase { // measurements were mid-circuit measurements that have been previously // accounted for and saved. assert(bits_per_sample >= qubits.size()); - std::size_t first_bit_to_save = executionContext->explicitMeasurements - ? 0 - : bits_per_sample - qubits.size(); + std::size_t first_bit_to_save = + cudaq::details::isExplicitSamplingMode(executionContext) + ? 0 + : bits_per_sample - qubits.size(); CountsDictionary counts; sequentialData.reserve(shots); for (std::size_t shot = 0; shot < shots; shot++) {