From 539417c0d70ab8ed145101a6c7a4ea2dd3717c34 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 31 Aug 2026 14:14:02 +0200 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=A8=20Support=20directional=20gates?= =?UTF-8?q?=20in=20mapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/mlir/register_mlir.cpp | 44 ++- mlir/include/mlir/Compiler/QDMIAdapter.h | 6 +- mlir/include/mlir/Compiler/Target.h | 60 ++- .../mlir/Dialect/QCO/Transforms/Passes.td | 8 +- mlir/lib/Compiler/QDMIAdapter.cpp | 64 ++-- mlir/lib/Compiler/Target.cpp | 362 ++++++++++++++++-- .../QCO/Transforms/Mapping/Mapping.cpp | 225 +++++++++-- .../Compiler/test_compiler_qdmi_adapter.cpp | 19 +- .../Compiler/test_compiler_target.cpp | 95 ++++- .../QCO/Transforms/Mapping/test_mapping.cpp | 150 +++++++- python/mqt/core/mlir.pyi | 19 +- test/python/test_mlir.py | 18 +- 12 files changed, 922 insertions(+), 148 deletions(-) diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 1c59629a45..4b2eaab15b 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -580,7 +580,8 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); auto targetOperation = nb::class_( compilerTarget, "Operation", - "A homogeneous target-wide operation capability and its calibration."); + "A target operation capability, calibration, and ordered " + "applicability."); targetOperation .def( "__init__", @@ -590,7 +591,10 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::optional> siteTuples, const std::optional duration, - const std::optional fidelity) { + const std::optional fidelity, + std::optional< + std::vector>> + applicableSiteTuples) { constructFromExpected( self, mlir::CompilerTarget::Operation::create( @@ -598,10 +602,11 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::move(siteTuples) .value_or( std::vector{}), - duration, fidelity)); + duration, fidelity, std::move(applicableSiteTuples))); }, "name"_a, "arity"_a, "num_parameters"_a, "site_tuples"_a = nb::none(), - "duration"_a = nb::none(), "fidelity"_a = nb::none()) + "duration"_a = nb::none(), "fidelity"_a = nb::none(), + "applicable_site_tuples"_a = nb::none()) .def( "__init__", [](mlir::CompilerTarget::Operation& self, std::string name, @@ -609,7 +614,10 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::optional> siteTuples, const std::optional duration, - const std::optional fidelity) { + const std::optional fidelity, + std::optional< + std::vector>> + applicableSiteTuples) { constructFromExpected( self, mlir::CompilerTarget::Operation::create( @@ -617,10 +625,11 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::move(siteTuples) .value_or( std::vector{}), - duration, fidelity)); + duration, fidelity, std::move(applicableSiteTuples))); }, "name"_a, "arity"_a, "num_parameters"_a, "site_tuples"_a = nb::none(), - "duration"_a = nb::none(), "fidelity"_a = nb::none()) + "duration"_a = nb::none(), "fidelity"_a = nb::none(), + "applicable_site_tuples"_a = nb::none()) .def_prop_ro( "name", [](const mlir::CompilerTarget::Operation& operation) { @@ -645,6 +654,17 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); operation.siteTuples().begin(), operation.siteTuples().end()); }, "Ordered site-specific calibration data.") + .def_prop_ro("has_explicit_applicability", + &mlir::CompilerTarget::Operation::hasExplicitApplicability, + "Whether ordered site applicability is explicit.") + .def_prop_ro( + "applicable_site_tuples", + [](const mlir::CompilerTarget::Operation& operation) { + return std::vector>( + operation.applicableSiteTuples().begin(), + operation.applicableSiteTuples().end()); + }, + "The explicitly applicable ordered target-site tuples.") .def_prop_ro("duration", &mlir::CompilerTarget::Operation::duration, "The raw default duration, if available.") .def_prop_ro("fidelity", &mlir::CompilerTarget::Operation::fidelity, @@ -905,11 +925,17 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); .def( "supports_operation", [](const mlir::CompilerTarget& target, const std::string_view name, - const size_t arity, const std::optional numParameters) { + const size_t arity, const std::optional numParameters, + const std::optional>& + sites) { + if (sites) { + return target.supportsOperation(name, arity, numParameters, + *sites); + } return target.supportsOperation(name, arity, numParameters); }, "name"_a, "arity"_a, "num_parameters"_a = nb::none(), - "Whether the target supports an operation."); + "sites"_a = nb::none(), "Whether the target supports an operation."); auto program = nb::class_( m, "Program", R"pb(Base class for a typed MLIR compiler program. diff --git a/mlir/include/mlir/Compiler/QDMIAdapter.h b/mlir/include/mlir/Compiler/QDMIAdapter.h index 979a7286d0..90efdca8e4 100644 --- a/mlir/include/mlir/Compiler/QDMIAdapter.h +++ b/mlir/include/mlir/Compiler/QDMIAdapter.h @@ -29,8 +29,10 @@ namespace mlir { * * @details The returned target owns all queried metadata and remains valid * after the originating device and session have been destroyed. Neutral-atom - * zone models and site-dependent operation support are not supported by the - * circuit-model compiler pipeline. + * zone models are not supported. Explicit QDMI site lists are accepted for + * one- and two-qubit operations only: one-qubit lists must cover every site and + * two-qubit lists every undirected topology edge. Their ordered tuples and + * calibration data are preserved. */ [[nodiscard]] llvm::Expected compilerTargetFromDevice(const qdmi::Device& device); diff --git a/mlir/include/mlir/Compiler/Target.h b/mlir/include/mlir/Compiler/Target.h index 41ac93832d..4b7d6236de 100644 --- a/mlir/include/mlir/Compiler/Target.h +++ b/mlir/include/mlir/Compiler/Target.h @@ -156,8 +156,9 @@ class CompilerTarget { /// /// The reported name is retained verbatim while /// @ref canonicalName contains its normalized compiler spelling. Operations - /// are available throughout the target; site tuples carry optional - /// site-specific calibration data only. + /// are available throughout the target unless explicit ordered applicable + /// site tuples restrict their placement. Site tuples carry optional + /// site-specific calibration data independently of applicability. class Operation { public: /// The accepted number of qubits for an operation capability. @@ -191,18 +192,22 @@ class CompilerTarget { }; /// Create a validated operation capability. - [[nodiscard]] static llvm::Expected - create(std::string name, size_t arity, size_t numParameters, - std::vector siteTuples = {}, - std::optional duration = std::nullopt, - std::optional fidelity = std::nullopt); + [[nodiscard]] static llvm::Expected create( + std::string name, size_t arity, size_t numParameters, + std::vector siteTuples = {}, + std::optional duration = std::nullopt, + std::optional fidelity = std::nullopt, + std::optional>> applicableSiteTuples = + std::nullopt); /// Create a validated operation capability. - [[nodiscard]] static llvm::Expected - create(std::string name, Arity arity, size_t numParameters, - std::vector siteTuples = {}, - std::optional duration = std::nullopt, - std::optional fidelity = std::nullopt); + [[nodiscard]] static llvm::Expected create( + std::string name, Arity arity, size_t numParameters, + std::vector siteTuples = {}, + std::optional duration = std::nullopt, + std::optional fidelity = std::nullopt, + std::optional>> applicableSiteTuples = + std::nullopt); /// Return the exact reported operation name. [[nodiscard]] llvm::StringRef name() const noexcept; @@ -219,6 +224,13 @@ class CompilerTarget { /// Return ordered site-specific calibration data. [[nodiscard]] llvm::ArrayRef siteTuples() const noexcept; + /// Return whether the operation defines explicit ordered applicability. + [[nodiscard]] bool hasExplicitApplicability() const noexcept; + + /// Return the explicitly applicable ordered target-site tuples. + [[nodiscard]] llvm::ArrayRef> + applicableSiteTuples() const noexcept; + /// Return the raw default operation duration, if available. [[nodiscard]] std::optional duration() const noexcept; @@ -226,9 +238,11 @@ class CompilerTarget { [[nodiscard]] std::optional fidelity() const noexcept; private: - Operation(std::string name, std::string canonicalName, Arity arity, - size_t numParameters, std::vector siteTuples, - std::optional duration, std::optional fidelity); + Operation( + std::string name, std::string canonicalName, Arity arity, + size_t numParameters, std::vector siteTuples, + std::optional duration, std::optional fidelity, + std::optional>> applicableSiteTuples); std::string name_; std::string canonicalName_; @@ -237,6 +251,7 @@ class CompilerTarget { std::vector siteTuples_; std::optional duration_; std::optional fidelity_; + std::optional>> applicableSiteTuples_; }; /// Native-operation support. @@ -390,12 +405,25 @@ class CompilerTarget { supportsOperation(llvm::StringRef name, size_t arity, std::optional numParameters = std::nullopt) const; + /// Return whether an operation capability is supported on ordered sites. + [[nodiscard]] bool supportsOperation(llvm::StringRef name, size_t arity, + std::optional numParameters, + llvm::ArrayRef sites) const; + /// Return whether a QCO operation is supported. [[nodiscard]] bool supports(::mlir::Operation* operation) const; - /// Return whether a recognized gate is supported. + /// Return whether a QCO operation is supported on ordered target sites. + [[nodiscard]] bool supports(::mlir::Operation* operation, + llvm::ArrayRef sites) const; + + /// Return whether a recognized gate is supported by the target. [[nodiscard]] bool supports(GateKind gate) const; + /// Return whether a recognized gate is supported on ordered target sites. + [[nodiscard]] bool supports(GateKind gate, + llvm::ArrayRef sites) const; + /// Return the recognized gates supported by the target. [[nodiscard]] llvm::ArrayRef supportedGates() const noexcept; diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 1974f80faf..0d974a7a16 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -146,15 +146,17 @@ def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { - `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))` + - `h(n) = sum(pow(lambda, i) * h(gate, p) for [i, gate] in enumerate(window))` Where: - `p` is the dynamic-to-static mapping associated with search node `n`. - - `layers` is an array of layers with size `1 + nlookahead`. + - `window` contains at most `1 + nlookahead` two-qubit operations in program order. - `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)` + - `h(gate, p)` is `dist(p[gate.first], p[gate.second]) - 1` for nonadjacent operands, zero for an adjacent native operand order, one when only the reverse operand order is native, and infinity when neither adjacent order is supported. + + Routing uses the undirected connectivity underlying the target topology, while gate costs and executability retain the semantic operand order. Inserted SWAP operations are ordered so that their eventual target-basis entanglers use a legal direction. 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 diff --git a/mlir/lib/Compiler/QDMIAdapter.cpp b/mlir/lib/Compiler/QDMIAdapter.cpp index ad215d07e7..5171b524ec 100644 --- a/mlir/lib/Compiler/QDMIAdapter.cpp +++ b/mlir/lib/Compiler/QDMIAdapter.cpp @@ -15,7 +15,6 @@ #include "qdmi/driver/Driver.hpp" #include -#include #include #include #include @@ -127,15 +126,6 @@ allToAllCouplingCount(size_t numSites) { return llvm::checkedMulUnsigned(first, second); } -[[nodiscard]] static bool -isSwapInvariantOperation(llvm::StringRef operationName) { - const auto canonicalName = operationName.trim().lower(); - return llvm::StringSwitch(canonicalName) - .Cases({"cz", "swap", "iswap"}, true) - .Cases({"rxx", "ryy", "rzz"}, true) - .Default(false); -} - [[nodiscard]] static llvm::Error validateHomogeneousSupport( const qdmi::Operation& operation, size_t arity, const std::vector& flattenedSites, @@ -257,25 +247,10 @@ isSwapInvariantOperation(llvm::StringRef operationName) { return supportedCouplings.contains(coupling); }); } - if (auto error = requireRepresentableOperation( - coversTarget, deviceName, operationName, - couplings ? "support is not homogeneous across all topology edges" - : "support is not homogeneous across all-to-all site " - "pairs")) { - return error; - } - return requireRepresentableOperation( - isSwapInvariantOperation(operationName) || - std::ranges::all_of( - supportedCouplings, - [&](const auto& coupling) { - return reportedTuples.contains(coupling) && - reportedTuples.contains(CompilerTarget::Coupling{ - coupling.second, coupling.first}); - }), - deviceName, operationName, - "both orientations must be available on every supported site pair"); + coversTarget, deviceName, operationName, + couplings ? "support is not homogeneous across all topology edges" + : "support is not homogeneous across all-to-all site pairs"); } [[nodiscard]] static llvm::Expected> @@ -334,6 +309,27 @@ snapshotSiteTuples(const qdmi::Operation& operation, size_t arity, return siteTuples; } +[[nodiscard]] static llvm::Expected< + std::vector>> +snapshotApplicableSiteTuples(size_t arity, + const std::vector& flattenedSites) { + std::vector> applicableSiteTuples; + applicableSiteTuples.reserve(flattenedSites.size() / arity); + for (size_t offset = 0; offset < flattenedSites.size(); offset += arity) { + std::vector siteIds; + siteIds.reserve(arity); + for (size_t index = 0; index < arity; ++index) { + auto siteId = checkedSiteId(flattenedSites[offset + index].getIndex()); + if (!siteId) { + return siteId.takeError(); + } + siteIds.emplace_back(*siteId); + } + applicableSiteTuples.emplace_back(std::move(siteIds)); + } + return applicableSiteTuples; +} + [[nodiscard]] static llvm::Expected snapshotOperations( const std::vector& operations, @@ -372,6 +368,8 @@ snapshotOperations( const auto duration = operation.getDuration(); const auto fidelity = operation.getFidelity(); std::vector siteTuples; + std::optional>> + applicableSiteTuples; if (*arity == 0) { if (auto error = requireRepresentableOperation( !flattenedSites || flattenedSites->empty(), deviceName, @@ -391,6 +389,13 @@ snapshotOperations( return tuples.takeError(); } siteTuples = std::move(*tuples); + if (!hasArbitraryPositiveControls) { + auto applicable = snapshotApplicableSiteTuples(*arity, *flattenedSites); + if (!applicable) { + return applicable.takeError(); + } + applicableSiteTuples = std::move(*applicable); + } } if (auto error = requireRepresentableOperation( !hasArbitraryPositiveControls || siteTuples.empty(), deviceName, @@ -404,7 +409,8 @@ snapshotOperations( : CompilerTarget::Operation::Arity::fixed(*arity); auto targetOperation = CompilerTarget::Operation::create( operation.getName(), targetArity, operation.getParametersNum(), - std::move(siteTuples), duration, fidelity); + std::move(siteTuples), duration, fidelity, + std::move(applicableSiteTuples)); if (!targetOperation) { return targetOperation.takeError(); } diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index d72309d422..5289e1e21a 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -14,6 +14,7 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include #include #include #include @@ -308,15 +309,18 @@ CompilerTarget::Operation::Arity::Arity(Kind kind, size_t value) noexcept llvm::Expected CompilerTarget::Operation::create( std::string name, size_t arity, size_t numParameters, std::vector siteTuples, std::optional duration, - std::optional fidelity) { + std::optional fidelity, + std::optional>> applicableSiteTuples) { return create(std::move(name), Arity::fixed(arity), numParameters, - std::move(siteTuples), duration, fidelity); + std::move(siteTuples), duration, fidelity, + std::move(applicableSiteTuples)); } llvm::Expected CompilerTarget::Operation::create( std::string name, Arity arity, size_t numParameters, std::vector siteTuples, std::optional duration, - std::optional fidelity) { + std::optional fidelity, + std::optional>> applicableSiteTuples) { auto canonicalName = canonicalOperationName(name); if (canonicalName.empty()) { return invalidTarget("Compiler target operation name must not be empty"); @@ -351,20 +355,48 @@ llvm::Expected CompilerTarget::Operation::create( } uniqueSiteCombinations.emplace_back(siteTuple.sites()); } + + SmallVector> uniqueApplicableSiteCombinations; + if (applicableSiteTuples) { + for (const auto& sites : *applicableSiteTuples) { + if (!arity.accepts(sites.size())) { + return invalidTarget("Compiler target operation applicable site tuple " + "does not match its arity"); + } + std::unordered_set uniqueSites; + for (const auto site : sites) { + if (site < 0) { + return invalidTarget("Compiler target operation applicable site " + "tuple contains a negative site ID"); + } + if (!uniqueSites.insert(site).second) { + return invalidTarget("Compiler target operation applicable site " + "tuple contains a duplicate site"); + } + } + if (llvm::is_contained(uniqueApplicableSiteCombinations, + ArrayRef(sites))) { + return invalidTarget("Compiler target operation contains a duplicate " + "applicable site tuple"); + } + uniqueApplicableSiteCombinations.emplace_back(sites); + } + } return Operation(std::move(name), std::move(canonicalName), arity, - numParameters, std::move(siteTuples), duration, fidelity); + numParameters, std::move(siteTuples), duration, fidelity, + std::move(applicableSiteTuples)); } -CompilerTarget::Operation::Operation(std::string name, - std::string canonicalName, Arity arity, - size_t numParameters, - std::vector siteTuples, - std::optional duration, - std::optional fidelity) +CompilerTarget::Operation::Operation( + std::string name, std::string canonicalName, Arity arity, + size_t numParameters, std::vector siteTuples, + std::optional duration, std::optional fidelity, + std::optional>> applicableSiteTuples) : name_(std::move(name)), canonicalName_(std::move(canonicalName)), arity_(arity), numParameters_(numParameters), siteTuples_(std::move(siteTuples)), duration_(duration), - fidelity_(fidelity) {} + fidelity_(fidelity), + applicableSiteTuples_(std::move(applicableSiteTuples)) {} StringRef CompilerTarget::Operation::name() const noexcept { return name_; } @@ -386,6 +418,18 @@ CompilerTarget::Operation::siteTuples() const noexcept { return siteTuples_; } +bool CompilerTarget::Operation::hasExplicitApplicability() const noexcept { + return applicableSiteTuples_.has_value(); +} + +ArrayRef> +CompilerTarget::Operation::applicableSiteTuples() const noexcept { + if (!applicableSiteTuples_) { + return {}; + } + return *applicableSiteTuples_; +} + std::optional CompilerTarget::Operation::duration() const noexcept { return duration_; } @@ -437,12 +481,24 @@ struct CompilerTarget::Storage { [[nodiscard]] llvm::Error initialize(); + [[nodiscard]] bool isApplicable(size_t operationIndex, size_t arity) const; + [[nodiscard]] bool isApplicable(size_t operationIndex, + ArrayRef orderedSites) const; [[nodiscard]] bool supportsOperation(StringRef name, size_t arity, std::optional numParameters) const; + [[nodiscard]] bool supportsOperation(StringRef name, size_t arity, + std::optional numParameters, + ArrayRef orderedSites) const; [[nodiscard]] bool supportsVariadicOperation(StringRef name, size_t arity, std::optional numParameters) const; + [[nodiscard]] bool + supportsVariadicOperation(StringRef name, size_t arity, + std::optional numParameters, + ArrayRef orderedSites) const; + [[nodiscard]] bool supportsGate(GateKind gate, + ArrayRef orderedSites) const; [[nodiscard]] std::optional resolveSynthesisBasis() const; std::optional name; @@ -458,6 +514,8 @@ struct CompilerTarget::Storage { NativeOperations::Kind nativeOperationsKind; SmallVector operations; llvm::StringMap> capabilities; + std::vector>> explicitOneQubitSites; + std::vector>> explicitTwoQubitSites; SmallVector supportedGates; std::optional basis; }; @@ -571,6 +629,8 @@ llvm::Error CompilerTarget::Storage::initialize() { } if (nativeOperationsKind == NativeOperations::Kind::Explicit) { + explicitOneQubitSites.resize(operations.size()); + explicitTwoQubitSites.resize(operations.size()); for (const auto [index, operation] : llvm::enumerate(operations)) { if (operation.arity().value() > sites.size()) { if (operation.arity().kind() == Operation::Arity::Kind::Variadic) { @@ -588,6 +648,26 @@ llvm::Error CompilerTarget::Storage::initialize() { "references an unknown site"); } } + if (operation.hasExplicitApplicability()) { + auto& oneQubitSites = explicitOneQubitSites[index].emplace(); + auto& twoQubitSites = explicitTwoQubitSites[index].emplace(); + oneQubitSites.reserve(operation.applicableSiteTuples().size()); + twoQubitSites.reserve(operation.applicableSiteTuples().size()); + for (const auto& applicableSites : operation.applicableSiteTuples()) { + if (llvm::any_of(applicableSites, [&](const auto site) { + return !siteToVertex.contains(site); + })) { + return invalidTarget("Compiler target operation applicable site " + "tuple references an unknown site"); + } + if (applicableSites.size() == 1) { + oneQubitSites.insert(applicableSites.front()); + } else if (applicableSites.size() == 2) { + twoQubitSites.insert( + {applicableSites.front(), applicableSites.back()}); + } + } + } capabilities[operation.canonicalName()].emplace_back(index); } } @@ -625,6 +705,43 @@ llvm::Error CompilerTarget::Storage::initialize() { return llvm::Error::success(); } +bool CompilerTarget::Storage::isApplicable(size_t operationIndex, + size_t arity) const { + const auto& operation = operations[operationIndex]; + if (!operation.hasExplicitApplicability()) { + return true; + } + if (arity == 1) { + return !explicitOneQubitSites[operationIndex]->empty(); + } + if (arity == 2) { + return !explicitTwoQubitSites[operationIndex]->empty(); + } + return llvm::any_of(operation.applicableSiteTuples(), + [&](const auto& applicableSites) { + return applicableSites.size() == arity; + }); +} + +bool CompilerTarget::Storage::isApplicable( + size_t operationIndex, ArrayRef orderedSites) const { + const auto& operation = operations[operationIndex]; + if (!operation.hasExplicitApplicability()) { + return true; + } + if (orderedSites.size() == 1) { + return explicitOneQubitSites[operationIndex]->contains(orderedSites[0]); + } + if (orderedSites.size() == 2) { + return explicitTwoQubitSites[operationIndex]->contains( + {orderedSites[0], orderedSites[1]}); + } + return llvm::any_of( + operation.applicableSiteTuples(), [&](const auto& applicableSites) { + return ArrayRef(applicableSites) == orderedSites; + }); +} + bool CompilerTarget::Storage::supportsOperation( StringRef operationName, size_t arity, std::optional numParameters) const { @@ -642,7 +759,37 @@ bool CompilerTarget::Storage::supportsOperation( return llvm::any_of(found->second, [&](const auto index) { const auto& operation = operations[index]; return operation.arity().accepts(arity) && - (!numParameters || operation.numParameters() == *numParameters); + (!numParameters || operation.numParameters() == *numParameters) && + isApplicable(index, arity); + }); +} + +bool CompilerTarget::Storage::supportsOperation( + StringRef operationName, size_t arity, std::optional numParameters, + ArrayRef orderedSites) const { + const auto canonical = canonicalOperationName(operationName); + if (canonical.empty() || arity > sites.size() || + orderedSites.size() != arity) { + return false; + } + for (const auto [index, site] : llvm::enumerate(orderedSites)) { + if (!siteToVertex.contains(site) || + llvm::is_contained(orderedSites.take_front(index), site)) { + return false; + } + } + if (nativeOperationsKind == NativeOperations::Kind::Unrestricted) { + return true; + } + const auto found = capabilities.find(canonical); + if (found == capabilities.end()) { + return false; + } + return llvm::any_of(found->second, [&](const auto index) { + const auto& operation = operations[index]; + return operation.arity().accepts(arity) && + (!numParameters || operation.numParameters() == *numParameters) && + isApplicable(index, orderedSites); }); } @@ -664,40 +811,116 @@ bool CompilerTarget::Storage::supportsVariadicOperation( const auto& operation = operations[index]; return operation.arity().kind() == Operation::Arity::Kind::Variadic && operation.arity().accepts(arity) && - (!numParameters || operation.numParameters() == *numParameters); + (!numParameters || operation.numParameters() == *numParameters) && + isApplicable(index, arity); + }); +} + +bool CompilerTarget::Storage::supportsVariadicOperation( + StringRef operationName, size_t arity, std::optional numParameters, + ArrayRef orderedSites) const { + const auto canonical = canonicalOperationName(operationName); + if (canonical.empty() || arity > sites.size() || + orderedSites.size() != arity) { + return false; + } + for (const auto [index, site] : llvm::enumerate(orderedSites)) { + if (!siteToVertex.contains(site) || + llvm::is_contained(orderedSites.take_front(index), site)) { + return false; + } + } + if (nativeOperationsKind == NativeOperations::Kind::Unrestricted) { + return true; + } + const auto found = capabilities.find(canonical); + if (found == capabilities.end()) { + return false; + } + return llvm::any_of(found->second, [&](const auto index) { + const auto& operation = operations[index]; + return operation.arity().kind() == Operation::Arity::Kind::Variadic && + operation.arity().accepts(arity) && + (!numParameters || operation.numParameters() == *numParameters) && + isApplicable(index, orderedSites); }); } +bool CompilerTarget::Storage::supportsGate( + GateKind gate, ArrayRef orderedSites) const { + if ((gate == GateKind::CX && + supportsVariadicOperation("x", 2, 0, orderedSites)) || + (gate == GateKind::CZ && + supportsVariadicOperation("z", 2, 0, orderedSites))) { + return true; + } + const auto specification = + std::ranges::find_if(GATE_SPECIFICATIONS, [&](const auto& candidate) { + return candidate.kind == gate; + }); + assert(specification != GATE_SPECIFICATIONS.end() && + "unknown compiler target gate"); + return supportsOperation(specification->name, specification->arity, + specification->numParameters, orderedSites); +} + std::optional CompilerTarget::Storage::resolveSynthesisBasis() const { - const auto supports = [&](GateKind gate) { - return llvm::is_contained(supportedGates, gate); + const auto supportsOnEverySite = [&](GateKind gate) { + return llvm::all_of(siteIds, [&](SiteId site) { + return supportsGate(gate, ArrayRef(&site, 1)); + }); }; std::optional singleQubit; - if (supports(GateKind::U)) { + if (supportsOnEverySite(GateKind::U)) { singleQubit = SingleQubitBasis::U; - } else if (supports(GateKind::X) && supports(GateKind::SX) && - supports(GateKind::RZ)) { + } else if (supportsOnEverySite(GateKind::X) && + supportsOnEverySite(GateKind::SX) && + supportsOnEverySite(GateKind::RZ)) { singleQubit = SingleQubitBasis::ZSXX; - } else if (supports(GateKind::R)) { + } else if (supportsOnEverySite(GateKind::R)) { singleQubit = SingleQubitBasis::R; - } else if (supports(GateKind::RX) && supports(GateKind::RZ)) { + } else if (supportsOnEverySite(GateKind::RX) && + supportsOnEverySite(GateKind::RZ)) { singleQubit = SingleQubitBasis::XZX; - } else if (supports(GateKind::RX) && supports(GateKind::RY)) { + } else if (supportsOnEverySite(GateKind::RX) && + supportsOnEverySite(GateKind::RY)) { singleQubit = SingleQubitBasis::XYX; - } else if (supports(GateKind::RY) && supports(GateKind::RZ)) { + } else if (supportsOnEverySite(GateKind::RY) && + supportsOnEverySite(GateKind::RZ)) { singleQubit = SingleQubitBasis::ZYZ; } + const auto supportsOnEveryCoupling = [&](GateKind gate) { + if (sites.size() < 2) { + return false; + } + const auto supportsPair = [&](SiteId source, SiteId target) { + const std::array forward{source, target}; + const std::array reverse{target, source}; + return supportsGate(gate, forward) || supportsGate(gate, reverse); + }; + if (connectivityKind == Connectivity::Kind::Explicit) { + return llvm::all_of(couplings, [&](const auto& coupling) { + return supportsPair(coupling.first, coupling.second); + }); + } + for (size_t source = 0; source < siteIds.size(); ++source) { + for (size_t target = source + 1; target < siteIds.size(); ++target) { + if (!supportsPair(siteIds[source], siteIds[target])) { + return false; + } + } + } + return true; + }; + constexpr std::array entanglerPreference{ GateKind::RXX, GateKind::RYY, GateKind::RZX, GateKind::RZZ, GateKind::ISWAP, GateKind::CZ, GateKind::CX, GateKind::ECR, }; - // NOLINTNEXTLINE(readability-qualified-auto) const auto entangler = - std::ranges::find_if(entanglerPreference, [&](const auto candidate) { - return supports(candidate); - }); + std::ranges::find_if(entanglerPreference, supportsOnEveryCoupling); if (!singleQubit || entangler == entanglerPreference.end()) { return std::nullopt; } @@ -836,6 +1059,18 @@ CompilerTarget::create(const mqt::CompilationTargetAttr attribute) { siteTuples.emplace_back(std::move(*siteTuple)); } + std::optional>> applicableSiteTuples; + if (operationAttr.getApplicability() == + mqt::OperationApplicabilityKind::Explicit) { + applicableSiteTuples.emplace(); + applicableSiteTuples->reserve( + operationAttr.getApplicableSiteTuples().size()); + for (const auto tupleAttr : operationAttr.getApplicableSiteTuples()) { + applicableSiteTuples->emplace_back(tupleAttr.getSites().begin(), + tupleAttr.getSites().end()); + } + } + std::optional fidelity; if (const auto fidelityAttr = operationAttr.getFidelity()) { fidelity = fidelityAttr.getValueAsDouble(); @@ -849,7 +1084,8 @@ CompilerTarget::create(const mqt::CompilationTargetAttr attribute) { auto operation = Operation::create( operationAttr.getName().getValue().str(), arity, static_cast(operationAttr.getNumParameters()), - std::move(siteTuples), operationAttr.getDuration(), fidelity); + std::move(siteTuples), operationAttr.getDuration(), fidelity, + std::move(applicableSiteTuples)); if (!operation) { return operation.takeError(); } @@ -986,6 +1222,13 @@ bool CompilerTarget::supportsOperation( return storage_->supportsOperation(operationName, arity, numParameters); } +bool CompilerTarget::supportsOperation(StringRef operationName, size_t arity, + std::optional numParameters, + ArrayRef sites) const { + return storage_->supportsOperation(operationName, arity, numParameters, + sites); +} + bool CompilerTarget::supports(::mlir::Operation* operation) const { if (operation == nullptr) { return false; @@ -1033,10 +1276,62 @@ bool CompilerTarget::supports(::mlir::Operation* operation) const { return false; } +bool CompilerTarget::supports(::mlir::Operation* operation, + ArrayRef sites) const { + if (operation == nullptr) { + return false; + } + + if (auto unitary = dyn_cast(operation)) { + if (isa(operation)) { + return true; + } + if (auto controlled = dyn_cast(operation)) { + if (controlled.getNumControls() == 0 || + controlled.getNumBodyUnitaries() != 1) { + return false; + } + auto body = controlled.getBodyUnitary(0); + if (body.getNumQubits() != controlled.getNumTargets()) { + return false; + } + if (storage_->supportsVariadicOperation(body.getBaseSymbol(), + controlled.getNumQubits(), + body.getNumParams(), sites)) { + return true; + } + if (controlled.getNumControls() != 1 || controlled.getNumTargets() != 1) { + return false; + } + if (isa(body.getOperation())) { + return storage_->supportsOperation("cx", 2, 0, sites); + } + if (isa(body.getOperation())) { + return storage_->supportsOperation("cz", 2, 0, sites); + } + return false; + } + return storage_->supportsOperation(unitary.getBaseSymbol(), + unitary.getNumQubits(), + unitary.getNumParams(), sites); + } + if (isa(operation)) { + return storage_->supportsOperation("measure", 1, 0, sites); + } + if (isa(operation)) { + return storage_->supportsOperation("reset", 1, 0, sites); + } + return false; +} + bool CompilerTarget::supports(GateKind gate) const { return llvm::is_contained(storage_->supportedGates, gate); } +bool CompilerTarget::supports(GateKind gate, ArrayRef sites) const { + return storage_->supportsGate(gate, sites); +} + ArrayRef CompilerTarget::supportedGates() const noexcept { return storage_->supportedGates; } @@ -1094,6 +1389,13 @@ CompilerTarget::materialize(MLIRContext& context) const { &context, siteTuple.sites(), siteTuple.duration(), fidelityAttr)); } + SmallVector applicableSiteTupleAttrs; + applicableSiteTupleAttrs.reserve(operation.applicableSiteTuples().size()); + for (const auto& applicableSites : operation.applicableSiteTuples()) { + applicableSiteTupleAttrs.emplace_back( + mqt::ApplicableSiteTupleAttr::get(&context, applicableSites)); + } + FloatAttr fidelityAttr; if (const auto fidelity = operation.fidelity()) { fidelityAttr = builder.getF64FloatAttr(*fidelity); @@ -1104,10 +1406,14 @@ CompilerTarget::materialize(MLIRContext& context) const { : mqt::OperationArityKind::Variadic; const auto arityAttr = mqt::OperationArityAttr::get( &context, arityKind, operation.arity().value()); + const auto applicability = + operation.hasExplicitApplicability() + ? mqt::OperationApplicabilityKind::Explicit + : mqt::OperationApplicabilityKind::Unrestricted; operationAttrs.emplace_back(mqt::NativeOperationAttr::get( &context, builder.getStringAttr(operation.name()), arityAttr, operation.numParameters(), siteTupleAttrs, operation.duration(), - fidelityAttr)); + fidelityAttr, applicability, applicableSiteTupleAttrs)); } const auto connectivity = connectivityKind() == Connectivity::Kind::AllToAll diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 3c3a81affc..4a14afeebb 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -54,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -363,7 +364,13 @@ struct PlacementPass final struct MappingPass : impl::MappingPassBase { private: using IndexPairType = std::pair; - using Window = SmallVector; + + struct WindowEntry { + Operation* operation; + IndexPairType programs; + }; + + using Window = SmallVector; enum class RoutingMode : bool { Cold, Hot }; @@ -416,6 +423,84 @@ struct MappingPass : impl::MappingPassBase { } }; + /// Return whether the operation ultimately emitted for a window entry is + /// available on the ordered target sites. + [[nodiscard]] static bool + supportsOnSites(const WindowEntry& entry, const CompilerTarget& target, + const ArrayRef sites) { + if (target.supports(entry.operation)) { + return target.supports(entry.operation, sites); + } + if (const auto basis = target.synthesisBasis()) { + return target.supports(basis->entangler, sites); + } + // Mapping remains usable as a topology-only pass for targets without a + // synthesis basis. Target-native synthesis diagnoses the missing basis. + return true; + } + + /// Return the target capability used to route a native two-qubit operation. + [[nodiscard]] static StringRef routingSymbol(Operation* operation) { + if (auto controlled = dyn_cast(operation); + controlled && controlled.getNumControls() == 1 && + controlled.getNumTargets() == 1 && + controlled.getNumBodyUnitaries() == 1) { + Operation* body = controlled.getBodyUnitary(0).getOperation(); + if (isa(body)) { + return "cx"; + } + if (isa(body)) { + return "cz"; + } + } + return cast(operation).getBaseSymbol(); + } + + /// Return whether consecutive entries have identical routing constraints. + [[nodiscard]] static bool + hasEquivalentRoutingBehavior(const WindowEntry& lhs, const WindowEntry& rhs, + const CompilerTarget& target) { + if (lhs.programs != rhs.programs) { + return false; + } + + const auto lhsIsNative = target.supports(lhs.operation); + const auto rhsIsNative = target.supports(rhs.operation); + if (lhsIsNative != rhsIsNative) { + return false; + } + if (!lhsIsNative) { + return true; + } + + auto lhsUnitary = cast(lhs.operation); + auto rhsUnitary = cast(rhs.operation); + return routingSymbol(lhs.operation) == routingSymbol(rhs.operation) && + lhsUnitary.getNumParams() == rhsUnitary.getNumParams(); + } + + /// Return the routing cost of an ordered two-qubit operation. + [[nodiscard]] static float routingCost(const WindowEntry& entry, + const Layout& layout, + const CompilerTarget& target) { + const auto [prog0, prog1] = entry.programs; + const auto [hw0, hw1] = layout.getHardwareIndices(prog0, prog1); + const auto distance = target.distanceBetween(hw0, hw1); + if (distance > 1) { + return static_cast(distance - 1); + } + + std::array sites{target.siteForVertex(hw0), target.siteForVertex(hw1)}; + if (supportsOnSites(entry, target, sites)) { + return 0.F; + } + std::ranges::reverse(sites); + if (supportsOnSites(entry, target, sites)) { + return 1.F; + } + return std::numeric_limits::infinity(); + } + /// Describes a node in the A* search graph. struct Node { struct ComparePointer { @@ -447,11 +532,9 @@ struct MappingPass : impl::MappingPassBase { /// Return true, if the current SWAP sequence makes all gates in the front /// executable. - [[nodiscard]] bool isGoal(const IndexPairType& front, + [[nodiscard]] bool isGoal(const WindowEntry& front, const CompilerTarget& target) const { - const auto [hw0, hw1] = - layout.getHardwareIndices(front.first, front.second); - return target.areAdjacent(hw0, hw1); + return routingCost(front, layout, target) == 0.F; } private: @@ -473,11 +556,8 @@ struct MappingPass : impl::MappingPassBase { float costs{0}; float decay{1.}; - for (const auto& [i, progs] : enumerate(window)) { - const auto [prog0, prog1] = progs; - const auto [hw0, hw1] = layout.getHardwareIndices(prog0, prog1); - const size_t nswaps = target.distanceBetween(hw0, hw1) - 1; - costs += decay * static_cast(nswaps); + for (const auto& entry : window) { + costs += decay * routingCost(entry, layout, target); decay *= params.lambda; } return costs; @@ -884,7 +964,7 @@ struct MappingPass : impl::MappingPassBase { constexpr size_t cap = 25'000'000UL; const size_t b = target->maxDegree() * ((target->numSites() + 1) / 2); - const size_t budget = std::min(b * b * b, cap); + const size_t budget = std::max(2, std::min(b * b * b, cap)); const Parameters params{.alpha = alpha, .lambda = lambda}; @@ -943,9 +1023,13 @@ struct MappingPass : impl::MappingPassBase { // between two neighboring hardware qubits. expansionSet.clear(); - for (const auto& [q0, q1] = window.front(); const auto prog : {q0, q1}) { + for (const auto& [q0, q1] = window.front().programs; + const auto prog : {q0, q1}) { const auto hw0 = curr->layout.getHardwareIndex(prog); target->forEachNeighbour(hw0, [&](const auto hw1) { + if (!shouldReverseSWAPOperands(hw0, hw1).has_value()) { + return; + } // Ensure consistent hashing/comparison. const IndexPairType swap = std::minmax(hw0, hw1); if (is_contained(expansionSet, swap)) { @@ -1088,6 +1172,29 @@ struct MappingPass : impl::MappingPassBase { return curr; } + /// Return a two-qubit operation's program indices in semantic operand order. + /// Wire traversal records ready indices in wire order. The iterator's qubit + /// is the operation result on that wire in both traversal directions, so use + /// the ordered results to recover the operation's operand order. + [[nodiscard]] static IndexPairType + orderedPrograms(Operation* operation, const ArrayRef indices, + const Wires& wires, const WireInfos& infos) { + auto unitary = cast(operation); + assert(unitary.getNumQubits() == 2 && indices.size() == 2 && + "expected a ready two-qubit operation"); + + const auto programForOutput = [&](Value output) { + const auto found = llvm::find_if(indices, [&](const size_t index) { + return wires[index].qubit() == output; + }); + assert(found != indices.end() && "operation result has no ready wire"); + return infos.lookupProgram(*found); + }; + + return {programForOutput(unitary.getOutputQubit(0)), + programForOutput(unitary.getOutputQubit(1))}; + } + /// Collect a routing lookahead window of up to `1 + nlookahead` ready /// two-qubit gates, while skipping qubit-pair blocks. template @@ -1117,7 +1224,8 @@ struct MappingPass : impl::MappingPassBase { const IndexPairType gate = std::minmax(prog0, prog1); if (!is_contained(prev, gate)) { - window.emplace_back(gate); + window.emplace_back(WindowEntry{ + op, orderedPrograms(op, indices, wires, infos)}); if (window.size() == 1 + nlookahead) { return WalkResult::interrupt(); } @@ -1141,11 +1249,40 @@ struct MappingPass : impl::MappingPassBase { /// Insert SWAP operations, exchanging two qubits, virtually /// (`RoutingMode::Cold`) or into the IR (`RoutingMode::Hot`). The function /// expects that each wire points at the correct insertion point. + [[nodiscard]] std::optional + shouldReverseSWAPOperands(const size_t hw0, const size_t hw1) const { + const auto basis = target->synthesisBasis(); + if (!basis) { + return false; + } + std::array sites{target->siteForVertex(hw0), target->siteForVertex(hw1)}; + if (target->supports(basis->entangler, sites)) { + return false; + } + std::ranges::reverse(sites); + if (target->supports(basis->entangler, sites)) { + return true; + } + return std::nullopt; + } + template - static void insertSWAPs(ArrayRef swaps, RoutingBundle& bundle, - Statistics& stats, IRRewriter* rewriter) { - auto& [wires, infos, layout] = bundle; + LogicalResult insertSWAPs(ArrayRef swaps, + RoutingBundle& bundle, Statistics& stats, + IRRewriter* rewriter) { + SmallVector reverseOperands; + reverseOperands.reserve(swaps.size()); for (const auto& [hw0, hw1] : swaps) { + const auto reverse = shouldReverseSWAPOperands(hw0, hw1); + if (!reverse) { + return failure(); + } + reverseOperands.emplace_back(*reverse); + } + + auto& [wires, infos, layout] = bundle; + for (size_t index = 0; index < swaps.size(); ++index) { + const auto [hw0, hw1] = swaps[index]; const auto [prog0, prog1] = layout.getProgramIndices(hw0, hw1); if constexpr (Mode == RoutingMode::Hot) { @@ -1157,17 +1294,22 @@ struct MappingPass : impl::MappingPassBase { auto& w0 = wires[i0]; auto& w1 = wires[i1]; - auto in0 = w0.qubit(); - auto in1 = w1.qubit(); + const auto in0 = w0.qubit(); + const auto in1 = w1.qubit(); + auto first = in0; + auto second = in1; + if (reverseOperands[index]) { + std::swap(first, second); + } rewriter->setInsertionPointAfterValue(in0); // Valid bc. Hot => Forward. - auto swapOp = SWAPOp::create(*rewriter, in0.getLoc(), in0, in1); + auto swapOp = SWAPOp::create(*rewriter, first.getLoc(), first, second); - auto out0 = swapOp.getQubit0Out(); - auto out1 = swapOp.getQubit1Out(); + const auto firstOut = swapOp.getQubit0Out(); + const auto secondOut = swapOp.getQubit1Out(); - rewriter->replaceAllUsesExcept(in0, out1, swapOp); - rewriter->replaceAllUsesExcept(in1, out0, swapOp); + rewriter->replaceAllUsesExcept(first, secondOut, swapOp); + rewriter->replaceAllUsesExcept(second, firstOut, swapOp); infos.swap(prog0, prog1); @@ -1179,6 +1321,7 @@ struct MappingPass : impl::MappingPassBase { } stats.nswaps += swaps.size(); + return success(); } /// Advance past all executable gates and return operations with nested @@ -1207,11 +1350,9 @@ struct MappingPass : impl::MappingPassBase { return true; } - const auto prog0 = infos.lookupProgram(indices[0]); - const auto prog1 = infos.lookupProgram(indices[1]); - const auto [hw0, hw1] = - layout.getHardwareIndices(prog0, prog1); - return target->areAdjacent(hw0, hw1); + const WindowEntry entry{ + op, orderedPrograms(op, indices, wires, infos)}; + return routingCost(entry, layout, *target) == 0.F; }) .template Case([](auto&) { return true; }) .template Case([](MeasureOp& m) { @@ -1551,16 +1692,19 @@ struct MappingPass : impl::MappingPassBase { // using the restore (scf::ForOp, scf::While), converge (IfOp), and vote // and restore (IndexSwitchOp) strategies. + bool swapInsertionFailed = false; Layout exit = TypeSwitch(op) .Case([&](scf::ForOp) { const auto swaps = restore(children[0].layout, parent.layout); - insertSWAPs(swaps, children[0], totalStats, rewriter); + swapInsertionFailed = failed( + insertSWAPs(swaps, children[0], totalStats, rewriter)); return parent.layout; }) .template Case([&](scf::WhileOp) { const auto swaps = restore(children[1].layout, parent.layout); - insertSWAPs(swaps, children[1], totalStats, rewriter); + swapInsertionFailed = failed( + insertSWAPs(swaps, children[1], totalStats, rewriter)); // The scf::YieldOp is the terminator in the before region and // thus determines the final output layout. return children[0].layout; @@ -1568,8 +1712,11 @@ struct MappingPass : impl::MappingPassBase { .template Case([&](IfOp) { const auto [convergedLayout, fst, snd] = converge(children[0].layout, children[1].layout); - insertSWAPs(fst, children[0], totalStats, rewriter); - insertSWAPs(snd, children[1], totalStats, rewriter); + swapInsertionFailed = + failed(insertSWAPs(fst, children[0], totalStats, + rewriter)) || + failed(insertSWAPs(snd, children[1], totalStats, + rewriter)); return convergedLayout; }) .template Case([&](IndexSwitchOp) { @@ -1579,11 +1726,19 @@ struct MappingPass : impl::MappingPassBase { })); for (RoutingBundle& child : children) { const auto swaps = restore(child.layout, compromise); - insertSWAPs(swaps, child, totalStats, rewriter); + if (failed(insertSWAPs(swaps, child, totalStats, + rewriter))) { + swapInsertionFailed = true; + break; + } } return compromise; }); + if (swapInsertionFailed) { + return failure(); + } + if constexpr (Mode == RoutingMode::Hot) { // Realign terminator values to ensure that i-th input qubit and the // i-th output qubit represent the equivalent hardware qubit. This is @@ -1707,7 +1862,9 @@ struct MappingPass : impl::MappingPassBase { for_each(wires, [](auto& it) { std::ranges::advance(it, -1); }); } - insertSWAPs(*swaps, bundle, stats, rewriter); + if (failed(insertSWAPs(*swaps, bundle, stats, rewriter))) { + return failure(); + } if constexpr (Mode == RoutingMode::Hot) { diff --git a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp index 307ab095fe..d653fe5b85 100644 --- a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp +++ b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp @@ -159,15 +159,23 @@ TEST(CompilerQDMIAdapterTest, SnapshotsHomogeneousHigherArityOperation) { EXPECT_TRUE(target.supportsOperation("ccnot", 3, 0)); } -TEST(CompilerQDMIAdapterTest, RejectsDirectionalOperationWithoutReverseSites) { +TEST(CompilerQDMIAdapterTest, PreservesOneWayDirectionalOperationSupport) { qdmi::DeviceSessionConfig overrides; overrides.deviceConfiguration = qdmi::FileDeviceConfiguration{ MQT_CORE_MLIR_DIRECTIONAL_ONE_WAY_SC_CONFIG}; const auto device = qdmi::Session::openDevice("mqt.sc.default", overrides); - auto target = mlir::compilerTargetFromDevice(device); - ASSERT_FALSE(target); - const auto message = llvm::toString(target.takeError()); - EXPECT_NE(message.find("both orientations"), std::string::npos); + const auto target = llvm::cantFail(mlir::compilerTargetFromDevice(device)); + + ASSERT_EQ(target.couplings().size(), 1); + const auto& cx = findOperation(target, "cx"); + EXPECT_TRUE(cx.hasExplicitSiteTuples()); + ASSERT_EQ(cx.siteTuples().size(), 1); + EXPECT_EQ(cx.siteTuples()[0].sites(), + (llvm::ArrayRef{0, 1})); + EXPECT_FALSE(cx.siteTuples()[0].duration()); + EXPECT_FALSE(cx.siteTuples()[0].fidelity()); + EXPECT_TRUE(target.supports(CompilerTarget::GateKind::CX, {0, 1})); + EXPECT_FALSE(target.supports(CompilerTarget::GateKind::CX, {1, 0})); } TEST(CompilerQDMIAdapterTest, @@ -180,6 +188,7 @@ TEST(CompilerQDMIAdapterTest, ASSERT_EQ(target.couplings().size(), 1); const auto& cx = findOperation(target, "cx"); + EXPECT_TRUE(cx.hasExplicitSiteTuples()); ASSERT_EQ(cx.siteTuples().size(), 2); EXPECT_EQ(cx.siteTuples()[0].sites(), (llvm::ArrayRef{0, 1})); diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index 2955d69e0e..77cd5ddd63 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -398,11 +398,13 @@ TEST(CompilerTargetTest, RoundTripsTypedCompilationTargetAttribute) { valid(Site::create(2, std::nullopt, 120, std::nullopt)), valid(Site::create(11, "right"))}; std::vector operations{ - valid( - Operation::create(" PRX ", 1, 2, - std::vector{valid(SiteTuple::create({7}, 0, 0.99)), - valid(SiteTuple::create({2}, 5, 0.98))}, - 0, 0.97)), + valid(Operation::create( + " PRX ", 1, 2, + std::vector{valid(SiteTuple::create({7}, 0, 0.99)), + valid(SiteTuple::create({2}, 5, 0.98))}, + 0, 0.97, std::vector>{{7}, {2}})), + valid(Operation::create("rx", 1, 1, {}, std::nullopt, std::nullopt, + std::vector>{})), valid(Operation::create("gphase", Arity::fixed(0), 1)), valid(Operation::create("h", Arity::variadic(1), 0))}; const auto target = @@ -417,10 +419,14 @@ TEST(CompilerTargetTest, RoundTripsTypedCompilationTargetAttribute) { EXPECT_EQ(reconstructed.materialize(context), attribute); EXPECT_EQ(reconstructed.couplings(), target.couplings()); EXPECT_EQ(reconstructed.supportsOperation("r", 1, 2), true); + EXPECT_TRUE(reconstructed.supportsOperation("r", 1, 2, {7})); + EXPECT_FALSE(reconstructed.supportsOperation("r", 1, 2, {11})); + EXPECT_TRUE(reconstructed.operations()[1].hasExplicitApplicability()); + EXPECT_TRUE(reconstructed.operations()[1].applicableSiteTuples().empty()); EXPECT_EQ(reconstructed.supportsOperation("gphase", 0, 1), true); EXPECT_EQ(reconstructed.supportsOperation("h", 3, 0), true); - EXPECT_EQ(reconstructed.operations()[1].arity(), Arity::fixed(0)); - EXPECT_EQ(reconstructed.operations()[2].arity(), Arity::variadic(1)); + EXPECT_EQ(reconstructed.operations()[2].arity(), Arity::fixed(0)); + EXPECT_EQ(reconstructed.operations()[3].arity(), Arity::variadic(1)); EXPECT_EQ(reconstructed.synthesisBasis(), target.synthesisBasis()); } @@ -455,6 +461,56 @@ TEST(CompilerTargetTest, RoundTripsSupportedTargetStates) { "Compiler target topology must be connected"); } +TEST(CompilerTargetTest, EnforcesExactOrderedOperationApplicability) { + std::vector sites{valid(Site::create(10)), valid(Site::create(20)), + valid(Site::create(30))}; + const auto globalU = valid(Operation::create("u", 1, 3)); + const auto restrictedX = + valid(Operation::create("x", 1, 0, {}, std::nullopt, std::nullopt, + std::vector>{{10}})); + const auto directionalCX = valid( + Operation::create("cx", 2, 0, {}, std::nullopt, std::nullopt, + std::vector>{{10, 20}, {20, 30}})); + const auto exactCZ = + valid(Operation::create("cz", 2, 0, {}, std::nullopt, std::nullopt, + std::vector>{{10, 20}})); + const auto unavailableECR = + valid(Operation::create("ecr", 2, 0, {}, std::nullopt, std::nullopt, + std::vector>{})); + const auto threeQubit = valid(Operation::create( + "device.operation", 3, 0, {}, std::nullopt, std::nullopt, + std::vector>{{10, 20, 30}})); + const auto target = valid(Target::create( + std::move(sites), Connectivity::fromCouplings({{10, 20}, {20, 30}}), + NativeOperations::fromOperations({globalU, restrictedX, directionalCX, + exactCZ, unavailableECR, threeQubit}))); + + EXPECT_FALSE(globalU.hasExplicitApplicability()); + EXPECT_TRUE(restrictedX.hasExplicitApplicability()); + EXPECT_TRUE(directionalCX.hasExplicitApplicability()); + EXPECT_TRUE(unavailableECR.hasExplicitApplicability()); + EXPECT_TRUE(unavailableECR.applicableSiteTuples().empty()); + + EXPECT_TRUE(target.supports(GateKind::U, {30})); + EXPECT_TRUE(target.supports(GateKind::X, {10})); + EXPECT_FALSE(target.supports(GateKind::X, {20})); + EXPECT_TRUE(target.supports(GateKind::CX, {10, 20})); + EXPECT_FALSE(target.supports(GateKind::CX, {20, 10})); + EXPECT_TRUE(target.supports(GateKind::CX, {20, 30})); + EXPECT_FALSE(target.supports(GateKind::CX, {30, 20})); + EXPECT_FALSE(target.supports(GateKind::CX, {10, 30})); + EXPECT_TRUE(target.supports(GateKind::CZ, {10, 20})); + EXPECT_FALSE(target.supports(GateKind::CZ, {20, 10})); + EXPECT_FALSE(target.supports(GateKind::ECR)); + EXPECT_FALSE(target.supports(GateKind::ECR, {10, 20})); + EXPECT_FALSE(target.supports(GateKind::CX, {10})); + EXPECT_FALSE(target.supports(GateKind::CX, {10, 10})); + EXPECT_FALSE(target.supports(GateKind::CX, {10, 40})); + EXPECT_TRUE(target.supportsOperation("device.operation", 3, 0, {10, 20, 30})); + EXPECT_FALSE( + target.supportsOperation("device.operation", 3, 0, {30, 20, 10})); +} + TEST(CompilerTargetTest, ClassifiesEveryEntangler) { using Entangler = std::tuple; const std::array entanglers{Entangler{GateKind::CZ, "cz", 0}, @@ -570,26 +626,29 @@ TEST(CompilerTargetTest, SupportsRealQCOOperationsAndStructuralOps) { ASSERT_NE(gphase, nullptr); std::vector sites{valid(Site::create(10)), valid(Site::create(20))}; - std::vector directionalTuples{valid(SiteTuple::create({10, 20})), - valid(SiteTuple::create({20, 10}))}; + std::vector directionalTuples{valid(SiteTuple::create({10, 20}))}; std::vector operations{ valid(Operation::create("x", 1, 0)), valid(Operation::create("gphase", 0, 1)), valid(Operation::create("measure", 1, 0)), valid(Operation::create("reset", 1, 0)), - valid(Operation::create("cnot", 2, 0, std::move(directionalTuples))), + valid(Operation::create("cnot", 2, 0, std::move(directionalTuples), + std::nullopt, std::nullopt, + std::vector>{{10, 20}})), valid(Operation::create("cz", 2, 0))}; const auto target = valid(Target::create(std::move(sites), Connectivity::allToAll(), NativeOperations::fromOperations(operations))); - EXPECT_EQ(target.supports(x), true); - EXPECT_EQ(target.supports(cx), true); - EXPECT_EQ(target.supports(cz), true); - EXPECT_EQ(target.supports(measure), true); - EXPECT_EQ(target.supports(reset), true); - EXPECT_EQ(target.supports(barrier), true); - EXPECT_EQ(target.supports(gphase), true); - EXPECT_EQ(target.supports(nullptr), false); + EXPECT_TRUE(target.supports(x)); + EXPECT_TRUE(target.supports(cx)); + EXPECT_TRUE(target.supports(cz)); + EXPECT_TRUE(target.supports(measure)); + EXPECT_TRUE(target.supports(reset)); + EXPECT_TRUE(target.supports(barrier)); + EXPECT_TRUE(target.supports(gphase)); + EXPECT_TRUE(target.supports(cx, {10, 20})); + EXPECT_FALSE(target.supports(cx, {20, 10})); + EXPECT_FALSE(target.supports(nullptr)); const auto closed = valid(Target::create( 2, Connectivity::allToAll(), NativeOperations::fromOperations({}))); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index c7c26d66eb..36483f07eb 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -48,6 +48,7 @@ #include #include +#include #include #include #include @@ -97,7 +98,15 @@ static bool isExecutable(Region& body, const auto siteB = m.at(unitaryOp.getInputQubit(1)); const auto vertexA = target.vertexForSite(siteA); const auto vertexB = target.vertexForSite(siteB); - if (!vertexA || !vertexB || !target.areAdjacent(*vertexA, *vertexB)) { + std::array sites{siteA, siteB}; + const auto supported = + target.supports(&op) + ? target.supports(&op, sites) + : !target.synthesisBasis() || + target.supports(target.synthesisBasis()->entangler, + sites); + if (!vertexA || !vertexB || !target.areAdjacent(*vertexA, *vertexB) || + !supported) { llvm::dbgs() << "The two-qubit gate (" << siteA << ", " << siteB << ") is not executable: \n"; unitaryOp->dump(); @@ -342,6 +351,145 @@ class MappingPassTest : public MappingPassFixture, }; // namespace +TEST_F(MappingPassFixture, MapOneWayCXInNativeDirection) { + using Operation = CompilerTarget::Operation; + using SiteTuple = CompilerTarget::SiteTuple; + + std::vector cxSites{llvm::cantFail(SiteTuple::create({0, 1}))}; + std::vector operations{ + llvm::cantFail(Operation::create("cx", 2, 0, std::move(cxSites)))}; + const auto target = llvm::cantFail( + CompilerTarget::create(2, std::nullopt, std::move(operations))); + + QCOProgramBuilder builder(context.get()); + builder.initialize(); + auto targetQubit = builder.allocQubit(); + auto controlQubit = builder.allocQubit(); + std::tie(controlQubit, targetQubit) = builder.cx(controlQubit, targetQubit); + builder.sink(controlQubit); + builder.sink(targetQubit); + + auto module = builder.finalize(); + ASSERT_TRUE(runPass(module.get(), target, + MappingPassOptions{.ntrials = 1, .seed = 42}) + .succeeded()); + ASSERT_TRUE(succeeded(verify(*module))); + EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target)); + + CtrlOp cx; + module->walk([&](CtrlOp op) { cx = op; }); + ASSERT_TRUE(cx); + auto mappedControl = cx.getInputControl(0).getDefiningOp(); + auto mappedTarget = cx.getInputTarget(0).getDefiningOp(); + ASSERT_TRUE(mappedControl); + ASSERT_TRUE(mappedTarget); + EXPECT_EQ(mappedControl.getIndex(), 0); + EXPECT_EQ(mappedTarget.getIndex(), 1); + + size_t numSwaps = 0; + module->walk([&](SWAPOp) { ++numSwaps; }); + EXPECT_EQ(numSwaps, 0); +} + +TEST_F(MappingPassFixture, OrientRoutedSWAPForOneWayEntangler) { + using Operation = CompilerTarget::Operation; + using SiteTuple = CompilerTarget::SiteTuple; + + std::vector cxSites{llvm::cantFail(SiteTuple::create({1, 0})), + llvm::cantFail(SiteTuple::create({2, 1}))}; + std::vector operations{ + llvm::cantFail(Operation::create("u", 1, 3)), + llvm::cantFail(Operation::create("cx", 2, 0, std::move(cxSites)))}; + const auto target = llvm::cantFail(CompilerTarget::create( + 3, std::vector{{0, 1}, {1, 2}}, + std::move(operations))); + + QCOProgramBuilder builder(context.get()); + builder.initialize(); + SmallVector qubits{builder.allocQubit(), builder.allocQubit(), + builder.allocQubit()}; + std::tie(qubits[0], qubits[1]) = builder.rxx(0.25, qubits[0], qubits[1]); + std::tie(qubits[1], qubits[2]) = builder.rzx(0.5, qubits[1], qubits[2]); + std::tie(qubits[0], qubits[2]) = builder.cx(qubits[0], qubits[2]); + for (const auto qubit : qubits) { + builder.sink(qubit); + } + + auto module = builder.finalize(); + ASSERT_TRUE(runPass(module.get(), target, MappingPassOptions{.ntrials = 1}) + .succeeded()); + ASSERT_TRUE(succeeded(verify(*module))); + EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target)); + + size_t numSwaps = 0; + module->walk([&](SWAPOp) { ++numSwaps; }); + EXPECT_GT(numSwaps, 0); +} + +TEST_F(MappingPassFixture, RejectSWAPWithoutSupportedEntanglerDirection) { + using Operation = CompilerTarget::Operation; + using SiteTuple = CompilerTarget::SiteTuple; + + std::vector cxSites{llvm::cantFail(SiteTuple::create({0, 1}))}; + std::vector operations{ + llvm::cantFail(Operation::create("u", 1, 3)), + llvm::cantFail(Operation::create("cx", 2, 0, std::move(cxSites)))}; + const auto target = llvm::cantFail(CompilerTarget::create( + 3, std::vector{{0, 1}, {1, 2}}, + std::move(operations))); + + QCOProgramBuilder builder(context.get()); + builder.initialize(); + SmallVector qubits{builder.allocQubit(), builder.allocQubit(), + builder.allocQubit()}; + std::tie(qubits[0], qubits[1]) = builder.cx(qubits[0], qubits[1]); + std::tie(qubits[1], qubits[2]) = builder.cx(qubits[1], qubits[2]); + std::tie(qubits[0], qubits[2]) = builder.cx(qubits[0], qubits[2]); + for (const auto qubit : qubits) { + builder.sink(qubit); + } + + auto module = builder.finalize(); + EXPECT_TRUE(failed( + runPass(module.get(), target, + MappingPassOptions{.niterations = 1, .ntrials = 1, .seed = 42}))); +} + +TEST_F(MappingPassFixture, CoalesceEquivalentDirectionalLookahead) { + using Operation = CompilerTarget::Operation; + using SiteTuple = CompilerTarget::SiteTuple; + + std::vector cxSites{llvm::cantFail(SiteTuple::create({0, 1})), + llvm::cantFail(SiteTuple::create({1, 2}))}; + std::vector operations{ + llvm::cantFail(Operation::create("u", 1, 3)), + llvm::cantFail(Operation::create("cx", 2, 0, std::move(cxSites)))}; + const auto target = llvm::cantFail(CompilerTarget::create( + 3, std::vector{{0, 1}, {1, 2}}, + std::move(operations))); + + QCOProgramBuilder builder(context.get()); + builder.initialize(); + SmallVector qubits{builder.allocQubit(), builder.allocQubit(), + builder.allocQubit()}; + for (size_t i = 0; i < 8; ++i) { + std::tie(qubits[0], qubits[1]) = builder.cx(qubits[0], qubits[1]); + } + std::tie(qubits[1], qubits[2]) = builder.cx(qubits[1], qubits[2]); + std::tie(qubits[1], qubits[0]) = builder.cx(qubits[1], qubits[0]); + for (const auto qubit : qubits) { + builder.sink(qubit); + } + + auto module = builder.finalize(); + ASSERT_TRUE( + runPass(module.get(), target, + MappingPassOptions{.niterations = 1, .ntrials = 1, .seed = 42}) + .succeeded()); + ASSERT_TRUE(succeeded(verify(*module))); + EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target)); +} + TEST_F(MappingPassFixture, MapTopologyOnlyWithEmptyOperationSet) { constexpr int64_t size = 3; diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index 0f6076f9d7..eadabc2f90 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -187,7 +187,7 @@ class CompilerTarget: """Whether this arity accepts a concrete width.""" class Operation: - """A homogeneous target-wide operation capability and its calibration.""" + """A target operation capability, applicability, and calibration.""" def __init__( self, @@ -197,6 +197,7 @@ class CompilerTarget: site_tuples: Sequence[CompilerTarget.SiteTuple] | None = None, duration: int | None = None, fidelity: float | None = None, + applicable_site_tuples: Sequence[Sequence[int]] | None = None, ) -> None: ... @property def name(self) -> str: @@ -214,10 +215,18 @@ class CompilerTarget: def num_parameters(self) -> int: """The number of real-valued parameters.""" + @property + def has_explicit_applicability(self) -> bool: + """Whether applicability is explicitly enumerated.""" + @property def site_tuples(self) -> list[CompilerTarget.SiteTuple]: """Ordered site-specific calibration data.""" + @property + def applicable_site_tuples(self) -> list[list[int]]: + """The exact ordered tuples with operation support, if explicit.""" + @property def duration(self) -> int | None: """The raw default duration, if available.""" @@ -385,7 +394,13 @@ class CompilerTarget: def synthesis_basis(self) -> CompilerTarget.SynthesisBasis | None: """A complete target-wide synthesis basis, if available.""" - def supports_operation(self, name: str, arity: int, num_parameters: int | None = None) -> bool: + def supports_operation( + self, + name: str, + arity: int, + num_parameters: int | None = None, + sites: Sequence[int] | None = None, + ) -> bool: """Whether the target supports an operation.""" class Program: diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index 9f571e832c..05ca6eedcc 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -449,7 +449,15 @@ def test_compiler_target_constructors_preserve_python_api() -> None: CompilerTarget.Site(20, "q1"), ] site_tuple = CompilerTarget.SiteTuple([10, 20], duration=10, fidelity=0.99) - operation = CompilerTarget.Operation("cx", 2, 0, site_tuples=[site_tuple], duration=20, fidelity=0.98) + operation = CompilerTarget.Operation( + "cx", + 2, + 0, + site_tuples=[site_tuple], + duration=20, + fidelity=0.98, + applicable_site_tuples=[[10, 20]], + ) fixed_zero = CompilerTarget.OperationArity.fixed(0) variadic = CompilerTarget.OperationArity.variadic(2) global_phase = CompilerTarget.Operation("gphase", fixed_zero, 1) @@ -486,6 +494,14 @@ def test_compiler_target_constructors_preserve_python_api() -> None: assert site_tuple.sites == [10, 20] assert len(operation.site_tuples) == 1 assert operation.site_tuples[0].sites == [10, 20] + assert operation.has_explicit_applicability + assert operation.applicable_site_tuples == [[10, 20]] + assert not CompilerTarget.Operation("x", 1, 0).has_explicit_applicability + explicitly_unavailable = CompilerTarget.Operation("ecr", 2, 0, applicable_site_tuples=[]) + assert explicitly_unavailable.has_explicit_applicability + assert explicitly_unavailable.applicable_site_tuples == [] + assert targets[2].supports_operation("cx", 2, sites=[10, 20]) + assert not targets[2].supports_operation("cx", 2, sites=[20, 10]) assert operation.arity.kind == CompilerTarget.OperationArityKind.FIXED assert operation.arity.value == 2 assert global_phase.arity.kind == CompilerTarget.OperationArityKind.FIXED From 835eabbfc281dd077e8b09457ddf7619a34bee03 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 31 Aug 2026 17:30:13 +0200 Subject: [PATCH 2/6] =?UTF-8?q?=E2=9C=A8=20Refine=20directional=20target?= =?UTF-8?q?=20mapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Compiler/TargetCost.h | 49 +++ .../mlir/Dialect/QCO/Transforms/Passes.td | 2 +- mlir/lib/Compiler/CMakeLists.txt | 12 +- mlir/lib/Compiler/TargetCost.cpp | 149 +++++++++ .../QCO/Transforms/Mapping/Mapping.cpp | 235 +++++-------- .../NativeSynthesis/TargetSynthesis.cpp | 229 ++++++++++++- .../Compiler/test_compiler_pipeline.cpp | 55 ++++ .../Compiler/test_compiler_target.cpp | 4 + .../QCO/Transforms/Mapping/test_mapping.cpp | 308 +++++++++--------- .../NativeSynthesis/test_target_synthesis.cpp | 190 ++++++++++- 10 files changed, 904 insertions(+), 329 deletions(-) create mode 100644 mlir/include/mlir/Compiler/TargetCost.h create mode 100644 mlir/lib/Compiler/TargetCost.cpp diff --git a/mlir/include/mlir/Compiler/TargetCost.h b/mlir/include/mlir/Compiler/TargetCost.h new file mode 100644 index 0000000000..fb86818800 --- /dev/null +++ b/mlir/include/mlir/Compiler/TargetCost.h @@ -0,0 +1,49 @@ +/* + * 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/Compiler/Target.h" + +#include + +#include + +namespace mlir { + +/** + * @brief Cached routing costs for a target gate. + * + * @details The cache is derived from an immutable compiler target. A native + * ordered coupling has cost zero, a coupling available only in the opposite + * direction has cost one, and an unavailable coupling has infinite cost. + * Nonadjacent costs remain the target's shortest-path distance minus one. + * Construction is linear in the explicit topology or reported site tuples; + * unrestricted all-to-all targets do not enumerate every qubit pair. + */ +class TargetGateCosts { +public: + /// Construct routing costs for a recognized two-qubit gate. + TargetGateCosts(const CompilerTarget& target, CompilerTarget::GateKind gate); + + /// Return the cached routing cost between two valid target vertices. + [[nodiscard]] float routingCostBetween(size_t source, size_t target) const; + + /// Return whether every adjacent ordered pair has zero gate cost. + [[nodiscard]] bool isUniform() const noexcept; + +private: + CompilerTarget target_; + llvm::DenseMap costs_; + float defaultAdjacentCost_ = 0.F; + bool uniform_ = true; +}; + +} // namespace mlir diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 0d974a7a16..f459c0f3df 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -156,7 +156,7 @@ def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { - `dist(i, j)` returns the distance between the qubits `i` and `j` on the target's coupling graph. - `h(gate, p)` is `dist(p[gate.first], p[gate.second]) - 1` for nonadjacent operands, zero for an adjacent native operand order, one when only the reverse operand order is native, and infinity when neither adjacent order is supported. - Routing uses the undirected connectivity underlying the target topology, while gate costs and executability retain the semantic operand order. Inserted SWAP operations are ordered so that their eventual target-basis entanglers use a legal direction. + Routing uses the undirected connectivity underlying the target topology, while gate costs retain the semantic operand order. Target-native synthesis realizes operations and inserted SWAPs in a supported direction. 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 diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index dd6b46da0e..f48f78de9c 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -11,6 +11,7 @@ add_mlir_library( MQTCompilerTarget PARTIAL_SOURCES_INTENDED Target.cpp + TargetCost.cpp ADDITIONAL_HEADER_DIRS ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler LINK_LIBS @@ -21,8 +22,15 @@ add_mlir_library( mqt_mlir_target_use_project_options(MQTCompilerTarget) -target_sources(MQTCompilerTarget PUBLIC FILE_SET HEADERS BASE_DIRS ${MQT_MLIR_SOURCE_INCLUDE_DIR} - FILES ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/Target.h) +target_sources( + MQTCompilerTarget + PUBLIC FILE_SET + HEADERS + BASE_DIRS + ${MQT_MLIR_SOURCE_INCLUDE_DIR} + FILES + ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/Target.h + ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/TargetCost.h) # Build the optional QDMI-to-compiler-target adapter set(LLVM_REQUIRES_EH ON) diff --git a/mlir/lib/Compiler/TargetCost.cpp b/mlir/lib/Compiler/TargetCost.cpp new file mode 100644 index 0000000000..53c52d285e --- /dev/null +++ b/mlir/lib/Compiler/TargetCost.cpp @@ -0,0 +1,149 @@ +/* + * 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/Compiler/TargetCost.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace mlir { +namespace { + +struct GateSignature { + llvm::StringLiteral name; + size_t numParameters; +}; + +} // namespace + +[[nodiscard]] static GateSignature +gateSignature(const CompilerTarget::GateKind gate) { + using Gate = CompilerTarget::GateKind; + constexpr std::array signatures{ + std::pair{Gate::RXX, GateSignature{"rxx", 1}}, + std::pair{Gate::RYY, GateSignature{"ryy", 1}}, + std::pair{Gate::RZX, GateSignature{"rzx", 1}}, + std::pair{Gate::RZZ, GateSignature{"rzz", 1}}, + std::pair{Gate::ISWAP, GateSignature{"iswap", 0}}, + std::pair{Gate::CZ, GateSignature{"cz", 0}}, + std::pair{Gate::CX, GateSignature{"cx", 0}}, + std::pair{Gate::ECR, GateSignature{"ecr", 0}}, + }; + const llvm::ArrayRef> signatureList{ + signatures}; + const auto* const signature = + llvm::find_if(signatureList, [gate](const auto& candidate) { + return candidate.first == gate; + }); + assert(signature != signatureList.end() && + "routing costs require a two-qubit gate"); + return signature->second; +} + +TargetGateCosts::TargetGateCosts(const CompilerTarget& target, + const CompilerTarget::GateKind gate) + : target_(target) { + constexpr auto unavailable = std::numeric_limits::infinity(); + if (!target_.hasExplicitTopology()) { + if (!target_.hasExplicitOperations()) { + return; + } + + const auto [name, numParameters] = gateSignature(gate); + defaultAdjacentCost_ = unavailable; + uniform_ = false; + for (const auto& operation : target_.operations()) { + if (operation.canonicalName() != name || operation.numQubits() != 2 || + operation.numParameters() != numParameters) { + continue; + } + if (!operation.hasExplicitSiteTuples()) { + defaultAdjacentCost_ = 0.F; + costs_.clear(); + uniform_ = true; + return; + } + for (const auto& tuple : operation.siteTuples()) { + assert(tuple.sites().size() == 2 && + "two-qubit gate must have two-site tuples"); + const auto source = tuple.sites()[0]; + const auto target = tuple.sites()[1]; + costs_.insert_or_assign({source, target}, 0.F); + const std::array reverseSites{target, source}; + costs_.insert_or_assign( + {target, source}, target_.supports(gate, reverseSites) ? 0.F : 1.F); + } + } + const auto numQubits = target_.numQubits(); + if (costs_.size() == numQubits * (numQubits - 1) && + llvm::all_of(costs_, + [](const auto& cost) { return cost.second == 0.F; })) { + defaultAdjacentCost_ = 0.F; + costs_.clear(); + uniform_ = true; + } + return; + } + + for (size_t source = 0; source < target_.numQubits(); ++source) { + target_.forEachNeighbour(source, [&](const size_t target) { + if (target < source) { + return; + } + + const auto sourceSite = target_.siteForVertex(source); + const auto targetSite = target_.siteForVertex(target); + const std::array forwardSites{sourceSite, targetSite}; + const std::array reverseSites{targetSite, sourceSite}; + const bool forward = target_.supports(gate, forwardSites); + const bool reverse = target_.supports(gate, reverseSites); + + if (!forward) { + costs_.try_emplace(CompilerTarget::Coupling{sourceSite, targetSite}, + reverse ? 1.F : unavailable); + } + if (!reverse) { + costs_.try_emplace(CompilerTarget::Coupling{targetSite, sourceSite}, + forward ? 1.F : unavailable); + } + }); + } + uniform_ = costs_.empty(); +} + +float TargetGateCosts::routingCostBetween(const size_t source, + const size_t target) const { + assert(source < target_.numQubits() && target < target_.numQubits() && + "compiler target vertex is out of range"); + if (source == target) { + return 0.F; + } + const auto distance = target_.distanceBetween(source, target); + if (distance > 1) { + return static_cast(distance - 1); + } + const CompilerTarget::Coupling coupling{target_.siteForVertex(source), + target_.siteForVertex(target)}; + if (const auto found = costs_.find(coupling); found != costs_.end()) { + return found->second; + } + return defaultAdjacentCost_; +} + +bool TargetGateCosts::isUniform() const noexcept { return uniform_; } + +} // namespace mlir diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 4a14afeebb..f0db76db07 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -11,6 +11,7 @@ #include "mlir/Dialect/QCO/Transforms/Mapping/Mapping.h" #include "mlir/Compiler/Target.h" +#include "mlir/Compiler/TargetCost.h" #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" @@ -52,9 +53,9 @@ #include #include #include +#include #include #include -#include #include #include #include @@ -364,13 +365,7 @@ struct PlacementPass final struct MappingPass : impl::MappingPassBase { private: using IndexPairType = std::pair; - - struct WindowEntry { - Operation* operation; - IndexPairType programs; - }; - - using Window = SmallVector; + using Window = SmallVector; enum class RoutingMode : bool { Cold, Hot }; @@ -423,84 +418,6 @@ struct MappingPass : impl::MappingPassBase { } }; - /// Return whether the operation ultimately emitted for a window entry is - /// available on the ordered target sites. - [[nodiscard]] static bool - supportsOnSites(const WindowEntry& entry, const CompilerTarget& target, - const ArrayRef sites) { - if (target.supports(entry.operation)) { - return target.supports(entry.operation, sites); - } - if (const auto basis = target.synthesisBasis()) { - return target.supports(basis->entangler, sites); - } - // Mapping remains usable as a topology-only pass for targets without a - // synthesis basis. Target-native synthesis diagnoses the missing basis. - return true; - } - - /// Return the target capability used to route a native two-qubit operation. - [[nodiscard]] static StringRef routingSymbol(Operation* operation) { - if (auto controlled = dyn_cast(operation); - controlled && controlled.getNumControls() == 1 && - controlled.getNumTargets() == 1 && - controlled.getNumBodyUnitaries() == 1) { - Operation* body = controlled.getBodyUnitary(0).getOperation(); - if (isa(body)) { - return "cx"; - } - if (isa(body)) { - return "cz"; - } - } - return cast(operation).getBaseSymbol(); - } - - /// Return whether consecutive entries have identical routing constraints. - [[nodiscard]] static bool - hasEquivalentRoutingBehavior(const WindowEntry& lhs, const WindowEntry& rhs, - const CompilerTarget& target) { - if (lhs.programs != rhs.programs) { - return false; - } - - const auto lhsIsNative = target.supports(lhs.operation); - const auto rhsIsNative = target.supports(rhs.operation); - if (lhsIsNative != rhsIsNative) { - return false; - } - if (!lhsIsNative) { - return true; - } - - auto lhsUnitary = cast(lhs.operation); - auto rhsUnitary = cast(rhs.operation); - return routingSymbol(lhs.operation) == routingSymbol(rhs.operation) && - lhsUnitary.getNumParams() == rhsUnitary.getNumParams(); - } - - /// Return the routing cost of an ordered two-qubit operation. - [[nodiscard]] static float routingCost(const WindowEntry& entry, - const Layout& layout, - const CompilerTarget& target) { - const auto [prog0, prog1] = entry.programs; - const auto [hw0, hw1] = layout.getHardwareIndices(prog0, prog1); - const auto distance = target.distanceBetween(hw0, hw1); - if (distance > 1) { - return static_cast(distance - 1); - } - - std::array sites{target.siteForVertex(hw0), target.siteForVertex(hw1)}; - if (supportsOnSites(entry, target, sites)) { - return 0.F; - } - std::ranges::reverse(sites); - if (supportsOnSites(entry, target, sites)) { - return 1.F; - } - return std::numeric_limits::infinity(); - } - /// Describes a node in the A* search graph. struct Node { struct ComparePointer { @@ -523,18 +440,25 @@ struct MappingPass : impl::MappingPassBase { /// Construct a non-root node from its parent node. Apply the given swap to /// the layout of the parent node. Node(Node* parent, const IndexPairType& swap, const Window& window, - const CompilerTarget& target, const Parameters& params) + const CompilerTarget& target, const TargetGateCosts* gateCosts, + const Parameters& params) : layout(parent->layout), swap(swap), parent(parent), depth(parent->depth + 1), f(0) { layout.swap(swap.first, swap.second); - f = g(params.alpha) + h(window, target, params); // NOLINT + f = g(params.alpha) + h(window, target, gateCosts, params); // NOLINT } /// Return true, if the current SWAP sequence makes all gates in the front /// executable. - [[nodiscard]] bool isGoal(const WindowEntry& front, - const CompilerTarget& target) const { - return routingCost(front, layout, target) == 0.F; + [[nodiscard]] bool isGoal(const IndexPairType& front, + const CompilerTarget& target, + const TargetGateCosts* gateCosts) const { + const auto [hw0, hw1] = + layout.getHardwareIndices(front.first, front.second); + if (gateCosts != nullptr) { + return gateCosts->routingCostBetween(hw0, hw1) == 0.F; + } + return target.areAdjacent(hw0, hw1); } private: @@ -552,12 +476,18 @@ struct MappingPass : impl::MappingPassBase { /// that a naive router would insert to route the layers (with a constant /// layout). [[nodiscard]] float h(const Window& window, const CompilerTarget& target, + const TargetGateCosts* gateCosts, const Parameters& params) const { float costs{0}; float decay{1.}; - for (const auto& entry : window) { - costs += decay * routingCost(entry, layout, target); + for (const auto& [prog0, prog1] : window) { + const auto [hw0, hw1] = layout.getHardwareIndices(prog0, prog1); + const auto routingCost = + gateCosts != nullptr + ? gateCosts->routingCostBetween(hw0, hw1) + : static_cast(target.distanceBetween(hw0, hw1) - 1); + costs += decay * routingCost; decay *= params.lambda; } return costs; @@ -647,7 +577,14 @@ struct MappingPass : impl::MappingPassBase { /// Construct mapping for a compiler target. explicit MappingPass(const CompilerTarget& compilerTarget, const MappingPassOptions& options) - : MappingPassBase(options), target(compilerTarget) {} + : MappingPassBase(options), target(compilerTarget) { + if (const auto basis = compilerTarget.synthesisBasis()) { + gateCosts.emplace(compilerTarget, basis->entangler); + if (gateCosts->isUniform()) { + gateCosts.reset(); + } + } + } protected: void runOnOperation() override { @@ -972,12 +909,7 @@ struct MappingPass : impl::MappingPassBase { llvm::PriorityQueue, Node::ComparePointer> frontier; - // Early exit, if the root node is a goal node already. Node* root = std::construct_at(arena.Allocate(), layout); - if (root->isGoal(window.front(), *target)) { - return SmallVector{}; - } - frontier.emplace(root); DenseMap, size_t> bestDepth; @@ -1008,7 +940,8 @@ struct MappingPass : impl::MappingPassBase { // If the currently visited node is a goal node, reconstruct the // sequence of SWAPs from this node to the root. - if (curr->isGoal(window.front(), *target)) { + if (curr->isGoal(window.front(), *target, + gateCosts ? &*gateCosts : nullptr)) { SmallVector seq(curr->depth); size_t j = seq.size() - 1; for (const Node* n = curr; n->parent != nullptr; n = n->parent) { @@ -1023,11 +956,10 @@ struct MappingPass : impl::MappingPassBase { // between two neighboring hardware qubits. expansionSet.clear(); - for (const auto& [q0, q1] = window.front().programs; - const auto prog : {q0, q1}) { + for (const auto& [q0, q1] = window.front(); const auto prog : {q0, q1}) { const auto hw0 = curr->layout.getHardwareIndex(prog); target->forEachNeighbour(hw0, [&](const auto hw1) { - if (!shouldReverseSWAPOperands(hw0, hw1).has_value()) { + if (!canSwap(hw0, hw1)) { return; } // Ensure consistent hashing/comparison. @@ -1037,8 +969,9 @@ struct MappingPass : impl::MappingPassBase { } expansionSet.push_back(swap); - frontier.emplace(std::construct_at(arena.Allocate(), curr, swap, - window, *target, params)); + frontier.emplace( + std::construct_at(arena.Allocate(), curr, swap, window, *target, + gateCosts ? &*gateCosts : nullptr, params)); }); } @@ -1172,25 +1105,18 @@ struct MappingPass : impl::MappingPassBase { return curr; } - /// Return a two-qubit operation's program indices in semantic operand order. - /// Wire traversal records ready indices in wire order. The iterator's qubit - /// is the operation result on that wire in both traversal directions, so use - /// the ordered results to recover the operation's operand order. [[nodiscard]] static IndexPairType - orderedPrograms(Operation* operation, const ArrayRef indices, + orderedPrograms(UnitaryOpInterface unitary, const ArrayRef indices, const Wires& wires, const WireInfos& infos) { - auto unitary = cast(operation); assert(unitary.getNumQubits() == 2 && indices.size() == 2 && "expected a ready two-qubit operation"); - const auto programForOutput = [&](Value output) { - const auto found = llvm::find_if(indices, [&](const size_t index) { + const auto* const found = llvm::find_if(indices, [&](const size_t index) { return wires[index].qubit() == output; }); assert(found != indices.end() && "operation result has no ready wire"); return infos.lookupProgram(*found); }; - return {programForOutput(unitary.getOutputQubit(0)), programForOutput(unitary.getOutputQubit(1))}; } @@ -1216,7 +1142,8 @@ struct MappingPass : impl::MappingPassBase { if (released.empty()) { for (const auto& [op, indices] : frontier) { - if (!isa(op) && isa(op)) { + if (auto unitary = dyn_cast(op); + !isa(op) && unitary) { const auto i0 = indices[0]; const auto i1 = indices[1]; const auto prog0 = infos.lookupProgram(i0); @@ -1224,8 +1151,8 @@ struct MappingPass : impl::MappingPassBase { const IndexPairType gate = std::minmax(prog0, prog1); if (!is_contained(prev, gate)) { - window.emplace_back(WindowEntry{ - op, orderedPrograms(op, indices, wires, infos)}); + window.emplace_back( + orderedPrograms(unitary, indices, wires, infos)); if (window.size() == 1 + nlookahead) { return WalkResult::interrupt(); } @@ -1249,40 +1176,22 @@ struct MappingPass : impl::MappingPassBase { /// Insert SWAP operations, exchanging two qubits, virtually /// (`RoutingMode::Cold`) or into the IR (`RoutingMode::Hot`). The function /// expects that each wire points at the correct insertion point. - [[nodiscard]] std::optional - shouldReverseSWAPOperands(const size_t hw0, const size_t hw1) const { - const auto basis = target->synthesisBasis(); - if (!basis) { - return false; - } - std::array sites{target->siteForVertex(hw0), target->siteForVertex(hw1)}; - if (target->supports(basis->entangler, sites)) { - return false; - } - std::ranges::reverse(sites); - if (target->supports(basis->entangler, sites)) { - return true; - } - return std::nullopt; + [[nodiscard]] bool canSwap(const size_t hw0, const size_t hw1) const { + return !gateCosts || std::isfinite(gateCosts->routingCostBetween(hw0, hw1)); } template LogicalResult insertSWAPs(ArrayRef swaps, RoutingBundle& bundle, Statistics& stats, IRRewriter* rewriter) { - SmallVector reverseOperands; - reverseOperands.reserve(swaps.size()); - for (const auto& [hw0, hw1] : swaps) { - const auto reverse = shouldReverseSWAPOperands(hw0, hw1); - if (!reverse) { - return failure(); - } - reverseOperands.emplace_back(*reverse); + if (llvm::any_of(swaps, [&](const auto& swap) { + return !canSwap(swap.first, swap.second); + })) { + return failure(); } auto& [wires, infos, layout] = bundle; - for (size_t index = 0; index < swaps.size(); ++index) { - const auto [hw0, hw1] = swaps[index]; + for (const auto& [hw0, hw1] : swaps) { const auto [prog0, prog1] = layout.getProgramIndices(hw0, hw1); if constexpr (Mode == RoutingMode::Hot) { @@ -1294,22 +1203,17 @@ struct MappingPass : impl::MappingPassBase { auto& w0 = wires[i0]; auto& w1 = wires[i1]; - const auto in0 = w0.qubit(); - const auto in1 = w1.qubit(); - auto first = in0; - auto second = in1; - if (reverseOperands[index]) { - std::swap(first, second); - } + auto in0 = w0.qubit(); + auto in1 = w1.qubit(); rewriter->setInsertionPointAfterValue(in0); // Valid bc. Hot => Forward. - auto swapOp = SWAPOp::create(*rewriter, first.getLoc(), first, second); + auto swapOp = SWAPOp::create(*rewriter, in0.getLoc(), in0, in1); - const auto firstOut = swapOp.getQubit0Out(); - const auto secondOut = swapOp.getQubit1Out(); + auto out0 = swapOp.getQubit0Out(); + auto out1 = swapOp.getQubit1Out(); - rewriter->replaceAllUsesExcept(first, secondOut, swapOp); - rewriter->replaceAllUsesExcept(second, firstOut, swapOp); + rewriter->replaceAllUsesExcept(in0, out1, swapOp); + rewriter->replaceAllUsesExcept(in1, out0, swapOp); infos.swap(prog0, prog1); @@ -1345,15 +1249,21 @@ struct MappingPass : impl::MappingPassBase { const auto release = TypeSwitch(op) .Case([](auto&) { return true; }) - .template Case([&](auto&) { - if (indices.size() == 1) { - return true; - } + .template Case( + [&](UnitaryOpInterface unitary) { + if (indices.size() == 1) { + return true; + } - const WindowEntry entry{ - op, orderedPrograms(op, indices, wires, infos)}; - return routingCost(entry, layout, *target) == 0.F; - }) + const auto [prog0, prog1] = + orderedPrograms(unitary, indices, wires, infos); + const auto [hw0, hw1] = + layout.getHardwareIndices(prog0, prog1); + return gateCosts + ? gateCosts->routingCostBetween(hw0, hw1) == + 0.F + : target->areAdjacent(hw0, hw1); + }) .template Case([](auto&) { return true; }) .template Case([](MeasureOp& m) { if (Direction == WireDirection::Backward) { @@ -1883,6 +1793,7 @@ struct MappingPass : impl::MappingPassBase { } std::optional target; + std::optional gateCosts; }; } // namespace diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 2df7dd8655..69d5a0c2d9 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -20,10 +20,13 @@ #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" +#include #include +#include #include #include // IWYU pragma: keep (Passes.h.inc) #include +#include #include #include #include @@ -38,10 +41,12 @@ #include #include +#include #include #include #include #include +#include namespace mlir::qco { @@ -295,9 +300,114 @@ static bool fuseTwoQubitGateRun(IRRewriter& rewriter, UnitaryOpInterface head, return true; } -static bool requiresTargetSynthesis(Operation* operation, - const CompilerTarget& target) { - return !target.supports(operation); +namespace { + +using SiteId = CompilerTarget::SiteId; +using SiteMap = DenseMap; + +} // namespace + +static SmallVector getQubitValues(ValueRange values) { + return llvm::to_vector(llvm::make_filter_range( + values, [](Value value) { return isa(value.getType()); })); +} + +static void propagateSites(ValueRange inputs, ValueRange outputs, + SiteMap& sites) { + const auto inputQubits = getQubitValues(inputs); + const auto outputQubits = getQubitValues(outputs); + for (const auto [input, output] : + llvm::zip_equal(inputQubits, outputQubits)) { + if (const auto found = sites.find(input); found != sites.end()) { + const SiteId site = found->second; + sites.try_emplace(output, site); + } + } +} + +static ValueRange structuredInputs(Operation* operation) { + return TypeSwitch(operation) + .Case([](IfOp op) { return op.getQubits(); }) + .Case([](IndexSwitchOp op) { return op.getTargets(); }) + .Case([](scf::ForOp op) { return op.getInits(); }) + .Case([](scf::WhileOp op) { return op.getInits(); }) + .Default([](Operation*) -> ValueRange { return {}; }); +} + +static void collectStaticSites(Region& region, SiteMap& sites) { + for (Operation& operation : region.getOps()) { + if (auto staticOp = dyn_cast(operation)) { + sites.try_emplace(staticOp.getQubit(), staticOp.getIndex()); + continue; + } + if (auto unitary = dyn_cast(operation)) { + propagateSites(unitary.getInputQubits(), unitary.getOutputQubits(), + sites); + continue; + } + if (auto reset = dyn_cast(operation)) { + propagateSites(reset.getQubitIn(), reset.getQubitOut(), sites); + continue; + } + if (auto measure = dyn_cast(operation)) { + propagateSites(measure.getQubitIn(), measure.getQubitOut(), sites); + continue; + } + + if (isa(operation)) { + const auto inputs = structuredInputs(&operation); + for (Region& nested : operation.getRegions()) { + propagateSites(inputs, nested.getArguments(), sites); + collectStaticSites(nested, sites); + } + propagateSites(inputs, operation.getResults(), sites); + continue; + } + + for (Region& nested : operation.getRegions()) { + collectStaticSites(nested, sites); + } + } +} + +static SiteMap collectStaticSites(Operation* root) { + SiteMap sites; + for (Region& region : root->getRegions()) { + collectStaticSites(region, sites); + } + return sites; +} + +static std::optional> +getOperationSites(Operation* operation, const SiteMap& sites) { + SmallVector qubits; + if (auto unitary = dyn_cast(operation)) { + llvm::append_range(qubits, unitary.getInputQubits()); + } else if (auto reset = dyn_cast(operation)) { + qubits.emplace_back(reset.getQubitIn()); + } else if (auto measure = dyn_cast(operation)) { + qubits.emplace_back(measure.getQubitIn()); + } else { + return std::nullopt; + } + + SmallVector result; + result.reserve(qubits.size()); + for (Value qubit : qubits) { + const auto found = sites.find(qubit); + if (found == sites.end()) { + return std::nullopt; + } + result.emplace_back(found->second); + } + return result; +} + +static bool +requiresTargetSynthesis(Operation* operation, const CompilerTarget& target, + const std::optional>& sites) { + return sites ? !target.supports(operation, *sites) + : !target.supports(operation); } /// Normalize relative phase effects and discard only the unobservable global @@ -324,10 +434,16 @@ static LogicalResult prepareGlobalPhases(ModuleOp moduleOp, namespace { +struct PlannedOperation { + Operation* operation; + std::optional> sites; + bool reverseEntangler = false; +}; + struct SynthesisPlan { Operation* firstNeed = nullptr; Operation* matrixUnavailable = nullptr; - SmallVector operations; + SmallVector operations; }; } // namespace @@ -335,13 +451,15 @@ struct SynthesisPlan { static SynthesisPlan planTargetSynthesis(Operation* root, const CompilerTarget& target) { SynthesisPlan plan; + const auto sites = collectStaticSites(root); root->walk([&](Operation* operation) { auto unitary = dyn_cast(operation); if (!unitary || !isWalkableUnitaryShell(operation) || (unitary.getNumQubits() != 1 && unitary.getNumQubits() != 2)) { return WalkResult::advance(); } - if (!requiresTargetSynthesis(operation, target)) { + auto operationSites = getOperationSites(operation, sites); + if (!requiresTargetSynthesis(operation, target, operationSites)) { return WalkResult::advance(); } if (plan.firstNeed == nullptr) { @@ -352,13 +470,15 @@ static SynthesisPlan planTargetSynthesis(Operation* root, Matrix2x2 matrix; if (unitary.getUnitaryMatrix2x2(matrix) || decomposition::canSynthesizeParameterizedUnitary1Q(operation)) { - plan.operations.emplace_back(operation); + plan.operations.emplace_back( + PlannedOperation{operation, std::move(operationSites)}); return WalkResult::advance(); } } else { Matrix4x4 matrix; if (assignTwoQubitOpMatrix(operation, matrix)) { - plan.operations.emplace_back(operation); + plan.operations.emplace_back( + PlannedOperation{operation, std::move(operationSites)}); return WalkResult::advance(); } } @@ -368,8 +488,71 @@ static SynthesisPlan planTargetSynthesis(Operation* root, return plan; } +static bool +supportsSingleQubitBasisOnSite(const CompilerTarget& target, + const CompilerTarget::SingleQubitBasis basis, + const SiteId site) { + const std::array sites{site}; + using Gate = CompilerTarget::GateKind; + switch (basis) { + case CompilerTarget::SingleQubitBasis::U: + return target.supports(Gate::U, sites); + case CompilerTarget::SingleQubitBasis::ZSXX: + return target.supports(Gate::X, sites) && + target.supports(Gate::SX, sites) && target.supports(Gate::RZ, sites); + case CompilerTarget::SingleQubitBasis::R: + return target.supports(Gate::R, sites); + case CompilerTarget::SingleQubitBasis::XZX: + case CompilerTarget::SingleQubitBasis::ZXZ: + return target.supports(Gate::RX, sites) && target.supports(Gate::RZ, sites); + case CompilerTarget::SingleQubitBasis::XYX: + return target.supports(Gate::RX, sites) && target.supports(Gate::RY, sites); + case CompilerTarget::SingleQubitBasis::ZYZ: + return target.supports(Gate::RY, sites) && target.supports(Gate::RZ, sites); + } + llvm_unreachable("unknown single-qubit synthesis basis"); +} + +static LogicalResult +prepareSynthesisPlan(SynthesisPlan& plan, const CompilerTarget& target, + const CompilerTarget::SynthesisBasis basis) { + for (auto& action : plan.operations) { + if (!action.sites) { + continue; + } + for (const SiteId site : *action.sites) { + if (!supportsSingleQubitBasisOnSite(target, basis.singleQubit, site)) { + action.operation->emitError() + << "target-native synthesis has no usable single-qubit basis on " + "site " + << site; + return failure(); + } + } + + if (action.sites->size() == 1 || + target.supports(basis.entangler, *action.sites)) { + continue; + } + assert(action.sites->size() == 2 && + "target synthesis only handles one- and two-qubit operations"); + const std::array reverseSites{(*action.sites)[1], (*action.sites)[0]}; + if (target.supports(basis.entangler, reverseSites)) { + action.reverseEntangler = true; + continue; + } + + action.operation->emitError() + << "target-native synthesis has no usable entangler on sites " + << (*action.sites)[0] << " and " << (*action.sites)[1]; + return failure(); + } + return success(); +} + static void lowerTargetOperation(IRRewriter& rewriter, UnitaryOpInterface op, - const CompilerTarget::SynthesisBasis basis) { + const CompilerTarget::SynthesisBasis basis, + const bool reverseEntangler) { Operation* const operation = op.getOperation(); rewriter.setInsertionPoint(operation); if (op.isSingleQubit()) { @@ -404,13 +587,22 @@ static void lowerTargetOperation(IRRewriter& rewriter, UnitaryOpInterface op, input1 = op.getInputQubit(1); } + if (reverseEntangler) { + matrix = matrix.reorderForQubits(1, 0); + std::swap(input0, input1); + } const auto native = decomposeUnitary2QWeyl(matrix, basis.entangler); const auto synthesized = emitUnitary2QWeyl(rewriter, operation->getLoc(), input0, input1, native, basis); decomposition::emitGPhaseIfNeeded(rewriter, operation->getLoc(), synthesized.globalPhase); - rewriter.replaceOp(operation, - ValueRange{synthesized.qubit0, synthesized.qubit1}); + if (reverseEntangler) { + rewriter.replaceOp(operation, + ValueRange{synthesized.qubit1, synthesized.qubit0}); + } else { + rewriter.replaceOp(operation, + ValueRange{synthesized.qubit0, synthesized.qubit1}); + } } static LogicalResult fuseTwoQubitGates(ModuleOp moduleOp) { @@ -483,7 +675,7 @@ struct TargetNativeSynthesisPass final signalPassFailure(); return; } - const auto plan = planTargetSynthesis(moduleOp, target); + auto plan = planTargetSynthesis(moduleOp, target); if (plan.firstNeed == nullptr) { return; } @@ -504,11 +696,15 @@ struct TargetNativeSynthesisPass final signalPassFailure(); return; } + if (failed(prepareSynthesisPlan(plan, target, *targetBasis))) { + signalPassFailure(); + return; + } IRRewriter rewriter(&getContext()); - for (Operation* operation : plan.operations) { - lowerTargetOperation(rewriter, cast(operation), - *targetBasis); + for (const auto& action : plan.operations) { + lowerTargetOperation(rewriter, cast(action.operation), + *targetBasis, action.reverseEntangler); } if (failed(prepareGlobalPhases(moduleOp, target))) { signalPassFailure(); @@ -527,6 +723,7 @@ struct VerifyTargetConformancePass final protected: void runOnOperation() override { + const auto sites = collectStaticSites(getOperation()); WalkResult result = getOperation()->walk([&](Operation* operation) { if (auto function = dyn_cast(operation); function && @@ -570,7 +767,9 @@ struct VerifyTargetConformancePass final return WalkResult::advance(); } - if (target.supports(operation)) { + const auto operationSites = getOperationSites(operation, sites); + if (operationSites ? target.supports(operation, *operationSites) + : target.supports(operation)) { return WalkResult::advance(); } diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 8e5a3724af..e6795779ae 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -33,6 +33,7 @@ #include #include +#include #include #include #include @@ -1478,6 +1479,60 @@ gphase(0.5); EXPECT_EQ(globalPhases, 1U); } +TEST_F(CompilerPipelineTest, QCOProgramCompilesForOneWayEntangler) { + constexpr llvm::StringLiteral source = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +cx q[0], q[1]; +cx q[1], q[0]; +)qasm"; + auto qc = QCProgram::fromQASMString(source.str()); + ASSERT_TRUE(qc); + auto qco = std::move(*qc).intoQCO(); + ASSERT_TRUE(qco); + + using TargetOperation = CompilerTarget::Operation; + using SiteId = CompilerTarget::SiteId; + std::vector operations{llvm::cantFail(TargetOperation::create("u", 1, 3)), + llvm::cantFail(TargetOperation::create( + "cx", 2, 0, {}, std::nullopt, std::nullopt, + std::vector>{{0, 1}}))}; + const auto target = llvm::cantFail(CompilerTarget::create( + 2, CompilerTarget::Connectivity::fromCouplings({{0, 1}}), + CompilerTarget::NativeOperations::fromOperations(operations))); + + ASSERT_TRUE(qco->compileForTarget(target)); + auto compiled = parseRecordedModule(qco->str()); + ASSERT_TRUE(compiled); + EXPECT_TRUE(verify(*compiled).succeeded()); + + llvm::DenseMap sites; + size_t numTwoQubitOperations = 0; + for (Operation& operation : + ::mlir::mqt::getEntryPoint(compiled.get()).getFunctionBody().getOps()) { + if (auto staticOp = dyn_cast(operation)) { + sites.try_emplace(staticOp.getQubit(), staticOp.getIndex()); + continue; + } + auto unitary = dyn_cast(operation); + if (!unitary) { + continue; + } + if (unitary.getNumQubits() == 2) { + ++numTwoQubitOperations; + const llvm::SmallVector orderedSites{ + sites.at(unitary.getInputQubit(0)), + sites.at(unitary.getInputQubit(1))}; + EXPECT_TRUE(target.supports(&operation, orderedSites)); + } + for (const auto [input, output] : + llvm::zip_equal(unitary.getInputQubits(), unitary.getOutputQubits())) { + sites.try_emplace(output, sites.at(input)); + } + } + EXPECT_GT(numTwoQubitOperations, 0U); +} + TEST_F(CompilerPipelineTest, QCOProgramCompilesDynamicRunForSupportedTargets) { constexpr llvm::StringLiteral source = R"mlir(module { func.func @main(%theta: f64 {mqt.input_name = "theta"}) attributes {mqt.entry_point} { diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index 77cd5ddd63..618c6f3260 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -648,7 +648,11 @@ TEST(CompilerTargetTest, SupportsRealQCOOperationsAndStructuralOps) { EXPECT_TRUE(target.supports(gphase)); EXPECT_TRUE(target.supports(cx, {10, 20})); EXPECT_FALSE(target.supports(cx, {20, 10})); + EXPECT_TRUE(target.supports(measure, {10})); + EXPECT_TRUE(target.supports(reset, {10})); EXPECT_FALSE(target.supports(nullptr)); + EXPECT_FALSE(target.supports(nullptr, {10})); + EXPECT_FALSE(target.supports(moduleOp->getOperation(), {10})); const auto closed = valid(Target::create( 2, Connectivity::allToAll(), NativeOperations::fromOperations({}))); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index 36483f07eb..394e4440b6 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -98,15 +98,7 @@ static bool isExecutable(Region& body, const auto siteB = m.at(unitaryOp.getInputQubit(1)); const auto vertexA = target.vertexForSite(siteA); const auto vertexB = target.vertexForSite(siteB); - std::array sites{siteA, siteB}; - const auto supported = - target.supports(&op) - ? target.supports(&op, sites) - : !target.synthesisBasis() || - target.supports(target.synthesisBasis()->entangler, - sites); - if (!vertexA || !vertexB || !target.areAdjacent(*vertexA, *vertexB) || - !supported) { + if (!vertexA || !vertexB || !target.areAdjacent(*vertexA, *vertexB)) { llvm::dbgs() << "The two-qubit gate (" << siteA << ", " << siteB << ") is not executable: \n"; unitaryOp->dump(); @@ -351,145 +343,6 @@ class MappingPassTest : public MappingPassFixture, }; // namespace -TEST_F(MappingPassFixture, MapOneWayCXInNativeDirection) { - using Operation = CompilerTarget::Operation; - using SiteTuple = CompilerTarget::SiteTuple; - - std::vector cxSites{llvm::cantFail(SiteTuple::create({0, 1}))}; - std::vector operations{ - llvm::cantFail(Operation::create("cx", 2, 0, std::move(cxSites)))}; - const auto target = llvm::cantFail( - CompilerTarget::create(2, std::nullopt, std::move(operations))); - - QCOProgramBuilder builder(context.get()); - builder.initialize(); - auto targetQubit = builder.allocQubit(); - auto controlQubit = builder.allocQubit(); - std::tie(controlQubit, targetQubit) = builder.cx(controlQubit, targetQubit); - builder.sink(controlQubit); - builder.sink(targetQubit); - - auto module = builder.finalize(); - ASSERT_TRUE(runPass(module.get(), target, - MappingPassOptions{.ntrials = 1, .seed = 42}) - .succeeded()); - ASSERT_TRUE(succeeded(verify(*module))); - EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target)); - - CtrlOp cx; - module->walk([&](CtrlOp op) { cx = op; }); - ASSERT_TRUE(cx); - auto mappedControl = cx.getInputControl(0).getDefiningOp(); - auto mappedTarget = cx.getInputTarget(0).getDefiningOp(); - ASSERT_TRUE(mappedControl); - ASSERT_TRUE(mappedTarget); - EXPECT_EQ(mappedControl.getIndex(), 0); - EXPECT_EQ(mappedTarget.getIndex(), 1); - - size_t numSwaps = 0; - module->walk([&](SWAPOp) { ++numSwaps; }); - EXPECT_EQ(numSwaps, 0); -} - -TEST_F(MappingPassFixture, OrientRoutedSWAPForOneWayEntangler) { - using Operation = CompilerTarget::Operation; - using SiteTuple = CompilerTarget::SiteTuple; - - std::vector cxSites{llvm::cantFail(SiteTuple::create({1, 0})), - llvm::cantFail(SiteTuple::create({2, 1}))}; - std::vector operations{ - llvm::cantFail(Operation::create("u", 1, 3)), - llvm::cantFail(Operation::create("cx", 2, 0, std::move(cxSites)))}; - const auto target = llvm::cantFail(CompilerTarget::create( - 3, std::vector{{0, 1}, {1, 2}}, - std::move(operations))); - - QCOProgramBuilder builder(context.get()); - builder.initialize(); - SmallVector qubits{builder.allocQubit(), builder.allocQubit(), - builder.allocQubit()}; - std::tie(qubits[0], qubits[1]) = builder.rxx(0.25, qubits[0], qubits[1]); - std::tie(qubits[1], qubits[2]) = builder.rzx(0.5, qubits[1], qubits[2]); - std::tie(qubits[0], qubits[2]) = builder.cx(qubits[0], qubits[2]); - for (const auto qubit : qubits) { - builder.sink(qubit); - } - - auto module = builder.finalize(); - ASSERT_TRUE(runPass(module.get(), target, MappingPassOptions{.ntrials = 1}) - .succeeded()); - ASSERT_TRUE(succeeded(verify(*module))); - EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target)); - - size_t numSwaps = 0; - module->walk([&](SWAPOp) { ++numSwaps; }); - EXPECT_GT(numSwaps, 0); -} - -TEST_F(MappingPassFixture, RejectSWAPWithoutSupportedEntanglerDirection) { - using Operation = CompilerTarget::Operation; - using SiteTuple = CompilerTarget::SiteTuple; - - std::vector cxSites{llvm::cantFail(SiteTuple::create({0, 1}))}; - std::vector operations{ - llvm::cantFail(Operation::create("u", 1, 3)), - llvm::cantFail(Operation::create("cx", 2, 0, std::move(cxSites)))}; - const auto target = llvm::cantFail(CompilerTarget::create( - 3, std::vector{{0, 1}, {1, 2}}, - std::move(operations))); - - QCOProgramBuilder builder(context.get()); - builder.initialize(); - SmallVector qubits{builder.allocQubit(), builder.allocQubit(), - builder.allocQubit()}; - std::tie(qubits[0], qubits[1]) = builder.cx(qubits[0], qubits[1]); - std::tie(qubits[1], qubits[2]) = builder.cx(qubits[1], qubits[2]); - std::tie(qubits[0], qubits[2]) = builder.cx(qubits[0], qubits[2]); - for (const auto qubit : qubits) { - builder.sink(qubit); - } - - auto module = builder.finalize(); - EXPECT_TRUE(failed( - runPass(module.get(), target, - MappingPassOptions{.niterations = 1, .ntrials = 1, .seed = 42}))); -} - -TEST_F(MappingPassFixture, CoalesceEquivalentDirectionalLookahead) { - using Operation = CompilerTarget::Operation; - using SiteTuple = CompilerTarget::SiteTuple; - - std::vector cxSites{llvm::cantFail(SiteTuple::create({0, 1})), - llvm::cantFail(SiteTuple::create({1, 2}))}; - std::vector operations{ - llvm::cantFail(Operation::create("u", 1, 3)), - llvm::cantFail(Operation::create("cx", 2, 0, std::move(cxSites)))}; - const auto target = llvm::cantFail(CompilerTarget::create( - 3, std::vector{{0, 1}, {1, 2}}, - std::move(operations))); - - QCOProgramBuilder builder(context.get()); - builder.initialize(); - SmallVector qubits{builder.allocQubit(), builder.allocQubit(), - builder.allocQubit()}; - for (size_t i = 0; i < 8; ++i) { - std::tie(qubits[0], qubits[1]) = builder.cx(qubits[0], qubits[1]); - } - std::tie(qubits[1], qubits[2]) = builder.cx(qubits[1], qubits[2]); - std::tie(qubits[1], qubits[0]) = builder.cx(qubits[1], qubits[0]); - for (const auto qubit : qubits) { - builder.sink(qubit); - } - - auto module = builder.finalize(); - ASSERT_TRUE( - runPass(module.get(), target, - MappingPassOptions{.niterations = 1, .ntrials = 1, .seed = 42}) - .succeeded()); - ASSERT_TRUE(succeeded(verify(*module))); - EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target)); -} - TEST_F(MappingPassFixture, MapTopologyOnlyWithEmptyOperationSet) { constexpr int64_t size = 3; @@ -581,6 +434,165 @@ TEST_F(MappingPassFixture, EXPECT_TRUE(isa(*measurement.getQubitOut().getUsers().begin())); } +TEST_F(MappingPassFixture, PrefersNativeDirectionWhenRouting) { + using Operation = CompilerTarget::Operation; + using SiteTuple = CompilerTarget::SiteTuple; + const auto target = llvm::cantFail(CompilerTarget::create( + 3, std::vector{{0, 1}, {1, 2}}, + std::vector{ + llvm::cantFail(Operation::create("u", 1, 3)), + llvm::cantFail(Operation::create( + "cx", 2, 0, + std::vector{llvm::cantFail(SiteTuple::create({1, 0})), + llvm::cantFail(SiteTuple::create({1, 2}))}))})); + + QCOProgramBuilder builder(context.get()); + builder.initialize(SmallVector(3, builder.getI1Type())); + SmallVector qubits(3); + SmallVector bits(3); + for (auto& qubit : qubits) { + qubit = builder.allocQubit(); + } + std::tie(qubits[0], qubits[1]) = builder.cx(qubits[0], qubits[1]); + std::tie(qubits[0], qubits[2]) = builder.cx(qubits[0], qubits[2]); + std::tie(qubits[2], qubits[1]) = builder.cx(qubits[2], qubits[1]); + for (size_t i = 0; i < qubits.size(); ++i) { + std::tie(qubits[i], bits[i]) = builder.measure(qubits[i]); + builder.sink(qubits[i]); + } + auto module = builder.finalize(bits); + + ASSERT_TRUE( + runPass(module.get(), target, + MappingPassOptions{.niterations = 1, .ntrials = 1, .seed = 42}) + .succeeded()); + EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target)); + + DenseMap sites; + size_t numControls = 0; + size_t numSwaps = 0; + for (mlir::Operation& operation : + getEntryPoint(module.get()).getFunctionBody().getOps()) { + if (auto staticOp = dyn_cast(operation)) { + sites.try_emplace(staticOp.getQubit(), staticOp.getIndex()); + continue; + } + if (auto unitary = dyn_cast(operation)) { + if (isa(operation)) { + ++numControls; + const std::array orderedSites{sites.at(unitary.getInputQubit(0)), + sites.at(unitary.getInputQubit(1))}; + EXPECT_TRUE(target.supports(&operation, orderedSites)) + << "control " << numControls << " uses sites " << orderedSites[0] + << " -> " << orderedSites[1]; + } + if (isa(operation)) { + ++numSwaps; + } + for (const auto [input, output] : llvm::zip_equal( + unitary.getInputQubits(), unitary.getOutputQubits())) { + const auto site = sites.at(input); + sites.try_emplace(output, site); + } + continue; + } + if (auto measure = dyn_cast(operation)) { + const auto site = sites.at(measure.getQubitIn()); + sites.try_emplace(measure.getQubitOut(), site); + } + } + EXPECT_EQ(numControls, 3); + EXPECT_EQ(numSwaps, 1); +} + +TEST_F(MappingPassFixture, MapsAllToAllGateToNativeDirection) { + using Operation = CompilerTarget::Operation; + using SiteTuple = CompilerTarget::SiteTuple; + const auto target = llvm::cantFail(CompilerTarget::create( + 3, std::nullopt, + std::vector{ + llvm::cantFail(Operation::create("u", 1, 3)), + llvm::cantFail(Operation::create( + "cx", 2, 0, + std::vector{llvm::cantFail(SiteTuple::create({1, 0})), + llvm::cantFail(SiteTuple::create({2, 0})), + llvm::cantFail(SiteTuple::create({2, 1}))}))})); + + QCOProgramBuilder builder(context.get()); + builder.initialize(SmallVector(2, builder.getI1Type())); + auto q0 = builder.allocQubit(); + auto q1 = builder.allocQubit(); + std::tie(q0, q1) = builder.cx(q0, q1); + auto [q0Out, b0] = builder.measure(q0); + auto [q1Out, b1] = builder.measure(q1); + builder.sink(q0Out); + builder.sink(q1Out); + auto module = builder.finalize({b0, b1}); + + ASSERT_TRUE( + runPass(module.get(), target, + MappingPassOptions{.niterations = 1, .ntrials = 1, .seed = 42}) + .succeeded()); + + DenseMap sites; + bool foundControl = false; + for (mlir::Operation& operation : + getEntryPoint(module.get()).getFunctionBody().getOps()) { + if (auto staticOp = dyn_cast(operation)) { + sites.try_emplace(staticOp.getQubit(), staticOp.getIndex()); + continue; + } + if (auto unitary = dyn_cast(operation)) { + if (isa(operation)) { + foundControl = true; + const std::array orderedSites{sites.at(unitary.getInputQubit(0)), + sites.at(unitary.getInputQubit(1))}; + EXPECT_TRUE(target.supports(&operation, orderedSites)); + } + for (const auto [input, output] : llvm::zip_equal( + unitary.getInputQubits(), unitary.getOutputQubits())) { + sites.try_emplace(output, sites.at(input)); + } + continue; + } + if (auto measure = dyn_cast(operation)) { + sites.try_emplace(measure.getQubitOut(), sites.at(measure.getQubitIn())); + } + } + EXPECT_TRUE(foundControl); +} + +TEST_F(MappingPassFixture, RejectsUnreachableAllToAllGate) { + using Operation = CompilerTarget::Operation; + using SiteTuple = CompilerTarget::SiteTuple; + const auto target = llvm::cantFail(CompilerTarget::create( + 3, std::nullopt, + std::vector{ + llvm::cantFail(Operation::create("u", 1, 3)), + llvm::cantFail(Operation::create( + "cx", 2, 0, + std::vector{llvm::cantFail(SiteTuple::create({0, 1}))}))})); + + QCOProgramBuilder builder(context.get()); + builder.initialize(SmallVector(3, builder.getI1Type())); + SmallVector qubits(3); + SmallVector bits(3); + for (auto& qubit : qubits) { + qubit = builder.allocQubit(); + } + std::tie(qubits[0], qubits[1]) = builder.cx(qubits[0], qubits[1]); + std::tie(qubits[0], qubits[2]) = builder.cx(qubits[0], qubits[2]); + for (size_t i = 0; i < qubits.size(); ++i) { + std::tie(qubits[i], bits[i]) = builder.measure(qubits[i]); + builder.sink(qubits[i]); + } + auto module = builder.finalize(bits); + + EXPECT_TRUE(failed( + runPass(module.get(), target, + MappingPassOptions{.niterations = 1, .ntrials = 1, .seed = 42}))); +} + TEST_F(MappingPassFixture, PreserveNoncontiguousTargetSiteIds) { constexpr int64_t size = 3; diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp index 28d3e660a4..abd44ffd31 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -61,6 +62,7 @@ using Connectivity = Target::Connectivity; using NativeOperations = Target::NativeOperations; using Operation = Target::Operation; using Site = Target::Site; +using SiteTuple = Target::SiteTuple; using mlir::ModuleOp; using mlir::OwningOpRef; using mlir::Value; @@ -165,6 +167,14 @@ makeUCxTarget(std::optional> sites = std::nullopt) { NativeOperations::fromOperations(operations))); } +[[nodiscard]] static Target makeOneWayUCxTarget() { + std::vector operations{ + valid(Operation::create("u", 1, 3)), + valid(Operation::create("cx", 2, 0, + std::vector{valid(SiteTuple::create({1, 0}))}))}; + return valid(Target::create(2, std::nullopt, std::move(operations))); +} + [[nodiscard]] static mlir::DenseElementsAttr denseMatrix(QCOProgramBuilder& builder, const int64_t dimension, const llvm::ArrayRef> values) { @@ -207,7 +217,8 @@ class TargetSynthesisTest : public testing::Test { void SetUp() override { mlir::DialectRegistry registry; registry.insert(); + mlir::qco::QCODialect, mlir::qtensor::QTensorDialect, + mlir::scf::SCFDialect>(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); @@ -384,6 +395,183 @@ TEST_F(TargetSynthesisTest, TargetNativeSynthesisRemovesOrdinarySwap) { expectEquivalent(expected, synthesized); } +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisKeepsSupportedEntanglerDirection) { + auto module = build([](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q1, q0) = builder.cx(q1, q0); + return builder.intConstant(0); + }); + const auto target = makeOneWayUCxTarget(); + const auto before = printModule(*module); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_EQ(printModule(*module), before); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); +} + +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisReversesEntanglerWithoutChangingSemantics) { + const auto forwardCx = [](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.cx(q0, q1); + return builder.intConstant(0); + }; + auto expected = build(forwardCx); + auto synthesized = build(forwardCx); + const auto before = printModule(*synthesized); + const auto target = makeOneWayUCxTarget(); + + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_NE(printModule(*synthesized), before); + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*synthesized))); + expectEquivalent(expected, synthesized); +} + +TEST_F(TargetSynthesisTest, ConformanceRejectsUnsupportedEntanglerDirection) { + auto module = build([](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.cx(q0, q1); + return builder.intConstant(0); + }); + const auto diagnostics = expectFailure( + *module, mlir::qco::createVerifyTargetConformance(makeOneWayUCxTarget())); + EXPECT_NE(diagnostics.find("target does not support operation 'qco.ctrl'"), + std::string::npos) + << diagnostics; +} + +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisTracksSitesThroughStructuredControlFlow) { + auto module = build([](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + const auto outputs = + builder.qcoIf(true, ValueRange{q0, q1}, [&](ValueRange arguments) { + auto first = arguments[0]; + auto second = arguments[1]; + std::tie(first, second) = builder.cx(first, second); + return mlir::SmallVector{first, second}; + }); + for (Value output : outputs) { + builder.sink(output); + } + return builder.intConstant(0); + }); + const auto target = makeOneWayUCxTarget(); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*module))); +} + +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisTracksSitesThroughAllStructuredOperations) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func @main() { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %false = arith.constant false + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %f0, %f1 = scf.for %i = %c0 to %c1 step %c1 + iter_args(%a = %q0, %b = %q1) + -> (!qco.qubit, !qco.qubit) { + %s0, %s1 = qco.swap %a, %b + : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + scf.yield %s0, %s1 : !qco.qubit, !qco.qubit + } + %w0, %w1 = scf.while (%a = %f0, %b = %f1) + : (!qco.qubit, !qco.qubit) -> (!qco.qubit, !qco.qubit) { + %s0, %s1 = qco.swap %a, %b + : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + scf.condition(%false) %s0, %s1 : !qco.qubit, !qco.qubit + } do { + ^bb0(%a: !qco.qubit, %b: !qco.qubit): + %s0, %s1 = qco.swap %a, %b + : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + scf.yield %s0, %s1 : !qco.qubit, !qco.qubit + } + %i0, %i1 = qco.index_switch %c0 -> (!qco.qubit, !qco.qubit) + case 0 args(%a = %w0, %b = %w1) { + %s0, %s1 = qco.swap %a, %b + : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + qco.yield %s0, %s1 : !qco.qubit, !qco.qubit + } + default args(%a = %w0, %b = %w1) { + qco.yield %a, %b : !qco.qubit, !qco.qubit + } + qco.sink %i0 : !qco.qubit + qco.sink %i1 : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + const auto target = makeOneWayUCxTarget(); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_EQ(countOps(*module), 0U); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*module))); +} + +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisRejectsUnavailableSiteLocalBasis) { + auto module = build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(1); + qubit = builder.h(qubit); + return builder.intConstant(0); + }); + const auto target = valid(Target::create( + 2, std::nullopt, + std::vector{valid(Operation::create( + "u", 1, 3, std::vector{valid(SiteTuple::create({0}))})), + valid(Operation::create("cx", 2, 0))})); + + const auto diagnostics = + expectFailure(*module, mlir::qco::createTargetNativeSynthesis(target)); + EXPECT_NE(diagnostics.find("no usable single-qubit basis on site 1"), + std::string::npos) + << diagnostics; +} + +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisRejectsUnavailableEntanglerPair) { + auto module = build([](QCOProgramBuilder& builder) { + auto q1 = builder.staticQubit(1); + auto q2 = builder.staticQubit(2); + std::tie(q1, q2) = builder.swap(q1, q2); + return builder.intConstant(0); + }); + const auto target = valid(Target::create( + 3, std::nullopt, + std::vector{ + valid(Operation::create("u", 1, 3)), + valid(Operation::create( + "cx", 2, 0, std::vector{valid(SiteTuple::create({0, 1}))}))})); + + const auto diagnostics = + expectFailure(*module, mlir::qco::createTargetNativeSynthesis(target)); + EXPECT_NE(diagnostics.find("no usable entangler on sites 1 and 2"), + std::string::npos) + << diagnostics; +} + TEST_F(TargetSynthesisTest, TargetNativeSynthesisLowersConstantSingleQubitGate) { const auto hadamard = [](QCOProgramBuilder& builder) { From 33710b4fe09b5a669792dd44f66cc19c1b2518e8 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 3 Sep 2026 18:36:25 +0200 Subject: [PATCH 3/6] =?UTF-8?q?=E2=9C=A8=20Finalize=20directional=20target?= =?UTF-8?q?=20mapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the intermediate target-cost helper with a focused mapping wrapper, keep target applicability exact, and make native synthesis direction-aware without partial rewrites. Add focused C++, Python, QDMI, and documentation coverage. Assisted-by: OpenAI Codex --- .agent/plans/directional-gate-mapping.md | 190 +++++++++ CHANGELOG.md | 8 +- bindings/mlir/register_mlir.cpp | 12 +- bindings/patterns.txt | 1 + docs/mlir/target_compilation.md | 26 +- mlir/include/mlir/Compiler/MappingTarget.h | 60 +++ mlir/include/mlir/Compiler/QDMIAdapter.h | 8 +- mlir/include/mlir/Compiler/Target.h | 4 + mlir/include/mlir/Compiler/TargetCost.h | 49 --- .../include/mlir/Dialect/MQT/IR/MQTDialect.td | 38 +- mlir/lib/Compiler/CMakeLists.txt | 8 +- mlir/lib/Compiler/MappingTarget.cpp | 99 +++++ mlir/lib/Compiler/QDMIAdapter.cpp | 77 ++-- mlir/lib/Compiler/Target.cpp | 275 +++++-------- mlir/lib/Compiler/TargetCost.cpp | 149 -------- mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 72 +++- .../QCO/Transforms/Mapping/Mapping.cpp | 136 ++----- .../NativeSynthesis/TargetSynthesis.cpp | 360 ++++++++++-------- mlir/unittests/Compiler/CMakeLists.txt | 5 +- .../Compiler/test_compiler_qdmi_adapter.cpp | 43 ++- .../Compiler/test_compiler_target.cpp | 67 ++++ .../Compiler/test_mapping_target.cpp | 136 +++++++ mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp | 106 +++++- .../QCO/Transforms/Mapping/test_mapping.cpp | 147 ++----- .../NativeSynthesis/test_target_synthesis.cpp | 190 +++++++-- python/mqt/core/mlir.pyi | 16 +- test/python/test_mlir.py | 24 +- 27 files changed, 1411 insertions(+), 895 deletions(-) create mode 100644 .agent/plans/directional-gate-mapping.md create mode 100644 mlir/include/mlir/Compiler/MappingTarget.h delete mode 100644 mlir/include/mlir/Compiler/TargetCost.h create mode 100644 mlir/lib/Compiler/MappingTarget.cpp delete mode 100644 mlir/lib/Compiler/TargetCost.cpp create mode 100644 mlir/unittests/Compiler/test_mapping_target.cpp diff --git a/.agent/plans/directional-gate-mapping.md b/.agent/plans/directional-gate-mapping.md new file mode 100644 index 0000000000..8898955089 --- /dev/null +++ b/.agent/plans/directional-gate-mapping.md @@ -0,0 +1,190 @@ +# Support directional target gates during mapping + +This ExecPlan is a living document. The sections `Progress`, +`Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must +be kept up to date as work proceeds. + +This ExecPlan must be maintained in accordance with `.agent/PLANS.md` from the +repository root. + +## Purpose / Big Picture + +MQT Core target compilation must accept devices that expose a two-qubit gate on +each topology edge in only one operand order. After this change, mapping uses +the undirected topology for reachability but prefers a layout whose ordered +operands match the device. Native synthesis repairs an unavoidable opposite +order without changing program semantics, and final conformance verifies the +exact ordered placement. + +The result is visible by compiling alternating CX directions for a two-site +one-way target: compilation succeeds, every emitted two-qubit operation uses the +reported direction, and final target conformance succeeds. + +## Progress + +- [x] (2026-09-03) Added exact ordered operation applicability to the target + model, QDMI adapter, typed attributes, and Python bindings. +- [x] (2026-09-03) Added cached directional mapping costs while preserving + undirected topology traversal. +- [x] (2026-09-03) Added site-aware native synthesis and exact final conformance + checks. +- [x] (2026-09-03) Added focused C++ and Python regressions, regenerated stubs, + and completed the Core build and lint checks. + +## Surprises & Discoveries + +- Observation: QDMI site tuples are sparse calibration records and cannot also + represent operation availability. Evidence: operations with default + calibration have no `SiteTuple` entries even though their QDMI site list is + complete. +- Observation: mathematically symmetric gates can still be reported in one + syntactic operand order. Runtime-parameter RXX cannot be matrix-decomposed, so + native synthesis must clone it with swapped inputs and restore the result + order. +- Observation: a qubit emerging from structured control flow may have several + possible sites. A one-qubit operation is conformant only when it is supported + on every possible site; a direction-dependent two-qubit operation requires an + exact ordered placement. +- Observation: target site IDs span all nonnegative `int64_t` values, including + values reserved internally by LLVM dense containers. Scalar site-ID caches + therefore use standard unordered sets; maximum-value round-trip tests cover + this boundary. + +## Decision Log + +- Decision: keep `CompilerTarget` immutable and represent mapping policy in a + cached `MappingTarget` wrapper. Rationale: mapping costs are derived data, + useful as one coherent view, and should not mutate the device snapshot. + Date/Author: 2026-09-03, contributor. +- Decision: preserve exact ordered applicability for every operation, including + mathematically symmetric gates. Rationale: the target model records what a + backend actually reports; any safe operand reorder is an explicit synthesis + transformation and final conformance remains exact. Date/Author: 2026-09-03, + contributor. +- Decision: keep Mapping synthesis-free and base its directional penalty on the + target-wide synthesis entangler. Rationale: arbitrary non-native two-qubit + gates are lowered through that entangler, while actual reversal belongs in + native synthesis. Date/Author: 2026-09-03, contributor. +- Decision: use a unit routing penalty for an adjacent edge available only in + reverse. Rationale: it models the additional local direction repair while + leaving nonadjacent cost equal to shortest-path SWAP distance. Date/Author: + 2026-09-03, contributor. + +## Outcomes & Retrospective + +The Core compiler now preserves exact ordered applicability through target +materialization, QDMI snapshots, mapping, native synthesis, and conformance. The +focused Compiler, MQT IR, Mapping, NativeSynthesis, and Python MLIR suites pass. +Generated stubs, repository lint, C++ lint, documentation, and +`git diff --check` also pass. Regression coverage includes one-way entanglers, +ambiguous structured-control-flow sites, and the full nonnegative site-ID +domain. + +## Context and Orientation + +`mlir/include/mlir/Compiler/Target.h` and `mlir/lib/Compiler/Target.cpp` define +the immutable device snapshot. An operation may be unrestricted or may list +exact ordered physical site tuples. `SiteTuple` remains calibration-only. + +`mlir/include/mlir/Compiler/MappingTarget.h` and +`mlir/lib/Compiler/MappingTarget.cpp` form a cheap wrapper around that snapshot. +For each explicit topology edge they cache whether the synthesis entangler is +native in the forward order, reverse order, or both. Mapping uses the undirected +edge to move qubits and the cached ordered cost to select a layout. + +`mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp` implements placement and +routing. Its lookahead window must retain semantic operand order while its +pair-block bookkeeping remains order-independent. + +`mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp` lowers +operations after mapping. It derives each linear qubit value's static target +site through structured control flow, checks exact native support, and either +keeps, reorders, or decomposes an operation. The final conformance pass uses the +same ordered site facts. + +`mlir/lib/Compiler/QDMIAdapter.cpp` snapshots device data. QDMI operation site +lists become applicability; per-site duration and fidelity differences become +sparse calibration tuples. The MQT dialect attribute files serialize both states +without losing an explicit empty applicability list. + +## Plan of Work + +First, complete the compiler-target representation and its typed MLIR attribute. +Validate tuple arity, distinct nonnegative sites, known target sites, and +calibration references. Cache one- and two-site applicability for constant +ordered queries, while retaining exact tuple matching for higher arities. + +Second, wrap the target in `MappingTarget`. Construct its adjacent direction +costs once, proxy the topology operations required by Mapping, and change only +goal, heuristic, placement delegation, and advance checks. Preserve the merged +window traversal and use semantic operand order recovered from each unitary's +outputs. + +Third, make native synthesis site-aware. Propagate static sites to a fixed point +through QCO and SCF structured operations. Reject ambiguous or nonadjacent +placements before rewriting. Reverse asymmetric basis synthesis mathematically; +for a symmetric operation supported only in reverse, clone it with swapped +operands and map its outputs back, which also supports runtime parameters. + +Finally, validate the layers independently and together and regenerate the +Python stubs through the repository's Nox session. + +## Concrete Steps + +Run all commands from the repository root. Set `MLIR_DIR` to the directory that +contains `MLIRConfig.cmake` for MLIR 23.1 or newer, then configure Core: + + cmake --preset release + +Build and run the focused binaries: + + cmake --build build/release --target \ + mqt-core-mlir-unittests-compiler \ + mqt-core-mlir-unittest-mqt-ir \ + mqt-core-mlir-unittest-mapping \ + mqt-core-mlir-unittest-target-synthesis + build/release/mlir/unittests/Compiler/mqt-core-mlir-unittests-compiler + build/release/mlir/unittests/Dialect/MQT/IR/mqt-core-mlir-unittest-mqt-ir + build/release/mlir/unittests/Dialect/QCO/Transforms/Mapping/mqt-core-mlir-unittest-mapping + build/release/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/mqt-core-mlir-unittest-target-synthesis + +Regenerate and test Python bindings, then run repository checks: + + uvx nox -s stubs + uvx nox -s tests-3.13 -- test/python/test_mlir.py -q + uvx nox -s cpp-lint + uvx nox -s docs + git diff --check + uvx nox -s lint + +## Validation and Acceptance + +Compiler-target tests must distinguish unrestricted, explicit, and +explicit-empty applicability and preserve those states through typed MLIR and +Python. QDMI tests must preserve exact one-way and two-way site lists while +keeping calibration sparse. + +Mapping tests must show that a three-site one-way chain chooses the native +orientation and inserts exactly the expected SWAP, and that the two-site search +budget can repair an opposite direction. Native synthesis tests must prove +semantic equivalence for reversed CX, exact conformance rejection before +synthesis, runtime RXX operand reordering without a synthesis basis, and safe +failure for ambiguous structured-control-flow sites. + +The compiler pipeline test must compile alternating CX directions and verify +that every final two-qubit operation is supported on its exact static sites. The +C++ linter, Python lint, strict documentation, generated-stub check, and +`git diff --check` must pass without LCOV exclusions. + +## Idempotence and Recovery + +Source edits, formatting, configuration, builds, and tests are repeatable. +Preserve unrelated changes and do not modify another task's worktree. If +generated stubs differ, rerun the repository's `stubs` Nox session instead of +editing them by hand. If a target cannot supply a global synthesis basis, direct +symmetric native reordering may still proceed, but directional mapping falls +back to the ordinary topology because there is no single entangler whose +direction can safely represent every non-native operation. + +Revision note (2026-09-03): Retained only Core design, recovery, and validation +information. diff --git a/CHANGELOG.md b/CHANGELOG.md index 283327b8f7..2f73b04d38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,9 +38,10 @@ releases may include breaking changes. direct lowering and dense-array helpers for supported compiler inputs ([#1915], [#1973], [#2077], [#2078], [#2079], [#2334]) ([**@simon1hofmann**], [**@burgholzer**]) -- ✨ Add immutable MLIR compiler targets, QDMI device integration, and target - compilation through C++, Python, and `mqt-cc` ([#1687], [#1993], [#1999], - [#2049]) ([**@MatthiasReumann**], [**@simon1hofmann**], [**@burgholzer**]) +- ✨ Add immutable MLIR compiler targets, QDMI device integration, ordered + operation applicability, directional mapping, and target compilation through + C++, Python, and `mqt-cc` ([#1687], [#1993], [#1999], [#2049], [#2285]) + ([**@MatthiasReumann**], [**@simon1hofmann**], [**@burgholzer**]) #### Import and export @@ -876,6 +877,7 @@ for previous changelogs._ [#2315]: https://github.com/munich-quantum-toolkit/core/pull/2315 [#2299]: https://github.com/munich-quantum-toolkit/core/pull/2299 [#2298]: https://github.com/munich-quantum-toolkit/core/pull/2298 +[#2285]: https://github.com/munich-quantum-toolkit/core/pull/2285 [#2284]: https://github.com/munich-quantum-toolkit/core/pull/2284 [#2283]: https://github.com/munich-quantum-toolkit/core/pull/2283 [#2288]: https://github.com/munich-quantum-toolkit/core/pull/2288 diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 4b2eaab15b..760996c97a 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -654,17 +654,19 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); operation.siteTuples().begin(), operation.siteTuples().end()); }, "Ordered site-specific calibration data.") - .def_prop_ro("has_explicit_applicability", - &mlir::CompilerTarget::Operation::hasExplicitApplicability, - "Whether ordered site applicability is explicit.") .def_prop_ro( "applicable_site_tuples", - [](const mlir::CompilerTarget::Operation& operation) { + [](const mlir::CompilerTarget::Operation& operation) + -> std::optional< + std::vector>> { + if (!operation.hasExplicitApplicability()) { + return std::nullopt; + } return std::vector>( operation.applicableSiteTuples().begin(), operation.applicableSiteTuples().end()); }, - "The explicitly applicable ordered target-site tuples.") + "The ordered target-site tuples, or None when unrestricted.") .def_prop_ro("duration", &mlir::CompilerTarget::Operation::duration, "The raw default duration, if available.") .def_prop_ro("fidelity", &mlir::CompilerTarget::Operation::fidelity, diff --git a/bindings/patterns.txt b/bindings/patterns.txt index d2a24897c9..29183e0568 100644 --- a/bindings/patterns.txt +++ b/bindings/patterns.txt @@ -138,6 +138,7 @@ mqt\.core\.mlir\.CompilerTarget\.Operation\.__init__$: site_tuples: Sequence[CompilerTarget.SiteTuple] | None = None, duration: int | None = None, fidelity: float | None = None, + applicable_site_tuples: Sequence[Sequence[int]] | None = None, ) -> None: \doc diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 6a096e949b..b57f89a09f 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -2,9 +2,9 @@ An MLIR {code}`mlir::CompilerTarget` is an immutable snapshot of a circuit-model device. It contains the device sites, topology, native operations, and available -calibration data. Compilation decomposes supported multi-qubit operations, -optimizes and maps the program, synthesizes native gates, and verifies that the -result conforms to the target. +calibration and ordered-applicability data. Compilation decomposes supported +multi-qubit operations, optimizes and maps the program, synthesizes native +gates, and verifies that the result conforms to the target. The snapshot is independent of its originating QDMI session. It can therefore be stored, copied cheaply, and reused for multiple compilations. @@ -41,7 +41,12 @@ target = CompilerTarget( num_parameters=1, ), CompilerTarget.Operation("u", arity=1, num_parameters=3), - CompilerTarget.Operation("cx", arity=2, num_parameters=0), + CompilerTarget.Operation( + "cx", + arity=2, + num_parameters=0, + applicable_site_tuples=[(1, 0), (1, 2)], + ), CompilerTarget.Operation("measure", arity=1, num_parameters=0), CompilerTarget.Operation("reset", arity=1, num_parameters=0), ]), @@ -59,8 +64,11 @@ set. An explicit operation arity is either fixed or variadic with a positive, inclusive minimum. Fixed zero represents a global-phase operation. A variadic capability accepts every total width from its minimum through the target's site count; site-specific calibration tuples are therefore available only for fixed, -positive arities. Structural and program-format constructs are not -compiler-target operations. +positive arities. An omitted `applicable_site_tuples` value makes an operation +available on every valid placement. An explicit list restricts support to those +ordered tuples; it is independent of the sparse calibration entries in +`site_tuples`. Structural and program-format constructs are not compiler-target +operations. Target synthesis preserves a native `gphase`. If the target does not support `gphase`, target synthesis preserves relative phase effects and removes only the @@ -136,9 +144,9 @@ if (!qco || !qco->compileForTarget(*target)) { } ``` -The adapter accepts circuit-model devices whose operations are available -throughout the topology in both operand orientations. Operand-symmetric gates, -such as CZ, may report each edge once. Operations with arity above two must +The adapter accepts circuit-model devices whose two-qubit operations cover every +topology edge in at least one operand orientation and preserves the exact +ordered tuples reported by the device. Operations with arity above two must report every ordered tuple of distinct sites. Neutral-atom zone models require a different compilation model and are rejected with a diagnostic. diff --git a/mlir/include/mlir/Compiler/MappingTarget.h b/mlir/include/mlir/Compiler/MappingTarget.h new file mode 100644 index 0000000000..eccff8e7ca --- /dev/null +++ b/mlir/include/mlir/Compiler/MappingTarget.h @@ -0,0 +1,60 @@ +/* + * 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/Compiler/Target.h" + +#include +#include + +#include + +namespace mlir { + +/// Compiler-target topology with cached directional mapping costs. +/// +/// Adjacent native entangler directions have cost zero. A direction that can +/// be synthesized from the reverse native direction has cost one. Symmetric +/// entanglers have cost zero in both directions. Nonadjacent costs are the +/// target's shortest-path distance minus one. Construction visits each target +/// coupling at most once and all cost lookups are constant time. +class MappingTarget { +public: + explicit MappingTarget(const CompilerTarget& target); + + /// Return the immutable compiler target. + [[nodiscard]] const CompilerTarget& compilerTarget() const noexcept; + + /// Return the number of target sites. + [[nodiscard]] size_t numSites() const noexcept; + + /// Return the target topology's maximum degree. + [[nodiscard]] size_t maxDegree() const noexcept; + + /// Return the shortest-path distance between two valid target vertices. + [[nodiscard]] size_t distanceBetween(size_t source, size_t target) const; + + /// Invoke @p callback for every neighbour of a valid target vertex. + void forEachNeighbour(size_t vertex, + llvm::function_ref callback) const; + + /// Return the routing cost from @p source to @p target. + [[nodiscard]] float pathCostBetween(size_t source, size_t target) const; + + /// Return whether a two-qubit gate is executable in this order. + [[nodiscard]] bool isExecutable(size_t source, size_t target) const; + +private: + CompilerTarget target_; + llvm::DenseSet penalizedDirections_; +}; + +} // namespace mlir diff --git a/mlir/include/mlir/Compiler/QDMIAdapter.h b/mlir/include/mlir/Compiler/QDMIAdapter.h index 90efdca8e4..62b41baf52 100644 --- a/mlir/include/mlir/Compiler/QDMIAdapter.h +++ b/mlir/include/mlir/Compiler/QDMIAdapter.h @@ -29,10 +29,10 @@ namespace mlir { * * @details The returned target owns all queried metadata and remains valid * after the originating device and session have been destroyed. Neutral-atom - * zone models are not supported. Explicit QDMI site lists are accepted for - * one- and two-qubit operations only: one-qubit lists must cover every site and - * two-qubit lists every undirected topology edge. Their ordered tuples and - * calibration data are preserved. + * zone models are not supported. Explicit QDMI site lists must cover every + * site for one-qubit operations, every undirected topology edge for two-qubit + * operations, and every ordered tuple of distinct sites for higher arities. + * Their ordered applicability and calibration data are preserved separately. */ [[nodiscard]] llvm::Expected compilerTargetFromDevice(const qdmi::Device& device); diff --git a/mlir/include/mlir/Compiler/Target.h b/mlir/include/mlir/Compiler/Target.h index 4b7d6236de..e438397609 100644 --- a/mlir/include/mlir/Compiler/Target.h +++ b/mlir/include/mlir/Compiler/Target.h @@ -444,6 +444,10 @@ class CompilerTarget { Connectivity connectivity, NativeOperations nativeOperations, std::optional durationUnit); + [[nodiscard]] bool + supportsImpl(::mlir::Operation* operation, + std::optional> sites) const; + [[nodiscard]] llvm::ArrayRef explicitNeighbours(size_t vertex) const; std::shared_ptr storage_; diff --git a/mlir/include/mlir/Compiler/TargetCost.h b/mlir/include/mlir/Compiler/TargetCost.h deleted file mode 100644 index fb86818800..0000000000 --- a/mlir/include/mlir/Compiler/TargetCost.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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/Compiler/Target.h" - -#include - -#include - -namespace mlir { - -/** - * @brief Cached routing costs for a target gate. - * - * @details The cache is derived from an immutable compiler target. A native - * ordered coupling has cost zero, a coupling available only in the opposite - * direction has cost one, and an unavailable coupling has infinite cost. - * Nonadjacent costs remain the target's shortest-path distance minus one. - * Construction is linear in the explicit topology or reported site tuples; - * unrestricted all-to-all targets do not enumerate every qubit pair. - */ -class TargetGateCosts { -public: - /// Construct routing costs for a recognized two-qubit gate. - TargetGateCosts(const CompilerTarget& target, CompilerTarget::GateKind gate); - - /// Return the cached routing cost between two valid target vertices. - [[nodiscard]] float routingCostBetween(size_t source, size_t target) const; - - /// Return whether every adjacent ordered pair has zero gate cost. - [[nodiscard]] bool isUniform() const noexcept; - -private: - CompilerTarget target_; - llvm::DenseMap costs_; - float defaultAdjacentCost_ = 0.F; - bool uniform_ = true; -}; - -} // namespace mlir diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td index f5c762b6bc..f684dc5ca3 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td @@ -84,6 +84,14 @@ def OperationArityKind let genSpecializedAttr = 0; } +def OperationApplicabilityKind + : I32EnumAttr<"OperationApplicabilityKind", "Operation applicability", + [I32EnumAttrCase<"Unrestricted", 0, "unrestricted">, + I32EnumAttrCase<"Explicit", 1, "explicit">]> { + let cppNamespace = "::mlir::mqt"; + let genSpecializedAttr = 0; +} + def DurationUnitAttr : MQTAttr<"DurationUnit", "duration_unit"> { let summary = "Unit for raw compiler-target durations"; let description = [{ @@ -124,7 +132,8 @@ def CouplingAttr : MQTAttr<"Coupling", "coupling"> { def SiteTupleAttr : MQTAttr<"SiteTuple", "site_tuple"> { let summary = "Calibration data for an ordered site tuple"; let description = [{ - The tuple defines one placement of a native operation. For example, + The tuple records calibration for one ordered placement without constraining + operation applicability. For example, `#mqt.site_tuple` records an ordered two-site placement with a raw duration of 40. }]; @@ -135,6 +144,18 @@ def SiteTupleAttr : MQTAttr<"SiteTuple", "site_tuple"> { let genVerifyDecl = 1; } +def ApplicableSiteTupleAttr + : MQTAttr<"ApplicableSiteTuple", "applicable_site_tuple"> { + let summary = "One ordered site tuple supporting an operation"; + let description = [{ + The tuple records one exact ordered placement on which a native operation + is available. Unlike `#mqt.site_tuple`, it carries no calibration data. + }]; + let parameters = (ins MQTArrayRefParameter<"int64_t">:$sites); + let assemblyFormat = "`<` struct(params) `>`"; + let genVerifyDecl = 1; +} + def OperationArityAttr : MQTAttr<"OperationArity", "operation_arity"> { let summary = "Accepted width of a compiler-target operation"; let description = [{ @@ -153,20 +174,24 @@ def NativeOperationAttr : MQTAttr<"NativeOperation", "native_operation"> { let summary = "One native compiler-target operation"; let description = [{ The operation records its spelling, arity, parameter count, and optional - global or site-specific calibration data. The following example records a - placed controlled-X operation: + global or site-specific calibration data. Applicability is either + unrestricted or an explicit list of exact ordered site tuples. The + following example records a directional controlled-X operation: ```mlir #mqt.native_operation, - num_parameters = 0, site_tuples = []> + num_parameters = 0, site_tuples = [], applicability = explicit, + applicable_site_tuples = []> ``` }]; let parameters = (ins "StringAttr":$name, "OperationArityAttr":$arity, "uint64_t":$num_parameters, MQTArrayRefParameter<"SiteTupleAttr">:$site_tuples, MQTOptionalUInt64Parameter<>:$duration, - OptionalParameter<"FloatAttr">:$fidelity); + OptionalParameter<"FloatAttr">:$fidelity, + EnumParameter:$applicability, + MQTArrayRefParameter<"ApplicableSiteTupleAttr">:$applicable_site_tuples); let assemblyFormat = "`<` struct(params) `>`"; let genVerifyDecl = 1; } @@ -188,7 +213,8 @@ def CompilationTargetAttr : MQTAttr<"CompilationTarget", "compilation_target"> { native_operations = explicit, operations = [, - num_parameters = 0, site_tuples = []>]> + num_parameters = 0, site_tuples = [], applicability = explicit, + applicable_site_tuples = []>]> ``` }]; let parameters = (ins OptionalParameter<"StringAttr">:$name, diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index f48f78de9c..0411204374 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -10,8 +10,8 @@ add_mlir_library( MQTCompilerTarget PARTIAL_SOURCES_INTENDED + MappingTarget.cpp Target.cpp - TargetCost.cpp ADDITIONAL_HEADER_DIRS ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler LINK_LIBS @@ -29,8 +29,8 @@ target_sources( BASE_DIRS ${MQT_MLIR_SOURCE_INCLUDE_DIR} FILES - ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/Target.h - ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/TargetCost.h) + ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/MappingTarget.h + ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/Target.h) # Build the optional QDMI-to-compiler-target adapter set(LLVM_REQUIRES_EH ON) @@ -93,7 +93,7 @@ mqt_mlir_target_use_project_options(MQTCompilerPipeline) # collect header files file(GLOB_RECURSE COMPILER_HEADERS_SOURCE "${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/*.h") -list(FILTER COMPILER_HEADERS_SOURCE EXCLUDE REGEX "/(QDMIAdapter|Target)\\.h$") +list(FILTER COMPILER_HEADERS_SOURCE EXCLUDE REGEX "/(MappingTarget|QDMIAdapter|Target)\\.h$") target_sources(MQTCompilerPipeline PUBLIC FILE_SET HEADERS BASE_DIRS ${MQT_MLIR_SOURCE_INCLUDE_DIR} FILES ${COMPILER_HEADERS_SOURCE}) diff --git a/mlir/lib/Compiler/MappingTarget.cpp b/mlir/lib/Compiler/MappingTarget.cpp new file mode 100644 index 0000000000..cd017d42b4 --- /dev/null +++ b/mlir/lib/Compiler/MappingTarget.cpp @@ -0,0 +1,99 @@ +/* + * 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/Compiler/MappingTarget.h" + +#include +#include + +namespace mlir { + +[[nodiscard]] static constexpr bool +isSwapInvariant(CompilerTarget::GateKind gate) { + using Gate = CompilerTarget::GateKind; + switch (gate) { + case Gate::CZ: + case Gate::ISWAP: + case Gate::RXX: + case Gate::RYY: + case Gate::RZZ: + return true; + default: + return false; + } +} + +MappingTarget::MappingTarget(const CompilerTarget& target) : target_(target) { + const auto basis = target_.synthesisBasis(); + if (!basis || isSwapInvariant(basis->entangler)) { + return; + } + + for (size_t source = 0; source < target_.numSites(); ++source) { + target_.forEachNeighbour(source, [&](size_t targetVertex) { + if (targetVertex < source) { + return; + } + + const auto sourceSite = target_.siteForVertex(source); + const auto targetSite = target_.siteForVertex(targetVertex); + const std::array forwardSites{sourceSite, targetSite}; + const std::array reverseSites{targetSite, sourceSite}; + const bool forward = target_.supports(basis->entangler, forwardSites); + const bool reverse = target_.supports(basis->entangler, reverseSites); + + if (!forward) { + penalizedDirections_.insert( + CompilerTarget::Coupling{sourceSite, targetSite}); + } + if (!reverse) { + penalizedDirections_.insert( + CompilerTarget::Coupling{targetSite, sourceSite}); + } + }); + } +} + +const CompilerTarget& MappingTarget::compilerTarget() const noexcept { + return target_; +} + +size_t MappingTarget::numSites() const noexcept { return target_.numSites(); } + +size_t MappingTarget::maxDegree() const noexcept { return target_.maxDegree(); } + +size_t MappingTarget::distanceBetween(size_t source, size_t target) const { + return target_.distanceBetween(source, target); +} + +void MappingTarget::forEachNeighbour( + size_t vertex, llvm::function_ref callback) const { + target_.forEachNeighbour(vertex, callback); +} + +float MappingTarget::pathCostBetween(size_t source, size_t target) const { + if (source == target) { + return 0.F; + } + const auto distance = target_.distanceBetween(source, target); + if (distance > 1) { + return static_cast(distance - 1); + } + + const CompilerTarget::Coupling coupling{target_.siteForVertex(source), + target_.siteForVertex(target)}; + return penalizedDirections_.contains(coupling) ? 1.F : 0.F; +} + +bool MappingTarget::isExecutable(size_t source, size_t target) const { + return source != target && pathCostBetween(source, target) == 0.F; +} + +} // namespace mlir diff --git a/mlir/lib/Compiler/QDMIAdapter.cpp b/mlir/lib/Compiler/QDMIAdapter.cpp index 5171b524ec..c506e1fbf4 100644 --- a/mlir/lib/Compiler/QDMIAdapter.cpp +++ b/mlir/lib/Compiler/QDMIAdapter.cpp @@ -273,13 +273,27 @@ snapshotDurationUnit(const qdmi::Device& device) { return std::optional(std::move(*durationUnit)); } -[[nodiscard]] static llvm::Expected> -snapshotSiteTuples(const qdmi::Operation& operation, size_t arity, - const std::vector& flattenedSites, - std::optional defaultDuration, - std::optional defaultFidelity) { - std::vector siteTuples; - siteTuples.reserve(flattenedSites.size() / arity); +namespace { + +struct OperationSiteSnapshot { + std::vector calibration; + std::optional>> applicability; +}; + +} // namespace + +[[nodiscard]] static llvm::Expected +snapshotOperationSites(const qdmi::Operation& operation, size_t arity, + const std::vector& flattenedSites, + std::optional defaultDuration, + std::optional defaultFidelity, + bool preserveApplicability) { + OperationSiteSnapshot result; + result.calibration.reserve(flattenedSites.size() / arity); + if (preserveApplicability) { + result.applicability.emplace(); + result.applicability->reserve(flattenedSites.size() / arity); + } for (size_t offset = 0; offset < flattenedSites.size(); offset += arity) { std::vector sites; std::vector siteIds; @@ -297,37 +311,23 @@ snapshotSiteTuples(const qdmi::Operation& operation, size_t arity, const auto duration = operation.getDuration(sites); const auto fidelity = operation.getFidelity(sites); - if (duration != defaultDuration || fidelity != defaultFidelity) { + const bool hasSiteCalibration = + duration != defaultDuration || fidelity != defaultFidelity; + if (hasSiteCalibration) { + if (result.applicability) { + result.applicability->emplace_back(siteIds); + } auto siteTuple = CompilerTarget::SiteTuple::create(std::move(siteIds), duration, fidelity); if (!siteTuple) { return siteTuple.takeError(); } - siteTuples.emplace_back(std::move(*siteTuple)); - } - } - return siteTuples; -} - -[[nodiscard]] static llvm::Expected< - std::vector>> -snapshotApplicableSiteTuples(size_t arity, - const std::vector& flattenedSites) { - std::vector> applicableSiteTuples; - applicableSiteTuples.reserve(flattenedSites.size() / arity); - for (size_t offset = 0; offset < flattenedSites.size(); offset += arity) { - std::vector siteIds; - siteIds.reserve(arity); - for (size_t index = 0; index < arity; ++index) { - auto siteId = checkedSiteId(flattenedSites[offset + index].getIndex()); - if (!siteId) { - return siteId.takeError(); - } - siteIds.emplace_back(*siteId); + result.calibration.emplace_back(std::move(*siteTuple)); + } else if (result.applicability) { + result.applicability->emplace_back(std::move(siteIds)); } - applicableSiteTuples.emplace_back(std::move(siteIds)); } - return applicableSiteTuples; + return result; } [[nodiscard]] static llvm::Expected @@ -383,19 +383,14 @@ snapshotOperations( deviceSites, couplings, deviceName)) { return error; } - auto tuples = snapshotSiteTuples(operation, *arity, *flattenedSites, - duration, fidelity); + auto tuples = + snapshotOperationSites(operation, *arity, *flattenedSites, duration, + fidelity, !hasArbitraryPositiveControls); if (!tuples) { return tuples.takeError(); } - siteTuples = std::move(*tuples); - if (!hasArbitraryPositiveControls) { - auto applicable = snapshotApplicableSiteTuples(*arity, *flattenedSites); - if (!applicable) { - return applicable.takeError(); - } - applicableSiteTuples = std::move(*applicable); - } + siteTuples = std::move(tuples->calibration); + applicableSiteTuples = std::move(tuples->applicability); } if (auto error = requireRepresentableOperation( !hasArbitraryPositiveControls || siteTuples.empty(), deviceName, diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index 5289e1e21a..69875fb82c 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -381,6 +381,14 @@ llvm::Expected CompilerTarget::Operation::create( } uniqueApplicableSiteCombinations.emplace_back(sites); } + if (llvm::any_of(siteTuples, [&](const auto& siteTuple) { + return llvm::none_of(*applicableSiteTuples, [&](const auto& sites) { + return ArrayRef(sites) == siteTuple.sites(); + }); + })) { + return invalidTarget("Compiler target operation calibration references " + "an inapplicable site tuple"); + } } return Operation(std::move(name), std::move(canonicalName), arity, numParameters, std::move(siteTuples), duration, fidelity, @@ -481,22 +489,14 @@ struct CompilerTarget::Storage { [[nodiscard]] llvm::Error initialize(); - [[nodiscard]] bool isApplicable(size_t operationIndex, size_t arity) const; - [[nodiscard]] bool isApplicable(size_t operationIndex, - ArrayRef orderedSites) const; - [[nodiscard]] bool - supportsOperation(StringRef name, size_t arity, - std::optional numParameters) const; - [[nodiscard]] bool supportsOperation(StringRef name, size_t arity, - std::optional numParameters, - ArrayRef orderedSites) const; [[nodiscard]] bool - supportsVariadicOperation(StringRef name, size_t arity, - std::optional numParameters) const; + isApplicable(size_t operationIndex, size_t arity, + std::optional> orderedSites) const; [[nodiscard]] bool - supportsVariadicOperation(StringRef name, size_t arity, - std::optional numParameters, - ArrayRef orderedSites) const; + supportsOperation(StringRef name, size_t arity, + std::optional numParameters, + std::optional> orderedSites = std::nullopt, + bool variadicOnly = false) const; [[nodiscard]] bool supportsGate(GateKind gate, ArrayRef orderedSites) const; [[nodiscard]] std::optional resolveSynthesisBasis() const; @@ -514,7 +514,7 @@ struct CompilerTarget::Storage { NativeOperations::Kind nativeOperationsKind; SmallVector operations; llvm::StringMap> capabilities; - std::vector>> explicitOneQubitSites; + std::vector>> explicitOneQubitSites; std::vector>> explicitTwoQubitSites; SmallVector supportedGates; std::optional basis; @@ -690,11 +690,13 @@ llvm::Error CompilerTarget::Storage::initialize() { for (const auto& specification : GATE_SPECIFICATIONS) { const bool supportsControlledBase = (specification.kind == GateKind::CX && - supportsVariadicOperation("x", specification.arity, - specification.numParameters)) || + supportsOperation("x", specification.arity, + specification.numParameters, std::nullopt, + /*variadicOnly=*/true)) || (specification.kind == GateKind::CZ && - supportsVariadicOperation("z", specification.arity, - specification.numParameters)); + supportsOperation("z", specification.arity, + specification.numParameters, std::nullopt, + /*variadicOnly=*/true)); if (supportsControlledBase || supportsOperation(specification.name, specification.arity, specification.numParameters)) { @@ -705,129 +707,52 @@ llvm::Error CompilerTarget::Storage::initialize() { return llvm::Error::success(); } -bool CompilerTarget::Storage::isApplicable(size_t operationIndex, - size_t arity) const { +bool CompilerTarget::Storage::isApplicable( + size_t operationIndex, size_t arity, + std::optional> orderedSites) const { const auto& operation = operations[operationIndex]; if (!operation.hasExplicitApplicability()) { return true; } + if (!orderedSites) { + if (arity == 1) { + return !explicitOneQubitSites[operationIndex]->empty(); + } + if (arity == 2) { + return !explicitTwoQubitSites[operationIndex]->empty(); + } + return llvm::any_of(operation.applicableSiteTuples(), + [&](const auto& applicableSites) { + return applicableSites.size() == arity; + }); + } if (arity == 1) { - return !explicitOneQubitSites[operationIndex]->empty(); + return explicitOneQubitSites[operationIndex]->contains((*orderedSites)[0]); } if (arity == 2) { - return !explicitTwoQubitSites[operationIndex]->empty(); - } - return llvm::any_of(operation.applicableSiteTuples(), - [&](const auto& applicableSites) { - return applicableSites.size() == arity; - }); -} - -bool CompilerTarget::Storage::isApplicable( - size_t operationIndex, ArrayRef orderedSites) const { - const auto& operation = operations[operationIndex]; - if (!operation.hasExplicitApplicability()) { - return true; - } - if (orderedSites.size() == 1) { - return explicitOneQubitSites[operationIndex]->contains(orderedSites[0]); - } - if (orderedSites.size() == 2) { return explicitTwoQubitSites[operationIndex]->contains( - {orderedSites[0], orderedSites[1]}); + {(*orderedSites)[0], (*orderedSites)[1]}); } return llvm::any_of( operation.applicableSiteTuples(), [&](const auto& applicableSites) { - return ArrayRef(applicableSites) == orderedSites; + return ArrayRef(applicableSites) == *orderedSites; }); } -bool CompilerTarget::Storage::supportsOperation( - StringRef operationName, size_t arity, - std::optional numParameters) const { - const auto canonical = canonicalOperationName(operationName); - if (canonical.empty() || arity > sites.size()) { - return false; - } - if (nativeOperationsKind == NativeOperations::Kind::Unrestricted) { - return true; - } - const auto found = capabilities.find(canonical); - if (found == capabilities.end()) { - return false; - } - return llvm::any_of(found->second, [&](const auto index) { - const auto& operation = operations[index]; - return operation.arity().accepts(arity) && - (!numParameters || operation.numParameters() == *numParameters) && - isApplicable(index, arity); - }); -} - bool CompilerTarget::Storage::supportsOperation( StringRef operationName, size_t arity, std::optional numParameters, - ArrayRef orderedSites) const { + std::optional> orderedSites, bool variadicOnly) const { const auto canonical = canonicalOperationName(operationName); if (canonical.empty() || arity > sites.size() || - orderedSites.size() != arity) { - return false; - } - for (const auto [index, site] : llvm::enumerate(orderedSites)) { - if (!siteToVertex.contains(site) || - llvm::is_contained(orderedSites.take_front(index), site)) { - return false; - } - } - if (nativeOperationsKind == NativeOperations::Kind::Unrestricted) { - return true; - } - const auto found = capabilities.find(canonical); - if (found == capabilities.end()) { - return false; - } - return llvm::any_of(found->second, [&](const auto index) { - const auto& operation = operations[index]; - return operation.arity().accepts(arity) && - (!numParameters || operation.numParameters() == *numParameters) && - isApplicable(index, orderedSites); - }); -} - -bool CompilerTarget::Storage::supportsVariadicOperation( - StringRef operationName, size_t arity, - std::optional numParameters) const { - const auto canonical = canonicalOperationName(operationName); - if (canonical.empty() || arity > sites.size()) { + (orderedSites && orderedSites->size() != arity)) { return false; } - if (nativeOperationsKind == NativeOperations::Kind::Unrestricted) { - return true; - } - const auto found = capabilities.find(canonical); - if (found == capabilities.end()) { - return false; - } - return llvm::any_of(found->second, [&](const auto index) { - const auto& operation = operations[index]; - return operation.arity().kind() == Operation::Arity::Kind::Variadic && - operation.arity().accepts(arity) && - (!numParameters || operation.numParameters() == *numParameters) && - isApplicable(index, arity); - }); -} - -bool CompilerTarget::Storage::supportsVariadicOperation( - StringRef operationName, size_t arity, std::optional numParameters, - ArrayRef orderedSites) const { - const auto canonical = canonicalOperationName(operationName); - if (canonical.empty() || arity > sites.size() || - orderedSites.size() != arity) { - return false; - } - for (const auto [index, site] : llvm::enumerate(orderedSites)) { - if (!siteToVertex.contains(site) || - llvm::is_contained(orderedSites.take_front(index), site)) { - return false; + if (orderedSites) { + for (const auto [index, site] : llvm::enumerate(*orderedSites)) { + if (!siteToVertex.contains(site) || + llvm::is_contained(orderedSites->take_front(index), site)) { + return false; + } } } if (nativeOperationsKind == NativeOperations::Kind::Unrestricted) { @@ -839,22 +764,23 @@ bool CompilerTarget::Storage::supportsVariadicOperation( } return llvm::any_of(found->second, [&](const auto index) { const auto& operation = operations[index]; - return operation.arity().kind() == Operation::Arity::Kind::Variadic && + return (!variadicOnly || + operation.arity().kind() == Operation::Arity::Kind::Variadic) && operation.arity().accepts(arity) && (!numParameters || operation.numParameters() == *numParameters) && - isApplicable(index, orderedSites); + isApplicable(index, arity, orderedSites); }); } bool CompilerTarget::Storage::supportsGate( GateKind gate, ArrayRef orderedSites) const { if ((gate == GateKind::CX && - supportsVariadicOperation("x", 2, 0, orderedSites)) || + supportsOperation("x", 2, 0, orderedSites, /*variadicOnly=*/true)) || (gate == GateKind::CZ && - supportsVariadicOperation("z", 2, 0, orderedSites))) { + supportsOperation("z", 2, 0, orderedSites, /*variadicOnly=*/true))) { return true; } - const auto specification = + const decltype(GATE_SPECIFICATIONS.cbegin()) specification = std::ranges::find_if(GATE_SPECIFICATIONS, [&](const auto& candidate) { return candidate.kind == gate; }); @@ -866,6 +792,25 @@ bool CompilerTarget::Storage::supportsGate( std::optional CompilerTarget::Storage::resolveSynthesisBasis() const { + const auto supportsEveryPlacement = [&](StringRef operationName, size_t arity, + size_t numParameters, + bool variadicOnly = false) { + if (nativeOperationsKind == NativeOperations::Kind::Unrestricted) { + return true; + } + const auto found = capabilities.find(operationName); + if (found == capabilities.end()) { + return false; + } + return llvm::any_of(found->second, [&](const auto index) { + const auto& operation = operations[index]; + return (!variadicOnly || + operation.arity().kind() == Operation::Arity::Kind::Variadic) && + operation.arity().accepts(arity) && + operation.numParameters() == numParameters && + !operation.hasExplicitApplicability(); + }); + }; const auto supportsOnEverySite = [&](GateKind gate) { return llvm::all_of(siteIds, [&](SiteId site) { return supportsGate(gate, ArrayRef(&site, 1)); @@ -895,6 +840,20 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { if (sites.size() < 2) { return false; } + if ((gate == GateKind::CX && supportsEveryPlacement("x", 2, 0, true)) || + (gate == GateKind::CZ && supportsEveryPlacement("z", 2, 0, true))) { + return true; + } + const decltype(GATE_SPECIFICATIONS.cbegin()) specification = + std::ranges::find_if(GATE_SPECIFICATIONS, [&](const auto& candidate) { + return candidate.kind == gate; + }); + assert(specification != GATE_SPECIFICATIONS.end() && + "unknown compiler target gate"); + if (supportsEveryPlacement(specification->name, specification->arity, + specification->numParameters)) { + return true; + } const auto supportsPair = [&](SiteId source, SiteId target) { const std::array forward{source, target}; const std::array reverse{target, source}; @@ -919,7 +878,7 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { GateKind::RXX, GateKind::RYY, GateKind::RZX, GateKind::RZZ, GateKind::ISWAP, GateKind::CZ, GateKind::CX, GateKind::ECR, }; - const auto entangler = + const decltype(entanglerPreference.cbegin()) entangler = std::ranges::find_if(entanglerPreference, supportsOnEveryCoupling); if (!singleQubit || entangler == entanglerPreference.end()) { return std::nullopt; @@ -1069,6 +1028,9 @@ CompilerTarget::create(const mqt::CompilationTargetAttr attribute) { applicableSiteTuples->emplace_back(tupleAttr.getSites().begin(), tupleAttr.getSites().end()); } + } else if (!operationAttr.getApplicableSiteTuples().empty()) { + return invalidTarget("Compiler target applicable site tuples require " + "explicit operation applicability"); } std::optional fidelity; @@ -1230,54 +1192,16 @@ bool CompilerTarget::supportsOperation(StringRef operationName, size_t arity, } bool CompilerTarget::supports(::mlir::Operation* operation) const { - if (operation == nullptr) { - return false; - } - - if (auto unitary = dyn_cast(operation)) { - if (isa(operation)) { - return true; - } - if (auto controlled = dyn_cast(operation)) { - if (controlled.getNumControls() == 0 || - controlled.getNumBodyUnitaries() != 1) { - return false; - } - auto body = controlled.getBodyUnitary(0); - if (body.getNumQubits() != controlled.getNumTargets()) { - return false; - } - if (storage_->supportsVariadicOperation(body.getBaseSymbol(), - controlled.getNumQubits(), - body.getNumParams())) { - return true; - } - if (controlled.getNumControls() != 1 || controlled.getNumTargets() != 1) { - return false; - } - if (isa(body.getOperation())) { - return storage_->supportsOperation("cx", 2, 0); - } - if (isa(body.getOperation())) { - return storage_->supportsOperation("cz", 2, 0); - } - return false; - } - return storage_->supportsOperation(unitary.getBaseSymbol(), - unitary.getNumQubits(), - unitary.getNumParams()); - } - if (isa(operation)) { - return storage_->supportsOperation("measure", 1, 0); - } - if (isa(operation)) { - return storage_->supportsOperation("reset", 1, 0); - } - return false; + return supportsImpl(operation, std::nullopt); } bool CompilerTarget::supports(::mlir::Operation* operation, ArrayRef sites) const { + return supportsImpl(operation, sites); +} + +bool CompilerTarget::supportsImpl(::mlir::Operation* operation, + std::optional> sites) const { if (operation == nullptr) { return false; } @@ -1295,9 +1219,10 @@ bool CompilerTarget::supports(::mlir::Operation* operation, if (body.getNumQubits() != controlled.getNumTargets()) { return false; } - if (storage_->supportsVariadicOperation(body.getBaseSymbol(), - controlled.getNumQubits(), - body.getNumParams(), sites)) { + if (storage_->supportsOperation(body.getBaseSymbol(), + controlled.getNumQubits(), + body.getNumParams(), sites, + /*variadicOnly=*/true)) { return true; } if (controlled.getNumControls() != 1 || controlled.getNumTargets() != 1) { diff --git a/mlir/lib/Compiler/TargetCost.cpp b/mlir/lib/Compiler/TargetCost.cpp deleted file mode 100644 index 53c52d285e..0000000000 --- a/mlir/lib/Compiler/TargetCost.cpp +++ /dev/null @@ -1,149 +0,0 @@ -/* - * 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/Compiler/TargetCost.h" - -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace mlir { -namespace { - -struct GateSignature { - llvm::StringLiteral name; - size_t numParameters; -}; - -} // namespace - -[[nodiscard]] static GateSignature -gateSignature(const CompilerTarget::GateKind gate) { - using Gate = CompilerTarget::GateKind; - constexpr std::array signatures{ - std::pair{Gate::RXX, GateSignature{"rxx", 1}}, - std::pair{Gate::RYY, GateSignature{"ryy", 1}}, - std::pair{Gate::RZX, GateSignature{"rzx", 1}}, - std::pair{Gate::RZZ, GateSignature{"rzz", 1}}, - std::pair{Gate::ISWAP, GateSignature{"iswap", 0}}, - std::pair{Gate::CZ, GateSignature{"cz", 0}}, - std::pair{Gate::CX, GateSignature{"cx", 0}}, - std::pair{Gate::ECR, GateSignature{"ecr", 0}}, - }; - const llvm::ArrayRef> signatureList{ - signatures}; - const auto* const signature = - llvm::find_if(signatureList, [gate](const auto& candidate) { - return candidate.first == gate; - }); - assert(signature != signatureList.end() && - "routing costs require a two-qubit gate"); - return signature->second; -} - -TargetGateCosts::TargetGateCosts(const CompilerTarget& target, - const CompilerTarget::GateKind gate) - : target_(target) { - constexpr auto unavailable = std::numeric_limits::infinity(); - if (!target_.hasExplicitTopology()) { - if (!target_.hasExplicitOperations()) { - return; - } - - const auto [name, numParameters] = gateSignature(gate); - defaultAdjacentCost_ = unavailable; - uniform_ = false; - for (const auto& operation : target_.operations()) { - if (operation.canonicalName() != name || operation.numQubits() != 2 || - operation.numParameters() != numParameters) { - continue; - } - if (!operation.hasExplicitSiteTuples()) { - defaultAdjacentCost_ = 0.F; - costs_.clear(); - uniform_ = true; - return; - } - for (const auto& tuple : operation.siteTuples()) { - assert(tuple.sites().size() == 2 && - "two-qubit gate must have two-site tuples"); - const auto source = tuple.sites()[0]; - const auto target = tuple.sites()[1]; - costs_.insert_or_assign({source, target}, 0.F); - const std::array reverseSites{target, source}; - costs_.insert_or_assign( - {target, source}, target_.supports(gate, reverseSites) ? 0.F : 1.F); - } - } - const auto numQubits = target_.numQubits(); - if (costs_.size() == numQubits * (numQubits - 1) && - llvm::all_of(costs_, - [](const auto& cost) { return cost.second == 0.F; })) { - defaultAdjacentCost_ = 0.F; - costs_.clear(); - uniform_ = true; - } - return; - } - - for (size_t source = 0; source < target_.numQubits(); ++source) { - target_.forEachNeighbour(source, [&](const size_t target) { - if (target < source) { - return; - } - - const auto sourceSite = target_.siteForVertex(source); - const auto targetSite = target_.siteForVertex(target); - const std::array forwardSites{sourceSite, targetSite}; - const std::array reverseSites{targetSite, sourceSite}; - const bool forward = target_.supports(gate, forwardSites); - const bool reverse = target_.supports(gate, reverseSites); - - if (!forward) { - costs_.try_emplace(CompilerTarget::Coupling{sourceSite, targetSite}, - reverse ? 1.F : unavailable); - } - if (!reverse) { - costs_.try_emplace(CompilerTarget::Coupling{targetSite, sourceSite}, - forward ? 1.F : unavailable); - } - }); - } - uniform_ = costs_.empty(); -} - -float TargetGateCosts::routingCostBetween(const size_t source, - const size_t target) const { - assert(source < target_.numQubits() && target < target_.numQubits() && - "compiler target vertex is out of range"); - if (source == target) { - return 0.F; - } - const auto distance = target_.distanceBetween(source, target); - if (distance > 1) { - return static_cast(distance - 1); - } - const CompilerTarget::Coupling coupling{target_.siteForVertex(source), - target_.siteForVertex(target)}; - if (const auto found = costs_.find(coupling); found != costs_.end()) { - return found->second; - } - return defaultAdjacentCost_; -} - -bool TargetGateCosts::isUniform() const noexcept { return uniform_; } - -} // namespace mlir diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index 037ee60f49..894d36ed1b 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include using namespace mlir; @@ -125,7 +126,7 @@ SiteTupleAttr::verify(const function_ref emitError, const ArrayRef sites, const std::optional /*duration*/, const FloatAttr fidelity) { - llvm::SmallDenseSet seen; + std::unordered_set seen; seen.reserve(sites.size()); for (const int64_t site : sites) { if (site < 0) { @@ -141,6 +142,24 @@ SiteTupleAttr::verify(const function_ref emitError, "compiler target site-tuple fidelity"); } +LogicalResult ApplicableSiteTupleAttr::verify( + const function_ref emitError, + ArrayRef sites) { + std::unordered_set seen; + seen.reserve(sites.size()); + for (int64_t site : sites) { + if (site < 0) { + return emitError() << "compiler target applicable site tuple contains a " + "negative site ID"; + } + if (!seen.insert(site).second) { + return emitError() << "compiler target applicable site tuple contains a " + "duplicate site"; + } + } + return success(); +} + LogicalResult OperationArityAttr::verify(const function_ref emitError, const OperationArityKind kind, @@ -156,7 +175,9 @@ LogicalResult NativeOperationAttr::verify( const function_ref emitError, const StringAttr name, const OperationArityAttr arity, const uint64_t /*numParameters*/, const ArrayRef siteTuples, - const std::optional /*duration*/, const FloatAttr fidelity) { + const std::optional /*duration*/, const FloatAttr fidelity, + OperationApplicabilityKind applicability, + ArrayRef applicableSiteTuples) { if (name.getValue().trim().empty()) { return emitError() << "compiler target operation name must not be empty"; } @@ -187,6 +208,41 @@ LogicalResult NativeOperationAttr::verify( } seen.emplace_back(siteTuple.getSites()); } + + if (applicability != OperationApplicabilityKind::Explicit && + !applicableSiteTuples.empty()) { + return emitError() << "compiler target applicable site tuples require " + "explicit operation applicability"; + } + + SmallVector> seenApplicable; + seenApplicable.reserve(applicableSiteTuples.size()); + for (ApplicableSiteTupleAttr siteTuple : applicableSiteTuples) { + const auto numSites = siteTuple.getSites().size(); + const bool acceptsArity = arity.getKind() == OperationArityKind::Variadic + ? numSites >= arity.getValue() + : numSites == arity.getValue(); + if (!acceptsArity) { + return emitError() << "compiler target operation applicable site tuple " + "does not match its arity"; + } + if (llvm::is_contained(seenApplicable, siteTuple.getSites())) { + return emitError() << "compiler target operation contains a duplicate " + "applicable site tuple"; + } + seenApplicable.emplace_back(siteTuple.getSites()); + } + if (applicability == OperationApplicabilityKind::Explicit && + llvm::any_of(siteTuples, [&](const SiteTupleAttr siteTuple) { + return llvm::none_of(applicableSiteTuples, + [&](const ApplicableSiteTupleAttr applicable) { + return applicable.getSites() == + siteTuple.getSites(); + }); + })) { + return emitError() << "compiler target operation calibration references " + "an inapplicable site tuple"; + } return success(); } @@ -203,7 +259,7 @@ LogicalResult CompilationTargetAttr::verify( return emitError() << "compiler target must contain at least one site"; } - llvm::SmallDenseSet siteIds; + std::unordered_set siteIds; siteIds.reserve(sites.size()); for (const SiteAttr site : sites) { if (!siteIds.insert(site.getId()).second) { @@ -255,6 +311,16 @@ LogicalResult CompilationTargetAttr::verify( "an unknown site"; } } + for (ApplicableSiteTupleAttr siteTuple : + operation.getApplicableSiteTuples()) { + if (llvm::any_of(siteTuple.getSites(), [&](const int64_t site) { + return !siteIds.contains(site); + })) { + return emitError() + << "compiler target operation applicable site tuple references " + "an unknown site"; + } + } } const bool hasTiming = diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index f0db76db07..e7e7a189d6 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -10,8 +10,7 @@ #include "mlir/Dialect/QCO/Transforms/Mapping/Mapping.h" -#include "mlir/Compiler/Target.h" -#include "mlir/Compiler/TargetCost.h" +#include "mlir/Compiler/MappingTarget.h" #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" @@ -53,7 +52,6 @@ #include #include #include -#include #include #include #include @@ -440,25 +438,20 @@ struct MappingPass : impl::MappingPassBase { /// Construct a non-root node from its parent node. Apply the given swap to /// the layout of the parent node. Node(Node* parent, const IndexPairType& swap, const Window& window, - const CompilerTarget& target, const TargetGateCosts* gateCosts, - const Parameters& params) + const MappingTarget& target, const Parameters& params) : layout(parent->layout), swap(swap), parent(parent), depth(parent->depth + 1), f(0) { layout.swap(swap.first, swap.second); - f = g(params.alpha) + h(window, target, gateCosts, params); // NOLINT + f = g(params.alpha) + h(window, target, params); // NOLINT } /// Return true, if the current SWAP sequence makes all gates in the front /// executable. [[nodiscard]] bool isGoal(const IndexPairType& front, - const CompilerTarget& target, - const TargetGateCosts* gateCosts) const { + const MappingTarget& target) const { const auto [hw0, hw1] = layout.getHardwareIndices(front.first, front.second); - if (gateCosts != nullptr) { - return gateCosts->routingCostBetween(hw0, hw1) == 0.F; - } - return target.areAdjacent(hw0, hw1); + return target.isExecutable(hw0, hw1); } private: @@ -475,19 +468,14 @@ struct MappingPass : impl::MappingPassBase { /// 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(const Window& window, const CompilerTarget& target, - const TargetGateCosts* gateCosts, + [[nodiscard]] float h(const Window& window, const MappingTarget& target, const Parameters& params) const { float costs{0}; float decay{1.}; for (const auto& [prog0, prog1] : window) { const auto [hw0, hw1] = layout.getHardwareIndices(prog0, prog1); - const auto routingCost = - gateCosts != nullptr - ? gateCosts->routingCostBetween(hw0, hw1) - : static_cast(target.distanceBetween(hw0, hw1) - 1); - costs += decay * routingCost; + costs += decay * target.pathCostBetween(hw0, hw1); decay *= params.lambda; } return costs; @@ -496,7 +484,7 @@ struct MappingPass : impl::MappingPassBase { /// Describes the graph F of arXiv:1602.05150v3. struct FGraph { - explicit FGraph(const CompilerTarget& target) + explicit FGraph(const MappingTarget& target) : f_(llvm::to_vector(llvm::seq(target.numSites()))), target_(&target) {}; @@ -563,7 +551,7 @@ struct MappingPass : impl::MappingPassBase { } Graph f_; - const CompilerTarget* target_; + const MappingTarget* target_; }; public: @@ -577,14 +565,7 @@ struct MappingPass : impl::MappingPassBase { /// Construct mapping for a compiler target. explicit MappingPass(const CompilerTarget& compilerTarget, const MappingPassOptions& options) - : MappingPassBase(options), target(compilerTarget) { - if (const auto basis = compilerTarget.synthesisBasis()) { - gateCosts.emplace(compilerTarget, basis->entangler); - if (gateCosts->isUniform()) { - gateCosts.reset(); - } - } - } + : MappingPassBase(options), target(compilerTarget) {} protected: void runOnOperation() override { @@ -597,7 +578,7 @@ struct MappingPass : impl::MappingPassBase { } auto moduleOp = getOperation(); - if (target->connectivityKind() != + if (target->compilerTarget().connectivityKind() != CompilerTarget::Connectivity::Kind::Explicit) { moduleOp.emitError() << "place-and-route requires an explicit target topology"; @@ -619,7 +600,7 @@ struct MappingPass : impl::MappingPassBase { auto computation = discoverComputation(func); if (failed(computation) || - failed(checkCapacity(func, *target, *computation))) { + failed(checkCapacity(func, target->compilerTarget(), *computation))) { signalPassFailure(); return; } @@ -635,8 +616,8 @@ struct MappingPass : impl::MappingPassBase { } IRRewriter rewriter(&getContext()); - std::tie(wires, infos) = std::move( - applyPlacement(body, *target, *layout, *computation, rewriter)); + std::tie(wires, infos) = std::move(applyPlacement( + body, target->compilerTarget(), *layout, *computation, rewriter)); RoutingBundle bundle{.wires = std::move(wires), .infos = std::move(infos), @@ -940,8 +921,7 @@ struct MappingPass : impl::MappingPassBase { // If the currently visited node is a goal node, reconstruct the // sequence of SWAPs from this node to the root. - if (curr->isGoal(window.front(), *target, - gateCosts ? &*gateCosts : nullptr)) { + if (curr->isGoal(window.front(), *target)) { SmallVector seq(curr->depth); size_t j = seq.size() - 1; for (const Node* n = curr; n->parent != nullptr; n = n->parent) { @@ -959,9 +939,6 @@ struct MappingPass : impl::MappingPassBase { for (const auto& [q0, q1] = window.front(); const auto prog : {q0, q1}) { const auto hw0 = curr->layout.getHardwareIndex(prog); target->forEachNeighbour(hw0, [&](const auto hw1) { - if (!canSwap(hw0, hw1)) { - return; - } // Ensure consistent hashing/comparison. const IndexPairType swap = std::minmax(hw0, hw1); if (is_contained(expansionSet, swap)) { @@ -969,9 +946,8 @@ struct MappingPass : impl::MappingPassBase { } expansionSet.push_back(swap); - frontier.emplace( - std::construct_at(arena.Allocate(), curr, swap, window, *target, - gateCosts ? &*gateCosts : nullptr, params)); + frontier.emplace(std::construct_at(arena.Allocate(), curr, swap, + window, *target, params)); }); } @@ -1105,20 +1081,22 @@ struct MappingPass : impl::MappingPassBase { return curr; } - [[nodiscard]] static IndexPairType - orderedPrograms(UnitaryOpInterface unitary, const ArrayRef indices, - const Wires& wires, const WireInfos& infos) { + [[nodiscard]] static IndexPairType orderedPrograms(UnitaryOpInterface unitary, + ArrayRef indices, + const Wires& wires, + const WireInfos& infos) { assert(unitary.getNumQubits() == 2 && indices.size() == 2 && "expected a ready two-qubit operation"); - const auto programForOutput = [&](Value output) { - const auto* const found = llvm::find_if(indices, [&](const size_t index) { - return wires[index].qubit() == output; - }); - assert(found != indices.end() && "operation result has no ready wire"); - return infos.lookupProgram(*found); - }; - return {programForOutput(unitary.getOutputQubit(0)), - programForOutput(unitary.getOutputQubit(1))}; + const bool reversed = + wires[indices.front()].qubit() == unitary.getOutputQubit(1); + assert(wires[indices.front()].qubit() == + unitary.getOutputQubit(reversed ? 1 : 0) && + wires[indices.back()].qubit() == + unitary.getOutputQubit(reversed ? 0 : 1) && + "ready wires do not match operation results"); + const IndexPairType programs{infos.lookupProgram(indices.front()), + infos.lookupProgram(indices.back())}; + return reversed ? IndexPairType{programs.second, programs.first} : programs; } /// Collect a routing lookahead window of up to `1 + nlookahead` ready @@ -1176,20 +1154,9 @@ struct MappingPass : impl::MappingPassBase { /// Insert SWAP operations, exchanging two qubits, virtually /// (`RoutingMode::Cold`) or into the IR (`RoutingMode::Hot`). The function /// expects that each wire points at the correct insertion point. - [[nodiscard]] bool canSwap(const size_t hw0, const size_t hw1) const { - return !gateCosts || std::isfinite(gateCosts->routingCostBetween(hw0, hw1)); - } - template - LogicalResult insertSWAPs(ArrayRef swaps, - RoutingBundle& bundle, Statistics& stats, - IRRewriter* rewriter) { - if (llvm::any_of(swaps, [&](const auto& swap) { - return !canSwap(swap.first, swap.second); - })) { - return failure(); - } - + static void insertSWAPs(ArrayRef swaps, RoutingBundle& bundle, + Statistics& stats, IRRewriter* rewriter) { auto& [wires, infos, layout] = bundle; for (const auto& [hw0, hw1] : swaps) { const auto [prog0, prog1] = layout.getProgramIndices(hw0, hw1); @@ -1225,7 +1192,6 @@ struct MappingPass : impl::MappingPassBase { } stats.nswaps += swaps.size(); - return success(); } /// Advance past all executable gates and return operations with nested @@ -1259,10 +1225,7 @@ struct MappingPass : impl::MappingPassBase { orderedPrograms(unitary, indices, wires, infos); const auto [hw0, hw1] = layout.getHardwareIndices(prog0, prog1); - return gateCosts - ? gateCosts->routingCostBetween(hw0, hw1) == - 0.F - : target->areAdjacent(hw0, hw1); + return target->isExecutable(hw0, hw1); }) .template Case([](auto&) { return true; }) .template Case([](MeasureOp& m) { @@ -1602,19 +1565,16 @@ struct MappingPass : impl::MappingPassBase { // using the restore (scf::ForOp, scf::While), converge (IfOp), and vote // and restore (IndexSwitchOp) strategies. - bool swapInsertionFailed = false; Layout exit = TypeSwitch(op) .Case([&](scf::ForOp) { const auto swaps = restore(children[0].layout, parent.layout); - swapInsertionFailed = failed( - insertSWAPs(swaps, children[0], totalStats, rewriter)); + insertSWAPs(swaps, children[0], totalStats, rewriter); return parent.layout; }) .template Case([&](scf::WhileOp) { const auto swaps = restore(children[1].layout, parent.layout); - swapInsertionFailed = failed( - insertSWAPs(swaps, children[1], totalStats, rewriter)); + insertSWAPs(swaps, children[1], totalStats, rewriter); // The scf::YieldOp is the terminator in the before region and // thus determines the final output layout. return children[0].layout; @@ -1622,11 +1582,8 @@ struct MappingPass : impl::MappingPassBase { .template Case([&](IfOp) { const auto [convergedLayout, fst, snd] = converge(children[0].layout, children[1].layout); - swapInsertionFailed = - failed(insertSWAPs(fst, children[0], totalStats, - rewriter)) || - failed(insertSWAPs(snd, children[1], totalStats, - rewriter)); + insertSWAPs(fst, children[0], totalStats, rewriter); + insertSWAPs(snd, children[1], totalStats, rewriter); return convergedLayout; }) .template Case([&](IndexSwitchOp) { @@ -1636,19 +1593,11 @@ struct MappingPass : impl::MappingPassBase { })); for (RoutingBundle& child : children) { const auto swaps = restore(child.layout, compromise); - if (failed(insertSWAPs(swaps, child, totalStats, - rewriter))) { - swapInsertionFailed = true; - break; - } + insertSWAPs(swaps, child, totalStats, rewriter); } return compromise; }); - if (swapInsertionFailed) { - return failure(); - } - if constexpr (Mode == RoutingMode::Hot) { // Realign terminator values to ensure that i-th input qubit and the // i-th output qubit represent the equivalent hardware qubit. This is @@ -1772,9 +1721,7 @@ struct MappingPass : impl::MappingPassBase { for_each(wires, [](auto& it) { std::ranges::advance(it, -1); }); } - if (failed(insertSWAPs(*swaps, bundle, stats, rewriter))) { - return failure(); - } + insertSWAPs(*swaps, bundle, stats, rewriter); if constexpr (Mode == RoutingMode::Hot) { @@ -1792,8 +1739,7 @@ struct MappingPass : impl::MappingPassBase { return stats; } - std::optional target; - std::optional gateCosts; + std::optional target; }; } // namespace diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 69d5a0c2d9..7a7b68e2e7 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -22,7 +22,6 @@ #include #include -#include #include #include // IWYU pragma: keep (Passes.h.inc) #include @@ -30,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -276,7 +276,7 @@ static void eraseFusableRun(RewriterBase& rewriter, /// its two-qubit operation count. static bool fuseTwoQubitGateRun(IRRewriter& rewriter, UnitaryOpInterface head, const Matrix4x4& headMatrix, - const CompilerTarget::SynthesisBasis basis) { + CompilerTarget::SynthesisBasis basis) { FusableTwoQubitRun run = scanFusableTwoQubitRun(head, headMatrix); if (run.ops.size() < 2) { return false; @@ -303,7 +303,7 @@ static bool fuseTwoQubitGateRun(IRRewriter& rewriter, UnitaryOpInterface head, namespace { using SiteId = CompilerTarget::SiteId; -using SiteMap = DenseMap; +using SiteMap = DenseMap>; } // namespace @@ -312,69 +312,94 @@ static SmallVector getQubitValues(ValueRange values) { values, [](Value value) { return isa(value.getType()); })); } -static void propagateSites(ValueRange inputs, ValueRange outputs, +static bool joinSite(Value value, std::optional site, SiteMap& sites) { + const auto [position, inserted] = sites.try_emplace(value, site); + if (inserted || !position->second) { + return inserted; + } + if (!site || *position->second != *site) { + position->second.reset(); + return true; + } + return false; +} + +static bool propagateSites(ValueRange inputs, ValueRange outputs, SiteMap& sites) { + bool changed = false; const auto inputQubits = getQubitValues(inputs); const auto outputQubits = getQubitValues(outputs); for (const auto [input, output] : llvm::zip_equal(inputQubits, outputQubits)) { if (const auto found = sites.find(input); found != sites.end()) { - const SiteId site = found->second; - sites.try_emplace(output, site); + changed |= joinSite(output, found->second, sites); } } + return changed; } -static ValueRange structuredInputs(Operation* operation) { - return TypeSwitch(operation) - .Case([](IfOp op) { return op.getQubits(); }) - .Case([](IndexSwitchOp op) { return op.getTargets(); }) - .Case([](scf::ForOp op) { return op.getInits(); }) - .Case([](scf::WhileOp op) { return op.getInits(); }) - .Default([](Operation*) -> ValueRange { return {}; }); +static bool propagateBranchSites(ValueRange inputs, + MutableArrayRef regions, + ValueRange results, SiteMap& sites) { + bool changed = false; + for (Region& region : regions) { + changed |= propagateSites(inputs, region.getArguments(), sites); + changed |= propagateSites(region.front().getTerminator()->getOperands(), + results, sites); + } + return changed; } -static void collectStaticSites(Region& region, SiteMap& sites) { - for (Operation& operation : region.getOps()) { - if (auto staticOp = dyn_cast(operation)) { - sites.try_emplace(staticOp.getQubit(), staticOp.getIndex()); - continue; - } - if (auto unitary = dyn_cast(operation)) { - propagateSites(unitary.getInputQubits(), unitary.getOutputQubits(), - sites); - continue; - } - if (auto reset = dyn_cast(operation)) { - propagateSites(reset.getQubitIn(), reset.getQubitOut(), sites); - continue; - } - if (auto measure = dyn_cast(operation)) { - propagateSites(measure.getQubitIn(), measure.getQubitOut(), sites); - continue; - } - - if (isa(operation)) { - const auto inputs = structuredInputs(&operation); - for (Region& nested : operation.getRegions()) { - propagateSites(inputs, nested.getArguments(), sites); - collectStaticSites(nested, sites); - } - propagateSites(inputs, operation.getResults(), sites); - continue; - } +static bool propagateForSites(scf::ForOp op, SiteMap& sites) { + bool changed = propagateSites(op.getInits(), op.getRegionIterArgs(), sites); + auto yield = cast(op.getBody()->getTerminator()); + changed |= propagateSites(yield.getResults(), op.getRegionIterArgs(), sites); + changed |= propagateSites(op.getRegionIterArgs(), op.getResults(), sites); + return changed; +} - for (Region& nested : operation.getRegions()) { - collectStaticSites(nested, sites); - } - } +static bool propagateWhileSites(scf::WhileOp op, SiteMap& sites) { + bool changed = propagateSites(op.getInits(), op.getBeforeArguments(), sites); + auto afterYield = cast(op.getAfterBody()->getTerminator()); + changed |= + propagateSites(afterYield.getResults(), op.getBeforeArguments(), sites); + auto condition = cast(op.getBeforeBody()->getTerminator()); + changed |= propagateSites(condition.getArgs(), op.getAfterArguments(), sites); + changed |= propagateSites(condition.getArgs(), op.getResults(), sites); + return changed; } static SiteMap collectStaticSites(Operation* root) { SiteMap sites; - for (Region& region : root->getRegions()) { - collectStaticSites(region, sites); - } + bool changed = false; + do { + changed = false; + root->walk([&](Operation* operation) { + if (auto staticOp = dyn_cast(operation)) { + changed |= joinSite(staticOp.getQubit(), staticOp.getIndex(), sites); + } else if (auto unitary = dyn_cast(operation)) { + changed |= propagateSites(unitary.getInputQubits(), + unitary.getOutputQubits(), sites); + } else if (auto reset = dyn_cast(operation)) { + changed |= + propagateSites(reset.getQubitIn(), reset.getQubitOut(), sites); + } else if (auto measure = dyn_cast(operation)) { + changed |= + propagateSites(measure.getQubitIn(), measure.getQubitOut(), sites); + } else if (auto ifOp = dyn_cast(operation)) { + changed |= propagateBranchSites(ifOp.getQubits(), ifOp->getRegions(), + ifOp.getResults(), sites); + } else if (auto switchOp = dyn_cast(operation)) { + changed |= + propagateBranchSites(switchOp.getTargets(), switchOp->getRegions(), + switchOp.getResults(), sites); + } else if (auto forOp = dyn_cast(operation)) { + changed |= propagateForSites(forOp, sites); + } else if (auto whileOp = dyn_cast(operation)) { + changed |= propagateWhileSites(whileOp, sites); + } + }); + } while (changed); return sites; } @@ -398,16 +423,24 @@ getOperationSites(Operation* operation, const SiteMap& sites) { if (found == sites.end()) { return std::nullopt; } - result.emplace_back(found->second); + result.emplace_back(found->second.value_or(-1)); } return result; } static bool -requiresTargetSynthesis(Operation* operation, const CompilerTarget& target, +supportsAtPossibleSites(Operation* operation, const CompilerTarget& target, const std::optional>& sites) { - return sites ? !target.supports(operation, *sites) - : !target.supports(operation); + if (!sites) { + return target.supports(operation); + } + if (!llvm::is_contained(*sites, SiteId{-1})) { + return target.supports(operation, *sites); + } + return sites->size() == 1 && + llvm::all_of(target.siteIds(), [&](const SiteId site) { + return target.supports(operation, ArrayRef{site}); + }); } /// Normalize relative phase effects and discard only the unobservable global @@ -436,123 +469,118 @@ namespace { struct PlannedOperation { Operation* operation; - std::optional> sites; bool reverseEntangler = false; -}; - -struct SynthesisPlan { - Operation* firstNeed = nullptr; - Operation* matrixUnavailable = nullptr; - SmallVector operations; + bool reorderOperands = false; }; } // namespace -static SynthesisPlan planTargetSynthesis(Operation* root, - const CompilerTarget& target) { - SynthesisPlan plan; +static bool isOperandSwapInvariant(UnitaryOpInterface unitary) { + Operation* operation = unitary.getOperation(); + if (isa(operation)) { + return true; + } + auto controlled = dyn_cast(operation); + return controlled && controlled.getNumControls() == 1 && + controlled.getNumTargets() == 1 && + controlled.getNumBodyUnitaries() == 1 && + isa(controlled.getBodyUnitary(0).getOperation()); +} + +static FailureOr> planTargetSynthesis( + Operation* root, const CompilerTarget& target, + const std::optional& targetBasis) { + SmallVector plan; const auto sites = collectStaticSites(root); - root->walk([&](Operation* operation) { + const auto result = root->walk([&](Operation* operation) { auto unitary = dyn_cast(operation); if (!unitary || !isWalkableUnitaryShell(operation) || (unitary.getNumQubits() != 1 && unitary.getNumQubits() != 2)) { return WalkResult::advance(); } auto operationSites = getOperationSites(operation, sites); - if (!requiresTargetSynthesis(operation, target, operationSites)) { + if (supportsAtPossibleSites(operation, target, operationSites)) { return WalkResult::advance(); } - if (plan.firstNeed == nullptr) { - plan.firstNeed = operation; + const bool sitesKnown = + !operationSites || !llvm::is_contained(*operationSites, SiteId{-1}); + if (operationSites && unitary.isTwoQubit() && sitesKnown && + isOperandSwapInvariant(unitary)) { + const std::array reverseSites{(*operationSites)[1], (*operationSites)[0]}; + if (target.supports(operation, reverseSites)) { + plan.emplace_back( + PlannedOperation{.operation = operation, .reorderOperands = true}); + return WalkResult::advance(); + } } + bool matrixAvailable = false; if (unitary.isSingleQubit()) { Matrix2x2 matrix; - if (unitary.getUnitaryMatrix2x2(matrix) || - decomposition::canSynthesizeParameterizedUnitary1Q(operation)) { - plan.operations.emplace_back( - PlannedOperation{operation, std::move(operationSites)}); - return WalkResult::advance(); - } + matrixAvailable = + unitary.getUnitaryMatrix2x2(matrix) || + decomposition::canSynthesizeParameterizedUnitary1Q(operation); } else { Matrix4x4 matrix; - if (assignTwoQubitOpMatrix(operation, matrix)) { - plan.operations.emplace_back( - PlannedOperation{operation, std::move(operationSites)}); - return WalkResult::advance(); - } + matrixAvailable = assignTwoQubitOpMatrix(operation, matrix); } - plan.matrixUnavailable = operation; - return WalkResult::interrupt(); - }); - return plan; -} - -static bool -supportsSingleQubitBasisOnSite(const CompilerTarget& target, - const CompilerTarget::SingleQubitBasis basis, - const SiteId site) { - const std::array sites{site}; - using Gate = CompilerTarget::GateKind; - switch (basis) { - case CompilerTarget::SingleQubitBasis::U: - return target.supports(Gate::U, sites); - case CompilerTarget::SingleQubitBasis::ZSXX: - return target.supports(Gate::X, sites) && - target.supports(Gate::SX, sites) && target.supports(Gate::RZ, sites); - case CompilerTarget::SingleQubitBasis::R: - return target.supports(Gate::R, sites); - case CompilerTarget::SingleQubitBasis::XZX: - case CompilerTarget::SingleQubitBasis::ZXZ: - return target.supports(Gate::RX, sites) && target.supports(Gate::RZ, sites); - case CompilerTarget::SingleQubitBasis::XYX: - return target.supports(Gate::RX, sites) && target.supports(Gate::RY, sites); - case CompilerTarget::SingleQubitBasis::ZYZ: - return target.supports(Gate::RY, sites) && target.supports(Gate::RZ, sites); - } - llvm_unreachable("unknown single-qubit synthesis basis"); -} - -static LogicalResult -prepareSynthesisPlan(SynthesisPlan& plan, const CompilerTarget& target, - const CompilerTarget::SynthesisBasis basis) { - for (auto& action : plan.operations) { - if (!action.sites) { - continue; + if (!matrixAvailable) { + operation->emitError() + << "target-native synthesis cannot lower operation '" + << operation->getName() + << "': its unitary matrix is not available at compile time"; + return WalkResult::interrupt(); } - for (const SiteId site : *action.sites) { - if (!supportsSingleQubitBasisOnSite(target, basis.singleQubit, site)) { - action.operation->emitError() - << "target-native synthesis has no usable single-qubit basis on " - "site " - << site; - return failure(); - } + if (!targetBasis) { + operation->emitError() + << "target-native synthesis cannot lower operation '" + << operation->getName() + << "': the target has no usable synthesis basis"; + return WalkResult::interrupt(); } - - if (action.sites->size() == 1 || - target.supports(basis.entangler, *action.sites)) { - continue; + if (unitary.isTwoQubit() && !sitesKnown) { + operation->emitError() + << "no supported synthesis-basis placement is known for its " + "static sites"; + return WalkResult::interrupt(); } - assert(action.sites->size() == 2 && - "target synthesis only handles one- and two-qubit operations"); - const std::array reverseSites{(*action.sites)[1], (*action.sites)[0]}; - if (target.supports(basis.entangler, reverseSites)) { - action.reverseEntangler = true; - continue; + bool reverseEntangler = false; + if (operationSites && unitary.isTwoQubit() && + !target.supports(targetBasis->entangler, *operationSites)) { + const std::array reverseSites{(*operationSites)[1], (*operationSites)[0]}; + if (!target.supports(targetBasis->entangler, reverseSites)) { + operation->emitError() + << "no supported synthesis-basis placement is known for its " + "static sites"; + return WalkResult::interrupt(); + } + reverseEntangler = true; } - - action.operation->emitError() - << "target-native synthesis has no usable entangler on sites " - << (*action.sites)[0] << " and " << (*action.sites)[1]; + plan.emplace_back(PlannedOperation{operation, reverseEntangler}); + return WalkResult::advance(); + }); + if (result.wasInterrupted()) { return failure(); } - return success(); + return plan; +} + +static void reorderTwoQubitOperation(IRRewriter& rewriter, + UnitaryOpInterface unitary) { + IRMapping mapping; + mapping.map(unitary.getInputQubit(0), unitary.getInputQubit(1)); + mapping.map(unitary.getInputQubit(1), unitary.getInputQubit(0)); + rewriter.setInsertionPoint(unitary); + auto reordered = cast( + rewriter.clone(*unitary.getOperation(), mapping)); + rewriter.replaceOp( + unitary.getOperation(), + ValueRange{reordered.getOutputQubit(1), reordered.getOutputQubit(0)}); } static void lowerTargetOperation(IRRewriter& rewriter, UnitaryOpInterface op, - const CompilerTarget::SynthesisBasis basis, - const bool reverseEntangler) { + CompilerTarget::SynthesisBasis basis, + bool reverseEntangler) { Operation* const operation = op.getOperation(); rewriter.setInsertionPoint(operation); if (op.isSingleQubit()) { @@ -671,40 +699,43 @@ struct TargetNativeSynthesisPass final return; } ModuleOp moduleOp = getOperation(); - if (failed(prepareGlobalPhases(moduleOp, target))) { - signalPassFailure(); - return; - } - auto plan = planTargetSynthesis(moduleOp, target); - if (plan.firstNeed == nullptr) { - return; - } const auto targetBasis = target.synthesisBasis(); - if (!targetBasis) { - plan.firstNeed->emitError() - << "target-native synthesis cannot lower operation '" - << plan.firstNeed->getName() - << "': the target has no usable synthesis basis"; + bool hasGlobalPhases = false; + moduleOp.walk([&](GPhaseOp) { hasGlobalPhases = true; }); + + if (hasGlobalPhases) { + OwningOpRef normalized = cast(moduleOp->clone()); + if (failed(prepareGlobalPhases(*normalized, target))) { + signalPassFailure(); + return; + } + if (failed(planTargetSynthesis(*normalized, target, targetBasis))) { + signalPassFailure(); + return; + } + } + if (failed(prepareGlobalPhases(moduleOp, target))) { signalPassFailure(); return; } - if (plan.matrixUnavailable != nullptr) { - plan.matrixUnavailable->emitError() - << "target-native synthesis cannot lower operation '" - << plan.matrixUnavailable->getName() - << "': its unitary matrix is not available at compile time"; + auto plan = planTargetSynthesis(moduleOp, target, targetBasis); + if (failed(plan)) { signalPassFailure(); return; } - if (failed(prepareSynthesisPlan(plan, target, *targetBasis))) { - signalPassFailure(); + if (plan->empty()) { return; } IRRewriter rewriter(&getContext()); - for (const auto& action : plan.operations) { - lowerTargetOperation(rewriter, cast(action.operation), - *targetBasis, action.reverseEntangler); + for (const auto& action : *plan) { + auto unitary = cast(action.operation); + if (action.reorderOperands) { + reorderTwoQubitOperation(rewriter, unitary); + } else { + lowerTargetOperation(rewriter, unitary, *targetBasis, + action.reverseEntangler); + } } if (failed(prepareGlobalPhases(moduleOp, target))) { signalPassFailure(); @@ -768,8 +799,7 @@ struct VerifyTargetConformancePass final } const auto operationSites = getOperationSites(operation, sites); - if (operationSites ? target.supports(operation, *operationSites) - : target.supports(operation)) { + if (supportsAtPossibleSites(operation, target, operationSites)) { return WalkResult::advance(); } diff --git a/mlir/unittests/Compiler/CMakeLists.txt b/mlir/unittests/Compiler/CMakeLists.txt index 398f6489b3..63fbcac678 100644 --- a/mlir/unittests/Compiler/CMakeLists.txt +++ b/mlir/unittests/Compiler/CMakeLists.txt @@ -6,8 +6,9 @@ # # Licensed under the MIT License -add_executable(mqt-core-mlir-unittests-compiler - test_compiler_pipeline.cpp test_compiler_qdmi_adapter.cpp test_compiler_target.cpp) +add_executable( + mqt-core-mlir-unittests-compiler test_compiler_pipeline.cpp test_compiler_qdmi_adapter.cpp + test_compiler_target.cpp test_mapping_target.cpp) target_link_libraries( mqt-core-mlir-unittests-compiler diff --git a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp index d653fe5b85..f9376e76e1 100644 --- a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp +++ b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp @@ -21,8 +21,10 @@ #include #include +#include #include #include +#include using mlir::CompilerTarget; @@ -66,12 +68,16 @@ TEST(CompilerQDMIAdapterTest, SnapshotsIQMCalibrationAndLifetime) { EXPECT_EQ(cz.siteTuples().size(), 30); EXPECT_EQ(measure.siteTuples().size(), 20); for (const auto& operation : target.operations()) { + EXPECT_TRUE(operation.hasExplicitApplicability()); EXPECT_FALSE(operation.duration()); for (const auto& tuple : operation.siteTuples()) { EXPECT_FALSE(tuple.duration()); EXPECT_TRUE(tuple.fidelity()); } } + EXPECT_EQ(r.applicableSiteTuples().size(), 20); + EXPECT_EQ(cz.applicableSiteTuples().size(), 30); + EXPECT_EQ(measure.applicableSiteTuples().size(), 20); EXPECT_EQ(target.supportsOperation("r", 1, 2), true); EXPECT_EQ(target.supportsOperation("cz", 2, 0), true); @@ -108,12 +114,18 @@ TEST(CompilerQDMIAdapterTest, InfersDDSIMTargetFacts) { CompilerTarget::Operation::Arity::Kind::Variadic) << name.str(); EXPECT_EQ(operation.arity().value(), minimum) << name.str(); + EXPECT_FALSE(operation.hasExplicitApplicability()) << name.str(); EXPECT_TRUE( target.supportsOperation(name, minimum, operation.numParameters())) << name.str(); EXPECT_TRUE( target.supportsOperation(name, minimum + 4, operation.numParameters())) << name.str(); + std::vector sites(minimum + 4); + std::iota(sites.begin(), sites.end(), 0); + EXPECT_TRUE(target.supportsOperation(name, minimum + 4, + operation.numParameters(), sites)) + << name.str(); } EXPECT_TRUE(target.supportsOperation("gphase", 0, 1)); EXPECT_EQ(target.supportsOperation("h", 1, 0), true); @@ -157,6 +169,9 @@ TEST(CompilerQDMIAdapterTest, SnapshotsHomogeneousHigherArityOperation) { const auto target = llvm::cantFail(mlir::compilerTargetFromDevice(device)); EXPECT_TRUE(target.supportsOperation("ccnot", 3, 0)); + EXPECT_TRUE(target.supportsOperation("ccnot", 3, 0, {0, 1, 2})); + EXPECT_TRUE(target.supportsOperation("ccnot", 3, 0, {2, 1, 0})); + EXPECT_FALSE(target.supportsOperation("ccnot", 3, 0, {0, 1, 3})); } TEST(CompilerQDMIAdapterTest, PreservesOneWayDirectionalOperationSupport) { @@ -166,16 +181,17 @@ TEST(CompilerQDMIAdapterTest, PreservesOneWayDirectionalOperationSupport) { const auto device = qdmi::Session::openDevice("mqt.sc.default", overrides); const auto target = llvm::cantFail(mlir::compilerTargetFromDevice(device)); - ASSERT_EQ(target.couplings().size(), 1); + ASSERT_EQ(target.couplings().size(), 1U); const auto& cx = findOperation(target, "cx"); - EXPECT_TRUE(cx.hasExplicitSiteTuples()); - ASSERT_EQ(cx.siteTuples().size(), 1); - EXPECT_EQ(cx.siteTuples()[0].sites(), - (llvm::ArrayRef{0, 1})); - EXPECT_FALSE(cx.siteTuples()[0].duration()); - EXPECT_FALSE(cx.siteTuples()[0].fidelity()); - EXPECT_TRUE(target.supports(CompilerTarget::GateKind::CX, {0, 1})); - EXPECT_FALSE(target.supports(CompilerTarget::GateKind::CX, {1, 0})); + EXPECT_TRUE(cx.hasExplicitApplicability()); + ASSERT_EQ(cx.applicableSiteTuples().size(), 1U); + EXPECT_EQ(cx.applicableSiteTuples()[0], + (std::vector{0, 1})); + EXPECT_TRUE(cx.siteTuples().empty()); + EXPECT_TRUE(target.supportsOperation("cx", 2, 0, {0, 1})); + EXPECT_FALSE(target.supportsOperation("cx", 2, 0, {1, 0})); + ASSERT_TRUE(target.synthesisBasis()); + EXPECT_EQ(target.synthesisBasis()->entangler, CompilerTarget::GateKind::CX); } TEST(CompilerQDMIAdapterTest, @@ -188,7 +204,14 @@ TEST(CompilerQDMIAdapterTest, ASSERT_EQ(target.couplings().size(), 1); const auto& cx = findOperation(target, "cx"); - EXPECT_TRUE(cx.hasExplicitSiteTuples()); + EXPECT_TRUE(cx.hasExplicitApplicability()); + ASSERT_EQ(cx.applicableSiteTuples().size(), 2U); + EXPECT_EQ(cx.applicableSiteTuples()[0], + (std::vector{0, 1})); + EXPECT_EQ(cx.applicableSiteTuples()[1], + (std::vector{1, 0})); + EXPECT_TRUE(target.supportsOperation("cx", 2, 0, {0, 1})); + EXPECT_TRUE(target.supportsOperation("cx", 2, 0, {1, 0})); ASSERT_EQ(cx.siteTuples().size(), 2); EXPECT_EQ(cx.siteTuples()[0].sites(), (llvm::ArrayRef{0, 1})); diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index 618c6f3260..386a79e0c3 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -264,6 +264,27 @@ TEST(CompilerTargetTest, RejectsInvalidMetadata) { std::vector{valid(SiteTuple::create({0})), valid(SiteTuple::create({0}))}), "Compiler target operation contains a duplicate site tuple"); + expectInvalid( + Operation::create("x", 1, 0, {}, std::nullopt, std::nullopt, + std::vector>{{-1}}), + "Compiler target operation applicable site tuple contains a negative " + "site ID"); + expectInvalid( + Operation::create("cx", 2, 0, {}, std::nullopt, std::nullopt, + std::vector>{{0, 0}}), + "Compiler target operation applicable site tuple contains a duplicate " + "site"); + expectInvalid( + Operation::create("cx", 2, 0, {}, std::nullopt, std::nullopt, + std::vector>{{0}}), + "Compiler target operation applicable site tuple does not match its " + "arity"); + expectInvalid( + Operation::create("x", 1, 0, std::vector{valid(SiteTuple::create({0}))}, + std::nullopt, std::nullopt, + std::vector>{{1}}), + "Compiler target operation calibration references an inapplicable site " + "tuple"); expectInvalid( Operation::create("x", 1, 0, {}, std::nullopt, std::numeric_limits::quiet_NaN()), @@ -311,6 +332,13 @@ TEST(CompilerTargetTest, RejectsInvalidMetadata) { NativeOperations::fromOperations({valid(Operation::create( "x", 1, 0, std::vector{valid(SiteTuple::create({2}))}))})), "Compiler target operation site tuple references an unknown site"); + expectInvalid( + Target::create(2, Connectivity::allToAll(), + NativeOperations::fromOperations({valid(Operation::create( + "x", 1, 0, {}, std::nullopt, std::nullopt, + std::vector>{{2}}))})), + "Compiler target operation applicable site tuple references an unknown " + "site"); expectInvalid(Target::create(1, Connectivity::allToAll(), NativeOperations::fromOperations( {valid(Operation::create("cx", 2, 0))})), @@ -430,6 +458,32 @@ TEST(CompilerTargetTest, RoundTripsTypedCompilationTargetAttribute) { EXPECT_EQ(reconstructed.synthesisBasis(), target.synthesisBasis()); } +TEST(CompilerTargetTest, SupportsMaximumSiteIds) { + constexpr auto maxSite = std::numeric_limits::max(); + constexpr auto nextSite = maxSite - 1; + std::vector sites{valid(Site::create(nextSite)), + valid(Site::create(maxSite))}; + const auto x = valid(Operation::create( + "x", 1, 0, std::vector{valid(SiteTuple::create({maxSite}))}, std::nullopt, + std::nullopt, std::vector>{{nextSite}, {maxSite}})); + const auto cx = valid(Operation::create( + "cx", 2, 0, std::vector{valid(SiteTuple::create({nextSite, maxSite}))}, + std::nullopt, std::nullopt, + std::vector>{{nextSite, maxSite}})); + const auto target = + valid(Target::create(std::move(sites), Connectivity::allToAll(), + NativeOperations::fromOperations({x, cx}))); + + EXPECT_TRUE(target.supports(GateKind::X, {nextSite})); + EXPECT_TRUE(target.supports(GateKind::X, {maxSite})); + EXPECT_TRUE(target.supports(GateKind::CX, {nextSite, maxSite})); + + mlir::MLIRContext context; + context.loadDialect(); + const auto attribute = target.materialize(context); + EXPECT_EQ(valid(Target::create(attribute)).materialize(context), attribute); +} + TEST(CompilerTargetTest, RoundTripsSupportedTargetStates) { mlir::MLIRContext context; context.loadDialect(); @@ -562,6 +616,19 @@ TEST(CompilerTargetTest, DerivesControlledEntanglersFromVariadicBases) { } } +TEST(CompilerTargetTest, ResolvesLargeAllToAllVariadicBasis) { + constexpr size_t numSites = 65'535; + const auto target = valid(Target::create( + numSites, Connectivity::allToAll(), + NativeOperations::fromOperations( + {valid(Operation::create("u", 1, 3)), + valid(Operation::create("x", Arity::variadic(1), 0))}))); + + ASSERT_TRUE(target.synthesisBasis()); + EXPECT_EQ(target.synthesisBasis()->singleQubit, Target::SingleQubitBasis::U); + EXPECT_EQ(target.synthesisBasis()->entangler, GateKind::CX); +} + TEST(CompilerTargetTest, SupportsRealQCOOperationsAndStructuralOps) { mlir::DialectRegistry registry; registry.insert +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace mqt::test::compiler { + +using Target = mlir::CompilerTarget; +using Connectivity = Target::Connectivity; +using GateKind = Target::GateKind; +using MappingTarget = mlir::MappingTarget; +using NativeOperations = Target::NativeOperations; +using Operation = Target::Operation; +using SiteId = Target::SiteId; + +template +[[nodiscard]] static T validMappingValue(llvm::Expected value) { + return llvm::cantFail(std::move(value)); +} + +[[nodiscard]] static Operation globalUMappingOperation() { + return validMappingValue(Operation::create("u", 1, 3)); +} + +[[nodiscard]] static Operation +oneWayMappingGate(std::string name, size_t numParameters, + std::vector> applicableSiteTuples) { + return validMappingValue(Operation::create(std::move(name), 2, numParameters, + {}, std::nullopt, std::nullopt, + std::move(applicableSiteTuples))); +} + +TEST(MappingTargetTest, CachesDirectionalCostsOnExplicitTopology) { + const auto target = validMappingValue( + Target::create(3, Connectivity::fromCouplings({{0, 1}, {1, 2}}), + NativeOperations::fromOperations( + {globalUMappingOperation(), + oneWayMappingGate("cx", 0, {{1, 0}, {1, 2}})}))); + + const MappingTarget mappingTarget(target); + EXPECT_EQ(mappingTarget.compilerTarget().sites().data(), + target.sites().data()); + EXPECT_EQ(mappingTarget.numSites(), 3); + EXPECT_EQ(mappingTarget.maxDegree(), 2); + EXPECT_EQ(mappingTarget.distanceBetween(0, 2), 2); + std::vector neighbours; + mappingTarget.forEachNeighbour( + 1, [&](size_t neighbour) { neighbours.emplace_back(neighbour); }); + EXPECT_EQ(neighbours, (std::vector{0, 2})); + EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(0, 1), 1.F); + EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(1, 0), 0.F); + EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(1, 2), 0.F); + EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(2, 1), 1.F); + EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(0, 2), 1.F); + EXPECT_FALSE(mappingTarget.isExecutable(0, 1)); + EXPECT_TRUE(mappingTarget.isExecutable(1, 0)); + EXPECT_FALSE(mappingTarget.isExecutable(1, 1)); +} + +TEST(MappingTargetTest, TreatsSwapInvariantEntanglersAsBidirectional) { + struct Gate { + GateKind kind; + std::string_view name; + size_t numParameters; + }; + constexpr std::array gates{ + Gate{GateKind::CZ, "cz", 0}, Gate{GateKind::ISWAP, "iswap", 0}, + Gate{GateKind::RXX, "rxx", 1}, Gate{GateKind::RYY, "ryy", 1}, + Gate{GateKind::RZZ, "rzz", 1}, + }; + + for (const auto& [kind, name, numParameters] : gates) { + SCOPED_TRACE(name); + const auto target = validMappingValue(Target::create( + 2, Connectivity::fromCouplings({{0, 1}}), + NativeOperations::fromOperations( + {globalUMappingOperation(), + oneWayMappingGate(std::string{name}, numParameters, {{0, 1}})}))); + ASSERT_TRUE(target.synthesisBasis()); + EXPECT_EQ(target.synthesisBasis()->entangler, kind); + + const MappingTarget mappingTarget(target); + EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(0, 1), 0.F); + EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(1, 0), 0.F); + EXPECT_TRUE(mappingTarget.isExecutable(0, 1)); + EXPECT_TRUE(mappingTarget.isExecutable(1, 0)); + } +} + +TEST(MappingTargetTest, KeepsTopologyOnlyTargetsUsable) { + const auto target = validMappingValue( + Target::create(3, Connectivity::fromCouplings({{0, 1}, {1, 2}}), + NativeOperations::fromOperations({}))); + const MappingTarget mappingTarget(target); + + EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(0, 0), 0.F); + EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(0, 1), 0.F); + EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(0, 2), 1.F); + EXPECT_FALSE(mappingTarget.isExecutable(0, 0)); + EXPECT_TRUE(mappingTarget.isExecutable(0, 1)); + EXPECT_FALSE(mappingTarget.isExecutable(0, 2)); +} + +TEST(MappingTargetTest, SupportsImplicitAllToAllTopology) { + const auto target = validMappingValue(Target::create( + 3, Connectivity::allToAll(), + NativeOperations::fromOperations( + {globalUMappingOperation(), + oneWayMappingGate("cx", 0, {{0, 1}, {0, 2}, {1, 2}})}))); + const MappingTarget mappingTarget(target); + + EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(0, 2), 0.F); + EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(2, 0), 1.F); + EXPECT_TRUE(mappingTarget.isExecutable(0, 2)); + EXPECT_FALSE(mappingTarget.isExecutable(2, 0)); +} + +} // namespace mqt::test::compiler diff --git a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp index 63edc58e5c..0caf50d500 100644 --- a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -35,6 +35,7 @@ #include #include +#include #include #include @@ -114,13 +115,18 @@ TEST_F(MQTIRTest, RoundTripsTypedCompilationTarget) { operations = [ , - num_parameters = 0, site_tuples = []>, + num_parameters = 0, site_tuples = [], + applicability = explicit, + applicable_site_tuples = []>, , - num_parameters = 1, site_tuples = []>, + num_parameters = 1, site_tuples = [], + applicability = explicit, applicable_site_tuples = []>, , - num_parameters = 0, site_tuples = []>]>)mlir")); + num_parameters = 0, site_tuples = [], + applicability = unrestricted, + applicable_site_tuples = []>]>)mlir")); ASSERT_TRUE(compilationTarget); EXPECT_EQ(compilationTarget.getName().getValue(), "device"); ASSERT_EQ(compilationTarget.getSites().size(), 2U); @@ -138,7 +144,39 @@ TEST_F(MQTIRTest, RoundTripsTypedCompilationTarget) { mqt::OperationArityKind::Variadic); EXPECT_TRUE( compilationTarget.getOperations().front().getSiteTuples().empty()); + EXPECT_EQ(compilationTarget.getOperations()[0].getApplicability(), + mqt::OperationApplicabilityKind::Explicit); + ASSERT_EQ( + compilationTarget.getOperations()[0].getApplicableSiteTuples().size(), + 1U); + EXPECT_EQ(compilationTarget.getOperations()[0] + .getApplicableSiteTuples()[0] + .getSites(), + (ArrayRef{4, 7})); + EXPECT_EQ(compilationTarget.getOperations()[1].getApplicability(), + mqt::OperationApplicabilityKind::Explicit); + EXPECT_TRUE( + compilationTarget.getOperations()[1].getApplicableSiteTuples().empty()); + EXPECT_EQ(compilationTarget.getOperations()[2].getApplicability(), + mqt::OperationApplicabilityKind::Unrestricted); + + EXPECT_EQ(roundTrip(compilationTarget), compilationTarget); +} +TEST_F(MQTIRTest, RoundTripsMaximumSiteIds) { + const auto compilationTarget = parseAttr(R"mlir(#mqt.compilation_target< + sites = [, ], + connectivity = all_to_all, couplings = [], + native_operations = explicit, + operations = [, + num_parameters = 0, + site_tuples = [], + applicability = explicit, + applicable_site_tuples = []>]>)mlir"); + ASSERT_TRUE(compilationTarget); EXPECT_EQ(roundTrip(compilationTarget), compilationTarget); } @@ -167,6 +205,10 @@ TEST_F(MQTIRTest, RejectsInvalidTargetLeaves) { EXPECT_FALSE(parseAttr(R"mlir(#mqt.site)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.coupling)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.site_tuple)mlir")); + EXPECT_FALSE( + parseAttr(R"mlir(#mqt.applicable_site_tuple)mlir")); + EXPECT_FALSE( + parseAttr(R"mlir(#mqt.applicable_site_tuple)mlir")); EXPECT_FALSE( parseAttr(R"mlir(#mqt.site_tuple)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.site_tuple)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 0, site_tuples = []>)mlir")); + num_parameters = 0, site_tuples = [], applicability = unrestricted, + applicable_site_tuples = []>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 1, site_tuples = []>)mlir")); + num_parameters = 1, site_tuples = [], + applicability = unrestricted, applicable_site_tuples = []>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 0, site_tuples = []>)mlir")); + num_parameters = 0, site_tuples = [], + applicability = unrestricted, applicable_site_tuples = []>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 0, site_tuples = []>)mlir")); + num_parameters = 0, site_tuples = [], + applicability = unrestricted, applicable_site_tuples = []>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, num_parameters = 0, - site_tuples = [, ]>)mlir")); + site_tuples = [, ], + applicability = unrestricted, applicable_site_tuples = []>)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, + num_parameters = 0, site_tuples = [], duration =>, + applicability = unrestricted, applicable_site_tuples = []>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 0, site_tuples = [], duration =>)mlir")); + num_parameters = 0, site_tuples = [], applicability = unrestricted, + applicable_site_tuples = []>)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, + num_parameters = 0, site_tuples = [], applicability = explicit, + applicable_site_tuples = []>)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, + num_parameters = 0, site_tuples = [], applicability = explicit, + applicable_site_tuples = [, ]>)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, + num_parameters = 0, site_tuples = [], + applicability = explicit, + applicable_site_tuples = []>)mlir")); } TEST_F(MQTIRTest, RejectsInvalidCompilationTargets) { @@ -213,7 +278,8 @@ TEST_F(MQTIRTest, RejectsInvalidCompilationTargets) { native_operations = unrestricted, operations = [, - num_parameters = 0, site_tuples = []>]>)mlir")); + num_parameters = 0, site_tuples = [], applicability = unrestricted, + applicable_site_tuples = []>]>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.compilation_target< sites = [, ], connectivity = explicit, couplings = [], @@ -230,25 +296,37 @@ TEST_F(MQTIRTest, RejectsInvalidCompilationTargets) { native_operations = explicit, operations = [, - num_parameters = 0, site_tuples = []>]>)mlir")); + num_parameters = 0, site_tuples = [], + applicability = unrestricted, applicable_site_tuples = []>]>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.compilation_target< sites = [], connectivity = all_to_all, couplings = [], native_operations = explicit, operations = [, - num_parameters = 0, site_tuples = []>]>)mlir")); + num_parameters = 0, site_tuples = [], applicability = unrestricted, + applicable_site_tuples = []>]>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.compilation_target< sites = [], connectivity = all_to_all, couplings = [], native_operations = explicit, operations = [, - num_parameters = 0, site_tuples = []>]>)mlir")); + num_parameters = 0, site_tuples = [], applicability = unrestricted, + applicable_site_tuples = []>]>)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.compilation_target< + sites = [], connectivity = all_to_all, couplings = [], + native_operations = explicit, + operations = [, + num_parameters = 0, site_tuples = [], duration = 1, + applicability = unrestricted, applicable_site_tuples = []>]>)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.compilation_target< sites = [], connectivity = all_to_all, couplings = [], native_operations = explicit, operations = [, - num_parameters = 0, site_tuples = [], duration = 1>]>)mlir")); + num_parameters = 0, site_tuples = [], applicability = explicit, + applicable_site_tuples = []>]>)mlir")); } TEST_F(MQTIRTest, ManagesAndFindsEntryPoint) { diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index 394e4440b6..9a143ad2c7 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -54,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -83,7 +84,8 @@ static SmallVector getQubitValues(ValueRange values) { /// constraints. static bool isExecutable(Region& body, DenseMap& m, - const CompilerTarget& target) { + const CompilerTarget& target, + const bool requireNativeDirection = false) { for (Operation& op : body.getOps()) { if (auto staticOp = dyn_cast(op)) { m.try_emplace(staticOp.getQubit(), staticOp.getIndex()); @@ -98,7 +100,9 @@ static bool isExecutable(Region& body, const auto siteB = m.at(unitaryOp.getInputQubit(1)); const auto vertexA = target.vertexForSite(siteA); const auto vertexB = target.vertexForSite(siteB); - if (!vertexA || !vertexB || !target.areAdjacent(*vertexA, *vertexB)) { + if (!vertexA || !vertexB || !target.areAdjacent(*vertexA, *vertexB) || + (requireNativeDirection && isa(op) && + !target.supports(&op, std::array{siteA, siteB}))) { llvm::dbgs() << "The two-qubit gate (" << siteA << ", " << siteB << ") is not executable: \n"; unitaryOp->dump(); @@ -154,7 +158,7 @@ static bool isExecutable(Region& body, localM.try_emplace(arg, hw); } - if (!isExecutable(region, localM, target)) { + if (!isExecutable(region, localM, target, requireNativeDirection)) { return false; } @@ -222,9 +226,11 @@ static bool isExecutable(Region& body, } /// Return true, if the entry point fulfills the given coupling constraints. -static bool isExecutable(func::FuncOp entry, const CompilerTarget& target) { +static bool isExecutable(func::FuncOp entry, const CompilerTarget& target, + const bool requireNativeDirection = false) { DenseMap m; - return isExecutable(entry.getFunctionBody(), m, target); + return isExecutable(entry.getFunctionBody(), m, target, + requireNativeDirection); } /// Return a nxn square-grid compiler target. @@ -436,15 +442,14 @@ TEST_F(MappingPassFixture, TEST_F(MappingPassFixture, PrefersNativeDirectionWhenRouting) { using Operation = CompilerTarget::Operation; - using SiteTuple = CompilerTarget::SiteTuple; const auto target = llvm::cantFail(CompilerTarget::create( - 3, std::vector{{0, 1}, {1, 2}}, - std::vector{ - llvm::cantFail(Operation::create("u", 1, 3)), - llvm::cantFail(Operation::create( - "cx", 2, 0, - std::vector{llvm::cantFail(SiteTuple::create({1, 0})), - llvm::cantFail(SiteTuple::create({1, 2}))}))})); + 3, Connectivity::fromCouplings({{0, 1}, {1, 2}}), + NativeOperations::fromOperations( + {llvm::cantFail(Operation::create("u", 1, 3)), + llvm::cantFail(Operation::create( + "cx", 2, 0, {}, std::nullopt, std::nullopt, + std::vector>{{1, 0}, + {1, 2}}))}))); QCOProgramBuilder builder(context.get()); builder.initialize(SmallVector(3, builder.getI1Type())); @@ -466,63 +471,32 @@ TEST_F(MappingPassFixture, PrefersNativeDirectionWhenRouting) { runPass(module.get(), target, MappingPassOptions{.niterations = 1, .ntrials = 1, .seed = 42}) .succeeded()); - EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target)); + EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target, true)); - DenseMap sites; size_t numControls = 0; size_t numSwaps = 0; - for (mlir::Operation& operation : - getEntryPoint(module.get()).getFunctionBody().getOps()) { - if (auto staticOp = dyn_cast(operation)) { - sites.try_emplace(staticOp.getQubit(), staticOp.getIndex()); - continue; - } - if (auto unitary = dyn_cast(operation)) { - if (isa(operation)) { - ++numControls; - const std::array orderedSites{sites.at(unitary.getInputQubit(0)), - sites.at(unitary.getInputQubit(1))}; - EXPECT_TRUE(target.supports(&operation, orderedSites)) - << "control " << numControls << " uses sites " << orderedSites[0] - << " -> " << orderedSites[1]; - } - if (isa(operation)) { - ++numSwaps; - } - for (const auto [input, output] : llvm::zip_equal( - unitary.getInputQubits(), unitary.getOutputQubits())) { - const auto site = sites.at(input); - sites.try_emplace(output, site); - } - continue; - } - if (auto measure = dyn_cast(operation)) { - const auto site = sites.at(measure.getQubitIn()); - sites.try_emplace(measure.getQubitOut(), site); - } - } + module->walk([&](CtrlOp) { ++numControls; }); + module->walk([&](SWAPOp) { ++numSwaps; }); EXPECT_EQ(numControls, 3); EXPECT_EQ(numSwaps, 1); } -TEST_F(MappingPassFixture, MapsAllToAllGateToNativeDirection) { +TEST_F(MappingPassFixture, RoutesOppositeDirectionsOnTwoSites) { using Operation = CompilerTarget::Operation; - using SiteTuple = CompilerTarget::SiteTuple; const auto target = llvm::cantFail(CompilerTarget::create( - 3, std::nullopt, - std::vector{ - llvm::cantFail(Operation::create("u", 1, 3)), - llvm::cantFail(Operation::create( - "cx", 2, 0, - std::vector{llvm::cantFail(SiteTuple::create({1, 0})), - llvm::cantFail(SiteTuple::create({2, 0})), - llvm::cantFail(SiteTuple::create({2, 1}))}))})); + 2, Connectivity::fromCouplings({{0, 1}}), + NativeOperations::fromOperations( + {llvm::cantFail(Operation::create("u", 1, 3)), + llvm::cantFail(Operation::create( + "cx", 2, 0, {}, std::nullopt, std::nullopt, + std::vector>{{0, 1}}))}))); QCOProgramBuilder builder(context.get()); builder.initialize(SmallVector(2, builder.getI1Type())); auto q0 = builder.allocQubit(); auto q1 = builder.allocQubit(); std::tie(q0, q1) = builder.cx(q0, q1); + std::tie(q1, q0) = builder.cx(q1, q0); auto [q0Out, b0] = builder.measure(q0); auto [q1Out, b1] = builder.measure(q1); builder.sink(q0Out); @@ -534,63 +508,14 @@ TEST_F(MappingPassFixture, MapsAllToAllGateToNativeDirection) { MappingPassOptions{.niterations = 1, .ntrials = 1, .seed = 42}) .succeeded()); - DenseMap sites; - bool foundControl = false; - for (mlir::Operation& operation : - getEntryPoint(module.get()).getFunctionBody().getOps()) { - if (auto staticOp = dyn_cast(operation)) { - sites.try_emplace(staticOp.getQubit(), staticOp.getIndex()); - continue; - } - if (auto unitary = dyn_cast(operation)) { - if (isa(operation)) { - foundControl = true; - const std::array orderedSites{sites.at(unitary.getInputQubit(0)), - sites.at(unitary.getInputQubit(1))}; - EXPECT_TRUE(target.supports(&operation, orderedSites)); - } - for (const auto [input, output] : llvm::zip_equal( - unitary.getInputQubits(), unitary.getOutputQubits())) { - sites.try_emplace(output, sites.at(input)); - } - continue; - } - if (auto measure = dyn_cast(operation)) { - sites.try_emplace(measure.getQubitOut(), sites.at(measure.getQubitIn())); - } - } - EXPECT_TRUE(foundControl); -} + EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target, true)); -TEST_F(MappingPassFixture, RejectsUnreachableAllToAllGate) { - using Operation = CompilerTarget::Operation; - using SiteTuple = CompilerTarget::SiteTuple; - const auto target = llvm::cantFail(CompilerTarget::create( - 3, std::nullopt, - std::vector{ - llvm::cantFail(Operation::create("u", 1, 3)), - llvm::cantFail(Operation::create( - "cx", 2, 0, - std::vector{llvm::cantFail(SiteTuple::create({0, 1}))}))})); - - QCOProgramBuilder builder(context.get()); - builder.initialize(SmallVector(3, builder.getI1Type())); - SmallVector qubits(3); - SmallVector bits(3); - for (auto& qubit : qubits) { - qubit = builder.allocQubit(); - } - std::tie(qubits[0], qubits[1]) = builder.cx(qubits[0], qubits[1]); - std::tie(qubits[0], qubits[2]) = builder.cx(qubits[0], qubits[2]); - for (size_t i = 0; i < qubits.size(); ++i) { - std::tie(qubits[i], bits[i]) = builder.measure(qubits[i]); - builder.sink(qubits[i]); - } - auto module = builder.finalize(bits); - - EXPECT_TRUE(failed( - runPass(module.get(), target, - MappingPassOptions{.niterations = 1, .ntrials = 1, .seed = 42}))); + size_t numControls = 0; + size_t numSwaps = 0; + module->walk([&](CtrlOp) { ++numControls; }); + module->walk([&](SWAPOp) { ++numSwaps; }); + EXPECT_EQ(numControls, 2); + EXPECT_EQ(numSwaps, 1); } TEST_F(MappingPassFixture, PreserveNoncontiguousTargetSiteIds) { diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp index abd44ffd31..c5885690ac 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -62,7 +62,6 @@ using Connectivity = Target::Connectivity; using NativeOperations = Target::NativeOperations; using Operation = Target::Operation; using Site = Target::Site; -using SiteTuple = Target::SiteTuple; using mlir::ModuleOp; using mlir::OwningOpRef; using mlir::Value; @@ -168,11 +167,21 @@ makeUCxTarget(std::optional> sites = std::nullopt) { } [[nodiscard]] static Target makeOneWayUCxTarget() { - std::vector operations{ - valid(Operation::create("u", 1, 3)), - valid(Operation::create("cx", 2, 0, - std::vector{valid(SiteTuple::create({1, 0}))}))}; - return valid(Target::create(2, std::nullopt, std::move(operations))); + std::vector operations{valid(Operation::create("u", 1, 3)), + valid(Operation::create( + "cx", 2, 0, {}, std::nullopt, std::nullopt, + std::vector>{{1, 0}})), + valid(Operation::create("gphase", 0, 1))}; + return valid(Target::create(2, Connectivity::allToAll(), + NativeOperations::fromOperations(operations))); +} + +[[nodiscard]] static Target makeOneWayRxxTarget() { + std::vector operations{valid( + Operation::create("rxx", 2, 1, {}, std::nullopt, std::nullopt, + std::vector>{{1, 0}}))}; + return valid(Target::create(2, Connectivity::allToAll(), + NativeOperations::fromOperations(operations))); } [[nodiscard]] static mlir::DenseElementsAttr @@ -488,26 +497,26 @@ TEST_F(TargetSynthesisTest, %f0, %f1 = scf.for %i = %c0 to %c1 step %c1 iter_args(%a = %q0, %b = %q1) -> (!qco.qubit, !qco.qubit) { - %s0, %s1 = qco.swap %a, %b - : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit - scf.yield %s0, %s1 : !qco.qubit, !qco.qubit + %h0 = qco.h %a : !qco.qubit -> !qco.qubit + %h1 = qco.h %b : !qco.qubit -> !qco.qubit + scf.yield %h0, %h1 : !qco.qubit, !qco.qubit } %w0, %w1 = scf.while (%a = %f0, %b = %f1) : (!qco.qubit, !qco.qubit) -> (!qco.qubit, !qco.qubit) { - %s0, %s1 = qco.swap %a, %b - : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit - scf.condition(%false) %s0, %s1 : !qco.qubit, !qco.qubit + %h0 = qco.h %a : !qco.qubit -> !qco.qubit + %h1 = qco.h %b : !qco.qubit -> !qco.qubit + scf.condition(%false) %h0, %h1 : !qco.qubit, !qco.qubit } do { ^bb0(%a: !qco.qubit, %b: !qco.qubit): - %s0, %s1 = qco.swap %a, %b - : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit - scf.yield %s0, %s1 : !qco.qubit, !qco.qubit + %h0 = qco.h %a : !qco.qubit -> !qco.qubit + %h1 = qco.h %b : !qco.qubit -> !qco.qubit + scf.yield %h0, %h1 : !qco.qubit, !qco.qubit } %i0, %i1 = qco.index_switch %c0 -> (!qco.qubit, !qco.qubit) case 0 args(%a = %w0, %b = %w1) { - %s0, %s1 = qco.swap %a, %b - : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit - qco.yield %s0, %s1 : !qco.qubit, !qco.qubit + %h0 = qco.h %a : !qco.qubit -> !qco.qubit + %h1 = qco.h %b : !qco.qubit -> !qco.qubit + qco.yield %h0, %h1 : !qco.qubit, !qco.qubit } default args(%a = %w0, %b = %w1) { qco.yield %a, %b : !qco.qubit, !qco.qubit @@ -524,50 +533,119 @@ TEST_F(TargetSynthesisTest, ASSERT_TRUE(mlir::succeeded( runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); - EXPECT_EQ(countOps(*module), 0U); + EXPECT_EQ(countOps(*module), 0U); ASSERT_TRUE(mlir::succeeded( runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); ASSERT_TRUE(mlir::succeeded(mlir::verify(*module))); } TEST_F(TargetSynthesisTest, - TargetNativeSynthesisRejectsUnavailableSiteLocalBasis) { + TargetNativeSynthesisHandlesAmbiguousSingleQubitSite) { + auto module = build([](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + const auto outputs = builder.qcoIf( + true, ValueRange{q0, q1}, + [](ValueRange arguments) { + return mlir::SmallVector{arguments[0], arguments[1]}; + }, + [](ValueRange arguments) { + return mlir::SmallVector{arguments[1], arguments[0]}; + }); + auto h = builder.h(outputs[0]); + builder.sink(h); + builder.sink(outputs[1]); + return builder.intConstant(0); + }); + const auto target = makeOneWayUCxTarget(); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_EQ(countOps(*module), 0U); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*module))); +} + +TEST_F(TargetSynthesisTest, TargetNativeSynthesisRejectsAmbiguousBranchSites) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func @main() { + %true = arith.constant true + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %r0, %r1 = qco.if %true args(%a = %q0, %b = %q1) + -> (!qco.qubit, !qco.qubit) { + qco.yield %a, %b : !qco.qubit, !qco.qubit + } else args(%a = %q0, %b = %q1) { + qco.yield %b, %a : !qco.qubit, !qco.qubit + } + %c, %t = qco.ctrl(%r0) targets(%arg = %r1) { + %x = qco.x %arg : !qco.qubit -> !qco.qubit + qco.yield %x : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) + -> ({!qco.qubit}, {!qco.qubit}) + qco.sink %c : !qco.qubit + qco.sink %t : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + + const auto diagnostics = expectFailure( + *module, mlir::qco::createTargetNativeSynthesis(makeOneWayUCxTarget())); + EXPECT_NE(diagnostics.find( + "no supported synthesis-basis placement is known for its " + "static sites"), + std::string::npos) + << diagnostics; +} + +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisRejectsIncompleteGlobalSingleQubitBasis) { auto module = build([](QCOProgramBuilder& builder) { auto qubit = builder.staticQubit(1); qubit = builder.h(qubit); return builder.intConstant(0); }); - const auto target = valid(Target::create( - 2, std::nullopt, - std::vector{valid(Operation::create( - "u", 1, 3, std::vector{valid(SiteTuple::create({0}))})), - valid(Operation::create("cx", 2, 0))})); + const auto target = valid( + Target::create(2, Connectivity::allToAll(), + NativeOperations::fromOperations( + {valid(Operation::create( + "u", 1, 3, {}, std::nullopt, std::nullopt, + std::vector>{{0}})), + valid(Operation::create("cx", 2, 0))}))); const auto diagnostics = expectFailure(*module, mlir::qco::createTargetNativeSynthesis(target)); - EXPECT_NE(diagnostics.find("no usable single-qubit basis on site 1"), - std::string::npos) + EXPECT_NE(diagnostics.find("no usable synthesis basis"), std::string::npos) << diagnostics; } TEST_F(TargetSynthesisTest, - TargetNativeSynthesisRejectsUnavailableEntanglerPair) { + TargetNativeSynthesisRejectsNonadjacentSynthesisPlacement) { auto module = build([](QCOProgramBuilder& builder) { - auto q1 = builder.staticQubit(1); + auto q1 = builder.staticQubit(0); auto q2 = builder.staticQubit(2); std::tie(q1, q2) = builder.swap(q1, q2); return builder.intConstant(0); }); const auto target = valid(Target::create( - 3, std::nullopt, - std::vector{ - valid(Operation::create("u", 1, 3)), - valid(Operation::create( - "cx", 2, 0, std::vector{valid(SiteTuple::create({0, 1}))}))})); + 3, Connectivity::fromCouplings({{0, 1}, {1, 2}}), + NativeOperations::fromOperations( + {valid(Operation::create("u", 1, 3)), + valid(Operation::create( + "cx", 2, 0, {}, std::nullopt, std::nullopt, + std::vector>{{0, 1}, {1, 2}})), + valid(Operation::create("gphase", 0, 1))}))); const auto diagnostics = expectFailure(*module, mlir::qco::createTargetNativeSynthesis(target)); - EXPECT_NE(diagnostics.find("no usable entangler on sites 1 and 2"), + EXPECT_NE(diagnostics.find( + "no supported synthesis-basis placement is known for its " + "static sites"), std::string::npos) << diagnostics; } @@ -916,6 +994,46 @@ TEST_F(TargetSynthesisTest, SupportedRuntimeParameterizedGateStaysUntouched) { EXPECT_EQ(printModule(*module), before); } +TEST_F(TargetSynthesisTest, + RuntimeRxxUsesReverseNativeTupleWithoutSynthesisBasis) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func @main(%theta: f64) -> (!qco.qubit, !qco.qubit) { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %q2, %q3 = qco.rxx(%theta) %q0, %q1 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + return %q2, %q3 : !qco.qubit, !qco.qubit + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + const auto target = makeOneWayRxxTarget(); + ASSERT_FALSE(target.synthesisBasis()); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*module))); + + RXXOp rxx; + module->walk([&](RXXOp candidate) { rxx = candidate; }); + ASSERT_TRUE(rxx); + auto unitary = mlir::cast(rxx.getOperation()); + auto input0 = unitary.getInputQubit(0).getDefiningOp(); + auto input1 = unitary.getInputQubit(1).getDefiningOp(); + ASSERT_TRUE(input0); + ASSERT_TRUE(input1); + EXPECT_EQ(input0.getIndex(), 1U); + EXPECT_EQ(input1.getIndex(), 0U); + + auto returnOp = mlir::cast( + mainFunction(*module).getBody().front().getTerminator()); + EXPECT_EQ(returnOp.getOperand(0), unitary.getOutputQubit(1)); + EXPECT_EQ(returnOp.getOperand(1), unitary.getOutputQubit(0)); +} + TEST_F(TargetSynthesisTest, UnsupportedRuntimeParameterizedGateHasLocalDiagnostic) { auto module = mlir::parseSourceString(R"mlir( @@ -948,6 +1066,8 @@ TEST_F(TargetSynthesisTest, func.func @main(%theta: f64) -> (!qco.qubit, !qco.qubit) { %q0 = qco.static 0 : !qco.qubit %q1 = qco.static 1 : !qco.qubit + %phase = arith.constant 0.25 : f64 + qco.gphase(%phase) %q2 = qco.rz(%theta) %q0 : !qco.qubit -> !qco.qubit %q3, %q4 = qco.rxx(%theta) %q2, %q1 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit return %q3, %q4 : !qco.qubit, !qco.qubit diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index eadabc2f90..9d20fb83b6 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -187,7 +187,7 @@ class CompilerTarget: """Whether this arity accepts a concrete width.""" class Operation: - """A target operation capability, applicability, and calibration.""" + """A target operation capability, calibration, and ordered applicability.""" def __init__( self, @@ -215,17 +215,13 @@ class CompilerTarget: def num_parameters(self) -> int: """The number of real-valued parameters.""" - @property - def has_explicit_applicability(self) -> bool: - """Whether applicability is explicitly enumerated.""" - @property def site_tuples(self) -> list[CompilerTarget.SiteTuple]: """Ordered site-specific calibration data.""" @property - def applicable_site_tuples(self) -> list[list[int]]: - """The exact ordered tuples with operation support, if explicit.""" + def applicable_site_tuples(self) -> list[list[int]] | None: + """The ordered target-site tuples, or None when unrestricted.""" @property def duration(self) -> int | None: @@ -395,11 +391,7 @@ class CompilerTarget: """A complete target-wide synthesis basis, if available.""" def supports_operation( - self, - name: str, - arity: int, - num_parameters: int | None = None, - sites: Sequence[int] | None = None, + self, name: str, arity: int, num_parameters: int | None = None, sites: Sequence[int] | None = None ) -> bool: """Whether the target supports an operation.""" diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index 05ca6eedcc..5037a9c4ac 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -494,12 +494,17 @@ def test_compiler_target_constructors_preserve_python_api() -> None: assert site_tuple.sites == [10, 20] assert len(operation.site_tuples) == 1 assert operation.site_tuples[0].sites == [10, 20] - assert operation.has_explicit_applicability assert operation.applicable_site_tuples == [[10, 20]] - assert not CompilerTarget.Operation("x", 1, 0).has_explicit_applicability + assert CompilerTarget.Operation("x", 1, 0).applicable_site_tuples is None explicitly_unavailable = CompilerTarget.Operation("ecr", 2, 0, applicable_site_tuples=[]) - assert explicitly_unavailable.has_explicit_applicability assert explicitly_unavailable.applicable_site_tuples == [] + explicitly_unavailable_target = CompilerTarget( + 2, + connectivity=connectivity, + native_operations=CompilerTarget.NativeOperations([explicitly_unavailable]), + ) + assert targets[0].supports_operation("ecr", 2, sites=[0, 1]) + assert not explicitly_unavailable_target.supports_operation("ecr", 2, sites=[0, 1]) assert targets[2].supports_operation("cx", 2, sites=[10, 20]) assert not targets[2].supports_operation("cx", 2, sites=[20, 10]) assert operation.arity.kind == CompilerTarget.OperationArityKind.FIXED @@ -549,6 +554,18 @@ def test_compiler_target_construction_preserves_validation_errors() -> None: 0, site_tuples=[CompilerTarget.SiteTuple([0, 1])], ) + with pytest.raises(ValueError, match="applicable site tuple does not match its arity"): + CompilerTarget.Operation("cx", 2, 0, applicable_site_tuples=[[0]]) + with pytest.raises(ValueError, match="applicable site tuple contains a duplicate site"): + CompilerTarget.Operation("cx", 2, 0, applicable_site_tuples=[[0, 0]]) + with pytest.raises(ValueError, match="calibration references an inapplicable site tuple"): + CompilerTarget.Operation( + "cx", + 2, + 0, + site_tuples=[CompilerTarget.SiteTuple([0, 1])], + applicable_site_tuples=[[1, 0]], + ) def test_compiler_target_snapshots_qdmi_device(garnet_target: CompilerTarget) -> None: @@ -601,6 +618,7 @@ def _compiler_target_metadata(target: CompilerTarget) -> dict[str, object]: operation.num_parameters, operation.duration, operation.fidelity, + operation.applicable_site_tuples, [(site_tuple.sites, site_tuple.duration, site_tuple.fidelity) for site_tuple in operation.site_tuples], ) for operation in target.operations From 404ee61fcd458f59fba8a06009724ed0b22aae9d Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 4 Sep 2026 09:33:52 +0000 Subject: [PATCH 4/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20directional?= =?UTF-8?q?=20target=20compilation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route by adjacency and repair operand direction during native synthesis. Use one checked staged walk for exact static sites, with branch agreement and site-preserving loop backedges. Remove the module clone and duplicate synthesis planning. Validation: 303 focused tests, strict documentation, and repository lint pass. Direct whole-file clang-tidy finds no diagnostics in changed source files. Full C++ lint is blocked by unrelated QIR/QTensor linking; a locationless binding macro warning remains. Assisted-by: OpenAI Codex --- .agent/plans/directional-gate-mapping.md | 241 ++++++-------- CHANGELOG.md | 6 +- docs/mlir/target_compilation.md | 8 + mlir/include/mlir/Compiler/MappingTarget.h | 60 ---- .../mlir/Dialect/QCO/Transforms/Passes.h | 11 +- .../mlir/Dialect/QCO/Transforms/Passes.td | 4 +- mlir/lib/Compiler/CMakeLists.txt | 14 +- mlir/lib/Compiler/MappingTarget.cpp | 99 ------ mlir/lib/Compiler/Target.cpp | 5 +- mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 6 +- .../QCO/Transforms/Mapping/Mapping.cpp | 82 ++--- .../NativeSynthesis/TargetSynthesis.cpp | 297 +++++++----------- mlir/unittests/Compiler/CMakeLists.txt | 5 +- .../Compiler/test_mapping_target.cpp | 136 -------- .../QCO/Transforms/Mapping/test_mapping.cpp | 95 +----- .../NativeSynthesis/test_target_synthesis.cpp | 186 +++++++++-- 16 files changed, 439 insertions(+), 816 deletions(-) delete mode 100644 mlir/include/mlir/Compiler/MappingTarget.h delete mode 100644 mlir/lib/Compiler/MappingTarget.cpp delete mode 100644 mlir/unittests/Compiler/test_mapping_target.cpp diff --git a/.agent/plans/directional-gate-mapping.md b/.agent/plans/directional-gate-mapping.md index 8898955089..988bbe682b 100644 --- a/.agent/plans/directional-gate-mapping.md +++ b/.agent/plans/directional-gate-mapping.md @@ -1,190 +1,127 @@ -# Support directional target gates during mapping +# Compile directional target gates through native synthesis -This ExecPlan is a living document. The sections `Progress`, -`Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must -be kept up to date as work proceeds. - -This ExecPlan must be maintained in accordance with `.agent/PLANS.md` from the -repository root. +This ExecPlan follows `.agent/PLANS.md` and records the supported contract, +implementation, and validation for ordered compiler-target applicability. ## Purpose / Big Picture -MQT Core target compilation must accept devices that expose a two-qubit gate on -each topology edge in only one operand order. After this change, mapping uses -the undirected topology for reachability but prefers a layout whose ordered -operands match the device. Native synthesis repairs an unavoidable opposite -order without changing program semantics, and final conformance verifies the -exact ordered placement. - -The result is visible by compiling alternating CX directions for a two-site -one-way target: compilation succeeds, every emitted two-qubit operation uses the -reported direction, and final target conformance succeeds. +Compile gates for devices that support an entangler in only one operand order. +Routing makes operands adjacent; native synthesis repairs their direction and +final conformance checks exact physical sites. Alternating CX directions on two +adjacent sites must not introduce routing SWAPs. ## Progress -- [x] (2026-09-03) Added exact ordered operation applicability to the target - model, QDMI adapter, typed attributes, and Python bindings. -- [x] (2026-09-03) Added cached directional mapping costs while preserving - undirected topology traversal. -- [x] (2026-09-03) Added site-aware native synthesis and exact final conformance - checks. -- [x] (2026-09-03) Added focused C++ and Python regressions, regenerated stubs, - and completed the Core build and lint checks. - -## Surprises & Discoveries - -- Observation: QDMI site tuples are sparse calibration records and cannot also - represent operation availability. Evidence: operations with default - calibration have no `SiteTuple` entries even though their QDMI site list is - complete. -- Observation: mathematically symmetric gates can still be reported in one - syntactic operand order. Runtime-parameter RXX cannot be matrix-decomposed, so - native synthesis must clone it with swapped inputs and restore the result - order. -- Observation: a qubit emerging from structured control flow may have several - possible sites. A one-qubit operation is conformant only when it is supported - on every possible site; a direction-dependent two-qubit operation requires an - exact ordered placement. -- Observation: target site IDs span all nonnegative `int64_t` values, including - values reserved internally by LLVM dense containers. Scalar site-ID caches - therefore use standard unordered sets; maximum-value round-trip tests cover - this boundary. +- [x] (2026-09-03) Preserve independent applicability and calibration metadata. +- [x] (2026-09-04) Remove directional routing and its dedicated wrapper. +- [x] (2026-09-04) Replace ambiguous-site analysis with a checked staged walk. +- [x] (2026-09-04) Remove whole-module cloning for failed synthesis. +- [x] (2026-09-04) Add focused regressions and align public pass documentation. +- [x] (2026-09-04) Apply specialist, adversarial, and Ponytail Review feedback. +- [x] (2026-09-04) Pass 303 focused tests, documentation, and repository lint. +- [x] (2026-09-04) Attempt full C++ lint and analyze changed sources directly; + record the unrelated build blocker below. ## Decision Log -- Decision: keep `CompilerTarget` immutable and represent mapping policy in a - cached `MappingTarget` wrapper. Rationale: mapping costs are derived data, - useful as one coherent view, and should not mutate the device snapshot. - Date/Author: 2026-09-03, contributor. -- Decision: preserve exact ordered applicability for every operation, including - mathematically symmetric gates. Rationale: the target model records what a - backend actually reports; any safe operand reorder is an explicit synthesis - transformation and final conformance remains exact. Date/Author: 2026-09-03, - contributor. -- Decision: keep Mapping synthesis-free and base its directional penalty on the - target-wide synthesis entangler. Rationale: arbitrary non-native two-qubit - gates are lowered through that entangler, while actual reversal belongs in - native synthesis. Date/Author: 2026-09-03, contributor. -- Decision: use a unit routing penalty for an adjacent edge available only in - reverse. Rationale: it models the additional local direction repair while - leaving nonadjacent cost equal to shortest-path SWAP distance. Date/Author: - 2026-09-03, contributor. +On 2026-09-03 the maintainer approved adjacency-only routing. Direction repair +belongs to synthesis. Weighted routing edges remain possible future work; there +is no current need for an extra cost wrapper. -## Outcomes & Retrospective +On 2026-09-03 the maintainer approved requiring one known physical site per +quantum value, equal branch-result sites, and site-preserving loop backedges. +These conditions are checked, including for all-to-all placement and standalone +passes. Ordinary structured control flow remains supported. -The Core compiler now preserves exact ordered applicability through target -materialization, QDMI snapshots, mapping, native synthesis, and conformance. The -focused Compiler, MQT IR, Mapping, NativeSynthesis, and Python MLIR suites pass. -Generated stubs, repository lint, C++ lint, documentation, and -`git diff --check` also pass. Regression coverage includes one-way entanglers, -ambiguous structured-control-flow sites, and the full nonnegative site-ID -domain. +On 2026-09-03 the maintainer approved removing synthesis rollback. Compilation +runs in place; callers must not rely on program contents after failure. +Independent applicability and calibration metadata, generic capability tuples, +and constant-time ordered-pair support queries remain part of the target model. -## Context and Orientation +## Surprises & Discoveries -`mlir/include/mlir/Compiler/Target.h` and `mlir/lib/Compiler/Target.cpp` define -the immutable device snapshot. An operation may be unrestricted or may list -exact ordered physical site tuples. `SiteTuple` remains calibration-only. +An executed two-site probe produced five native CXs with directional routing and +two with direct synthesis. The extra SWAP is avoidable. Another valid-IR probe +passed a site through `scf.execute_region`; name-only fallback incorrectly +accepted a reversed CX. Unknown site transfers must fail with a diagnostic. -`mlir/include/mlir/Compiler/MappingTarget.h` and -`mlir/lib/Compiler/MappingTarget.cpp` form a cheap wrapper around that snapshot. -For each explicit topology edge they cache whether the synthesis entangler is -native in the forward order, reverse order, or both. Mapping uses the undirected -edge to move qubits and the cached ordered cost to select a layout. +Explicit mapping realigns structured region exits to physical slots. All-to-all +placement only replaces allocations, so site consistency must be checked rather +than assumed. Runtime symmetric gates such as RXX need direct operand reordering +because their matrix is unavailable at compile time. -`mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp` implements placement and -routing. Its lookahead window must retain semantic operand order while its -pair-block bookkeeping remains order-independent. +## Context and Orientation -`mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp` lowers -operations after mapping. It derives each linear qubit value's static target -site through structured control flow, checks exact native support, and either -keeps, reorders, or decomposes an operation. The final conformance pass uses the -same ordered site facts. +`mlir/lib/Compiler/Target.cpp` owns immutable target capabilities and basis +selection. A usable synthesis basis supplies one-qubit gates on every site and +an entangler on every routing edge in at least one direction. Applicability is +independent of sparse calibration records. -`mlir/lib/Compiler/QDMIAdapter.cpp` snapshots device data. QDMI operation site -lists become applicability; per-site duration and fidelity differences become -sparse calibration tuples. The MQT dialect attribute files serialize both states -without losing an explicit empty applicability list. +`mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp` performs placement and +routing. `mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp` +then assigns exact sites, preserves or reorders native gates, and decomposes +other supported gates. Its conformance pass checks emitted capabilities. ## Plan of Work -First, complete the compiler-target representation and its typed MLIR attribute. -Validate tuple arity, distinct nonnegative sites, known target sites, and -calibration references. Cache one- and two-site applicability for constant -ordered queries, while retaining exact tuple matching for higher arities. +First remove `mlir/include/mlir/Compiler/MappingTarget.h`, its implementation +and dedicated tests, and restore topology-only mapping and build wiring. Prove +that alternating CXs need no routing SWAPs and only two native entanglers. -Second, wrap the target in `MappingTarget`. Construct its adjacent direction -costs once, proxy the topology operations required by Mapping, and change only -goal, heuristic, placement delegation, and advance checks. Preserve the merged -window traversal and use semantic operand order recovered from each unitary's -outputs. +Next use MLIR's staged operation walk and one site map. Propagate sites through +unitaries, reset, and measurement, seed supported region arguments, and compare +branch results and loop backedges. Reject unknown or conflicting sites. Remove +the module clone and duplicate planning. Preserve matrix/output permutation for +directional synthesis and direct symmetric operand reordering. -Third, make native synthesis site-aware. Propagate static sites to a fixed point -through QCO and SCF structured operations. Reject ambiguous or nonadjacent -placements before rewriting. Reverse asymmetric basis synthesis mathematically; -for a symmetric operation supported only in reverse, clone it with swapped -operands and map its outputs back, which also supports runtime parameters. +Finally update `docs/mlir/target_compilation.md`, pass descriptions, and the +existing changelog entry. Keep regression tests in the established native +synthesis and compiler test suites. Obtain independent reviews after the first +implementation, then incorporate the separate Ponytail Review findings. -Finally, validate the layers independently and together and regenerate the -Python stubs through the repository's Nox session. +## Concrete Steps and Validation -## Concrete Steps - -Run all commands from the repository root. Set `MLIR_DIR` to the directory that -contains `MLIRConfig.cmake` for MLIR 23.1 or newer, then configure Core: +Run from the repository root with the configured LLVM/MLIR 23 installation: cmake --preset release - -Build and run the focused binaries: - - cmake --build build/release --target \ - mqt-core-mlir-unittests-compiler \ - mqt-core-mlir-unittest-mqt-ir \ - mqt-core-mlir-unittest-mapping \ - mqt-core-mlir-unittest-target-synthesis + cmake --build --preset release --target mqt-core-mlir-unittests-compiler mqt-core-mlir-unittest-mapping mqt-core-mlir-unittest-target-synthesis mqt-core-mlir-unittest-mqt-ir -j 8 build/release/mlir/unittests/Compiler/mqt-core-mlir-unittests-compiler - build/release/mlir/unittests/Dialect/MQT/IR/mqt-core-mlir-unittest-mqt-ir build/release/mlir/unittests/Dialect/QCO/Transforms/Mapping/mqt-core-mlir-unittest-mapping build/release/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/mqt-core-mlir-unittest-target-synthesis - -Regenerate and test Python bindings, then run repository checks: - - uvx nox -s stubs - uvx nox -s tests-3.13 -- test/python/test_mlir.py -q + build/release/mlir/unittests/Dialect/MQT/IR/mqt-core-mlir-unittest-mqt-ir uvx nox -s cpp-lint - uvx nox -s docs + uvx nox --non-interactive -s docs git diff --check uvx nox -s lint -## Validation and Acceptance - -Compiler-target tests must distinguish unrestricted, explicit, and -explicit-empty applicability and preserve those states through typed MLIR and -Python. QDMI tests must preserve exact one-way and two-way site lists while -keeping calibration sparse. - -Mapping tests must show that a three-site one-way chain chooses the native -orientation and inserts exactly the expected SWAP, and that the two-site search -budget can repair an opposite direction. Native synthesis tests must prove -semantic equivalence for reversed CX, exact conformance rejection before -synthesis, runtime RXX operand reordering without a synthesis basis, and safe -failure for ambiguous structured-control-flow sites. - -The compiler pipeline test must compile alternating CX directions and verify -that every final two-qubit operation is supported on its exact static sites. The -C++ linter, Python lint, strict documentation, generated-stub check, and -`git diff --check` must pass without LCOV exclusions. +Successful output must verify, retain exact ordered target applicability and +quantum semantics, and support consistent if/switch/for/while site transfers. +Unknown sites, conflicting branch exits, and changing loop-backedge sites must +be diagnosed. Failed compilation need not preserve input IR. No bindings or +capability serialization interfaces change. ## Idempotence and Recovery -Source edits, formatting, configuration, builds, and tests are repeatable. -Preserve unrelated changes and do not modify another task's worktree. If -generated stubs differ, rerun the repository's `stubs` Nox session instead of -editing them by hand. If a target cannot supply a global synthesis basis, direct -symmetric native reordering may still proceed, but directional mapping falls -back to the ordinary topology because there is no single entangler whose -direction can safely represent every non-native operation. +Builds and checks are repeatable. Preserve unrelated changes and keep generated +build output untracked. No dependency additions or generated-file edits are +needed. + +## Outcomes & Retrospective -Revision note (2026-09-03): Retained only Core design, recovery, and validation -information. +Independent specialists and the adversarial reviewer found no blocking defect in +the staged transfer or target contracts. Their feedback removed duplicate +site-lookup validation and added a while-result/backedge regression. Ponytail +Review removed control-specific input selection and duplicate tuple-membership +lambdas. Production code is 250 lines smaller than the reviewed head. + +All 303 focused tests pass: compiler 152, mapping 94, target synthesis 42, and +MQT IR 15. Strict documentation and repository lint pass. Full C++ lint stops +before analysis because unchanged QIR runtime test executables have unresolved +QTensor symbols. Building the changed translation units directly succeeds; +whole-file clang-tidy reports no diagnostics in the 12 changed source files. +Generated/header diagnostics are outside the repository lint scope, and the +binding command produces a locationless macro-parentheses warning. No lint +configuration or unrelated build wiring was changed. + +Revision note: aligned the scope with the approved routing, site, and failure +contracts while retaining exact device metadata. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f73b04d38..7a43767a78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,9 +39,9 @@ releases may include breaking changes. ([#1915], [#1973], [#2077], [#2078], [#2079], [#2334]) ([**@simon1hofmann**], [**@burgholzer**]) - ✨ Add immutable MLIR compiler targets, QDMI device integration, ordered - operation applicability, directional mapping, and target compilation through - C++, Python, and `mqt-cc` ([#1687], [#1993], [#1999], [#2049], [#2285]) - ([**@MatthiasReumann**], [**@simon1hofmann**], [**@burgholzer**]) + operation applicability, directional native synthesis, and target compilation + through C++, Python, and `mqt-cc` ([#1687], [#1993], [#1999], [#2049], + [#2285]) ([**@MatthiasReumann**], [**@simon1hofmann**], [**@burgholzer**]) #### Import and export diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index b57f89a09f..f5a89aa9aa 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -70,6 +70,14 @@ ordered tuples; it is independent of the sparse calibration entries in `site_tuples`. Structural and program-format constructs are not compiler-target operations. +Routing uses undirected adjacency; native synthesis repairs unsupported operand +directions. Target compilation requires a known static physical site for each +qubit. Structured branch exits must agree on sites, and loop backedges must +preserve the entry sites. Unsupported or inconsistent site transfers are +diagnosed, including after all-to-all placement. A synthesis basis must provide +the same one-qubit gate family on every site and an entangler on every routing +edge in at least one direction. + Target synthesis preserves a native `gphase`. If the target does not support `gphase`, target synthesis preserves relative phase effects and removes only the unobservable global phase of the entry point. diff --git a/mlir/include/mlir/Compiler/MappingTarget.h b/mlir/include/mlir/Compiler/MappingTarget.h deleted file mode 100644 index eccff8e7ca..0000000000 --- a/mlir/include/mlir/Compiler/MappingTarget.h +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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/Compiler/Target.h" - -#include -#include - -#include - -namespace mlir { - -/// Compiler-target topology with cached directional mapping costs. -/// -/// Adjacent native entangler directions have cost zero. A direction that can -/// be synthesized from the reverse native direction has cost one. Symmetric -/// entanglers have cost zero in both directions. Nonadjacent costs are the -/// target's shortest-path distance minus one. Construction visits each target -/// coupling at most once and all cost lookups are constant time. -class MappingTarget { -public: - explicit MappingTarget(const CompilerTarget& target); - - /// Return the immutable compiler target. - [[nodiscard]] const CompilerTarget& compilerTarget() const noexcept; - - /// Return the number of target sites. - [[nodiscard]] size_t numSites() const noexcept; - - /// Return the target topology's maximum degree. - [[nodiscard]] size_t maxDegree() const noexcept; - - /// Return the shortest-path distance between two valid target vertices. - [[nodiscard]] size_t distanceBetween(size_t source, size_t target) const; - - /// Invoke @p callback for every neighbour of a valid target vertex. - void forEachNeighbour(size_t vertex, - llvm::function_ref callback) const; - - /// Return the routing cost from @p source to @p target. - [[nodiscard]] float pathCostBetween(size_t source, size_t target) const; - - /// Return whether a two-qubit gate is executable in this order. - [[nodiscard]] bool isExecutable(size_t source, size_t target) const; - -private: - CompilerTarget target_; - llvm::DenseSet penalizedDirections_; -}; - -} // namespace mlir diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h index 33a70cede8..25eb69810f 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h @@ -47,15 +47,14 @@ namespace mlir::qco { createDecomposeMultiControlled(const CompilerTarget& target, uint64_t minQubits = 3); -/** - * @brief Create post-routing synthesis for one immutable compiler target. - */ +/// Create post-routing synthesis for one immutable compiler target. +/// Each qubit must have a known static site. Structured branch exits must agree +/// on sites and loop backedges must preserve their entry sites. +/// The input may be modified on failure. [[nodiscard]] std::unique_ptr createTargetNativeSynthesis(const CompilerTarget& target); -/** - * @brief Create the final mapped-operation conformance verifier. - */ +/// Create the final mapped-operation verifier, requiring known static sites. [[nodiscard]] std::unique_ptr createVerifyTargetConformance(const CompilerTarget& target); diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index f459c0f3df..3ba1af6181 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -154,9 +154,9 @@ def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { - `window` contains at most `1 + nlookahead` two-qubit operations in program order. - `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(gate, p)` is `dist(p[gate.first], p[gate.second]) - 1` for nonadjacent operands, zero for an adjacent native operand order, one when only the reverse operand order is native, and infinity when neither adjacent order is supported. + - `h(gate, p)` is `dist(p[gate.first], p[gate.second]) - 1`. - Routing uses the undirected connectivity underlying the target topology, while gate costs retain the semantic operand order. Target-native synthesis realizes operations and inserted SWAPs in a supported direction. + Routing uses undirected target connectivity. Target-native synthesis realizes operations and inserted SWAPs in a supported operand direction. 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 diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index 0411204374..dd6b46da0e 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -10,7 +10,6 @@ add_mlir_library( MQTCompilerTarget PARTIAL_SOURCES_INTENDED - MappingTarget.cpp Target.cpp ADDITIONAL_HEADER_DIRS ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler @@ -22,15 +21,8 @@ add_mlir_library( mqt_mlir_target_use_project_options(MQTCompilerTarget) -target_sources( - MQTCompilerTarget - PUBLIC FILE_SET - HEADERS - BASE_DIRS - ${MQT_MLIR_SOURCE_INCLUDE_DIR} - FILES - ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/MappingTarget.h - ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/Target.h) +target_sources(MQTCompilerTarget PUBLIC FILE_SET HEADERS BASE_DIRS ${MQT_MLIR_SOURCE_INCLUDE_DIR} + FILES ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/Target.h) # Build the optional QDMI-to-compiler-target adapter set(LLVM_REQUIRES_EH ON) @@ -93,7 +85,7 @@ mqt_mlir_target_use_project_options(MQTCompilerPipeline) # collect header files file(GLOB_RECURSE COMPILER_HEADERS_SOURCE "${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/*.h") -list(FILTER COMPILER_HEADERS_SOURCE EXCLUDE REGEX "/(MappingTarget|QDMIAdapter|Target)\\.h$") +list(FILTER COMPILER_HEADERS_SOURCE EXCLUDE REGEX "/(QDMIAdapter|Target)\\.h$") target_sources(MQTCompilerPipeline PUBLIC FILE_SET HEADERS BASE_DIRS ${MQT_MLIR_SOURCE_INCLUDE_DIR} FILES ${COMPILER_HEADERS_SOURCE}) diff --git a/mlir/lib/Compiler/MappingTarget.cpp b/mlir/lib/Compiler/MappingTarget.cpp deleted file mode 100644 index cd017d42b4..0000000000 --- a/mlir/lib/Compiler/MappingTarget.cpp +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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/Compiler/MappingTarget.h" - -#include -#include - -namespace mlir { - -[[nodiscard]] static constexpr bool -isSwapInvariant(CompilerTarget::GateKind gate) { - using Gate = CompilerTarget::GateKind; - switch (gate) { - case Gate::CZ: - case Gate::ISWAP: - case Gate::RXX: - case Gate::RYY: - case Gate::RZZ: - return true; - default: - return false; - } -} - -MappingTarget::MappingTarget(const CompilerTarget& target) : target_(target) { - const auto basis = target_.synthesisBasis(); - if (!basis || isSwapInvariant(basis->entangler)) { - return; - } - - for (size_t source = 0; source < target_.numSites(); ++source) { - target_.forEachNeighbour(source, [&](size_t targetVertex) { - if (targetVertex < source) { - return; - } - - const auto sourceSite = target_.siteForVertex(source); - const auto targetSite = target_.siteForVertex(targetVertex); - const std::array forwardSites{sourceSite, targetSite}; - const std::array reverseSites{targetSite, sourceSite}; - const bool forward = target_.supports(basis->entangler, forwardSites); - const bool reverse = target_.supports(basis->entangler, reverseSites); - - if (!forward) { - penalizedDirections_.insert( - CompilerTarget::Coupling{sourceSite, targetSite}); - } - if (!reverse) { - penalizedDirections_.insert( - CompilerTarget::Coupling{targetSite, sourceSite}); - } - }); - } -} - -const CompilerTarget& MappingTarget::compilerTarget() const noexcept { - return target_; -} - -size_t MappingTarget::numSites() const noexcept { return target_.numSites(); } - -size_t MappingTarget::maxDegree() const noexcept { return target_.maxDegree(); } - -size_t MappingTarget::distanceBetween(size_t source, size_t target) const { - return target_.distanceBetween(source, target); -} - -void MappingTarget::forEachNeighbour( - size_t vertex, llvm::function_ref callback) const { - target_.forEachNeighbour(vertex, callback); -} - -float MappingTarget::pathCostBetween(size_t source, size_t target) const { - if (source == target) { - return 0.F; - } - const auto distance = target_.distanceBetween(source, target); - if (distance > 1) { - return static_cast(distance - 1); - } - - const CompilerTarget::Coupling coupling{target_.siteForVertex(source), - target_.siteForVertex(target)}; - return penalizedDirections_.contains(coupling) ? 1.F : 0.F; -} - -bool MappingTarget::isExecutable(size_t source, size_t target) const { - return source != target && pathCostBetween(source, target) == 0.F; -} - -} // namespace mlir diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index 69875fb82c..2429007331 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -382,9 +382,8 @@ llvm::Expected CompilerTarget::Operation::create( uniqueApplicableSiteCombinations.emplace_back(sites); } if (llvm::any_of(siteTuples, [&](const auto& siteTuple) { - return llvm::none_of(*applicableSiteTuples, [&](const auto& sites) { - return ArrayRef(sites) == siteTuple.sites(); - }); + return !llvm::is_contained(uniqueApplicableSiteCombinations, + siteTuple.sites()); })) { return invalidTarget("Compiler target operation calibration references " "an inapplicable site tuple"); diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index 894d36ed1b..f98087d1b4 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -234,11 +234,7 @@ LogicalResult NativeOperationAttr::verify( } if (applicability == OperationApplicabilityKind::Explicit && llvm::any_of(siteTuples, [&](const SiteTupleAttr siteTuple) { - return llvm::none_of(applicableSiteTuples, - [&](const ApplicableSiteTupleAttr applicable) { - return applicable.getSites() == - siteTuple.getSites(); - }); + return !llvm::is_contained(seenApplicable, siteTuple.getSites()); })) { return emitError() << "compiler target operation calibration references " "an inapplicable site tuple"; diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index e7e7a189d6..3c3a81affc 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -10,7 +10,7 @@ #include "mlir/Dialect/QCO/Transforms/Mapping/Mapping.h" -#include "mlir/Compiler/MappingTarget.h" +#include "mlir/Compiler/Target.h" #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" @@ -438,7 +438,7 @@ struct MappingPass : impl::MappingPassBase { /// Construct a non-root node from its parent node. Apply the given swap to /// the layout of the parent node. Node(Node* parent, const IndexPairType& swap, const Window& window, - const MappingTarget& target, const Parameters& params) + const CompilerTarget& target, const Parameters& params) : layout(parent->layout), swap(swap), parent(parent), depth(parent->depth + 1), f(0) { layout.swap(swap.first, swap.second); @@ -448,10 +448,10 @@ struct MappingPass : impl::MappingPassBase { /// Return true, if the current SWAP sequence makes all gates in the front /// executable. [[nodiscard]] bool isGoal(const IndexPairType& front, - const MappingTarget& target) const { + const CompilerTarget& target) const { const auto [hw0, hw1] = layout.getHardwareIndices(front.first, front.second); - return target.isExecutable(hw0, hw1); + return target.areAdjacent(hw0, hw1); } private: @@ -468,14 +468,16 @@ struct MappingPass : impl::MappingPassBase { /// 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(const Window& window, const MappingTarget& target, + [[nodiscard]] float h(const Window& window, const CompilerTarget& target, const Parameters& params) const { float costs{0}; float decay{1.}; - for (const auto& [prog0, prog1] : window) { + for (const auto& [i, progs] : enumerate(window)) { + const auto [prog0, prog1] = progs; const auto [hw0, hw1] = layout.getHardwareIndices(prog0, prog1); - costs += decay * target.pathCostBetween(hw0, hw1); + const size_t nswaps = target.distanceBetween(hw0, hw1) - 1; + costs += decay * static_cast(nswaps); decay *= params.lambda; } return costs; @@ -484,7 +486,7 @@ struct MappingPass : impl::MappingPassBase { /// Describes the graph F of arXiv:1602.05150v3. struct FGraph { - explicit FGraph(const MappingTarget& target) + explicit FGraph(const CompilerTarget& target) : f_(llvm::to_vector(llvm::seq(target.numSites()))), target_(&target) {}; @@ -551,7 +553,7 @@ struct MappingPass : impl::MappingPassBase { } Graph f_; - const MappingTarget* target_; + const CompilerTarget* target_; }; public: @@ -578,7 +580,7 @@ struct MappingPass : impl::MappingPassBase { } auto moduleOp = getOperation(); - if (target->compilerTarget().connectivityKind() != + if (target->connectivityKind() != CompilerTarget::Connectivity::Kind::Explicit) { moduleOp.emitError() << "place-and-route requires an explicit target topology"; @@ -600,7 +602,7 @@ struct MappingPass : impl::MappingPassBase { auto computation = discoverComputation(func); if (failed(computation) || - failed(checkCapacity(func, target->compilerTarget(), *computation))) { + failed(checkCapacity(func, *target, *computation))) { signalPassFailure(); return; } @@ -616,8 +618,8 @@ struct MappingPass : impl::MappingPassBase { } IRRewriter rewriter(&getContext()); - std::tie(wires, infos) = std::move(applyPlacement( - body, target->compilerTarget(), *layout, *computation, rewriter)); + std::tie(wires, infos) = std::move( + applyPlacement(body, *target, *layout, *computation, rewriter)); RoutingBundle bundle{.wires = std::move(wires), .infos = std::move(infos), @@ -882,7 +884,7 @@ struct MappingPass : impl::MappingPassBase { constexpr size_t cap = 25'000'000UL; const size_t b = target->maxDegree() * ((target->numSites() + 1) / 2); - const size_t budget = std::max(2, std::min(b * b * b, cap)); + const size_t budget = std::min(b * b * b, cap); const Parameters params{.alpha = alpha, .lambda = lambda}; @@ -890,7 +892,12 @@ struct MappingPass : impl::MappingPassBase { llvm::PriorityQueue, Node::ComparePointer> frontier; + // Early exit, if the root node is a goal node already. Node* root = std::construct_at(arena.Allocate(), layout); + if (root->isGoal(window.front(), *target)) { + return SmallVector{}; + } + frontier.emplace(root); DenseMap, size_t> bestDepth; @@ -1081,24 +1088,6 @@ struct MappingPass : impl::MappingPassBase { return curr; } - [[nodiscard]] static IndexPairType orderedPrograms(UnitaryOpInterface unitary, - ArrayRef indices, - const Wires& wires, - const WireInfos& infos) { - assert(unitary.getNumQubits() == 2 && indices.size() == 2 && - "expected a ready two-qubit operation"); - const bool reversed = - wires[indices.front()].qubit() == unitary.getOutputQubit(1); - assert(wires[indices.front()].qubit() == - unitary.getOutputQubit(reversed ? 1 : 0) && - wires[indices.back()].qubit() == - unitary.getOutputQubit(reversed ? 0 : 1) && - "ready wires do not match operation results"); - const IndexPairType programs{infos.lookupProgram(indices.front()), - infos.lookupProgram(indices.back())}; - return reversed ? IndexPairType{programs.second, programs.first} : programs; - } - /// Collect a routing lookahead window of up to `1 + nlookahead` ready /// two-qubit gates, while skipping qubit-pair blocks. template @@ -1120,8 +1109,7 @@ struct MappingPass : impl::MappingPassBase { if (released.empty()) { for (const auto& [op, indices] : frontier) { - if (auto unitary = dyn_cast(op); - !isa(op) && unitary) { + if (!isa(op) && isa(op)) { const auto i0 = indices[0]; const auto i1 = indices[1]; const auto prog0 = infos.lookupProgram(i0); @@ -1129,8 +1117,7 @@ struct MappingPass : impl::MappingPassBase { const IndexPairType gate = std::minmax(prog0, prog1); if (!is_contained(prev, gate)) { - window.emplace_back( - orderedPrograms(unitary, indices, wires, infos)); + window.emplace_back(gate); if (window.size() == 1 + nlookahead) { return WalkResult::interrupt(); } @@ -1215,18 +1202,17 @@ struct MappingPass : impl::MappingPassBase { const auto release = TypeSwitch(op) .Case([](auto&) { return true; }) - .template Case( - [&](UnitaryOpInterface unitary) { - if (indices.size() == 1) { - return true; - } + .template Case([&](auto&) { + if (indices.size() == 1) { + return true; + } - const auto [prog0, prog1] = - orderedPrograms(unitary, indices, wires, infos); - const auto [hw0, hw1] = - layout.getHardwareIndices(prog0, prog1); - return target->isExecutable(hw0, hw1); - }) + const auto prog0 = infos.lookupProgram(indices[0]); + const auto prog1 = infos.lookupProgram(indices[1]); + const auto [hw0, hw1] = + layout.getHardwareIndices(prog0, prog1); + return target->areAdjacent(hw0, hw1); + }) .template Case([](auto&) { return true; }) .template Case([](MeasureOp& m) { if (Direction == WireDirection::Backward) { @@ -1739,7 +1725,7 @@ struct MappingPass : impl::MappingPassBase { return stats; } - std::optional target; + std::optional target; }; } // namespace diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 7a7b68e2e7..6ba41d288d 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -303,7 +304,7 @@ static bool fuseTwoQubitGateRun(IRRewriter& rewriter, UnitaryOpInterface head, namespace { using SiteId = CompilerTarget::SiteId; -using SiteMap = DenseMap>; +using SiteMap = DenseMap; } // namespace @@ -312,137 +313,116 @@ static SmallVector getQubitValues(ValueRange values) { values, [](Value value) { return isa(value.getType()); })); } -static bool joinSite(Value value, std::optional site, SiteMap& sites) { - const auto [position, inserted] = sites.try_emplace(value, site); - if (inserted || !position->second) { - return inserted; - } - if (!site || *position->second != *site) { - position->second.reset(); - return true; +/// Propagate exact sites, rejecting unknown inputs or inconsistent joins. +static LogicalResult propagateSites(ValueRange inputs, ValueRange outputs, + SiteMap& sites) { + auto inputQubits = getQubitValues(inputs); + auto outputQubits = getQubitValues(outputs); + if (inputQubits.size() != outputQubits.size()) { + return failure(); } - return false; -} - -static bool propagateSites(ValueRange inputs, ValueRange outputs, - SiteMap& sites) { - bool changed = false; - const auto inputQubits = getQubitValues(inputs); - const auto outputQubits = getQubitValues(outputs); - for (const auto [input, output] : - llvm::zip_equal(inputQubits, outputQubits)) { - if (const auto found = sites.find(input); found != sites.end()) { - changed |= joinSite(output, found->second, sites); + for (auto [input, output] : llvm::zip_equal(inputQubits, outputQubits)) { + auto found = sites.find(input); + if (found == sites.end()) { + return failure(); + } + const auto site = found->second; + const auto [position, inserted] = sites.try_emplace(output, site); + if (!inserted && position->second != site) { + return failure(); } } - return changed; -} - -static bool propagateBranchSites(ValueRange inputs, - MutableArrayRef regions, - ValueRange results, SiteMap& sites) { - bool changed = false; - for (Region& region : regions) { - changed |= propagateSites(inputs, region.getArguments(), sites); - changed |= propagateSites(region.front().getTerminator()->getOperands(), - results, sites); - } - return changed; -} - -static bool propagateForSites(scf::ForOp op, SiteMap& sites) { - bool changed = propagateSites(op.getInits(), op.getRegionIterArgs(), sites); - auto yield = cast(op.getBody()->getTerminator()); - changed |= propagateSites(yield.getResults(), op.getRegionIterArgs(), sites); - changed |= propagateSites(op.getRegionIterArgs(), op.getResults(), sites); - return changed; -} - -static bool propagateWhileSites(scf::WhileOp op, SiteMap& sites) { - bool changed = propagateSites(op.getInits(), op.getBeforeArguments(), sites); - auto afterYield = cast(op.getAfterBody()->getTerminator()); - changed |= - propagateSites(afterYield.getResults(), op.getBeforeArguments(), sites); - auto condition = cast(op.getBeforeBody()->getTerminator()); - changed |= propagateSites(condition.getArgs(), op.getAfterArguments(), sites); - changed |= propagateSites(condition.getArgs(), op.getResults(), sites); - return changed; + return success(); } -static SiteMap collectStaticSites(Operation* root) { +/// Visit each region once. Branches must agree and loop backedges must retain +/// the entry sites; neither rule is implied by all-to-all placement. +static FailureOr collectStaticSites(Operation* root) { SiteMap sites; - bool changed = false; - do { - changed = false; - root->walk([&](Operation* operation) { - if (auto staticOp = dyn_cast(operation)) { - changed |= joinSite(staticOp.getQubit(), staticOp.getIndex(), sites); - } else if (auto unitary = dyn_cast(operation)) { - changed |= propagateSites(unitary.getInputQubits(), - unitary.getOutputQubits(), sites); - } else if (auto reset = dyn_cast(operation)) { - changed |= - propagateSites(reset.getQubitIn(), reset.getQubitOut(), sites); - } else if (auto measure = dyn_cast(operation)) { - changed |= - propagateSites(measure.getQubitIn(), measure.getQubitOut(), sites); - } else if (auto ifOp = dyn_cast(operation)) { - changed |= propagateBranchSites(ifOp.getQubits(), ifOp->getRegions(), - ifOp.getResults(), sites); - } else if (auto switchOp = dyn_cast(operation)) { - changed |= - propagateBranchSites(switchOp.getTargets(), switchOp->getRegions(), - switchOp.getResults(), sites); - } else if (auto forOp = dyn_cast(operation)) { - changed |= propagateForSites(forOp, sites); - } else if (auto whileOp = dyn_cast(operation)) { - changed |= propagateWhileSites(whileOp, sites); + auto result = root->walk([&](Operation* operation, const WalkStage& stage) { + const auto propagate = [&](ValueRange inputs, ValueRange outputs) { + if (succeeded(propagateSites(inputs, outputs, sites))) { + return WalkResult::advance(); } - }); - } while (changed); + operation->emitError("target compilation requires known, consistent " + "static sites across branches and loop backedges"); + return WalkResult::interrupt(); + }; + if (auto function = dyn_cast(operation); + function && + llvm::any_of(function.getArgumentTypes(), [](const auto type) { + if (isa(type)) { + return true; + } + const auto tensor = dyn_cast(type); + return tensor && isa(tensor.getElementType()); + })) { + function.emitError() + << "target compilation requires quantum function inputs to be " + "assigned to qco.static target sites"; + return WalkResult::interrupt(); + } + if (isa(operation)) { + operation->emitError() + << "target compilation requires qubits to be assigned to " + "qco.static target sites"; + return WalkResult::interrupt(); + } + if (auto staticOp = dyn_cast(operation)) { + sites.try_emplace(staticOp.getQubit(), staticOp.getIndex()); + } else if (isa(operation)) { + if (propagate(operation->getOperands(), operation->getResults()) + .wasInterrupted()) { + return WalkResult::interrupt(); + } + return WalkResult::skip(); + } else if (isa(operation)) { + if (!stage.isBeforeAllRegions()) { + auto& region = operation->getRegion(stage.getNextRegion() - 1); + auto yielded = region.front().getTerminator()->getOperands(); + if (isa(operation) && + propagate(yielded, region.getArguments()).wasInterrupted()) { + return WalkResult::interrupt(); + } + auto outputs = isa(operation) && stage.isAfterRegion(1) + ? ValueRange(operation->getRegion(0).getArguments()) + : ValueRange(operation->getResults()); + if (propagate(yielded, outputs).wasInterrupted()) { + return WalkResult::interrupt(); + } + } + if (!stage.isAfterAllRegions()) { + auto& region = operation->getRegion(stage.getNextRegion()); + if (!region.hasOneBlock()) { + operation->emitError("target compilation requires single-block " + "structured control flow"); + return WalkResult::interrupt(); + } + auto inputs = + isa(operation) && stage.isBeforeRegion(1) + ? operation->getRegion(0).front().getTerminator()->getOperands() + : operation->getOperands(); + return propagate(inputs, region.getArguments()); + } + } + return WalkResult::advance(); + }); + if (result.wasInterrupted()) { + return failure(); + } return sites; } -static std::optional> -getOperationSites(Operation* operation, const SiteMap& sites) { - SmallVector qubits; - if (auto unitary = dyn_cast(operation)) { - llvm::append_range(qubits, unitary.getInputQubits()); - } else if (auto reset = dyn_cast(operation)) { - qubits.emplace_back(reset.getQubitIn()); - } else if (auto measure = dyn_cast(operation)) { - qubits.emplace_back(measure.getQubitIn()); - } else { - return std::nullopt; - } - +/// Collection has validated the inputs of every unitary, reset, and measure. +static SmallVector getOperationSites(Operation* operation, + const SiteMap& sites) { SmallVector result; - result.reserve(qubits.size()); - for (Value qubit : qubits) { - const auto found = sites.find(qubit); - if (found == sites.end()) { - return std::nullopt; - } - result.emplace_back(found->second.value_or(-1)); + for (Value qubit : getQubitValues(operation->getOperands())) { + result.push_back(sites.at(qubit)); } return result; } -static bool -supportsAtPossibleSites(Operation* operation, const CompilerTarget& target, - const std::optional>& sites) { - if (!sites) { - return target.supports(operation); - } - if (!llvm::is_contained(*sites, SiteId{-1})) { - return target.supports(operation, *sites); - } - return sites->size() == 1 && - llvm::all_of(target.siteIds(), [&](const SiteId site) { - return target.supports(operation, ArrayRef{site}); - }); -} - /// Normalize relative phase effects and discard only the unobservable global /// phase of an entry point when the target cannot represent it. static LogicalResult prepareGlobalPhases(ModuleOp moduleOp, @@ -491,22 +471,22 @@ static FailureOr> planTargetSynthesis( Operation* root, const CompilerTarget& target, const std::optional& targetBasis) { SmallVector plan; - const auto sites = collectStaticSites(root); + auto sites = collectStaticSites(root); + if (failed(sites)) { + return failure(); + } const auto result = root->walk([&](Operation* operation) { auto unitary = dyn_cast(operation); if (!unitary || !isWalkableUnitaryShell(operation) || (unitary.getNumQubits() != 1 && unitary.getNumQubits() != 2)) { return WalkResult::advance(); } - auto operationSites = getOperationSites(operation, sites); - if (supportsAtPossibleSites(operation, target, operationSites)) { + auto operationSites = getOperationSites(operation, *sites); + if (target.supports(operation, operationSites)) { return WalkResult::advance(); } - const bool sitesKnown = - !operationSites || !llvm::is_contained(*operationSites, SiteId{-1}); - if (operationSites && unitary.isTwoQubit() && sitesKnown && - isOperandSwapInvariant(unitary)) { - const std::array reverseSites{(*operationSites)[1], (*operationSites)[0]}; + if (unitary.isTwoQubit() && isOperandSwapInvariant(unitary)) { + const std::array reverseSites{operationSites[1], operationSites[0]}; if (target.supports(operation, reverseSites)) { plan.emplace_back( PlannedOperation{.operation = operation, .reorderOperands = true}); @@ -538,16 +518,10 @@ static FailureOr> planTargetSynthesis( << "': the target has no usable synthesis basis"; return WalkResult::interrupt(); } - if (unitary.isTwoQubit() && !sitesKnown) { - operation->emitError() - << "no supported synthesis-basis placement is known for its " - "static sites"; - return WalkResult::interrupt(); - } bool reverseEntangler = false; - if (operationSites && unitary.isTwoQubit() && - !target.supports(targetBasis->entangler, *operationSites)) { - const std::array reverseSites{(*operationSites)[1], (*operationSites)[0]}; + if (unitary.isTwoQubit() && + !target.supports(targetBasis->entangler, operationSites)) { + const std::array reverseSites{operationSites[1], operationSites[0]}; if (!target.supports(targetBasis->entangler, reverseSites)) { operation->emitError() << "no supported synthesis-basis placement is known for its " @@ -605,15 +579,8 @@ static void lowerTargetOperation(IRRewriter& rewriter, UnitaryOpInterface op, Matrix4x4 matrix; assignTwoQubitOpMatrix(operation, matrix); - Value input0; - Value input1; - if (auto ctrl = dyn_cast(operation)) { - input0 = ctrl.getInputControl(0); - input1 = ctrl.getInputTarget(0); - } else { - input0 = op.getInputQubit(0); - input1 = op.getInputQubit(1); - } + Value input0 = op.getInputQubit(0); + Value input1 = op.getInputQubit(1); if (reverseEntangler) { matrix = matrix.reorderForQubits(1, 0); @@ -700,20 +667,6 @@ struct TargetNativeSynthesisPass final } ModuleOp moduleOp = getOperation(); const auto targetBasis = target.synthesisBasis(); - bool hasGlobalPhases = false; - moduleOp.walk([&](GPhaseOp) { hasGlobalPhases = true; }); - - if (hasGlobalPhases) { - OwningOpRef normalized = cast(moduleOp->clone()); - if (failed(prepareGlobalPhases(*normalized, target))) { - signalPassFailure(); - return; - } - if (failed(planTargetSynthesis(*normalized, target, targetBasis))) { - signalPassFailure(); - return; - } - } if (failed(prepareGlobalPhases(moduleOp, target))) { signalPassFailure(); return; @@ -754,22 +707,12 @@ struct VerifyTargetConformancePass final protected: void runOnOperation() override { - const auto sites = collectStaticSites(getOperation()); + auto sites = collectStaticSites(getOperation()); + if (failed(sites)) { + signalPassFailure(); + return; + } WalkResult result = getOperation()->walk([&](Operation* operation) { - if (auto function = dyn_cast(operation); - function && - llvm::any_of(function.getArgumentTypes(), [](const auto type) { - if (isa(type)) { - return true; - } - const auto tensor = dyn_cast(type); - return tensor && isa(tensor.getElementType()); - })) { - function.emitError() - << "target conformance requires quantum function inputs to be " - "assigned to qco.static target sites"; - return WalkResult::interrupt(); - } if (auto staticOp = dyn_cast(operation)) { const auto site = static_cast(staticOp.getIndex()); @@ -779,12 +722,6 @@ struct VerifyTargetConformancePass final staticOp.emitError() << "target does not contain static site " << site; return WalkResult::interrupt(); } - if (isa(operation)) { - operation->emitError() - << "target conformance requires qubits to be assigned to " - "qco.static target sites"; - return WalkResult::interrupt(); - } size_t arity = 1; size_t parameterCount = 0; @@ -798,8 +735,8 @@ struct VerifyTargetConformancePass final return WalkResult::advance(); } - const auto operationSites = getOperationSites(operation, sites); - if (supportsAtPossibleSites(operation, target, operationSites)) { + auto operationSites = getOperationSites(operation, *sites); + if (target.supports(operation, operationSites)) { return WalkResult::advance(); } diff --git a/mlir/unittests/Compiler/CMakeLists.txt b/mlir/unittests/Compiler/CMakeLists.txt index 63fbcac678..398f6489b3 100644 --- a/mlir/unittests/Compiler/CMakeLists.txt +++ b/mlir/unittests/Compiler/CMakeLists.txt @@ -6,9 +6,8 @@ # # Licensed under the MIT License -add_executable( - mqt-core-mlir-unittests-compiler test_compiler_pipeline.cpp test_compiler_qdmi_adapter.cpp - test_compiler_target.cpp test_mapping_target.cpp) +add_executable(mqt-core-mlir-unittests-compiler + test_compiler_pipeline.cpp test_compiler_qdmi_adapter.cpp test_compiler_target.cpp) target_link_libraries( mqt-core-mlir-unittests-compiler diff --git a/mlir/unittests/Compiler/test_mapping_target.cpp b/mlir/unittests/Compiler/test_mapping_target.cpp deleted file mode 100644 index b7dbc89c13..0000000000 --- a/mlir/unittests/Compiler/test_mapping_target.cpp +++ /dev/null @@ -1,136 +0,0 @@ -/* - * 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/Compiler/MappingTarget.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace mqt::test::compiler { - -using Target = mlir::CompilerTarget; -using Connectivity = Target::Connectivity; -using GateKind = Target::GateKind; -using MappingTarget = mlir::MappingTarget; -using NativeOperations = Target::NativeOperations; -using Operation = Target::Operation; -using SiteId = Target::SiteId; - -template -[[nodiscard]] static T validMappingValue(llvm::Expected value) { - return llvm::cantFail(std::move(value)); -} - -[[nodiscard]] static Operation globalUMappingOperation() { - return validMappingValue(Operation::create("u", 1, 3)); -} - -[[nodiscard]] static Operation -oneWayMappingGate(std::string name, size_t numParameters, - std::vector> applicableSiteTuples) { - return validMappingValue(Operation::create(std::move(name), 2, numParameters, - {}, std::nullopt, std::nullopt, - std::move(applicableSiteTuples))); -} - -TEST(MappingTargetTest, CachesDirectionalCostsOnExplicitTopology) { - const auto target = validMappingValue( - Target::create(3, Connectivity::fromCouplings({{0, 1}, {1, 2}}), - NativeOperations::fromOperations( - {globalUMappingOperation(), - oneWayMappingGate("cx", 0, {{1, 0}, {1, 2}})}))); - - const MappingTarget mappingTarget(target); - EXPECT_EQ(mappingTarget.compilerTarget().sites().data(), - target.sites().data()); - EXPECT_EQ(mappingTarget.numSites(), 3); - EXPECT_EQ(mappingTarget.maxDegree(), 2); - EXPECT_EQ(mappingTarget.distanceBetween(0, 2), 2); - std::vector neighbours; - mappingTarget.forEachNeighbour( - 1, [&](size_t neighbour) { neighbours.emplace_back(neighbour); }); - EXPECT_EQ(neighbours, (std::vector{0, 2})); - EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(0, 1), 1.F); - EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(1, 0), 0.F); - EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(1, 2), 0.F); - EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(2, 1), 1.F); - EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(0, 2), 1.F); - EXPECT_FALSE(mappingTarget.isExecutable(0, 1)); - EXPECT_TRUE(mappingTarget.isExecutable(1, 0)); - EXPECT_FALSE(mappingTarget.isExecutable(1, 1)); -} - -TEST(MappingTargetTest, TreatsSwapInvariantEntanglersAsBidirectional) { - struct Gate { - GateKind kind; - std::string_view name; - size_t numParameters; - }; - constexpr std::array gates{ - Gate{GateKind::CZ, "cz", 0}, Gate{GateKind::ISWAP, "iswap", 0}, - Gate{GateKind::RXX, "rxx", 1}, Gate{GateKind::RYY, "ryy", 1}, - Gate{GateKind::RZZ, "rzz", 1}, - }; - - for (const auto& [kind, name, numParameters] : gates) { - SCOPED_TRACE(name); - const auto target = validMappingValue(Target::create( - 2, Connectivity::fromCouplings({{0, 1}}), - NativeOperations::fromOperations( - {globalUMappingOperation(), - oneWayMappingGate(std::string{name}, numParameters, {{0, 1}})}))); - ASSERT_TRUE(target.synthesisBasis()); - EXPECT_EQ(target.synthesisBasis()->entangler, kind); - - const MappingTarget mappingTarget(target); - EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(0, 1), 0.F); - EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(1, 0), 0.F); - EXPECT_TRUE(mappingTarget.isExecutable(0, 1)); - EXPECT_TRUE(mappingTarget.isExecutable(1, 0)); - } -} - -TEST(MappingTargetTest, KeepsTopologyOnlyTargetsUsable) { - const auto target = validMappingValue( - Target::create(3, Connectivity::fromCouplings({{0, 1}, {1, 2}}), - NativeOperations::fromOperations({}))); - const MappingTarget mappingTarget(target); - - EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(0, 0), 0.F); - EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(0, 1), 0.F); - EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(0, 2), 1.F); - EXPECT_FALSE(mappingTarget.isExecutable(0, 0)); - EXPECT_TRUE(mappingTarget.isExecutable(0, 1)); - EXPECT_FALSE(mappingTarget.isExecutable(0, 2)); -} - -TEST(MappingTargetTest, SupportsImplicitAllToAllTopology) { - const auto target = validMappingValue(Target::create( - 3, Connectivity::allToAll(), - NativeOperations::fromOperations( - {globalUMappingOperation(), - oneWayMappingGate("cx", 0, {{0, 1}, {0, 2}, {1, 2}})}))); - const MappingTarget mappingTarget(target); - - EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(0, 2), 0.F); - EXPECT_FLOAT_EQ(mappingTarget.pathCostBetween(2, 0), 1.F); - EXPECT_TRUE(mappingTarget.isExecutable(0, 2)); - EXPECT_FALSE(mappingTarget.isExecutable(2, 0)); -} - -} // namespace mqt::test::compiler diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index 9a143ad2c7..c7c26d66eb 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -48,13 +48,11 @@ #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -84,8 +82,7 @@ static SmallVector getQubitValues(ValueRange values) { /// constraints. static bool isExecutable(Region& body, DenseMap& m, - const CompilerTarget& target, - const bool requireNativeDirection = false) { + const CompilerTarget& target) { for (Operation& op : body.getOps()) { if (auto staticOp = dyn_cast(op)) { m.try_emplace(staticOp.getQubit(), staticOp.getIndex()); @@ -100,9 +97,7 @@ static bool isExecutable(Region& body, const auto siteB = m.at(unitaryOp.getInputQubit(1)); const auto vertexA = target.vertexForSite(siteA); const auto vertexB = target.vertexForSite(siteB); - if (!vertexA || !vertexB || !target.areAdjacent(*vertexA, *vertexB) || - (requireNativeDirection && isa(op) && - !target.supports(&op, std::array{siteA, siteB}))) { + if (!vertexA || !vertexB || !target.areAdjacent(*vertexA, *vertexB)) { llvm::dbgs() << "The two-qubit gate (" << siteA << ", " << siteB << ") is not executable: \n"; unitaryOp->dump(); @@ -158,7 +153,7 @@ static bool isExecutable(Region& body, localM.try_emplace(arg, hw); } - if (!isExecutable(region, localM, target, requireNativeDirection)) { + if (!isExecutable(region, localM, target)) { return false; } @@ -226,11 +221,9 @@ static bool isExecutable(Region& body, } /// Return true, if the entry point fulfills the given coupling constraints. -static bool isExecutable(func::FuncOp entry, const CompilerTarget& target, - const bool requireNativeDirection = false) { +static bool isExecutable(func::FuncOp entry, const CompilerTarget& target) { DenseMap m; - return isExecutable(entry.getFunctionBody(), m, target, - requireNativeDirection); + return isExecutable(entry.getFunctionBody(), m, target); } /// Return a nxn square-grid compiler target. @@ -440,84 +433,6 @@ TEST_F(MappingPassFixture, EXPECT_TRUE(isa(*measurement.getQubitOut().getUsers().begin())); } -TEST_F(MappingPassFixture, PrefersNativeDirectionWhenRouting) { - using Operation = CompilerTarget::Operation; - const auto target = llvm::cantFail(CompilerTarget::create( - 3, Connectivity::fromCouplings({{0, 1}, {1, 2}}), - NativeOperations::fromOperations( - {llvm::cantFail(Operation::create("u", 1, 3)), - llvm::cantFail(Operation::create( - "cx", 2, 0, {}, std::nullopt, std::nullopt, - std::vector>{{1, 0}, - {1, 2}}))}))); - - QCOProgramBuilder builder(context.get()); - builder.initialize(SmallVector(3, builder.getI1Type())); - SmallVector qubits(3); - SmallVector bits(3); - for (auto& qubit : qubits) { - qubit = builder.allocQubit(); - } - std::tie(qubits[0], qubits[1]) = builder.cx(qubits[0], qubits[1]); - std::tie(qubits[0], qubits[2]) = builder.cx(qubits[0], qubits[2]); - std::tie(qubits[2], qubits[1]) = builder.cx(qubits[2], qubits[1]); - for (size_t i = 0; i < qubits.size(); ++i) { - std::tie(qubits[i], bits[i]) = builder.measure(qubits[i]); - builder.sink(qubits[i]); - } - auto module = builder.finalize(bits); - - ASSERT_TRUE( - runPass(module.get(), target, - MappingPassOptions{.niterations = 1, .ntrials = 1, .seed = 42}) - .succeeded()); - EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target, true)); - - size_t numControls = 0; - size_t numSwaps = 0; - module->walk([&](CtrlOp) { ++numControls; }); - module->walk([&](SWAPOp) { ++numSwaps; }); - EXPECT_EQ(numControls, 3); - EXPECT_EQ(numSwaps, 1); -} - -TEST_F(MappingPassFixture, RoutesOppositeDirectionsOnTwoSites) { - using Operation = CompilerTarget::Operation; - const auto target = llvm::cantFail(CompilerTarget::create( - 2, Connectivity::fromCouplings({{0, 1}}), - NativeOperations::fromOperations( - {llvm::cantFail(Operation::create("u", 1, 3)), - llvm::cantFail(Operation::create( - "cx", 2, 0, {}, std::nullopt, std::nullopt, - std::vector>{{0, 1}}))}))); - - QCOProgramBuilder builder(context.get()); - builder.initialize(SmallVector(2, builder.getI1Type())); - auto q0 = builder.allocQubit(); - auto q1 = builder.allocQubit(); - std::tie(q0, q1) = builder.cx(q0, q1); - std::tie(q1, q0) = builder.cx(q1, q0); - auto [q0Out, b0] = builder.measure(q0); - auto [q1Out, b1] = builder.measure(q1); - builder.sink(q0Out); - builder.sink(q1Out); - auto module = builder.finalize({b0, b1}); - - ASSERT_TRUE( - runPass(module.get(), target, - MappingPassOptions{.niterations = 1, .ntrials = 1, .seed = 42}) - .succeeded()); - - EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target, true)); - - size_t numControls = 0; - size_t numSwaps = 0; - module->walk([&](CtrlOp) { ++numControls; }); - module->walk([&](SWAPOp) { ++numSwaps; }); - EXPECT_EQ(numControls, 2); - EXPECT_EQ(numSwaps, 1); -} - TEST_F(MappingPassFixture, PreserveNoncontiguousTargetSiteIds) { constexpr int64_t size = 3; diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp index c5885690ac..378d476568 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -15,6 +15,8 @@ #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" +#include "mlir/Dialect/QCO/Transforms/Mapping/Mapping.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/DDFunctionality.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" @@ -166,13 +168,14 @@ makeUCxTarget(std::optional> sites = std::nullopt) { NativeOperations::fromOperations(operations))); } -[[nodiscard]] static Target makeOneWayUCxTarget() { +[[nodiscard]] static Target +makeOneWayUCxTarget(Connectivity connectivity = Connectivity::allToAll()) { std::vector operations{valid(Operation::create("u", 1, 3)), valid(Operation::create( "cx", 2, 0, {}, std::nullopt, std::nullopt, std::vector>{{1, 0}})), valid(Operation::create("gphase", 0, 1))}; - return valid(Target::create(2, Connectivity::allToAll(), + return valid(Target::create(2, std::move(connectivity), NativeOperations::fromOperations(operations))); } @@ -444,6 +447,64 @@ TEST_F(TargetSynthesisTest, expectEquivalent(expected, synthesized); } +TEST_F(TargetSynthesisTest, MappingLeavesDirectionRepairToSynthesis) { + auto moduleOp = build([](QCOProgramBuilder& builder) { + auto q0 = builder.allocQubit(); + auto q1 = builder.allocQubit(); + std::tie(q0, q1) = builder.cx(q0, q1); + std::tie(q1, q0) = builder.cx(q1, q0); + return builder.intConstant(0); + }); + const auto target = + makeOneWayUCxTarget(Connectivity::fromCouplings({{0, 1}})); + ASSERT_TRUE(mlir::succeeded( + runPass(*moduleOp, + mlir::qco::createMappingPass( + target, mlir::qco::MappingPassOptions{ + .niterations = 1, .ntrials = 1, .seed = 42})))); + EXPECT_EQ(countOps(*moduleOp), 0U); + auto expected = + mlir::OwningOpRef(mlir::cast(moduleOp->clone())); + ASSERT_TRUE(mlir::succeeded( + runPass(*moduleOp, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_EQ(countOps(*moduleOp), 2U); + ASSERT_TRUE(mlir::succeeded( + runPass(*moduleOp, mlir::qco::createVerifyTargetConformance(target)))); + expectEquivalent(expected, moduleOp); +} + +TEST_F(TargetSynthesisTest, RejectsUnknownSitesWithoutWideningNativeSupport) { + auto moduleOp = mlir::parseSourceString(R"mlir( + module { + func.func @main() { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %r = scf.execute_region -> !qco.qubit { + scf.yield %q0 : !qco.qubit + } + %c, %t = qco.ctrl(%r) targets(%a = %q1) { + %x = qco.x %a : !qco.qubit -> !qco.qubit + qco.yield %x : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) -> ({!qco.qubit}, {!qco.qubit}) + qco.sink %c : !qco.qubit + qco.sink %t : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*moduleOp))); + ASSERT_TRUE(mlir::succeeded(mlir::qco::verifyLinearity(*moduleOp))); + const auto target = makeOneWayUCxTarget(); + for (auto pass : {false, true}) { + const auto diagnostics = expectFailure( + *moduleOp, pass ? mlir::qco::createTargetNativeSynthesis(target) + : mlir::qco::createVerifyTargetConformance(target)); + EXPECT_NE(diagnostics.find("static sites"), std::string::npos); + } +} + TEST_F(TargetSynthesisTest, ConformanceRejectsUnsupportedEntanglerDirection) { auto module = build([](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); @@ -540,7 +601,7 @@ TEST_F(TargetSynthesisTest, } TEST_F(TargetSynthesisTest, - TargetNativeSynthesisHandlesAmbiguousSingleQubitSite) { + TargetNativeSynthesisRejectsAmbiguousSingleQubitSite) { auto module = build([](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); auto q1 = builder.staticQubit(1); @@ -559,12 +620,9 @@ TEST_F(TargetSynthesisTest, }); const auto target = makeOneWayUCxTarget(); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); - EXPECT_EQ(countOps(*module), 0U); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); - ASSERT_TRUE(mlir::succeeded(mlir::verify(*module))); + const auto diagnostics = + expectFailure(*module, mlir::qco::createTargetNativeSynthesis(target)); + EXPECT_NE(diagnostics.find("consistent static sites"), std::string::npos); } TEST_F(TargetSynthesisTest, TargetNativeSynthesisRejectsAmbiguousBranchSites) { @@ -596,13 +654,105 @@ TEST_F(TargetSynthesisTest, TargetNativeSynthesisRejectsAmbiguousBranchSites) { const auto diagnostics = expectFailure( *module, mlir::qco::createTargetNativeSynthesis(makeOneWayUCxTarget())); - EXPECT_NE(diagnostics.find( - "no supported synthesis-basis placement is known for its " - "static sites"), - std::string::npos) + EXPECT_NE(diagnostics.find("consistent static sites"), std::string::npos) << diagnostics; } +TEST_F(TargetSynthesisTest, RejectsLoopCarriedSitePermutations) { + constexpr std::array loops{ + R"mlir( + %r0, %r1 = scf.for %i = %c0 to %n step %c1 + iter_args(%a = %q0, %b = %q1) -> (!qco.qubit, !qco.qubit) { + scf.yield %b, %a : !qco.qubit, !qco.qubit + } + )mlir", + R"mlir( + %r0, %r1 = scf.while (%a = %q0, %b = %q1) + : (!qco.qubit, !qco.qubit) -> (!qco.qubit, !qco.qubit) { + scf.condition(%continue) %a, %b : !qco.qubit, !qco.qubit + } do { + ^bb0(%a: !qco.qubit, %b: !qco.qubit): + scf.yield %b, %a : !qco.qubit, !qco.qubit + } + )mlir"}; + for (const auto* loop : loops) { + const std::string source = std::string{R"mlir( + module { + func.func @main(%n: index, %continue: i1) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + )mlir"} + loop + R"mlir( + qco.sink %r0 : !qco.qubit + qco.sink %r1 : !qco.qubit + return + } + } + )mlir"; + auto moduleOp = mlir::parseSourceString(source, context.get()); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*moduleOp))); + const auto diagnostics = expectFailure( + *moduleOp, + mlir::qco::createTargetNativeSynthesis(makeOneWayUCxTarget())); + EXPECT_NE(diagnostics.find("consistent static sites"), std::string::npos); + } +} + +TEST_F(TargetSynthesisTest, WhileResultsMayDifferFromLoopEntrySites) { + auto moduleOp = mlir::parseSourceString(R"mlir( + module { + func.func @main(%continue: i1) { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %r0, %r1 = scf.while (%a = %q0, %b = %q1) + : (!qco.qubit, !qco.qubit) -> (!qco.qubit, !qco.qubit) { + scf.condition(%continue) %b, %a : !qco.qubit, !qco.qubit + } do { + ^bb0(%a: !qco.qubit, %b: !qco.qubit): + scf.yield %b, %a : !qco.qubit, !qco.qubit + } + %c, %t = qco.ctrl(%r0) targets(%a = %r1) { + %x = qco.x %a : !qco.qubit -> !qco.qubit + qco.yield %x : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) -> ({!qco.qubit}, {!qco.qubit}) + qco.sink %c : !qco.qubit + qco.sink %t : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*moduleOp))); + const auto target = makeOneWayUCxTarget(); + const auto before = printModule(*moduleOp); + ASSERT_TRUE(mlir::succeeded( + runPass(*moduleOp, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_EQ(printModule(*moduleOp), before); + ASSERT_TRUE(mlir::succeeded( + runPass(*moduleOp, mlir::qco::createVerifyTargetConformance(target)))); +} + +TEST_F(TargetSynthesisTest, AcceptsMatchingBranchSitePermutations) { + auto moduleOp = build([](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + const auto swap = [](ValueRange args) { + return mlir::SmallVector{args[1], args[0]}; + }; + auto outputs = builder.qcoIf(true, ValueRange{q0, q1}, swap, swap); + std::tie(q0, q1) = builder.cx(outputs[0], outputs[1]); + return builder.intConstant(0); + }); + const auto target = makeOneWayUCxTarget(); + ASSERT_TRUE(mlir::succeeded( + runPass(*moduleOp, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*moduleOp, mlir::qco::createVerifyTargetConformance(target)))); +} + TEST_F(TargetSynthesisTest, TargetNativeSynthesisRejectsIncompleteGlobalSingleQubitBasis) { auto module = build([](QCOProgramBuilder& builder) { @@ -1060,7 +1210,7 @@ TEST_F(TargetSynthesisTest, } TEST_F(TargetSynthesisTest, - UnsupportedRuntimeParameterizedGateDoesNotPartiallyRewrite) { + UnsupportedRuntimeParameterizedGateWithGlobalPhaseIsDiagnosed) { auto module = mlir::parseSourceString(R"mlir( module { func.func @main(%theta: f64) -> (!qco.qubit, !qco.qubit) { @@ -1076,11 +1226,11 @@ TEST_F(TargetSynthesisTest, )mlir", context.get()); ASSERT_TRUE(module); - const auto before = printModule(*module); - static_cast(expectFailure( - *module, mlir::qco::createTargetNativeSynthesis(makeUCxTarget()))); - EXPECT_EQ(printModule(*module), before); + const auto diagnostics = expectFailure( + *module, mlir::qco::createTargetNativeSynthesis(makeUCxTarget())); + EXPECT_NE(diagnostics.find("unitary matrix is not available"), + std::string::npos); } TEST_F(TargetSynthesisTest, From 136ec92a025048448af2f050a7288e7391014f88 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 4 Sep 2026 10:30:44 +0000 Subject: [PATCH 5/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Unify=20target=20site?= =?UTF-8?q?=20tuples=20and=20simplify=20synthesis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use one list of supported ordered placements with optional calibration. Empty site tuples mean general applicability; omit unavailable QDMI operations. Remove duplicate applicability metadata from C++, MLIR, and Python. Rewrite gates in reverse order to retain collected site facts without an action plan or repeated matrix extraction. Keep the shared guard for unsupported controlled-gate matrix shapes. Validation: 305 C++ tests, 49 Python tests, regenerated stubs, strict docs, and repository lint pass. Whole-file C++ lint reports zero findings in all ten changed sources; its full build remains blocked by unrelated QIR/QTensor linking. Assisted-by: OpenAI Codex --- .agent/plans/directional-gate-mapping.md | 65 ++++-- bindings/mlir/register_mlir.cpp | 38 +--- bindings/patterns.txt | 1 - docs/mlir/target_compilation.md | 18 +- mlir/include/mlir/Compiler/QDMIAdapter.h | 2 +- mlir/include/mlir/Compiler/Target.h | 56 ++---- .../include/mlir/Dialect/MQT/IR/MQTDialect.td | 43 +--- mlir/lib/Compiler/QDMIAdapter.cpp | 50 ++--- mlir/lib/Compiler/Target.cpp | 168 ++++------------ mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 62 +----- .../NativeSynthesis/TargetSynthesis.cpp | 190 +++++++----------- .../Compiler/test_compiler_pipeline.cpp | 9 +- .../Compiler/test_compiler_qdmi_adapter.cpp | 43 ++-- .../Compiler/test_compiler_target.cpp | 119 ++++------- mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp | 103 +++------- .../NativeSynthesis/test_target_synthesis.cpp | 72 +++++-- python/mqt/core/mlir.pyi | 9 +- test/python/test_mlir.py | 28 +-- 18 files changed, 375 insertions(+), 701 deletions(-) diff --git a/.agent/plans/directional-gate-mapping.md b/.agent/plans/directional-gate-mapping.md index 988bbe682b..8a6d03c189 100644 --- a/.agent/plans/directional-gate-mapping.md +++ b/.agent/plans/directional-gate-mapping.md @@ -12,7 +12,7 @@ adjacent sites must not introduce routing SWAPs. ## Progress -- [x] (2026-09-03) Preserve independent applicability and calibration metadata. +- [x] (2026-09-03) Preserve ordered operation support and calibration metadata. - [x] (2026-09-04) Remove directional routing and its dedicated wrapper. - [x] (2026-09-04) Replace ambiguous-site analysis with a checked staged walk. - [x] (2026-09-04) Remove whole-module cloning for failed synthesis. @@ -21,6 +21,10 @@ adjacent sites must not introduce routing SWAPs. - [x] (2026-09-04) Pass 303 focused tests, documentation, and repository lint. - [x] (2026-09-04) Attempt full C++ lint and analyze changed sources directly; record the unrelated build blocker below. +- [x] (2026-09-04) Unify site tuples across the target, attributes, QDMI, and + Python. +- [x] (2026-09-04) Remove synthesis planning and repeated matrix extraction. +- [x] (2026-09-04) Validate the revised model and obtain adversarial review. ## Decision Log @@ -34,9 +38,15 @@ These conditions are checked, including for all-to-all placement and standalone passes. Ordinary structured control flow remains supported. On 2026-09-03 the maintainer approved removing synthesis rollback. Compilation -runs in place; callers must not rely on program contents after failure. -Independent applicability and calibration metadata, generic capability tuples, -and constant-time ordered-pair support queries remain part of the target model. +runs in place; callers must not rely on program contents after failure. Generic +capability tuples and constant-time ordered-pair support queries remain part of +the target model. + +On 2026-09-04 the maintainer approved one `site_tuples` list and no +applicability enum. An empty list means general applicability; a nonempty list +contains every supported ordered placement with optional calibration. Missing +values inherit operation defaults. The QDMI adapter omits operations reported +with no supported placements and retains uncalibrated supported tuples. ## Surprises & Discoveries @@ -54,8 +64,8 @@ because their matrix is unavailable at compile time. `mlir/lib/Compiler/Target.cpp` owns immutable target capabilities and basis selection. A usable synthesis basis supplies one-qubit gates on every site and -an entangler on every routing edge in at least one direction. Applicability is -independent of sparse calibration records. +an entangler on every routing edge in at least one direction. Each supported +site tuple may carry calibration overrides. `mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp` performs placement and routing. `mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp` @@ -89,6 +99,8 @@ Run from the repository root with the configured LLVM/MLIR 23 installation: build/release/mlir/unittests/Dialect/QCO/Transforms/Mapping/mqt-core-mlir-unittest-mapping build/release/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/mqt-core-mlir-unittest-target-synthesis build/release/mlir/unittests/Dialect/MQT/IR/mqt-core-mlir-unittest-mqt-ir + uvx nox -s stubs + SKBUILD_CMAKE_ARGS=-DBUILD_MQT_CORE_QDMI_SC_DEVICE=ON uvx nox -s tests-3.13 -- test/python/test_mlir.py -q uvx nox -s cpp-lint uvx nox --non-interactive -s docs git diff --check @@ -97,8 +109,15 @@ Run from the repository root with the configured LLVM/MLIR 23 installation: Successful output must verify, retain exact ordered target applicability and quantum semantics, and support consistent if/switch/for/while site transfers. Unknown sites, conflicting branch exits, and changing loop-backedge sites must -be diagnosed. Failed compilation need not preserve input IR. No bindings or -capability serialization interfaces change. +be diagnosed. Failed compilation need not preserve input IR. C++, Python, and +serialized targets use only `site_tuples` for ordered availability and +calibration. + +The tuple simplification removes duplicate lists, attributes, and validation +from C++, MLIR, QDMI, and Python. Native synthesis processes users before their +producers, keeping original site facts valid while rewriting each operation +immediately. Use the existing bounded site walk: generic control-flow interfaces +prune known loop edges and require extra exceptions for this contract. ## Idempotence and Recovery @@ -108,19 +127,23 @@ needed. ## Outcomes & Retrospective -Independent specialists and the adversarial reviewer found no blocking defect in -the staged transfer or target contracts. Their feedback removed duplicate -site-lookup validation and added a while-result/backedge regression. Ponytail -Review removed control-specific input selection and duplicate tuple-membership -lambdas. Production code is 250 lines smaller than the reviewed head. - -All 303 focused tests pass: compiler 152, mapping 94, target synthesis 42, and -MQT IR 15. Strict documentation and repository lint pass. Full C++ lint stops -before analysis because unchanged QIR runtime test executables have unresolved -QTensor symbols. Building the changed translation units directly succeeds; -whole-file clang-tidy reports no diagnostics in the 12 changed source files. -Generated/header diagnostics are outside the repository lint scope, and the -binding command produces a locationless macro-parentheses warning. No lint +The target model now has one tuple list with optional calibration. Its enum, +duplicate lists, attributes, validators, and serialization paths are removed. +Synthesis checks and rewrites each gate in reverse order, without a separate +plan or repeated matrix extraction. This round removes 284 production lines. + +Specialist and adversarial review found no remaining blockers. Adversarial +review retained a compact shared matrix guard for unsupported multi-target +control shells; its regression verifies that the input is valid and linear +before checking the diagnostic. Ordinary dependent rewrites retain semantic +equivalence. + +All 305 focused C++ tests pass: compiler 153, mapping 94, target synthesis 43, +and MQT IR 15. All 49 Python MLIR tests pass. Python stubs are regenerated, and +strict documentation and repository lint pass. Full C++ lint stops before +analysis because unchanged QIR runtime test executables have unresolved QTensor +symbols. Building the ten changed C++ translation units directly succeeds; the +same whole-file linter reports zero findings across all ten files. No lint configuration or unrelated build wiring was changed. Revision note: aligned the scope with the approved routing, site, and failure diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 760996c97a..14633d335d 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -530,7 +530,7 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); auto siteTuple = nb::class_( compilerTarget, "SiteTuple", - "Calibration data for an ordered tuple of target sites."); + "A supported ordered placement with optional calibration."); siteTuple .def( "__init__", @@ -591,10 +591,7 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::optional> siteTuples, const std::optional duration, - const std::optional fidelity, - std::optional< - std::vector>> - applicableSiteTuples) { + const std::optional fidelity) { constructFromExpected( self, mlir::CompilerTarget::Operation::create( @@ -602,11 +599,10 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::move(siteTuples) .value_or( std::vector{}), - duration, fidelity, std::move(applicableSiteTuples))); + duration, fidelity)); }, "name"_a, "arity"_a, "num_parameters"_a, "site_tuples"_a = nb::none(), - "duration"_a = nb::none(), "fidelity"_a = nb::none(), - "applicable_site_tuples"_a = nb::none()) + "duration"_a = nb::none(), "fidelity"_a = nb::none()) .def( "__init__", [](mlir::CompilerTarget::Operation& self, std::string name, @@ -614,10 +610,7 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::optional> siteTuples, const std::optional duration, - const std::optional fidelity, - std::optional< - std::vector>> - applicableSiteTuples) { + const std::optional fidelity) { constructFromExpected( self, mlir::CompilerTarget::Operation::create( @@ -625,11 +618,10 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::move(siteTuples) .value_or( std::vector{}), - duration, fidelity, std::move(applicableSiteTuples))); + duration, fidelity)); }, "name"_a, "arity"_a, "num_parameters"_a, "site_tuples"_a = nb::none(), - "duration"_a = nb::none(), "fidelity"_a = nb::none(), - "applicable_site_tuples"_a = nb::none()) + "duration"_a = nb::none(), "fidelity"_a = nb::none()) .def_prop_ro( "name", [](const mlir::CompilerTarget::Operation& operation) { @@ -653,20 +645,8 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); return std::vector( operation.siteTuples().begin(), operation.siteTuples().end()); }, - "Ordered site-specific calibration data.") - .def_prop_ro( - "applicable_site_tuples", - [](const mlir::CompilerTarget::Operation& operation) - -> std::optional< - std::vector>> { - if (!operation.hasExplicitApplicability()) { - return std::nullopt; - } - return std::vector>( - operation.applicableSiteTuples().begin(), - operation.applicableSiteTuples().end()); - }, - "The ordered target-site tuples, or None when unrestricted.") + "Supported ordered placements with optional calibration; empty means " + "general applicability.") .def_prop_ro("duration", &mlir::CompilerTarget::Operation::duration, "The raw default duration, if available.") .def_prop_ro("fidelity", &mlir::CompilerTarget::Operation::fidelity, diff --git a/bindings/patterns.txt b/bindings/patterns.txt index 29183e0568..d2a24897c9 100644 --- a/bindings/patterns.txt +++ b/bindings/patterns.txt @@ -138,7 +138,6 @@ mqt\.core\.mlir\.CompilerTarget\.Operation\.__init__$: site_tuples: Sequence[CompilerTarget.SiteTuple] | None = None, duration: int | None = None, fidelity: float | None = None, - applicable_site_tuples: Sequence[Sequence[int]] | None = None, ) -> None: \doc diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index f5a89aa9aa..90db157085 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -45,7 +45,10 @@ target = CompilerTarget( "cx", arity=2, num_parameters=0, - applicable_site_tuples=[(1, 0), (1, 2)], + site_tuples=[ + CompilerTarget.SiteTuple([1, 0]), + CompilerTarget.SiteTuple([1, 2]), + ], ), CompilerTarget.Operation("measure", arity=1, num_parameters=0), CompilerTarget.Operation("reset", arity=1, num_parameters=0), @@ -63,12 +66,13 @@ not provide a complete connectivity model and a representable native-operation set. An explicit operation arity is either fixed or variadic with a positive, inclusive minimum. Fixed zero represents a global-phase operation. A variadic capability accepts every total width from its minimum through the target's site -count; site-specific calibration tuples are therefore available only for fixed, -positive arities. An omitted `applicable_site_tuples` value makes an operation -available on every valid placement. An explicit list restricts support to those -ordered tuples; it is independent of the sparse calibration entries in -`site_tuples`. Structural and program-format constructs are not compiler-target -operations. +count; site tuples are therefore available only for fixed, positive arities. An +empty `site_tuples` list makes an operation available on every valid placement. +A nonempty list contains all supported ordered placements. Each tuple may carry +calibration values; omitted values inherit the operation-wide defaults. Retain +placements without calibration in this list, and omit operations that are not +available anywhere. Structural and program-format constructs are not +compiler-target operations. Routing uses undirected adjacency; native synthesis repairs unsupported operand directions. Target compilation requires a known static physical site for each diff --git a/mlir/include/mlir/Compiler/QDMIAdapter.h b/mlir/include/mlir/Compiler/QDMIAdapter.h index 62b41baf52..ab3577e6b0 100644 --- a/mlir/include/mlir/Compiler/QDMIAdapter.h +++ b/mlir/include/mlir/Compiler/QDMIAdapter.h @@ -32,7 +32,7 @@ namespace mlir { * zone models are not supported. Explicit QDMI site lists must cover every * site for one-qubit operations, every undirected topology edge for two-qubit * operations, and every ordered tuple of distinct sites for higher arities. - * Their ordered applicability and calibration data are preserved separately. + * Each supported ordered placement carries optional calibration data. */ [[nodiscard]] llvm::Expected compilerTargetFromDevice(const qdmi::Device& device); diff --git a/mlir/include/mlir/Compiler/Target.h b/mlir/include/mlir/Compiler/Target.h index e438397609..1e207fc3e5 100644 --- a/mlir/include/mlir/Compiler/Target.h +++ b/mlir/include/mlir/Compiler/Target.h @@ -125,10 +125,10 @@ class CompilerTarget { std::optional t2_; }; - /// Calibration data for an ordered tuple of hardware sites. + /// One supported ordered placement and its optional calibration data. class SiteTuple { public: - /// Create validated calibration data for a site tuple. + /// Create a validated site tuple with optional calibration overrides. [[nodiscard]] static llvm::Expected create(std::vector sites, std::optional duration = std::nullopt, @@ -137,10 +137,10 @@ class CompilerTarget { /// Return the ordered target site identifiers. [[nodiscard]] llvm::ArrayRef sites() const noexcept; - /// Return the raw operation duration, if available. + /// Return the raw duration override; nullopt uses the operation default. [[nodiscard]] std::optional duration() const noexcept; - /// Return the operation fidelity, if available. + /// Return the fidelity override; nullopt uses the operation default. [[nodiscard]] std::optional fidelity() const noexcept; private: @@ -156,9 +156,9 @@ class CompilerTarget { /// /// The reported name is retained verbatim while /// @ref canonicalName contains its normalized compiler spelling. Operations - /// are available throughout the target unless explicit ordered applicable - /// site tuples restrict their placement. Site tuples carry optional - /// site-specific calibration data independently of applicability. + /// with no site tuples are generally applicable. A nonempty list gives all + /// supported ordered placements. Missing tuple calibration values inherit + /// the operation defaults. class Operation { public: /// The accepted number of qubits for an operation capability. @@ -192,22 +192,18 @@ class CompilerTarget { }; /// Create a validated operation capability. - [[nodiscard]] static llvm::Expected create( - std::string name, size_t arity, size_t numParameters, - std::vector siteTuples = {}, - std::optional duration = std::nullopt, - std::optional fidelity = std::nullopt, - std::optional>> applicableSiteTuples = - std::nullopt); + [[nodiscard]] static llvm::Expected + create(std::string name, size_t arity, size_t numParameters, + std::vector siteTuples = {}, + std::optional duration = std::nullopt, + std::optional fidelity = std::nullopt); /// Create a validated operation capability. - [[nodiscard]] static llvm::Expected create( - std::string name, Arity arity, size_t numParameters, - std::vector siteTuples = {}, - std::optional duration = std::nullopt, - std::optional fidelity = std::nullopt, - std::optional>> applicableSiteTuples = - std::nullopt); + [[nodiscard]] static llvm::Expected + create(std::string name, Arity arity, size_t numParameters, + std::vector siteTuples = {}, + std::optional duration = std::nullopt, + std::optional fidelity = std::nullopt); /// Return the exact reported operation name. [[nodiscard]] llvm::StringRef name() const noexcept; @@ -221,16 +217,9 @@ class CompilerTarget { /// Return the number of real-valued operation parameters. [[nodiscard]] size_t numParameters() const noexcept; - /// Return ordered site-specific calibration data. + /// Return all supported ordered placements, or empty for general support. [[nodiscard]] llvm::ArrayRef siteTuples() const noexcept; - /// Return whether the operation defines explicit ordered applicability. - [[nodiscard]] bool hasExplicitApplicability() const noexcept; - - /// Return the explicitly applicable ordered target-site tuples. - [[nodiscard]] llvm::ArrayRef> - applicableSiteTuples() const noexcept; - /// Return the raw default operation duration, if available. [[nodiscard]] std::optional duration() const noexcept; @@ -238,11 +227,9 @@ class CompilerTarget { [[nodiscard]] std::optional fidelity() const noexcept; private: - Operation( - std::string name, std::string canonicalName, Arity arity, - size_t numParameters, std::vector siteTuples, - std::optional duration, std::optional fidelity, - std::optional>> applicableSiteTuples); + Operation(std::string name, std::string canonicalName, Arity arity, + size_t numParameters, std::vector siteTuples, + std::optional duration, std::optional fidelity); std::string name_; std::string canonicalName_; @@ -251,7 +238,6 @@ class CompilerTarget { std::vector siteTuples_; std::optional duration_; std::optional fidelity_; - std::optional>> applicableSiteTuples_; }; /// Native-operation support. diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td index f684dc5ca3..53dfcbb3d5 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td @@ -84,14 +84,6 @@ def OperationArityKind let genSpecializedAttr = 0; } -def OperationApplicabilityKind - : I32EnumAttr<"OperationApplicabilityKind", "Operation applicability", - [I32EnumAttrCase<"Unrestricted", 0, "unrestricted">, - I32EnumAttrCase<"Explicit", 1, "explicit">]> { - let cppNamespace = "::mlir::mqt"; - let genSpecializedAttr = 0; -} - def DurationUnitAttr : MQTAttr<"DurationUnit", "duration_unit"> { let summary = "Unit for raw compiler-target durations"; let description = [{ @@ -130,10 +122,11 @@ def CouplingAttr : MQTAttr<"Coupling", "coupling"> { } def SiteTupleAttr : MQTAttr<"SiteTuple", "site_tuple"> { - let summary = "Calibration data for an ordered site tuple"; + let summary = "One supported ordered placement with optional calibration"; let description = [{ - The tuple records calibration for one ordered placement without constraining - operation applicability. For example, + The tuple records one supported ordered placement and optional calibration + overrides. Missing calibration values inherit the operation defaults. + For example, `#mqt.site_tuple` records an ordered two-site placement with a raw duration of 40. }]; @@ -144,18 +137,6 @@ def SiteTupleAttr : MQTAttr<"SiteTuple", "site_tuple"> { let genVerifyDecl = 1; } -def ApplicableSiteTupleAttr - : MQTAttr<"ApplicableSiteTuple", "applicable_site_tuple"> { - let summary = "One ordered site tuple supporting an operation"; - let description = [{ - The tuple records one exact ordered placement on which a native operation - is available. Unlike `#mqt.site_tuple`, it carries no calibration data. - }]; - let parameters = (ins MQTArrayRefParameter<"int64_t">:$sites); - let assemblyFormat = "`<` struct(params) `>`"; - let genVerifyDecl = 1; -} - def OperationArityAttr : MQTAttr<"OperationArity", "operation_arity"> { let summary = "Accepted width of a compiler-target operation"; let description = [{ @@ -174,24 +155,21 @@ def NativeOperationAttr : MQTAttr<"NativeOperation", "native_operation"> { let summary = "One native compiler-target operation"; let description = [{ The operation records its spelling, arity, parameter count, and optional - global or site-specific calibration data. Applicability is either - unrestricted or an explicit list of exact ordered site tuples. The - following example records a directional controlled-X operation: + global or site-specific calibration data. An empty site-tuple list means + general applicability; a nonempty list gives all supported ordered + placements. The following example records a directional controlled-X operation: ```mlir #mqt.native_operation, - num_parameters = 0, site_tuples = [], applicability = explicit, - applicable_site_tuples = []> + num_parameters = 0, site_tuples = []> ``` }]; let parameters = (ins "StringAttr":$name, "OperationArityAttr":$arity, "uint64_t":$num_parameters, MQTArrayRefParameter<"SiteTupleAttr">:$site_tuples, MQTOptionalUInt64Parameter<>:$duration, - OptionalParameter<"FloatAttr">:$fidelity, - EnumParameter:$applicability, - MQTArrayRefParameter<"ApplicableSiteTupleAttr">:$applicable_site_tuples); + OptionalParameter<"FloatAttr">:$fidelity); let assemblyFormat = "`<` struct(params) `>`"; let genVerifyDecl = 1; } @@ -213,8 +191,7 @@ def CompilationTargetAttr : MQTAttr<"CompilationTarget", "compilation_target"> { native_operations = explicit, operations = [, - num_parameters = 0, site_tuples = [], applicability = explicit, - applicable_site_tuples = []>]> + num_parameters = 0, site_tuples = []>]> ``` }]; let parameters = (ins OptionalParameter<"StringAttr">:$name, diff --git a/mlir/lib/Compiler/QDMIAdapter.cpp b/mlir/lib/Compiler/QDMIAdapter.cpp index c506e1fbf4..98f495c135 100644 --- a/mlir/lib/Compiler/QDMIAdapter.cpp +++ b/mlir/lib/Compiler/QDMIAdapter.cpp @@ -273,27 +273,13 @@ snapshotDurationUnit(const qdmi::Device& device) { return std::optional(std::move(*durationUnit)); } -namespace { - -struct OperationSiteSnapshot { - std::vector calibration; - std::optional>> applicability; -}; - -} // namespace - -[[nodiscard]] static llvm::Expected +[[nodiscard]] static llvm::Expected> snapshotOperationSites(const qdmi::Operation& operation, size_t arity, const std::vector& flattenedSites, std::optional defaultDuration, - std::optional defaultFidelity, - bool preserveApplicability) { - OperationSiteSnapshot result; - result.calibration.reserve(flattenedSites.size() / arity); - if (preserveApplicability) { - result.applicability.emplace(); - result.applicability->reserve(flattenedSites.size() / arity); - } + std::optional defaultFidelity, bool variadic) { + std::vector result; + result.reserve(flattenedSites.size() / arity); for (size_t offset = 0; offset < flattenedSites.size(); offset += arity) { std::vector sites; std::vector siteIds; @@ -313,18 +299,15 @@ snapshotOperationSites(const qdmi::Operation& operation, size_t arity, const auto fidelity = operation.getFidelity(sites); const bool hasSiteCalibration = duration != defaultDuration || fidelity != defaultFidelity; - if (hasSiteCalibration) { - if (result.applicability) { - result.applicability->emplace_back(siteIds); - } - auto siteTuple = CompilerTarget::SiteTuple::create(std::move(siteIds), - duration, fidelity); + if (!variadic || hasSiteCalibration) { + auto siteTuple = CompilerTarget::SiteTuple::create( + std::move(siteIds), + duration == defaultDuration ? std::nullopt : duration, + fidelity == defaultFidelity ? std::nullopt : fidelity); if (!siteTuple) { return siteTuple.takeError(); } - result.calibration.emplace_back(std::move(*siteTuple)); - } else if (result.applicability) { - result.applicability->emplace_back(std::move(siteIds)); + result.emplace_back(std::move(*siteTuple)); } } return result; @@ -359,6 +342,9 @@ snapshotOperations( return error; } const auto flattenedSites = operation.getSites(); + if (*arity > 0 && flattenedSites && flattenedSites->empty()) { + continue; + } if (auto error = requireRepresentableOperation( *arity == 0 || flattenedSites || homogeneousOperationSupport, deviceName, operation.getName(), @@ -368,8 +354,6 @@ snapshotOperations( const auto duration = operation.getDuration(); const auto fidelity = operation.getFidelity(); std::vector siteTuples; - std::optional>> - applicableSiteTuples; if (*arity == 0) { if (auto error = requireRepresentableOperation( !flattenedSites || flattenedSites->empty(), deviceName, @@ -385,12 +369,11 @@ snapshotOperations( } auto tuples = snapshotOperationSites(operation, *arity, *flattenedSites, duration, - fidelity, !hasArbitraryPositiveControls); + fidelity, hasArbitraryPositiveControls); if (!tuples) { return tuples.takeError(); } - siteTuples = std::move(tuples->calibration); - applicableSiteTuples = std::move(tuples->applicability); + siteTuples = std::move(*tuples); } if (auto error = requireRepresentableOperation( !hasArbitraryPositiveControls || siteTuples.empty(), deviceName, @@ -404,8 +387,7 @@ snapshotOperations( : CompilerTarget::Operation::Arity::fixed(*arity); auto targetOperation = CompilerTarget::Operation::create( operation.getName(), targetArity, operation.getParametersNum(), - std::move(siteTuples), duration, fidelity, - std::move(applicableSiteTuples)); + std::move(siteTuples), duration, fidelity); if (!targetOperation) { return targetOperation.takeError(); } diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index 2429007331..e2607d8056 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -309,18 +309,15 @@ CompilerTarget::Operation::Arity::Arity(Kind kind, size_t value) noexcept llvm::Expected CompilerTarget::Operation::create( std::string name, size_t arity, size_t numParameters, std::vector siteTuples, std::optional duration, - std::optional fidelity, - std::optional>> applicableSiteTuples) { + std::optional fidelity) { return create(std::move(name), Arity::fixed(arity), numParameters, - std::move(siteTuples), duration, fidelity, - std::move(applicableSiteTuples)); + std::move(siteTuples), duration, fidelity); } llvm::Expected CompilerTarget::Operation::create( std::string name, Arity arity, size_t numParameters, std::vector siteTuples, std::optional duration, - std::optional fidelity, - std::optional>> applicableSiteTuples) { + std::optional fidelity) { auto canonicalName = canonicalOperationName(name); if (canonicalName.empty()) { return invalidTarget("Compiler target operation name must not be empty"); @@ -356,54 +353,20 @@ llvm::Expected CompilerTarget::Operation::create( uniqueSiteCombinations.emplace_back(siteTuple.sites()); } - SmallVector> uniqueApplicableSiteCombinations; - if (applicableSiteTuples) { - for (const auto& sites : *applicableSiteTuples) { - if (!arity.accepts(sites.size())) { - return invalidTarget("Compiler target operation applicable site tuple " - "does not match its arity"); - } - std::unordered_set uniqueSites; - for (const auto site : sites) { - if (site < 0) { - return invalidTarget("Compiler target operation applicable site " - "tuple contains a negative site ID"); - } - if (!uniqueSites.insert(site).second) { - return invalidTarget("Compiler target operation applicable site " - "tuple contains a duplicate site"); - } - } - if (llvm::is_contained(uniqueApplicableSiteCombinations, - ArrayRef(sites))) { - return invalidTarget("Compiler target operation contains a duplicate " - "applicable site tuple"); - } - uniqueApplicableSiteCombinations.emplace_back(sites); - } - if (llvm::any_of(siteTuples, [&](const auto& siteTuple) { - return !llvm::is_contained(uniqueApplicableSiteCombinations, - siteTuple.sites()); - })) { - return invalidTarget("Compiler target operation calibration references " - "an inapplicable site tuple"); - } - } return Operation(std::move(name), std::move(canonicalName), arity, - numParameters, std::move(siteTuples), duration, fidelity, - std::move(applicableSiteTuples)); + numParameters, std::move(siteTuples), duration, fidelity); } -CompilerTarget::Operation::Operation( - std::string name, std::string canonicalName, Arity arity, - size_t numParameters, std::vector siteTuples, - std::optional duration, std::optional fidelity, - std::optional>> applicableSiteTuples) +CompilerTarget::Operation::Operation(std::string name, + std::string canonicalName, Arity arity, + size_t numParameters, + std::vector siteTuples, + std::optional duration, + std::optional fidelity) : name_(std::move(name)), canonicalName_(std::move(canonicalName)), arity_(arity), numParameters_(numParameters), siteTuples_(std::move(siteTuples)), duration_(duration), - fidelity_(fidelity), - applicableSiteTuples_(std::move(applicableSiteTuples)) {} + fidelity_(fidelity) {} StringRef CompilerTarget::Operation::name() const noexcept { return name_; } @@ -425,18 +388,6 @@ CompilerTarget::Operation::siteTuples() const noexcept { return siteTuples_; } -bool CompilerTarget::Operation::hasExplicitApplicability() const noexcept { - return applicableSiteTuples_.has_value(); -} - -ArrayRef> -CompilerTarget::Operation::applicableSiteTuples() const noexcept { - if (!applicableSiteTuples_) { - return {}; - } - return *applicableSiteTuples_; -} - std::optional CompilerTarget::Operation::duration() const noexcept { return duration_; } @@ -513,8 +464,8 @@ struct CompilerTarget::Storage { NativeOperations::Kind nativeOperationsKind; SmallVector operations; llvm::StringMap> capabilities; - std::vector>> explicitOneQubitSites; - std::vector>> explicitTwoQubitSites; + std::vector> explicitOneQubitSites; + std::vector> explicitTwoQubitSites; SmallVector supportedGates; std::optional basis; }; @@ -639,32 +590,27 @@ llvm::Error CompilerTarget::Storage::initialize() { return invalidTarget( "Compiler target operation arity exceeds its site count"); } + auto& oneQubitSites = explicitOneQubitSites[index]; + auto& twoQubitSites = explicitTwoQubitSites[index]; + if (!operation.siteTuples().empty()) { + if (operation.arity().value() == 1) { + oneQubitSites.reserve(operation.siteTuples().size()); + } else if (operation.arity().value() == 2) { + twoQubitSites.reserve(operation.siteTuples().size()); + } + } for (const auto& siteTuple : operation.siteTuples()) { - if (llvm::any_of(siteTuple.sites(), [&](const auto site) { + auto tupleSites = siteTuple.sites(); + if (llvm::any_of(tupleSites, [&](const auto site) { return !siteToVertex.contains(site); })) { return invalidTarget("Compiler target operation site tuple " "references an unknown site"); } - } - if (operation.hasExplicitApplicability()) { - auto& oneQubitSites = explicitOneQubitSites[index].emplace(); - auto& twoQubitSites = explicitTwoQubitSites[index].emplace(); - oneQubitSites.reserve(operation.applicableSiteTuples().size()); - twoQubitSites.reserve(operation.applicableSiteTuples().size()); - for (const auto& applicableSites : operation.applicableSiteTuples()) { - if (llvm::any_of(applicableSites, [&](const auto site) { - return !siteToVertex.contains(site); - })) { - return invalidTarget("Compiler target operation applicable site " - "tuple references an unknown site"); - } - if (applicableSites.size() == 1) { - oneQubitSites.insert(applicableSites.front()); - } else if (applicableSites.size() == 2) { - twoQubitSites.insert( - {applicableSites.front(), applicableSites.back()}); - } + if (tupleSites.size() == 1) { + oneQubitSites.insert(tupleSites.front()); + } else if (tupleSites.size() == 2) { + twoQubitSites.insert({tupleSites.front(), tupleSites.back()}); } } capabilities[operation.canonicalName()].emplace_back(index); @@ -710,32 +656,19 @@ bool CompilerTarget::Storage::isApplicable( size_t operationIndex, size_t arity, std::optional> orderedSites) const { const auto& operation = operations[operationIndex]; - if (!operation.hasExplicitApplicability()) { + if (operation.siteTuples().empty() || !orderedSites) { return true; } - if (!orderedSites) { - if (arity == 1) { - return !explicitOneQubitSites[operationIndex]->empty(); - } - if (arity == 2) { - return !explicitTwoQubitSites[operationIndex]->empty(); - } - return llvm::any_of(operation.applicableSiteTuples(), - [&](const auto& applicableSites) { - return applicableSites.size() == arity; - }); - } if (arity == 1) { - return explicitOneQubitSites[operationIndex]->contains((*orderedSites)[0]); + return explicitOneQubitSites[operationIndex].contains((*orderedSites)[0]); } if (arity == 2) { - return explicitTwoQubitSites[operationIndex]->contains( + return explicitTwoQubitSites[operationIndex].contains( {(*orderedSites)[0], (*orderedSites)[1]}); } - return llvm::any_of( - operation.applicableSiteTuples(), [&](const auto& applicableSites) { - return ArrayRef(applicableSites) == *orderedSites; - }); + return llvm::any_of(operation.siteTuples(), [&](const auto& siteTuple) { + return siteTuple.sites() == *orderedSites; + }); } bool CompilerTarget::Storage::supportsOperation( @@ -807,7 +740,7 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { operation.arity().kind() == Operation::Arity::Kind::Variadic) && operation.arity().accepts(arity) && operation.numParameters() == numParameters && - !operation.hasExplicitApplicability(); + operation.siteTuples().empty(); }); }; const auto supportsOnEverySite = [&](GateKind gate) { @@ -1017,21 +950,6 @@ CompilerTarget::create(const mqt::CompilationTargetAttr attribute) { siteTuples.emplace_back(std::move(*siteTuple)); } - std::optional>> applicableSiteTuples; - if (operationAttr.getApplicability() == - mqt::OperationApplicabilityKind::Explicit) { - applicableSiteTuples.emplace(); - applicableSiteTuples->reserve( - operationAttr.getApplicableSiteTuples().size()); - for (const auto tupleAttr : operationAttr.getApplicableSiteTuples()) { - applicableSiteTuples->emplace_back(tupleAttr.getSites().begin(), - tupleAttr.getSites().end()); - } - } else if (!operationAttr.getApplicableSiteTuples().empty()) { - return invalidTarget("Compiler target applicable site tuples require " - "explicit operation applicability"); - } - std::optional fidelity; if (const auto fidelityAttr = operationAttr.getFidelity()) { fidelity = fidelityAttr.getValueAsDouble(); @@ -1045,8 +963,7 @@ CompilerTarget::create(const mqt::CompilationTargetAttr attribute) { auto operation = Operation::create( operationAttr.getName().getValue().str(), arity, static_cast(operationAttr.getNumParameters()), - std::move(siteTuples), operationAttr.getDuration(), fidelity, - std::move(applicableSiteTuples)); + std::move(siteTuples), operationAttr.getDuration(), fidelity); if (!operation) { return operation.takeError(); } @@ -1313,13 +1230,6 @@ CompilerTarget::materialize(MLIRContext& context) const { &context, siteTuple.sites(), siteTuple.duration(), fidelityAttr)); } - SmallVector applicableSiteTupleAttrs; - applicableSiteTupleAttrs.reserve(operation.applicableSiteTuples().size()); - for (const auto& applicableSites : operation.applicableSiteTuples()) { - applicableSiteTupleAttrs.emplace_back( - mqt::ApplicableSiteTupleAttr::get(&context, applicableSites)); - } - FloatAttr fidelityAttr; if (const auto fidelity = operation.fidelity()) { fidelityAttr = builder.getF64FloatAttr(*fidelity); @@ -1330,14 +1240,10 @@ CompilerTarget::materialize(MLIRContext& context) const { : mqt::OperationArityKind::Variadic; const auto arityAttr = mqt::OperationArityAttr::get( &context, arityKind, operation.arity().value()); - const auto applicability = - operation.hasExplicitApplicability() - ? mqt::OperationApplicabilityKind::Explicit - : mqt::OperationApplicabilityKind::Unrestricted; operationAttrs.emplace_back(mqt::NativeOperationAttr::get( &context, builder.getStringAttr(operation.name()), arityAttr, operation.numParameters(), siteTupleAttrs, operation.duration(), - fidelityAttr, applicability, applicableSiteTupleAttrs)); + fidelityAttr)); } const auto connectivity = connectivityKind() == Connectivity::Kind::AllToAll diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index f98087d1b4..5439de92bc 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -142,24 +142,6 @@ SiteTupleAttr::verify(const function_ref emitError, "compiler target site-tuple fidelity"); } -LogicalResult ApplicableSiteTupleAttr::verify( - const function_ref emitError, - ArrayRef sites) { - std::unordered_set seen; - seen.reserve(sites.size()); - for (int64_t site : sites) { - if (site < 0) { - return emitError() << "compiler target applicable site tuple contains a " - "negative site ID"; - } - if (!seen.insert(site).second) { - return emitError() << "compiler target applicable site tuple contains a " - "duplicate site"; - } - } - return success(); -} - LogicalResult OperationArityAttr::verify(const function_ref emitError, const OperationArityKind kind, @@ -175,9 +157,7 @@ LogicalResult NativeOperationAttr::verify( const function_ref emitError, const StringAttr name, const OperationArityAttr arity, const uint64_t /*numParameters*/, const ArrayRef siteTuples, - const std::optional /*duration*/, const FloatAttr fidelity, - OperationApplicabilityKind applicability, - ArrayRef applicableSiteTuples) { + const std::optional /*duration*/, const FloatAttr fidelity) { if (name.getValue().trim().empty()) { return emitError() << "compiler target operation name must not be empty"; } @@ -209,36 +189,6 @@ LogicalResult NativeOperationAttr::verify( seen.emplace_back(siteTuple.getSites()); } - if (applicability != OperationApplicabilityKind::Explicit && - !applicableSiteTuples.empty()) { - return emitError() << "compiler target applicable site tuples require " - "explicit operation applicability"; - } - - SmallVector> seenApplicable; - seenApplicable.reserve(applicableSiteTuples.size()); - for (ApplicableSiteTupleAttr siteTuple : applicableSiteTuples) { - const auto numSites = siteTuple.getSites().size(); - const bool acceptsArity = arity.getKind() == OperationArityKind::Variadic - ? numSites >= arity.getValue() - : numSites == arity.getValue(); - if (!acceptsArity) { - return emitError() << "compiler target operation applicable site tuple " - "does not match its arity"; - } - if (llvm::is_contained(seenApplicable, siteTuple.getSites())) { - return emitError() << "compiler target operation contains a duplicate " - "applicable site tuple"; - } - seenApplicable.emplace_back(siteTuple.getSites()); - } - if (applicability == OperationApplicabilityKind::Explicit && - llvm::any_of(siteTuples, [&](const SiteTupleAttr siteTuple) { - return !llvm::is_contained(seenApplicable, siteTuple.getSites()); - })) { - return emitError() << "compiler target operation calibration references " - "an inapplicable site tuple"; - } return success(); } @@ -307,16 +257,6 @@ LogicalResult CompilationTargetAttr::verify( "an unknown site"; } } - for (ApplicableSiteTupleAttr siteTuple : - operation.getApplicableSiteTuples()) { - if (llvm::any_of(siteTuple.getSites(), [&](const int64_t site) { - return !siteIds.contains(site); - })) { - return emitError() - << "compiler target operation applicable site tuple references " - "an unknown site"; - } - } } const bool hasTiming = diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 6ba41d288d..aa6c8cdf27 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include // IWYU pragma: keep (Passes.h.inc) #include @@ -30,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -82,22 +84,10 @@ static bool isWalkableUnitaryShell(Operation* op) { !isExcludedFromTopLevelUnitaryWalk(op); } -/// Builds the constant 4x4 matrix for a two-qubit op (bare or single-target -/// `CtrlOp`). Returns false for a `CtrlOp` that is not -/// single-control/single-target, or an op whose matrix is not known at compile -/// time. -static bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { - if (auto ctrl = dyn_cast(op)) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - return false; - } - return cast(ctrl.getOperation()) - .getUnitaryMatrix4x4(matrix); - } - auto unitary = cast(op); - assert(unitary.isTwoQubit() && - "only two-qubit unitary shells are passed to assignTwoQubitOpMatrix"); - return unitary.getUnitaryMatrix4x4(matrix); +/// Multi-target control bodies lack a supported operand-to-matrix mapping. +static bool assignTwoQubitOpMatrix(UnitaryOpInterface op, Matrix4x4& matrix) { + return (!isa(op) || op.getNumControls() == 1) && + op.getUnitaryMatrix4x4(matrix); } /// Return the constant matrix when `unitary` is a single-qubit run member. @@ -122,7 +112,7 @@ twoQubitRunMemberMatrix(UnitaryOpInterface unitary) { return std::nullopt; } Matrix4x4 matrix; - if (!assignTwoQubitOpMatrix(unitary.getOperation(), matrix)) { + if (!assignTwoQubitOpMatrix(unitary, matrix)) { return std::nullopt; } return matrix; @@ -445,16 +435,6 @@ static LogicalResult prepareGlobalPhases(ModuleOp moduleOp, return success(); } -namespace { - -struct PlannedOperation { - Operation* operation; - bool reverseEntangler = false; - bool reorderOperands = false; -}; - -} // namespace - static bool isOperandSwapInvariant(UnitaryOpInterface unitary) { Operation* operation = unitary.getOperation(); if (isa(operation)) { @@ -467,78 +447,6 @@ static bool isOperandSwapInvariant(UnitaryOpInterface unitary) { isa(controlled.getBodyUnitary(0).getOperation()); } -static FailureOr> planTargetSynthesis( - Operation* root, const CompilerTarget& target, - const std::optional& targetBasis) { - SmallVector plan; - auto sites = collectStaticSites(root); - if (failed(sites)) { - return failure(); - } - const auto result = root->walk([&](Operation* operation) { - auto unitary = dyn_cast(operation); - if (!unitary || !isWalkableUnitaryShell(operation) || - (unitary.getNumQubits() != 1 && unitary.getNumQubits() != 2)) { - return WalkResult::advance(); - } - auto operationSites = getOperationSites(operation, *sites); - if (target.supports(operation, operationSites)) { - return WalkResult::advance(); - } - if (unitary.isTwoQubit() && isOperandSwapInvariant(unitary)) { - const std::array reverseSites{operationSites[1], operationSites[0]}; - if (target.supports(operation, reverseSites)) { - plan.emplace_back( - PlannedOperation{.operation = operation, .reorderOperands = true}); - return WalkResult::advance(); - } - } - - bool matrixAvailable = false; - if (unitary.isSingleQubit()) { - Matrix2x2 matrix; - matrixAvailable = - unitary.getUnitaryMatrix2x2(matrix) || - decomposition::canSynthesizeParameterizedUnitary1Q(operation); - } else { - Matrix4x4 matrix; - matrixAvailable = assignTwoQubitOpMatrix(operation, matrix); - } - if (!matrixAvailable) { - operation->emitError() - << "target-native synthesis cannot lower operation '" - << operation->getName() - << "': its unitary matrix is not available at compile time"; - return WalkResult::interrupt(); - } - if (!targetBasis) { - operation->emitError() - << "target-native synthesis cannot lower operation '" - << operation->getName() - << "': the target has no usable synthesis basis"; - return WalkResult::interrupt(); - } - bool reverseEntangler = false; - if (unitary.isTwoQubit() && - !target.supports(targetBasis->entangler, operationSites)) { - const std::array reverseSites{operationSites[1], operationSites[0]}; - if (!target.supports(targetBasis->entangler, reverseSites)) { - operation->emitError() - << "no supported synthesis-basis placement is known for its " - "static sites"; - return WalkResult::interrupt(); - } - reverseEntangler = true; - } - plan.emplace_back(PlannedOperation{operation, reverseEntangler}); - return WalkResult::advance(); - }); - if (result.wasInterrupted()) { - return failure(); - } - return plan; -} - static void reorderTwoQubitOperation(IRRewriter& rewriter, UnitaryOpInterface unitary) { IRMapping mapping; @@ -552,21 +460,42 @@ static void reorderTwoQubitOperation(IRRewriter& rewriter, ValueRange{reordered.getOutputQubit(1), reordered.getOutputQubit(0)}); } -static void lowerTargetOperation(IRRewriter& rewriter, UnitaryOpInterface op, - CompilerTarget::SynthesisBasis basis, - bool reverseEntangler) { +static LogicalResult synthesizeTargetOperation( + IRRewriter& rewriter, UnitaryOpInterface op, const CompilerTarget& target, + const std::optional& basis, + ArrayRef sites) { Operation* const operation = op.getOperation(); + if (target.supports(operation, sites)) { + return success(); + } + if (op.isTwoQubit() && isOperandSwapInvariant(op) && + target.supports(operation, std::array{sites[1], sites[0]})) { + reorderTwoQubitOperation(rewriter, op); + return success(); + } + const auto unsupported = [&](StringRef reason) -> LogicalResult { + return operation->emitError() + << "target-native synthesis cannot lower operation '" + << operation->getName() << "': " << reason; + }; + if (!basis) { + return unsupported("the target has no usable synthesis basis"); + } rewriter.setInsertionPoint(operation); if (op.isSingleQubit()) { Matrix2x2 matrix; if (!op.getUnitaryMatrix2x2(matrix)) { + if (!decomposition::canSynthesizeParameterizedUnitary1Q(operation)) { + return unsupported( + "its unitary matrix is not available at compile time"); + } decomposition::synthesizeParameterizedUnitary1Q(rewriter, operation, - basis.singleQubit); - return; + basis->singleQubit); + return success(); } const auto synthesized = decomposition::synthesizeUnitary1QEuler( rewriter, operation->getLoc(), op.getInputQubit(0), matrix, - /*runSize=*/1, /*hasNonBasisGate=*/true, basis.singleQubit); + /*runSize=*/1, /*hasNonBasisGate=*/true, basis->singleQubit); if (!synthesized) { llvm::reportFatalInternalError( "target single-qubit basis failed to synthesize a unitary matrix"); @@ -574,11 +503,20 @@ static void lowerTargetOperation(IRRewriter& rewriter, UnitaryOpInterface op, decomposition::emitGPhaseIfNeeded(rewriter, operation->getLoc(), synthesized->globalPhase); rewriter.replaceOp(operation, synthesized->qubit); - return; + return success(); } Matrix4x4 matrix; - assignTwoQubitOpMatrix(operation, matrix); + if (!assignTwoQubitOpMatrix(op, matrix)) { + return unsupported("its unitary matrix is not available at compile time"); + } + const bool reverseEntangler = !target.supports(basis->entangler, sites); + if (reverseEntangler && + !target.supports(basis->entangler, std::array{sites[1], sites[0]})) { + return operation->emitError() + << "no supported synthesis-basis placement is known for its " + "static sites"; + } Value input0 = op.getInputQubit(0); Value input1 = op.getInputQubit(1); @@ -586,9 +524,9 @@ static void lowerTargetOperation(IRRewriter& rewriter, UnitaryOpInterface op, matrix = matrix.reorderForQubits(1, 0); std::swap(input0, input1); } - const auto native = decomposeUnitary2QWeyl(matrix, basis.entangler); + const auto native = decomposeUnitary2QWeyl(matrix, basis->entangler); const auto synthesized = emitUnitary2QWeyl(rewriter, operation->getLoc(), - input0, input1, native, basis); + input0, input1, native, *basis); decomposition::emitGPhaseIfNeeded(rewriter, operation->getLoc(), synthesized.globalPhase); if (reverseEntangler) { @@ -598,6 +536,7 @@ static void lowerTargetOperation(IRRewriter& rewriter, UnitaryOpInterface op, rewriter.replaceOp(operation, ValueRange{synthesized.qubit0, synthesized.qubit1}); } + return success(); } static LogicalResult fuseTwoQubitGates(ModuleOp moduleOp) { @@ -671,24 +610,31 @@ struct TargetNativeSynthesisPass final signalPassFailure(); return; } - auto plan = planTargetSynthesis(moduleOp, target, targetBasis); - if (failed(plan)) { + auto sites = collectStaticSites(moduleOp); + if (failed(sites)) { signalPassFailure(); return; } - if (plan->empty()) { - return; - } IRRewriter rewriter(&getContext()); - for (const auto& action : *plan) { - auto unitary = cast(action.operation); - if (action.reorderOperands) { - reorderTwoQubitOperation(rewriter, unitary); - } else { - lowerTargetOperation(rewriter, unitary, *targetBasis, - action.reverseEntangler); - } + /// Rewrite users before producers so each unvisited operation retains its + /// original operands and their collected sites. + const auto result = moduleOp->walk( + [&](Operation* operation) { + auto unitary = dyn_cast(operation); + if (!unitary || !isWalkableUnitaryShell(operation) || + (!unitary.isSingleQubit() && !unitary.isTwoQubit())) { + return WalkResult::advance(); + } + return failed(synthesizeTargetOperation( + rewriter, unitary, target, targetBasis, + getOperationSites(operation, *sites))) + ? WalkResult::interrupt() + : WalkResult::advance(); + }); + if (result.wasInterrupted()) { + signalPassFailure(); + return; } if (failed(prepareGlobalPhases(moduleOp, target))) { signalPassFailure(); diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index e6795779ae..1b079367ac 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -1493,10 +1493,11 @@ cx q[1], q[0]; using TargetOperation = CompilerTarget::Operation; using SiteId = CompilerTarget::SiteId; - std::vector operations{llvm::cantFail(TargetOperation::create("u", 1, 3)), - llvm::cantFail(TargetOperation::create( - "cx", 2, 0, {}, std::nullopt, std::nullopt, - std::vector>{{0, 1}}))}; + std::vector operations{ + llvm::cantFail(TargetOperation::create("u", 1, 3)), + llvm::cantFail(TargetOperation::create( + "cx", 2, 0, + {llvm::cantFail(CompilerTarget::SiteTuple::create({0, 1}))}))}; const auto target = llvm::cantFail(CompilerTarget::create( 2, CompilerTarget::Connectivity::fromCouplings({{0, 1}}), CompilerTarget::NativeOperations::fromOperations(operations))); diff --git a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp index f9376e76e1..778d06a8c8 100644 --- a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp +++ b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp @@ -68,16 +68,12 @@ TEST(CompilerQDMIAdapterTest, SnapshotsIQMCalibrationAndLifetime) { EXPECT_EQ(cz.siteTuples().size(), 30); EXPECT_EQ(measure.siteTuples().size(), 20); for (const auto& operation : target.operations()) { - EXPECT_TRUE(operation.hasExplicitApplicability()); EXPECT_FALSE(operation.duration()); for (const auto& tuple : operation.siteTuples()) { EXPECT_FALSE(tuple.duration()); EXPECT_TRUE(tuple.fidelity()); } } - EXPECT_EQ(r.applicableSiteTuples().size(), 20); - EXPECT_EQ(cz.applicableSiteTuples().size(), 30); - EXPECT_EQ(measure.applicableSiteTuples().size(), 20); EXPECT_EQ(target.supportsOperation("r", 1, 2), true); EXPECT_EQ(target.supportsOperation("cz", 2, 0), true); @@ -114,7 +110,7 @@ TEST(CompilerQDMIAdapterTest, InfersDDSIMTargetFacts) { CompilerTarget::Operation::Arity::Kind::Variadic) << name.str(); EXPECT_EQ(operation.arity().value(), minimum) << name.str(); - EXPECT_FALSE(operation.hasExplicitApplicability()) << name.str(); + EXPECT_TRUE(operation.siteTuples().empty()) << name.str(); EXPECT_TRUE( target.supportsOperation(name, minimum, operation.numParameters())) << name.str(); @@ -183,17 +179,38 @@ TEST(CompilerQDMIAdapterTest, PreservesOneWayDirectionalOperationSupport) { ASSERT_EQ(target.couplings().size(), 1U); const auto& cx = findOperation(target, "cx"); - EXPECT_TRUE(cx.hasExplicitApplicability()); - ASSERT_EQ(cx.applicableSiteTuples().size(), 1U); - EXPECT_EQ(cx.applicableSiteTuples()[0], - (std::vector{0, 1})); - EXPECT_TRUE(cx.siteTuples().empty()); + ASSERT_EQ(cx.siteTuples().size(), 1U); + EXPECT_EQ(cx.siteTuples()[0].sites(), + (llvm::ArrayRef{0, 1})); + EXPECT_FALSE(cx.siteTuples()[0].duration()); + EXPECT_FALSE(cx.siteTuples()[0].fidelity()); EXPECT_TRUE(target.supportsOperation("cx", 2, 0, {0, 1})); EXPECT_FALSE(target.supportsOperation("cx", 2, 0, {1, 0})); ASSERT_TRUE(target.synthesisBasis()); EXPECT_EQ(target.synthesisBasis()->entangler, CompilerTarget::GateKind::CX); } +TEST(CompilerQDMIAdapterTest, OmitsOperationsWithNoSupportedPlacements) { + qdmi::DeviceSessionConfig overrides; + overrides.deviceConfiguration = qdmi::InlineDeviceConfiguration{.json = R"({ + "schema-version": 1, + "name": "Unavailable operation", + "numQubits": 1, + "durationUnit": {"unit": "ns", "scaleFactor": 1}, + "qubitProperties": {"defaults": {}, "overrides": []}, + "couplings": [], + "operations": [ + {"name": "x", "numQubits": 1, "numParameters": 0, "sites": []} + ] + })"}; + const auto device = qdmi::Session::openDevice("mqt.sc.default", overrides); + const auto target = llvm::cantFail(mlir::compilerTargetFromDevice(device)); + EXPECT_EQ(target.nativeOperationsKind(), + CompilerTarget::NativeOperations::Kind::Explicit); + EXPECT_TRUE(target.operations().empty()); + EXPECT_FALSE(target.supportsOperation("x", 1, 0, {0})); +} + TEST(CompilerQDMIAdapterTest, PreservesDirectionalCalibrationWhenBothOrientationsExist) { qdmi::DeviceSessionConfig overrides; @@ -204,12 +221,6 @@ TEST(CompilerQDMIAdapterTest, ASSERT_EQ(target.couplings().size(), 1); const auto& cx = findOperation(target, "cx"); - EXPECT_TRUE(cx.hasExplicitApplicability()); - ASSERT_EQ(cx.applicableSiteTuples().size(), 2U); - EXPECT_EQ(cx.applicableSiteTuples()[0], - (std::vector{0, 1})); - EXPECT_EQ(cx.applicableSiteTuples()[1], - (std::vector{1, 0})); EXPECT_TRUE(target.supportsOperation("cx", 2, 0, {0, 1})); EXPECT_TRUE(target.supportsOperation("cx", 2, 0, {1, 0})); ASSERT_EQ(cx.siteTuples().size(), 2); diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index 386a79e0c3..db368731c4 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -71,7 +71,8 @@ TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { std::vector operations; std::vector siteTuples{valid(SiteTuple::create({7}, 0, 0.99)), - valid(SiteTuple::create({2}, 5, 0.98))}; + valid(SiteTuple::create({2}, 5, 0.98)), + valid(SiteTuple::create({11}))}; operations.emplace_back( valid(Operation::create(" PRX ", 1, 2, std::move(siteTuples), 0, 0.97))); @@ -102,7 +103,7 @@ TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { EXPECT_EQ(target.operations()[0].numParameters(), 2); EXPECT_EQ(target.operations()[0].duration(), 0); EXPECT_EQ(target.operations()[0].fidelity(), 0.97); - ASSERT_EQ(target.operations()[0].siteTuples().size(), 2); + ASSERT_EQ(target.operations()[0].siteTuples().size(), 3); EXPECT_EQ(target.operations()[0].siteTuples()[0].duration(), 0); EXPECT_EQ(target.operations()[0].siteTuples()[0].fidelity(), 0.99); @@ -264,27 +265,6 @@ TEST(CompilerTargetTest, RejectsInvalidMetadata) { std::vector{valid(SiteTuple::create({0})), valid(SiteTuple::create({0}))}), "Compiler target operation contains a duplicate site tuple"); - expectInvalid( - Operation::create("x", 1, 0, {}, std::nullopt, std::nullopt, - std::vector>{{-1}}), - "Compiler target operation applicable site tuple contains a negative " - "site ID"); - expectInvalid( - Operation::create("cx", 2, 0, {}, std::nullopt, std::nullopt, - std::vector>{{0, 0}}), - "Compiler target operation applicable site tuple contains a duplicate " - "site"); - expectInvalid( - Operation::create("cx", 2, 0, {}, std::nullopt, std::nullopt, - std::vector>{{0}}), - "Compiler target operation applicable site tuple does not match its " - "arity"); - expectInvalid( - Operation::create("x", 1, 0, std::vector{valid(SiteTuple::create({0}))}, - std::nullopt, std::nullopt, - std::vector>{{1}}), - "Compiler target operation calibration references an inapplicable site " - "tuple"); expectInvalid( Operation::create("x", 1, 0, {}, std::nullopt, std::numeric_limits::quiet_NaN()), @@ -332,13 +312,6 @@ TEST(CompilerTargetTest, RejectsInvalidMetadata) { NativeOperations::fromOperations({valid(Operation::create( "x", 1, 0, std::vector{valid(SiteTuple::create({2}))}))})), "Compiler target operation site tuple references an unknown site"); - expectInvalid( - Target::create(2, Connectivity::allToAll(), - NativeOperations::fromOperations({valid(Operation::create( - "x", 1, 0, {}, std::nullopt, std::nullopt, - std::vector>{{2}}))})), - "Compiler target operation applicable site tuple references an unknown " - "site"); expectInvalid(Target::create(1, Connectivity::allToAll(), NativeOperations::fromOperations( {valid(Operation::create("cx", 2, 0))})), @@ -396,8 +369,11 @@ TEST(CompilerTargetTest, DistinguishesOperationSupport) { TEST(CompilerTargetTest, PreservesCalibrationAndResolvesHomogeneousBasis) { const std::vector chain{{0, 1}, {1, 2}}; const auto globalU = valid(Operation::create("U3", 1, 3)); - const auto cz = valid(Operation::create( - "cz", 2, 0, std::vector{valid(SiteTuple::create({1, 0}, 5, 0.99))})); + const auto cz = valid( + Operation::create("cz", 2, 0, + std::vector{valid(SiteTuple::create({1, 0}, 5, 0.99)), + valid(SiteTuple::create({1, 2}))}, + 7, 0.98)); const auto target = valid(Target::create(3, Connectivity::fromCouplings(chain), NativeOperations::fromOperations({globalU, cz}), @@ -408,11 +384,17 @@ TEST(CompilerTargetTest, PreservesCalibrationAndResolvesHomogeneousBasis) { EXPECT_EQ(target.supports(GateKind::CZ), true); EXPECT_TRUE(llvm::is_contained(target.supportedGates(), GateKind::CZ)); ASSERT_EQ(target.operations().size(), 2U); - ASSERT_EQ(target.operations()[1].siteTuples().size(), 1U); + ASSERT_EQ(target.operations()[1].siteTuples().size(), 2U); EXPECT_EQ(target.operations()[1].siteTuples()[0].sites(), (llvm::ArrayRef{1, 0})); EXPECT_EQ(target.operations()[1].siteTuples()[0].duration(), 5); EXPECT_EQ(target.operations()[1].siteTuples()[0].fidelity(), 0.99); + EXPECT_FALSE(target.operations()[1].siteTuples()[1].duration()); + EXPECT_FALSE(target.operations()[1].siteTuples()[1].fidelity()); + EXPECT_EQ(target.operations()[1].duration(), 7); + EXPECT_EQ(target.operations()[1].fidelity(), 0.98); + EXPECT_TRUE(target.supports(GateKind::CZ, {1, 2})); + EXPECT_FALSE(target.supports(GateKind::CZ, {2, 1})); ASSERT_TRUE(target.synthesisBasis()); EXPECT_EQ(target.synthesisBasis()->singleQubit, Target::SingleQubitBasis::U); EXPECT_EQ(target.synthesisBasis()->entangler, GateKind::CZ); @@ -426,13 +408,11 @@ TEST(CompilerTargetTest, RoundTripsTypedCompilationTargetAttribute) { valid(Site::create(2, std::nullopt, 120, std::nullopt)), valid(Site::create(11, "right"))}; std::vector operations{ - valid(Operation::create( - " PRX ", 1, 2, - std::vector{valid(SiteTuple::create({7}, 0, 0.99)), - valid(SiteTuple::create({2}, 5, 0.98))}, - 0, 0.97, std::vector>{{7}, {2}})), - valid(Operation::create("rx", 1, 1, {}, std::nullopt, std::nullopt, - std::vector>{})), + valid( + Operation::create(" PRX ", 1, 2, + std::vector{valid(SiteTuple::create({7}, 0, 0.99)), + valid(SiteTuple::create({2}, 5, 0.98))}, + 0, 0.97)), valid(Operation::create("gphase", Arity::fixed(0), 1)), valid(Operation::create("h", Arity::variadic(1), 0))}; const auto target = @@ -449,12 +429,10 @@ TEST(CompilerTargetTest, RoundTripsTypedCompilationTargetAttribute) { EXPECT_EQ(reconstructed.supportsOperation("r", 1, 2), true); EXPECT_TRUE(reconstructed.supportsOperation("r", 1, 2, {7})); EXPECT_FALSE(reconstructed.supportsOperation("r", 1, 2, {11})); - EXPECT_TRUE(reconstructed.operations()[1].hasExplicitApplicability()); - EXPECT_TRUE(reconstructed.operations()[1].applicableSiteTuples().empty()); EXPECT_EQ(reconstructed.supportsOperation("gphase", 0, 1), true); EXPECT_EQ(reconstructed.supportsOperation("h", 3, 0), true); - EXPECT_EQ(reconstructed.operations()[2].arity(), Arity::fixed(0)); - EXPECT_EQ(reconstructed.operations()[3].arity(), Arity::variadic(1)); + EXPECT_EQ(reconstructed.operations()[1].arity(), Arity::fixed(0)); + EXPECT_EQ(reconstructed.operations()[2].arity(), Arity::variadic(1)); EXPECT_EQ(reconstructed.synthesisBasis(), target.synthesisBasis()); } @@ -463,13 +441,12 @@ TEST(CompilerTargetTest, SupportsMaximumSiteIds) { constexpr auto nextSite = maxSite - 1; std::vector sites{valid(Site::create(nextSite)), valid(Site::create(maxSite))}; - const auto x = valid(Operation::create( - "x", 1, 0, std::vector{valid(SiteTuple::create({maxSite}))}, std::nullopt, - std::nullopt, std::vector>{{nextSite}, {maxSite}})); + const auto x = valid( + Operation::create("x", 1, 0, + std::vector{valid(SiteTuple::create({nextSite})), + valid(SiteTuple::create({maxSite}))})); const auto cx = valid(Operation::create( - "cx", 2, 0, std::vector{valid(SiteTuple::create({nextSite, maxSite}))}, - std::nullopt, std::nullopt, - std::vector>{{nextSite, maxSite}})); + "cx", 2, 0, std::vector{valid(SiteTuple::create({nextSite, maxSite}))})); const auto target = valid(Target::create(std::move(sites), Connectivity::allToAll(), NativeOperations::fromOperations({x, cx}))); @@ -519,31 +496,25 @@ TEST(CompilerTargetTest, EnforcesExactOrderedOperationApplicability) { std::vector sites{valid(Site::create(10)), valid(Site::create(20)), valid(Site::create(30))}; const auto globalU = valid(Operation::create("u", 1, 3)); - const auto restrictedX = - valid(Operation::create("x", 1, 0, {}, std::nullopt, std::nullopt, - std::vector>{{10}})); - const auto directionalCX = valid( - Operation::create("cx", 2, 0, {}, std::nullopt, std::nullopt, - std::vector>{{10, 20}, {20, 30}})); - const auto exactCZ = - valid(Operation::create("cz", 2, 0, {}, std::nullopt, std::nullopt, - std::vector>{{10, 20}})); - const auto unavailableECR = - valid(Operation::create("ecr", 2, 0, {}, std::nullopt, std::nullopt, - std::vector>{})); - const auto threeQubit = valid(Operation::create( - "device.operation", 3, 0, {}, std::nullopt, std::nullopt, - std::vector>{{10, 20, 30}})); + const auto restrictedX = valid(Operation::create( + "x", 1, 0, std::vector{valid(SiteTuple::create({10}))})); + const auto directionalCX = + valid(Operation::create("cx", 2, 0, + std::vector{valid(SiteTuple::create({10, 20})), + valid(SiteTuple::create({20, 30}))})); + const auto exactCZ = valid(Operation::create( + "cz", 2, 0, std::vector{valid(SiteTuple::create({10, 20}))})); + const auto threeQubit = valid( + Operation::create("device.operation", 3, 0, + std::vector{valid(SiteTuple::create({10, 20, 30}))})); const auto target = valid(Target::create( std::move(sites), Connectivity::fromCouplings({{10, 20}, {20, 30}}), - NativeOperations::fromOperations({globalU, restrictedX, directionalCX, - exactCZ, unavailableECR, threeQubit}))); + NativeOperations::fromOperations( + {globalU, restrictedX, directionalCX, exactCZ, threeQubit}))); - EXPECT_FALSE(globalU.hasExplicitApplicability()); - EXPECT_TRUE(restrictedX.hasExplicitApplicability()); - EXPECT_TRUE(directionalCX.hasExplicitApplicability()); - EXPECT_TRUE(unavailableECR.hasExplicitApplicability()); - EXPECT_TRUE(unavailableECR.applicableSiteTuples().empty()); + EXPECT_TRUE(globalU.siteTuples().empty()); + EXPECT_FALSE(restrictedX.siteTuples().empty()); + EXPECT_FALSE(directionalCX.siteTuples().empty()); EXPECT_TRUE(target.supports(GateKind::U, {30})); EXPECT_TRUE(target.supports(GateKind::X, {10})); @@ -699,9 +670,7 @@ TEST(CompilerTargetTest, SupportsRealQCOOperationsAndStructuralOps) { valid(Operation::create("gphase", 0, 1)), valid(Operation::create("measure", 1, 0)), valid(Operation::create("reset", 1, 0)), - valid(Operation::create("cnot", 2, 0, std::move(directionalTuples), - std::nullopt, std::nullopt, - std::vector>{{10, 20}})), + valid(Operation::create("cnot", 2, 0, std::move(directionalTuples))), valid(Operation::create("cz", 2, 0))}; const auto target = valid(Target::create(std::move(sites), Connectivity::allToAll(), diff --git a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp index 0caf50d500..18f28f3666 100644 --- a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -115,18 +115,14 @@ TEST_F(MQTIRTest, RoundTripsTypedCompilationTarget) { operations = [ , - num_parameters = 0, site_tuples = [], - applicability = explicit, - applicable_site_tuples = []>, + num_parameters = 0, + site_tuples = []>, , - num_parameters = 1, site_tuples = [], - applicability = explicit, applicable_site_tuples = []>, + num_parameters = 1, site_tuples = []>, , - num_parameters = 0, site_tuples = [], - applicability = unrestricted, - applicable_site_tuples = []>]>)mlir")); + num_parameters = 0, site_tuples = []>]>)mlir")); ASSERT_TRUE(compilationTarget); EXPECT_EQ(compilationTarget.getName().getValue(), "device"); ASSERT_EQ(compilationTarget.getSites().size(), 2U); @@ -142,23 +138,12 @@ TEST_F(MQTIRTest, RoundTripsTypedCompilationTarget) { EXPECT_EQ(compilationTarget.getOperations()[1].getArity().getValue(), 0U); EXPECT_EQ(compilationTarget.getOperations()[2].getArity().getKind(), mqt::OperationArityKind::Variadic); - EXPECT_TRUE( - compilationTarget.getOperations().front().getSiteTuples().empty()); - EXPECT_EQ(compilationTarget.getOperations()[0].getApplicability(), - mqt::OperationApplicabilityKind::Explicit); - ASSERT_EQ( - compilationTarget.getOperations()[0].getApplicableSiteTuples().size(), - 1U); - EXPECT_EQ(compilationTarget.getOperations()[0] - .getApplicableSiteTuples()[0] - .getSites(), - (ArrayRef{4, 7})); - EXPECT_EQ(compilationTarget.getOperations()[1].getApplicability(), - mqt::OperationApplicabilityKind::Explicit); - EXPECT_TRUE( - compilationTarget.getOperations()[1].getApplicableSiteTuples().empty()); - EXPECT_EQ(compilationTarget.getOperations()[2].getApplicability(), - mqt::OperationApplicabilityKind::Unrestricted); + ASSERT_EQ(compilationTarget.getOperations()[0].getSiteTuples().size(), 1U); + const auto tuple = compilationTarget.getOperations()[0].getSiteTuples()[0]; + EXPECT_EQ(tuple.getSites(), (ArrayRef{4, 7})); + EXPECT_EQ(tuple.getFidelity().getValueAsDouble(), 0.99); + EXPECT_TRUE(compilationTarget.getOperations()[1].getSiteTuples().empty()); + EXPECT_TRUE(compilationTarget.getOperations()[2].getSiteTuples().empty()); EXPECT_EQ(roundTrip(compilationTarget), compilationTarget); } @@ -172,10 +157,7 @@ TEST_F(MQTIRTest, RoundTripsMaximumSiteIds) { arity = #mqt.operation_arity, num_parameters = 0, site_tuples = [], - applicability = explicit, - applicable_site_tuples = []>]>)mlir"); + 9223372036854775807]>]>]>)mlir"); ASSERT_TRUE(compilationTarget); EXPECT_EQ(roundTrip(compilationTarget), compilationTarget); } @@ -205,10 +187,7 @@ TEST_F(MQTIRTest, RejectsInvalidTargetLeaves) { EXPECT_FALSE(parseAttr(R"mlir(#mqt.site)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.coupling)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.site_tuple)mlir")); - EXPECT_FALSE( - parseAttr(R"mlir(#mqt.applicable_site_tuple)mlir")); - EXPECT_FALSE( - parseAttr(R"mlir(#mqt.applicable_site_tuple)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.site_tuple)mlir")); EXPECT_FALSE( parseAttr(R"mlir(#mqt.site_tuple)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.site_tuple)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 0, site_tuples = [], applicability = unrestricted, - applicable_site_tuples = []>)mlir")); + num_parameters = 0, site_tuples = []>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 1, site_tuples = [], - applicability = unrestricted, applicable_site_tuples = []>)mlir")); + num_parameters = 1, site_tuples = []>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 0, site_tuples = [], - applicability = unrestricted, applicable_site_tuples = []>)mlir")); + num_parameters = 0, site_tuples = []>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 0, site_tuples = [], - applicability = unrestricted, applicable_site_tuples = []>)mlir")); + num_parameters = 0, site_tuples = []>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, num_parameters = 0, - site_tuples = [, ], - applicability = unrestricted, applicable_site_tuples = []>)mlir")); - EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 0, site_tuples = [], duration =>, - applicability = unrestricted, applicable_site_tuples = []>)mlir")); - EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 0, site_tuples = [], applicability = unrestricted, - applicable_site_tuples = []>)mlir")); - EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 0, site_tuples = [], applicability = explicit, - applicable_site_tuples = []>)mlir")); + site_tuples = [, ]>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 0, site_tuples = [], applicability = explicit, - applicable_site_tuples = [, ]>)mlir")); - EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 0, site_tuples = [], - applicability = explicit, - applicable_site_tuples = []>)mlir")); + num_parameters = 0, site_tuples = [], duration =>>)mlir")); } TEST_F(MQTIRTest, RejectsInvalidCompilationTargets) { @@ -278,8 +234,7 @@ TEST_F(MQTIRTest, RejectsInvalidCompilationTargets) { native_operations = unrestricted, operations = [, - num_parameters = 0, site_tuples = [], applicability = unrestricted, - applicable_site_tuples = []>]>)mlir")); + num_parameters = 0, site_tuples = []>]>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.compilation_target< sites = [, ], connectivity = explicit, couplings = [], @@ -296,37 +251,25 @@ TEST_F(MQTIRTest, RejectsInvalidCompilationTargets) { native_operations = explicit, operations = [, - num_parameters = 0, site_tuples = [], - applicability = unrestricted, applicable_site_tuples = []>]>)mlir")); + num_parameters = 0, site_tuples = []>]>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.compilation_target< sites = [], connectivity = all_to_all, couplings = [], native_operations = explicit, operations = [, - num_parameters = 0, site_tuples = [], applicability = unrestricted, - applicable_site_tuples = []>]>)mlir")); + num_parameters = 0, site_tuples = []>]>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.compilation_target< sites = [], connectivity = all_to_all, couplings = [], native_operations = explicit, operations = [, - num_parameters = 0, site_tuples = [], applicability = unrestricted, - applicable_site_tuples = []>]>)mlir")); - EXPECT_FALSE(parseAttr(R"mlir(#mqt.compilation_target< - sites = [], connectivity = all_to_all, couplings = [], - native_operations = explicit, - operations = [, - num_parameters = 0, site_tuples = [], duration = 1, - applicability = unrestricted, applicable_site_tuples = []>]>)mlir")); - + num_parameters = 0, site_tuples = []>]>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.compilation_target< sites = [], connectivity = all_to_all, couplings = [], native_operations = explicit, operations = [, - num_parameters = 0, site_tuples = [], applicability = explicit, - applicable_site_tuples = []>]>)mlir")); + num_parameters = 0, site_tuples = [], duration = 1>]>)mlir")); } TEST_F(MQTIRTest, ManagesAndFindsEntryPoint) { diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp index 378d476568..c4328470da 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -64,6 +64,7 @@ using Connectivity = Target::Connectivity; using NativeOperations = Target::NativeOperations; using Operation = Target::Operation; using Site = Target::Site; +using SiteTuple = Target::SiteTuple; using mlir::ModuleOp; using mlir::OwningOpRef; using mlir::Value; @@ -170,19 +171,17 @@ makeUCxTarget(std::optional> sites = std::nullopt) { [[nodiscard]] static Target makeOneWayUCxTarget(Connectivity connectivity = Connectivity::allToAll()) { - std::vector operations{valid(Operation::create("u", 1, 3)), - valid(Operation::create( - "cx", 2, 0, {}, std::nullopt, std::nullopt, - std::vector>{{1, 0}})), - valid(Operation::create("gphase", 0, 1))}; + std::vector operations{ + valid(Operation::create("u", 1, 3)), + valid(Operation::create("cx", 2, 0, {valid(SiteTuple::create({1, 0}))})), + valid(Operation::create("gphase", 0, 1))}; return valid(Target::create(2, std::move(connectivity), NativeOperations::fromOperations(operations))); } [[nodiscard]] static Target makeOneWayRxxTarget() { std::vector operations{valid( - Operation::create("rxx", 2, 1, {}, std::nullopt, std::nullopt, - std::vector>{{1, 0}}))}; + Operation::create("rxx", 2, 1, {valid(SiteTuple::create({1, 0}))}))}; return valid(Target::create(2, Connectivity::allToAll(), NativeOperations::fromOperations(operations))); } @@ -427,14 +426,16 @@ TEST_F(TargetSynthesisTest, TEST_F(TargetSynthesisTest, TargetNativeSynthesisReversesEntanglerWithoutChangingSemantics) { - const auto forwardCx = [](QCOProgramBuilder& builder) { + const auto circuit = [](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); auto q1 = builder.staticQubit(1); + q0 = builder.h(q0); std::tie(q0, q1) = builder.cx(q0, q1); + q1 = builder.h(q1); return builder.intConstant(0); }; - auto expected = build(forwardCx); - auto synthesized = build(forwardCx); + auto expected = build(circuit); + auto synthesized = build(circuit); const auto before = printModule(*synthesized); const auto target = makeOneWayUCxTarget(); @@ -760,13 +761,11 @@ TEST_F(TargetSynthesisTest, qubit = builder.h(qubit); return builder.intConstant(0); }); - const auto target = valid( - Target::create(2, Connectivity::allToAll(), - NativeOperations::fromOperations( - {valid(Operation::create( - "u", 1, 3, {}, std::nullopt, std::nullopt, - std::vector>{{0}})), - valid(Operation::create("cx", 2, 0))}))); + const auto target = valid(Target::create( + 2, Connectivity::allToAll(), + NativeOperations::fromOperations( + {valid(Operation::create("u", 1, 3, {valid(SiteTuple::create({0}))})), + valid(Operation::create("cx", 2, 0))}))); const auto diagnostics = expectFailure(*module, mlir::qco::createTargetNativeSynthesis(target)); @@ -786,9 +785,9 @@ TEST_F(TargetSynthesisTest, 3, Connectivity::fromCouplings({{0, 1}, {1, 2}}), NativeOperations::fromOperations( {valid(Operation::create("u", 1, 3)), - valid(Operation::create( - "cx", 2, 0, {}, std::nullopt, std::nullopt, - std::vector>{{0, 1}, {1, 2}})), + valid(Operation::create("cx", 2, 0, + {valid(SiteTuple::create({0, 1})), + valid(SiteTuple::create({1, 2}))})), valid(Operation::create("gphase", 0, 1))}))); const auto diagnostics = @@ -1084,6 +1083,39 @@ TEST_F(TargetSynthesisTest, NativePowShellHidesItsImplementationBody) { EXPECT_EQ(printModule(*module), before); } +TEST_F(TargetSynthesisTest, RejectsUnsupportedMultiTargetControlShell) { + auto moduleOp = mlir::parseSourceString(R"mlir( + module { + func.func @main() { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %r0, %r1 = "qco.ctrl"(%q0, %q1) <{ + operandSegmentSizes = array, + resultSegmentSizes = array + }> ({ + ^bb0(%a: !qco.qubit, %b: !qco.qubit): + %c, %t = qco.ctrl(%b) targets(%x = %a) { + %flipped = qco.x %x : !qco.qubit -> !qco.qubit + qco.yield %flipped : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) -> ({!qco.qubit}, {!qco.qubit}) + qco.yield %t, %c : !qco.qubit, !qco.qubit + }) : (!qco.qubit, !qco.qubit) -> (!qco.qubit, !qco.qubit) + qco.sink %r0 : !qco.qubit + qco.sink %r1 : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*moduleOp))); + ASSERT_TRUE(mlir::succeeded(mlir::qco::verifyLinearity(*moduleOp))); + const auto diagnostics = expectFailure( + *moduleOp, mlir::qco::createTargetNativeSynthesis(makeUCxTarget())); + EXPECT_NE(diagnostics.find("unitary matrix is not available"), + std::string::npos); +} + TEST_F(TargetSynthesisTest, MissingBasisIsDiagnosedOnlyWhenLoweringIsNeeded) { const auto hOnly = valid(Target::create( 1, Connectivity::allToAll(), diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index 9d20fb83b6..9f74149ce9 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -140,7 +140,7 @@ class CompilerTarget: """The raw T2 coherence time, if available.""" class SiteTuple: - """Calibration data for an ordered tuple of target sites.""" + """A supported ordered placement with optional calibration.""" def __init__( self, sites: Sequence[int], duration: int | None = None, fidelity: float | None = None @@ -197,7 +197,6 @@ class CompilerTarget: site_tuples: Sequence[CompilerTarget.SiteTuple] | None = None, duration: int | None = None, fidelity: float | None = None, - applicable_site_tuples: Sequence[Sequence[int]] | None = None, ) -> None: ... @property def name(self) -> str: @@ -217,11 +216,7 @@ class CompilerTarget: @property def site_tuples(self) -> list[CompilerTarget.SiteTuple]: - """Ordered site-specific calibration data.""" - - @property - def applicable_site_tuples(self) -> list[list[int]] | None: - """The ordered target-site tuples, or None when unrestricted.""" + """Supported ordered placements with optional calibration; empty means general applicability.""" @property def duration(self) -> int | None: diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index 5037a9c4ac..d56f8dac28 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -456,7 +456,6 @@ def test_compiler_target_constructors_preserve_python_api() -> None: site_tuples=[site_tuple], duration=20, fidelity=0.98, - applicable_site_tuples=[[10, 20]], ) fixed_zero = CompilerTarget.OperationArity.fixed(0) variadic = CompilerTarget.OperationArity.variadic(2) @@ -494,17 +493,9 @@ def test_compiler_target_constructors_preserve_python_api() -> None: assert site_tuple.sites == [10, 20] assert len(operation.site_tuples) == 1 assert operation.site_tuples[0].sites == [10, 20] - assert operation.applicable_site_tuples == [[10, 20]] - assert CompilerTarget.Operation("x", 1, 0).applicable_site_tuples is None - explicitly_unavailable = CompilerTarget.Operation("ecr", 2, 0, applicable_site_tuples=[]) - assert explicitly_unavailable.applicable_site_tuples == [] - explicitly_unavailable_target = CompilerTarget( - 2, - connectivity=connectivity, - native_operations=CompilerTarget.NativeOperations([explicitly_unavailable]), - ) + assert not CompilerTarget.Operation("x", 1, 0).site_tuples assert targets[0].supports_operation("ecr", 2, sites=[0, 1]) - assert not explicitly_unavailable_target.supports_operation("ecr", 2, sites=[0, 1]) + assert not targets[2].supports_operation("ecr", 2, sites=[10, 20]) assert targets[2].supports_operation("cx", 2, sites=[10, 20]) assert not targets[2].supports_operation("cx", 2, sites=[20, 10]) assert operation.arity.kind == CompilerTarget.OperationArityKind.FIXED @@ -554,18 +545,8 @@ def test_compiler_target_construction_preserves_validation_errors() -> None: 0, site_tuples=[CompilerTarget.SiteTuple([0, 1])], ) - with pytest.raises(ValueError, match="applicable site tuple does not match its arity"): - CompilerTarget.Operation("cx", 2, 0, applicable_site_tuples=[[0]]) - with pytest.raises(ValueError, match="applicable site tuple contains a duplicate site"): - CompilerTarget.Operation("cx", 2, 0, applicable_site_tuples=[[0, 0]]) - with pytest.raises(ValueError, match="calibration references an inapplicable site tuple"): - CompilerTarget.Operation( - "cx", - 2, - 0, - site_tuples=[CompilerTarget.SiteTuple([0, 1])], - applicable_site_tuples=[[1, 0]], - ) + with pytest.raises(ValueError, match="site tuple does not match its arity"): + CompilerTarget.Operation("cx", 2, 0, site_tuples=[CompilerTarget.SiteTuple([0])]) def test_compiler_target_snapshots_qdmi_device(garnet_target: CompilerTarget) -> None: @@ -618,7 +599,6 @@ def _compiler_target_metadata(target: CompilerTarget) -> dict[str, object]: operation.num_parameters, operation.duration, operation.fidelity, - operation.applicable_site_tuples, [(site_tuple.sites, site_tuple.duration, site_tuple.fidelity) for site_tuple in operation.site_tuples], ) for operation in target.operations From 9e6696ec0ff8a89ec770982eb88ee839ed0d4663 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 4 Sep 2026 14:59:36 +0000 Subject: [PATCH 6/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20target=20tu?= =?UTF-8?q?ples,=20lookups,=20and=20fusion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Accept plain Python tuples and lists for uncalibrated placements, and print positional sites in MLIR. Keep explicit SiteTuple values for calibration. Use LLVM 23 dense containers and one immutable tuple cache for every arity. Remove redundant factories and lookup helpers. Fuse runs during reverse traversal to avoid snapshots and repeated matrix extraction. Validation: 307 focused C++ tests, 51 Python tests, regenerated stubs, strict docs, and repository lint pass. Whole-file C++ analysis reports zero findings across ten changed sources. The full lint build remains blocked by unrelated QIR/QTensor linking. Assisted-by: OpenAI Codex --- .agent/plans/directional-gate-mapping.md | 38 ++++- bindings/mlir/register_mlir.cpp | 3 + bindings/patterns.txt | 2 +- docs/mlir/target_compilation.md | 9 +- mlir/include/mlir/Compiler/Target.h | 2 - .../include/mlir/Dialect/MQT/IR/MQTDialect.td | 8 +- mlir/lib/Compiler/Target.cpp | 137 ++++-------------- mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 12 +- .../NativeSynthesis/TargetSynthesis.cpp | 31 ++-- .../Compiler/test_compiler_target.cpp | 17 ++- mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp | 41 ++++-- .../NativeSynthesis/test_target_synthesis.cpp | 20 +++ python/mqt/core/mlir.pyi | 2 +- test/python/test_mlir.py | 19 +++ 14 files changed, 173 insertions(+), 168 deletions(-) diff --git a/.agent/plans/directional-gate-mapping.md b/.agent/plans/directional-gate-mapping.md index 8a6d03c189..56b5d2b257 100644 --- a/.agent/plans/directional-gate-mapping.md +++ b/.agent/plans/directional-gate-mapping.md @@ -25,6 +25,11 @@ adjacent sites must not introduce routing SWAPs. Python. - [x] (2026-09-04) Remove synthesis planning and repeated matrix extraction. - [x] (2026-09-04) Validate the revised model and obtain adversarial review. +- [x] (2026-09-04) Accept plain Python placements and positional MLIR tuple + sites. +- [x] (2026-09-04) Consolidate target lookups and restore LLVM containers. +- [ ] (2026-09-04) Validate compact syntax and the final synthesis + simplification. ## Decision Log @@ -48,6 +53,10 @@ contains every supported ordered placement with optional calibration. Missing values inherit operation defaults. The QDMI adapter omits operations reported with no supported placements and retains uncalibrated supported tuples. +Plain Python tuples and lists denote uncalibrated placements. Explicit +`SiteTuple` values remain available for calibration. MLIR prints positional +sites as `<[4, 7]>`, with named optional calibration fields. + ## Surprises & Discoveries An executed two-site probe produced five native CXs with directional routing and @@ -60,6 +69,11 @@ placement only replaces allocations, so site consistency must be checked rather than assumed. Runtime symmetric gates such as RXX need direct operand reordering because their matrix is unavailable at compile time. +LLVM 23 dense maps track occupancy separately and no longer reserve sentinel +keys. Standard LLVM dense containers therefore support the full nonnegative +site-ID range without custom traits. One per-operation tuple set borrows keys +from immutable target storage and replaces arity-specific lookup caches. + ## Context and Orientation `mlir/lib/Compiler/Target.cpp` owns immutable target capabilities and basis @@ -119,6 +133,12 @@ producers, keeping original site facts valid while rewriting each operation immediately. Use the existing bounded site walk: generic control-flow interfaces prune known loop edges and require extra exceptions for this contract. +Fusion also visits operations in reverse order. When a run head fuses its +successors, those operations have already been visited. This removes the +run-head snapshot and duplicate matrix extraction, and can expose earlier +cancellations when a later run disappears. Each rewrite still strictly reduces +the number of two-qubit operations. + ## Idempotence and Recovery Builds and checks are repeatable. Preserve unrelated changes and keep generated @@ -130,7 +150,11 @@ needed. The target model now has one tuple list with optional calibration. Its enum, duplicate lists, attributes, validators, and serialization paths are removed. Synthesis checks and rewrites each gate in reverse order, without a separate -plan or repeated matrix extraction. This round removes 284 production lines. +plan or repeated matrix extraction. The tuple-model round removed 284 production +lines. The compact-syntax and lookup round removes another 91 production lines: +a shared LLVM tuple cache, fewer single-use wrappers, and direct reverse fusion. +Python accepts plain tuples or lists; explicit `SiteTuple` values add +calibration. MLIR prints positional tuple sites. Specialist and adversarial review found no remaining blockers. Adversarial review retained a compact shared matrix guard for unsupported multi-target @@ -138,13 +162,21 @@ control shells; its regression verifies that the input is valid and linear before checking the diagnostic. Ordinary dependent rewrites retain semantic equivalence. -All 305 focused C++ tests pass: compiler 153, mapping 94, target synthesis 43, -and MQT IR 15. All 49 Python MLIR tests pass. Python stubs are regenerated, and +All 307 focused C++ tests pass: compiler 153, mapping 94, target synthesis 44, +and MQT IR 16. All 51 Python MLIR tests pass. Python stubs are regenerated, and strict documentation and repository lint pass. Full C++ lint stops before analysis because unchanged QIR runtime test executables have unresolved QTensor symbols. Building the ten changed C++ translation units directly succeeds; the same whole-file linter reports zero findings across all ten files. No lint configuration or unrelated build wiring was changed. +The final specialist, adversarial, and Ponytail reviews found no further useful +deletion within the supported contract. The new fusion regression cancels +`CX01, CX02, CX02, CX01` and checks decision-diagram equivalence. Compact syntax +checks cover mixed calibrated placements and calibration roundtrips. Cache +checks cover maximum site IDs and retained target copies. The initial Python run +reused a package without the test device; rebuilding with +`BUILD_MQT_CORE_QDMI_SC_DEVICE=ON` resolves both device fixture errors. + Revision note: aligned the scope with the approved routing, site, and failure contracts while retaining exact device metadata. diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 14633d335d..17bbf81098 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -555,6 +555,9 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); .def_prop_ro("fidelity", &mlir::CompilerTarget::SiteTuple::fidelity, "The operation fidelity, if available."); + nb::implicitly_convertible, + mlir::CompilerTarget::SiteTuple>(); + nb::enum_( compilerTarget, "OperationArityKind", "How an operation capability accepts qubit widths.") diff --git a/bindings/patterns.txt b/bindings/patterns.txt index d2a24897c9..46db30f7e4 100644 --- a/bindings/patterns.txt +++ b/bindings/patterns.txt @@ -135,7 +135,7 @@ mqt\.core\.mlir\.CompilerTarget\.Operation\.__init__$: name: str, arity: int | CompilerTarget.OperationArity, num_parameters: int, - site_tuples: Sequence[CompilerTarget.SiteTuple] | None = None, + site_tuples: Sequence[CompilerTarget.SiteTuple | Sequence[int]] | None = None, duration: int | None = None, fidelity: float | None = None, ) -> None: diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 90db157085..c50244c583 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -45,10 +45,7 @@ target = CompilerTarget( "cx", arity=2, num_parameters=0, - site_tuples=[ - CompilerTarget.SiteTuple([1, 0]), - CompilerTarget.SiteTuple([1, 2]), - ], + site_tuples=[(1, 0), (1, 2)], ), CompilerTarget.Operation("measure", arity=1, num_parameters=0), CompilerTarget.Operation("reset", arity=1, num_parameters=0), @@ -74,6 +71,10 @@ placements without calibration in this list, and omit operations that are not available anywhere. Structural and program-format constructs are not compiler-target operations. +Use plain tuples for placements without calibration. Use +`CompilerTarget.SiteTuple([1, 0], duration=40, fidelity=0.99)` to attach +calibration to a placement; both forms can appear in the same list. + Routing uses undirected adjacency; native synthesis repairs unsupported operand directions. Target compilation requires a known static physical site for each qubit. Structured branch exits must agree on sites, and loop backedges must diff --git a/mlir/include/mlir/Compiler/Target.h b/mlir/include/mlir/Compiler/Target.h index 1e207fc3e5..e341e1a9dc 100644 --- a/mlir/include/mlir/Compiler/Target.h +++ b/mlir/include/mlir/Compiler/Target.h @@ -434,8 +434,6 @@ class CompilerTarget { supportsImpl(::mlir::Operation* operation, std::optional> sites) const; - [[nodiscard]] llvm::ArrayRef explicitNeighbours(size_t vertex) const; - std::shared_ptr storage_; }; diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td index 53dfcbb3d5..aa0c0362e0 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td @@ -127,13 +127,13 @@ def SiteTupleAttr : MQTAttr<"SiteTuple", "site_tuple"> { The tuple records one supported ordered placement and optional calibration overrides. Missing calibration values inherit the operation defaults. For example, - `#mqt.site_tuple` records an ordered + `#mqt.site_tuple<[4, 7], duration = 40>` records an ordered two-site placement with a raw duration of 40. }]; let parameters = (ins MQTArrayRefParameter<"int64_t">:$sites, MQTOptionalUInt64Parameter<>:$duration, OptionalParameter<"FloatAttr">:$fidelity); - let assemblyFormat = "`<` struct(params) `>`"; + let assemblyFormat = "`<` $sites (`,` struct($duration, $fidelity)^)? `>`"; let genVerifyDecl = 1; } @@ -162,7 +162,7 @@ def NativeOperationAttr : MQTAttr<"NativeOperation", "native_operation"> { ```mlir #mqt.native_operation, - num_parameters = 0, site_tuples = []> + num_parameters = 0, site_tuples = [<[4, 7]>]> ``` }]; let parameters = (ins "StringAttr":$name, "OperationArityAttr":$arity, @@ -191,7 +191,7 @@ def CompilationTargetAttr : MQTAttr<"CompilationTarget", "compilation_target"> { native_operations = explicit, operations = [, - num_parameters = 0, site_tuples = []>]> + num_parameters = 0, site_tuples = [<[4, 7]>]>]> ``` }]; let parameters = (ins OptionalParameter<"StringAttr">:$name, diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index e2607d8056..627abaef2a 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -14,6 +14,8 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include +#include #include #include #include @@ -37,8 +39,6 @@ #include #include #include -#include -#include #include #include @@ -245,7 +245,7 @@ llvm::Expected CompilerTarget::SiteTuple::create(std::vector sites, std::optional duration, std::optional fidelity) { - std::unordered_set uniqueSites; + llvm::SmallDenseSet uniqueSites; for (const auto site : sites) { if (site < 0) { return invalidTarget( @@ -340,17 +340,16 @@ llvm::Expected CompilerTarget::Operation::create( "Compiler target zero-arity operation cannot contain site tuples"); } - SmallVector> uniqueSiteCombinations; + llvm::SmallDenseSet> uniqueSiteCombinations; for (const auto& siteTuple : siteTuples) { if (!arity.accepts(siteTuple.sites().size())) { return invalidTarget( "Compiler target operation site tuple does not match its arity"); } - if (llvm::is_contained(uniqueSiteCombinations, siteTuple.sites())) { + if (!uniqueSiteCombinations.insert(siteTuple.sites()).second) { return invalidTarget( "Compiler target operation contains a duplicate site tuple"); } - uniqueSiteCombinations.emplace_back(siteTuple.sites()); } return Operation(std::move(name), std::move(canonicalName), arity, @@ -429,33 +428,23 @@ struct CompilerTarget::Storage { SmallVector targetOperations, std::optional targetDurationUnit); - [[nodiscard]] static llvm::Expected> - create(std::optional targetName, std::vector targetSites, - Connectivity::Kind targetConnectivityKind, - SmallVector targetCouplings, - NativeOperations::Kind targetNativeOperationsKind, - SmallVector targetOperations, - std::optional targetDurationUnit); - [[nodiscard]] llvm::Error initialize(); - [[nodiscard]] bool - isApplicable(size_t operationIndex, size_t arity, - std::optional> orderedSites) const; [[nodiscard]] bool supportsOperation(StringRef name, size_t arity, std::optional numParameters, std::optional> orderedSites = std::nullopt, bool variadicOnly = false) const; - [[nodiscard]] bool supportsGate(GateKind gate, - ArrayRef orderedSites) const; + [[nodiscard]] bool supportsGate( + GateKind gate, + std::optional> orderedSites = std::nullopt) const; [[nodiscard]] std::optional resolveSynthesisBasis() const; std::optional name; std::optional durationUnit; std::vector sites; SmallVector siteIds; - std::unordered_map siteToVertex; + llvm::DenseMap siteToVertex; Connectivity::Kind connectivityKind; SmallVector couplings; SmallVector> adjacency; @@ -464,8 +453,8 @@ struct CompilerTarget::Storage { NativeOperations::Kind nativeOperationsKind; SmallVector operations; llvm::StringMap> capabilities; - std::vector> explicitOneQubitSites; - std::vector> explicitTwoQubitSites; + /// Keys borrow the immutable site tuples owned by operations. + std::vector>> operationSites; SmallVector supportedGates; std::optional basis; }; @@ -483,24 +472,6 @@ CompilerTarget::Storage::Storage( nativeOperationsKind(targetNativeOperationsKind), operations(std::move(targetOperations)) {} -llvm::Expected> -CompilerTarget::Storage::create( - std::optional targetName, std::vector targetSites, - Connectivity::Kind targetConnectivityKind, - SmallVector targetCouplings, - NativeOperations::Kind targetNativeOperationsKind, - SmallVector targetOperations, - std::optional targetDurationUnit) { - auto storage = std::make_shared( - std::move(targetName), std::move(targetSites), targetConnectivityKind, - std::move(targetCouplings), targetNativeOperationsKind, - std::move(targetOperations), std::move(targetDurationUnit)); - if (auto error = storage->initialize()) { - return std::move(error); - } - return std::shared_ptr(std::move(storage)); -} - llvm::Error CompilerTarget::Storage::initialize() { if (name && name->empty()) { return invalidTarget("Compiler target name must not be empty when present"); @@ -537,8 +508,8 @@ llvm::Error CompilerTarget::Storage::initialize() { adjacency.resize(sites.size()); for (const auto& [source, target] : couplings) { - const auto sourceVertex = siteToVertex.at(source); - const auto targetVertex = siteToVertex.at(target); + const auto sourceVertex = siteToVertex.lookup(source); + const auto targetVertex = siteToVertex.lookup(target); adjacency[sourceVertex].emplace_back(targetVertex); adjacency[targetVertex].emplace_back(sourceVertex); } @@ -579,8 +550,7 @@ llvm::Error CompilerTarget::Storage::initialize() { } if (nativeOperationsKind == NativeOperations::Kind::Explicit) { - explicitOneQubitSites.resize(operations.size()); - explicitTwoQubitSites.resize(operations.size()); + operationSites.resize(operations.size()); for (const auto [index, operation] : llvm::enumerate(operations)) { if (operation.arity().value() > sites.size()) { if (operation.arity().kind() == Operation::Arity::Kind::Variadic) { @@ -590,15 +560,8 @@ llvm::Error CompilerTarget::Storage::initialize() { return invalidTarget( "Compiler target operation arity exceeds its site count"); } - auto& oneQubitSites = explicitOneQubitSites[index]; - auto& twoQubitSites = explicitTwoQubitSites[index]; - if (!operation.siteTuples().empty()) { - if (operation.arity().value() == 1) { - oneQubitSites.reserve(operation.siteTuples().size()); - } else if (operation.arity().value() == 2) { - twoQubitSites.reserve(operation.siteTuples().size()); - } - } + auto& supportedSites = operationSites[index]; + supportedSites.reserve(operation.siteTuples().size()); for (const auto& siteTuple : operation.siteTuples()) { auto tupleSites = siteTuple.sites(); if (llvm::any_of(tupleSites, [&](const auto site) { @@ -607,11 +570,7 @@ llvm::Error CompilerTarget::Storage::initialize() { return invalidTarget("Compiler target operation site tuple " "references an unknown site"); } - if (tupleSites.size() == 1) { - oneQubitSites.insert(tupleSites.front()); - } else if (tupleSites.size() == 2) { - twoQubitSites.insert({tupleSites.front(), tupleSites.back()}); - } + supportedSites.insert(tupleSites); } capabilities[operation.canonicalName()].emplace_back(index); } @@ -633,18 +592,7 @@ llvm::Error CompilerTarget::Storage::initialize() { } for (const auto& specification : GATE_SPECIFICATIONS) { - const bool supportsControlledBase = - (specification.kind == GateKind::CX && - supportsOperation("x", specification.arity, - specification.numParameters, std::nullopt, - /*variadicOnly=*/true)) || - (specification.kind == GateKind::CZ && - supportsOperation("z", specification.arity, - specification.numParameters, std::nullopt, - /*variadicOnly=*/true)); - if (supportsControlledBase || - supportsOperation(specification.name, specification.arity, - specification.numParameters)) { + if (supportsGate(specification.kind)) { supportedGates.emplace_back(specification.kind); } } @@ -652,25 +600,6 @@ llvm::Error CompilerTarget::Storage::initialize() { return llvm::Error::success(); } -bool CompilerTarget::Storage::isApplicable( - size_t operationIndex, size_t arity, - std::optional> orderedSites) const { - const auto& operation = operations[operationIndex]; - if (operation.siteTuples().empty() || !orderedSites) { - return true; - } - if (arity == 1) { - return explicitOneQubitSites[operationIndex].contains((*orderedSites)[0]); - } - if (arity == 2) { - return explicitTwoQubitSites[operationIndex].contains( - {(*orderedSites)[0], (*orderedSites)[1]}); - } - return llvm::any_of(operation.siteTuples(), [&](const auto& siteTuple) { - return siteTuple.sites() == *orderedSites; - }); -} - bool CompilerTarget::Storage::supportsOperation( StringRef operationName, size_t arity, std::optional numParameters, std::optional> orderedSites, bool variadicOnly) const { @@ -700,12 +629,13 @@ bool CompilerTarget::Storage::supportsOperation( operation.arity().kind() == Operation::Arity::Kind::Variadic) && operation.arity().accepts(arity) && (!numParameters || operation.numParameters() == *numParameters) && - isApplicable(index, arity, orderedSites); + (!orderedSites || operation.siteTuples().empty() || + operationSites[index].contains(*orderedSites)); }); } bool CompilerTarget::Storage::supportsGate( - GateKind gate, ArrayRef orderedSites) const { + GateKind gate, std::optional> orderedSites) const { if ((gate == GateKind::CX && supportsOperation("x", 2, 0, orderedSites, /*variadicOnly=*/true)) || (gate == GateKind::CZ && @@ -713,9 +643,7 @@ bool CompilerTarget::Storage::supportsGate( return true; } const decltype(GATE_SPECIFICATIONS.cbegin()) specification = - std::ranges::find_if(GATE_SPECIFICATIONS, [&](const auto& candidate) { - return candidate.kind == gate; - }); + std::ranges::find(GATE_SPECIFICATIONS, gate, &GateSpecification::kind); assert(specification != GATE_SPECIFICATIONS.end() && "unknown compiler target gate"); return supportsOperation(specification->name, specification->arity, @@ -777,9 +705,7 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { return true; } const decltype(GATE_SPECIFICATIONS.cbegin()) specification = - std::ranges::find_if(GATE_SPECIFICATIONS, [&](const auto& candidate) { - return candidate.kind == gate; - }); + std::ranges::find(GATE_SPECIFICATIONS, gate, &GateSpecification::kind); assert(specification != GATE_SPECIFICATIONS.end() && "unknown compiler target gate"); if (supportsEveryPlacement(specification->name, specification->arity, @@ -981,14 +907,14 @@ CompilerTarget::createImpl(std::optional name, std::vector sites, Connectivity connectivity, NativeOperations nativeOperations, std::optional durationUnit) { - auto storage = Storage::create( + auto storage = std::make_shared( std::move(name), std::move(sites), connectivity.kind_, std::move(connectivity.couplings_), nativeOperations.kind_, std::move(nativeOperations.operations_), std::move(durationUnit)); - if (!storage) { - return storage.takeError(); + if (auto error = storage->initialize()) { + return std::move(error); } - return CompilerTarget(std::move(*storage)); + return CompilerTarget(std::move(storage)); } CompilerTarget::CompilerTarget(std::shared_ptr storage) @@ -1052,8 +978,8 @@ bool CompilerTarget::areAdjacent(size_t source, size_t target) const { void CompilerTarget::forEachNeighbour( size_t vertex, llvm::function_ref callback) const { + assert(vertex < numSites() && "Compiler target vertex is out of range"); if (connectivityKind() == Connectivity::Kind::AllToAll) { - assert(vertex < numSites() && "Compiler target vertex is out of range"); for (size_t neighbour = 0; neighbour < numSites(); ++neighbour) { if (neighbour != vertex) { callback(neighbour); @@ -1061,7 +987,7 @@ void CompilerTarget::forEachNeighbour( } return; } - for (const auto neighbour : explicitNeighbours(vertex)) { + for (const auto neighbour : storage_->adjacency[vertex]) { callback(neighbour); } } @@ -1075,11 +1001,6 @@ size_t CompilerTarget::distanceBetween(size_t source, size_t target) const { return storage_->distances[(source * numSites()) + target]; } -ArrayRef CompilerTarget::explicitNeighbours(size_t vertex) const { - assert(vertex < numSites() && "Compiler target vertex is out of range"); - return storage_->adjacency[vertex]; -} - size_t CompilerTarget::maxDegree() const noexcept { return storage_->maximumDegree; } diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index 5439de92bc..24863adb49 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -16,9 +16,9 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" +#include #include #include -#include #include #include // IWYU pragma: keep #include @@ -39,7 +39,6 @@ #include #include #include -#include #include using namespace mlir; @@ -126,7 +125,7 @@ SiteTupleAttr::verify(const function_ref emitError, const ArrayRef sites, const std::optional /*duration*/, const FloatAttr fidelity) { - std::unordered_set seen; + llvm::SmallDenseSet seen; seen.reserve(sites.size()); for (const int64_t site : sites) { if (site < 0) { @@ -175,18 +174,17 @@ LogicalResult NativeOperationAttr::verify( << "compiler target zero-arity operation cannot contain site tuples"; } - SmallVector> seen; + llvm::SmallDenseSet> seen; seen.reserve(siteTuples.size()); for (const SiteTupleAttr siteTuple : siteTuples) { if (siteTuple.getSites().size() != arity.getValue()) { return emitError() << "compiler target operation site tuple does not match its arity"; } - if (llvm::is_contained(seen, siteTuple.getSites())) { + if (!seen.insert(siteTuple.getSites()).second) { return emitError() << "compiler target operation contains a duplicate site tuple"; } - seen.emplace_back(siteTuple.getSites()); } return success(); @@ -205,7 +203,7 @@ LogicalResult CompilationTargetAttr::verify( return emitError() << "compiler target must contain at least one site"; } - std::unordered_set siteIds; + llvm::SmallDenseSet siteIds; siteIds.reserve(sites.size()); for (const SiteAttr site : sites) { if (!siteIds.insert(site.getId()).second) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index aa6c8cdf27..273a8ac923 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -97,11 +97,7 @@ oneQubitRunMemberMatrix(UnitaryOpInterface unitary) { !isWalkableUnitaryShell(unitary.getOperation())) { return std::nullopt; } - Matrix2x2 matrix; - if (!unitary.getUnitaryMatrix2x2(matrix)) { - return std::nullopt; - } - return matrix; + return unitary.getUnitaryMatrix(); } /// Return the constant matrix when `unitary` is a two-qubit run member. @@ -544,24 +540,17 @@ static LogicalResult fuseTwoQubitGates(ModuleOp moduleOp) { .singleQubit = CompilerTarget::SingleQubitBasis::U, .entangler = CompilerTarget::GateKind::CZ}; - SmallVector runHeads; - moduleOp.walk([&](Operation* operation) { - auto unitary = dyn_cast(operation); - const auto matrix = twoQubitRunMemberMatrix(unitary); - if (matrix && !feedsFromSameTwoQubitRun(unitary)) { - runHeads.emplace_back(operation); - } - }); - bool changed = false; IRRewriter rewriter(moduleOp.getContext()); - for (Operation* operation : runHeads) { - auto unitary = cast(operation); - const auto matrix = twoQubitRunMemberMatrix(unitary); - if (matrix) { - changed |= fuseTwoQubitGateRun(rewriter, unitary, *matrix, basis); - } - } + /// A run's successors have already been visited when its head erases them. + moduleOp->walk( + [&](Operation* operation) { + auto unitary = dyn_cast(operation); + const auto matrix = twoQubitRunMemberMatrix(unitary); + if (matrix && !feedsFromSameTwoQubitRun(unitary)) { + changed |= fuseTwoQubitGateRun(rewriter, unitary, *matrix, basis); + } + }); if (!changed) { return success(); } diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index db368731c4..46eb413622 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -76,14 +76,14 @@ TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { operations.emplace_back( valid(Operation::create(" PRX ", 1, 2, std::move(siteTuples), 0, 0.97))); - const auto target = valid( + auto target = valid( Target::create("device", std::move(sites), Connectivity::fromCouplings({{11, 2}, {2, 11}, {7, 2}}), NativeOperations::fromOperations(operations), valid(DurationUnit::create("ns", 0.5)))); - // The copy itself is the behavior under test: both objects must share the - // immutable backing storage. - // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) + /// The copy itself is the behavior under test: both objects must share the + /// immutable backing storage. + /// NOLINTNEXTLINE(performance-unnecessary-copy-initialization) const auto copy = target; ASSERT_TRUE(target.name()); @@ -110,6 +110,13 @@ TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { EXPECT_EQ(copy.sites().data(), target.sites().data()); EXPECT_EQ(copy.couplings().data(), target.couplings().data()); EXPECT_EQ(copy.operations().data(), target.operations().data()); + + operations.clear(); + target = valid(Target::create(1, Connectivity::allToAll(), + NativeOperations::unrestricted())); + EXPECT_TRUE(copy.supportsOperation("r", 1, 2, {7})); + EXPECT_TRUE(copy.supportsOperation("r", 1, 2, {2})); + EXPECT_TRUE(copy.supportsOperation("r", 1, 2, {11})); } TEST(CompilerTargetTest, ConstructsDenseUnnamedAllToAllTarget) { @@ -181,6 +188,8 @@ TEST(CompilerTargetTest, PreservesFullNonnegativeSiteIdDomain) { (llvm::ArrayRef{{nextSite, maxSite}})); EXPECT_EQ(target.operations().front().siteTuples().front().sites(), (llvm::ArrayRef{maxSite, nextSite})); + EXPECT_TRUE(target.supportsOperation("cx", 2, 0, {maxSite, nextSite})); + EXPECT_FALSE(target.supportsOperation("cx", 2, 0, {nextSite, maxSite})); } TEST(CompilerTargetTest, CanonicalizesConnectedTopologyAndCachesDistances) { diff --git a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp index 18f28f3666..f1e45d2ae4 100644 --- a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -116,7 +116,7 @@ TEST_F(MQTIRTest, RoundTripsTypedCompilationTarget) { , num_parameters = 0, - site_tuples = []>, + site_tuples = [<[4, 7], fidelity = 9.900000e-01 : f64>]>, , num_parameters = 1, site_tuples = []>, @@ -156,12 +156,28 @@ TEST_F(MQTIRTest, RoundTripsMaximumSiteIds) { operations = [, num_parameters = 0, - site_tuples = []>]>)mlir"); + site_tuples = [<[9223372036854775806, + 9223372036854775807]>]>]>)mlir"); ASSERT_TRUE(compilationTarget); EXPECT_EQ(roundTrip(compilationTarget), compilationTarget); } +TEST_F(MQTIRTest, RoundTripsSiteTupleCalibration) { + const auto durationOnly = dyn_cast_if_present( + parseAttr(R"mlir(#mqt.site_tuple<[4, 7], duration = 0>)mlir")); + ASSERT_TRUE(durationOnly); + EXPECT_EQ(durationOnly.getDuration(), 0U); + EXPECT_FALSE(durationOnly.getFidelity()); + EXPECT_EQ(roundTrip(durationOnly), durationOnly); + + const auto calibrated = dyn_cast_if_present(parseAttr( + R"mlir(#mqt.site_tuple<[4, 7], fidelity = 9.900000e-01 : f64, duration = 40>)mlir")); + ASSERT_TRUE(calibrated); + EXPECT_EQ(calibrated.getDuration(), 40U); + EXPECT_EQ(calibrated.getFidelity().getValueAsDouble(), 0.99); + EXPECT_EQ(roundTrip(calibrated), calibrated); +} + TEST_F(MQTIRTest, RepresentsUnrestrictedTargetFacts) { const auto unrestricted = dyn_cast_if_present( parseAttr(R"mlir(#mqt.compilation_target< @@ -186,11 +202,10 @@ TEST_F(MQTIRTest, RejectsInvalidTargetLeaves) { EXPECT_FALSE(parseAttr(R"mlir(#mqt.site)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.site)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.coupling)mlir")); - EXPECT_FALSE(parseAttr(R"mlir(#mqt.site_tuple)mlir")); - EXPECT_FALSE(parseAttr(R"mlir(#mqt.site_tuple)mlir")); - EXPECT_FALSE( - parseAttr(R"mlir(#mqt.site_tuple)mlir")); - EXPECT_FALSE(parseAttr(R"mlir(#mqt.site_tuple)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.site_tuple<[-1]>)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.site_tuple<[0], duration =>)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.site_tuple<[0], fidelity = 1.100000e+00 : f64>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.operation_arity< kind = variadic, value = 0>)mlir")); @@ -199,17 +214,17 @@ TEST_F(MQTIRTest, RejectsInvalidTargetLeaves) { num_parameters = 0, site_tuples = []>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 1, site_tuples = []>)mlir")); + num_parameters = 1, site_tuples = [<[0]>]>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 0, site_tuples = []>)mlir")); + num_parameters = 0, site_tuples = [<[0]>]>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 0, site_tuples = []>)mlir")); + num_parameters = 0, site_tuples = [<[0]>]>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, num_parameters = 0, - site_tuples = [, ]>)mlir")); + site_tuples = [<[0]>, <[0]>]>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, num_parameters = 0, site_tuples = [], duration =>>)mlir")); @@ -251,7 +266,7 @@ TEST_F(MQTIRTest, RejectsInvalidCompilationTargets) { native_operations = explicit, operations = [, - num_parameters = 0, site_tuples = []>]>)mlir")); + num_parameters = 0, site_tuples = [<[1]>]>]>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.compilation_target< sites = [], connectivity = all_to_all, couplings = [], native_operations = explicit, diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp index c4328470da..b33e90cac0 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -328,6 +328,26 @@ TEST_F(TargetSynthesisTest, expectEquivalent(expected, optimized); } +TEST_F(TargetSynthesisTest, TwoQubitGateFusionExposesEarlierRunContinuations) { + const auto adjacentRuns = [](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + auto q2 = builder.staticQubit(2); + std::tie(q0, q1) = builder.cx(q0, q1); + std::tie(q0, q2) = builder.cx(q0, q2); + std::tie(q0, q2) = builder.cx(q0, q2); + std::tie(q0, q1) = builder.cx(q0, q1); + return builder.intConstant(0); + }; + auto expected = build(adjacentRuns); + auto optimized = build(adjacentRuns); + + ASSERT_TRUE(mlir::succeeded( + runPass(*optimized, mlir::qco::createFuseTwoQubitGates()))); + EXPECT_EQ(countOps(*optimized), 0U); + expectEquivalent(expected, optimized); +} + TEST_F(TargetSynthesisTest, TwoQubitGateFusionEmitsSymmetricEntangler) { const auto reducible = [](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index 9f74149ce9..7487b315ff 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -194,7 +194,7 @@ class CompilerTarget: name: str, arity: int | CompilerTarget.OperationArity, num_parameters: int, - site_tuples: Sequence[CompilerTarget.SiteTuple] | None = None, + site_tuples: Sequence[CompilerTarget.SiteTuple | Sequence[int]] | None = None, duration: int | None = None, fidelity: float | None = None, ) -> None: ... diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index d56f8dac28..10cba46ce7 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -512,6 +512,25 @@ def test_compiler_target_constructors_preserve_python_api() -> None: assert duration_unit.unit == "ns" +@pytest.mark.parametrize("arity", [2, CompilerTarget.OperationArity.fixed(2)]) +def test_compiler_target_accepts_plain_site_tuples(arity: int | CompilerTarget.OperationArity) -> None: + """Mix plain placements and calibrated tuples without widening support.""" + operation = CompilerTarget.Operation( + "cx", arity, 0, site_tuples=[(1, 0), [1, 2], CompilerTarget.SiteTuple([2, 0], fidelity=0.99)] + ) + target = CompilerTarget( + 3, + connectivity=CompilerTarget.Connectivity.all_to_all(), + native_operations=CompilerTarget.NativeOperations([operation]), + ) + assert [entry.sites for entry in operation.site_tuples] == [[1, 0], [1, 2], [2, 0]] + assert [entry.fidelity for entry in operation.site_tuples] == [None, None, 0.99] + assert target.supports_operation("cx", 2, sites=[1, 0]) + assert not target.supports_operation("cx", 2, sites=[0, 1]) + with pytest.raises(ValueError, match="site tuple does not match its arity"): + CompilerTarget.Operation("cx", arity, 0, site_tuples=[(0,)]) + + def test_compiler_target_construction_preserves_validation_errors() -> None: """Translate explicit C++ construction errors to Python ``ValueError``.""" with pytest.raises(TypeError):