Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**],
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions mlir/include/mlir/Support/Passes.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
13 changes: 13 additions & 0 deletions mlir/lib/Support/Passes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <gtest/gtest.h>
#include <mlir/Dialect/Arith/IR/Arith.h>
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <gtest/gtest.h>
#include <mlir/Support/LLVM.h>

#include <numbers>
#include <tuple>

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()));
}
Loading