From 29ab7fea513dcc591b8564f8021d2bf71bcd3dd2 Mon Sep 17 00:00:00 2001 From: Matthias Reumann Date: Fri, 20 Mar 2026 13:37:02 +0100 Subject: [PATCH 01/17] Add discovered-layouts set --- .../QCO/Transforms/Mapping/Mapping.cpp | 72 +++++++++++++------ 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index ebd8fa2cf8..60d5ac7454 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -15,6 +15,7 @@ #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/WireIterator.h" +#include #include #include #include @@ -70,6 +71,8 @@ struct MappingPass : impl::MappingPassBase { */ enum class Direction : std::uint8_t { Forward, Backward }; + struct LayoutInfo; + /** * @brief A qubit layout that maps program and hardware indices without * storing Values. Used for efficient memory usage when Value tracking isn't @@ -191,18 +194,6 @@ struct MappingPass : impl::MappingPassBase { return programToHardware_.size(); } - void dump() { - llvm::dbgs() << "prog= "; - for (std::size_t i = 0; i < nqubits(); ++i) { - llvm::dbgs() << i << " "; - } - llvm::dbgs() << "\nhw= "; - for (std::size_t i = 0; i < nqubits(); ++i) { - llvm::dbgs() << programToHardware_[i] << ' '; - } - llvm::dbgs() << '\n'; - } - protected: /** * @brief Maps a program qubit index to its hardware index. @@ -215,10 +206,44 @@ struct MappingPass : impl::MappingPassBase { SmallVector hardwareToProgram_; private: + friend struct MappingPass::LayoutInfo; + + Layout() = default; explicit Layout(const std::size_t nqubits) : programToHardware_(nqubits), hardwareToProgram_(nqubits) {} }; + /** + * @brief Required to use Layout as a key for LLVM maps and sets. + */ + class LayoutInfo { + using Info = DenseMapInfo>; + + public: + static Layout getEmptyKey() { + Layout l; + l.programToHardware_ = Info::getEmptyKey(); + l.hardwareToProgram_ = Info::getEmptyKey(); + return l; + } + + static Layout getTombstoneKey() { + Layout l; + l.programToHardware_ = Info::getTombstoneKey(); + l.hardwareToProgram_ = Info::getTombstoneKey(); + return l; + } + + static unsigned getHashValue(const Layout& l) { + return Info::getHashValue(l.programToHardware_); + } + + static bool isEqual(const Layout& a, const Layout& b) { + using Info = DenseMapInfo; + return Info::isEqual(a.programToHardware_, b.programToHardware_); + } + }; + /** * @brief Parameters influencing the behavior of the A* search algorithm. */ @@ -507,9 +532,9 @@ struct MappingPass : impl::MappingPassBase { * @brief Perform A* search to find a sequence of SWAPs that makes the * two-qubit operations inside the first layer (the front) executable. * @details - * The iteration budget is then b^{3}, which corresponds to - * exhausting all paths of length up to b^{2} in a search tree with branching - * factor b. A hard cap prevents impractical runtimes on larger architectures. + * The iteration budget is b^{3}, which corresponds to exhausting all paths of + * length up to b^{2} in a search tree with branching factor b. A hard cap + * prevents impractical runtimes on larger architectures. * * The branching factor b of the A* search is the product of the * architecture's maximum qubit degree and the maximum number of two-qubit @@ -534,6 +559,7 @@ struct MappingPass : impl::MappingPassBase { MinQueue frontier{}; frontier.emplace(root); + DenseSet discovered{root.layout}; DenseSet expansionSet; std::size_t i = 0; @@ -549,10 +575,6 @@ struct MappingPass : impl::MappingPassBase { // two neighbouring hardware qubits. expansionSet.clear(); - if (!curr.sequence.empty()) { - expansionSet.insert(curr.sequence.back()); - } - for (const IndexGate& gate : layers.front()) { for (const auto prog : {gate.first, gate.second}) { const auto hw0 = curr.layout.getHardwareIndex(prog); @@ -563,7 +585,17 @@ struct MappingPass : impl::MappingPassBase { continue; } - frontier.emplace(curr, swap, layers, arch, params); + Node child(curr, swap, layers, arch, params); + + // Multiple sequences of SWAPs may lead to the same layout. + // The if below ensures that we don't visit the same layout twice. + // TODO: In the future, should fidelities be ever considered, the + // sequence of SWAPs matters - so this will become more difficult. + if (!discovered.insert(child.layout).second) { + continue; + } + + frontier.emplace(std::move(child)); } } } From 67e2eff4aa9d7d8684d28390471239b6863b0d89 Mon Sep 17 00:00:00 2001 From: Matthias Reumann Date: Fri, 20 Mar 2026 14:13:04 +0100 Subject: [PATCH 02/17] Fix linting --- mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 60d5ac7454..d6988cb995 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -15,11 +15,9 @@ #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/WireIterator.h" -#include #include #include #include -#include #include #include #include From badfc926dac292d02d10795fde01404e44a1ba77 Mon Sep 17 00:00:00 2001 From: Matthias Reumann Date: Fri, 20 Mar 2026 14:30:22 +0100 Subject: [PATCH 03/17] Fix documentation --- mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index d6988cb995..2f113e1ad9 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -530,9 +530,9 @@ struct MappingPass : impl::MappingPassBase { * @brief Perform A* search to find a sequence of SWAPs that makes the * two-qubit operations inside the first layer (the front) executable. * @details - * The iteration budget is b^{3}, which corresponds to exhausting all paths of - * length up to b^{2} in a search tree with branching factor b. A hard cap - * prevents impractical runtimes on larger architectures. + * The iteration budget is b^{3} node expansions, i.e. roughly a depth-3 + * search in a tree with branching factor b. A hard cap prevents impractical + * runtimes on larger architectures. * * The branching factor b of the A* search is the product of the * architecture's maximum qubit degree and the maximum number of two-qubit From 5bbe70444c96d380241d52c26fd9e723a6cce698 Mon Sep 17 00:00:00 2001 From: Matthias Reumann Date: Fri, 20 Mar 2026 15:02:58 +0100 Subject: [PATCH 04/17] Apply bunny suggestion --- .../QCO/Transforms/Mapping/Mapping.cpp | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 2f113e1ad9..6d64e380f4 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -237,7 +237,6 @@ struct MappingPass : impl::MappingPassBase { } static bool isEqual(const Layout& a, const Layout& b) { - using Info = DenseMapInfo; return Info::isEqual(a.programToHardware_, b.programToHardware_); } }; @@ -275,19 +274,23 @@ struct MappingPass : impl::MappingPassBase { /** * @brief Construct a non-root node from its parent node. Apply the given - * swap to the layout of the parent node and evaluate the cost. + * swap to the layout of the parent node. */ - Node(const Node& parent, IndexGate swap, ArrayRef layers, - const Architecture& arch, const Parameters& params) + Node(const Node& parent, IndexGate swap) : sequence(parent.sequence), layout(parent.layout), f(0) { // Apply node-specific swap to given layout. layout.swap(swap.first, swap.second); // Add swap to sequence. sequence.emplace_back(swap); + } - // Evaluate cost function. - f = g(params.alpha) + h(layers, arch, params); // NOLINT + /** + * @brief Evaluate the cost function. + */ + void evalCost(ArrayRef layers, const Architecture& arch, + const Parameters& params) { + f = g(params.alpha) + h(layers, arch, params); } /** @@ -577,22 +580,24 @@ struct MappingPass : impl::MappingPassBase { for (const auto prog : {gate.first, gate.second}) { const auto hw0 = curr.layout.getHardwareIndex(prog); for (const auto hw1 : arch.neighboursOf(hw0)) { - /// Ensure consistent hashing/comparison. + // Ensure consistent hashing/comparison. const IndexGate swap = std::minmax(hw0, hw1); if (!expansionSet.insert(swap).second) { continue; } - Node child(curr, swap, layers, arch, params); - // Multiple sequences of SWAPs may lead to the same layout. - // The if below ensures that we don't visit the same layout twice. + // The discovered set ensures that we do not visit the same layout + // multiple times. Only after ensuring that the layout hasn't been + // discovered so far, we evaluate the "expensive" cost function. // TODO: In the future, should fidelities be ever considered, the // sequence of SWAPs matters - so this will become more difficult. + + Node child(curr, swap); if (!discovered.insert(child.layout).second) { continue; } - + child.evalCost(layers, arch, params); frontier.emplace(std::move(child)); } } From 231f66410bb1d34016221ef33b23290a78f2c5c0 Mon Sep 17 00:00:00 2001 From: Matthias Reumann Date: Mon, 23 Mar 2026 10:01:48 +0100 Subject: [PATCH 05/17] Implement arena --- .../QCO/Transforms/Mapping/Mapping.cpp | 85 ++++++++++--------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 6d64e380f4..97bf9a9077 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -262,27 +264,33 @@ struct MappingPass : impl::MappingPassBase { * @brief Describes a node in the A* search graph. */ struct Node { - SmallVector sequence; + struct ComparePointer { + bool operator()(const Node* lhs, const Node* rhs) const { + return lhs->f > rhs->f; + } + }; + Layout layout; + IndexGate swap; + Node* parent; + std::size_t depth; float f; /** * @brief Construct a root node with the given layout. Initialize the * sequence with an empty vector and set the cost to zero. */ - explicit Node(Layout layout) : layout(std::move(layout)), f(0) {} + explicit Node(Layout layout) + : layout(std::move(layout)), parent(nullptr), depth(0), f(0) {} /** * @brief Construct a non-root node from its parent node. Apply the given * swap to the layout of the parent node. */ - Node(const Node& parent, IndexGate swap) - : sequence(parent.sequence), layout(parent.layout), f(0) { - // Apply node-specific swap to given layout. + Node(Node* parent, IndexGate swap) + : layout(parent->layout), swap(swap), parent(parent), + depth(parent->depth + 1), f(0) { layout.swap(swap.first, swap.second); - - // Add swap to sequence. - sequence.emplace_back(swap); } /** @@ -305,12 +313,6 @@ struct MappingPass : impl::MappingPassBase { }); } - /** - * @returns true iff. the costs of this node are higher than the one of @p - * rhs. - */ - [[nodiscard]] bool operator>(const Node& rhs) const { return f > rhs.f; } - private: /** * @brief Calculate the path cost for the A* search algorithm. @@ -319,7 +321,7 @@ struct MappingPass : impl::MappingPassBase { * SWAPs. */ [[nodiscard]] float g(float alpha) const { - return alpha * static_cast(sequence.size()); + return alpha * static_cast(depth); } /** @@ -345,12 +347,20 @@ struct MappingPass : impl::MappingPassBase { } }; - using MinQueue = std::priority_queue, std::greater<>>; + struct [[nodiscard]] TrialResult { + explicit TrialResult(Layout layout) : layout(std::move(layout)) {} + + /// @brief The computed initial layout. + Layout layout; + /// @brief A vector of SWAPs for each layer. + SmallVector> swaps; + /// @brief The number of inserted SWAPs. + std::size_t nswaps{}; + }; public: using MappingPassBase::MappingPassBase; -protected: void runOnOperation() override { std::mt19937_64 rng{this->seed}; IRRewriter rewriter(&getContext()); @@ -408,17 +418,6 @@ struct MappingPass : impl::MappingPassBase { } private: - struct [[nodiscard]] TrialResult { - explicit TrialResult(Layout layout) : layout(std::move(layout)) {} - - /// @brief The computed initial layout. - Layout layout; - /// @brief A vector of SWAPs for each layer. - SmallVector> swaps; - /// @brief The number of inserted SWAPs. - std::size_t nswaps{}; - }; - /** * @brief Find the best trial result in terms of the number of SWAPs. * @returns the best trial result or nullptr if no result is valid. @@ -553,23 +552,28 @@ struct MappingPass : impl::MappingPassBase { const std::size_t b = arch.maxDegree() * ((arch.nqubits() + 1) / 2); const std::size_t budget = std::min(b * b * b, cap); - Node root(layout); - if (root.isGoal(layers.front(), arch)) { - return SmallVector{}; - } + llvm::SpecificBumpPtrAllocator arena; + std::priority_queue, Node::ComparePointer> + frontier; + frontier.emplace(std::construct_at(arena.Allocate(), layout)); - MinQueue frontier{}; - frontier.emplace(root); - DenseSet discovered{root.layout}; + DenseSet discovered{layout}; DenseSet expansionSet; std::size_t i = 0; while (!frontier.empty() && i < budget) { - Node curr = frontier.top(); + Node* curr = frontier.top(); frontier.pop(); - if (curr.isGoal(layers.front(), arch)) { - return curr.sequence; + // If the currently visited node is a goal node, reconstruct the sequence + // of SWAPs from this node to the root. + + if (curr->isGoal(layers.front(), arch)) { + SmallVector seq; + for (Node* n = curr; n->parent != nullptr; n = n->parent) { + seq.push_back(n->swap); + } + return to_vector(reverse(seq)); } // Given a layout, create child-nodes for each possible SWAP between @@ -578,7 +582,7 @@ struct MappingPass : impl::MappingPassBase { expansionSet.clear(); for (const IndexGate& gate : layers.front()) { for (const auto prog : {gate.first, gate.second}) { - const auto hw0 = curr.layout.getHardwareIndex(prog); + const auto hw0 = curr->layout.getHardwareIndex(prog); for (const auto hw1 : arch.neighboursOf(hw0)) { // Ensure consistent hashing/comparison. const IndexGate swap = std::minmax(hw0, hw1); @@ -598,7 +602,8 @@ struct MappingPass : impl::MappingPassBase { continue; } child.evalCost(layers, arch, params); - frontier.emplace(std::move(child)); + frontier.emplace( + std::construct_at(arena.Allocate(), std::move(child))); } } } From 2989f61864499ab819225ae92b0177b421130634 Mon Sep 17 00:00:00 2001 From: Matthias Reumann Date: Mon, 23 Mar 2026 11:57:55 +0100 Subject: [PATCH 06/17] Update discovered map --- .../QCO/Transforms/Mapping/Mapping.cpp | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 97bf9a9077..d1d27bf860 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -287,18 +287,12 @@ struct MappingPass : impl::MappingPassBase { * @brief Construct a non-root node from its parent node. Apply the given * swap to the layout of the parent node. */ - Node(Node* parent, IndexGate swap) + Node(Node* parent, IndexGate swap, ArrayRef layers, + const Architecture& arch, const Parameters& params) : layout(parent->layout), swap(swap), parent(parent), depth(parent->depth + 1), f(0) { layout.swap(swap.first, swap.second); - } - - /** - * @brief Evaluate the cost function. - */ - void evalCost(ArrayRef layers, const Architecture& arch, - const Parameters& params) { - f = g(params.alpha) + h(layers, arch, params); + f = g(params.alpha) + h(layers, arch, params); // NOLINT } /** @@ -557,7 +551,10 @@ struct MappingPass : impl::MappingPassBase { frontier; frontier.emplace(std::construct_at(arena.Allocate(), layout)); - DenseSet discovered{layout}; + // The following maps discovered layouts to the lowest-depth parent + // pointers. + DenseMap discovered; + DenseSet expansionSet; std::size_t i = 0; @@ -576,6 +573,16 @@ struct MappingPass : impl::MappingPassBase { return to_vector(reverse(seq)); } + const auto [it, inserted] = discovered.try_emplace(curr->layout, curr); + if (!inserted) { + Node* other = it->getSecond(); + if (curr->depth < other->depth) { + discovered[curr->layout] = curr; + } + ++i; + continue; + } + // Given a layout, create child-nodes for each possible SWAP between // two neighbouring hardware qubits. @@ -597,13 +604,8 @@ struct MappingPass : impl::MappingPassBase { // TODO: In the future, should fidelities be ever considered, the // sequence of SWAPs matters - so this will become more difficult. - Node child(curr, swap); - if (!discovered.insert(child.layout).second) { - continue; - } - child.evalCost(layers, arch, params); - frontier.emplace( - std::construct_at(arena.Allocate(), std::move(child))); + frontier.emplace(std::construct_at(arena.Allocate(), curr, swap, + layers, arch, params)); } } } From 1cda491d52f9aabc4b8c09bf3c331ceb2d2f6695 Mon Sep 17 00:00:00 2001 From: Matthias Reumann Date: Mon, 23 Mar 2026 11:58:27 +0100 Subject: [PATCH 07/17] Update mapping test --- mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index 8b65bf57f6..0cbb437b46 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -108,7 +108,7 @@ class MappingPassTest : public testing::Test, .alpha = 1, .lambda = 0.85, .niterations = 2, - .ntrials = 8, + .ntrials = 16, .seed = 1337})); pm.addPass(createQCOToQC()); auto res = pm.run(*moduleOp); From ca59171707e28c7b05374e57451f9e7bca3cd4e3 Mon Sep 17 00:00:00 2001 From: Matthias Reumann Date: Tue, 24 Mar 2026 12:53:59 +0100 Subject: [PATCH 08/17] Fix SSA dominance issues --- .../QCO/Transforms/Mapping/Mapping.cpp | 148 +++++++++++++----- .../QCO/Transforms/Mapping/test_mapping.cpp | 12 ++ 2 files changed, 120 insertions(+), 40 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index ebd8fa2cf8..f16a89b02f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -321,6 +322,65 @@ struct MappingPass : impl::MappingPassBase { using MinQueue = std::priority_queue, std::greater<>>; + struct [[nodiscard]] TrialResult { + explicit TrialResult(Layout layout) : layout(std::move(layout)) {} + + /// @brief The computed initial layout. + Layout layout; + /// @brief A vector of SWAPs for each layer. + SmallVector> swaps; + /// @brief The number of inserted SWAPs. + 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; + }; + public: using MappingPassBase::MappingPassBase; @@ -382,17 +442,6 @@ struct MappingPass : impl::MappingPassBase { } private: - struct [[nodiscard]] TrialResult { - explicit TrialResult(Layout layout) : layout(std::move(layout)) {} - - /// @brief The computed initial layout. - Layout layout; - /// @brief A vector of SWAPs for each layer. - SmallVector> swaps; - /// @brief The number of inserted SWAPs. - std::size_t nswaps{}; - }; - /** * @brief Find the best trial result in terms of the number of SWAPs. * @returns the best trial result or nullptr if no result is valid. @@ -615,28 +664,33 @@ struct MappingPass : impl::MappingPassBase { while (shouldContinue(it)) { const auto res = TypeSwitch(it.operation()) - .Case([&](UnitaryOpInterface op) { - assert(op.getNumQubits() > 0 && op.getNumQubits() <= 2); + .Case([&](auto) { + std::ranges::advance(it, step); + return WalkResult::advance(); + }) + .template Case( + [&](UnitaryOpInterface op) { + assert(op.getNumQubits() > 0 && op.getNumQubits() <= 2); - if (op.getNumQubits() == 1) { - std::ranges::advance(it, step); - return WalkResult::advance(); - } + if (op.getNumQubits() == 1) { + std::ranges::advance(it, step); + return WalkResult::advance(); + } - if (visited.contains(op)) { - const auto otherIndex = visited[op]; - layer.insert(std::make_pair(index, otherIndex)); + if (visited.contains(op)) { + const auto otherIndex = visited[op]; + layer.insert(std::make_pair(index, otherIndex)); - std::ranges::advance(wires[index], step); - std::ranges::advance(wires[otherIndex], step); + std::ranges::advance(wires[index], step); + std::ranges::advance(wires[otherIndex], step); - visited.erase(op); - } else { - visited.try_emplace(op, index); - } + visited.erase(op); + } else { + visited.try_emplace(op, index); + } - return WalkResult::interrupt(); - }) + return WalkResult::interrupt(); + }) .template Case([&](auto) { std::ranges::advance(it, step); @@ -709,25 +763,30 @@ struct MappingPass : impl::MappingPassBase { // 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) { - const auto next = std::next(it); if (isa(next.operation())) { break; } + if (isa(next.operation())) { + break; + } + auto op = dyn_cast(next.operation()); if (op && op.getNumQubits() > 1) { break; } std::ranges::advance(it, 1); + std::ranges::advance(next, 1); } }; auto wires = toWires(place(dynQubits, result.layout, funcBody, rewriter)); - DenseMap seen; - for (const auto [i, swaps] : enumerate(result.swaps)) { + 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); @@ -760,21 +819,30 @@ struct MappingPass : impl::MappingPassBase { std::ranges::advance(wires[hw1], 1); } - // Jump over "ready" two-qubit gates. + // Jump over "ready" gates. for (auto& it : wires) { auto op = dyn_cast(std::next(it).operation()); - if (op && op.getNumQubits() > 1) { - if (seen.contains(op)) { - std::ranges::advance(it, 1); - std::ranges::advance(*seen[op], 1); - continue; - } + if (!op) { + continue; + } + + if (op.getNumQubits() < 2) { + continue; + } - seen.try_emplace(op, &it); + 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); + } } } - seen.clear(); // Prepare for next iteration. + ready.clear(); // Prepare for next iteration. } } }; diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index 8b65bf57f6..d8913bf571 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -63,6 +64,9 @@ class MappingPassTest : public testing::Test, bool executable = true; std::ignore = moduleOp->walk([&](qc::UnitaryOpInterface op) { + if (isa(op)) { + return WalkResult::advance(); + } if (op.getNumQubits() > 1) { assert(op.getNumQubits() == 2 && "Expected only 2-qubit gates after decomposition"); @@ -178,6 +182,14 @@ TEST_P(MappingPassTest, Sabre) { builder.cx(q3, q0); + builder.barrier({q0, q1, q2, q3, q4, q5}); + builder.measure(q0); + builder.measure(q1); + builder.measure(q2); + builder.measure(q3); + builder.measure(q4); + builder.measure(q5); + builder.dealloc(q0); builder.dealloc(q1); builder.dealloc(q2); From 660d27cebc4aa726f7cdc45f702b281b14fd021c Mon Sep 17 00:00:00 2001 From: Matthias Reumann Date: Tue, 24 Mar 2026 12:58:02 +0100 Subject: [PATCH 09/17] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d798926f7..b152d08760 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]) ([**@MatthiasReumann**]) +- ✨ Add a `place-and-route` pass for mapping circuits to architectures with restricted topologies ([#1537], [#1547], [#1568], [#1581]) ([**@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 +[#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 [#1571]: https://github.com/munich-quantum-toolkit/core/pull/1571 From 8830b64fb62b1a16de0bbe27cbba8bbce7f3fe3e Mon Sep 17 00:00:00 2001 From: Matthias Reumann Date: Tue, 24 Mar 2026 12:59:10 +0100 Subject: [PATCH 10/17] Remove unused header file --- mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index f16a89b02f..8602a98b82 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include From d15387d130ba36bf427ed161b98125f9730a811f Mon Sep 17 00:00:00 2001 From: Matthias Reumann Date: Tue, 24 Mar 2026 13:13:12 +0100 Subject: [PATCH 11/17] Fix lint --- mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index d8913bf571..1ab817660a 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -20,7 +20,6 @@ #include #include -#include #include #include #include From 50a9c62b72fa0cc020bf58e16879a756478587ff Mon Sep 17 00:00:00 2001 From: Matthias Reumann Date: Tue, 24 Mar 2026 15:47:54 +0100 Subject: [PATCH 12/17] Implement bestDepth map --- .../QCO/Transforms/Mapping/Mapping.cpp | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 37406fab28..ba0ded8fc5 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -370,8 +370,8 @@ struct MappingPass : impl::MappingPassBase { /** * @brief Decrement ref count of op and potentially release its iterators. */ - std::optional> visit(Operation* op, - WireIterator* it) { + std::optional> visit(Operation* op, + WireIterator* it) { assert(refCount.contains(op) && "expected sync map to contain op"); // Add iterator for later release. @@ -395,7 +395,7 @@ struct MappingPass : impl::MappingPassBase { private: /// @brief Maps operations to to-be-released iterators. - DenseMap> onHold; + DenseMap> onHold; /// @brief Maps operations to ref counts. DenseMap refCount; }; @@ -599,10 +599,7 @@ struct MappingPass : impl::MappingPassBase { frontier; frontier.emplace(std::construct_at(arena.Allocate(), layout)); - // The following maps discovered layouts to the lowest-depth parent - // pointers. - DenseMap discovered; - + DenseMap bestDepth; DenseSet expansionSet; std::size_t i = 0; @@ -610,6 +607,23 @@ struct MappingPass : impl::MappingPassBase { Node* curr = frontier.top(); frontier.pop(); + // Multiple sequences of SWAPs can lead to the same layout and the same + // layout creates the same child-nodes. Thus, if we've seen a layout + // already at a lower depth don't reexpand the current node (and hence + // recreate the same child nodes). + + const auto [it, inserted] = + bestDepth.try_emplace(curr->layout, curr->depth); + if (!inserted) { + const auto otherDepth = it->getSecond(); + if (curr->depth >= otherDepth) { + ++i; + continue; + } + + it->second = curr->depth; + } + // If the currently visited node is a goal node, reconstruct the sequence // of SWAPs from this node to the root. @@ -621,18 +635,8 @@ struct MappingPass : impl::MappingPassBase { return to_vector(reverse(seq)); } - const auto [it, inserted] = discovered.try_emplace(curr->layout, curr); - if (!inserted) { - Node* other = it->getSecond(); - if (curr->depth < other->depth) { - discovered[curr->layout] = curr; - } - ++i; - continue; - } - - // Given a layout, create child-nodes for each possible SWAP between - // two neighbouring hardware qubits. + // Given a layout, create child-nodes for each possible SWAP + // between two neighbouring hardware qubits. expansionSet.clear(); for (const IndexGate& gate : layers.front()) { @@ -645,13 +649,6 @@ struct MappingPass : impl::MappingPassBase { continue; } - // Multiple sequences of SWAPs may lead to the same layout. - // The discovered set ensures that we do not visit the same layout - // multiple times. Only after ensuring that the layout hasn't been - // discovered so far, we evaluate the "expensive" cost function. - // TODO: In the future, should fidelities be ever considered, the - // sequence of SWAPs matters - so this will become more difficult. - frontier.emplace(std::construct_at(arena.Allocate(), curr, swap, layers, arch, params)); } From 1775f5e672894611ab73c6894695dbbd73ce10e5 Mon Sep 17 00:00:00 2001 From: Matthias Reumann Date: Tue, 24 Mar 2026 15:49:37 +0100 Subject: [PATCH 13/17] Fix merge --- .../QCO/Transforms/Mapping/Mapping.cpp | 59 ------------------- 1 file changed, 59 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 130105b9d6..ba0ded8fc5 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -400,65 +400,6 @@ struct MappingPass : impl::MappingPassBase { DenseMap refCount; }; - struct [[nodiscard]] TrialResult { - explicit TrialResult(Layout layout) : layout(std::move(layout)) {} - - /// @brief The computed initial layout. - Layout layout; - /// @brief A vector of SWAPs for each layer. - SmallVector> swaps; - /// @brief The number of inserted SWAPs. - 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; - }; - public: using MappingPassBase::MappingPassBase; From 589ab21fc77b84234fedf99e035f278109ebf16a Mon Sep 17 00:00:00 2001 From: Matthias Reumann Date: Tue, 24 Mar 2026 15:59:59 +0100 Subject: [PATCH 14/17] Fix linting --- mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index ba0ded8fc5..63e28b502e 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -400,7 +400,7 @@ struct MappingPass : impl::MappingPassBase { DenseMap refCount; }; -public: +protected: using MappingPassBase::MappingPassBase; void runOnOperation() override { From 4b2af3e987f80fe0419b6196ba5ed270bccc1192 Mon Sep 17 00:00:00 2001 From: Matthias Reumann Date: Tue, 24 Mar 2026 16:39:07 +0100 Subject: [PATCH 15/17] Preallocate sequence vector --- .../mlir/Dialect/QCO/Transforms/Passes.td | 2 ++ .../QCO/Transforms/Mapping/Mapping.cpp | 26 ++++++++----------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 1200912148..bbb0c05865 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -69,6 +69,8 @@ def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { "the forwards and backwards mechanism.">, Option<"seed", "seed", "std::size_t", "42", "A seed used for randomization.">]; + let statistics = [Statistic<"numSwaps", "num-inserted-swaps", + "The number of inserted SWAPs">]; } #endif // MLIR_DIALECT_QCO_TRANSFORMS_PASSES_TD diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 63e28b502e..ef81658d17 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -628,11 +629,13 @@ struct MappingPass : impl::MappingPassBase { // of SWAPs from this node to the root. if (curr->isGoal(layers.front(), arch)) { - SmallVector seq; + SmallVector seq(curr->depth); + std::size_t i = seq.size() - 1; for (Node* n = curr; n->parent != nullptr; n = n->parent) { - seq.push_back(n->swap); + seq[i] = n->swap; + --i; } - return to_vector(reverse(seq)); + return seq; } // Given a layout, create child-nodes for each possible SWAP @@ -795,9 +798,8 @@ struct MappingPass : impl::MappingPassBase { * @details Replace the dynamic with static qubits ("placement") and inserts * the SWAPs of the trial result into the IR. */ - static void commitTrial(const TrialResult& result, - ArrayRef dynQubits, Region& funcBody, - IRRewriter& rewriter) { + 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) { @@ -836,15 +838,6 @@ struct MappingPass : impl::MappingPassBase { const auto in0 = wires[hw0].qubit(); const auto in1 = wires[hw1].qubit(); - // Reorder to avoid SSA dominance issues. - assert(op0->getBlock()->isOpOrderValid() && - "An invalid op order leads to a significant runtime overhead."); - if (op0->isBeforeInBlock(op1)) { - rewriter.setInsertionPointAfterValue(in1); - } else { - rewriter.setInsertionPointAfterValue(in0); - } - auto op = SWAPOp::create(rewriter, rewriter.getUnknownLoc(), in0, in1); const auto out0 = op.getQubit0Out(); const auto out1 = op.getQubit1Out(); @@ -881,7 +874,10 @@ struct MappingPass : impl::MappingPassBase { } ready.clear(); // Prepare for next iteration. + this->numSwaps += swaps.size(); } + + for_each(funcBody.getBlocks(), [](Block& b) { sortTopologically(&b); }); } }; From cc558f1cf35e1f04ee27dfb5776937f5385ad442 Mon Sep 17 00:00:00 2001 From: Matthias Reumann Date: Tue, 24 Mar 2026 16:43:11 +0100 Subject: [PATCH 16/17] Fix linting --- mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index ef81658d17..3693b7ac6f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -833,8 +833,6 @@ struct MappingPass : impl::MappingPassBase { // Apply the sequence of SWAPs and rewire the qubit SSA values. for (const auto& [hw0, hw1] : swaps) { - Operation* op0 = wires[hw0].operation(); - Operation* op1 = wires[hw1].operation(); const auto in0 = wires[hw0].qubit(); const auto in1 = wires[hw1].qubit(); From febc127f675fcd01003cfcd9666a84cb3bd438a4 Mon Sep 17 00:00:00 2001 From: Matthias Reumann Date: Wed, 25 Mar 2026 07:54:44 +0100 Subject: [PATCH 17/17] Add root node check --- mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 3693b7ac6f..ef861b88b0 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -598,7 +598,12 @@ struct MappingPass : impl::MappingPassBase { llvm::SpecificBumpPtrAllocator arena; std::priority_queue, Node::ComparePointer> frontier; - frontier.emplace(std::construct_at(arena.Allocate(), layout)); + + Node* root = std::construct_at(arena.Allocate(), layout); + if (root->isGoal(layers.front(), arch)) { + return SmallVector{}; + } + frontier.emplace(root); DenseMap bestDepth; DenseSet expansionSet; @@ -630,10 +635,10 @@ struct MappingPass : impl::MappingPassBase { if (curr->isGoal(layers.front(), arch)) { SmallVector seq(curr->depth); - std::size_t i = seq.size() - 1; + std::size_t j = seq.size() - 1; for (Node* n = curr; n->parent != nullptr; n = n->parent) { - seq[i] = n->swap; - --i; + seq[j] = n->swap; + --j; } return seq; }