From d7d915b099202f2d23df826fd06d1ebf78d6f3bb Mon Sep 17 00:00:00 2001 From: Damian Rovara Date: Fri, 28 Aug 2026 12:27:23 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20the=20quantum=20IPO=20pipelin?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register the interprocedural optimization sequence and cover direct and named pipeline execution. Assisted-by: Claude Opus 5 Assisted-by: Codex --- CHANGELOG.md | 4 +- mlir/include/mlir/Support/Passes.h | 5 + mlir/lib/Support/Passes.cpp | 13 + .../Transforms/Optimizations/CMakeLists.txt | 1 + .../Transforms/Optimizations/IPOTestFixture.h | 27 ++ .../Optimizations/test_qco_quantum_ipo.cpp | 232 ++++++++++++++++++ 6 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quantum_ipo.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b740ea859..a1139538db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,7 +64,8 @@ releases may include breaking changes. #### Passes and transformations - ✨ Add passes for quantum-specific interprocedural optimizations ([#2193], - [#2197], [#2198], [#2199], [#2200]) ([**@DRovara**], [**@burgholzer**]) + [#2197], [#2198], [#2199], [#2200], [#2201]) ([**@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 +891,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 +[#2201]: https://github.com/munich-quantum-toolkit/core/pull/2201 [#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 diff --git a/mlir/include/mlir/Support/Passes.h b/mlir/include/mlir/Support/Passes.h index 8f005c3b6e..8e9986241f 100644 --- a/mlir/include/mlir/Support/Passes.h +++ b/mlir/include/mlir/Support/Passes.h @@ -37,6 +37,11 @@ void populateDefaultQCOOptimizationPipeline(mlir::OpPassManager& pm); /// Populate the qubit reuse pipeline including its preparation passes. void populateQubitReusePipeline(mlir::OpPassManager& pm); +/// Populate @p pm with the interprocedural optimization passes, in the order +/// they are meant to run. The passes are also registered individually, so a +/// caller assembling its own pipeline can pick only the ones it wants. +void populateQuantumIPOPipeline(mlir::OpPassManager& pm); + /// Return whether @p minQubits is valid for multi-controlled decomposition. [[nodiscard]] bool isDecomposeMultiControlledConfigValid(uint64_t minQubits); diff --git a/mlir/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index 56c52ec662..d3e6bce255 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -70,6 +70,10 @@ void registerMQTCompilerPasses() { PassPipelineRegistration<>("mqt-qco-default", "Run the default MQT QCO optimization pipeline.", populateDefaultQCOOptimizationPipeline); + PassPipelineRegistration<>( + "quantum-ipo", + "Run the interprocedural optimizations across function boundaries.", + populateQuantumIPOPipeline); PassPipelineRegistration<>( "mqt-qubit-reuse", "Prepare a QCO program for qubit reuse and reuse eligible qubits.", @@ -90,6 +94,15 @@ void populateQubitReusePipeline(OpPassManager& pm) { pm.addPass(qco::createReuseQubits()); } +void populateQuantumIPOPipeline(OpPassManager& pm) { + pm.addPass(qco::createContextSensitiveSpecialization()); + pm.addPass(qco::createQuantumArgumentPromotion()); + pm.addPass(qco::createAuxiliaryQubitHoisting()); + // Commutation can expose further cancellations, so it runs twice. + pm.addPass(qco::createQuantumFunctionBoundaryCommutation()); + pm.addPass(qco::createQuantumFunctionBoundaryCommutation()); +} + bool isDecomposeMultiControlledConfigValid(const uint64_t minQubits) { return minQubits >= 3; } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt index c0f1c1a409..e883e31ce9 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt @@ -17,6 +17,7 @@ add_executable( test_qco_merge_single_qubit_rotation.cpp test_qco_pauli_twirling.cpp test_qco_quantum_argument_promotion.cpp + test_qco_quantum_ipo.cpp test_qco_remove_dead_gates.cpp test_qco_replace_classical_controls.cpp test_qco_reuse_qubits.cpp diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/IPOTestFixture.h b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/IPOTestFixture.h index b47e16f838..4c0ae2f221 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/IPOTestFixture.h +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/IPOTestFixture.h @@ -25,6 +25,7 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" +#include "mlir/Support/Passes.h" #include #include @@ -85,6 +86,32 @@ class IPOTestBase : public testing::Test { areModulesEquivalentWithPermutations(moduleOp.get(), reference.get())); } + /** + * @brief Runs the whole interprocedural pipeline on a module. + * + * @details + * Only for the cross-stage cases. A case about one pass belongs in that + * pass's own suite, scheduled on the pass alone. + * + * @param moduleOp The module to transform. + */ + static mlir::LogicalResult runQuantumIPOPipeline(mlir::ModuleOp moduleOp) { + mlir::PassManager pm(moduleOp.getContext()); + populateQuantumIPOPipeline(pm); + pm.addPass(mlir::createCanonicalizerPass()); + return pm.run(moduleOp); + } + + /** + * @brief Runs the whole pipeline and compares against the reference. + */ + void expectPipelineMatchesReference() { + ASSERT_TRUE(runQuantumIPOPipeline(moduleOp.get()).succeeded()); + ASSERT_TRUE(runCanonicalizerPass(reference.get()).succeeded()); + EXPECT_TRUE( + areModulesEquivalentWithPermutations(moduleOp.get(), reference.get())); + } + /** * @brief Parses a module from MLIR source. * diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quantum_ipo.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quantum_ipo.cpp new file mode 100644 index 0000000000..43e52d2898 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quantum_ipo.cpp @@ -0,0 +1,232 @@ +/* + * 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_quantum_ipo.cpp + * @brief Cross-stage tests for the `quantum-ipo` pipeline. + * + * @details + * Only scenarios that need more than one pass belong here. Everything about a + * single pass lives in that pass's own suite, scheduled on the pass alone. + */ + +#include "IPOTestFixture.h" +#include "Support/IRVerification.h" +#include "mlir/Support/Passes.h" + +#include +#include + +#include +#include + +namespace { + +using QCOQuantumIPOPipelineTest = ::mqt::test::IPOTestBase; +using namespace mlir; +using namespace mlir::qco; + +// Integration tests combining several IPO approaches. +// ========================================================================== + +/** + * @brief A callee that both starts with a gate that is trivial on |0> and uses + * an internal auxiliary qubit is first specialized and then hoisted. The + * hoisting applies to the original and the specialized copy alike. + */ +TEST_F(QCOQuantumIPOPipelineTest, specializationAndHoistingCombined) { + const auto qubitType = programBuilder.getQubitType(); + + programBuilder.initialize(); + auto args = programBuilder.startFunction("f", {qubitType}, {qubitType}); + auto aux = programBuilder.allocQubit(); + auto target = programBuilder.z(args[0]); + std::tie(aux, target) = programBuilder.cx(aux, target); + programBuilder.sink(aux); + programBuilder.endFunction({target}); + + auto q = programBuilder.allocQubit(); + auto results = programBuilder.call("f", {q}); + programBuilder.sink(results[0]); + moduleOp = programBuilder.finalize(); + + referenceBuilder.initialize(); + // The original loses its only caller to the specialization and is dropped + // before hoisting runs, so it is never given an auxiliary argument. + // The specialization drops the `z` gate and is hoisted. + auto specArgs = referenceBuilder.startFunction( + "f_spec_zero_arg_0", {qubitType, qubitType}, {qubitType, qubitType}); + auto specAux = specArgs[1]; + auto specTarget = specArgs[0]; + std::tie(specAux, specTarget) = referenceBuilder.cx(specAux, specTarget); + specAux = referenceBuilder.reset(specAux); + referenceBuilder.endFunction({specTarget, specAux}); + + auto refQ = referenceBuilder.allocQubit(); + auto refAuxAlloc = referenceBuilder.allocQubit(); + auto refResults = + referenceBuilder.call("f_spec_zero_arg_0", {refQ, refAuxAlloc}); + referenceBuilder.sink(refResults[0]); + referenceBuilder.sink(refResults[1]); + reference = referenceBuilder.finalize(); + + expectPipelineMatchesReference(); +} + +/** + * @brief A callee with two qubit arguments where one argument is specialized + * for the |0> state and the other cancels a gate across the call boundary. + */ +TEST_F(QCOQuantumIPOPipelineTest, + specializationAndBoundaryCommutationCombined) { + const auto qubitType = programBuilder.getQubitType(); + + programBuilder.initialize(); + auto args = programBuilder.startFunction("f", {qubitType, qubitType}, + {qubitType, qubitType}); + auto first = programBuilder.z(args[0]); + auto second = programBuilder.x(args[1]); + second = programBuilder.h(second); + programBuilder.endFunction({first, second}); + + auto q0 = programBuilder.allocQubit(); + auto q1 = programBuilder.x(programBuilder.allocQubit()); + auto results = programBuilder.call("f", {q0, q1}); + programBuilder.sink(results[0]); + programBuilder.sink(results[1]); + moduleOp = programBuilder.finalize(); + + referenceBuilder.initialize(); + // The |0> specialization drops the `z` gate on the first argument and the + // boundary commutation then specializes that copy in turn, so both the + // original and the intermediate end up without callers. + // Only the last link of the chain survives: it has neither the `z` gate on + // the first argument nor the `x` gate on the second. + auto commutedArgs = referenceBuilder.startFunction( + "f_spec_zero_arg_0_spec_boundary_commutation_arg_1", + {qubitType, qubitType}, {qubitType, qubitType}); + referenceBuilder.endFunction( + {commutedArgs[0], referenceBuilder.h(commutedArgs[1])}); + + auto refQ0 = referenceBuilder.allocQubit(); + auto refQ1 = referenceBuilder.allocQubit(); + auto refResults = referenceBuilder.call( + "f_spec_zero_arg_0_spec_boundary_commutation_arg_1", {refQ0, refQ1}); + referenceBuilder.sink(refResults[0]); + referenceBuilder.sink(refResults[1]); + reference = referenceBuilder.finalize(); + + expectPipelineMatchesReference(); +} + +/** + * @brief A program with several distinct callees, each hitting a different IPO + * approach: a |0> specialization, a fixed rotation angle, and a cancellation + * across the call boundary. + */ +TEST_F(QCOQuantumIPOPipelineTest, multipleFunctionsWithDistinctOptimizations) { + const auto qubitType = programBuilder.getQubitType(); + const auto floatType = programBuilder.getF64Type(); + + programBuilder.initialize(); + auto prepareArgs = + programBuilder.startFunction("prepare", {qubitType}, {qubitType}); + programBuilder.endFunction( + {programBuilder.h(programBuilder.z(prepareArgs[0]))}); + + auto rotateArgs = programBuilder.startFunction( + "rotate", {qubitType, floatType}, {qubitType}); + programBuilder.endFunction({programBuilder.rz(rotateArgs[1], rotateArgs[0])}); + + auto flipArgs = + programBuilder.startFunction("flip", {qubitType}, {qubitType}); + programBuilder.endFunction({programBuilder.y(programBuilder.x(flipArgs[0]))}); + + auto q0 = programBuilder.allocQubit(); + auto q1 = programBuilder.allocQubit(); + auto q2 = programBuilder.x(programBuilder.allocQubit()); + auto prepared = programBuilder.call("prepare", {q0}); + auto angle = programBuilder.floatConstant(std::numbers::pi / 2); + auto rotated = programBuilder.call("rotate", {q1, angle}); + auto flipped = programBuilder.call("flip", {q2}); + programBuilder.sink(prepared[0]); + programBuilder.sink(rotated[0]); + programBuilder.sink(flipped[0]); + moduleOp = programBuilder.finalize(); + + referenceBuilder.initialize(); + // Each original loses its only caller to its specialization and is dropped. + auto preparedSpecArgs = referenceBuilder.startFunction( + "prepare_spec_zero_arg_0", {qubitType}, {qubitType}); + referenceBuilder.endFunction({referenceBuilder.h(preparedSpecArgs[0])}); + + auto rotateSpecArgs = referenceBuilder.startFunction( + "rotate_spec_fixed_angle_1", {qubitType, floatType}, {qubitType}); + referenceBuilder.endFunction( + {referenceBuilder.rz(std::numbers::pi / 2, rotateSpecArgs[0])}); + + auto flipSpecArgs = referenceBuilder.startFunction( + "flip_spec_boundary_commutation_arg_0", {qubitType}, {qubitType}); + referenceBuilder.endFunction({referenceBuilder.y(flipSpecArgs[0])}); + + auto refQ0 = referenceBuilder.allocQubit(); + auto refQ1 = referenceBuilder.allocQubit(); + auto refQ2 = referenceBuilder.allocQubit(); + auto refPrepared = referenceBuilder.call("prepare_spec_zero_arg_0", {refQ0}); + auto refAngle = referenceBuilder.floatConstant(std::numbers::pi / 2); + auto refRotated = + referenceBuilder.call("rotate_spec_fixed_angle_1", {refQ1, refAngle}); + auto refFlipped = + referenceBuilder.call("flip_spec_boundary_commutation_arg_0", {refQ2}); + referenceBuilder.sink(refPrepared[0]); + referenceBuilder.sink(refRotated[0]); + referenceBuilder.sink(refFlipped[0]); + reference = referenceBuilder.finalize(); + + expectPipelineMatchesReference(); +} + +// ========================================================================== + +} // namespace + +/** + * @brief The pipeline is reachable under the name it registers. + * + * @details + * Scheduling the passes directly, as the cases above do, would still pass if + * `quantum-ipo` were never registered. Running it by name is what `mqt-cc` + * does, so this is the contract that has to hold for the pipeline to be usable + * from the command line at all. + */ +TEST_F(QCOQuantumIPOPipelineTest, PipelineRunsUnderItsRegisteredName) { + const auto qubitType = programBuilder.getQubitType(); + + const auto build = [&qubitType](QCOProgramBuilder& b) { + b.initialize(); + auto args = b.startFunction("f", {qubitType}, {qubitType}); + b.endFunction({b.z(args[0])}); + auto q = b.allocQubit(); + auto results = b.call("f", {q}); + b.sink(results[0]); + return b.finalize(); + }; + + moduleOp = build(programBuilder); + ASSERT_TRUE( + runPassPipeline(moduleOp.get(), "quantum-ipo", false, false).succeeded()); + + // The `z` acts trivially on |0>, so the specialized callee drops it. + reference = build(referenceBuilder); + ASSERT_TRUE(runQuantumIPOPipeline(reference.get()).succeeded()); + ASSERT_TRUE(runCanonicalizerPass(moduleOp.get()).succeeded()); + EXPECT_TRUE( + areModulesEquivalentWithPermutations(moduleOp.get(), reference.get())); +}