diff --git a/.agent/plans/preserve-mapped-classical-control.md b/.agent/plans/preserve-mapped-classical-control.md new file mode 100644 index 0000000000..d1bebb610e --- /dev/null +++ b/.agent/plans/preserve-mapped-classical-control.md @@ -0,0 +1,63 @@ +# Preserve classical control during target mapping + +Status: complete. Routing, terminal-measurement, and performance regressions +pass. + +## Goal and scope + +Prevent routing SWAPs from creating a cyclic dependency through later classical +control. An independent wire can advance through a conditional whose measurement +depends on an unresolved two-qubit gate. Using that conditional's output as a +SWAP input can make the unresolved gate depend on its own result. + +The production change belongs in +`mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp`. The mapping regression +checks valid output, target connectivity, and preservation of conditional work. +Two sorter regressions cover repeated stores to one register element and a +whole-register write followed by an indexed load during dominance repair. + +## Decisions + +- Keep main's recursive memory-effect ordering and FIFO topological sorter from + #2436 unchanged. The sorter preserves mutable-register dependencies but cannot + repair a cycle introduced by routing. +- Do not require measurement/store adjacency in the mapper. Native Qiskit export + supports intervening quantum operations since #2439. +- Defer composites while an earlier wire operation still needs routing. Terminal + sinks and output-only measurements must not block independent quantum work. +- Use one measurement classification for advancement and composite deferral. + Follow SSA result uses and forward whole-register effects until reaching + quantum work. Register accesses before the measurement run do not require + measured-qubit reuse. Output-only loads and overwrites remain terminal; + register accesses inside quantum composites still impose ordering. +- Keep consecutive measurements in scope so an earlier measurement does not hide + a later result used for quantum control. Reuse LLVM slice analysis and a + bounded worklist. Traverse it by index because discovering more effects can + append entries and invalidate iterators. No public API or dependency changes + are required. +- Cache consecutive-measurement suffix classifications within each `advance` + invocation, sharing slice and register work across the run. Discard the cache + before hot routing mutates the graph. +- Backward traversal of an idle nested block argument can reach its sentinel; + that exhausted wire does not defer another composite. +- Select the wire value crossing the composite's block-order boundary when + extending it. Another wire may already have advanced through a gate that + consumes a classical result of that composite; its current value would + introduce an SSA cycle. +- Prefer terminal measurements even when the target accepts adaptive programs. + Target permission does not replace program dependencies or placement + readiness. + +## Validation + +Release unit tests pass: 100 mapping tests, 192 QCO utility tests, and 181 +compiler tests. The added cases cover idle nested wires, a conditional angle +consumed on another wire, terminal measurements after earlier register control, +and consecutive measurements with first/last result control and shared stores. + +The consecutive-measurement probe at 4,000 measurements improved from 327.85 ms +to 2.398 ms before the boundary and earlier-access fixes. This measures mapping +alone on a synthetic workload, not a whole-program or corpus speedup. + +Repository lint and full-file C++ lint against fixed base `ce608b082` pass. +These are local checks, not hosted CI or a full Benchpress corpus run. diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 013fe17ae5..5a07f6378e 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -12,6 +12,8 @@ #include "mlir/Compiler/Target.h" #include "mlir/Compiler/TargetEnvironment.h" +#include "mlir/Dialect/CBit/IR/CBitDialect.h" +#include "mlir/Dialect/CBit/IR/CBitOps.h" #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" @@ -25,12 +27,13 @@ #include "mlir/Dialect/QTensor/IR/QTensorOps.h" #include "mlir/Dialect/QTensor/Utils/TensorIterator.h" +#include #include #include #include +#include #include #include -#include #include #include #include @@ -47,6 +50,8 @@ #include #include #include +#include +#include #include #include @@ -777,33 +782,16 @@ struct MappingPass : impl::MappingPassBase { return newWhileOp; } - /// Return the value whose wire edge crosses a composite in block order. + /// Return the wire value before a composite, even if advancement passed it. static Value valueBeforeBoundary(WireIterator iterator, Operation* boundary) { - assert(boundary != nullptr && boundary->getBlock() != nullptr); - - // Independent wires can advance beyond `boundary`. Rewind to the qubit - // value that crosses it so extending the composite does not move later - // operations before the boundary. if (iterator == std::default_sentinel) { --iterator; } - while (iterator.operation() != nullptr && !iterator.operation()->isBeforeInBlock(boundary)) { - assert(iterator.operation()->getBlock() == boundary->getBlock()); --iterator; } - - Value value = iterator.qubit(); - assert(value && "expected a qubit value before the composite boundary"); - assert(value.hasOneUse() && "expected linear qubit use at boundary"); - Operation* consumer = boundary->getBlock()->findAncestorOpInBlock( - *value.use_begin()->getOwner()); - assert(consumer != nullptr && "expected consumer in boundary block"); - assert((consumer == boundary || boundary->isBeforeInBlock(consumer) || - isa(consumer)) && - "selected qubit value does not cross composite boundary"); - return value; + return iterator.qubit(); } /// Execute `ntrials` many (parallel) initial layout refinement trials and @@ -1179,17 +1167,148 @@ struct MappingPass : impl::MappingPassBase { stats.nswaps += swaps.size(); } - /// Advance past all executable gates and return operations with nested - /// regions and the respective wire indices. Stops when no more executable - /// gates are found. The function positions each wire on a non-executable - /// two-qubit gate or a composite unitary, if possible. The function never - /// advances past sink-like operation and thus, each wire will never reach the - /// sentinel state. + /// Classify a consecutive measurement run from its end, so each suffix is + /// visited once. The cache is valid only while the IR remains unchanged. + static bool measurementNeedsRouting(MeasureOp measurement, + DenseMap& cache) { + SmallVector measurements; + bool needsRouting = false; + WireIterator it(measurement.getQubitOut()); + for (; it != std::default_sentinel; ++it) { + Operation* op = it.operation(); + if (const auto cached = cache.find(op); cached != cache.end()) { + needsRouting = cached->second; + break; + } + if (auto next = dyn_cast(op)) { + measurements.push_back(next); + continue; + } + needsRouting = !isa(op); + break; + } + + Block* const block = measurement->getBlock(); + + const auto addSlice = [](Operation* root, SetVector& worklist) { + SetVector slice; + ForwardSliceOptions options; + options.inclusive = true; + options.filter = [&](Operation* op) { return !worklist.contains(op); }; + getForwardSlice(root, &slice, options); + worklist.insert(slice.begin(), slice.end()); + }; + + SetVector worklist; + + DenseSet> processed; + + size_t cursor = 0; + const auto resultNeedsRouting = [&](MeasureOp next) { + for_each(next.getResult().getUsers(), + [&](Operation* user) { addSlice(user, worklist); }); + for (; cursor < worklist.size(); ++cursor) { + Operation* op = worklist[cursor]; + + /// Quantum consumers require the measurement to advance, independent + /// of the selected target's permission to reuse measured qubits. + + if (any_of(op->getOperandTypes(), + [](auto type) { return isa(type); })) { + return true; + } + + // Captures and nested register accesses also constrain their enclosing + // composite, whose placement threads every physical wire through it. + + if (op->getBlock() != block) { + if (Operation* ancestor = block->findAncestorOpInBlock(*op); + ancestor != nullptr) { + addSlice(ancestor, worklist); + } + } + + const auto effects = getEffectsRecursively(op); + if (!effects) { + continue; + } + + for (const auto& effect : *effects) { + auto value = effect.getValue(); + auto reg = dyn_cast_if_present>(value); + if (!reg) { + continue; + } + + auto [it, inserted] = processed.insert(reg); + if (!inserted) { + continue; + } + + for (Operation* user : reg.getUsers()) { + Operation* ancestor = block->findAncestorOpInBlock(*user); + if (user == op || + (ancestor && !measurement->isBeforeInBlock(ancestor))) { + continue; + } + + addSlice(user, worklist); + } + } + } + + return false; + }; + + for (auto next : llvm::reverse(measurements)) { + needsRouting = needsRouting || resultNeedsRouting(next); + cache.try_emplace(next, needsRouting); + } + return needsRouting; + } + + /// Advance past executable gates and return ready composite operations. + /// Leave wires at non-executable gates, composites, terminal measurements, + /// or sink-like operations. Backward traversal can exhaust block arguments. template SmallVector advance(Wires& wires, const WireInfos& infos, const Layout& layout) { DenseSet visited; SmallVector composites; + /// Advancement only moves iterators. Discard classifications before routing + /// inserts SWAPs or replaces composites. + DenseMap measurementRouting; + + // The wire traversal does not follow classical dependencies. Defer a + // composite until earlier routing work is complete, but let independent + // composites pass terminal wires. Reverse block order for backward routing. + + const auto defer = [&wires, &measurementRouting](Operation* candidate) { + return any_of(wires, [&](WireIterator& it) { + if (it == std::default_sentinel) { + return false; + } + + Operation* op = it.operation(); + if (op == nullptr || op == candidate) { + return false; + } + + if (isa(op)) { + return false; + } + if constexpr (Direction == WireDirection::Forward) { + if (auto measurement = dyn_cast(op); + measurement && + !measurementNeedsRouting(measurement, measurementRouting)) { + return false; + } + return op->isBeforeInBlock(candidate); + } + + return candidate->isBeforeInBlock(op); + }); + }; // Advance wires past all executable gates and push composite unitaries // and the respective wire indices of their inputs onto the vector. @@ -1212,32 +1331,12 @@ struct MappingPass : impl::MappingPassBase { return target->areAdjacent(hw0, hw1); }) .Case([](ResetOp&) { return true; }) - .Case([](MeasureOp& m) { + .Case([&](MeasureOp& m) { if (Direction == WireDirection::Backward) { return true; } - /// Only advance past measurements in adaptive-profile - /// scenarios, where a qubit is used after measurement - /// (multiple subsequent measurements are fine) or a bit is - /// used to determine a subsequent chain of unitaries. - /// The forward slice follows SSA def-use chains only. - - Value qubit = m.getQubitOut(); - Value bit = m.getResult(); - - assert(qubit.hasOneUse()); - Operation* user = *qubit.user_begin(); - if (!isa(user)) { - return true; - } - - SetVector slice; - getForwardSlice(bit, &slice); - return any_of(slice, [](Operation* op) { - return isa(op); - }); + return measurementNeedsRouting(m, measurementRouting); }) .template Case( [](auto&) { return Direction == WireDirection::Forward; }) @@ -1245,11 +1344,8 @@ struct MappingPass : impl::MappingPassBase { scf::YieldOp, scf::ConditionOp>( [](auto&) { return Direction == WireDirection::Backward; }) .template Case( - [&](auto&) { - if (indices.size() == 1) { - return true; - } - if (visited.insert(op).second) { + [&](auto& cf) { + if (!defer(cf) && visited.insert(op).second) { composites.emplace_back(op, indices); } return false; @@ -1272,32 +1368,13 @@ struct MappingPass : impl::MappingPassBase { // become ready at once. Hot routing threads every qubit through each // composite, so processing a later operation first could introduce a // use-before-definition for an earlier operation. + llvm::sort(composites, [](const CompositeUnitary& lhs, const CompositeUnitary& rhs) { assert(lhs.op->getBlock() == rhs.op->getBlock()); return lhs.op->isBeforeInBlock(rhs.op); }); - // Defer a composite while another active wire points to an operation that - // precedes it in the traversal direction. Otherwise, dispatch would remove - // that operation from the routing frontier. - llvm::erase_if(composites, [&](const CompositeUnitary& composite) { - return llvm::any_of(wires, [&](const WireIterator& iterator) { - if (iterator == std::default_sentinel) { - return false; - } - Operation* operation = iterator.operation(); - if (operation == nullptr || operation == composite.op) { - return false; - } - assert(operation->getBlock() == composite.op->getBlock()); - if constexpr (Direction == WireDirection::Forward) { - return operation->isBeforeInBlock(composite.op); - } - return composite.op->isBeforeInBlock(operation); - }); - }); - return composites; } @@ -1657,7 +1734,6 @@ struct MappingPass : impl::MappingPassBase { auto& [wires, infos, layout] = bundle; Statistics stats; - while (true) { while (true) { auto composites = advance(wires, infos, layout); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index afed1e621e..3ad277a997 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -358,6 +358,61 @@ class MappingPassTest : public MappingPassFixture, }; // namespace +TEST_F(MappingPassFixture, RouteBeforeLaterClassicalControl) { + const auto target = llvm::cantFail(CompilerTarget::create( + 4, Connectivity::fromCouplings({{0, 1}, {0, 2}, {0, 3}}), + NativeOperations::unrestricted())); + + QCOProgramBuilder builder(context.get()); + builder.initialize(); + + auto q0 = builder.allocQubit(); + auto q1 = builder.allocQubit(); + auto q2 = builder.allocQubit(); + auto ancilla = builder.allocQubit(); + + std::tie(q0, ancilla) = builder.cx(q0, ancilla); + std::tie(q1, ancilla) = builder.cx(q1, ancilla); + std::tie(q0, q2) = builder.cx(q0, q2); + + Value condition; + std::tie(ancilla, condition) = builder.measure(ancilla); + q0 = builder.qcoIf(condition, q0, + [&](Value qubit) { return builder.x(qubit); }); + + builder.sink(q0); + builder.sink(q1); + builder.sink(q2); + builder.sink(ancilla); + + auto moduleOp = builder.finalize(); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(runPass(moduleOp.get(), target, + MappingPassOptions{.ntrials = 4, .seed = 42}) + .succeeded()); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + EXPECT_TRUE(isExecutable(getEntryPoint(moduleOp.get()), target)); + + IfOp conditional; + size_t numControlledGates = 0; + moduleOp->walk([&](IfOp candidate) { conditional = candidate; }); + ASSERT_TRUE(conditional); + + size_t numSwaps = 0; + size_t numSwapsBeforeControl = 0; + moduleOp->walk([&](SWAPOp swap) { + ++numSwaps; + if (swap->getBlock() == conditional->getBlock() && + swap->isBeforeInBlock(conditional)) { + ++numSwapsBeforeControl; + } + }); + conditional->walk([&](XOp) { ++numControlledGates; }); + EXPECT_GT(numSwaps, 0); + EXPECT_EQ(numSwapsBeforeControl, numSwaps); + EXPECT_EQ(numControlledGates, 1); +} + TEST_F(MappingPassFixture, StandalonePassesUseSharedAllocationVerifier) { const auto target = llvm::cantFail( CompilerTarget::create(1, Connectivity::fromCouplings({}), @@ -511,6 +566,306 @@ TEST_F(MappingPassFixture, EXPECT_TRUE(isa(*measurement.getQubitOut().getUsers().begin())); } +TEST_F(MappingPassFixture, RouteIndependentControlAfterTerminalWire) { + const auto target = llvm::cantFail(CompilerTarget::create( + 3, Connectivity::fromCouplings({{0, 1}, {1, 2}, {0, 2}}), + NativeOperations::unrestricted())); + + for (const bool measure : {false, true}) { + SCOPED_TRACE(measure); + QCOProgramBuilder builder(context.get()); + builder.initialize(); + auto q0 = builder.allocQubit(); + auto q1 = builder.allocQubit(); + auto q2 = builder.allocQubit(); + if (measure) { + q0 = builder.measure(q0).first; + } + builder.sink(q0); + q1 = builder.qcoIf(true, q1, [&](Value qubit) { return builder.x(qubit); }); + std::tie(q1, q2) = builder.cx(q1, q2); + builder.sink(q1); + builder.sink(q2); + auto moduleOp = builder.finalize(); + + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(runPass( + *moduleOp, target, MappingPassOptions{.ntrials = 1, .seed = 42}))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + EXPECT_TRUE(isExecutable(getEntryPoint(*moduleOp), target)); + } +} + +TEST_F(MappingPassFixture, RouteControlAfterConsecutiveMeasurements) { + const auto target = llvm::cantFail(CompilerTarget::create( + 3, Connectivity::fromCouplings({{0, 1}, {1, 2}, {0, 2}}), + NativeOperations::unrestricted())); + QCOProgramBuilder builder(context.get()); + builder.initialize(); + auto q0 = builder.allocQubit(); + auto q1 = builder.allocQubit(); + auto q2 = builder.allocQubit(); + q0 = builder.measure(q0).first; + Value bit; + std::tie(q0, bit) = builder.measure(q0); + builder.sink(q0); + q1 = builder.qcoIf(bit, q1, [&](Value qubit) { return builder.x(qubit); }); + std::tie(q1, q2) = builder.cx(q1, q2); + builder.sink(q1); + builder.sink(q2); + auto moduleOp = builder.finalize(); + + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(runPass(*moduleOp, target, + MappingPassOptions{.ntrials = 1, .seed = 42}))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + EXPECT_TRUE(isExecutable(getEntryPoint(*moduleOp), target)); +} + +TEST_F(MappingPassFixture, MapNestedControlWithIdleWire) { + const auto target = llvm::cantFail( + CompilerTarget::create(2, Connectivity::fromCouplings({{0, 1}}), + NativeOperations::unrestricted())); + auto moduleOp = parseSourceString(R"mlir( +module { + func.func @main(%c: i1, %innerCondition: i1) attributes {mqt.entry_point} { + %q0 = qco.alloc : !qco.qubit + %q1 = qco.alloc : !qco.qubit + %out0, %out1 = qco.if %c args(%a = %q0, %b = %q1) -> (!qco.qubit, !qco.qubit) { + %inner = qco.if %innerCondition args(%i = %a) -> (!qco.qubit) { + %x = qco.x %i : !qco.qubit -> !qco.qubit + qco.yield %x : !qco.qubit + } else args(%i = %a) { + qco.yield %i : !qco.qubit + } + qco.yield %inner, %b : !qco.qubit, !qco.qubit + } else args(%a = %q0, %b = %q1) { + qco.yield %a, %b : !qco.qubit, !qco.qubit + } + qco.sink %out0 : !qco.qubit + qco.sink %out1 : !qco.qubit + return + } +} + )mlir", + context.get()); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded( + runPass(*moduleOp, target, MappingPassOptions{.ntrials = 1, .seed = 0}))); + EXPECT_TRUE(succeeded(verify(*moduleOp))); + EXPECT_TRUE(isExecutable(getEntryPoint(*moduleOp), target)); +} + +TEST_F(MappingPassFixture, PreserveConditionalGateParameterDependency) { + const auto target = llvm::cantFail( + CompilerTarget::create(2, Connectivity::fromCouplings({{0, 1}}), + NativeOperations::unrestricted())); + auto moduleOp = parseSourceString(R"mlir( +module { + func.func @main(%condition: i1) -> (i1, i1) attributes {mqt.entry_point} { + %q0 = qco.alloc : !qco.qubit + %q1 = qco.alloc : !qco.qubit + %angle, %out = qco.if %condition args(%a = %q0) -> (f64, !qco.qubit) { + %v = arith.constant 1.0 : f64 + %x = qco.x %a : !qco.qubit -> !qco.qubit + qco.yield %v, %x : f64, !qco.qubit + } else args(%a = %q0) { + %v = arith.constant 2.0 : f64 + %h = qco.h %a : !qco.qubit -> !qco.qubit + qco.yield %v, %h : f64, !qco.qubit + } + %rotated = qco.rx(%angle) %q1 : !qco.qubit -> !qco.qubit + %done0, %bit0 = qco.measure %out : !qco.qubit + %done1, %bit1 = qco.measure %rotated : !qco.qubit + qco.sink %done0 : !qco.qubit + qco.sink %done1 : !qco.qubit + return %bit0, %bit1 : i1, i1 + } +} + )mlir", + context.get()); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded( + runPass(*moduleOp, target, MappingPassOptions{.ntrials = 1, .seed = 0}))); + EXPECT_TRUE(succeeded(verify(*moduleOp))); + EXPECT_TRUE(isExecutable(getEntryPoint(*moduleOp), target)); +} + +TEST_F(MappingPassFixture, + KeepMeasurementsTerminalAfterEarlierRegisterControl) { + const auto target = llvm::cantFail( + CompilerTarget::create(3, Connectivity::fromCouplings({{0, 1}, {1, 2}}), + NativeOperations::unrestricted())); + QCOProgramBuilder builder(context.get()); + builder.initialize({cbit::RegisterType::get(context.get(), 1)}); + auto reg = builder.allocClassicalBitRegister(1); + Value index = arith::ConstantIndexOp::create(builder, 0); + auto q0 = builder.allocQubit(); + auto q1 = builder.allocQubit(); + auto q2 = builder.allocQubit(); + auto oldBit = builder.loadClassicalBit(reg, index); + q0 = builder.qcoIf(oldBit, q0, [&](Value q) { return builder.x(q); }); + std::tie(q0, q1) = builder.cx(q0, q1); + std::tie(q0, q2) = builder.cx(q0, q2); + Value bit; + std::tie(q0, bit) = builder.measure(q0); + builder.storeClassicalBit(bit, reg, index); + builder.sink(q0); + std::tie(q1, q2) = builder.cx(q1, q2); + std::tie(q1, bit) = builder.measure(q1); + builder.storeClassicalBit(bit, reg, index); + builder.sink(q1); + builder.sink(q2); + auto moduleOp = builder.finalize(reg); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded( + runPass(*moduleOp, target, MappingPassOptions{.ntrials = 1, .seed = 0}))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + moduleOp->walk([](MeasureOp measurement) { + EXPECT_TRUE(( + isa(*measurement.getQubitOut().getUsers().begin()))); + }); +} + +TEST_F(MappingPassFixture, RouteControlFromConsecutiveMeasurementResults) { + const auto target = llvm::cantFail( + CompilerTarget::create(2, Connectivity::fromCouplings({{0, 1}}), + NativeOperations::unrestricted())); + constexpr size_t numMeasurements = 128; + for (const bool storeResults : {false, true}) { + SCOPED_TRACE(storeResults); + for (const size_t controlIndex : {size_t{0}, numMeasurements - 1}) { + SCOPED_TRACE(controlIndex); + QCOProgramBuilder builder(context.get()); + builder.initialize({cbit::RegisterType::get(context.get(), 1)}); + auto reg = builder.allocClassicalBitRegister(1); + Value index = arith::ConstantIndexOp::create(builder, 0); + auto q0 = builder.allocQubit(); + auto q1 = builder.allocQubit(); + Value condition; + for (size_t i = 0; i < numMeasurements; ++i) { + Value bit; + std::tie(q0, bit) = builder.measure(q0); + if (storeResults) { + builder.storeClassicalBit(bit, reg, index); + } + if (i == controlIndex) { + condition = bit; + } + } + q1 = builder.qcoIf(condition, q1, + [&](Value qubit) { return builder.x(qubit); }); + builder.sink(q0); + builder.sink(q1); + auto moduleOp = builder.finalize(reg); + + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(runPass( + *moduleOp, target, MappingPassOptions{.ntrials = 1, .seed = 0}))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + auto entry = getEntryPoint(*moduleOp); + EXPECT_TRUE(isExecutable(entry, target)); + auto measurements = llvm::to_vector(entry.getOps()); + ASSERT_EQ(measurements.size(), numMeasurements); + auto conditional = *entry.getOps().begin(); + EXPECT_EQ(conditional.getCondition(), condition); + if (controlIndex == 0) { + /// A dependency of the first result must not make later measurements + /// nonterminal. + EXPECT_TRUE( + isa(*measurements.back().getQubitOut().getUsers().begin())); + } + } + } +} + +TEST_F(MappingPassFixture, KeepMeasurementStoreBeforeConditionalOverwrite) { + const auto target = llvm::cantFail(CompilerTarget::create( + 3, Connectivity::fromCouplings({{0, 1}, {1, 2}, {0, 2}}), + NativeOperations::unrestricted())); + QCOProgramBuilder builder(context.get()); + builder.initialize(); + auto reg = builder.allocClassicalBitRegister(1); + Value index = arith::ConstantIndexOp::create(builder, 0); + auto q0 = builder.allocQubit(); + auto q1 = builder.allocQubit(); + auto q2 = builder.allocQubit(); + Value bit; + std::tie(q0, bit) = builder.measure(q0); + builder.storeClassicalBit(bit, reg, index); + builder.sink(q0); + q1 = builder.qcoIf(true, q1, [&](Value qubit) { + Value zero = arith::ConstantIntOp::create(builder, 0, 1); + builder.storeClassicalBit(zero, reg, index); + return builder.x(qubit); + }); + std::tie(q1, q2) = builder.cx(q1, q2); + builder.sink(q1); + builder.sink(q2); + auto moduleOp = builder.finalize(); + + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(runPass(*moduleOp, target, + MappingPassOptions{.ntrials = 1, .seed = 42}))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + auto entry = getEntryPoint(*moduleOp); + EXPECT_TRUE(isExecutable(entry, target)); + auto store = *entry.getOps().begin(); + auto conditional = *entry.getOps().begin(); + EXPECT_TRUE(store->isBeforeInBlock(conditional)); +} + +TEST_F(MappingPassFixture, KeepOutputOnlyRegisterMeasurementsTerminal) { + const auto target = llvm::cantFail( + CompilerTarget::create(3, Connectivity::fromCouplings({{0, 1}, {1, 2}}), + NativeOperations::unrestricted())); + + for (const bool returnRead : {false, true}) { + SCOPED_TRACE(returnRead); + QCOProgramBuilder builder(context.get()); + Type resultType = builder.getI1Type(); + if (!returnRead) { + resultType = cbit::RegisterType::get(context.get(), 1); + } + builder.initialize({resultType}); + auto reg = builder.allocClassicalBitRegister(1); + Value index = arith::ConstantIndexOp::create(builder, 0); + auto q0 = builder.allocQubit(); + auto q1 = builder.allocQubit(); + auto q2 = builder.allocQubit(); + std::tie(q0, q1) = builder.cx(q0, q1); + std::tie(q0, q2) = builder.cx(q0, q2); + Value bit; + std::tie(q0, bit) = builder.measure(q0); + builder.storeClassicalBit(bit, reg, index); + builder.sink(q0); + std::tie(q1, q2) = builder.cx(q1, q2); + std::tie(q1, bit) = builder.measure(q1); + builder.storeClassicalBit(bit, reg, index); + builder.sink(q1); + builder.sink(q2); + Value output = reg; + if (returnRead) { + output = cbit::ReadOp::create(builder, builder.getI1Type(), reg); + } + auto moduleOp = builder.finalize(output); + + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(runPass( + *moduleOp, target, MappingPassOptions{.ntrials = 1, .seed = 0}))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + EXPECT_TRUE(isExecutable(getEntryPoint(*moduleOp), target)); + moduleOp->walk([](MeasureOp measurement) { + ASSERT_TRUE(measurement.getQubitOut().hasOneUse()); + // Output-only measurements must remain valid for the QIR base profile. + EXPECT_TRUE((isa( + *measurement.getQubitOut().getUsers().begin()))); + }); + } +} + TEST_F(MappingPassFixture, PreserveNoncontiguousTargetSiteIds) { constexpr int64_t size = 3; @@ -727,14 +1082,17 @@ TEST_F(MappingPassFixture, KeepWorkspaceSparseOnLargeTarget) { TEST_F(MappingPassFixture, PreserveStoredRegisterControlDuringRouting) { constexpr StringLiteral source = R"mlir( module { - func.func @main() attributes {mqt.entry_point} { + func.func @main() -> !cbit.reg<1> attributes {mqt.entry_point} { %c0 = arith.constant 0 : index %reg = cbit.alloc(#cbit.init) : !cbit.reg<1> + %extra = cbit.alloc(#cbit.init) : !cbit.reg<1> %q0 = qco.alloc : !qco.qubit %q1 = qco.alloc : !qco.qubit %q2 = qco.alloc : !qco.qubit %measured, %bit = qco.measure %q0 : !qco.qubit cbit.store %bit, %reg[%c0] : !cbit.reg<1> + cbit.store %bit, %extra[%c0] : !cbit.reg<1> + qco.sink %measured : !qco.qubit %one = arith.constant 1 : i64 %two = arith.addi %one, %one : i64 %snapshot = cbit.read %reg : !cbit.reg<1> -> i1 @@ -755,10 +1113,9 @@ TEST_F(MappingPassFixture, PreserveStoredRegisterControlDuringRouting) { } %next1, %next2 = qco.swap %controlled1, %q2 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit - qco.sink %measured : !qco.qubit qco.sink %next1 : !qco.qubit qco.sink %next2 : !qco.qubit - return + return %extra : !cbit.reg<1> } } )mlir"; diff --git a/mlir/unittests/Dialect/QCO/Utils/test_sorting.cpp b/mlir/unittests/Dialect/QCO/Utils/test_sorting.cpp index 9a3f1241d5..95d55c3bd5 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_sorting.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_sorting.cpp @@ -143,3 +143,56 @@ TEST_F(TopologicalSortingTest, PreservesReadyOperationDiscoveryOrder) { EXPECT_TRUE(mul->isBeforeInBlock(adds[1])); EXPECT_TRUE(adds[1]->isBeforeInBlock(adds[2])); } + +TEST_F(TopologicalSortingTest, PreservesRepeatedStoresToSameRegisterElement) { + auto moduleOp = parse(R"mlir( + func.func @test(%q0: !qco.qubit, %q1: !qco.qubit) + -> (!qco.qubit, !qco.qubit) { + %index = arith.constant 0 : index + %reg = cbit.alloc(#cbit.init) : !cbit.reg<1> + %flipped = qco.x %q0 : !qco.qubit -> !qco.qubit + %out0, %bit0 = qco.measure %flipped : !qco.qubit + cbit.store %bit0, %reg[%index] : !cbit.reg<1> + %out1, %bit1 = qco.measure %q1 : !qco.qubit + cbit.store %bit1, %reg[%index] : !cbit.reg<1> + return %out0, %out1 : !qco.qubit, !qco.qubit + } + )mlir"); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + auto function = *moduleOp->getOps().begin(); + auto flipped = *function.getOps().begin(); + auto stores = llvm::to_vector(function.getOps()); + ASSERT_EQ(stores.size(), 2U); + flipped->moveAfter(stores.back()); + + sort(*moduleOp); + + ASSERT_TRUE(succeeded(verify(*moduleOp))); + EXPECT_TRUE(stores.front()->isBeforeInBlock(stores.back())); +} + +TEST_F(TopologicalSortingTest, + KeepsRegisterWriteBeforeIndexedLoadDuringRepair) { + auto moduleOp = parse(R"mlir( + func.func @test() -> i1 { + %index = arith.constant 0 : index + %value = arith.constant 0 : i2 + %reg = cbit.alloc(#cbit.init) : !cbit.reg<2> + cbit.write %value, %reg : i2, !cbit.reg<2> + %loaded = cbit.load %reg[%index] : !cbit.reg<2> + return %loaded : i1 + } + )mlir"); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + auto function = *moduleOp->getOps().begin(); + auto write = *function.getOps().begin(); + auto load = *function.getOps().begin(); + write.getValue().getDefiningOp()->moveAfter(load); + + sort(*moduleOp); + + ASSERT_TRUE(succeeded(verify(*moduleOp))); + EXPECT_TRUE(write->isBeforeInBlock(load)); +}