diff --git a/CHANGELOG.md b/CHANGELOG.md index 173a0c3d6b..e7f018b1ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel ### Changed +- ♻️ Group circuit operations into scheduling units for MLIR routing ([#1301]) ([**@MatthiasReumann**]) - 👷 Use `munich-quantum-software/setup-mlir` to set up MLIR ([#1294]) ([**@denialhaag**]) - ♻️ Preserve tuple structure and improve site type clarity of the MQT NA Default QDMI Device ([#1299]) ([**@marcelwa**]) - ♻️ 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 [#1336]: https://github.com/munich-quantum-toolkit/core/pull/1336 [#1327]: https://github.com/munich-quantum-toolkit/core/pull/1327 [#1310]: https://github.com/munich-quantum-toolkit/core/pull/1310 +[#1301]: https://github.com/munich-quantum-toolkit/core/pull/1301 [#1300]: https://github.com/munich-quantum-toolkit/core/pull/1300 [#1299]: https://github.com/munich-quantum-toolkit/core/pull/1299 [#1294]: https://github.com/munich-quantum-toolkit/core/pull/1294 diff --git a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.h b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.h index 5b5593092b..5bc05293c5 100644 --- a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.h +++ b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.h @@ -29,7 +29,6 @@ class RewritePatternSet; namespace mqt::ir::opt { enum class PlacementStrategy : std::uint8_t { Random, Identity }; -enum class RoutingMethod : std::uint8_t { Naive, AStar }; #define GEN_PASS_DECL #include "mlir/Dialect/MQTOpt/Transforms/Passes.h.inc" // IWYU pragma: export diff --git a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.td b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.td index bdcc5138e6..b16059771f 100644 --- a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.td @@ -113,7 +113,7 @@ def ReuseQubitsPass : Pass<"reuse-qubits", "mlir::ModuleOp"> { //===----------------------------------------------------------------------===// def PlacementPassSC : Pass<"placement-sc", "mlir::ModuleOp"> { - let summary = "This pass maps dynamic qubits to static qubits on superconducting quantum devices using initial placement strategies."; + let summary = "This pass maps program qubits to hardware qubits on superconducting quantum devices using initial placement strategies."; let options = [ Option<"strategy", "strategy", "PlacementStrategy", "PlacementStrategy::Random", "The initial placement strategy to use.", [{llvm::cl::values( @@ -124,16 +124,27 @@ def PlacementPassSC : Pass<"placement-sc", "mlir::ModuleOp"> { ]; } -def RoutingPassSC : Pass<"route-sc", "mlir::ModuleOp"> { - let summary = "This pass ensures that a program meets the connectivity constraints of a given architecture."; +def NaiveRoutingPassSC : Pass<"route-naive-sc", "mlir::ModuleOp"> { + let summary = "This pass ensures that all two-qubit gates are executable on the target architecture."; let description = [{ - This pass inserts SWAP operations to ensure two-qubit gates are executable on a given target architecture. + Simple pre-order traversal of the IR that routes any non-executable gates by inserting SWAPs along the shortest path. + }]; + let options = [ + Option<"archName", "arch", "std::string", "", + "The name of the targeted architecture.">, + ]; + let statistics = [ + Statistic<"numSwaps", "num-additional-swaps", "The number of additional SWAPs"> + ]; +} + +def AStarRoutingPassSC : Pass<"route-astar-sc", "mlir::ModuleOp"> { + let summary = "This pass ensures that all two-qubit gates are executable on the target architecture."; + let description = [{ + Routes the program by dividing the circuit into layers of parallel two-qubit gates and iteratively searches and + inserts SWAPs for each layer using A*-search. }]; let options = [ - Option<"method", "method", "RoutingMethod", "RoutingMethod::AStar", - "The routing method to use.", [{llvm::cl::values( - clEnumValN(RoutingMethod::Naive, "naive", "Swap along shortest paths"), - clEnumValN(RoutingMethod::AStar, "astar", "A*-search-based routing algorithm"))}]>, Option<"archName", "arch", "std::string", "", "The name of the targeted architecture.">, Option<"nlookahead", "nlookahead", "std::size_t", "1", @@ -149,7 +160,7 @@ def RoutingPassSC : Pass<"route-sc", "mlir::ModuleOp"> { } def RoutingVerificationSCPass : Pass<"verify-routing-sc", "mlir::ModuleOp"> { - let summary = "This pass verifies that a program meets the connectivity constraints of a given architecture."; + let summary = "This pass verifies that all two-qubit gates are executable on the target architecture."; let description = [{ This pass ensures that all two-qubit gates are executable on the target's architecture. }]; diff --git a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Architecture.h b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Architecture.h index 597ce55fb8..9e0a0d11f2 100644 --- a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Architecture.h +++ b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Architecture.h @@ -10,16 +10,20 @@ #pragma once -#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Common.h" +#include "mlir/Dialect/MQTOpt/IR/MQTOptDialect.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h" #include #include +#include #include #include #include #include +#include #include #include +#include namespace mqt::ir::opt { @@ -29,8 +33,8 @@ namespace mqt::ir::opt { */ class Architecture { public: - using CouplingSet = mlir::DenseSet>; - using NeighbourVector = mlir::SmallVector>; + using CouplingSet = mlir::DenseSet>; + using NeighbourVector = mlir::SmallVector>; explicit Architecture(std::string name, std::size_t nqubits, CouplingSet couplingSet) @@ -55,27 +59,33 @@ class Architecture { /** * @brief Return true if @p u and @p v are adjacent. */ - [[nodiscard]] bool areAdjacent(QubitIndex u, QubitIndex v) const { + [[nodiscard]] bool areAdjacent(uint32_t u, uint32_t v) const { return couplingSet_.contains({u, v}); } /** - * @brief Collect the shortest path between @p u and @p v. - * @returns The path from the destination (v) to source (u) qubit. + * @brief Collect the shortest SWAP sequence to make @p u and @p v adjacent. + * @returns The SWAP sequence from the destination (v) to source (u) qubit. */ - [[nodiscard]] llvm::SmallVector - shortestPathBetween(QubitIndex u, QubitIndex v) const; + [[nodiscard]] llvm::SmallVector> + shortestSWAPsBetween(uint32_t u, uint32_t v) const; /** * @brief Return the length of the shortest path between @p u and @p v. */ - [[nodiscard]] std::size_t distanceBetween(QubitIndex u, QubitIndex v) const; + [[nodiscard]] std::size_t distanceBetween(uint32_t u, uint32_t v) const; /** * @brief Collect all neighbours of @p u. */ - [[nodiscard]] llvm::SmallVector - neighboursOf(QubitIndex u) const; + [[nodiscard]] llvm::SmallVector neighboursOf(uint32_t u) const; + + /** + * @brief Validate if a two-qubit op is executable on the architecture for a + * given layout. + */ + [[nodiscard]] bool isExecutable(UnitaryInterface op, + const Layout& layout) const; private: using Matrix = llvm::SmallVector>; diff --git a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Common.h b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Common.h index 4a9dc7ab6a..959873f1e2 100644 --- a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Common.h +++ b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Common.h @@ -11,29 +11,20 @@ #pragma once #include "mlir/Dialect/MQTOpt/IR/MQTOptDialect.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h" +#include #include +#include #include #include #include +#include #include +#include #include namespace mqt::ir::opt { -/** - * @brief 'For' pushes once onto the stack, hence the parent is at depth one. - */ -constexpr std::size_t FOR_PARENT_DEPTH = 1UL; - -/** - * @brief 'If' pushes twice onto the stack, hence the parent is at depth two. - */ -constexpr std::size_t IF_PARENT_DEPTH = 2UL; - -/** - * @brief Type alias for qubit indices. - */ -using QubitIndex = uint32_t; /** * @brief A pair of SSA Values. @@ -43,7 +34,7 @@ using ValuePair = std::pair; /** * @brief Represents a pair of qubit indices. */ -using QubitIndexPair = std::pair; +using QubitIndexPair = std::pair; /** * @brief Return true if the function contains "entry_point" in the passthrough @@ -80,4 +71,66 @@ using QubitIndexPair = std::pair; */ [[nodiscard]] mlir::Operation* getUserInRegion(mlir::Value v, mlir::Region* region); + +/** + * @brief Create and return SWAPOp for two qubits. + * + * Expects the rewriter to be set to the correct position. + * + * @param location The Location to attach to the created op. + * @param in0 First input qubit SSA value. + * @param in1 Second input qubit SSA value. + * @param rewriter A PatternRewriter. + * @return The created SWAPOp. + */ +[[nodiscard]] SWAPOp createSwap(mlir::Location location, mlir::Value in0, + mlir::Value in1, + mlir::PatternRewriter& rewriter); + +/** + * @brief Replace all uses of a value within a region and its nested regions, + * except for a specific operation. + * + * @param oldValue The value to replace. + * @param newValue The new value to use. + * @param region The region in which to perform replacements. + * @param exceptOp Operation to exclude from replacements. + * @param rewriter The pattern rewriter. + */ +void replaceAllUsesInRegionAndChildrenExcept(mlir::Value oldValue, + mlir::Value newValue, + mlir::Region* region, + mlir::Operation* exceptOp, + mlir::PatternRewriter& rewriter); + +/** + * @brief Insert SWAP ops at the rewriter's insertion point. + * + * @param loc The location of the inserted SWAP ops. + * @param swaps A range of hardware indices for the SWAPs. + * @param layout The current layout. + * @param rewriter The pattern rewriter. + */ +template + requires std::same_as, QubitIndexPair> +void insertSWAPs(mlir::Location loc, Range&& swaps, Layout& layout, + mlir::PatternRewriter& rewriter) { + for (const auto [hw0, hw1] : std::forward(swaps)) { + const mlir::Value in0 = layout.lookupHardwareValue(hw0); + const mlir::Value in1 = layout.lookupHardwareValue(hw1); + + auto swap = createSwap(loc, in0, in1, rewriter); + + rewriter.setInsertionPointAfter(swap); + + mlir::Region* region = swap->getParentRegion(); + mlir::Value out0 = swap.getOutQubits()[0]; + mlir::Value out1 = swap.getOutQubits()[1]; + + replaceAllUsesInRegionAndChildrenExcept(in0, out1, region, swap, rewriter); + replaceAllUsesInRegionAndChildrenExcept(in1, out0, region, swap, rewriter); + + layout.remap(swap); + } +} } // namespace mqt::ir::opt diff --git a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/LayeredUnit.h b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/LayeredUnit.h new file mode 100644 index 0000000000..804eb9a76b --- /dev/null +++ b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/LayeredUnit.h @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Common.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Unit.h" + +#include +#include +#include +#include + +namespace mqt::ir::opt { + +struct Layer { + /// @brief All (zero, one, two-qubit) ops contained inside this layer. + mlir::SmallVector ops; + /// @brief The program index pairs of all two-qubit ops. + mlir::SmallVector twoQubitProgs; + /// @brief The first op in ops in textual IR order. + mlir::Operation* anchor{}; + + /// @brief Add op to ops and reset anchor if necessary. + void addOp(mlir::Operation* op) { + ops.emplace_back(op); + if (anchor == nullptr || op->isBeforeInBlock(anchor)) { + anchor = op; + } + } + /// @returns true iff. there are no ops in this layer. + [[nodiscard]] bool hasZeroOps() const { return ops.empty(); } + /// @returns true iff. there are no two-qubit ops in this layer. + [[nodiscard]] bool hasZero2QOps() const { return twoQubitProgs.empty(); } +}; + +/// @brief A LayeredUnit traverses a program layer-by-layer. +class LayeredUnit : public Unit { +public: + using Layers = mlir::SmallVector; + + [[nodiscard]] static LayeredUnit + fromEntryPointFunction(mlir::func::FuncOp func, std::size_t nqubits); + + LayeredUnit(Layout layout, mlir::Region* region, bool restore = false); + + [[nodiscard]] mlir::SmallVector next(); + [[nodiscard]] Layers::const_iterator begin() const { return layers_.begin(); } + [[nodiscard]] Layers::const_iterator end() const { return layers_.end(); } + [[nodiscard]] const Layer& operator[](std::size_t i) const { + return layers_[i]; + } + [[nodiscard]] std::size_t size() const { return layers_.size(); } + +#ifndef NDEBUG + LLVM_DUMP_METHOD void dump(llvm::raw_ostream& os = llvm::dbgs()) const; +#endif + +private: + Layers layers_; +}; +} // namespace mqt::ir::opt diff --git a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h index 91d43f4458..ae77d656e8 100644 --- a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h +++ b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h @@ -10,16 +10,18 @@ #pragma once -#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Common.h" +#include "mlir/Dialect/MQTOpt/IR/MQTOptDialect.h" +#include #include #include +#include +#include #include #include #include namespace mqt::ir::opt { -using namespace mlir; /** * @brief A qubit layout that maps program and hardware indices without storing @@ -39,7 +41,7 @@ class [[nodiscard]] ThinLayout { * @param prog The program index. * @param hw The hardware index. */ - void add(QubitIndex prog, QubitIndex hw) { + void add(uint32_t prog, uint32_t hw) { assert(prog < programToHardware_.size() && "add: program index out of bounds"); assert(hw < hardwareToProgram_.size() && @@ -53,7 +55,7 @@ class [[nodiscard]] ThinLayout { * @param hw The hardware index. * @return The program index of the respective hardware index. */ - [[nodiscard]] QubitIndex getProgramIndex(const QubitIndex hw) const { + [[nodiscard]] uint32_t getProgramIndex(const uint32_t hw) const { assert(hw < hardwareToProgram_.size() && "getProgramIndex: hardware index out of bounds"); return hardwareToProgram_[hw]; @@ -64,7 +66,7 @@ class [[nodiscard]] ThinLayout { * @param prog The program index. * @return The hardware index of the respective program index. */ - [[nodiscard]] QubitIndex getHardwareIndex(const QubitIndex prog) const { + [[nodiscard]] uint32_t getHardwareIndex(const uint32_t prog) const { assert(prog < programToHardware_.size() && "getHardwareIndex: program index out of bounds"); return programToHardware_[prog]; @@ -77,9 +79,9 @@ class [[nodiscard]] ThinLayout { */ template requires(sizeof...(ProgIndices) > 0) && - ((std::is_convertible_v) && ...) + ((std::is_convertible_v) && ...) [[nodiscard]] auto getHardwareIndices(ProgIndices... progs) const { - return std::tuple{getHardwareIndex(static_cast(progs))...}; + return std::tuple{getHardwareIndex(static_cast(progs))...}; } /** @@ -89,32 +91,39 @@ class [[nodiscard]] ThinLayout { */ template requires(sizeof...(HwIndices) > 0) && - ((std::is_convertible_v) && ...) + ((std::is_convertible_v) && ...) [[nodiscard]] auto getProgramIndices(HwIndices... hws) const { - return std::tuple{getProgramIndex(static_cast(hws))...}; + return std::tuple{getProgramIndex(static_cast(hws))...}; } /** * @brief Swap the mapping to hardware indices of two program indices. */ - void swap(const QubitIndex prog0, const QubitIndex prog1) { - const QubitIndex hw0 = programToHardware_[prog0]; - const QubitIndex hw1 = programToHardware_[prog1]; + void swap(const uint32_t prog0, const uint32_t prog1) { + const uint32_t hw0 = programToHardware_[prog0]; + const uint32_t hw1 = programToHardware_[prog1]; std::swap(programToHardware_[prog0], programToHardware_[prog1]); std::swap(hardwareToProgram_[hw0], hardwareToProgram_[hw1]); } + /** + * @returns the number of qubits handled by the layout. + */ + [[nodiscard]] std::size_t getNumQubits() const { + return programToHardware_.size(); + } + protected: /** * @brief Maps a program qubit index to its hardware index. */ - SmallVector programToHardware_; + mlir::SmallVector programToHardware_; /** * @brief Maps a hardware qubit index to its program index. */ - SmallVector hardwareToProgram_; + mlir::SmallVector hardwareToProgram_; private: friend struct llvm::DenseMapInfo; @@ -122,8 +131,7 @@ class [[nodiscard]] ThinLayout { /** * @brief Enhanced layout that extends ThinLayout with Value tracking - * capabilities. This is the recommended replacement for the original Layout - * class. + * capabilities. */ class [[nodiscard]] Layout : public ThinLayout { public: @@ -138,7 +146,7 @@ class [[nodiscard]] Layout : public ThinLayout { * @param hw The hardware index. * @param q The SSA value associated with the indices. */ - void add(QubitIndex prog, QubitIndex hw, Value q) { + void add(uint32_t prog, uint32_t hw, mlir::Value q) { ThinLayout::add(prog, hw); qubits_[hw] = q; valueToMapping_.try_emplace(q, prog, hw); @@ -149,7 +157,7 @@ class [[nodiscard]] Layout : public ThinLayout { * @param q The SSA Value representing the qubit. * @return The hardware index where this qubit currently resides. */ - [[nodiscard]] QubitIndex lookupHardwareIndex(const Value q) const { + [[nodiscard]] uint32_t lookupHardwareIndex(const mlir::Value q) const { const auto it = valueToMapping_.find(q); assert(it != valueToMapping_.end() && "lookupHardwareIndex: unknown value"); return it->second.hw; @@ -161,7 +169,7 @@ class [[nodiscard]] Layout : public ThinLayout { * @return The SSA value currently representing the qubit at the hardware * location. */ - [[nodiscard]] Value lookupHardwareValue(const QubitIndex hw) const { + [[nodiscard]] mlir::Value lookupHardwareValue(const uint32_t hw) const { assert(hw < qubits_.size() && "lookupHardwareValue: hardware index out of bounds"); return qubits_[hw]; @@ -172,7 +180,7 @@ class [[nodiscard]] Layout : public ThinLayout { * @param q The SSA Value representing the qubit. * @return The program index where this qubit currently resides. */ - [[nodiscard]] QubitIndex lookupProgramIndex(const Value q) const { + [[nodiscard]] uint32_t lookupProgramIndex(const mlir::Value q) const { const auto it = valueToMapping_.find(q); assert(it != valueToMapping_.end() && "lookupProgramIndex: unknown value"); return it->second.prog; @@ -184,7 +192,7 @@ class [[nodiscard]] Layout : public ThinLayout { * @return The SSA value currently representing the qubit at the program * location. */ - [[nodiscard]] Value lookupProgramValue(const QubitIndex prog) const { + [[nodiscard]] mlir::Value lookupProgramValue(const uint32_t prog) const { assert(prog < this->programToHardware_.size() && "lookupProgramValue: program index out of bounds"); return qubits_[this->programToHardware_[prog]]; @@ -195,14 +203,14 @@ class [[nodiscard]] Layout : public ThinLayout { * @param q The SSA Value representing the qubit. * @return True if the layout contains the qubit, false otherwise. */ - [[nodiscard]] bool contains(const Value q) const { + [[nodiscard]] bool contains(const mlir::Value q) const { return valueToMapping_.contains(q); } /** * @brief Replace an old SSA value with a new one. */ - void remapQubitValue(const Value in, const Value out) { + void remapQubitValue(const mlir::Value in, const mlir::Value out) { const auto it = valueToMapping_.find(in); assert(it != valueToMapping_.end() && "remapQubitValue: unknown input value"); @@ -221,14 +229,14 @@ class [[nodiscard]] Layout : public ThinLayout { * @brief Swap the locations of two program qubits. This is the effect of a * SWAP gate. */ - void swap(const Value q0, const Value q1) { + void swap(const mlir::Value q0, const mlir::Value q1) { auto it0 = valueToMapping_.find(q0); auto it1 = valueToMapping_.find(q1); assert(it0 != valueToMapping_.end() && it1 != valueToMapping_.end() && "swap: unknown values"); - const QubitIndex prog0 = it0->second.prog; - const QubitIndex prog1 = it1->second.prog; + const uint32_t prog0 = it0->second.prog; + const uint32_t prog1 = it1->second.prog; std::swap(it0->second.prog, it1->second.prog); @@ -238,37 +246,116 @@ class [[nodiscard]] Layout : public ThinLayout { /** * @brief Return the current layout. */ - ArrayRef getCurrentLayout() const { + mlir::ArrayRef getCurrentLayout() const { return this->programToHardware_; } /** * @brief Return the SSA values for hardware indices from 0...nqubits. */ - [[nodiscard]] ArrayRef getHardwareQubits() const { return qubits_; } + [[nodiscard]] mlir::ArrayRef getHardwareQubits() const { + return qubits_; + } + + /** + * @brief Remap all input to output qubits for the given unitary op. + * + * If the unitary op is a SWAP, exchange the respective program qubits. + * + * @param op The unitary op. + */ + void remap(UnitaryInterface op) { + if (mlir::isa(op)) { + swap(op.getInQubits()[0], op.getInQubits()[1]); + } + + for (const auto& [in, out] : + llvm::zip_equal(op.getAllInQubits(), op.getAllOutQubits())) { + remapQubitValue(in, out); + } + } + + /** + * @brief Remap input to output qubit for the given reset op. + * + * @param op The reset op. + * @param layout The current layout. + */ + void remap(ResetOp op) { remapQubitValue(op.getInQubit(), op.getOutQubit()); } + + /** + * @brief Remap input to output qubit for the given measure op. + * + * @param op The measure op. + */ + void remap(MeasureOp op) { + remapQubitValue(op.getInQubit(), op.getOutQubit()); + } + + /** + * @brief Remap input qubits to in-loop-body values (iteration args). + * + * @param op The 'scf.for' op. + */ + void remapToLoopBody(mlir::scf::ForOp op) { + const auto nqubits = getNumQubits(); + const auto args = op.getInitArgs().take_front(nqubits); + const auto iterArgs = op.getRegionIterArgs().take_front(nqubits); + for (const auto [arg, iter] : llvm::zip(args, iterArgs)) { + remapQubitValue(arg, iter); + } + } + + /** + * @brief Remap input qubits to out-of-loop values (results). + * + * @param op The 'scf.for' op. + */ + void remapToLoopResults(mlir::scf::ForOp op) { + const auto nqubits = getNumQubits(); + const auto args = op.getInitArgs().take_front(nqubits); + const auto results = op.getResults().take_front(nqubits); + for (const auto [arg, iter] : llvm::zip(args, results)) { + remapQubitValue(arg, iter); + } + } + + /** + * @brief Remap current qubit values to if results. + * + * @param op The 'scf.if' op. + */ + void remapIfResults(mlir::scf::IfOp op) { + const auto nqubits = getNumQubits(); + const auto results = op->getResults().take_front(nqubits); + for (const auto [in, out] : llvm::zip(getHardwareQubits(), results)) { + remapQubitValue(in, out); + } + } private: struct QubitInfo { - QubitIndex prog; - QubitIndex hw; + uint32_t prog; + uint32_t hw; }; /** * @brief Maps an SSA value to its `QubitInfo`. */ - DenseMap valueToMapping_; + mlir::DenseMap valueToMapping_; /** * @brief Maps hardware qubit indices to SSA values. */ - SmallVector qubits_; + mlir::SmallVector qubits_; }; + } // namespace mqt::ir::opt namespace llvm { template <> struct DenseMapInfo { using Layout = mqt::ir::opt::ThinLayout; - using VectorInfo = DenseMapInfo>; + using VectorInfo = DenseMapInfo>; static Layout getEmptyKey() { Layout layout(0); diff --git a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Router.h b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Router.h index 88b6698c25..5a618ed092 100644 --- a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Router.h +++ b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Router.h @@ -13,63 +13,33 @@ #include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Architecture.h" #include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Common.h" #include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h" -#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Scheduler.h" #include +#include #include +#include #include -#include #include #include namespace mqt::ir::opt { -/** - * @brief A vector of SWAPs. - */ -using RouterResult = SmallVector; - -/** - * @brief A planner determines the sequence of swaps required to route an array -of gates. -*/ -struct RouterBase { - virtual ~RouterBase() = default; - [[nodiscard]] virtual RouterResult route(const Layers&, const ThinLayout&, - const Architecture&) const = 0; -}; - -/** - * @brief Use shortest path swapping to make one gate executable. - */ -struct NaiveRouter final : RouterBase { - [[nodiscard]] RouterResult route(const Layers& layers, - const ThinLayout& layout, - const Architecture& arch) const override { - if (layers.size() != 1 || layers.front().size() != 1) { - throw std::invalid_argument( - "NaiveRouter expects exactly one layer with one gate"); - } - - /// This assumes an avg. of 16 SWAPs per gate. - SmallVector swaps; - for (const auto [prog0, prog1] : layers.front()) { - const auto [hw0, hw1] = layout.getHardwareIndices(prog0, prog1); - const auto path = arch.shortestPathBetween(hw0, hw1); - for (std::size_t i = 0; i < path.size() - 2; ++i) { - swaps.emplace_back(path[i], path[i + 1]); - } - } - return swaps; +class NaiveRouter { +public: + [[nodiscard]] static mlir::SmallVector + route(QubitIndexPair gate, const ThinLayout& layout, + const Architecture& arch) { + mlir::SmallVector swaps; + const auto hw0 = layout.getHardwareIndex(gate.first); + const auto hw1 = layout.getHardwareIndex(gate.second); + return arch.shortestSWAPsBetween(hw0, hw1); } }; -/** - * @brief Specifies the weights for different terms in the cost function f. - */ +/// @brief Specifies the weights for different terms in the cost function f. struct HeuristicWeights { float alpha; - SmallVector lambdas; + mlir::SmallVector lambdas; HeuristicWeights(const float alpha, const float lambda, const std::size_t nlookahead) @@ -81,18 +51,14 @@ struct HeuristicWeights { } }; -/** - * @brief Use A*-search to make all gates executable. - */ -struct AStarHeuristicRouter final : RouterBase { +class AStarHeuristicRouter { +public: explicit AStarHeuristicRouter(HeuristicWeights weights) : weights_(std::move(weights)) {} private: - using ClosedMap = DenseMap; - struct Node { - SmallVector sequence; + mlir::SmallVector sequence; ThinLayout layout; float f; @@ -106,7 +72,8 @@ struct AStarHeuristicRouter final : RouterBase { * @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. */ - Node(const Node& parent, QubitIndexPair swap, const Layers& layers, + Node(const Node& parent, QubitIndexPair swap, + mlir::SmallVector> window, const Architecture& arch, const HeuristicWeights& weights) : sequence(parent.sequence), layout(parent.layout), f(0) { /// Apply node-specific swap to given layout. @@ -117,16 +84,16 @@ struct AStarHeuristicRouter final : RouterBase { sequence.push_back(swap); /// Evaluate cost function. - f = g(weights) + h(layers, arch, weights); // NOLINT + f = g(weights) + h(window, arch, weights); // NOLINT } /** * @brief Return true if the current sequence of SWAPs makes all gates * executable. */ - [[nodiscard]] bool isGoal(const ArrayRef& gates, + [[nodiscard]] bool isGoal(mlir::ArrayRef layer, const Architecture& arch) const { - return std::ranges::all_of(gates, [&](const QubitIndexPair gate) { + return std::ranges::all_of(layer, [&](const QubitIndexPair gate) { return arch.areAdjacent(layout.getHardwareIndex(gate.first), layout.getHardwareIndex(gate.second)); }); @@ -158,14 +125,14 @@ struct AStarHeuristicRouter final : RouterBase { * its hardware qubits. Intuitively, this is the number of SWAPs that a * naive router would insert to route the layers. */ - [[nodiscard]] float h(const Layers& layers, const Architecture& arch, - const HeuristicWeights& weights) const { + [[nodiscard]] float + h(mlir::SmallVector> window, + const Architecture& arch, const HeuristicWeights& weights) const { float nn{0}; - for (const auto [i, layer] : llvm::enumerate(layers)) { + for (const auto [i, layer] : llvm::enumerate(window)) { for (const auto [prog0, prog1] : layer) { const auto [hw0, hw1] = layout.getHardwareIndices(prog0, prog1); - const std::size_t dist = arch.distanceBetween(hw0, hw1); - const std::size_t nswaps = dist < 2 ? 0 : dist - 2; + const std::size_t nswaps = arch.distanceBetween(hw0, hw1) - 1; nn += weights.lambdas[i] * static_cast(nswaps); } } @@ -176,67 +143,60 @@ struct AStarHeuristicRouter final : RouterBase { using MinQueue = std::priority_queue, std::greater<>>; public: - [[nodiscard]] RouterResult route(const Layers& layers, - const ThinLayout& layout, - const Architecture& arch) const override { + [[nodiscard]] std::optional> + route(mlir::SmallVector> window, + const ThinLayout& layout, const Architecture& arch) const { Node root(layout); /// Early exit. No SWAPs required: - if (root.isGoal(layers.front(), arch)) { - return {}; + if (root.isGoal(window.front(), arch)) { + return mlir::SmallVector{}; } /// Initialize queue. MinQueue frontier{}; frontier.emplace(root); - /// Initialize visited map. - ClosedMap visited; - /// Iterative searching and expanding. while (!frontier.empty()) { Node curr = frontier.top(); frontier.pop(); - if (curr.isGoal(layers.front(), arch)) { + if (curr.isGoal(window.front(), arch)) { return curr.sequence; } - /// Don't revisit layouts that were discovered with a lower depth. - const auto [it, inserted] = - visited.try_emplace(curr.layout, curr.depth()); - if (!inserted) { - if (it->second <= curr.depth()) { - continue; - } - it->second = curr.sequence.size(); - } - /// Expand frontier with all neighbouring SWAPs in the current front. - expand(frontier, curr, layers, arch); + expand(frontier, curr, window, arch); } - return {}; + return std::nullopt; } private: - /** - * @brief Expand frontier with all neighbouring SWAPs in the current front. - */ - void expand(MinQueue& frontier, const Node& parent, const Layers& layers, + /// @brief Expand frontier with all neighbouring SWAPs in the current front. + void expand(MinQueue& frontier, const Node& parent, + mlir::SmallVector> window, const Architecture& arch) const { - llvm::SmallDenseSet swaps{}; - for (const QubitIndexPair gate : layers.front()) { + llvm::SmallDenseSet expansionSet{}; + + /// Currently: Don't revert last SWAP. + /// TODO: Idea? Don't revert "front" (independent) SWAPs? + if (!parent.sequence.empty()) { + expansionSet.insert(parent.sequence.back()); + } + + for (const QubitIndexPair gate : window.front()) { for (const auto prog : {gate.first, gate.second}) { const auto hw0 = parent.layout.getHardwareIndex(prog); for (const auto hw1 : arch.neighboursOf(hw0)) { /// Ensure consistent hashing/comparison. const QubitIndexPair swap = std::minmax(hw0, hw1); - if (!swaps.insert(swap).second) { + if (!expansionSet.insert(swap).second) { continue; } - frontier.emplace(parent, swap, layers, arch, weights_); + frontier.emplace(parent, swap, window, arch, weights_); } } } @@ -244,4 +204,5 @@ struct AStarHeuristicRouter final : RouterBase { HeuristicWeights weights_; }; + } // namespace mqt::ir::opt diff --git a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Scheduler.h b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Scheduler.h deleted file mode 100644 index e64b729d9b..0000000000 --- a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Scheduler.h +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM - * Copyright (c) 2025 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "mlir/Dialect/MQTOpt/IR/MQTOptDialect.h" -#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Common.h" -#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h" - -#include "llvm/ADT/STLExtras.h" -#include "llvm/Support/ErrorHandling.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#define DEBUG_TYPE "route-sc" - -namespace mqt::ir::opt { - -/** - * @brief A vector of gates. - */ -using Layer = SmallVector; - -/** - * @brief A vector of layers. - * [0]=current, [1]=lookahead (optional), >=2 future layers - */ -using Layers = SmallVector; - -/** - * @brief A scheduler divides the circuit into routable sections. - */ -struct SchedulerBase { - virtual ~SchedulerBase() = default; - [[nodiscard]] virtual Layers schedule(UnitaryInterface op, - Layout layout) const = 0; -}; - -/** - * @brief A sequential scheduler simply returns the given op. - */ -struct SequentialOpScheduler final : SchedulerBase { - [[nodiscard]] Layers schedule(UnitaryInterface op, - Layout layout) const override { - const auto [in0, in1] = getIns(op); - return {{{layout.lookupProgramIndex(in0), layout.lookupProgramIndex(in1)}}}; - } -}; - -/** - * @brief A parallel scheduler collects 1 + nlookahead layers of parallelly - * executable gates. - */ -struct ParallelOpScheduler final : SchedulerBase { - explicit ParallelOpScheduler(const std::size_t nlookahead) - : nlayers_(1 + nlookahead) {} - - [[nodiscard]] Layers schedule(UnitaryInterface op, - Layout layout) const override { - Layers layers; - layers.reserve(nlayers_); - - /// Worklist of active qubits. - SmallVector wl; - SmallVector nextWl; - wl.reserve(layout.getHardwareQubits().size()); - nextWl.reserve(layout.getHardwareQubits().size()); - - // Initialize worklist. - llvm::copy_if(layout.getHardwareQubits(), std::back_inserter(wl), - [](Value q) { return !q.use_empty(); }); - - /// Set of two-qubit gates seen at least once. - llvm::SmallDenseSet openTwoQubit; - - /// Vector of two-qubit gates seen twice. - SmallVector readyTwoQubit; - - Region* region = op->getParentRegion(); - - while (!wl.empty() && layers.size() < nlayers_) { - for (const Value q : wl) { - const auto opt = advanceToTwoQubitGate(q, region, layout); - if (!opt) { - continue; - } - - const auto& [qNext, gate] = opt.value(); - - if (q != qNext) { - layout.remapQubitValue(q, qNext); - } - - if (!openTwoQubit.insert(gate).second) { - readyTwoQubit.push_back(gate); - openTwoQubit.erase(gate); - continue; - } - } - - if (readyTwoQubit.empty()) { - break; - } - - nextWl.clear(); - layers.emplace_back(); - layers.back().reserve(readyTwoQubit.size()); - - /// At this point all qubit values are remapped to the input of a - /// two-qubit gate: "value.user == two-qubit-gate". - /// - /// We release the ready two-qubit gates by forwarding their inputs - /// to their outputs. Then, we advance to the end of its two-qubit block. - /// Intuitively, whenever a gate in readyTwoQubit is routed, all one and - /// two-qubit gates acting on the same qubits are executable as well. - - for (const auto& op : readyTwoQubit) { - /// Release. - const ValuePair ins = getIns(op); - const ValuePair outs = getOuts(op); - layers.back().emplace_back(layout.lookupProgramIndex(ins.first), - layout.lookupProgramIndex(ins.second)); - layout.remapQubitValue(ins.first, outs.first); - layout.remapQubitValue(ins.second, outs.second); - - /// Advance two-qubit block. - std::array gates; - std::array heads{outs.first, outs.second}; - - while (true) { - bool stop = false; - for (const auto [i, q] : llvm::enumerate(heads)) { - const auto opt = advanceToTwoQubitGate(q, region, layout); - if (!opt) { - heads[i] = nullptr; - stop = true; - break; - } - - const auto& [qNext, gate] = opt.value(); - - if (q != qNext) { - layout.remapQubitValue(q, qNext); - } - - heads[i] = qNext; - gates[i] = gate; - } - - if (stop || gates[0] != gates[1]) { - break; - } - - const ValuePair ins = getIns(gates[0]); - const ValuePair outs = getOuts(gates[0]); - layout.remapQubitValue(ins.first, outs.first); - layout.remapQubitValue(ins.second, outs.second); - heads = {outs.first, outs.second}; - } - - /// Initialize next worklist. - for (const auto q : heads) { - if (q != nullptr && !q.use_empty()) { - nextWl.push_back(q); - } - } - } - - /// Prepare for next iteration. - readyTwoQubit.clear(); - wl = std::move(nextWl); - } - - LLVM_DEBUG({ - llvm::dbgs() << "schedule: layers=\n"; - for (const auto [i, layer] : llvm::enumerate(layers)) { - llvm::dbgs() << '\t' << i << "= "; - for (const auto [prog0, prog1] : layer) { - llvm::dbgs() << "(" << prog0 << "," << prog1 << "), "; - } - llvm::dbgs() << '\n'; - } - }); - - return layers; - } - -private: - /** - * @returns Next two-qubit gate on qubit wire, or std::nullopt if none exists. - */ - static std::optional> - advanceToTwoQubitGate(const Value q, Region* region, const Layout& layout) { - Value head = q; - while (true) { - if (head.use_empty()) { // No two-qubit gate found. - return std::nullopt; - } - - Operation* user = getUserInRegion(head, region); - if (user == nullptr) { // No two-qubit gate found. - return std::nullopt; - } - - bool endOfRegion = false; - std::optional twoQubitOp; - - TypeSwitch(user) - /// MQT - /// BarrierOp is a UnitaryInterface, however, requires special care. - .Case([&](BarrierOp op) { - for (const auto [in, out] : - llvm::zip_equal(op.getInQubits(), op.getOutQubits())) { - if (in == head) { - head = out; - return; - } - } - llvm_unreachable("head must be in barrier"); - }) - .Case([&](UnitaryInterface op) { - if (isTwoQubitGate(op)) { - twoQubitOp = op; - return; // Found a two-qubit gate, stop advancing head. - } - // Otherwise, advance head. - head = op.getOutQubits().front(); - }) - .Case([&](ResetOp op) { head = op.getOutQubit(); }) - .Case([&](MeasureOp op) { head = op.getOutQubit(); }) - /// SCF - /// The scf funcs assume that the first n results are the hw qubits. - .Case([&](scf::ForOp op) { - head = op->getResult(layout.lookupHardwareIndex(q)); - }) - .Case([&](scf::IfOp op) { - head = op->getResult(layout.lookupHardwareIndex(q)); - }) - .Case([&](scf::YieldOp) { endOfRegion = true; }) - .Default([&]([[maybe_unused]] Operation* op) { - LLVM_DEBUG({ - llvm::dbgs() << "unknown operation in def-use chain: "; - op->dump(); - }); - llvm_unreachable("unknown operation in def-use chain"); - }); - - if (twoQubitOp) { // Two-qubit gate found. - return std::make_pair(head, *twoQubitOp); - } - - if (endOfRegion) { - return std::nullopt; - } - } - - return std::nullopt; - } - - std::size_t nlayers_; -}; -} // namespace mqt::ir::opt diff --git a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/SequentialUnit.h b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/SequentialUnit.h new file mode 100644 index 0000000000..07accbc648 --- /dev/null +++ b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/SequentialUnit.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Unit.h" + +#include +#include +#include +#include +#include + +namespace mqt::ir::opt { + +/// @brief A SequentialUnit traverses a program sequentially. +class SequentialUnit : public Unit { +public: + [[nodiscard]] static SequentialUnit + fromEntryPointFunction(mlir::func::FuncOp func, std::size_t nqubits); + + SequentialUnit(Layout layout, mlir::Region* region, + mlir::Region::OpIterator start, bool restore = false); + + SequentialUnit(Layout layout, mlir::Region* region, bool restore = false) + : SequentialUnit(std::move(layout), region, region->op_begin(), restore) { + } + + [[nodiscard]] mlir::SmallVector next(); + [[nodiscard]] mlir::Region::OpIterator begin() const { return start_; } + [[nodiscard]] mlir::Region::OpIterator end() const { return end_; } + +private: + mlir::Region::OpIterator start_; + mlir::Region::OpIterator end_; +}; +} // namespace mqt::ir::opt diff --git a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Unit.h b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Unit.h new file mode 100644 index 0000000000..dbcb35ecbc --- /dev/null +++ b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Unit.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h" + +#include + +namespace mqt::ir::opt { + +/// @brief A Unit divides a quantum-classical program into routable sections. +class Unit { +public: + Unit(Layout layout, mlir::Region* region, bool restore = false) + : layout_(std::move(layout)), region_(region), restore_(restore) {} + + /// @returns the managed layout. + [[nodiscard]] Layout& layout() { return layout_; } + + /// @returns true iff. the unit has to be restored. + [[nodiscard]] bool restore() const { return restore_; } + +protected: + /// @brief The layout this unit manages. + Layout layout_; + /// @brief The region this unit belongs to. + mlir::Region* region_; + /// @brief Pointer to the next dividing operation. + mlir::Operation* divider_{}; + /// @brief Whether to uncompute the inserted SWAP sequence. + bool restore_; +}; + +} // namespace mqt::ir::opt diff --git a/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/Architecture.cpp b/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/Architecture.cpp index 74d3dcaa4f..c8da124504 100644 --- a/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/Architecture.cpp +++ b/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/Architecture.cpp @@ -10,8 +10,11 @@ #include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Architecture.h" +#include "mlir/Dialect/MQTOpt/IR/MQTOptDialect.h" #include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Common.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h" +#include #include #include #include @@ -19,28 +22,35 @@ #include #include #include +#include namespace mqt::ir::opt { -[[nodiscard]] llvm::SmallVector -Architecture::shortestPathBetween(QubitIndex u, QubitIndex v) const { - llvm::SmallVector path; + +llvm::SmallVector> +Architecture::shortestSWAPsBetween(uint32_t u, uint32_t v) const { + if (u == v) { + return {}; + } if (prev_[u][v] == UINT64_MAX) { throw std::domain_error("No path between qubits " + std::to_string(u) + " and " + std::to_string(v)); } - path.push_back(v); - while (u != v) { - v = prev_[u][v]; - path.push_back(v); + llvm::SmallVector> swaps; + uint32_t curr = v; + uint32_t last = v; + + while (curr != u) { + curr = prev_[u][curr]; + swaps.emplace_back(last, curr); + last = curr; } - return path; + return swaps; } -[[nodiscard]] std::size_t Architecture::distanceBetween(QubitIndex u, - QubitIndex v) const { +std::size_t Architecture::distanceBetween(uint32_t u, uint32_t v) const { if (dist_[u][v] == UINT64_MAX) { throw std::domain_error("No path between qubits " + std::to_string(u) + " and " + std::to_string(v)); @@ -80,11 +90,18 @@ void Architecture::collectNeighbours() { } } -[[nodiscard]] llvm::SmallVector -Architecture::neighboursOf(QubitIndex u) const { +llvm::SmallVector Architecture::neighboursOf(uint32_t u) const { return neighbours_[u]; } +bool Architecture::isExecutable(UnitaryInterface op, + const Layout& layout) const { + assert(isTwoQubitGate(op)); + const auto ins = getIns(op); + return areAdjacent(layout.lookupHardwareIndex(ins.first), + layout.lookupHardwareIndex(ins.second)); +} + std::unique_ptr getArchitecture(const llvm::StringRef name) { if (name == "MQTTest") { static const Architecture::CouplingSet COUPLING{ diff --git a/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/Common.cpp b/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/Common.cpp index c0432a7639..4b4d20f575 100644 --- a/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/Common.cpp +++ b/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/Common.cpp @@ -14,11 +14,12 @@ #include #include -#include -#include #include #include +#include +#include #include +#include #include namespace mqt::ir::opt { @@ -26,12 +27,12 @@ namespace { /** * @brief A function attribute that specifies an (QIR) entry point function. */ -constexpr llvm::StringLiteral ENTRY_POINT_ATTR{"entry_point"}; +constexpr mlir::StringLiteral ENTRY_POINT_ATTR{"entry_point"}; /** * @brief Attribute to forward function-level attributes to LLVM IR. */ -constexpr llvm::StringLiteral PASSTHROUGH_ATTR{"passthrough"}; +constexpr mlir::StringLiteral PASSTHROUGH_ATTR{"passthrough"}; } // namespace bool isEntryPoint(mlir::func::FuncOp op) { @@ -42,8 +43,8 @@ bool isEntryPoint(mlir::func::FuncOp op) { } return llvm::any_of(passthroughAttr, [](const mlir::Attribute attr) { - return isa(attr) && - cast(attr) == ENTRY_POINT_ATTR; + return mlir::isa(attr) && + mlir::cast(attr) == ENTRY_POINT_ATTR; }); } @@ -91,4 +92,50 @@ bool isTwoQubitGate(UnitaryInterface op) { } return nullptr; } + +[[nodiscard]] SWAPOp createSwap(mlir::Location location, mlir::Value in0, + mlir::Value in1, + mlir::PatternRewriter& rewriter) { + const mlir::SmallVector resultTypes{in0.getType(), in1.getType()}; + const mlir::SmallVector inQubits{in0, in1}; + + return rewriter.create( + /* location = */ location, + /* out_qubits = */ resultTypes, + /* pos_ctrl_out_qubits = */ mlir::TypeRange{}, + /* neg_ctrl_out_qubits = */ mlir::TypeRange{}, + /* static_params = */ nullptr, + /* params_mask = */ nullptr, + /* params = */ mlir::ValueRange{}, + /* in_qubits = */ inQubits, + /* pos_ctrl_in_qubits = */ mlir::ValueRange{}, + /* neg_ctrl_in_qubits = */ mlir::ValueRange{}); +} + +void replaceAllUsesInRegionAndChildrenExcept(mlir::Value oldValue, + mlir::Value newValue, + mlir::Region* region, + mlir::Operation* exceptOp, + mlir::PatternRewriter& rewriter) { + if (oldValue == newValue) { + return; + } + + rewriter.replaceUsesWithIf(oldValue, newValue, [&](mlir::OpOperand& use) { + mlir::Operation* user = use.getOwner(); + if (user == exceptOp) { + return false; + } + + // For other blocks, check if in region tree + mlir::Region* userRegion = user->getParentRegion(); + while (userRegion) { + if (userRegion == region) { + return true; + } + userRegion = userRegion->getParentRegion(); + } + return false; + }); +} } // namespace mqt::ir::opt diff --git a/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/LayeredUnit.cpp b/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/LayeredUnit.cpp new file mode 100644 index 0000000000..c976fec6bb --- /dev/null +++ b/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/LayeredUnit.cpp @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/LayeredUnit.h" + +#include "mlir/Dialect/MQTOpt/IR/MQTOptDialect.h" +#include "mlir/Dialect/MQTOpt/IR/WireIterator.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Common.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Unit.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mqt::ir::opt { +namespace { + +/// @brief A wire links a WireIterator to a program index. +struct Wire { + Wire(const WireIterator& it, uint32_t index) : it(it), index(index) {} + WireIterator it; + uint32_t index; +}; + +/// @brief Map to handle multi-qubit gates when traversing the def-use chain. +class SynchronizationMap { +public: + /// @returns true iff. the operation is contained in the map. + bool contains(mlir::Operation* op) const { return onHold.contains(op); } + + /// @brief Add op with respective wire and ref count to map. + void add(mlir::Operation* op, Wire wire, const std::size_t cnt) { + onHold.try_emplace(op, mlir::SmallVector{wire}); + /// 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(mlir::Operation* op, + Wire wire) { + assert(refCount.contains(op) && "expected sync map to contain op"); + + /// Add iterator for later release. + onHold[op].push_back(wire); + + /// Release iterators whenever the ref count reaches zero. + if (--refCount[op] == 0) { + return onHold[op]; + } + + return std::nullopt; + } + +private: + /// @brief Maps operations to to-be-released iterators. + mlir::DenseMap> onHold; + /// @brief Maps operations to ref counts. + mlir::DenseMap refCount; +}; + +mlir::SmallVector skipTwoQubitBlock(mlir::ArrayRef wires, + Layer& opLayer) { + assert(wires.size() == 2 && "expected two wires"); + + auto [it0, index0] = wires[0]; + auto [it1, index1] = wires[1]; + while (it0 != std::default_sentinel && it1 != std::default_sentinel) { + mlir::Operation* op0 = *it0; + if (!mlir::isa(op0) || mlir::isa(op0)) { + break; + } + + mlir::Operation* op1 = *it1; + if (!mlir::isa(op1) || mlir::isa(op1)) { + break; + } + + const UnitaryInterface u0 = cast(op0); + + /// Advance for single qubit gate on wire 0. + if (!isTwoQubitGate(u0)) { + opLayer.addOp(u0); + ++it0; + continue; + } + + const UnitaryInterface u1 = cast(op1); + + /// Advance for single qubit gate on wire 1. + if (!isTwoQubitGate(u1)) { + opLayer.addOp(u1); + ++it1; + continue; + } + + /// Stop if the wires reach different two qubit gates. + if (op0 != op1) { + break; + } + + /// Advance if u0 == u1. + opLayer.addOp(u1); + + ++it0; + ++it1; + } + + return {Wire(it0, index0), Wire(it1, index1)}; +} +} // namespace + +LayeredUnit LayeredUnit::fromEntryPointFunction(mlir::func::FuncOp func, + const std::size_t nqubits) { + Layout layout(nqubits); + for_each(func.getOps(), [&](QubitOp op) { + layout.add(op.getIndex(), op.getIndex(), op.getQubit()); + }); + return {std::move(layout), &func.getBody()}; +} + +LayeredUnit::LayeredUnit(Layout layout, mlir::Region* region, bool restore) + : Unit(std::move(layout), region, restore) { + SynchronizationMap sync; + + mlir::SmallVector curr; + mlir::SmallVector next; + curr.reserve(layout_.getNumQubits()); + next.reserve(layout_.getNumQubits()); + + for (const auto q : layout_.getHardwareQubits()) { + /// Increment the iterator here to skip the defining operation. + curr.emplace_back(++WireIterator(q, region_), + layout_.lookupProgramIndex(q)); + } + + while (true) { + + /// Advance each wire until (>=2)-qubit gates are found, collect the indices + /// of the respective two-qubit gates, and prepare iterators for next + /// iteration. + + Layer layer; + + bool haltOnWire{}; + + for (auto [it, index] : curr) { + while (it != std::default_sentinel) { + haltOnWire = + mlir::TypeSwitch(*it) + .Case([&](UnitaryInterface op) { + const auto nins = op.getInQubits().size() + + op.getPosCtrlInQubits().size() + + op.getNegCtrlInQubits().size(); + + /// Skip over one-qubit gates. Note: Might be a BarrierOp. + if (nins == 1) { + layer.addOp(op); + ++it; + return false; + } + + /// Otherwise, add it to the sync map. + if (!sync.contains(op)) { + sync.add(op, Wire(++it, index), nins); + return true; + } + + if (const auto iterators = + sync.visit(op, Wire(++it, index))) { + layer.addOp(op); + + // Is ready two-qubit unitary? + if (!mlir::isa(op)) { + layer.twoQubitProgs.emplace_back((*iterators)[0].index, + (*iterators)[1].index); + next.append(skipTwoQubitBlock(*iterators, layer)); + } else { + next.append(*iterators); + } + } + + return true; + }) + .Case([&](auto op) { + layer.addOp(op); + ++it; + return false; + }) + .Case([&](mlir::scf::YieldOp yield) { + if (!sync.contains(yield)) { + sync.add(yield, Wire(++it, index), layout_.getNumQubits()); + return true; + } + + if (const auto iterators = + sync.visit(yield, Wire(++it, index))) { + layer.addOp(yield); + } + + return true; + }) + .Case( + [&](mlir::RegionBranchOpInterface op) { + if (!sync.contains(op)) { + sync.add(op, Wire(++it, index), layout_.getNumQubits()); + return true; + } + + if (const auto iterators = + sync.visit(op, Wire(++it, index))) { + divider_ = op; + } + return true; + }) + .Default([](auto) { + llvm_unreachable("unhandled operation"); + return true; + }); + + if (haltOnWire) { + break; + } + } + + if (divider_ != nullptr) { + break; + } + } + + if (!layer.hasZeroOps()) { + if (!layer.hasZero2QOps() || layers_.empty()) { + layers_.emplace_back(layer); + } else { + /// If there is no gates to route, merge into the previous layer. + layers_.back().ops.append(layer.ops); + if (layers_.back().anchor == nullptr) { + layers_.back().anchor = layer.anchor; + } + } + } + + /// Prepare next iteration or stop. + curr.swap(next); + next.clear(); + + if (curr.empty() || divider_ != nullptr) { + break; + } + }; +} + +mlir::SmallVector LayeredUnit::next() { + if (divider_ == nullptr) { + return {}; + } + + mlir::SmallVector units; + mlir::TypeSwitch(divider_) + .Case([&](mlir::scf::ForOp op) { + Layout forLayout(layout_); // Copy layout. + forLayout.remapToLoopBody(op); + layout_.remapToLoopResults(op); + units.emplace_back(std::move(layout_), region_, restore_); + units.emplace_back(std::move(forLayout), &op.getRegion(), true); + }) + .Case([&](mlir::scf::IfOp op) { + units.emplace_back(layout_, &op.getThenRegion(), true); + units.emplace_back(layout_, &op.getElseRegion(), true); + layout_.remapIfResults(op); + units.emplace_back(layout_, region_, restore_); + }) + .Default([](auto) { llvm_unreachable("invalid 'next' operation"); }); + + return units; +} + +#ifndef NDEBUG +LLVM_DUMP_METHOD void LayeredUnit::dump(llvm::raw_ostream& os) const { + os << "schedule: layers=\n"; + for (const auto [i, layer] : llvm::enumerate(layers_)) { + os << '\t' << '[' << i << "]:\n"; + os << "\t #ops= " << layer.ops.size(); + if (!layer.ops.empty()) { + os << " anchor= " << layer.anchor->getLoc(); + } + os << '\n'; + os << "\t gates= "; + if (!layer.hasZero2QOps()) { + for (const auto [prog0, prog1] : layer.twoQubitProgs) { + os << "(" << prog0 << "," << prog1 << "), "; + } + } else { + os << "(), "; + } + os << '\n'; + } + if (divider_ != nullptr) { + os << "schedule: followUp= " << divider_->getLoc() << '\n'; + } +} +#endif +} // namespace mqt::ir::opt diff --git a/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/SequentialUnit.cpp b/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/SequentialUnit.cpp new file mode 100644 index 0000000000..3169cdf7e6 --- /dev/null +++ b/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/SequentialUnit.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/SequentialUnit.h" + +#include "mlir/Dialect/MQTOpt/IR/MQTOptDialect.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Unit.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mqt::ir::opt { + +SequentialUnit +SequentialUnit::fromEntryPointFunction(mlir::func::FuncOp func, + const std::size_t nqubits) { + Layout layout(nqubits); + for_each(func.getOps(), [&](QubitOp op) { + layout.add(op.getIndex(), op.getIndex(), op.getQubit()); + }); + return {std::move(layout), &func.getBody()}; +} + +SequentialUnit::SequentialUnit(Layout layout, mlir::Region* region, + mlir::Region::OpIterator start, bool restore) + : Unit(std::move(layout), region, restore), start_(start), + end_(region->op_end()) { + mlir::Region::OpIterator it = start_; + for (; it != end_; ++it) { + mlir::Operation* op = &*it; + if (mlir::isa(op)) { + divider_ = op; + break; + } + } + end_ = it; +} + +mlir::SmallVector SequentialUnit::next() { + if (divider_ == nullptr) { + return {}; + } + + mlir::SmallVector units; + mlir::TypeSwitch(divider_) + .Case([&](mlir::scf::ForOp op) { + Layout forLayout(layout_); // Copy layout. + forLayout.remapToLoopBody(op); + layout_.remapToLoopResults(op); + units.emplace_back(std::move(layout_), region_, std::next(end_), + restore_); + units.emplace_back(std::move(forLayout), &op.getRegion(), true); + }) + .Case([&](mlir::scf::IfOp op) { + units.emplace_back(layout_, &op.getThenRegion(), true); + units.emplace_back(layout_, &op.getElseRegion(), true); + layout_.remapIfResults(op); + units.emplace_back(std::move(layout_), region_, std::next(end_), + restore_); + }) + .Default([](auto) { llvm_unreachable("invalid 'next' operation"); }); + + return units; +} +} // namespace mqt::ir::opt diff --git a/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/AStarRoutingPass.cpp b/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/AStarRoutingPass.cpp new file mode 100644 index 0000000000..3d3770d41e --- /dev/null +++ b/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/AStarRoutingPass.cpp @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/MQTOpt/IR/MQTOptDialect.h" +#include "mlir/Dialect/MQTOpt/Transforms/Passes.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Architecture.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Common.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/LayeredUnit.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Router.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEBUG_TYPE "route-astar-sc" + +namespace mqt::ir::opt { + +#define GEN_PASS_DEF_ASTARROUTINGPASSSC +#include "mlir/Dialect/MQTOpt/Transforms/Passes.h.inc" + +namespace { +using namespace mlir; + +/** + * @brief Routes the program by dividing the circuit into layers of parallel + * two-qubit gates and iteratively searches and inserts SWAPs for each layer + * using A*-search. + */ +struct AStarRoutingPassSC final + : impl::AStarRoutingPassSCBase { + using AStarRoutingPassSCBase::AStarRoutingPassSCBase; + + void runOnOperation() override { + if (failed(preflight())) { + signalPassFailure(); + return; + } + + if (failed(route())) { + signalPassFailure(); + return; + } + } + +private: + /** + * @brief Route the given module for the targeted architecture using + * A*-search. Processes each entry_point function separately. + */ + LogicalResult route() { + ModuleOp module(getOperation()); + PatternRewriter rewriter(module->getContext()); + const AStarHeuristicRouter router( + HeuristicWeights(alpha, lambda, nlookahead)); + std::unique_ptr arch(getArchitecture(archName)); + + if (!arch) { + const Location loc = UnknownLoc::get(&getContext()); + emitError(loc) << "unsupported architecture '" << archName << "'"; + return failure(); + } + + for (auto func : module.getOps()) { + LLVM_DEBUG(llvm::dbgs() << "handleFunc: " << func.getSymName() << '\n'); + + if (!isEntryPoint(func)) { + LLVM_DEBUG(llvm::dbgs() << "\tskip non entry\n"); + continue; + } + + /// Iteratively process each unit in the function. + std::queue units; + units.emplace(LayeredUnit::fromEntryPointFunction(func, arch->nqubits())); + for (; !units.empty(); units.pop()) { + LayeredUnit& unit = units.front(); + LLVM_DEBUG(unit.dump()); + + SmallVector history; + for (const auto& [i, layer] : llvm::enumerate(unit)) { + + /// Compute sliding window. + const auto len = std::min(1 + nlookahead, unit.size() - i); + SmallVector> window; + window.reserve(len); + llvm::transform(ArrayRef(unit.begin(), unit.end()).slice(i, len), + std::back_inserter(window), [&](const Layer& l) { + return ArrayRef(l.twoQubitProgs); + }); + + /// Find and insert SWAPs. + rewriter.setInsertionPoint(layer.anchor); + const auto swaps = router.route(window, unit.layout(), *arch); + if (!swaps) { + const Location loc = UnknownLoc::get(&getContext()); + return emitError(loc, "A* failed to find a valid SWAP sequence"); + } + + if (!swaps->empty()) { + history.append(*swaps); + insertSWAPs(layer.anchor->getLoc(), *swaps, unit.layout(), + rewriter); + numSwaps += swaps->size(); + + LLVM_DEBUG({ + for (const auto [hw0, hw1] : *swaps) { + llvm::dbgs() + << llvm::format("route: swap= hw(%d, %d)\n", hw0, hw1); + } + }); + } + + /// Process all operations contained in the layer. + for (Operation* curr : layer.ops) { + rewriter.setInsertionPoint(curr); + + /// Re-order to fix any SSA Dominance issues. + if (i + 1 < unit.size()) { + rewriter.moveOpBefore(curr, unit[i + 1].anchor); + } + + /// Forward layout. + TypeSwitch(curr) + .Case([&](UnitaryInterface op) { + if (auto swap = dyn_cast(op.getOperation())) { + const auto in0 = swap.getInQubits()[0]; + const auto in1 = swap.getInQubits()[1]; + history.emplace_back( + unit.layout().lookupHardwareIndex(in0), + unit.layout().lookupHardwareIndex(in1)); + } + unit.layout().remap(op); + }) + .Case([&](ResetOp op) { unit.layout().remap(op); }) + .Case([&](MeasureOp op) { unit.layout().remap(op); }) + .Case([&](scf::YieldOp op) { + if (unit.restore()) { + rewriter.setInsertionPoint(op); + insertSWAPs(op.getLoc(), llvm::reverse(history), + unit.layout(), rewriter); + } + }) + .Default([](auto) { + llvm_unreachable("unhandled 'curr' operation"); + }); + } + } + + for (const auto& next : unit.next()) { + units.emplace(next); + } + } + } + + return success(); + } + + LogicalResult preflight() { + if (archName.empty()) { + const Location loc = UnknownLoc::get(&getContext()); + return emitError(loc, "required option 'arch' not provided"); + } + + return success(); + } +}; + +} // namespace +} // namespace mqt::ir::opt diff --git a/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/NaiveRoutingPass.cpp b/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/NaiveRoutingPass.cpp new file mode 100644 index 0000000000..32d4cb5d99 --- /dev/null +++ b/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/NaiveRoutingPass.cpp @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/MQTOpt/IR/MQTOptDialect.h" +#include "mlir/Dialect/MQTOpt/Transforms/Passes.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Architecture.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Common.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Router.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/SequentialUnit.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEBUG_TYPE "route-naive-sc" + +namespace mqt::ir::opt { + +#define GEN_PASS_DEF_NAIVEROUTINGPASSSC +#include "mlir/Dialect/MQTOpt/Transforms/Passes.h.inc" + +namespace { +using namespace mlir; + +/** + * @brief Simple pre-order traversal of the IR that routes any non-executable + * gates by inserting SWAPs along the shortest path. + */ +struct NaiveRoutingPassSC final + : impl::NaiveRoutingPassSCBase { + using NaiveRoutingPassSCBase::NaiveRoutingPassSCBase; + + void runOnOperation() override { + if (failed(preflight())) { + signalPassFailure(); + return; + } + + if (failed(route())) { + signalPassFailure(); + return; + } + } + +private: + LogicalResult route() { + ModuleOp module(getOperation()); + PatternRewriter rewriter(module->getContext()); + std::unique_ptr arch(getArchitecture(archName)); + + if (!arch) { + const Location loc = UnknownLoc::get(&getContext()); + emitError(loc) << "unsupported architecture '" << archName << "'"; + return failure(); + } + + for (auto func : module.getOps()) { + LLVM_DEBUG(llvm::dbgs() << "handleFunc: " << func.getSymName() << '\n'); + + if (!isEntryPoint(func)) { + LLVM_DEBUG(llvm::dbgs() << "\tskip non entry\n"); + continue; + } + + /// Iteratively process each unit in the function. + std::queue units; + units.emplace( + SequentialUnit::fromEntryPointFunction(func, arch->nqubits())); + for (; !units.empty(); units.pop()) { + SequentialUnit& unit = units.front(); + + SmallVector history; + for (Operation& curr : unit) { + rewriter.setInsertionPoint(&curr); + + /// Forward layout. + TypeSwitch(&curr) + .Case([&](UnitaryInterface op) { + if (isTwoQubitGate(op)) { + if (!arch->isExecutable(op, unit.layout())) { + const auto ins = getIns(op); + const auto gate = std::make_pair( + unit.layout().lookupProgramIndex(ins.first), + unit.layout().lookupProgramIndex(ins.second)); + const auto swaps = + NaiveRouter::route(gate, unit.layout(), *arch); + if (!swaps.empty()) { + history.append(swaps); + insertSWAPs(op->getLoc(), swaps, unit.layout(), rewriter); + numSwaps += swaps.size(); + + LLVM_DEBUG({ + for (const auto [hw0, hw1] : swaps) { + llvm::dbgs() << llvm::format( + "route: swap= hw(%d, %d)\n", hw0, hw1); + } + }); + } + } + } + + if (auto swap = dyn_cast(op.getOperation())) { + const auto in0 = swap.getInQubits()[0]; + const auto in1 = swap.getInQubits()[1]; + history.emplace_back(unit.layout().lookupHardwareIndex(in0), + unit.layout().lookupHardwareIndex(in1)); + } + unit.layout().remap(op); + }) + .Case([&](ResetOp op) { unit.layout().remap(op); }) + .Case([&](MeasureOp op) { unit.layout().remap(op); }) + .Case([&](scf::YieldOp op) { + if (unit.restore()) { + rewriter.setInsertionPointAfter(op->getPrevNode()); + insertSWAPs(op.getLoc(), llvm::reverse(history), + unit.layout(), rewriter); + } + }); + } + + for (const auto& next : unit.next()) { + units.emplace(next); + } + } + } + + return success(); + } + + LogicalResult preflight() { + if (archName.empty()) { + return emitError(UnknownLoc::get(&getContext()), + "required option 'arch' not provided"); + } + + return success(); + } +}; + +} // namespace +} // namespace mqt::ir::opt diff --git a/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/PlacementPass.cpp b/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/PlacementPass.cpp index 76b036738a..c82a55a56f 100644 --- a/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/PlacementPass.cpp +++ b/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/PlacementPass.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -51,10 +52,20 @@ namespace mqt::ir::opt { namespace { using namespace mlir; +/** + * @brief 'For' pushes once onto the stack, hence the parent is at depth one. + */ +constexpr std::size_t FOR_PARENT_DEPTH = 1UL; + +/** + * @brief 'If' pushes twice onto the stack, hence the parent is at depth two. + */ +constexpr std::size_t IF_PARENT_DEPTH = 2UL; + /** * @brief A queue of hardware indices. */ -using HardwareIndexPool = std::deque; +using HardwareIndexPool = std::deque; /** * @brief A base class for all initial placement strategies. @@ -67,7 +78,7 @@ class InitialPlacer { InitialPlacer(InitialPlacer&&) noexcept = default; InitialPlacer& operator=(InitialPlacer&&) noexcept = default; virtual ~InitialPlacer() = default; - [[nodiscard]] virtual SmallVector operator()() = 0; + [[nodiscard]] virtual SmallVector operator()() = 0; }; /** @@ -77,8 +88,8 @@ class IdentityPlacer final : public InitialPlacer { public: explicit IdentityPlacer(const std::size_t nqubits) : nqubits_(nqubits) {} - [[nodiscard]] SmallVector operator()() override { - SmallVector mapping(nqubits_); + [[nodiscard]] SmallVector operator()() override { + SmallVector mapping(nqubits_); std::iota(mapping.begin(), mapping.end(), 0); return mapping; } @@ -95,8 +106,8 @@ class RandomPlacer final : public InitialPlacer { explicit RandomPlacer(const std::size_t nqubits, const std::mt19937_64& rng) : nqubits_(nqubits), rng_(rng) {} - [[nodiscard]] SmallVector operator()() override { - SmallVector mapping(nqubits_); + [[nodiscard]] SmallVector operator()() override { + SmallVector mapping(nqubits_); std::iota(mapping.begin(), mapping.end(), 0); std::ranges::shuffle(mapping, rng_); return mapping; @@ -138,7 +149,7 @@ WalkResult handleFunc(func::FuncOp op, PlacementContext& ctx, /// Create static / hardware qubits for entry_point functions. SmallVector qubits(ctx.arch->nqubits()); - for (QubitIndex i = 0; i < ctx.arch->nqubits(); ++i) { + for (uint32_t i = 0; i < ctx.arch->nqubits(); ++i) { auto qubitOp = rewriter.create(rewriter.getInsertionPoint()->getLoc(), i); rewriter.setInsertionPointAfter(qubitOp); @@ -421,7 +432,7 @@ LogicalResult run(ModuleOp module, MLIRContext* mlirCtx, } /** - * @brief This pass maps dynamic qubits to static qubits on superconducting + * @brief This pass maps program qubits to hardware qubits on superconducting * quantum devices using initial placement strategies. */ struct PlacementPassSC final : impl::PlacementPassSCBase { diff --git a/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/RoutingPass.cpp b/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/RoutingPass.cpp deleted file mode 100644 index 1a120741c4..0000000000 --- a/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/RoutingPass.cpp +++ /dev/null @@ -1,549 +0,0 @@ -/* - * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM - * Copyright (c) 2025 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/MQTOpt/IR/MQTOptDialect.h" -#include "mlir/Dialect/MQTOpt/Transforms/Passes.h" -#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Architecture.h" -#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Common.h" -#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h" -#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Router.h" -#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Scheduler.h" -#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Stack.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define DEBUG_TYPE "route-sc" - -namespace mqt::ir::opt { - -#define GEN_PASS_DEF_ROUTINGPASSSC -#include "mlir/Dialect/MQTOpt/Transforms/Passes.h.inc" - -namespace { -using namespace mlir; - -/** - * @brief Create and return SWAPOp for two qubits. - * - * Expects the rewriter to be set to the correct position. - * - * @param location The Location to attach to the created op. - * @param in0 First input qubit SSA value. - * @param in1 Second input qubit SSA value. - * @param rewriter A PatternRewriter. - * @return The created SWAPOp. - */ -[[nodiscard]] SWAPOp createSwap(Location location, Value in0, Value in1, - PatternRewriter& rewriter) { - const SmallVector resultTypes{in0.getType(), in1.getType()}; - const SmallVector inQubits{in0, in1}; - - return rewriter.create( - /* location = */ location, - /* out_qubits = */ resultTypes, - /* pos_ctrl_out_qubits = */ TypeRange{}, - /* neg_ctrl_out_qubits = */ TypeRange{}, - /* static_params = */ nullptr, - /* params_mask = */ nullptr, - /* params = */ ValueRange{}, - /* in_qubits = */ inQubits, - /* pos_ctrl_in_qubits = */ ValueRange{}, - /* neg_ctrl_in_qubits = */ ValueRange{}); -} - -/** - * @brief Replace all uses of a value within a region and its nested regions, - * except for a specific operation. - * - * @param oldValue The value to replace - * @param newValue The new value to use - * @param region The region in which to perform replacements - * @param exceptOp Operation to exclude from replacements - * @param rewriter The pattern rewriter - */ -void replaceAllUsesInRegionAndChildrenExcept(Value oldValue, Value newValue, - Region* region, - Operation* exceptOp, - PatternRewriter& rewriter) { - if (oldValue == newValue) { - return; - } - - rewriter.replaceUsesWithIf(oldValue, newValue, [&](OpOperand& use) { - Operation* user = use.getOwner(); - if (user == exceptOp) { - return false; - } - - // For other blocks, check if in region tree - Region* userRegion = user->getParentRegion(); - while (userRegion) { - if (userRegion == region) { - return true; - } - userRegion = userRegion->getParentRegion(); - } - return false; - }); -} - -class Mapper { -public: - explicit Mapper(std::unique_ptr arch, - std::unique_ptr scheduler, - std::unique_ptr router, Pass::Statistic& numSwaps) - : arch_(std::move(arch)), scheduler_(std::move(scheduler)), - router_(std::move(router)), numSwaps_(&numSwaps) {} - - /** - * @returns true iff @p op is executable on the targeted architecture. - */ - [[nodiscard]] bool isExecutable(UnitaryInterface op) { - const auto [in0, in1] = getIns(op); - return arch().areAdjacent(stack().top().lookupHardwareIndex(in0), - stack().top().lookupHardwareIndex(in1)); - } - - /** - * @brief Insert SWAPs such that the gates provided by the scheduler are - * executable. - */ - void map(UnitaryInterface op, PatternRewriter& rewriter) { - const auto layers = scheduler_->schedule(op, stack().top()); - const auto swaps = router_->route(layers, stack().top(), arch()); - insert(swaps, op->getLoc(), rewriter); - historyStack_.top().append(swaps.begin(), swaps.end()); - } - - /** - * @brief Restore layout by uncomputing. - * - * History is cleared by the caller (e.g., via stack/history pop in handlers). - * - * @todo Remove SWAP history and use advanced strategies. - */ - void restore(Location location, PatternRewriter& rewriter) { - const auto swaps = llvm::to_vector(llvm::reverse(historyStack_.top())); - insert(swaps, location, rewriter); - } - - /** - * @returns reference to the stack object. - */ - [[nodiscard]] LayoutStack& stack() { return stack_; } - - /** - * @returns reference to the history stack object. - */ - [[nodiscard]] LayoutStack>& historyStack() { - return historyStack_; - } - - /** - * @returns reference to architecture object. - */ - [[nodiscard]] Architecture& arch() const { return *arch_; } - -private: - void insert(ArrayRef swaps, Location location, - PatternRewriter& rewriter) { - for (const auto [hw0, hw1] : swaps) { - const Value in0 = stack().top().lookupHardwareValue(hw0); - const Value in1 = stack().top().lookupHardwareValue(hw1); - [[maybe_unused]] const auto [prog0, prog1] = - stack().top().getProgramIndices(hw0, hw1); - - LLVM_DEBUG({ - llvm::dbgs() << llvm::format( - "route: swap= p%d:h%d, p%d:h%d <- p%d:h%d, p%d:h%d\n", prog1, hw0, - prog0, hw1, prog0, hw0, prog1, hw1); - }); - - auto swap = createSwap(location, in0, in1, rewriter); - const auto [out0, out1] = getOuts(swap); - - rewriter.setInsertionPointAfter(swap); - replaceAllUsesInRegionAndChildrenExcept( - in0, out1, swap->getParentRegion(), swap, rewriter); - replaceAllUsesInRegionAndChildrenExcept( - in1, out0, swap->getParentRegion(), swap, rewriter); - - stack().top().swap(in0, in1); - stack().top().remapQubitValue(in0, out0); - stack().top().remapQubitValue(in1, out1); - - (*numSwaps_)++; - } - } - - std::unique_ptr arch_; - std::unique_ptr scheduler_; - std::unique_ptr router_; - - LayoutStack stack_{}; - LayoutStack> historyStack_{}; - - Pass::Statistic* numSwaps_; -}; - -/** - * @brief Push new state onto the stack. - */ -WalkResult handleFunc([[maybe_unused]] func::FuncOp op, Mapper& mapper) { - assert(mapper.stack().empty() && "handleFunc: stack must be empty"); - - LLVM_DEBUG({ - llvm::dbgs() << "handleFunc: entry_point= " << op.getSymName() << '\n'; - }); - - /// Function body state. - mapper.stack().emplace(mapper.arch().nqubits()); - mapper.historyStack().emplace(); - - return WalkResult::advance(); -} - -/** - * @brief Indicates the end of a region defined by a function. Consequently, - * we pop the region's state from the stack. - */ -WalkResult handleReturn(Mapper& mapper) { - mapper.stack().pop(); - mapper.historyStack().pop(); - return WalkResult::advance(); -} - -/** - * @brief Push new state for the loop body onto the stack. - */ -WalkResult handleFor(scf::ForOp op, Mapper& mapper) { - /// Loop body state. - mapper.stack().duplicateTop(); - mapper.historyStack().emplace(); - - /// Forward out-of-loop and in-loop values. - const auto initArgs = op.getInitArgs().take_front(mapper.arch().nqubits()); - const auto results = op.getResults().take_front(mapper.arch().nqubits()); - const auto iterArgs = - op.getRegionIterArgs().take_front(mapper.arch().nqubits()); - for (const auto [arg, res, iter] : llvm::zip(initArgs, results, iterArgs)) { - mapper.stack().getItemAtDepth(FOR_PARENT_DEPTH).remapQubitValue(arg, res); - mapper.stack().top().remapQubitValue(arg, iter); - } - - return WalkResult::advance(); -} - -/** - * @brief Push two new states for the then and else branches onto the stack. - */ -WalkResult handleIf(scf::IfOp op, Mapper& mapper) { - /// Prepare stack. - mapper.stack().duplicateTop(); /// Else. - mapper.stack().duplicateTop(); /// Then. - mapper.historyStack().emplace(); - mapper.historyStack().emplace(); - - /// Forward out-of-if values. - const auto results = op->getResults().take_front(mapper.arch().nqubits()); - Layout& layoutBeforeIf = mapper.stack().getItemAtDepth(IF_PARENT_DEPTH); - for (const auto [hw, res] : llvm::enumerate(results)) { - const Value q = layoutBeforeIf.lookupHardwareValue(hw); - layoutBeforeIf.remapQubitValue(q, res); - } - - return WalkResult::advance(); -} - -/** - * @brief Indicates the end of a region defined by a branching op. - * Consequently, we pop the region's state from the stack. - * - * Restores layout by uncomputation and replaces (invalid) yield. - * - * Using uncompute has the advantages of (1) being intuitive and - * (2) preserving the optimality of the original SWAP sequence. - * Essentially the better the routing algorithm the better the - * uncompute. Moreover, this has the nice property that routing - * a 'for' of 'if' region always requires 2 * #(SWAPs required for region) - * additional SWAPS. - */ -WalkResult handleYield(scf::YieldOp op, Mapper& mapper, - PatternRewriter& rewriter) { - if (!isa(op->getParentOp()) && - !isa(op->getParentOp())) { - return WalkResult::skip(); - } - - mapper.restore(op->getLoc(), rewriter); - mapper.stack().pop(); - mapper.historyStack().pop(); - - return WalkResult::advance(); -} - -/** - * @brief Add hardware qubit with respective program & hardware index to - * layout. - * - * Thanks to the placement pass, we can apply the identity layout here. - */ -WalkResult handleQubit(QubitOp op, Mapper& mapper) { - const std::size_t index = op.getIndex(); - mapper.stack().top().add(index, index, op.getQubit()); - return WalkResult::advance(); -} - -/** - * @brief Ensures the executability of two-qubit gates on the given target - * architecture by inserting SWAPs. - */ -WalkResult handleUnitary(UnitaryInterface op, Mapper& mapper, - PatternRewriter& rewriter) { - const std::vector inQubits = op.getAllInQubits(); - const std::vector outQubits = op.getAllOutQubits(); - const std::size_t nacts = inQubits.size(); - - // Global-phase or zero-qubit unitary: Nothing to do. - if (nacts == 0) { - return WalkResult::advance(); - } - - if (isa(op)) { - for (const auto [in, out] : llvm::zip(inQubits, outQubits)) { - mapper.stack().top().remapQubitValue(in, out); - } - return WalkResult::advance(); - } - - /// Expect two-qubit gate decomposition. - if (nacts > 2) { - return op->emitOpError() << "acts on more than two qubits"; - } - - /// Single-qubit: Forward mapping. - if (nacts == 1) { - mapper.stack().top().remapQubitValue(inQubits[0], outQubits[0]); - return WalkResult::advance(); - } - - if (!mapper.isExecutable(op)) { - mapper.map(op, rewriter); - } - - const auto [execIn0, execIn1] = getIns(op); - const auto [execOut0, execOut1] = getOuts(op); - - LLVM_DEBUG({ - llvm::dbgs() << llvm::format( - "handleUnitary: gate= p%d:h%d, p%d:h%d\n", - mapper.stack().top().lookupProgramIndex(execIn0), - mapper.stack().top().lookupHardwareIndex(execIn0), - mapper.stack().top().lookupProgramIndex(execIn1), - mapper.stack().top().lookupHardwareIndex(execIn1)); - }); - - if (isa(op)) { - mapper.stack().top().swap(execIn0, execIn1); - mapper.historyStack().top().push_back( - {mapper.stack().top().lookupHardwareIndex(execIn0), - mapper.stack().top().lookupHardwareIndex(execIn1)}); - } - - mapper.stack().top().remapQubitValue(execIn0, execOut0); - mapper.stack().top().remapQubitValue(execIn1, execOut1); - - return WalkResult::advance(); -} - -/** - * @brief Update layout. - */ -WalkResult handleReset(ResetOp op, Mapper& mapper) { - mapper.stack().top().remapQubitValue(op.getInQubit(), op.getOutQubit()); - return WalkResult::advance(); -} - -/** - * @brief Update layout. - */ -WalkResult handleMeasure(MeasureOp op, Mapper& mapper) { - mapper.stack().top().remapQubitValue(op.getInQubit(), op.getOutQubit()); - return WalkResult::advance(); -} - -/** - * @brief Route the given module by inserting SWAPs. - * - * @details - * Collects all functions marked with the 'entry_point' attribute, builds a - * preorder worklist of their operations, and processes that list. Each - * operation is handled via a TypeSwitch and may rewrite the IR in place via - * the provided PatternRewriter. If any handler signals an error (interrupt), - * this function returns failure. - * - * @note - * We consciously avoid MLIR pattern drivers: Idiomatic MLIR transformation - * patterns are independent and order-agnostic. Since we require state-sharing - * between patterns for the transformation we violate this assumption. - * Essentially this is also the reason why we can't utilize MLIR's - * `applyPatternsGreedily` function. Moreover, we require pre-order traversal - * which current drivers of MLIR don't support. However, even if such a driver - * would exist, it would probably not return logical results which we require - * for error-handling (similarly to `walkAndApplyPatterns`). Consequently, a - * custom driver would be required in any case, which adds unnecessary code to - * maintain. - */ -LogicalResult route(ModuleOp module, MLIRContext* ctx, Mapper& mapper) { - PatternRewriter rewriter(ctx); - - /// Prepare work-list. - SmallVector worklist; - for (const auto func : module.getOps()) { - if (!isEntryPoint(func)) { - continue; // Ignore non entry_point functions for now. - } - func->walk( - [&](Operation* op) { worklist.push_back(op); }); - } - - /// Iterate work-list. - for (Operation* curr : worklist) { - if (curr == nullptr) { - continue; // Skip erased ops. - } - - const OpBuilder::InsertionGuard guard(rewriter); - rewriter.setInsertionPoint(curr); - - /// TypeSwitch performs sequential dyn_cast checks. - /// Hence, always put most frequent ops first. - - const auto res = - TypeSwitch(curr) - /// mqtopt Dialect - .Case([&](UnitaryInterface op) { - return handleUnitary(op, mapper, rewriter); - }) - .Case([&](QubitOp op) { return handleQubit(op, mapper); }) - .Case([&](ResetOp op) { return handleReset(op, mapper); }) - .Case( - [&](MeasureOp op) { return handleMeasure(op, mapper); }) - /// built-in Dialect - .Case([&]([[maybe_unused]] ModuleOp op) { - return WalkResult::advance(); - }) - /// func Dialect - .Case( - [&](func::FuncOp op) { return handleFunc(op, mapper); }) - .Case([&]([[maybe_unused]] func::ReturnOp op) { - return handleReturn(mapper); - }) - /// scf Dialect - .Case( - [&](scf::ForOp op) { return handleFor(op, mapper); }) - .Case([&](scf::IfOp op) { return handleIf(op, mapper); }) - .Case([&](scf::YieldOp op) { - return handleYield(op, mapper, rewriter); - }) - /// Skip the rest. - .Default([](auto) { return WalkResult::skip(); }); - - if (res.wasInterrupted()) { - return failure(); - } - } - - return success(); -} - -/** - * @brief This pass ensures that the connectivity constraints of the target - * architecture are met. - */ -struct RoutingPassSC final : impl::RoutingPassSCBase { - using RoutingPassSCBase::RoutingPassSCBase; - - void runOnOperation() override { - if (preflight().failed()) { - signalPassFailure(); - return; - } - - auto arch = getArchitecture(archName); - if (!arch) { - emitError(UnknownLoc::get(&getContext())) - << "unsupported architecture '" << archName << "'"; - signalPassFailure(); - return; - } - - Mapper mapper = getMapper(std::move(arch)); - if (failed(route(getOperation(), &getContext(), mapper))) { - signalPassFailure(); - } - } - -private: - [[nodiscard]] Mapper getMapper(std::unique_ptr arch) { - switch (static_cast(method)) { - case RoutingMethod::Naive: - LLVM_DEBUG({ llvm::dbgs() << "getRouter: method=naive\n"; }); - return Mapper(std::move(arch), std::make_unique(), - std::make_unique(), numSwaps); - case RoutingMethod::AStar: - LLVM_DEBUG({ llvm::dbgs() << "getRouter: method=astar\n"; }); - const HeuristicWeights weights(alpha, lambda, nlookahead); - return Mapper(std::move(arch), - std::make_unique(nlookahead), - std::make_unique(weights), numSwaps); - } - - llvm_unreachable("Unknown method"); - } - - LogicalResult preflight() { - if (archName.empty()) { - return emitError(UnknownLoc::get(&getContext()), - "required option 'arch' not provided"); - } - - return success(); - } -}; - -} // namespace -} // namespace mqt::ir::opt diff --git a/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/RoutingVerificationPass.cpp b/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/RoutingVerificationPass.cpp index f09a8a3210..5f237a9307 100644 --- a/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/RoutingVerificationPass.cpp +++ b/mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/RoutingVerificationPass.cpp @@ -13,12 +13,13 @@ #include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Architecture.h" #include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Common.h" #include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h" -#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/Stack.h" +#include "mlir/Dialect/MQTOpt/Transforms/Transpilation/SequentialUnit.h" #include -#include #include #include +#include +#include #include #include #include @@ -30,8 +31,8 @@ #include #include #include +#include #include -#include #define DEBUG_TYPE "routing-verification-sc" @@ -44,183 +45,8 @@ namespace { using namespace mlir; /** - * @brief The necessary datastructures for verification. - */ -struct VerificationContext { - explicit VerificationContext(std::unique_ptr arch) - : arch(std::move(arch)) {} - - std::unique_ptr arch; - LayoutStack stack{}; -}; - -/** - * @brief Push new state onto the stack. Skip non entry-point functions. - */ -WalkResult handleFunc(func::FuncOp op, VerificationContext& ctx) { - if (!isEntryPoint(op)) { - return WalkResult::skip(); - } - - /// Function body state. - ctx.stack.emplace(ctx.arch->nqubits()); - - return WalkResult::advance(); -} - -/** - * @brief Defines the end of a region: Pop the top of the stack. - */ -WalkResult handleReturn(VerificationContext& ctx) { - ctx.stack.pop(); - return WalkResult::advance(); -} - -/** - * @brief Prepares state for nested regions: Pushes a copy of the state on - * the stack. Forwards all out-of-loop and in-loop SSA values for their - * respective map in the stack. - */ -WalkResult handleFor(scf::ForOp op, VerificationContext& ctx) { - /// Loop body state. - ctx.stack.duplicateTop(); - - /// Forward out-of-loop and in-loop values. - const auto initArgs = op.getInitArgs().take_front(ctx.arch->nqubits()); - const auto results = op.getResults().take_front(ctx.arch->nqubits()); - const auto iterArgs = op.getRegionIterArgs().take_front(ctx.arch->nqubits()); - for (const auto [arg, res, iter] : llvm::zip(initArgs, results, iterArgs)) { - ctx.stack.getItemAtDepth(FOR_PARENT_DEPTH).remapQubitValue(arg, res); - ctx.stack.top().remapQubitValue(arg, iter); - } - - return WalkResult::advance(); -} - -/** - * @brief Prepares state for nested regions: Pushes two copies of the state on - * the stack. Forwards the results in the parent state. - */ -WalkResult handleIf(scf::IfOp op, VerificationContext& ctx) { - /// Prepare stack. - ctx.stack.duplicateTop(); /// Else - ctx.stack.duplicateTop(); /// Then. - - /// Forward results for all hardware qubits. - const auto results = op->getResults().take_front(ctx.arch->nqubits()); - Layout& stateBeforeIf = ctx.stack.getItemAtDepth(IF_PARENT_DEPTH); - for (const auto [hardwareIdx, res] : llvm::enumerate(results)) { - const Value q = stateBeforeIf.lookupHardwareValue(hardwareIdx); - stateBeforeIf.remapQubitValue(q, res); - } - - return WalkResult::advance(); -} - -/** - * @brief Defines the end of a nested region: Pop the top of the stack. - */ -WalkResult handleYield(scf::YieldOp op, VerificationContext& ctx) { - if (isa(op->getParentOp()) || isa(op->getParentOp())) { - assert(ctx.stack.size() >= 2 && "expected at least two elements on stack."); - - if (!llvm::equal(ctx.stack.top().getCurrentLayout(), - ctx.stack.getItemAtDepth(1).getCurrentLayout())) { - return op.emitOpError() << "layouts must match after restoration"; - } - - ctx.stack.pop(); - } - return WalkResult::advance(); -} - -/** - * @brief Add hardware qubit with respective program & hardware index to layout. - */ -WalkResult handleQubit(QubitOp op, VerificationContext& ctx) { - const std::size_t index = op.getIndex(); - ctx.stack.top().add(index, index, op.getQubit()); - return WalkResult::advance(); -} - -/** - * @brief Verifies if the unitary acts on either zero, one, or two qubits: - * - Advances for zero qubit unitaries (Nothing to do) - * - Forwards SSA values for one qubit. - * - Verifies executability for two-qubit gates for the given architecture and - * forwards SSA values. - */ -WalkResult handleUnitary(UnitaryInterface op, VerificationContext& ctx) { - const std::vector inQubits = op.getAllInQubits(); - const std::vector outQubits = op.getAllOutQubits(); - const std::size_t nacts = inQubits.size(); - - if (nacts == 0) { - return WalkResult::advance(); - } - - if (isa(op)) { - for (const auto [in, out] : llvm::zip(inQubits, outQubits)) { - ctx.stack.top().remapQubitValue(in, out); - } - return WalkResult::advance(); - } - - if (nacts > 2) { - return op->emitOpError() << "acts on more than two qubits"; - } - - const Value in0 = inQubits[0]; - const Value out0 = outQubits[0]; - - Layout& state = ctx.stack.top(); - - if (nacts == 1) { - state.remapQubitValue(in0, out0); - return WalkResult::advance(); - } - - const Value in1 = inQubits[1]; - const Value out1 = outQubits[1]; - - const auto idx0 = state.lookupHardwareIndex(in0); - const auto idx1 = state.lookupHardwareIndex(in1); - - if (!ctx.arch->areAdjacent(idx0, idx1)) { - return op->emitOpError() << "(" << idx0 << "," << idx1 << ")" - << " is not executable on target architecture '" - << ctx.arch->name() << "'"; - } - - if (isa(op)) { - state.swap(in0, in1); - } - - state.remapQubitValue(in0, out0); - state.remapQubitValue(in1, out1); - - return WalkResult::advance(); -} - -/** - * @brief Update layout. - */ -WalkResult handleReset(ResetOp op, VerificationContext& ctx) { - ctx.stack.top().remapQubitValue(op.getInQubit(), op.getOutQubit()); - return WalkResult::advance(); -} - -/** - * @brief Update layout. - */ -WalkResult handleMeasure(MeasureOp op, VerificationContext& ctx) { - ctx.stack.top().remapQubitValue(op.getInQubit(), op.getOutQubit()); - return WalkResult::advance(); -} - -/** - * @brief This pass verifies that the constraints of a target architecture are - * met. + * @brief This pass verifies that all two-qubit gates are executable on the + * target architecture. */ struct RoutingVerificationPassSC final : impl::RoutingVerificationSCPassBase { @@ -228,59 +54,107 @@ struct RoutingVerificationPassSC final RoutingVerificationPassSC>::RoutingVerificationSCPassBase; void runOnOperation() override { - if (preflight().failed()) { + if (failed(preflight())) { signalPassFailure(); return; } - auto arch = getArchitecture(archName); - if (!arch) { - emitError(UnknownLoc::get(&getContext())) - << "unsupported architecture '" << archName << "'"; + if (failed(verify())) { signalPassFailure(); return; } + } - VerificationContext ctx(std::move(arch)); - const auto res = - getOperation()->walk([&](Operation* op) { - return TypeSwitch(op) - /// built-in Dialect - .Case( - [&](ModuleOp /* op */) { return WalkResult::advance(); }) - /// func Dialect - .Case( - [&](func::FuncOp op) { return handleFunc(op, ctx); }) - .Case( - [&](func::ReturnOp /* op */) { return handleReturn(ctx); }) - /// scf Dialect - .Case( - [&](scf::ForOp op) { return handleFor(op, ctx); }) - .Case([&](scf::IfOp op) { return handleIf(op, ctx); }) - .Case( - [&](scf::YieldOp op) { return handleYield(op, ctx); }) - /// mqtopt Dialect - .Case([&](QubitOp op) { return handleQubit(op, ctx); }) - .Case([&](auto op) { - return WalkResult( - op->emitOpError("not allowed for transpiled program")); - }) - .Case([&](ResetOp op) { return handleReset(op, ctx); }) - .Case( - [&](MeasureOp op) { return handleMeasure(op, ctx); }) - .Case([&](UnitaryInterface unitary) { - return handleUnitary(unitary, ctx); - }) - /// Skip the rest. - .Default([](auto) { return WalkResult::skip(); }); - }); +private: + LogicalResult verify() { + ModuleOp module(getOperation()); + std::unique_ptr arch(getArchitecture(archName)); - if (res.wasInterrupted()) { - signalPassFailure(); + if (!arch) { + const Location loc = UnknownLoc::get(&getContext()); + emitError(loc) << "unsupported architecture '" << archName << "'"; + return failure(); } + + for (auto func : module.getOps()) { + LLVM_DEBUG(llvm::dbgs() << "handleFunc: " << func.getSymName() << '\n'); + + if (!isEntryPoint(func)) { + LLVM_DEBUG(llvm::dbgs() << "\tskip non entry\n"); + continue; + } + + /// Iteratively process each unit in the function. + std::queue units; + units.emplace( + SequentialUnit::fromEntryPointFunction(func, arch->nqubits())); + for (; !units.empty(); units.pop()) { + SequentialUnit& unit = units.front(); + + Layout unmodified(unit.layout()); + for (const Operation& curr : unit) { + const auto res = + TypeSwitch(&curr) + .Case([&](UnitaryInterface op) + -> LogicalResult { + if (isTwoQubitGate(op)) { + /// Verify that the two-qubit gate is executable. + if (!arch->isExecutable(op, unit.layout())) { + const auto ins = getIns(op); + const auto hw0 = + unit.layout().lookupHardwareIndex(ins.first); + const auto hw1 = + unit.layout().lookupHardwareIndex(ins.second); + + return op->emitOpError() + << "(" << hw0 << "," << hw1 << ")" + << " is not executable on target architecture '" + << arch->name() << "'"; + } + } + + unit.layout().remap(op); + return success(); + }) + .Case([&](ResetOp op) { + unit.layout().remap(op); + return success(); + }) + .Case([&](MeasureOp op) { + unit.layout().remap(op); + return success(); + }) + .Case([&](scf::YieldOp op) -> LogicalResult { + if (!unit.restore()) { + return success(); + } + + /// Verify that the layouts match at the end. + const auto mappingBefore = unmodified.getCurrentLayout(); + const auto mappingNow = unit.layout().getCurrentLayout(); + if (llvm::equal(mappingBefore, mappingNow)) { + return success(); + } + + return op.emitOpError() + << "layouts must match after restoration"; + }) + .Default([](auto) { return success(); }); + + if (failed(res)) { + return res; + } + } + + for (const auto& next : unit.next()) { + units.emplace(next); + } + } + } + + return success(); } -private: LogicalResult preflight() { if (archName.empty()) { return emitError(UnknownLoc::get(&getContext()), diff --git a/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/basics.mlir b/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/basics.mlir index 43df3366ec..44cac2fe68 100644 --- a/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/basics.mlir +++ b/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/basics.mlir @@ -8,10 +8,10 @@ // Instead of applying checks, the routing verifier pass ensures the validity of this program. -// RUN: quantum-opt %s -split-input-file --pass-pipeline="builtin.module(placement-sc{strategy=identity arch=MQTTest}, route-sc{method=naive arch=MQTTest},verify-routing-sc{arch=MQTTest})" -verify-diagnostics | FileCheck %s -// RUN: quantum-opt %s -split-input-file --pass-pipeline="builtin.module(placement-sc{strategy=identity arch=MQTTest}, route-sc{method=astar arch=MQTTest},verify-routing-sc{arch=MQTTest})" -verify-diagnostics | FileCheck %s -// RUN: quantum-opt %s -split-input-file --pass-pipeline="builtin.module(placement-sc{strategy=identity arch=IBMFalcon}, route-sc{method=naive arch=IBMFalcon},verify-routing-sc{arch=IBMFalcon})" -verify-diagnostics | FileCheck %s -// RUN: quantum-opt %s -split-input-file --pass-pipeline="builtin.module(placement-sc{strategy=identity arch=IBMFalcon}, route-sc{method=astar arch=IBMFalcon},verify-routing-sc{arch=IBMFalcon})" -verify-diagnostics | FileCheck %s +// RUN: quantum-opt %s -split-input-file --pass-pipeline="builtin.module(placement-sc{strategy=identity arch=MQTTest}, route-naive-sc{arch=MQTTest},verify-routing-sc{arch=MQTTest})" -verify-diagnostics | FileCheck %s +// RUN: quantum-opt %s -split-input-file --pass-pipeline="builtin.module(placement-sc{strategy=identity arch=MQTTest}, route-astar-sc{arch=MQTTest},verify-routing-sc{arch=MQTTest})" -verify-diagnostics | FileCheck %s +// RUN: quantum-opt %s -split-input-file --pass-pipeline="builtin.module(placement-sc{strategy=identity arch=IBMFalcon}, route-naive-sc{arch=IBMFalcon},verify-routing-sc{arch=IBMFalcon})" -verify-diagnostics | FileCheck %s +// RUN: quantum-opt %s -split-input-file --pass-pipeline="builtin.module(placement-sc{strategy=identity arch=IBMFalcon}, route-astar-sc{arch=IBMFalcon},verify-routing-sc{arch=IBMFalcon})" -verify-diagnostics | FileCheck %s module { // CHECK-LABEL: func.func @entrySABRE @@ -223,7 +223,9 @@ module { scf.yield %q0_3, %q1_2 : !mqtopt.Qubit, !mqtopt.Qubit } - mqtopt.deallocQubit %q0_3 + %q0_4 = mqtopt.x() %q0_3 : !mqtopt.Qubit + + mqtopt.deallocQubit %q0_4 mqtopt.deallocQubit %q1_2 return diff --git a/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/grover_5.mlir b/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/grover_5.mlir index 66149b883b..f17fe699d2 100644 --- a/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/grover_5.mlir +++ b/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/grover_5.mlir @@ -8,10 +8,10 @@ // Instead of applying checks, the routing verifier pass ensures the validity of this program. -// RUN: quantum-opt %s -split-input-file --pass-pipeline="builtin.module(placement-sc{strategy=identity arch=MQTTest}, route-sc{method=naive arch=MQTTest},verify-routing-sc{arch=MQTTest})" -verify-diagnostics | FileCheck %s -// RUN: quantum-opt %s -split-input-file --pass-pipeline="builtin.module(placement-sc{strategy=identity arch=MQTTest}, route-sc{method=astar arch=MQTTest},verify-routing-sc{arch=MQTTest})" -verify-diagnostics | FileCheck %s -// RUN: quantum-opt %s -split-input-file --pass-pipeline="builtin.module(placement-sc{strategy=identity arch=IBMFalcon}, route-sc{method=naive arch=IBMFalcon},verify-routing-sc{arch=IBMFalcon})" -verify-diagnostics | FileCheck %s -// RUN: quantum-opt %s -split-input-file --pass-pipeline="builtin.module(placement-sc{strategy=identity arch=IBMFalcon}, route-sc{method=astar arch=IBMFalcon},verify-routing-sc{arch=IBMFalcon})" -verify-diagnostics | FileCheck %s +// RUN: quantum-opt %s -split-input-file --pass-pipeline="builtin.module(placement-sc{strategy=identity arch=MQTTest}, route-naive-sc{arch=MQTTest},verify-routing-sc{arch=MQTTest})" -verify-diagnostics | FileCheck %s +// RUN: quantum-opt %s -split-input-file --pass-pipeline="builtin.module(placement-sc{strategy=identity arch=MQTTest}, route-astar-sc{arch=MQTTest},verify-routing-sc{arch=MQTTest})" -verify-diagnostics | FileCheck %s +// RUN: quantum-opt %s -split-input-file --pass-pipeline="builtin.module(placement-sc{strategy=identity arch=IBMFalcon}, route-naive-sc{arch=IBMFalcon},verify-routing-sc{arch=IBMFalcon})" -verify-diagnostics | FileCheck %s +// RUN: quantum-opt %s -split-input-file --pass-pipeline="builtin.module(placement-sc{strategy=identity arch=IBMFalcon}, route-astar-sc{arch=IBMFalcon},verify-routing-sc{arch=IBMFalcon})" -verify-diagnostics | FileCheck %s module { // CHECK-LABEL: func.func @main diff --git a/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/invalid-arch-option.mlir b/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/invalid-arch-option.mlir index 111c894576..2b6da0a4d7 100644 --- a/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/invalid-arch-option.mlir +++ b/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/invalid-arch-option.mlir @@ -9,7 +9,8 @@ // Instead of applying checks, the routing verifier pass ensures the validity of this program. // RUN: quantum-opt %s --placement-sc="arch=invalid-127" -verify-diagnostics -// RUN: quantum-opt %s --route-sc="arch=invalid-127" -verify-diagnostics +// RUN: quantum-opt %s --route-naive-sc="arch=invalid-127" -verify-diagnostics +// RUN: quantum-opt %s --route-astar-sc="arch=invalid-127" -verify-diagnostics // RUN: quantum-opt %s --verify-routing-sc="arch=invalid-127" -verify-diagnostics // expected-error@unknown {{unsupported architecture}} diff --git a/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/missing-arch-option.mlir b/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/missing-arch-option.mlir index 9f11ee4c23..74cb256b48 100644 --- a/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/missing-arch-option.mlir +++ b/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/missing-arch-option.mlir @@ -9,7 +9,8 @@ // Instead of applying checks, the routing verifier pass ensures the validity of this program. // RUN: quantum-opt %s --placement-sc -verify-diagnostics -// RUN: quantum-opt %s --route-sc -verify-diagnostics +// RUN: quantum-opt %s --route-naive-sc -verify-diagnostics +// RUN: quantum-opt %s --route-astar-sc -verify-diagnostics // RUN: quantum-opt %s --verify-routing-sc -verify-diagnostics // expected-error@unknown {{required option 'arch' not provided}} diff --git a/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/routing-verification.mlir b/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/routing-verification.mlir index 2f37073eab..aa0a89a5f2 100644 --- a/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/routing-verification.mlir +++ b/mlir/test/Dialect/MQTOpt/Transforms/Transpilation/routing-verification.mlir @@ -8,21 +8,6 @@ // RUN: quantum-opt %s -split-input-file --verify-routing-sc="arch=MQTTest" -verify-diagnostics -module { - func.func @tooManyQubits() attributes {passthrough = ["entry_point"]} { - %q0_0 = mqtopt.qubit 0 - %q1_0 = mqtopt.qubit 1 - %q2_0 = mqtopt.qubit 2 - - // expected-error@+1 {{'mqtopt.x' op acts on more than two qubits}} - %q0_1, %q1_1, %q2_1 = mqtopt.x() %q0_0 ctrl %q1_0, %q2_0 : !mqtopt.Qubit ctrl !mqtopt.Qubit, !mqtopt.Qubit - - return - } -} - -// ----- - module { func.func @gateNotExecutable() attributes {passthrough = ["entry_point"]} { %q0_0 = mqtopt.qubit 0