diff --git a/CHANGELOG.md b/CHANGELOG.md index eb1013bee7..6b740ea859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,7 +64,7 @@ releases may include breaking changes. #### Passes and transformations - ✨ Add passes for quantum-specific interprocedural optimizations ([#2193], - [#2197], [#2198], [#2199]) ([**@DRovara**], [**@burgholzer**]) + [#2197], [#2198], [#2199], [#2200]) ([**@DRovara**], [**@burgholzer**]) - ✨ Add Pauli twirling, quantum loop unrolling, and qubit reuse passes ([#1705], [#1718], [#1755], [#1756], [#1923], [#1924], [#2039], [#2118], [#2216], [#2224]) ([**@MatthiasReumann**], [**@DRovara**], [**@burgholzer**], @@ -890,6 +890,7 @@ for previous changelogs._ [#2216]: https://github.com/munich-quantum-toolkit/core/pull/2216 [#2203]: https://github.com/munich-quantum-toolkit/core/pull/2203 [#2214]: https://github.com/munich-quantum-toolkit/core/pull/2214 +[#2200]: https://github.com/munich-quantum-toolkit/core/pull/2200 [#2199]: https://github.com/munich-quantum-toolkit/core/pull/2199 [#2198]: https://github.com/munich-quantum-toolkit/core/pull/2198 [#2197]: https://github.com/munich-quantum-toolkit/core/pull/2197 diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 42eebceed3..2e092cf5eb 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -390,6 +390,26 @@ def AuxiliaryQubitHoisting "mlir::qco::QCODialect"]; } +def QuantumFunctionBoundaryCommutation + : Pass<"quantum-function-boundary-commutation", "mlir::ModuleOp"> { + let summary = "Cancel self-inverse gates across a call boundary"; + let description = [{ + When the operation producing a qubit argument and the first operation + applied to it inside the callee are the same self-inverse single-qubit + gate, both are removed and the call is redirected to a copy of the + callee without the callee-side gate. + + Copies are cached per callee and per parameter, because the gate removed + inside belongs to one specific argument. Running the pass repeatedly can + expose further cancellations. + }]; + + let dependentDialects = ["::mlir::func::FuncDialect", + "::mlir::arith::ArithDialect", + "::mlir::qtensor::QTensorDialect", + "mlir::qco::QCODialect"]; +} + def RemoveDeadGates : Pass<"remove-dead-gates", "mlir::ModuleOp"> { let dependentDialects = ["mlir::qco::QCODialect"]; let summary = "Remove quantum gates whose results cannot be observed"; diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/QuantumFunctionBoundaryCommutation.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/QuantumFunctionBoundaryCommutation.cpp new file mode 100644 index 0000000000..15c336e9f2 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/QuantumFunctionBoundaryCommutation.cpp @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "IPOUtils.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/QTensor/IR/QTensorDialect.h" // IWYU pragma: keep (Passes.h.inc) + +#include +#include // IWYU pragma: keep (Passes.h.inc) +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace mlir::qco { + +#define GEN_PASS_DEF_QUANTUMFUNCTIONBOUNDARYCOMMUTATION +#include "mlir/Dialect/QCO/Transforms/Passes.h.inc" + +/** + * @brief Check if two single-qubit unitary operations cancel each other out + * because they are self-inverse. + * + * @param first The first unitary operation. + * @param second The second unitary operation. + * @return true if the operations cancel each other out, false otherwise. + */ +static bool doOpsCancel(UnitaryOpInterface first, UnitaryOpInterface second) { + if (first.getNumQubits() != 1) { + return false; + } + if (first.getOperation()->getName() != second.getOperation()->getName()) { + return false; + } + if (isa(first)) { + return true; + } + return false; +} + +/// Caches the specialization created for a callee, per parameter index. The +/// gate that is removed inside the callee belongs to one specific argument, so +/// specializations must not be shared between parameters. +using BoundarySpecializations = + llvm::StringMap>; + +/** + * @brief Cancel a gate in front of a call against the same gate at the start of + * the callee. + * + * @details + * When the operation producing the argument at @p parameter and the first + * operation applied to that argument inside the callee are the same + * self-inverse gate, both can be dropped. The caller-side gate is erased and + * the call is redirected to a copy of the callee without the callee-side gate. + * Copies are cached per callee and parameter, so repeated call sites share one + * specialization while different parameters get their own. + * + * @param call The call to look at. + * @param symbolTable The symbol table of the surrounding module. + * @param parameter The index of the qubit argument to consider. + * @param previousSpecializations Cache of already-created specializations. + */ +static void +tryBoundaryCommutation(func::CallOp call, SymbolTable& symbolTable, + uint32_t parameter, + BoundarySpecializations& previousSpecializations, + SmallVectorImpl* touchedFunctions) { + auto calleeName = call.getCallee(); + auto funcOp = symbolTable.lookup(calleeName); + + if (!funcOp || funcOp.isExternal()) { + return; + } + + auto argOutside = call.getArgOperands()[parameter]; + auto argInside = funcOp.getArgument(parameter); + + if (!argInside.hasOneUse()) { + return; + } + if (argOutside.getDefiningOp() == nullptr) { + return; + } + + auto lastOp = dyn_cast(argOutside.getDefiningOp()); + auto nextOp = dyn_cast(*argInside.getUsers().begin()); + + if (!lastOp || !nextOp) { + return; + } + + if (!doOpsCancel(lastOp, nextOp)) { + return; + } + argOutside.replaceAllUsesWith(lastOp.getInputQubit(0)); + lastOp.erase(); + + // The call is about to be redirected away from `funcOp`, so it may lose its + // last caller. + if (touchedFunctions != nullptr) { + touchedFunctions->emplace_back(funcOp); + } + + if (const auto it = previousSpecializations.find(calleeName); + it != previousSpecializations.end()) { + if (const auto cached = it->second.find(parameter); + cached != it->second.end()) { + call.setCallee(cached->second.getName()); + return; + } + } + + auto newFunc = copyFunction(funcOp, funcOp.getName().str() + + "_spec_boundary_commutation_arg_" + + std::to_string(parameter)); + symbolTable.insert(newFunc); + + auto newParameter = newFunc.getArgument(parameter); + auto newUser = dyn_cast(*newParameter.getUsers().begin()); + + for (auto i = 0U; i < newUser.getNumQubits(); ++i) { + newUser.getOutputQubit(i).replaceAllUsesWith(newUser.getInputQubit(i)); + } + newUser.erase(); + previousSpecializations[calleeName][parameter] = newFunc; + if (touchedFunctions != nullptr) { + touchedFunctions->emplace_back(newFunc); + } + + call.setCallee(newFunc.getName()); +} + +/** + * @brief Cancel gates across every call boundary in the module. + * + * @param moduleOp The module to transform. + * @param symbolTable The symbol table of @p moduleOp. + */ +namespace { +/// Cancels a self-inverse gate in front of a call against the same gate at the +/// start of the callee. +struct QuantumFunctionBoundaryCommutation final + : impl::QuantumFunctionBoundaryCommutationBase< + QuantumFunctionBoundaryCommutation> { + using impl::QuantumFunctionBoundaryCommutationBase< + QuantumFunctionBoundaryCommutation>:: + QuantumFunctionBoundaryCommutationBase; + +protected: + void runOnOperation() override { + auto moduleOp = getOperation(); + SymbolTable symbolTable(moduleOp); + BoundarySpecializations previousSpecializations; + // Callees this pass redirects calls away from, plus the copies it creates. + SmallVector touchedFunctions; + + // Collect the calls first: the commutation erases the caller-side gate, + // which would invalidate a walk in progress. + SmallVector calls; + moduleOp.walk([&](func::CallOp call) { calls.emplace_back(call); }); + + for (auto call : calls) { + for (uint32_t i = 0; i < call.getArgOperands().size(); ++i) { + const auto arg = call.getArgOperands()[i]; + if (!isa(arg.getType())) { + continue; + } + tryBoundaryCommutation(call, symbolTable, i, previousSpecializations, + &touchedFunctions); + } + } + + // Drop the callees this pass left without callers. + eraseOrphanedSpecializations(symbolTable, touchedFunctions); + } +}; +} // namespace + +} // namespace mlir::qco diff --git a/mlir/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index 3b2487c292..56c52ec662 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -64,6 +64,7 @@ void registerMQTCompilerPasses() { qco::registerContextSensitiveSpecialization(); qco::registerQuantumArgumentPromotion(); qco::registerAuxiliaryQubitHoisting(); + qco::registerQuantumFunctionBoundaryCommutation(); mqt::registerNormalizeGlobalPhases(); mqt::registerUnrollModifiers(); PassPipelineRegistration<>("mqt-qco-default", diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt index 7beba5b928..c0f1c1a409 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt @@ -11,6 +11,7 @@ add_executable( ${target_name} test_qco_auxiliary_qubit_hoisting.cpp test_qco_context_sensitive_specialization.cpp + test_qco_function_boundary_commutation.cpp test_qco_hadamard_lifting.cpp test_qco_measurement_lifting.cpp test_qco_merge_single_qubit_rotation.cpp diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_function_boundary_commutation.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_function_boundary_commutation.cpp new file mode 100644 index 0000000000..78d93d1481 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_function_boundary_commutation.cpp @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +/** + * @file test_qco_function_boundary_commutation.cpp + * @brief Tests for the `quantum-function-boundary-commutation` pass. + */ + +#include "IPOTestFixture.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" + +#include +#include + +#include + +namespace { + +using QCOFunctionBoundaryCommutationTest = ::mqt::test::IPOTestBase; +using namespace mlir; +using namespace mlir::qco; + +// Quantum function boundary commutation. +// ========================================================================== + +/** + * @brief A self-inverse gate applied right before a call cancels with the same + * gate at the start of the callee. + */ +TEST_F(QCOFunctionBoundaryCommutationTest, + cancelSelfInverseGateAcrossCallBoundary) { + programBuilder.initialize(); + auto args = programBuilder.startFunction("f", {programBuilder.getQubitType()}, + {programBuilder.getQubitType()}); + programBuilder.endFunction({programBuilder.h(programBuilder.x(args[0]))}); + + auto q = programBuilder.x(programBuilder.allocQubit()); + auto results = programBuilder.call("f", {q}); + programBuilder.sink(results[0]); + moduleOp = programBuilder.finalize(); + + referenceBuilder.initialize(); + + // Both the caller-side and the callee-side gate disappear. + auto specArgs = referenceBuilder.startFunction( + "f_spec_boundary_commutation_arg_0", {referenceBuilder.getQubitType()}, + {referenceBuilder.getQubitType()}); + referenceBuilder.endFunction({referenceBuilder.h(specArgs[0])}); + + auto refQ = referenceBuilder.allocQubit(); + auto refResults = + referenceBuilder.call("f_spec_boundary_commutation_arg_0", {refQ}); + referenceBuilder.sink(refResults[0]); + reference = referenceBuilder.finalize(); + + expectSingleStageMatchesReference(createQuantumFunctionBoundaryCommutation()); +} + +/** + * @brief Two different gates across the call boundary do not cancel. + */ +TEST_F(QCOFunctionBoundaryCommutationTest, noCancellationForDifferentGates) { + programBuilder.initialize(); + auto args = programBuilder.startFunction("f", {programBuilder.getQubitType()}, + {programBuilder.getQubitType()}); + programBuilder.endFunction({programBuilder.y(args[0])}); + + auto q = programBuilder.x(programBuilder.allocQubit()); + auto results = programBuilder.call("f", {q}); + programBuilder.sink(results[0]); + moduleOp = programBuilder.finalize(); + + referenceBuilder.initialize(); + auto refArgs = + referenceBuilder.startFunction("f", {referenceBuilder.getQubitType()}, + {referenceBuilder.getQubitType()}); + referenceBuilder.endFunction({referenceBuilder.y(refArgs[0])}); + + auto refQ = referenceBuilder.x(referenceBuilder.allocQubit()); + auto refResults = referenceBuilder.call("f", {refQ}); + referenceBuilder.sink(refResults[0]); + reference = referenceBuilder.finalize(); + + expectSingleStageMatchesReference(createQuantumFunctionBoundaryCommutation()); +} + +/** + * @brief Controlled gates are out of scope for boundary commutation, even when + * the same one appears on both sides of the call. Cancelling them would require + * reasoning about the control qubits as well. + */ +TEST_F(QCOFunctionBoundaryCommutationTest, noCancellationForControlledGates) { + const auto qubitType = programBuilder.getQubitType(); + + programBuilder.initialize(); + auto args = programBuilder.startFunction("f", {qubitType, qubitType}, + {qubitType, qubitType}); + auto innerControl = args[0]; + auto innerTarget = args[1]; + std::tie(innerControl, innerTarget) = + programBuilder.cx(innerControl, innerTarget); + programBuilder.endFunction({innerControl, innerTarget}); + + auto q0 = programBuilder.y(programBuilder.allocQubit()); + auto q1 = programBuilder.y(programBuilder.allocQubit()); + std::tie(q0, q1) = programBuilder.cx(q0, q1); + auto results = programBuilder.call("f", {q0, q1}); + programBuilder.sink(results[0]); + programBuilder.sink(results[1]); + moduleOp = programBuilder.finalize(); + + referenceBuilder.initialize(); + auto refArgs = referenceBuilder.startFunction("f", {qubitType, qubitType}, + {qubitType, qubitType}); + auto refInnerControl = refArgs[0]; + auto refInnerTarget = refArgs[1]; + std::tie(refInnerControl, refInnerTarget) = + referenceBuilder.cx(refInnerControl, refInnerTarget); + referenceBuilder.endFunction({refInnerControl, refInnerTarget}); + + auto refQ0 = referenceBuilder.y(referenceBuilder.allocQubit()); + auto refQ1 = referenceBuilder.y(referenceBuilder.allocQubit()); + std::tie(refQ0, refQ1) = referenceBuilder.cx(refQ0, refQ1); + auto refResults = referenceBuilder.call("f", {refQ0, refQ1}); + referenceBuilder.sink(refResults[0]); + referenceBuilder.sink(refResults[1]); + reference = referenceBuilder.finalize(); + + expectSingleStageMatchesReference(createQuantumFunctionBoundaryCommutation()); +} + +/** + * @brief Two call sites that cancel the same gate share a single commuted copy + * of the callee. + */ +TEST_F(QCOFunctionBoundaryCommutationTest, + reuseBoundaryCommutationAcrossCallSites) { + const auto qubitType = programBuilder.getQubitType(); + + programBuilder.initialize(); + auto args = programBuilder.startFunction("f", {qubitType}, {qubitType}); + programBuilder.endFunction({programBuilder.h(programBuilder.x(args[0]))}); + + auto q0 = programBuilder.x(programBuilder.allocQubit()); + auto q1 = programBuilder.x(programBuilder.allocQubit()); + auto results0 = programBuilder.call("f", {q0}); + auto results1 = programBuilder.call("f", {q1}); + programBuilder.sink(results0[0]); + programBuilder.sink(results1[0]); + moduleOp = programBuilder.finalize(); + + referenceBuilder.initialize(); + auto specArgs = referenceBuilder.startFunction( + "f_spec_boundary_commutation_arg_0", {qubitType}, {qubitType}); + referenceBuilder.endFunction({referenceBuilder.h(specArgs[0])}); + + auto refQ0 = referenceBuilder.allocQubit(); + auto refQ1 = referenceBuilder.allocQubit(); + auto refResults0 = + referenceBuilder.call("f_spec_boundary_commutation_arg_0", {refQ0}); + auto refResults1 = + referenceBuilder.call("f_spec_boundary_commutation_arg_0", {refQ1}); + referenceBuilder.sink(refResults0[0]); + referenceBuilder.sink(refResults1[0]); + reference = referenceBuilder.finalize(); + + expectSingleStageMatchesReference(createQuantumFunctionBoundaryCommutation()); +} + +/** + * @brief Two call sites that cancel a gate on different parameters of the same + * callee must get their own specialization, because the gate is removed from a + * specific argument. + */ +TEST_F(QCOFunctionBoundaryCommutationTest, + separateCommutationSpecializationPerParameter) { + const auto qubitType = programBuilder.getQubitType(); + + const auto buildCallee = [&qubitType](QCOProgramBuilder& b, StringRef name) { + auto args = + b.startFunction(name, {qubitType, qubitType}, {qubitType, qubitType}); + b.endFunction({b.x(args[0]), b.x(args[1])}); + }; + + programBuilder.initialize(); + buildCallee(programBuilder, "f"); + + // The first call cancels the gate on parameter 0, ... + auto a0 = programBuilder.x(programBuilder.allocQubit()); + auto a1 = programBuilder.allocQubit(); + auto results0 = programBuilder.call("f", {a0, a1}); + // ... the second one on parameter 1. + auto b0 = programBuilder.allocQubit(); + auto b1 = programBuilder.x(programBuilder.allocQubit()); + auto results1 = programBuilder.call("f", {b0, b1}); + programBuilder.sink(results0[0]); + programBuilder.sink(results0[1]); + programBuilder.sink(results1[0]); + programBuilder.sink(results1[1]); + moduleOp = programBuilder.finalize(); + + referenceBuilder.initialize(); + // Both call sites are redirected, so the original is left without callers. + // One specialization without the gate on parameter 0, ... + auto spec0Args = referenceBuilder.startFunction( + "f_spec_boundary_commutation_arg_0", {qubitType, qubitType}, + {qubitType, qubitType}); + referenceBuilder.endFunction( + {spec0Args[0], referenceBuilder.x(spec0Args[1])}); + // ... and one without the gate on parameter 1. + auto spec1Args = referenceBuilder.startFunction( + "f_spec_boundary_commutation_arg_1", {qubitType, qubitType}, + {qubitType, qubitType}); + referenceBuilder.endFunction( + {referenceBuilder.x(spec1Args[0]), spec1Args[1]}); + + auto refA0 = referenceBuilder.allocQubit(); + auto refA1 = referenceBuilder.allocQubit(); + auto refResults0 = referenceBuilder.call("f_spec_boundary_commutation_arg_0", + {refA0, refA1}); + auto refB0 = referenceBuilder.allocQubit(); + auto refB1 = referenceBuilder.allocQubit(); + auto refResults1 = referenceBuilder.call("f_spec_boundary_commutation_arg_1", + {refB0, refB1}); + referenceBuilder.sink(refResults0[0]); + referenceBuilder.sink(refResults0[1]); + referenceBuilder.sink(refResults1[0]); + referenceBuilder.sink(refResults1[1]); + reference = referenceBuilder.finalize(); + + expectSingleStageMatchesReference(createQuantumFunctionBoundaryCommutation()); +} + +// ========================================================================== + +} // namespace