Skip to content

Commit 2fdc815

Browse files
MatthiasReumannpre-commit-ci[bot]burgholzer
authored
✨Implement Unitized Routing Passes (#1301)
## Description This pull request implements an "unit"-ized approach to route quantum-classical programs. Furthermore, it solves the issue that the def-use and textual (program) operation order is not _that_ compatible. ### Unit An Unit divides a quantum-classical program into routable sections: ``` ┌─────┐ │unit | └─────┘ ---------------- divider (`scf.for`) ┃ ┌─────┐ ┃ |unit | // loop-body ┃ └─────┘ <-- `restore=true` ┌─────┐ │unit | └─────┘ ---------------- divider (`scf.if`) ┃ ┌─────┐ ┃ |unit | // then-body ┃ └─────┘<-- `restore=true` ┣ ┃ ┌─────┐ ┃ │unit | // else-body ┃ └─────┘<-- `restore=true` ┌─────┐ │unit | └─────┘ ``` Each unit is routed separately. This PR introduces two modes of traversal for such units: - `SequentialUnit`: Traverses the ops in the unit sequentially (Naive Routing) - `LayeredUnit`: Traverses the ops layer-by-layer (A* Routing) ## Changes - Split naive (`--route-naive-sc`) from A* routing pass (`--route-astar-sc`): This makes sense from a logical as well as a pass option perspective. The pass options for the A* routing pass belong to the A* routing pass. If we were to implement another method, let's say SABRE, its options would again reside in a separate pass. - Implement `SequentialUnit` and `LayeredUnit`. - Refactor A* and Naive routing passes to use the respective unit implementation. - Fix SSA Dominance Issues by implementing the reordering strategy discussed below. - Use `SequentialUnit` also for verification ## Checklist: <!--- This checklist serves as a reminder of a couple of things that ensure your pull request will be merged swiftly. --> - [x] The pull request only contains commits that are focused and relevant to this change. - [x] I have added appropriate tests that cover the new/changed functionality. - [x] I have updated the documentation to reflect these changes. - [x] I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals. - [x] I have added migration instructions to the upgrade guide (if needed). - [x] The changes follow the project's style guidelines and introduce no new warnings. - [x] The changes are fully tested and pass the CI checks. - [x] I have reviewed my own code changes. [^1]: Switching the custom stack for recursive function calls can probably also be applied to the placement and verification pass. Something for a future PR. --------- Signed-off-by: matthias <matthias@bereumann.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lukas Burgholzer <burgholzer@me.com>
1 parent e30566c commit 2fdc815

25 files changed

Lines changed: 1429 additions & 1255 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel
2020

2121
### Changed
2222

23+
- ♻️ Group circuit operations into scheduling units for MLIR routing ([#1301]) ([**@MatthiasReumann**])
2324
- 👷 Use `munich-quantum-software/setup-mlir` to set up MLIR ([#1294]) ([**@denialhaag**])
2425
- ♻️ Preserve tuple structure and improve site type clarity of the MQT NA Default QDMI Device ([#1299]) ([**@marcelwa**])
2526
- ♻️ Move DD package evaluation module to standalone script ([#1327]) ([**@burgholzer**])
@@ -266,6 +267,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool
266267
[#1336]: https://github.com/munich-quantum-toolkit/core/pull/1336
267268
[#1327]: https://github.com/munich-quantum-toolkit/core/pull/1327
268269
[#1310]: https://github.com/munich-quantum-toolkit/core/pull/1310
270+
[#1301]: https://github.com/munich-quantum-toolkit/core/pull/1301
269271
[#1300]: https://github.com/munich-quantum-toolkit/core/pull/1300
270272
[#1299]: https://github.com/munich-quantum-toolkit/core/pull/1299
271273
[#1294]: https://github.com/munich-quantum-toolkit/core/pull/1294

mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ class RewritePatternSet;
2929
namespace mqt::ir::opt {
3030

3131
enum class PlacementStrategy : std::uint8_t { Random, Identity };
32-
enum class RoutingMethod : std::uint8_t { Naive, AStar };
3332

3433
#define GEN_PASS_DECL
3534
#include "mlir/Dialect/MQTOpt/Transforms/Passes.h.inc" // IWYU pragma: export

mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.td

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def ReuseQubitsPass : Pass<"reuse-qubits", "mlir::ModuleOp"> {
113113
//===----------------------------------------------------------------------===//
114114

115115
def PlacementPassSC : Pass<"placement-sc", "mlir::ModuleOp"> {
116-
let summary = "This pass maps dynamic qubits to static qubits on superconducting quantum devices using initial placement strategies.";
116+
let summary = "This pass maps program qubits to hardware qubits on superconducting quantum devices using initial placement strategies.";
117117
let options = [
118118
Option<"strategy", "strategy", "PlacementStrategy", "PlacementStrategy::Random",
119119
"The initial placement strategy to use.", [{llvm::cl::values(
@@ -124,16 +124,27 @@ def PlacementPassSC : Pass<"placement-sc", "mlir::ModuleOp"> {
124124
];
125125
}
126126

127-
def RoutingPassSC : Pass<"route-sc", "mlir::ModuleOp"> {
128-
let summary = "This pass ensures that a program meets the connectivity constraints of a given architecture.";
127+
def NaiveRoutingPassSC : Pass<"route-naive-sc", "mlir::ModuleOp"> {
128+
let summary = "This pass ensures that all two-qubit gates are executable on the target architecture.";
129129
let description = [{
130-
This pass inserts SWAP operations to ensure two-qubit gates are executable on a given target architecture.
130+
Simple pre-order traversal of the IR that routes any non-executable gates by inserting SWAPs along the shortest path.
131+
}];
132+
let options = [
133+
Option<"archName", "arch", "std::string", "",
134+
"The name of the targeted architecture.">,
135+
];
136+
let statistics = [
137+
Statistic<"numSwaps", "num-additional-swaps", "The number of additional SWAPs">
138+
];
139+
}
140+
141+
def AStarRoutingPassSC : Pass<"route-astar-sc", "mlir::ModuleOp"> {
142+
let summary = "This pass ensures that all two-qubit gates are executable on the target architecture.";
143+
let description = [{
144+
Routes the program by dividing the circuit into layers of parallel two-qubit gates and iteratively searches and
145+
inserts SWAPs for each layer using A*-search.
131146
}];
132147
let options = [
133-
Option<"method", "method", "RoutingMethod", "RoutingMethod::AStar",
134-
"The routing method to use.", [{llvm::cl::values(
135-
clEnumValN(RoutingMethod::Naive, "naive", "Swap along shortest paths"),
136-
clEnumValN(RoutingMethod::AStar, "astar", "A*-search-based routing algorithm"))}]>,
137148
Option<"archName", "arch", "std::string", "",
138149
"The name of the targeted architecture.">,
139150
Option<"nlookahead", "nlookahead", "std::size_t", "1",
@@ -149,7 +160,7 @@ def RoutingPassSC : Pass<"route-sc", "mlir::ModuleOp"> {
149160
}
150161

151162
def RoutingVerificationSCPass : Pass<"verify-routing-sc", "mlir::ModuleOp"> {
152-
let summary = "This pass verifies that a program meets the connectivity constraints of a given architecture.";
163+
let summary = "This pass verifies that all two-qubit gates are executable on the target architecture.";
153164
let description = [{
154165
This pass ensures that all two-qubit gates are executable on the target's architecture.
155166
}];

mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Architecture.h

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,20 @@
1010

1111
#pragma once
1212

13-
#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Common.h"
13+
#include "mlir/Dialect/MQTOpt/IR/MQTOptDialect.h"
14+
#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h"
1415

1516
#include <cstddef>
1617
#include <cstdint>
18+
#include <llvm/ADT/DenseMapInfo.h>
1719
#include <llvm/ADT/DenseSet.h>
1820
#include <llvm/ADT/SmallVector.h>
1921
#include <llvm/ADT/StringRef.h>
2022
#include <memory>
23+
#include <mlir/Support/LLVM.h>
2124
#include <string>
2225
#include <string_view>
26+
#include <utility>
2327

2428
namespace mqt::ir::opt {
2529

@@ -29,8 +33,8 @@ namespace mqt::ir::opt {
2933
*/
3034
class Architecture {
3135
public:
32-
using CouplingSet = mlir::DenseSet<std::pair<QubitIndex, QubitIndex>>;
33-
using NeighbourVector = mlir::SmallVector<mlir::SmallVector<QubitIndex, 4>>;
36+
using CouplingSet = mlir::DenseSet<std::pair<uint32_t, uint32_t>>;
37+
using NeighbourVector = mlir::SmallVector<mlir::SmallVector<uint32_t, 4>>;
3438

3539
explicit Architecture(std::string name, std::size_t nqubits,
3640
CouplingSet couplingSet)
@@ -55,27 +59,33 @@ class Architecture {
5559
/**
5660
* @brief Return true if @p u and @p v are adjacent.
5761
*/
58-
[[nodiscard]] bool areAdjacent(QubitIndex u, QubitIndex v) const {
62+
[[nodiscard]] bool areAdjacent(uint32_t u, uint32_t v) const {
5963
return couplingSet_.contains({u, v});
6064
}
6165

6266
/**
63-
* @brief Collect the shortest path between @p u and @p v.
64-
* @returns The path from the destination (v) to source (u) qubit.
67+
* @brief Collect the shortest SWAP sequence to make @p u and @p v adjacent.
68+
* @returns The SWAP sequence from the destination (v) to source (u) qubit.
6569
*/
66-
[[nodiscard]] llvm::SmallVector<std::size_t>
67-
shortestPathBetween(QubitIndex u, QubitIndex v) const;
70+
[[nodiscard]] llvm::SmallVector<std::pair<uint32_t, uint32_t>>
71+
shortestSWAPsBetween(uint32_t u, uint32_t v) const;
6872

6973
/**
7074
* @brief Return the length of the shortest path between @p u and @p v.
7175
*/
72-
[[nodiscard]] std::size_t distanceBetween(QubitIndex u, QubitIndex v) const;
76+
[[nodiscard]] std::size_t distanceBetween(uint32_t u, uint32_t v) const;
7377

7478
/**
7579
* @brief Collect all neighbours of @p u.
7680
*/
77-
[[nodiscard]] llvm::SmallVector<QubitIndex, 4>
78-
neighboursOf(QubitIndex u) const;
81+
[[nodiscard]] llvm::SmallVector<uint32_t, 4> neighboursOf(uint32_t u) const;
82+
83+
/**
84+
* @brief Validate if a two-qubit op is executable on the architecture for a
85+
* given layout.
86+
*/
87+
[[nodiscard]] bool isExecutable(UnitaryInterface op,
88+
const Layout& layout) const;
7989

8090
private:
8191
using Matrix = llvm::SmallVector<llvm::SmallVector<std::size_t>>;

mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Common.h

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,20 @@
1111
#pragma once
1212

1313
#include "mlir/Dialect/MQTOpt/IR/MQTOptDialect.h"
14+
#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h"
1415

16+
#include <concepts>
1517
#include <cstddef>
18+
#include <cstdint>
1619
#include <llvm/ADT/StringRef.h>
1720
#include <mlir/Dialect/Func/IR/FuncOps.h>
1821
#include <mlir/IR/BuiltinAttributes.h>
22+
#include <mlir/IR/PatternMatch.h>
1923
#include <mlir/IR/Value.h>
24+
#include <ranges>
2025
#include <utility>
2126

2227
namespace mqt::ir::opt {
23-
/**
24-
* @brief 'For' pushes once onto the stack, hence the parent is at depth one.
25-
*/
26-
constexpr std::size_t FOR_PARENT_DEPTH = 1UL;
27-
28-
/**
29-
* @brief 'If' pushes twice onto the stack, hence the parent is at depth two.
30-
*/
31-
constexpr std::size_t IF_PARENT_DEPTH = 2UL;
32-
33-
/**
34-
* @brief Type alias for qubit indices.
35-
*/
36-
using QubitIndex = uint32_t;
3728

3829
/**
3930
* @brief A pair of SSA Values.
@@ -43,7 +34,7 @@ using ValuePair = std::pair<mlir::Value, mlir::Value>;
4334
/**
4435
* @brief Represents a pair of qubit indices.
4536
*/
46-
using QubitIndexPair = std::pair<QubitIndex, QubitIndex>;
37+
using QubitIndexPair = std::pair<uint32_t, uint32_t>;
4738

4839
/**
4940
* @brief Return true if the function contains "entry_point" in the passthrough
@@ -80,4 +71,66 @@ using QubitIndexPair = std::pair<QubitIndex, QubitIndex>;
8071
*/
8172
[[nodiscard]] mlir::Operation* getUserInRegion(mlir::Value v,
8273
mlir::Region* region);
74+
75+
/**
76+
* @brief Create and return SWAPOp for two qubits.
77+
*
78+
* Expects the rewriter to be set to the correct position.
79+
*
80+
* @param location The Location to attach to the created op.
81+
* @param in0 First input qubit SSA value.
82+
* @param in1 Second input qubit SSA value.
83+
* @param rewriter A PatternRewriter.
84+
* @return The created SWAPOp.
85+
*/
86+
[[nodiscard]] SWAPOp createSwap(mlir::Location location, mlir::Value in0,
87+
mlir::Value in1,
88+
mlir::PatternRewriter& rewriter);
89+
90+
/**
91+
* @brief Replace all uses of a value within a region and its nested regions,
92+
* except for a specific operation.
93+
*
94+
* @param oldValue The value to replace.
95+
* @param newValue The new value to use.
96+
* @param region The region in which to perform replacements.
97+
* @param exceptOp Operation to exclude from replacements.
98+
* @param rewriter The pattern rewriter.
99+
*/
100+
void replaceAllUsesInRegionAndChildrenExcept(mlir::Value oldValue,
101+
mlir::Value newValue,
102+
mlir::Region* region,
103+
mlir::Operation* exceptOp,
104+
mlir::PatternRewriter& rewriter);
105+
106+
/**
107+
* @brief Insert SWAP ops at the rewriter's insertion point.
108+
*
109+
* @param loc The location of the inserted SWAP ops.
110+
* @param swaps A range of hardware indices for the SWAPs.
111+
* @param layout The current layout.
112+
* @param rewriter The pattern rewriter.
113+
*/
114+
template <typename Range>
115+
requires std::same_as<std::ranges::range_value_t<Range>, QubitIndexPair>
116+
void insertSWAPs(mlir::Location loc, Range&& swaps, Layout& layout,
117+
mlir::PatternRewriter& rewriter) {
118+
for (const auto [hw0, hw1] : std::forward<Range>(swaps)) {
119+
const mlir::Value in0 = layout.lookupHardwareValue(hw0);
120+
const mlir::Value in1 = layout.lookupHardwareValue(hw1);
121+
122+
auto swap = createSwap(loc, in0, in1, rewriter);
123+
124+
rewriter.setInsertionPointAfter(swap);
125+
126+
mlir::Region* region = swap->getParentRegion();
127+
mlir::Value out0 = swap.getOutQubits()[0];
128+
mlir::Value out1 = swap.getOutQubits()[1];
129+
130+
replaceAllUsesInRegionAndChildrenExcept(in0, out1, region, swap, rewriter);
131+
replaceAllUsesInRegionAndChildrenExcept(in1, out0, region, swap, rewriter);
132+
133+
layout.remap(swap);
134+
}
135+
}
83136
} // namespace mqt::ir::opt
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright (c) 2023 - 2025 Chair for Design Automation, TUM
3+
* Copyright (c) 2025 Munich Quantum Software Company GmbH
4+
* All rights reserved.
5+
*
6+
* SPDX-License-Identifier: MIT
7+
*
8+
* Licensed under the MIT License
9+
*/
10+
11+
#pragma once
12+
13+
#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Common.h"
14+
#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h"
15+
#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Unit.h"
16+
17+
#include <cstddef>
18+
#include <llvm/ADT/STLExtras.h>
19+
#include <llvm/Support/Debug.h>
20+
#include <mlir/Support/LLVM.h>
21+
22+
namespace mqt::ir::opt {
23+
24+
struct Layer {
25+
/// @brief All (zero, one, two-qubit) ops contained inside this layer.
26+
mlir::SmallVector<mlir::Operation*, 64> ops;
27+
/// @brief The program index pairs of all two-qubit ops.
28+
mlir::SmallVector<QubitIndexPair, 16> twoQubitProgs;
29+
/// @brief The first op in ops in textual IR order.
30+
mlir::Operation* anchor{};
31+
32+
/// @brief Add op to ops and reset anchor if necessary.
33+
void addOp(mlir::Operation* op) {
34+
ops.emplace_back(op);
35+
if (anchor == nullptr || op->isBeforeInBlock(anchor)) {
36+
anchor = op;
37+
}
38+
}
39+
/// @returns true iff. there are no ops in this layer.
40+
[[nodiscard]] bool hasZeroOps() const { return ops.empty(); }
41+
/// @returns true iff. there are no two-qubit ops in this layer.
42+
[[nodiscard]] bool hasZero2QOps() const { return twoQubitProgs.empty(); }
43+
};
44+
45+
/// @brief A LayeredUnit traverses a program layer-by-layer.
46+
class LayeredUnit : public Unit {
47+
public:
48+
using Layers = mlir::SmallVector<Layer, 0>;
49+
50+
[[nodiscard]] static LayeredUnit
51+
fromEntryPointFunction(mlir::func::FuncOp func, std::size_t nqubits);
52+
53+
LayeredUnit(Layout layout, mlir::Region* region, bool restore = false);
54+
55+
[[nodiscard]] mlir::SmallVector<LayeredUnit, 3> next();
56+
[[nodiscard]] Layers::const_iterator begin() const { return layers_.begin(); }
57+
[[nodiscard]] Layers::const_iterator end() const { return layers_.end(); }
58+
[[nodiscard]] const Layer& operator[](std::size_t i) const {
59+
return layers_[i];
60+
}
61+
[[nodiscard]] std::size_t size() const { return layers_.size(); }
62+
63+
#ifndef NDEBUG
64+
LLVM_DUMP_METHOD void dump(llvm::raw_ostream& os = llvm::dbgs()) const;
65+
#endif
66+
67+
private:
68+
Layers layers_;
69+
};
70+
} // namespace mqt::ir::opt

0 commit comments

Comments
 (0)