diff --git a/CHANGELOG.md b/CHANGELOG.md index b152d08760..ea91c3ce55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel - ✨ Add Sampler and Estimator Primitives to the QDMI-Qiskit Interface ([#1507]) ([**@marcelwa**]) - ✨ Add conversions between Jeff and QCO ([#1479], [#1548], [#1565]) ([**@denialhaag**]) -- ✨ Add a `place-and-route` pass for mapping circuits to architectures with restricted topologies ([#1537], [#1547], [#1568], [#1581]) ([**@MatthiasReumann**]) +- ✨ Add a `place-and-route` pass for mapping circuits to architectures with restricted topologies ([#1537], [#1547], [#1568], [#1581], [#1583]) ([**@MatthiasReumann**]) - ✨ Add initial infrastructure for new QC and QCO MLIR dialects ([#1264], [#1330], [#1402], [#1428], [#1430], [#1436], [#1443], [#1446], [#1464], [#1465], [#1470], [#1471], [#1472], [#1474], [#1475], [#1506], [#1510], [#1513], [#1521], [#1542], [#1548], [#1550], [#1554], [#1570], [#1572], [#1573]) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**], [**@Ectras**], [**@MatthiasReumann**], [**@simon1hofmann**]) @@ -333,6 +333,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool +[#1583]: https://github.com/munich-quantum-toolkit/core/pull/1583 [#1581]: https://github.com/munich-quantum-toolkit/core/pull/1581 [#1573]: https://github.com/munich-quantum-toolkit/core/pull/1573 [#1572]: https://github.com/munich-quantum-toolkit/core/pull/1572 diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Drivers.h b/mlir/include/mlir/Dialect/QCO/Utils/Drivers.h new file mode 100644 index 0000000000..cce4590efc --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Utils/Drivers.h @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" + +#include +#include +#include +#include + +#include +#include + +namespace mlir::qco { +class Qubits { + /** + * @brief Specifies the qubit "location" (hardware or program). + */ + enum class QubitLocation : std::uint8_t { Hardware, Program }; + +public: + /** + * @brief Add qubit with automatically assigned dynamic index. + */ + [[maybe_unused]] void add(TypedValue q); + + /** + * @brief Add qubit with static index. + */ + void add(TypedValue q, std::size_t hw); + + /** + * @brief Remap the qubit value from prev to next. + */ + void remap(TypedValue prev, TypedValue next); + + /** + * @brief Remove the qubit value. + */ + void remove(TypedValue q); + + /** + * @returns the qubit value assigned to a program index. + */ + [[maybe_unused]] TypedValue getProgramQubit(std::size_t index); + + /** + * @returns the qubit value assigned to a hardware index. + */ + TypedValue getHardwareQubit(std::size_t index); + +private: + DenseMap> programToValue_; + DenseMap> hardwareToValue_; + DenseMap, std::pair> + valueToIndex_; +}; + +/** + * @brief Perform top-down non-recursive walk of all operations within a + * region and apply callback function. + * @details The signature of the callback function is: + * + * (Operation*, Qubits& q) -> WalkResult + * + * where the Qubits object tracks the front of qubit SSA values. + * @param region The targeted region. + * @param fn The callback function. + */ +template void walkUnit(Region& region, Fn&& fn) { + const auto ffn = std::forward(fn); + + Qubits qubits; + for (Operation& curr : region.getOps()) { + if (ffn(&curr, qubits).wasInterrupted()) { + break; + }; + + TypeSwitch(&curr) + .template Case( + [&](StaticOp op) { qubits.add(op.getQubit(), op.getIndex()); }) + .template Case([&](AllocOp op) { qubits.add(op.getResult()); }) + .template Case([&](UnitaryOpInterface op) { + for (const auto& [prevV, nextV] : + llvm::zip(op.getInputQubits(), op.getOutputQubits())) { + const auto prevQ = cast>(prevV); + const auto nextQ = cast>(nextV); + qubits.remap(prevQ, nextQ); + } + }) + .template Case([&](ResetOp op) { + qubits.remap(op.getQubitIn(), op.getQubitOut()); + }) + .template Case([&](MeasureOp op) { + qubits.remap(op.getQubitIn(), op.getQubitOut()); + }) + .template Case( + [&](DeallocOp op) { qubits.remove(op.getQubit()); }); + } +} +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index ef861b88b0..9d99892a63 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -13,6 +13,7 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Mapping/Architecture.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/QCO/Utils/Drivers.h" #include "mlir/Dialect/QCO/Utils/WireIterator.h" #include @@ -20,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -214,10 +214,18 @@ struct MappingPass : impl::MappingPassBase { : programToHardware_(nqubits), hardwareToProgram_(nqubits) {} }; + /** + * @brief The layers of a circuit and the respective IR anchor for each. + */ + struct [[nodiscard]] LayeringResult { + SmallVector layers; + SmallVector anchors; + }; + /** * @brief Required to use Layout as a key for LLVM maps and sets. */ - class LayoutInfo { + class [[nodiscard]] LayoutInfo { using Info = DenseMapInfo>; public: @@ -247,7 +255,7 @@ struct MappingPass : impl::MappingPassBase { /** * @brief Parameters influencing the behavior of the A* search algorithm. */ - struct Parameters { + struct [[nodiscard]] Parameters { Parameters(const float alpha, const float lambda, const std::size_t nlookahead) : alpha(alpha), decay(1 + nlookahead) { @@ -264,7 +272,7 @@ struct MappingPass : impl::MappingPassBase { /** * @brief Describes a node in the A* search graph. */ - struct Node { + struct [[nodiscard]] Node { struct ComparePointer { bool operator()(const Node* lhs, const Node* rhs) const { return lhs->f > rhs->f; @@ -353,54 +361,6 @@ struct MappingPass : impl::MappingPassBase { std::size_t nswaps{}; }; - struct SynchronizationMap { - /** - * @returns true if the operation is contained in the map. - */ - bool contains(Operation* op) const { return onHold.contains(op); } - - /** - * @brief Add op with respective iterator and ref count to the map. - */ - void add(Operation* op, WireIterator* it, const std::size_t cnt) { - onHold.try_emplace(op, SmallVector{it}); - // Decrease the cnt by one because the op was visited when adding. - refCount.try_emplace(op, cnt - 1); - } - - /** - * @brief Decrement ref count of op and potentially release its iterators. - */ - std::optional> visit(Operation* op, - WireIterator* it) { - assert(refCount.contains(op) && "expected sync map to contain op"); - - // Add iterator for later release. - onHold[op].push_back(it); - - // Release iterators whenever the ref count reaches zero. - if (--refCount[op] == 0) { - return onHold[op]; - } - - return std::nullopt; - } - - /** - * @brief Clear the contents of the map. - */ - void clear() { - onHold.clear(); - refCount.clear(); - } - - private: - /// @brief Maps operations to to-be-released iterators. - DenseMap> onHold; - /// @brief Maps operations to ref counts. - DenseMap refCount; - }; - protected: using MappingPassBase::MappingPassBase; @@ -417,15 +377,16 @@ struct MappingPass : impl::MappingPassBase { {5, 8}, {8, 5}, {6, 7}, {7, 6}, {7, 8}, {8, 7}}); for (auto func : getOperation().getOps()) { - const auto dyn = collectDynamicQubits(func.getFunctionBody()); - if (dyn.size() > arch.nqubits()) { + const auto dynQubits = collectDynamicQubits(func.getFunctionBody()); + if (dynQubits.size() > arch.nqubits()) { func.emitError() << "the targeted architecture supports " - << arch.nqubits() << " qubits, got " << dyn.size(); + << arch.nqubits() << " qubits, got " + << dynQubits.size(); signalPassFailure(); return; } - const auto [ltr, rtl] = computeBidirectionalLayers(dyn); + const auto [ltr, rtl] = computeBidirectionalLayers(dynQubits); // Create trials. Currently this includes the identity layout and // `ntrials` many random layouts. @@ -444,7 +405,8 @@ struct MappingPass : impl::MappingPassBase { parallelForEach( &getContext(), enumerate(trials), [&, this](auto indexedTrial) { auto [idx, layout] = indexedTrial; - auto res = runMappingTrial(ltr, rtl, arch, params, layout); + auto res = + runMappingTrial(ltr.layers, rtl.layers, arch, params, layout); if (succeeded(res)) { results[idx] = std::move(*res); } @@ -456,7 +418,8 @@ struct MappingPass : impl::MappingPassBase { return; } - commitTrial(*best, dyn, func.getFunctionBody(), rewriter); + place(dynQubits, best->layout, func.getFunctionBody(), rewriter); + commit(best->swaps, ltr.anchors, func.getFunctionBody(), rewriter); } } @@ -530,7 +493,7 @@ struct MappingPass : impl::MappingPassBase { * @brief Computes forwards and backwards layers. * @returns a pair of vectors of layers, where [0]=forward and [1]=backward. */ - [[nodiscard]] static std::pair, SmallVector> + [[nodiscard]] static std::pair computeBidirectionalLayers(ArrayRef dyn) { auto wires = toWires(dyn); const auto ltr = collectLayers(wires); @@ -538,39 +501,6 @@ struct MappingPass : impl::MappingPassBase { return std::make_pair(ltr, rtl); } - /** - * @brief Perform placement. - * @details Replaces dynamic with static qubits. Extends the computation with - * as many static qubits the architecture supports. - * @returns vector of SSA values produced by qco.static operations, ordered by - * the static index s.t. [i] = qco.static i. - */ - [[nodiscard]] static SmallVector - place(ArrayRef dynQubits, const Layout& layout, Region& funcBody, - IRRewriter& rewriter) { - SmallVector statics(layout.nqubits()); - - // 1. Replace existing dynamic allocations with mapped static ones. - for (const auto [p, q] : enumerate(dynQubits)) { - const auto hw = layout.getHardwareIndex(p); - rewriter.setInsertionPoint(q.getDefiningOp()); - auto op = rewriter.replaceOpWithNewOp(q.getDefiningOp(), hw); - statics[hw] = op.getQubit(); - } - - // 2. Create static qubits for the remaining (unused) hardware indices. - for (std::size_t p = dynQubits.size(); p < layout.nqubits(); ++p) { - rewriter.setInsertionPointToStart(&funcBody.front()); - const auto hw = layout.getHardwareIndex(p); - auto op = StaticOp::create(rewriter, rewriter.getUnknownLoc(), hw); - rewriter.setInsertionPoint(funcBody.back().getTerminator()); - DeallocOp::create(rewriter, rewriter.getUnknownLoc(), op.getQubit()); - statics[hw] = op.getQubit(); - } - - return statics; - } - /** * @brief Perform A* search to find a sequence of SWAPs that makes the * two-qubit operations inside the first layer (the front) executable. @@ -691,7 +621,7 @@ struct MappingPass : impl::MappingPassBase { * @returns a vector of layers. */ template - static SmallVector collectLayers(MutableArrayRef wires) { + static LayeringResult collectLayers(MutableArrayRef wires) { constexpr auto step = d == Direction::Forward ? 1 : -1; const auto shouldContinue = [](const WireIterator& it) { if constexpr (d == Direction::Forward) { @@ -701,11 +631,12 @@ struct MappingPass : impl::MappingPassBase { } }; - SmallVector layers; - DenseMap visited; + LayeringResult result; + DenseMap visited; while (true) { Layer layer{}; + Operation* anchor = nullptr; for (const auto [index, it] : enumerate(wires)) { while (shouldContinue(it)) { const auto res = @@ -730,6 +661,11 @@ struct MappingPass : impl::MappingPassBase { std::ranges::advance(wires[index], step); std::ranges::advance(wires[otherIndex], step); + if (anchor == nullptr || + op->isBeforeInBlock(anchor)) { + anchor = op; + } + visited.erase(op); } else { visited.try_emplace(op, index); @@ -758,11 +694,12 @@ struct MappingPass : impl::MappingPassBase { break; } - layers.emplace_back(layer); + result.layers.emplace_back(layer); + result.anchors.emplace_back(anchor); visited.clear(); } - return layers; + return result; } /** @@ -799,88 +736,78 @@ struct MappingPass : impl::MappingPassBase { } /** - * @brief Performs placement and inserts SWAPs into the IR. - * @details Replace the dynamic with static qubits ("placement") and inserts - * the SWAPs of the trial result into the IR. + * @brief Perform placement. + * @details Replaces dynamic with static qubits. Extends the computation with + * as many static qubits as the architecture supports. */ - void commitTrial(const TrialResult& result, ArrayRef dynQubits, - Region& funcBody, IRRewriter& rewriter) { - // Helper function that advances the iterator to the input qubit (the - // operation producing it) of a deallocation or two-qubit op. - const auto advFront = [](WireIterator& it) { - auto next = std::next(it); - while (true) { - if (isa(next.operation())) { - break; - } - - if (isa(next.operation())) { - break; - } + static void place(ArrayRef dynQubits, const Layout& layout, + Region& funcBody, IRRewriter& rewriter) { + // 1. Replace existing dynamic allocations with mapped static ones. + for (const auto [p, q] : enumerate(dynQubits)) { + const auto hw = layout.getHardwareIndex(p); + rewriter.setInsertionPoint(q.getDefiningOp()); + rewriter.replaceOpWithNewOp(q.getDefiningOp(), hw); + } - auto op = dyn_cast(next.operation()); - if (op && op.getNumQubits() > 1) { - break; - } + // 2. Create static qubits for the remaining (unused) hardware indices. + for (std::size_t p = dynQubits.size(); p < layout.nqubits(); ++p) { + rewriter.setInsertionPointToStart(&funcBody.front()); + const auto hw = layout.getHardwareIndex(p); + auto op = StaticOp::create(rewriter, rewriter.getUnknownLoc(), hw); + rewriter.setInsertionPoint(funcBody.back().getTerminator()); + DeallocOp::create(rewriter, rewriter.getUnknownLoc(), op.getQubit()); + } + } - std::ranges::advance(it, 1); - std::ranges::advance(next, 1); + /** + * @brief Inserts SWAPs into the IR. + */ + void commit(ArrayRef> swaps, + ArrayRef anchors, Region& funcBody, + IRRewriter& rewriter) { + ArrayRef::iterator anchorIt = anchors.begin(); + ArrayRef>::iterator swapIt = swaps.begin(); + + walkUnit(funcBody, [&](Operation* op, Qubits& qubits) { + // Early exit if we've processed all layers. + if (anchorIt == anchors.end()) { + return WalkResult::interrupt(); } - }; - auto wires = toWires(place(dynQubits, result.layout, funcBody, rewriter)); + if (op == *anchorIt) { + rewriter.setInsertionPoint(*anchorIt); - SynchronizationMap ready; - for (const auto& swaps : result.swaps) { - // Advance all wires to the next front of one-qubit outputs - // (the SSA values). - for_each(wires, advFront); + for (const auto& [hw0, hw1] : *swapIt) { + const auto in0 = qubits.getHardwareQubit(hw0); + const auto in1 = qubits.getHardwareQubit(hw1); - // Apply the sequence of SWAPs and rewire the qubit SSA values. - for (const auto& [hw0, hw1] : swaps) { - const auto in0 = wires[hw0].qubit(); - const auto in1 = wires[hw1].qubit(); + auto insertedOp = SWAPOp::create(rewriter, op->getLoc(), in0, in1); - auto op = SWAPOp::create(rewriter, rewriter.getUnknownLoc(), in0, in1); - const auto out0 = op.getQubit0Out(); - const auto out1 = op.getQubit1Out(); + const auto out0 = insertedOp.getQubit0Out(); + const auto out1 = insertedOp.getQubit1Out(); - rewriter.replaceAllUsesExcept(in0, out1, op); - rewriter.replaceAllUsesExcept(in1, out0, op); + rewriter.replaceAllUsesExcept(in0, out1, insertedOp); + rewriter.replaceAllUsesExcept(in1, out0, insertedOp); - // Jump over the SWAPOp. - std::ranges::advance(wires[hw0], 1); - std::ranges::advance(wires[hw1], 1); - } + // Remove old qubit values. + qubits.remove(in0); + qubits.remove(in1); - // Jump over "ready" gates. - for (auto& it : wires) { - auto op = dyn_cast(std::next(it).operation()); - if (!op) { - continue; + // Add permutated qubit value - hw index pair. + qubits.add(out0, hw1); + qubits.add(out1, hw0); } - if (op.getNumQubits() < 2) { - continue; - } + // Collect statistics. + this->numSwaps += swapIt->size(); - if (!ready.contains(op)) { - ready.add(op, &it, op.getNumQubits()); - continue; - } - - if (auto opt = ready.visit(op, &it)) { - for (WireIterator* wire : *opt) { - std::ranges::advance(*wire, 1); - } - } + // Move to the next layer and the next anchor. + std::ranges::advance(swapIt, 1); + std::ranges::advance(anchorIt, 1); } - ready.clear(); // Prepare for next iteration. - this->numSwaps += swaps.size(); - } - - for_each(funcBody.getBlocks(), [](Block& b) { sortTopologically(&b); }); + return WalkResult::advance(); + }); } }; diff --git a/mlir/lib/Dialect/QCO/Utils/Driver.cpp b/mlir/lib/Dialect/QCO/Utils/Driver.cpp new file mode 100644 index 0000000000..7f96a17003 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Utils/Driver.cpp @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/Utils/Drivers.h" + +#include + +#include +#include +#include + +namespace mlir::qco { +void Qubits::add(TypedValue q) { + const auto index = programToValue_.size(); + programToValue_.try_emplace(index, q); + valueToIndex_.try_emplace(q, std::make_pair(QubitLocation::Program, index)); +} + +void Qubits::add(TypedValue q, std::size_t hw) { + hardwareToValue_.try_emplace(hw, q); + valueToIndex_.try_emplace(q, std::make_pair(QubitLocation::Hardware, hw)); +} + +void Qubits::remap(TypedValue prev, TypedValue next) { + assert(valueToIndex_.contains(prev)); + const auto& [location, index] = valueToIndex_.lookup(prev); + + valueToIndex_.erase(prev); + valueToIndex_.try_emplace(next, std::make_pair(location, index)); + + if (location == QubitLocation::Program) { + programToValue_[index] = next; + return; + } + + hardwareToValue_[index] = next; +} + +void Qubits::remove(TypedValue q) { + assert(valueToIndex_.contains(q)); + const auto& [location, index] = valueToIndex_.lookup(q); + + valueToIndex_.erase(q); + + if (location == QubitLocation::Program) { + programToValue_.erase(index); + return; + } + + hardwareToValue_.erase(index); +} + +TypedValue Qubits::getProgramQubit(std::size_t index) { + assert(programToValue_.contains(index)); + return programToValue_.lookup(index); +} + +TypedValue Qubits::getHardwareQubit(std::size_t index) { + assert(hardwareToValue_.contains(index)); + return hardwareToValue_.lookup(index); +} +} // namespace mlir::qco diff --git a/mlir/unittests/Dialect/QCO/Utils/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Utils/CMakeLists.txt index 49b666ed41..a8070a6984 100644 --- a/mlir/unittests/Dialect/QCO/Utils/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Utils/CMakeLists.txt @@ -7,7 +7,7 @@ # Licensed under the MIT License set(qco_utils_target mqt-core-mlir-unittest-qco-utils) -add_executable(${qco_utils_target} test_wireiterator.cpp) +add_executable(${qco_utils_target} test_drivers.cpp test_wireiterator.cpp) target_link_libraries(${qco_utils_target} PRIVATE GTest::gtest_main MLIRQCODialect MLIRQCOUtils MLIRQCOProgramBuilder) mqt_mlir_configure_unittest_target(${qco_utils_target}) diff --git a/mlir/unittests/Dialect/QCO/Utils/test_drivers.cpp b/mlir/unittests/Dialect/QCO/Utils/test_drivers.cpp new file mode 100644 index 0000000000..39b16525bd --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Utils/test_drivers.cpp @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/Utils/Drivers.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace mlir; + +namespace { +class DriversTest : public testing::Test { +protected: + void SetUp() override { + DialectRegistry registry; + registry.insert(); + + context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); + } + + std::unique_ptr context; +}; +} // namespace + +TEST_F(DriversTest, FullWalk) { + qco::QCOProgramBuilder builder(context.get()); + builder.initialize(); + const auto q00 = builder.allocQubit(); + const auto q10 = builder.allocQubit(); + const auto q20 = builder.staticQubit(0); + const auto q30 = builder.staticQubit(1); + + const auto q01 = builder.h(q00); + const auto [q02, q11] = builder.cx(q01, q10); + const auto [q21, q31] = builder.cx(q20, q30); + + const auto [q03, c0] = builder.measure(q02); + const auto [q12, c1] = builder.measure(q11); + const auto [q22, c2] = builder.measure(q21); + const auto [q32, c3] = builder.measure(q31); + + builder.dealloc(q03); + builder.dealloc(q12); + builder.dealloc(q22); + builder.dealloc(q32); + + auto module = builder.finalize(); + auto func = *(module->getOps().begin()); + + Value ex0 = nullptr; + Value ex1 = nullptr; + Value ex2 = nullptr; + Value ex3 = nullptr; + + qco::walkUnit(func.getBody(), [&](Operation* op, qco::Qubits& qubits) { + if (op == q03.getDefiningOp()) { + ex0 = qubits.getProgramQubit(0); + ex1 = qubits.getProgramQubit(1); + ex2 = qubits.getHardwareQubit(0); + ex3 = qubits.getHardwareQubit(1); + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + + ASSERT_EQ(ex0, q02); + ASSERT_EQ(ex1, q11); + ASSERT_EQ(ex2, q21); + ASSERT_EQ(ex3, q31); +}