Skip to content

Conversation

taminob
Copy link
Collaborator

@taminob taminob commented Sep 5, 2025

Description

In order to build a compiler pipeline, we need different passes which can also be applied after the routing.
One such pass is this which will take a series of single qubit gates and transform them into one to three rotation gates.

Related to #1122

Checklist:

  • The pull request only contains commits that are focused and relevant to this change.
  • I have added appropriate tests that cover the new/changed functionality.
  • I have updated the documentation to reflect these changes.
  • I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals.
  • I have added migration instructions to the upgrade guide (if needed).
  • The changes follow the project's style guidelines and introduce no new warnings.
  • The changes are fully tested and pass the CI checks.
  • I have reviewed my own code changes.

@taminob taminob self-assigned this Sep 5, 2025
@taminob taminob added enhancement New feature or request c++ Anything related to C++ code MLIR Anything related to MLIR labels Sep 5, 2025
@taminob taminob force-pushed the taminob/mlir-gate-decomposition-pass branch 2 times, most recently from ee89a21 to 38f10fe Compare September 5, 2025 13:44
Copy link
Contributor

github-actions bot commented Sep 5, 2025

Cpp-Linter Report ⚠️

Some files did not pass the configured checks!

clang-tidy (v21.1.1) reports: 36 concern(s)
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:15:1: warning: [misc-include-cleaner]

    included header iterator is not used directly

       15 | #include <iterator>
          | ^~~~~~~~~~~~~~~~~~~
       16 | #include <llvm/ADT/STLExtras.h>
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:48:9: warning: [misc-include-cleaner]

    no header providing "dd::GateMatrix" is directly included

       12 |     dd::GateMatrix unitaryMatrix = dd::opToSingleQubitGateMatrix(qc::I);
          |         ^
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:48:40: warning: [misc-include-cleaner]

    no header providing "dd::opToSingleQubitGateMatrix" is directly included

       12 |     dd::GateMatrix unitaryMatrix = dd::opToSingleQubitGateMatrix(qc::I);
          |                                        ^
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:48:70: warning: [misc-include-cleaner]

    no header providing "qc::I" is directly included

       12 |     dd::GateMatrix unitaryMatrix = dd::opToSingleQubitGateMatrix(qc::I);
          |                                                                      ^
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:97:5: warning: [misc-include-cleaner]

    no header providing "assert" is directly included

       15 |     assert(op->hasOneUse());
          |     ^
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:99:18: warning: [misc-include-cleaner]

    no header providing "llvm::dyn_cast" is directly included

       14 |     return llvm::dyn_cast<UnitaryInterface>(*users.begin());
          |                  ^
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:113:44: warning: [misc-include-cleaner]

    no header providing "qc::fp" is directly included

       12 |                                        qc::fp parameter,
          |                                            ^
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:127:36: warning: [misc-include-cleaner]

    no header providing "std::pair" is directly included

       24 |       const llvm::SmallVector<std::pair<qc::OpType, qc::fp>, 3>& schematic,
          |                                    ^
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:127:45: warning: [misc-include-cleaner]

    no header providing "qc::OpType" is directly included

      127 |       const llvm::SmallVector<std::pair<qc::OpType, qc::fp>, 3>& schematic,
          |                                             ^
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:128:13: warning: [misc-include-cleaner]

    no header providing "mlir::Value" is directly included

       14 |       mlir::Value inQubit) {
          |             ^
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:131:23: warning: [misc-include-cleaner]

    no header providing "qc::RZ" is directly included

      131 |       if (type == qc::RZ) {
          |                       ^
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:135:30: warning: [misc-include-cleaner]

    no header providing "qc::RY" is directly included

      135 |       } else if (type == qc::RY) {
          |                              ^
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:167:21: warning: [misc-include-cleaner]

    no header providing "std::fmod" is directly included

       15 |       auto r = std::fmod(a, b);
          |                     ^
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:168:35: warning: [misc-include-cleaner]

    no header providing "std::abs" is directly included

       15 |       return (r < 0.0) ? r + std::abs(b) : r;
          |                                   ^
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:176:44: warning: [misc-include-cleaner]

    no header providing "qc::PI" is directly included

      176 |       auto wrapped = remEuclid(angle + qc::PI, 2. * qc::PI) - qc::PI;
          |                                            ^
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:235:29: warning: [misc-include-cleaner]

    no header providing "std::array" is directly included

       15 |   [[nodiscard]] static std::array<qc::fp, 4>
          |                             ^
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:243:24: warning: [misc-include-cleaner]

    no header providing "std::arg" is directly included

       15 |     auto detArg = std::arg(determinant(unitaryMatrix));
          |                        ^
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:245:28: warning: [misc-include-cleaner]

    no header providing "std::atan2" is directly included

      245 |     auto theta = 2. * std::atan2(std::abs(unitaryMatrix.at(getIndex(1, 0))),
          |                            ^
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:245:39: warning: [misc-include-cleaner]

    no header providing "std::abs" is directly included

      245 |     auto theta = 2. * std::atan2(std::abs(unitaryMatrix.at(getIndex(1, 0))),
          |                                       ^
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h:11:1: warning: [portability-avoid-pragma-once]

    avoid 'pragma once' directive; use include guards instead

       11 | #pragma once
          | ^
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h:14:1: warning: [misc-include-cleaner]

    included header Package.hpp is not used directly

       14 | #include "dd/Package.hpp"
          | ^~~~~~~~~~~~~~~~~~~~~~~~~
       15 | #include "ir/Definitions.hpp"
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h:24:6: warning: [misc-include-cleaner]

    no header providing "std::optional" is directly included

       21 | 
       22 | namespace mqt::ir::opt::helpers {
       23 | 
       24 | std::optional<qc::fp> mlirValueToFp(mlir::Value value);
          |      ^
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h:24:43: warning: [misc-include-cleaner]

    no header providing "mlir::Value" is directly included

       17 | 
       18 | #include <algorithm>
       19 | #include <mlir/Dialect/Arith/IR/Arith.h>
       20 | #include <mlir/IR/Operation.h>
       21 | 
       22 | namespace mqt::ir::opt::helpers {
       23 | 
       24 | std::optional<qc::fp> mlirValueToFp(mlir::Value value);
          |                                           ^
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h:35:15: warning: [misc-include-cleaner]

    no header providing "std::nullopt" is directly included

       35 |   return std::nullopt;
          |               ^
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h:50:27: warning: [misc-include-cleaner]

    no header providing "llvm::dyn_cast" is directly included

       17 |     if (auto attr = llvm::dyn_cast<mlir::FloatAttr>(op.getValue())) {
          |                           ^
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h:50:42: warning: [misc-include-cleaner]

    no header providing "mlir::FloatAttr" is directly included

       17 |     if (auto attr = llvm::dyn_cast<mlir::FloatAttr>(op.getValue())) {
          |                                          ^
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h:84:55: warning: [misc-include-cleaner]

    no header providing "std::fmod" is directly included

       19 |           value, [](qc::fp a, qc::fp b) { return std::fmod(a, b); })) {
          |                                                       ^
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h:106:27: warning: [misc-include-cleaner]

    no header providing "std::vector" is directly included

       21 | [[nodiscard]] inline std::vector<qc::fp> getParameters(UnitaryInterface op) {
          |                           ^
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h:116:26: warning: [misc-include-cleaner]

    no header providing "qc::OpType" is directly included

       16 | [[nodiscard]] inline qc::OpType getQcType(UnitaryInterface op) {
          |                          ^
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h:118:16: warning: [misc-include-cleaner]

    no header providing "std::string" is directly included

       21 |     const std::string type = op->getName().stripDialect().str();
          |                ^
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h:119:16: warning: [misc-include-cleaner]

    no header providing "qc::opTypeFromString" is directly included

      119 |     return qc::opTypeFromString(type);
          |                ^
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h:120:23: warning: [misc-include-cleaner]

    no header providing "std::invalid_argument" is directly included

       21 |   } catch (const std::invalid_argument& /*exception*/) {
          |                       ^
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h:128:3: warning: [misc-const-correctness]

    variable 'isSingleQubitOp' of type 'bool' can be declared 'const'

      128 |   bool isSingleQubitOp =
          |   ^
          |        const 
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h:130:3: warning: [misc-include-cleaner]

    no header providing "assert" is directly included

       19 |   assert(isSingleQubitOp == qc::isSingleQubitGate(getQcType(op)));
          |   ^
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h:130:33: warning: [misc-include-cleaner]

    no header providing "qc::isSingleQubitGate" is directly included

      130 |   assert(isSingleQubitOp == qc::isSingleQubitGate(getQcType(op)));
          |                                 ^
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h:134:40: warning: [misc-include-cleaner]

    no header providing "dd::GateMatrix" is directly included

       13 | [[nodiscard]] inline std::optional<dd::GateMatrix>
          |                                        ^

Have any feedback or feature suggestions? Share it here.

Copy link

codecov bot commented Sep 11, 2025

Codecov Report

❌ Patch coverage is 69.56522% with 63 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h 38.0% 52 Missing ⚠️
...ect/MQTOpt/Transforms/GateDecompositionPattern.cpp 91.3% 10 Missing ⚠️
...ib/Dialect/MQTOpt/Transforms/GateDecomposition.cpp 85.7% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@taminob
Copy link
Collaborator Author

taminob commented Sep 11, 2025

@burgholzer if you have some time to spare, feel free to have a look at the implementation.
While some parts are probably simpler in PDLL, I think that most parts should remain in C++ for this pass due to the more complex logic.
I pretty much 1:1 copied the logic from the Qiskit KAK decomposition and decided to stick with a ZYZ decomposition for now, do you think there is any benefit in offering different ones like in Qiskit (are there architectures which do not support Z/Y?)?

The results of the rotation test case were verified using https://www.cda.cit.tum.de/app/ddvis/:

OPENQASM 2.0;
include "qelib1.inc";

qreg q[1];
creg c[1];

gphase(-3.1415926535897931);
rz(-1.5707963267948966) q[0];
ry(2.2831853071795867) q[0];
rz(1.5707963267948966) q[0];

measure q -> c;

and

OPENQASM 2.0;
include "qelib1.inc";

qreg q[1];
creg c[1];

rx(1) q[0];
rx(1) q[0];
rx(1) q[0];
rx(1) q[0];

measure q -> c;

More test cases are probably desirable, do you have any suggestions for what to test? Also, floating-point imprecisions could make it necessary to reduce the precision of the CHECK statements.

@taminob taminob requested a review from burgholzer September 11, 2025 13:35
@taminob taminob marked this pull request as ready for review September 11, 2025 13:35
@taminob taminob force-pushed the taminob/mlir-gate-decomposition-pass branch from 65f913b to 7fddcb5 Compare September 11, 2025 13:37
Copy link
Member

@burgholzer burgholzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many thanks @taminob for this initial draft!
As I already alluded to previously, I have quite some comments on this PR and how it is currently set up.
As these are quite fundamental, I'd propose to try to split this up into a sequence of smaller PRs. I'd also propose to get the swap reconstruction and elision PR merged first in order to not have to many open PRs that try to add various things. After that, the best step is probably to extend the unitary interface with the necessary methods and implementations to facilitate the implementation here in a series of small but dedicated PRs. This will entail an investigation for how to best deal with linear algebra in MLIR (as this will be necessary for properly defining the unitaries of gates). Let's take it from there.

Comment on lines +39 to +41
let summary = "This pass will perform various gate decompositions to simplify the used gate set.";
let description = [{
}];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let summary = "This pass will perform various gate decompositions to simplify the used gate set.";
let description = [{
}];
let summary = "This pass performs various gate decompositions to translate quantum gates being used.";
let description = [{
}];

also needs a full description.

Comment on lines +23 to +25
/**
* @brief This pass attempts to cancel consecutive self-inverse operations.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to be fixed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd argue that headers should be put into the include directory.
But even more so, I am not a big fan of "helpers" or "utils" files. They tend to become a huge pile of unrelated functions that would be better suited elsewhere.

Most of the functions here should most likely be part of the unitary interface.
More precisely:

  • checking whether something is a single-qubit operation
  • getting the unitary belonging to an operation implementing the unitary interface.
  • getting the kind of operation (although this is kind of already possible by simply matching against the actual type)

Further comments on the rest to follow.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For headers, I guess it's a personal preference that "private" headers (only used in source files) are also in src - but I'll remove this file anyway to incorporate it into the file of the gate decomposition pattern or the unitary interface.

I already had a look at how to incorporate the matrix definitions into the unitary interface, but didn't find a satisfying solution to incorporate it without repeating the definitions (which as you said is the way to go since there should be no dependency to the DD target) and also how to deal with the fact that not all operations have a trivial 4x4 or 8x8 matrix. I'll have a look at the definition property of Qiskit that you mentioned below.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that private headers could be placed in the src tree. However, we haven't needed such headers in basically all of the MQT so far. Probably due to the fact that we typically follow a pretty object oriented design where these free functions would be private methods in the respective classes.

As for the unitary definitions:
Duplication is unavoidable and actually to some degree intended here. The fact that these unitary definitions are not part of the MQT CoreIR library is a pure consequence of history. Ideally these definitions would be part of the core IR from the start; which is essentially what we want to do here.

Small corrections: single qubit gate matrices are 22 and two-qubit gate matrices are 44.
For the static gates without parameters, this should be extremely straightforward. For the parameterized gates, it is no longer as straight forward, but should still be doable.
Any parameterized operation has mlir::Values for its parameters (or static attributes). These are the inputs to the unitary getter. Gates without parameters simply use the empty variadic list. If all the parameters are either static attributes or values that are constants, then the result is a fully specified complex-valued unitary. If any of the parameters is not a compile-time constant, the resulting matrix remains parametrized by the respective value.
Decompositions might not be applicable there or might need to be deferred to runtime.

Generally, I believe the tensor dialect of MLIR together with the linalg one seems like a very natural fit for representing this.
One could "simply" convert from the (single-qubit) unitary gates to corresponding tensors, runs of those could be contracted, the result could be converted back to a unitary operation according to the given decomposition.

Just some thoughts..

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for the unitary definitions: Duplication is unavoidable and actually to some degree intended here. The fact that these unitary definitions are not part of the MQT CoreIR library is a pure consequence of history. Ideally these definitions would be part of the core IR from the start; which is essentially what we want to do here.

I think I might have misunderstood you before. So you want to have the matrix definitions in CoreIR & CoreDD and not in MLIR & CoreDD, is that correct? Since you mentioned before that you also don't like to have a dependency to CoreIR, I expected to implement this in the MLIR library.

Small corrections: single qubit gate matrices are 2_2 and two-qubit gate matrices are 4_4.

My bad, of course - thanks for the correction.

For the static gates without parameters, this should be extremely straightforward. For the parameterized gates, it is no longer as straight forward, but should still be doable. Any parameterized operation has mlir::Values for its parameters (or static attributes). These are the inputs to the unitary getter. Gates without parameters simply use the empty variadic list. If all the parameters are either static attributes or values that are constants, then the result is a fully specified complex-valued unitary. If any of the parameters is not a compile-time constant, the resulting matrix remains parametrized by the respective value. Decompositions might not be applicable there or might need to be deferred to runtime.

If you look at

inline std::optional<qc::fp> mlirValueToFp(mlir::Value value) {
, I'm not sure if it is actually that straight-forward. I didn't find any elegant way to actually execute MLIR operations and we can't just assume ConstantOps because in e.g. merge-rotation-gates we also introduce e.g. AddFOps.

Generally, I believe the tensor dialect of MLIR together with the linalg one seems like a very natural fit for representing this. One could "simply" convert from the (single-qubit) unitary gates to corresponding tensors, runs of those could be contracted, the result could be converted back to a unitary operation according to the given decomposition.

Especially for the datatypes, matrix multiplications and kronecker products (necessary for 2-qubit series), this is a good idea. I'll look into it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for not being clear here (at a big event and only on the phone between sessions).

I definitely meant to have the definitions in MLIR. That is "the place to be" for newly added functionality at the moment.

As for the merge rotation pass introducing floating point operations: a constant propagation and folding pass should eliminates those operations. We have the first precedent of running existing MLIR passes as part of testing (see the latest IfElse PR that was merged a couple of days back). This already shows how to run passes on modules, which could prove useful here.

Similarly, conversions to linalg and then running transformation passes on that before converting back to unitary operations could be one way to get to our goal.

I figured from the very start that this feature will not be as easy to cleanly and idiomatically port over from existing software to MLIR.
However, all of these learnings are well worth writing down in some kind of formal document, so I believe it is truly worth to find the "right" way to do this.


get_property(dialect_libs GLOBAL PROPERTY MLIR_DIALECT_LIBS)
set(LIBRARIES ${dialect_libs} MQT::CoreIR)
set(LIBRARIES ${dialect_libs} MQT::CoreIR MQT::CoreDD)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should generally avoid to add further dependencies to existing MQT Core libraries if at all possible. On the contrary, we'd rather like to replace the MQT::CoreIR dependency in the other libraries with the MLIR one.
The necessary functionality here should boil down to getting the unitary of an operation implementing the unitary interface.
This is something that should be directly built into the interface and its implementation. Otherwise, we will never get a clear separation of concerns.
At the moment, I see no need to refactor the DD code to rely on the MLIR representation.

In the context of providing the unitary for a gate, it might make sense to, additionally, adopt something fairly similar to OpenQASM's and Qiskit's definition property, which essentially describes how to build the gate from known, already defined gates (starting with just the global phase gate and the control, inverse, and power modifiers). This might be beyond the scope of this PR though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understood that when you said that we already have the definitions that we want to re-use them. Good to know that you want to keep it separate 👍

return mlir::failure();
}

dd::GateMatrix unitaryMatrix = dd::opToSingleQubitGateMatrix(qc::I);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not rely on the dd::GateMatrix representation. MLIR/LLVM should have builtin support for working with matrices (or tensors) and their manipulations.
Maybe https://mlir.llvm.org/docs/Dialects/Linalg/ is a good first stop or https://www.stephendiehl.com/posts/mlir_linear_algebra/.

Generally, I would argue that we should be taking advantage of MLIR's infrastructure as much as possible.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the pointer, I'll look into it.

Comment on lines +42 to +46
auto series = getSingleQubitSeries(op);
if (series.size() <= 3) {
// TODO: find better way to prevent endless optimization loop
return mlir::failure();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely not ideal as a termination criterion, as there may still be better decompositions, e.g., for a sequence of two gates.
I think it pays off to take some inspiration from https://github.com/Qiskit/qiskit/blob/stable/2.1/crates/synthesis/src/euler_one_qubit_decomposer.rs here.

Copy link
Collaborator Author

@taminob taminob Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure what you mean here, the file does not contain the creation/detection of the gate series, I think that's in https://github.com/Qiskit/qiskit/blob/stable/2.1/crates/transpiler/src/passes/unitary_synthesis.rs. I can have a look again if I can find their abort criterion.

A naive approach I thought of was alternatively checking if the gate series only consists of Z/Y (so the used rotation gates for the decomposition). Does not catch everything, but at least better than this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I meant to imply following the import chain for the one qubit gate sequence struct. 🙂

I wouldn't tie the abortion criterion to a specific decomposition.

* indicating that they have been altered from the originals.
*/
[[nodiscard]] static std::array<qc::fp, 4>
paramsZyzInner(dd::GateMatrix unitaryMatrix) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it would be quite useful to not limit this gate decomposition to ZYZ, but to also include further bases. Different quantum computers provide different gate sets, which is why the different decompositions are almost a requirement.

  • the general circuit_kak method in Qiskit already provides a very solid basis for ZYZ, ZXZ, XZX, XYX
  • beyond that one of the decompositions including the SX gate would be interesting (native to IBM systems). PSX

While it is not extremely important for this PR here itself, it is fairly important for setting up the respective infrastructure to expand it in the future.

Similar to Qiskit, the type of decomposition should be a parameter of the pattern (and subsequently of the pass). Or just of the pass and depending on that parameter, the respective pattern is added to the pipeline (this could make more sense).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it is a good idea to add the other gates for the KAK decomposition to this pattern, but don't think it's a good idea to stuff every decomposition into this pattern. But you already said that it is probably out-of-scope for this PR anyway and it can be easily extended by adding additional patterns.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or just of the pass and depending on that parameter, the respective pattern is added to the pipeline (this could make more sense).

I believe the above is what makes sense for this PR. The available patterns in the beginning may only contain the different KAK decompositions, but the architecture should be flexible enough so that future PRs could easily add more patterns to the pass.

@taminob
Copy link
Collaborator Author

taminob commented Sep 17, 2025

I'll try to summarize here the next steps for this PR - each step is its own PR in my opinion:

  • create ConstantFolding pass which tries to execute all floating point values and replaces their uses by a constant to eliminate all float operations (using mlir::PassManager, see ✨ Support importing IfElseOpererations to MQTRef #1164 runPasses)
  • extend MQTOpt's UnitaryInterface by matrix function similar to Qiskit's Gate.definition property which will return the definition which must be defined in StdOps.td for each gate (since we don't know the actual size, we unfortunately must store this in a std::vector instead of std::array which will make compile-time assumptions harder to check); needs further experimentation since MLIR's Tablegen seems to make it impossible to directly declare new C++ attributes, probably via some new Trait
  • extend MQTOpt's UnitaryInterface by isSingleQubitGate() and isTwoQubitGate() functions
  • keep logic of this PR as-is, but use newly introduced helpers instead; add template parameter (or would you prefer run-time selection via constructor parameter?) to pattern class to allow pass to select decomposition (for all KAK decompositions available in Qiskit, not U3 etc.); I looked into the linalg and tensor Dialects and while it is possible to define the matrix multiplications in MLIR and then execute it via the mlir::PassManager (see first bullet point), I don't think that makes sense when instead we can either use an actual C++ LinAlg library or quickly define the very limited kind of matrix multiplication that we need as a helper (or did I misunderstand your intention?)

@burgholzer did I miss anything or do you have something else in mind for one of these steps?

@burgholzer
Copy link
Member

I'll try to summarize here the next steps for this PR - each step is its own PR in my opinion:

Couple of comments in addition to what you already nicely summarised (thanks for that!)

A lot of the inspiration for how to do this might come from the way Catalyst is handling this:

They essentially define something they refer to as the CompilerDriver, which actually orchestrates the passes being run and their sequence. Eventually this culminates in a CLI utility that can be called on programs. I believe this is something that, eventually, we want to be exposing to Python so that people have a high-level entry point to the compiler infrastructure very similar to Qiskit's transpire function. I think it would be fair to collect this information in a dedicated (sub-)issue as part of the overall MLIR Predictor epic. Eventually the predictor could be seen as a RL-based CompilerDriver. Before facilitating that, we definitely need a solid foundation for a "standard" compiler pipeline.

Just browsing through the above sources, this entails concepts like:

  • PassInstrumentation for registering callbacks
  • Parsing of text-based representations of programs
  • PassManager
  • CompilerOptions
  • Pipeline

But, naturally, our driver would be much simpler as we don't (yet) support as diverse of a set of dialects and programs as Catalyst does.
CUDA-Q seems to be going a bit of a different route here: https://github.com/NVIDIA/cuda-quantum/tree/main/tools/nvqpp
It might be worth checking with the literature, what the classical community is doing in these cases.
I'd also personally put this as the second issue here instead of the first, because I think it would be beneficial to first get the underlying dialect infrastructure on a solid basis before building the compiler pipeline on top of it.

  • extend MQTOpt's UnitaryInterface by matrix function similar to Qiskit's Gate.definition property which will return the definition which must be defined in StdOps.td for each gate (since we don't know the actual size, we unfortunately must store this in a std::vector instead of std::array which will make compile-time assumptions harder to check); needs further experimentation since MLIR's Tablegen seems to make it impossible to directly declare new C++ attributes, probably via some new Trait
  • extend MQTOpt's UnitaryInterface by isSingleQubitGate() and isTwoQubitGate() functions

I believe these two points might actually be combined, because they are fairly related to another.
I'd also argue that we should strive for as much compile-time knowledge as possible. We should be able to achieve this through specialisations of the interface or through (possibly existing) traits. We already have the traits for the target arity of a gate as well as the indication on how many parameters a certain gate has. These are fixed for every single gate.
These traits should allow us to fully define the shape of the unitary matrix definition at compile time for all known operations. Fleshing this out will most likely also help with #1119 as I believe we actually have all the information available via traits to specify how the most compact builder for an operation would have to look like. I am just not fully sure how to best make use of the traits for this purpose, but I would very much assume that we are not the first people to face such a problem in the broader community. So there has to be prior art for this.
Connecting this to your second point above, the target arity technically is already communicated through the traits.
Last comment on this part here: we probably should not use the std:: data types here but makes use of LLVM's optimised data types as much as possible.

  • keep logic of this PR as-is, but use newly introduced helpers instead; add template parameter (or would you prefer run-time selection via constructor parameter?) to pattern class to allow pass to select decomposition (for all KAK decompositions available in Qiskit, not U3 etc.); I looked into the linalg and tensor Dialects and while it is possible to define the matrix multiplications in MLIR and then execute it via the mlir::PassManager (see first bullet point), I don't think that makes sense when instead we can either use an actual C++ LinAlg library or quickly define the very limited kind of matrix multiplication that we need as a helper (or did I misunderstand your intention?)

I agree that it is probably overkill to use tensor/linalg operations here. I was just wondering whether they might provide some of the functionality that is implemented in this pass that goes beyond simple matrix multiplication, so we do not have to reinvent it from scratch. But I suppose the actual code is moderately self contained and low in complexity that we can maintain it on our own. I'd personally like to avoid external Linalg libraries as much as possible because I still think we should be very mindful about our dependencies here in this project.

As for the question of templates vs. runtime specification: I believe the actual decomposition to be used as part of the CompilerDriver has to be one of the CompilerOptions being passed to the driver. So the pass itself has to be configurable at runtime. The patterns themselves could be templated to cover all different kinds of decompositions with one single (templated) class that is instantiated multiple times as part of the pass.

I hope this provides you with enough context to proceed. Don't hesitate to ask further questions as they pop up!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Anything related to C++ code enhancement New feature or request MLIR Anything related to MLIR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants