diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b8176ba05..daa2ce3bcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel ### Added +- ✨ Add a `place-and-route` pass for mapping circuits to architectures with restricted topologies ([#1537]) ([**@MatthiasReumann**]) - ✨ Add initial infrastructure for new QC and QCO MLIR dialects ([#1264], [#1330], [#1402], [#1428], [#1430], [#1436], [#1443], [#1446], [#1464], [#1465], [#1470], [#1471], [#1472], [#1474], [#1475], [#1506], [#1510], [#1513], [#1521]) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**], [**@Ectras**], [**@MatthiasReumann**]) @@ -327,6 +328,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool +[#1537]: https://github.com/munich-quantum-toolkit/core/pull/1537 [#1521]: https://github.com/munich-quantum-toolkit/core/pull/1521 [#1513]: https://github.com/munich-quantum-toolkit/core/pull/1513 [#1510]: https://github.com/munich-quantum-toolkit/core/pull/1510 diff --git a/mlir/include/mlir/CMakeLists.txt b/mlir/include/mlir/CMakeLists.txt index 272dfcf570..7d688914a2 100644 --- a/mlir/include/mlir/CMakeLists.txt +++ b/mlir/include/mlir/CMakeLists.txt @@ -6,5 +6,6 @@ # # Licensed under the MIT License -add_subdirectory(Dialect) add_subdirectory(Conversion) +add_subdirectory(Dialect) +add_subdirectory(Passes) diff --git a/mlir/include/mlir/Passes/CMakeLists.txt b/mlir/include/mlir/Passes/CMakeLists.txt new file mode 100644 index 0000000000..9d11c5ca00 --- /dev/null +++ b/mlir/include/mlir/Passes/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +set(LLVM_TARGET_DEFINITIONS Passes.td) +mlir_tablegen(Passes.h.inc -gen-pass-decls -name QCO) +add_public_tablegen_target(QcoPassesIncGen) +add_mlir_doc(Passes QcoPasses Passes/ -gen-pass-doc) diff --git a/mlir/include/mlir/Passes/Mapping/Architecture.h b/mlir/include/mlir/Passes/Mapping/Architecture.h new file mode 100644 index 0000000000..657f8bff41 --- /dev/null +++ b/mlir/include/mlir/Passes/Mapping/Architecture.h @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +/** + * @brief A quantum accelerator's architecture. + */ +class [[nodiscard]] Architecture { +public: + using CouplingSet = mlir::DenseSet>; + using NeighbourVector = mlir::SmallVector>; + + explicit Architecture(std::string name, std::size_t nqubits, + CouplingSet couplingSet) + : name_(std::move(name)), nqubits_(nqubits), + couplingSet_(std::move(couplingSet)), neighbours_(nqubits), + dist_(nqubits, mlir::SmallVector(nqubits, UINT64_MAX)), + prev_(nqubits, mlir::SmallVector(nqubits, UINT64_MAX)) { + floydWarshallWithPathReconstruction(); + collectNeighbours(); + } + + /** + * @brief Return the architecture's name. + */ + [[nodiscard]] std::string_view name() const; + + /** + * @brief Return the architecture's number of qubits. + */ + [[nodiscard]] std::size_t nqubits() const; + + /** + * @brief Return true if @p u and @p v are adjacent. + */ + [[nodiscard]] bool areAdjacent(std::size_t u, std::size_t v) const; + + /** + * @brief Return the length of the shortest path between @p u and @p v. + */ + [[nodiscard]] std::size_t distanceBetween(std::size_t u, std::size_t v) const; + + /** + * @brief Collect all neighbours of @p u. + */ + [[nodiscard]] mlir::SmallVector + neighboursOf(std::size_t u) const; + +private: + using Matrix = mlir::SmallVector, 0>; + + /** + * @brief Find all shortest paths in the coupling map between two qubits. + * @details Vertices are the qubits. Edges connected two qubits. Has a time + * and memory complexity of O(nqubits^3) and O(nqubits^2), respectively. + * @link Adapted from https://en.wikipedia.org/wiki/Floyd–Warshall_algorithm + */ + void floydWarshallWithPathReconstruction(); + + /** + * @brief Collect the neighbours of all qubits. + * @details Has a time complexity of O(nqubits) + */ + void collectNeighbours(); + + std::string name_; + std::size_t nqubits_; + CouplingSet couplingSet_; + NeighbourVector neighbours_; + + Matrix dist_; + Matrix prev_; +}; diff --git a/mlir/include/mlir/Passes/Passes.h b/mlir/include/mlir/Passes/Passes.h new file mode 100644 index 0000000000..db55cc4039 --- /dev/null +++ b/mlir/include/mlir/Passes/Passes.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Dialect/QCO/IR/QCODialect.h" + +#include +#include +#include + +namespace mlir::qco { + +#define GEN_PASS_DECL +#include "mlir/Passes/Passes.h.inc" // IWYU pragma: export + +//===----------------------------------------------------------------------===// +// Registration +//===----------------------------------------------------------------------===// + +/// Generate the code for registering passes. +#define GEN_PASS_REGISTRATION +#include "mlir/Passes/Passes.h.inc" // IWYU pragma: export + +} // namespace mlir::qco diff --git a/mlir/include/mlir/Passes/Passes.td b/mlir/include/mlir/Passes/Passes.td new file mode 100644 index 0000000000..6f090fd44d --- /dev/null +++ b/mlir/include/mlir/Passes/Passes.td @@ -0,0 +1,65 @@ +// Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +// Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +// All rights reserved. +// +// SPDX-License-Identifier: MIT +// +// Licensed under the MIT License + +#ifndef QCO_PASSES +#define QCO_PASSES + +include "mlir/Pass/PassBase.td" + +//===----------------------------------------------------------------------===// +// Transpilation Passes +//===----------------------------------------------------------------------===// + +def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { + let dependentDialects = ["mlir::qco::QCODialect"]; + let summary = "This pass ensures that a program meets the connectivity constraints of a target architecture."; + let description = [{ + This pass maps the dynamically allocated qubits in a quantum program to the static qubits of a target architecture. + The pass performs both placement and routing of the qubits to ensure that all two-qubit operations in the program + can be executed on the target architecture. + + First, the pass assigns static qubits to the dynamically allocated ones by creating an initial dynamic-to-static + mapping, which is referred to as the initial layout. Then, it traverses the circuit and inserts `SWAP` operations to + ensure that all two-qubit operations are executable on the target architecture, a process known as routing. + + For routing, the pass first divides the circuit into layers. A layer is a set of independently executable (sequences + or blocks of) two-qubit operations. Subsequently, the pass performs an A* search for each layer to find and insert a + sequence of `SWAP` operations that makes these two-qubit operations executable. The A* search also considers + subsequent layers, which are determined by the `nlookahead` parameter. + + The cost function of the A* search is defined as follows: + + * `f(n) = g(n) + h(n)` + * `g(n) = alpha * depth(n)` + * `h(n) = sum(pow(lambda, i) * h(L, p) for [i, L] in enumerate(layers))` + + Where: + * `p` is the dynamic-to-static mapping associated with search node `n`. + * `layers` is an array of layers with size `1 + nlookahead`. + * `depth(n)` returns the distance from the node `n` to the root node. + * `dist(i, j)` returns the distance between the qubits `i` and `j` on the target's coupling graph. + * `h(L, p) := sum(dist(p[gate.first], p[gate.second]) for gate in L)` + + To iteratively refine the mapping, the pass performs multiple forward and backward traversals of the circuit. In + each traversal, the pass routes the circuit and updates the dynamic-to-static mapping based on the routing decisions + made during that traversal. By performing multiple traversals, the pass can iteratively refine the mapping and + potentially find a more optimal solution. This is behavior is controlled by the `niterations` parameter. + }]; + let options = [ + Option<"nlookahead", "nlookahead", "std::size_t", "1", + "The number of lookahead steps.">, + Option<"alpha", "alpha", "float", "1.0F", + "The alpha factor in the cost function. Must be > 0.">, + Option<"lambda", "lambda", "float", "0.5F", + "The lambda factor in the cost function.">, + Option<"niterations", "niterations", "std::size_t", "2", + "The number of forwards and backwards traversal to improve the initial layout."> + ]; +} + +#endif // QCO_PASSES diff --git a/mlir/lib/CMakeLists.txt b/mlir/lib/CMakeLists.txt index afc74137bd..7f46084b8c 100644 --- a/mlir/lib/CMakeLists.txt +++ b/mlir/lib/CMakeLists.txt @@ -6,7 +6,8 @@ # # Licensed under the MIT License -add_subdirectory(Dialect) add_subdirectory(Conversion) add_subdirectory(Compiler) +add_subdirectory(Dialect) +add_subdirectory(Passes) add_subdirectory(Support) diff --git a/mlir/lib/Passes/CMakeLists.txt b/mlir/lib/Passes/CMakeLists.txt new file mode 100644 index 0000000000..92d1494592 --- /dev/null +++ b/mlir/lib/Passes/CMakeLists.txt @@ -0,0 +1,41 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +file(GLOB_RECURSE PASSES_SOURCES *.cpp) + +get_property(dialect_libs GLOBAL PROPERTY MLIR_DIALECT_LIBS) + +add_mlir_library( + QcoPasses + ${PASSES_SOURCES} + LINK_LIBS + PUBLIC + PRIVATE + ${dialect_libs} + DEPENDS + QcoPassesIncGen) + +# collect header files +file(GLOB_RECURSE PASSES_HEADERS_SOURCE ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Passes/*.h) +file(GLOB_RECURSE PASSES_HEADERS_BUILD ${MQT_MLIR_BUILD_INCLUDE_DIR}/mlir/Passes/*.inc) + +# add public headers using file sets +target_sources( + QcoPasses + PUBLIC FILE_SET + HEADERS + BASE_DIRS + ${MQT_MLIR_SOURCE_INCLUDE_DIR} + FILES + ${PASSES_HEADERS_SOURCE} + FILE_SET + HEADERS + BASE_DIRS + ${MQT_MLIR_BUILD_INCLUDE_DIR} + FILES + ${PASSES_HEADERS_BUILD}) diff --git a/mlir/lib/Passes/Mapping/Architecture.cpp b/mlir/lib/Passes/Mapping/Architecture.cpp new file mode 100644 index 0000000000..70997b8321 --- /dev/null +++ b/mlir/lib/Passes/Mapping/Architecture.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Passes/Mapping/Architecture.h" + +#include +#include + +#include +#include +#include +#include + +using namespace mlir; + +std::string_view Architecture::name() const { return name_; } + +std::size_t Architecture::nqubits() const { return nqubits_; } + +bool Architecture::areAdjacent(std::size_t u, std::size_t v) const { + return couplingSet_.contains(std::make_pair(u, v)); +} + +std::size_t Architecture::distanceBetween(std::size_t u, std::size_t v) const { + if (dist_[u][v] == UINT64_MAX) { + report_fatal_error("Floyd-warshall failed to compute the distance " + "between qubits " + + Twine(u) + " and " + Twine(v)); + } + return dist_[u][v]; +} + +SmallVector Architecture::neighboursOf(std::size_t u) const { + return neighbours_[u]; +} + +void Architecture::floydWarshallWithPathReconstruction() { + for (const auto& [u, v] : couplingSet_) { + dist_[u][v] = 1; + prev_[u][v] = u; + } + for (std::size_t v = 0; v < nqubits(); ++v) { + dist_[v][v] = 0; + prev_[v][v] = v; + } + + for (std::size_t k = 0; k < nqubits(); ++k) { + for (std::size_t i = 0; i < nqubits(); ++i) { + for (std::size_t j = 0; j < nqubits(); ++j) { + if (dist_[i][k] == UINT64_MAX || dist_[k][j] == UINT64_MAX) { + continue; // Avoid overflow with "infinite" distances. + } + const std::size_t sum = dist_[i][k] + dist_[k][j]; + if (dist_[i][j] > sum) { + dist_[i][j] = sum; + prev_[i][j] = prev_[k][j]; + } + } + } + } +} + +void Architecture::collectNeighbours() { + for (const auto& [u, v] : couplingSet_) { + neighbours_[u].push_back(v); + } +} diff --git a/mlir/lib/Passes/Mapping/Mapping.cpp b/mlir/lib/Passes/Mapping/Mapping.cpp new file mode 100644 index 0000000000..0720cdbaa8 --- /dev/null +++ b/mlir/lib/Passes/Mapping/Mapping.cpp @@ -0,0 +1,659 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Utils/WireIterator.h" +#include "mlir/Passes/Mapping/Architecture.h" +#include "mlir/Passes/Passes.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 "mapping-pass" + +namespace mlir::qco { + +#define GEN_PASS_DEF_MAPPINGPASS +#include "mlir/Passes/Passes.h.inc" + +struct MappingPass : impl::MappingPassBase { +private: + using QubitValue = TypedValue; + using IndexType = std::size_t; + using IndexGate = std::pair; + using IndexGateSet = DenseSet; + using Layer = DenseSet; + + /** + * @brief Specifies the layering direction. + */ + enum class Direction : std::uint8_t { Forward, Backward }; + + /** + * @brief A qubit layout that maps program and hardware indices without + * storing Values. Used for efficient memory usage when Value tracking isn't + * needed. + * + * Note that we use the terminology "hardware" and "program" qubits here, + * because "virtual" (opposed to physical) and "static" (opposed to dynamic) + * are C++ keywords. + */ + class [[nodiscard]] Layout { + public: + /** + * @brief Constructs the identity (i->i) layout. + * @param nqubits The number of qubits. + * @return The identity layout. + */ + static Layout identity(const std::size_t nqubits) { + Layout layout(nqubits); + for (std::size_t i = 0; i < nqubits; ++i) { + layout.add(i, i); + } + return layout; + } + + /** + * @brief Insert program:hardware index mapping. + * @param prog The program index. + * @param hw The hardware index. + */ + void add(IndexType prog, IndexType hw) { + assert(prog < programToHardware_.size() && + "add: program index out of bounds"); + assert(hw < hardwareToProgram_.size() && + "add: hardware index out of bounds"); + programToHardware_[prog] = hw; + hardwareToProgram_[hw] = prog; + } + + /** + * @brief Look up program index for a hardware index. + * @param hw The hardware index. + * @return The program index of the respective hardware index. + */ + [[nodiscard]] IndexType getProgramIndex(const IndexType hw) const { + assert(hw < hardwareToProgram_.size() && + "getProgramIndex: hardware index out of bounds"); + return hardwareToProgram_[hw]; + } + + /** + * @brief Look up hardware index for a program index. + * @param prog The program index. + * @return The hardware index of the respective program index. + */ + [[nodiscard]] IndexType getHardwareIndex(const IndexType prog) const { + assert(prog < programToHardware_.size() && + "getHardwareIndex: program index out of bounds"); + return programToHardware_[prog]; + } + + /** + * @brief Convenience function to lookup multiple hardware indices at once. + * @param progs The program indices. + * @return A tuple of hardware indices. + */ + template + requires(sizeof...(ProgIndices) > 0) && + ((std::is_convertible_v) && ...) + [[nodiscard]] auto getHardwareIndices(ProgIndices... progs) const { + return std::tuple{getHardwareIndex(static_cast(progs))...}; + } + + /** + * @brief Convenience function to lookup multiple program indices at once. + * @param hws The hardware indices. + * @return A tuple of program indices. + */ + template + requires(sizeof...(HwIndices) > 0) && + ((std::is_convertible_v) && ...) + [[nodiscard]] auto getProgramIndices(HwIndices... hws) const { + return std::tuple{getProgramIndex(static_cast(hws))...}; + } + + /** + * @brief Swap the mapping to program indices of two hardware indices. + */ + void swap(const IndexType hw0, const IndexType hw1) { + const auto prog0 = hardwareToProgram_[hw0]; + const auto prog1 = hardwareToProgram_[hw1]; + + std::swap(hardwareToProgram_[hw0], hardwareToProgram_[hw1]); + std::swap(programToHardware_[prog0], programToHardware_[prog1]); + } + + /** + * @returns the number of qubits managed by the layout. + */ + [[nodiscard]] std::size_t nqubits() const { + return programToHardware_.size(); + } + + void dump() { + llvm::dbgs() << "prog= "; + for (std::size_t i = 0; i < nqubits(); ++i) { + llvm::dbgs() << i << " "; + } + llvm::dbgs() << "\nhw= "; + for (std::size_t i = 0; i < nqubits(); ++i) { + llvm::dbgs() << programToHardware_[i] << ' '; + } + llvm::dbgs() << '\n'; + } + + protected: + /** + * @brief Maps a program qubit index to its hardware index. + */ + SmallVector programToHardware_; + + /** + * @brief Maps a hardware qubit index to its program index. + */ + SmallVector hardwareToProgram_; + + private: + explicit Layout(const std::size_t nqubits) + : programToHardware_(nqubits), hardwareToProgram_(nqubits) {} + }; + + /** + * @brief Parameters influencing the behavior of the A* search algorithm. + */ + struct Parameters { + Parameters(const float alpha, const float lambda, + const std::size_t nlookahead) + : alpha(alpha), decay(1 + nlookahead) { + decay[0] = 1.; + for (std::size_t i = 1; i < decay.size(); ++i) { + decay[i] = decay[i - 1] * lambda; + } + } + + float alpha; + SmallVector decay; + }; + + /** + * @brief Describes a node in the A* search graph. + */ + struct Node { + SmallVector sequence; + Layout layout; + float f; + + /** + * @brief Construct a root node with the given layout. Initialize the + * sequence with an empty vector and set the cost to zero. + */ + explicit Node(Layout layout) : layout(std::move(layout)), f(0) {} + + /** + * @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, IndexGate swap, ArrayRef layers, + const Architecture& arch, const Parameters& params) + : sequence(parent.sequence), layout(parent.layout), f(0) { + // Apply node-specific swap to given layout. + layout.swap(swap.first, swap.second); + + // Add swap to sequence. + sequence.emplace_back(swap); + + // Evaluate cost function. + f = g(params.alpha) + h(layers, arch, params); // NOLINT + } + + /** + * @returns true if the current sequence of SWAPs makes all gates + * executable. + */ + [[nodiscard]] bool isGoal(const Layer& front, + const Architecture& arch) const { + return all_of(front, [&](const IndexGate gate) { + return arch.areAdjacent(layout.getHardwareIndex(gate.first), + layout.getHardwareIndex(gate.second)); + }); + } + + /** + * @returns true iff. the costs of this node are higher than the one of @p + * rhs. + */ + [[nodiscard]] bool operator>(const Node& rhs) const { return f > rhs.f; } + + private: + /** + * @brief Calculate the path cost for the A* search algorithm. + * + * The path cost function is the weighted sum of the currently required + * SWAPs. + */ + [[nodiscard]] float g(float alpha) const { + return alpha * static_cast(sequence.size()); + } + + /** + * @brief Calculate the heuristic cost for the A* search algorithm. + * + * Computes the minimal number of SWAPs required to route each gate in + * each layer. For each gate, this is determined by the shortest distance + * between its hardware qubits. Intuitively, this is the number of SWAPs + * that a naive router would insert to route the layers (with a constant + * layout). + */ + [[nodiscard]] float h(ArrayRef layers, const Architecture& arch, + const Parameters& params) const { + float costs{0}; + for (const auto& [decay, layer] : zip(params.decay, layers)) { + for (const auto& [prog0, prog1] : layer) { + const auto [hw0, hw1] = layout.getHardwareIndices(prog0, prog1); + const std::size_t nswaps = arch.distanceBetween(hw0, hw1) - 1; + costs += decay * static_cast(nswaps); + } + } + return costs; + } + }; + + using MinQueue = std::priority_queue, std::greater<>>; + +public: + using MappingPassBase::MappingPassBase; + + void runOnOperation() override { + Parameters params(this->alpha, this->lambda, this->nlookahead); + // TODO: Hardcoded architecture. + Architecture arch("RigettiNovera", 9, + {{0, 3}, {3, 0}, {0, 1}, {1, 0}, {1, 4}, {4, 1}, + {1, 2}, {2, 1}, {2, 5}, {5, 2}, {3, 6}, {6, 3}, + {3, 4}, {4, 3}, {4, 7}, {7, 4}, {4, 5}, {5, 4}, + {5, 8}, {8, 5}, {6, 7}, {7, 6}, {7, 8}, {8, 7}}); + + IRRewriter rewriter(&getContext()); + for (auto func : getOperation().getOps()) { + const auto dyn = collectDynamicQubits(func.getFunctionBody()); + if (dyn.size() > arch.nqubits()) { + func.emitError() << "the targeted architecture supports " + << arch.nqubits() << " qubits, got " << dyn.size(); + signalPassFailure(); + return; + } + + const auto [ltr, rtl] = computeBidirectionalLayers(dyn); + + // Use the SABRE Approach to improve the initial layout choice (here: + // identity): Traverse the layers from left-to-right-to-left and + // cold-route along the way. Repeat this procedure "niterations" times. + + Layout layout = Layout::identity(arch.nqubits()); + for (std::size_t r = 0; r < this->niterations; ++r) { + if (failed(routeCold(ltr, layout, arch, params))) { + signalPassFailure(); + return; + } + if (failed(routeCold(rtl, layout, arch, params))) { + signalPassFailure(); + return; + } + } + + // Once the initial layout is found, replace the dynamic with static + // qubits ("placement") and hot-route the circuit layer-by-layer. + + const auto stat = place(dyn, layout, func.getFunctionBody(), rewriter); + if (failed(routeHot(ltr, layout, stat, arch, params, rewriter))) { + signalPassFailure(); + return; + }; + } + } + +private: + /** + * @brief Collect dynamic qubits contained in the given function body. + * @returns a vector of SSA values produced by qco.alloc operations. + */ + [[nodiscard]] static SmallVector + collectDynamicQubits(Region& funcBody) { + return SmallVector(map_range( + funcBody.getOps(), [](AllocOp op) { return op.getResult(); })); + } + + /** + * @brief Computes forwards and backwards layers. + * @returns a pair of vectors of layers, where [0]=forward and [1]=backward. + */ + [[nodiscard]] static std::pair, SmallVector> + computeBidirectionalLayers(ArrayRef dyn) { + auto wires = toWires(dyn); + const auto ltr = collectLayers(wires); + const auto rtl = collectLayers(wires); + return std::make_pair(ltr, rtl); + } + + /** + * @brief Perform placement. + * @details Replaces dynamic with static qubits. Extends the computation with + * as many static qubits the architecture supports. + * @returns vector of SSA values produced by qco.static operations, ordered by + * the static index s.t. [i] = qco.static i. + */ + [[nodiscard]] static SmallVector + place(ArrayRef dynQubits, const Layout& layout, Region& funcBody, + IRRewriter& rewriter) { + SmallVector statics(layout.nqubits()); + + // 1. Replace existing dynamic allocations with mapped static ones. + for (const auto [p, q] : enumerate(dynQubits)) { + const auto hw = layout.getHardwareIndex(p); + rewriter.setInsertionPoint(q.getDefiningOp()); + auto op = rewriter.replaceOpWithNewOp(q.getDefiningOp(), hw); + statics[hw] = op.getQubit(); + } + + // 2. Create static qubits for the remaining (unused) hardware indices. + for (std::size_t p = dynQubits.size(); p < layout.nqubits(); ++p) { + rewriter.setInsertionPointToStart(&funcBody.front()); + const auto hw = layout.getHardwareIndex(p); + auto op = rewriter.create(rewriter.getUnknownLoc(), hw); + rewriter.setInsertionPoint(funcBody.back().getTerminator()); + rewriter.create(rewriter.getUnknownLoc(), op.getQubit()); + statics[hw] = op.getQubit(); + } + + return statics; + } + + /** + * @brief Perform A* search to find a sequence of SWAPs that makes the + * two-qubit operations inside the first layer (the front) executable. + * @returns a vector of hardware-index pairs (each denoting a SWAP) or + * failure() if A* fails. + */ + [[nodiscard]] static FailureOr> + search(ArrayRef layers, const Layout& layout, const Architecture& arch, + const Parameters& params) { + + Node root(layout); + if (root.isGoal(layers.front(), arch)) { + return SmallVector{}; + } + + MinQueue frontier{}; + frontier.emplace(root); + DenseSet expansionSet; + + while (!frontier.empty()) { + Node curr = frontier.top(); + frontier.pop(); + + if (curr.isGoal(layers.front(), arch)) { + return curr.sequence; + } + + // Given a layout, create child-nodes for each possible SWAP between + // two neighbouring hardware qubits. + + expansionSet.clear(); + if (!curr.sequence.empty()) { + expansionSet.insert(curr.sequence.back()); + } + + for (const IndexGate& gate : layers.front()) { + for (const auto prog : {gate.first, gate.second}) { + const auto hw0 = curr.layout.getHardwareIndex(prog); + for (const auto hw1 : arch.neighboursOf(hw0)) { + /// Ensure consistent hashing/comparison. + const IndexGate swap = std::minmax(hw0, hw1); + if (!expansionSet.insert(swap).second) { + continue; + } + + frontier.emplace(curr, swap, layers, arch, params); + } + } + } + } + + return failure(); + } + + /** + * @brief Transform a range of qubit values to a vector of wire iterators. + * @returns a vector of wire iterators. + */ + template + static SmallVector toWires(QubitRange qubits) { + return SmallVector( + map_range(qubits, [](auto q) { return WireIterator(q); })); + } + + /** + * @brief Collect the layers of independently executable two-qubit gates of a + * circuit. + * @details Depending on the template parameter, the function collects the + * layers in forward or backward direction, respectively. Towards that end, + * the function traverses the def-use chain of each qubit until a two-qubit + * gate is found. If a two-qubit gate is visited twice, it is considered ready + * and inserted into the layer. This process is repeated until no more + * two-qubit are found anymore. + * @returns a vector of layers. + */ + template + static SmallVector collectLayers(MutableArrayRef wires) { + constexpr auto step = d == Direction::Forward ? 1 : -1; + const auto shouldContinue = [](const WireIterator& it) { + if constexpr (d == Direction::Forward) { + return it != std::default_sentinel; + } else { + return !isa(it.operation()); + } + }; + + SmallVector layers; + DenseMap visited; + + while (true) { + Layer layer{}; + for (const auto [index, it] : enumerate(wires)) { + while (shouldContinue(it)) { + const auto res = + TypeSwitch(it.operation()) + .Case([&](UnitaryOpInterface op) { + assert(op.getNumQubits() > 0 && op.getNumQubits() <= 2); + + if (op.getNumQubits() == 1) { + std::ranges::advance(it, step); + return WalkResult::advance(); + } + + if (visited.contains(op)) { + const auto otherIndex = visited[op]; + layer.insert(std::make_pair(index, otherIndex)); + + std::ranges::advance(wires[index], step); + std::ranges::advance(wires[otherIndex], step); + + visited.erase(op); + } else { + visited.try_emplace(op, index); + } + + return WalkResult::interrupt(); + }) + .template Case([&](auto) { + std::ranges::advance(it, step); + return WalkResult::advance(); + }) + .Default([&](Operation* op) { + report_fatal_error("unknown op in wireiterator use: " + + op->getName().getStringRef()); + return WalkResult::interrupt(); + }); + + if (res.wasInterrupted()) { + break; + } + } + } + + if (layer.empty()) { + break; + } + + layers.emplace_back(layer); + visited.clear(); + } + + return layers; + } + + /** + * @brief "Cold" routing of the given layers. + * @details Iterates over a sliding window of layers and uses the A* search + * engine to find a sequence of SWAPs that makes that layer executable. + * Instead of inserting these SWAPs into the IR, this function only updates + * the layout. + * @returns failure() if A* search isn't able to find a solution. + */ + LogicalResult routeCold(ArrayRef layers, Layout& layout, + const Architecture& arch, const Parameters& params) { + for (std::size_t i = 0; i < layers.size(); ++i) { + const std::size_t len = std::min(1 + nlookahead, layers.size() - i); + const auto window = layers.slice(i, len); + const auto swaps = search(window, layout, arch, params); + if (failed(swaps)) { + return failure(); + } + + for (const auto& [hw0, hw1] : *swaps) { + layout.swap(hw0, hw1); + } + } + + return success(); + } + + /** + * @brief "Hot" routing of the given layers. + * @details Iterates over a sliding window of layers and uses the A* search + * engine to find a sequence of SWAPs that makes that layer executable. + * This function inserts SWAP ops. + * @returns failure() if A* search isn't able to find a solution. + */ + LogicalResult routeHot(ArrayRef ltr, Layout& layout, + ArrayRef statics, const Architecture& arch, + const Parameters& params, IRRewriter& rewriter) { + // Helper function that advances the iterator to the input qubit (the + // operation producing it) of a deallocation or two-qubit op. + const auto advFront = [](WireIterator& it) { + while (true) { + const auto next = std::next(it); + if (isa(next.operation())) { + break; + } + + auto op = dyn_cast(next.operation()); + if (op && op.getNumQubits() > 1) { + break; + } + + std::ranges::advance(it, 1); + } + }; + + auto wires = toWires(statics); + for (const auto [i, layer] : enumerate(ltr)) { + // Advance all wires to the next front of one-qubit outputs (the SSA + // values). + for_each(wires, advFront); + + // Collect window and use A* to find and insert a sequence of swaps. + const auto len = std::min(1 + this->nlookahead, ltr.size() - i); + const auto window = ltr.slice(i, len); + const auto swaps = search(window, layout, arch, params); + if (failed(swaps)) { + return failure(); + } + + const auto unknown = rewriter.getUnknownLoc(); + for (const auto& [hw0, hw1] : *swaps) { + Operation* op0 = wires[hw0].operation(); + Operation* op1 = wires[hw1].operation(); + const auto in0 = wires[hw0].qubit(); + const auto in1 = wires[hw1].qubit(); + + // Reorder to avoid SSA dominance issues. + assert(op0->getBlock()->isOpOrderValid() && + "An invalid op order leads to a significant runtime overhead."); + if (op0->isBeforeInBlock(op1)) { + rewriter.setInsertionPointAfterValue(in1); + } else { + rewriter.setInsertionPointAfterValue(in0); + } + + auto op = rewriter.create(unknown, in0, in1); + const auto out0 = op.getQubit0Out(); + const auto out1 = op.getQubit1Out(); + + rewriter.replaceAllUsesExcept(in0, out1, op); + rewriter.replaceAllUsesExcept(in1, out0, op); + + // Jump over the SWAPOp. + std::ranges::advance(wires[hw0], 1); + std::ranges::advance(wires[hw1], 1); + + layout.swap(hw0, hw1); + } + + // Jump over two-qubit gates contained in the layer. + for (const auto& [prog0, prog1] : layer) { + std::ranges::advance(wires[layout.getHardwareIndex(prog0)], 1); + std::ranges::advance(wires[layout.getHardwareIndex(prog1)], 1); + } + } + + return success(); + } +}; +} // namespace mlir::qco diff --git a/mlir/unittests/CMakeLists.txt b/mlir/unittests/CMakeLists.txt index da76b50c37..962a4a9018 100644 --- a/mlir/unittests/CMakeLists.txt +++ b/mlir/unittests/CMakeLists.txt @@ -18,3 +18,4 @@ add_subdirectory(programs) add_subdirectory(Compiler) add_subdirectory(Dialect) add_subdirectory(Conversion) +add_subdirectory(Passes) diff --git a/mlir/unittests/Passes/CMakeLists.txt b/mlir/unittests/Passes/CMakeLists.txt new file mode 100644 index 0000000000..30ddc4dc38 --- /dev/null +++ b/mlir/unittests/Passes/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +add_subdirectory(Mapping) diff --git a/mlir/unittests/Passes/Mapping/CMakeLists.txt b/mlir/unittests/Passes/Mapping/CMakeLists.txt new file mode 100644 index 0000000000..39aa3fbcc1 --- /dev/null +++ b/mlir/unittests/Passes/Mapping/CMakeLists.txt @@ -0,0 +1,17 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +set(target_name mqt-core-mlir-unittest-mapping) +add_executable(${target_name} test_mapping.cpp) + +target_link_libraries(${target_name} PRIVATE MLIRParser GTest::gtest_main MLIRQCProgramBuilder + QCToQCO QCOToQC QcoPasses) + +mqt_mlir_configure_unittest_target(${target_name}) + +gtest_discover_tests(${target_name} PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) diff --git a/mlir/unittests/Passes/Mapping/test_mapping.cpp b/mlir/unittests/Passes/Mapping/test_mapping.cpp new file mode 100644 index 0000000000..858baed5aa --- /dev/null +++ b/mlir/unittests/Passes/Mapping/test_mapping.cpp @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Conversion/QCOToQC/QCOToQC.h" +#include "mlir/Conversion/QCToQCO/QCToQCO.h" +#include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" +#include "mlir/Dialect/QC/IR/QCDialect.h" +#include "mlir/Dialect/QC/IR/QCInterfaces.h" +#include "mlir/Dialect/QC/IR/QCOps.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Passes/Mapping/Architecture.h" +#include "mlir/Passes/Passes.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace mlir; + +namespace { +struct ArchitectureParam { + std::string name; + Architecture (*factory)(); +}; + +class MappingPassTest : public testing::Test, + public testing::WithParamInterface { +public: + /** + * @brief Walks the IR and validates if each two-qubit op is executable on the + * given architecture. + * @returns true iff. all two-qubit gates are executable on the architecture. + */ + static bool isExecutable(OwningOpRef& moduleOp, + const Architecture& arch) { + auto entry = *(moduleOp->getOps().begin()); + DenseMap mappings; + for_each(entry.getOps(), [&](qc::StaticOp op) { + mappings.try_emplace(op.getQubit(), op.getIndex()); + }); + + bool executable = true; + std::ignore = moduleOp->walk([&](qc::UnitaryOpInterface op) { + if (op.getNumQubits() > 1) { + assert(op.getNumQubits() == 2 && + "Expected only 2-qubit gates after decomposition"); + assert(mappings.contains(op.getQubit(0)) && "Qubit 0 not in mapping"); + assert(mappings.contains(op.getQubit(1)) && "Qubit 1 not in mapping"); + const auto i0 = mappings[op.getQubit(0)]; + const auto i1 = mappings[op.getQubit(1)]; + if (!arch.areAdjacent(i0, i1)) { + executable = false; + return WalkResult::interrupt(); + } + } + return WalkResult::advance(); + }); + + return executable; + } + + static Architecture getRigettiNovera() { + // TODO: At some point this should be provided via QDMI. + const static Architecture::CouplingSet COUPLING{ + {0, 3}, {3, 0}, {0, 1}, {1, 0}, {1, 4}, {4, 1}, {1, 2}, {2, 1}, + {2, 5}, {5, 2}, {3, 6}, {6, 3}, {3, 4}, {4, 3}, {4, 7}, {7, 4}, + {4, 5}, {5, 4}, {5, 8}, {8, 5}, {6, 7}, {7, 6}, {7, 8}, {8, 7}}; + return Architecture("RigettiNovera", 9, COUPLING); + } + +protected: + void SetUp() override { + // Register all necessary dialects + DialectRegistry registry; + registry.insert(); + context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); + } + + static void runHeuristicMapping(OwningOpRef& moduleOp) { + PassManager pm(moduleOp->getContext()); + pm.addPass(createQCToQCO()); + pm.addPass(qco::createMappingPass(qco::MappingPassOptions{ + .nlookahead = 5, .alpha = 1, .lambda = 0.85, .niterations = 2})); + pm.addPass(createQCOToQC()); + auto res = pm.run(*moduleOp); + ASSERT_TRUE(succeeded(res)); + } + + std::unique_ptr context; +}; +}; // namespace + +TEST_P(MappingPassTest, GHZ) { + auto arch = GetParam().factory(); + + qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + const auto q2 = builder.allocQubit(); + + builder.h(q0); + builder.cx(q0, q1); + builder.cx(q0, q2); + + builder.dealloc(q0); + builder.dealloc(q1); + builder.dealloc(q2); + + auto moduleOp = builder.finalize(); + runHeuristicMapping(moduleOp); + EXPECT_TRUE(isExecutable(moduleOp, arch)); +} + +TEST_P(MappingPassTest, Sabre) { + auto arch = GetParam().factory(); + + qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + const auto q2 = builder.allocQubit(); + const auto q3 = builder.allocQubit(); + const auto q4 = builder.allocQubit(); + const auto q5 = builder.allocQubit(); + + builder.h(q0); + builder.h(q1); + builder.h(q4); + + builder.z(q0); + builder.cx(q1, q2); + builder.cx(q4, q5); + + builder.cx(q0, q1); + builder.cx(q2, q3); + + builder.h(q2); + builder.h(q3); + + builder.cx(q1, q2); + builder.cx(q3, q5); + + builder.z(q3); + + builder.cx(q3, q4); + + builder.cx(q3, q0); + + builder.dealloc(q0); + builder.dealloc(q1); + builder.dealloc(q2); + builder.dealloc(q3); + builder.dealloc(q4); + builder.dealloc(q5); + + auto moduleOp = builder.finalize(); + runHeuristicMapping(moduleOp); + EXPECT_TRUE(isExecutable(moduleOp, arch)); +} + +INSTANTIATE_TEST_SUITE_P( + Architectures, MappingPassTest, + testing::Values(ArchitectureParam{"RigettiNovera", + &MappingPassTest::getRigettiNovera}), + [](const testing::TestParamInfo& info) { + return info.param.name; + });