diff --git a/include/mqt-core/qir/runtime/QIR.h b/include/mqt-core/qir/runtime/QIR.h index fd1463a0f8..73d0b913ee 100644 --- a/include/mqt-core/qir/runtime/QIR.h +++ b/include/mqt-core/qir/runtime/QIR.h @@ -167,6 +167,31 @@ bool __quantum__rt__read_result(Result*); /// the label is included in the output or omitted. void __quantum__rt__result_record_output(Result*, const char*); +/// Adds a boolean value to the generated output. The second parameter defines +/// a string label for the value. Depending on the output schema, the label is +/// included in the output or omitted. +void __quantum__rt__bool_record_output(bool, const char*); + +/// Adds an integer value to the generated output. The second parameter defines +/// a string label for the value. Depending on the output schema, the label is +/// included in the output or omitted. +void __quantum__rt__int_record_output(int64_t, const char*); + +/// Adds a floating-point value to the generated output. The second parameter +/// defines a string label for the value. Depending on the output schema, the +/// label is included in the output or omitted. +void __quantum__rt__float_record_output(double, const char*); + +/// Inserts a marker in the generated output indicating that the next +/// `elementCount` recorded values form the contents of a tuple. The second +/// parameter defines a string label for the tuple. +void __quantum__rt__tuple_record_output(int64_t elementCount, const char*); + +/// Inserts a marker in the generated output indicating that the next `size` +/// recorded values form the contents of an array. The second parameter defines +/// a string label for the array. +void __quantum__rt__array_record_output(int64_t size, const char*); + // NOLINTEND(readability-identifier-naming) // NOLINTEND(modernize-deprecated-headers) // NOLINTEND(modernize-use-using) diff --git a/include/mqt-core/qir/runtime/Runtime.hpp b/include/mqt-core/qir/runtime/Runtime.hpp index 8459225d90..68475453fb 100644 --- a/include/mqt-core/qir/runtime/Runtime.hpp +++ b/include/mqt-core/qir/runtime/Runtime.hpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -237,7 +238,7 @@ class Runtime { std::vector qubitPermutation; static constexpr uintptr_t MIN_DYN_RESULT_ADDRESS = 0x10000; std::unordered_map rRegister; - std::string recordedOutputs; + std::string measurements; uintptr_t currentMaxQubitAddress; qc::Qubit currentMaxQubitId; uintptr_t currentMaxResultAddress; @@ -391,13 +392,17 @@ class Runtime { auto rFree(Result* result) -> void; auto equal(Result* result1, Result* result2) -> bool; - /// Append the value referenced by `result` to the recorded outputs bit - /// string in record order. - auto recordOutput(Result* result) -> void; + /// Append a measurement bit to the measurement string. + auto appendMeasurementBit(bool result) -> void; - /// @returns the outputs declared by the program as a bit string in record - /// order. - auto getRecordedOutputs() const -> const std::string&; + /// @returns the accumulated measurement string. + auto getMeasurements() const -> const std::string&; + + /// Emit `label:\n` to the output stream. + auto outputContainer(const char* label, int64_t elementCount) const -> void; + + /// Emit `label: valueStr\n` to the output stream. + auto outputValue(const char* label, std::string_view valueStr) const -> void; /// Move the quantum state out of the runtime. /// Then reset the runtime to a clean state ready for the next job. @@ -406,7 +411,7 @@ class Runtime { /// @returns the moved @c QState from the runtime. auto takeState() -> QState; - auto getOstream() -> std::ostream&; + auto getOstream() const -> std::ostream&; auto setOstream(std::ostream& other) -> void; auto resetOstream() -> void; }; diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index d20e0202d4..d30de4973e 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -490,7 +490,7 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgramSampling() llvm::formatv("QIR program failed with error: {}", rc)); } // Update the measurement counts. - ++counts_[runtime.getRecordedOutputs()]; + ++counts_[runtime.getMeasurements()]; } }); } diff --git a/src/qir/jit/Session.cpp b/src/qir/jit/Session.cpp index 11c86ee446..fe325d84f4 100644 --- a/src/qir/jit/Session.cpp +++ b/src/qir/jit/Session.cpp @@ -208,6 +208,11 @@ void JitSession::registerRuntimeSymbols() { REGISTER_SYMBOL(__quantum__rt__initialize); REGISTER_SYMBOL(__quantum__rt__read_result); REGISTER_SYMBOL(__quantum__rt__result_record_output); + REGISTER_SYMBOL(__quantum__rt__bool_record_output); + REGISTER_SYMBOL(__quantum__rt__int_record_output); + REGISTER_SYMBOL(__quantum__rt__float_record_output); + REGISTER_SYMBOL(__quantum__rt__tuple_record_output); + REGISTER_SYMBOL(__quantum__rt__array_record_output); }); } diff --git a/src/qir/runtime/QIR.cpp b/src/qir/runtime/QIR.cpp index a52c5c1cb5..8b2fbb0642 100644 --- a/src/qir/runtime/QIR.cpp +++ b/src/qir/runtime/QIR.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include extern "C" { @@ -381,10 +383,34 @@ bool __quantum__rt__read_result(Result* result) { } void __quantum__rt__result_record_output(Result* result, const char* label) { + const bool bit = __quantum__rt__read_result(result); auto& runtime = qir::Runtime::getInstance(); - runtime.recordOutput(result); - runtime.getOstream() << label << ": " - << (__quantum__rt__read_result(result) ? 1 : 0) << "\n"; + runtime.outputValue(label, bit ? "1" : "0"); + // Accumulate new measurement bit. + runtime.appendMeasurementBit(bit); +} + +void __quantum__rt__bool_record_output(bool value, const char* label) { + qir::Runtime::getInstance().outputValue(label, value ? "1" : "0"); +} + +void __quantum__rt__int_record_output(int64_t value, const char* label) { + qir::Runtime::getInstance().outputValue(label, std::to_string(value)); +} + +void __quantum__rt__float_record_output(double value, const char* label) { + std::ostringstream oss; + oss << value; + qir::Runtime::getInstance().outputValue(label, oss.str()); +} + +void __quantum__rt__tuple_record_output(int64_t elementCount, + const char* label) { + qir::Runtime::getInstance().outputContainer(label, elementCount); +} + +void __quantum__rt__array_record_output(int64_t size, const char* label) { + qir::Runtime::getInstance().outputContainer(label, size); } } // extern "C" diff --git a/src/qir/runtime/Runtime.cpp b/src/qir/runtime/Runtime.cpp index 16f53c1d88..9e6f0a3cc4 100644 --- a/src/qir/runtime/Runtime.cpp +++ b/src/qir/runtime/Runtime.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -52,13 +53,14 @@ auto Runtime::reset() -> void { addressMode = AddressMode::UNKNOWN; qRegister.clear(); rRegister.clear(); + measurements.clear(); // NOLINTBEGIN(performance-no-int-to-ptr) rRegister.emplace(reinterpret_cast(RESULT_ZERO_ADDRESS), ResultStruct{.refcount = 0, .r = false}); rRegister.emplace(reinterpret_cast(RESULT_ONE_ADDRESS), ResultStruct{.refcount = 0, .r = true}); // NOLINTEND(performance-no-int-to-ptr) - recordedOutputs.clear(); + measurements.clear(); currentMaxQubitAddress = MIN_DYN_QUBIT_ADDRESS; currentMaxQubitId = 0; currentMaxResultAddress = MIN_DYN_RESULT_ADDRESS; @@ -164,12 +166,12 @@ auto Runtime::equal(Result* result1, Result* result2) -> bool { return deref(result1).r == deref(result2).r; } -auto Runtime::recordOutput(Result* result) -> void { - recordedOutputs.push_back(deref(result).r ? '1' : '0'); +auto Runtime::appendMeasurementBit(bool result) -> void { + measurements.push_back(result ? '1' : '0'); } -auto Runtime::getRecordedOutputs() const -> const std::string& { - return recordedOutputs; +auto Runtime::getMeasurements() const -> const std::string& { + return measurements; } auto Runtime::takeState() -> QState { @@ -178,7 +180,17 @@ auto Runtime::takeState() -> QState { return ret; } -auto Runtime::getOstream() -> std::ostream& { return *os; } +auto Runtime::outputContainer(const char* label, + int64_t /* elementCount */) const -> void { + *os << (label != nullptr ? label : "") << ":\n"; +} + +auto Runtime::outputValue(const char* label, std::string_view valueStr) const + -> void { + *os << (label != nullptr ? label : "") << ": " << valueStr << "\n"; +} + +auto Runtime::getOstream() const -> std::ostream& { return *os; } auto Runtime::setOstream(std::ostream& other) -> void { os = &other; } diff --git a/test/circuits/AdaptiveRecordOutputs.ll b/test/circuits/AdaptiveRecordOutputs.ll new file mode 100644 index 0000000000..ce3fc02aa5 --- /dev/null +++ b/test/circuits/AdaptiveRecordOutputs.ll @@ -0,0 +1,102 @@ +; ModuleID = 'Adaptive module implementing a 3-qubit Hamming weight' +source_filename = "AdaptiveRecordOutputs.ll" + +%Qubit = type opaque +%Result = type opaque + +@r0_lbl = internal constant [3 x i8] c"r0\00" +@r1_lbl = internal constant [3 x i8] c"r1\00" +@r2_lbl = internal constant [3 x i8] c"r2\00" +@outputs_lbl = internal constant [8 x i8] c"outputs\00" +@measurements_lbl = internal constant [15 x i8] c" measurements\00" +@m0_lbl = internal constant [7 x i8] c" m0\00" +@m1_lbl = internal constant [7 x i8] c" m1\00" +@m2_lbl = internal constant [7 x i8] c" m2\00" +@weight_lbl = internal constant [17 x i8] c" hamming_weight\00" +@mean_lbl = internal constant [7 x i8] c" mean\00" + +define i32 @main() #0 { +entry: + call void @__quantum__rt__initialize(i8* null) + %q0 = call %Qubit* @__quantum__rt__qubit_allocate() + %q1 = call %Qubit* @__quantum__rt__qubit_allocate() + %q2 = call %Qubit* @__quantum__rt__qubit_allocate() + call void @__quantum__qis__h__body(%Qubit* %q0) + call void @__quantum__qis__h__body(%Qubit* %q1) + call void @__quantum__qis__h__body(%Qubit* %q2) + %r0 = call %Result* @__quantum__qis__m__body(%Qubit* %q0) + %r1 = call %Result* @__quantum__qis__m__body(%Qubit* %q1) + %r2 = call %Result* @__quantum__qis__m__body(%Qubit* %q2) + %b0 = call i1 @__quantum__rt__read_result(%Result* %r0) + %b1 = call i1 @__quantum__rt__read_result(%Result* %r1) + %b2 = call i1 @__quantum__rt__read_result(%Result* %r2) + + ; Classical compute: Hamming weight and its mean. + %c0 = zext i1 %b0 to i64 + %c1 = zext i1 %b1 to i64 + %c2 = zext i1 %b2 to i64 + %sum01 = add i64 %c0, %c1 + %weight = add i64 %sum01, %c2 + %weight_f = sitofp i64 %weight to double + %num_qubits_f = uitofp i64 3 to double + %mean_f = fdiv double %weight_f, %num_qubits_f + + call void @__quantum__rt__qubit_release(%Qubit* %q0) + call void @__quantum__rt__qubit_release(%Qubit* %q1) + call void @__quantum__rt__qubit_release(%Qubit* %q2) + + ; Record the raw measurement bits (these feed the histogram bucketing key). + call void @__quantum__rt__result_record_output(%Result* %r0, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @r0_lbl, i32 0, i32 0)) + call void @__quantum__rt__result_record_output(%Result* %r1, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @r1_lbl, i32 0, i32 0)) + call void @__quantum__rt__result_record_output(%Result* %r2, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @r2_lbl, i32 0, i32 0)) + + ; Output: tuple of 3 elements (array of 3 bools, int count, float mean). + call void @__quantum__rt__tuple_record_output(i64 3, i8* getelementptr inbounds ([8 x i8], [8 x i8]* @outputs_lbl, i32 0, i32 0)) + call void @__quantum__rt__array_record_output(i64 3, i8* getelementptr inbounds ([13 x i8], [13 x i8]* @measurements_lbl, i32 0, i32 0)) + call void @__quantum__rt__bool_record_output(i1 %b0, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @m0_lbl, i32 0, i32 0)) + call void @__quantum__rt__bool_record_output(i1 %b1, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @m1_lbl, i32 0, i32 0)) + call void @__quantum__rt__bool_record_output(i1 %b2, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @m2_lbl, i32 0, i32 0)) + call void @__quantum__rt__int_record_output(i64 %weight, i8* getelementptr inbounds ([15 x i8], [15 x i8]* @weight_lbl, i32 0, i32 0)) + call void @__quantum__rt__float_record_output(double %mean_f, i8* getelementptr inbounds ([5 x i8], [5 x i8]* @mean_lbl, i32 0, i32 0)) + + call void @__quantum__rt__result_update_reference_count(%Result* %r0, i32 -1) + call void @__quantum__rt__result_update_reference_count(%Result* %r1, i32 -1) + call void @__quantum__rt__result_update_reference_count(%Result* %r2, i32 -1) + ret i32 0 +} + +declare void @__quantum__qis__h__body(%Qubit*) + +declare %Result* @__quantum__qis__m__body(%Qubit*) #1 + +declare i1 @__quantum__rt__read_result(%Result*) + +declare void @__quantum__rt__initialize(i8*) + +declare %Qubit* @__quantum__rt__qubit_allocate() + +declare void @__quantum__rt__qubit_release(%Qubit*) + +declare void @__quantum__rt__result_record_output(%Result*, i8*) + +declare void @__quantum__rt__tuple_record_output(i64, i8*) + +declare void @__quantum__rt__array_record_output(i64, i8*) + +declare void @__quantum__rt__bool_record_output(i1, i8*) + +declare void @__quantum__rt__int_record_output(i64, i8*) + +declare void @__quantum__rt__float_record_output(double, i8*) + +declare void @__quantum__rt__result_update_reference_count(%Result*, i32) + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="3" "required_num_results"="3" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/test/qdmi/devices/dd/results_sampling_test.cpp b/test/qdmi/devices/dd/results_sampling_test.cpp index 622e14b83b..ff8905ff57 100644 --- a/test/qdmi/devices/dd/results_sampling_test.cpp +++ b/test/qdmi/devices/dd/results_sampling_test.cpp @@ -52,14 +52,15 @@ class HistogramTest : public ::testing::Test { #endif using Histogram = std::pair, std::vector>; - static constexpr size_t SHOTS = 1024; + static constexpr size_t NUM_SHOTS = 1024; + static constexpr size_t NUM_QUBITS = 3; static Histogram runProgram(const QDMI_Program_Format format, const std::string_view program) { const qdmi_test::SessionGuard s{}; const qdmi_test::JobGuard j{s.session}; EXPECT_EQ(qdmi_test::setProgram(j.job, format, program), QDMI_SUCCESS); - EXPECT_EQ(qdmi_test::setShots(j.job, SHOTS), QDMI_SUCCESS); + EXPECT_EQ(qdmi_test::setShots(j.job, NUM_SHOTS), QDMI_SUCCESS); EXPECT_EQ(qdmi_test::submitAndWait(j.job, 0), QDMI_SUCCESS); return qdmi_test::getHistogram(j.job); } @@ -71,13 +72,30 @@ class HistogramTest : public ::testing::Test { ASSERT_EQ(keys.size(), vals.size()); // Values should sum up to the number of SHOTS. const auto sum = std::accumulate(vals.cbegin(), vals.cend(), size_t{0}); - EXPECT_EQ(sum, SHOTS); + EXPECT_EQ(sum, NUM_SHOTS); // Both keys '00' and '11' should be expected. ASSERT_EQ(keys.size(), 2U); // And no other keys should be expected. EXPECT_TRUE(std::ranges::all_of( keys, [](const auto& k) { return k == "00" || k == "11"; })); } + + /// Smoke check: used for circuits whose distribution we don't pin down (e.g. + /// multi-output adaptive programs). + static void checkSmokeHistogram(const Histogram& hist) { + const auto& [keys, vals] = hist; + // Both vectors have the same size. + ASSERT_EQ(keys.size(), vals.size()); + // Values sum up to the number of SHOTS. + const auto sum = std::accumulate(vals.cbegin(), vals.cend(), size_t{0}); + EXPECT_EQ(sum, NUM_SHOTS); + // Every key is a NUM_QUBITS-character bit string. + EXPECT_TRUE(std::ranges::all_of(keys, [](const auto& k) { + return k.size() == NUM_QUBITS && std::ranges::all_of(k, [](char c) { + return c == '0' || c == '1'; + }); + })); + } }; #ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR @@ -144,6 +162,18 @@ TEST_F(QIRHistogramTestString, Adaptive) { checkHistogram( runProgram(format, qir_test::getProgram("BellPairAdaptive.ll"))); } + +TEST_F(QIRHistogramTestModule, AdaptiveRecordOutputs) { + constexpr auto format = QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE; + checkSmokeHistogram( + runProgram(format, getProgram("AdaptiveRecordOutputs.ll"))); +} + +TEST_F(QIRHistogramTestString, AdaptiveRecordOutputs) { + constexpr auto format = QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING; + checkSmokeHistogram( + runProgram(format, qir_test::getProgram("AdaptiveRecordOutputs.ll"))); +} #endif TEST(ResultsSampling, BufferTooSmallErrors) { diff --git a/test/qir/jit/test_jit_session.cpp b/test/qir/jit/test_jit_session.cpp index 5f1ff5feaf..3c614dd18f 100644 --- a/test/qir/jit/test_jit_session.cpp +++ b/test/qir/jit/test_jit_session.cpp @@ -37,7 +37,7 @@ TEST_F(JitSessionTest, LoadModuleFromMemory) { const auto program = qir_test::getProgram("BellPairStatic.ll"); const qir::JitSession session(program, "BellPairStatic.ll"); ASSERT_EQ(session.run(), 0); - EXPECT_FALSE(qir::Runtime::getInstance().getRecordedOutputs().empty()); + EXPECT_FALSE(qir::Runtime::getInstance().getMeasurements().empty()); } TEST_F(JitSessionTest, SamplingRecordsOutputs) { @@ -45,14 +45,14 @@ TEST_F(JitSessionTest, SamplingRecordsOutputs) { // qir::Execution::Sampling is the default Execution mode const qir::JitSession session(path.string()); ASSERT_EQ(session.run(), 0); - EXPECT_FALSE(qir::Runtime::getInstance().getRecordedOutputs().empty()); + EXPECT_FALSE(qir::Runtime::getInstance().getMeasurements().empty()); } TEST_F(JitSessionTest, StateExtractionLeavesNoRecordedOutputs) { const auto path = std::filesystem::path(QIR_FILES_DIR) / "BellPairStatic.ll"; const qir::JitSession session(path.string(), qir::Execution::StateExtraction); ASSERT_EQ(session.run(), 0); - EXPECT_TRUE(qir::Runtime::getInstance().getRecordedOutputs().empty()); + EXPECT_TRUE(qir::Runtime::getInstance().getMeasurements().empty()); } TEST(JitSessionErrors, MalformedIRThrows) { diff --git a/test/qir/runtime/test_qir_runtime.cpp b/test/qir/runtime/test_qir_runtime.cpp index 7486517916..56655b1755 100644 --- a/test/qir/runtime/test_qir_runtime.cpp +++ b/test/qir/runtime/test_qir_runtime.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -527,6 +528,53 @@ TEST_F(QIRRuntimeTest, TakeStateReturnsStateAndResetsRuntime) { EXPECT_NO_THROW(__quantum__qis__h__body(q0)); } +TEST_F(QIRRuntimeTest, AdaptiveRecordOutputs) { + __quantum__rt__initialize(nullptr); + auto* q0 = __quantum__rt__qubit_allocate(); + auto* q1 = __quantum__rt__qubit_allocate(); + auto* q2 = __quantum__rt__qubit_allocate(); + __quantum__qis__h__body(q0); + __quantum__qis__h__body(q1); + __quantum__qis__h__body(q2); + auto* r0 = __quantum__qis__m__body(q0); + auto* r1 = __quantum__qis__m__body(q1); + auto* r2 = __quantum__qis__m__body(q2); + const auto b0 = __quantum__rt__read_result(r0); + const auto b1 = __quantum__rt__read_result(r1); + const auto b2 = __quantum__rt__read_result(r2); + __quantum__rt__qubit_release(q0); + __quantum__rt__qubit_release(q1); + __quantum__rt__qubit_release(q2); + + // Classical compute: Hamming weight and its mean. + const int64_t weight = + static_cast(b0) + static_cast(b1) + static_cast(b2); + const double mean = static_cast(weight) / 3.0; + + // Output: tuple of 3 elements (array of 3 bools, int weight, float mean). + __quantum__rt__tuple_record_output(3, "outputs"); + __quantum__rt__array_record_output(3, " measurements"); + __quantum__rt__bool_record_output(b0, " m0"); + __quantum__rt__bool_record_output(b1, " m1"); + __quantum__rt__bool_record_output(b2, " m2"); + __quantum__rt__int_record_output(weight, " hamming_weight"); + __quantum__rt__float_record_output(mean, " mean"); + + std::ostringstream expected; + expected << "outputs:\n" + << " measurements:\n" + << " m0: " << b0 << "\n" + << " m1: " << b1 << "\n" + << " m2: " << b2 << "\n" + << " hamming_weight: " << weight << "\n" + << " mean: " << mean << "\n"; + EXPECT_EQ(sink.str(), expected.str()); + + __quantum__rt__result_update_reference_count(r0, -1); + __quantum__rt__result_update_reference_count(r1, -1); + __quantum__rt__result_update_reference_count(r2, -1); +} + namespace { class QIRFilesTest : public ::testing::TestWithParam {};