Skip to content

✨ Add absorb-swaps pass#1750

Open
jmoosburger wants to merge 38 commits into
munich-quantum-toolkit:mainfrom
jmoosburger:feat/swap-absorption-pass
Open

✨ Add absorb-swaps pass#1750
jmoosburger wants to merge 38 commits into
munich-quantum-toolkit:mainfrom
jmoosburger:feat/swap-absorption-pass

Conversation

@jmoosburger

@jmoosburger jmoosburger commented May 28, 2026

Copy link
Copy Markdown

Description

  • add new qco pass as warmup task (in cooperation with @MatthiasReumann)
  • remove leading swap-gates in a circuit
  • reorder static qubits instead

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.

If PR contains AI-assisted content:

  • I have disclosed the use of AI tools in the PR description as per our AI Usage Guidelines.
  • AI-assisted commits include an Assisted-by: [Model Name] via [Tool Name] footer.
  • I confirm that I have personally reviewed and understood all AI-generated content, and accept full responsibility for it.

@codecov

codecov Bot commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@jmoosburger jmoosburger changed the title Feat/swap absorption pass ✨ Feat/swap absorption pass May 28, 2026
@jmoosburger

Copy link
Copy Markdown
Author

@MatthiasReumann

works fine now 💪
nice setup with the CMakePresets by the way... :)

i have another conceptional question though: which of the following circuits is correct?

circuit 120

q0 ----X---------- q1
       |
q1 ----X----X----- q2
            |
q2 ---------X----- q0

or:

circuit 201

q0 ----X---------- q2
       |
q1 ----X----X----- q0
            |
q2 ---------X----- q1

in my opinion its the second circuit 201, but the implementation we discussed produces the first result.
i just wanted to make sure that i'm correct in my assumption before i fix it :)

@jmoosburger

jmoosburger commented May 28, 2026

Copy link
Copy Markdown
Author

the linter produces warnings on the one hand, but failes because of a http-error 403 (unauthorized) on the other hand. see here: https://github.com/munich-quantum-toolkit/core/actions/runs/26597942119/job/78373906843?pr=1750

is there anything i can do about it?

@denialhaag

Copy link
Copy Markdown
Member

the linter produces warnings on the one hand, but failes because of a http-error 403 (unauthorized) on the other hand. see here: https://github.com/munich-quantum-toolkit/core/actions/runs/26597942119/job/78373906843?pr=1750

is there anything i can do about it?

Unfortunately, the action cannot post comments on PRs coming from a fork. 😕

That said, the report can also be found in the summary of the run: https://github.com/munich-quantum-toolkit/core/actions/runs/26597942119/attempts/1#summary-78373906843

@denialhaag denialhaag changed the title ✨ Feat/swap absorption pass ✨ Add absorb-swaps pass May 29, 2026
@denialhaag denialhaag added feature New feature or request MLIR Anything related to MLIR labels May 29, 2026
@denialhaag denialhaag added this to the MLIR Support milestone May 29, 2026
@MatthiasReumann

Copy link
Copy Markdown
Collaborator

@jmoosburger

i have another conceptional question though: which of the following circuits is correct?

The best way to visualize this is to apply the SWAPs sequentially:

circuit 120

q0 ----X-- q1 ------------ q1
       |
q1 ----X-- q0 --X-- q2 --- q2
                |
q2 -------------X-- q0 --- q0

@jmoosburger

Copy link
Copy Markdown
Author

the linter produces warnings on the one hand, but failes because of a http-error 403 (unauthorized) on the other hand. see here: https://github.com/munich-quantum-toolkit/core/actions/runs/26597942119/job/78373906843?pr=1750
is there anything i can do about it?

Unfortunately, the action cannot post comments on PRs coming from a fork. 😕

That said, the report can also be found in the summary of the run: https://github.com/munich-quantum-toolkit/core/actions/runs/26597942119/attempts/1#summary-78373906843

so if i resolve all the warnings the lint check will succeed, as it does not post a comment?

@MatthiasReumann

MatthiasReumann commented May 29, 2026

Copy link
Copy Markdown
Collaborator

so if i resolve all the warnings the lint check will succeed, as it does not post a comment?

If you resolve all warnings the CI / Lint / 🚨 Lint (pull request) workflow will succeed. The pull request will look like this one.

@jmoosburger

Copy link
Copy Markdown
Author

so if i resolve all the warnings the lint check will succeed, as it does not post a comment?

If you resolve all warnings the CI / Lint / 🚨 Lint (pull request) workflow will succeed. The pull request will look like this one.

Which way is recommended?
the contribution.md suggests clang-tidy and the section Development Setup in installation.md suggests nox -s lint...

jmoosburger and others added 2 commits June 2, 2026 11:15
Co-authored-by: matthias <matthias@bereumann.com>
Signed-off-by: Johannes Moosburger <96540096+jmoosburger@users.noreply.github.com>
Signed-off-by: Johannes Moosburger <96540096+jmoosburger@users.noreply.github.com>
@jmoosburger

Copy link
Copy Markdown
Author

It took us a pretty long time and three pull requests but we are finally here! Congratulations on your potentially first contribution to the MLIR compiler collection 🎉

I've left some small remarks which should be fairly easy to integrate. Otherwise, great job! 🚀

Thanks for your help and patience!
Hopefully there will be more in the future 🥳

@MatthiasReumann

Copy link
Copy Markdown
Collaborator

@jmoosburger I've pushed some minor changes directly.

Let's see what @burgholzer has to say, but I think this one should be good to go 🚀

@mergify mergify Bot added the conflict label Jun 3, 2026
@mergify mergify Bot added conflict and removed conflict labels Jun 5, 2026
@mergify mergify Bot added conflict and removed conflict labels Jun 15, 2026
Signed-off-by: burgholzer <burgholzer@me.com>
Signed-off-by: burgholzer <burgholzer@me.com>
@mergify mergify Bot removed the conflict label Jun 18, 2026

@burgholzer burgholzer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Finally found some time to look at this. Thanks for your work on this @jmoosburger and @MatthiasReumann 🙏🏼

As you will probably see from the comments, I am not quite happy with this yet. Most of the comments are inline and should hopefully be rather self-explanatory. Let me know if anything is not clear.

One other issue that I still have with this is that the new pass is not added to the default pass pipeline, which is an oversight in my opinion. Why would we implement a pass when we do not intend to use it?

Comment thread CHANGELOG.md Outdated
SmallVector<WireIterator> wires;
do {
wires.clear();
for (auto op : func.getOps<StaticOp>()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This will collect all static operations repeatedly in every iteration of the do...while loop, which seems fairly wasteful. Pretty sure there is a more efficient way to do this.

ModuleOp anchor = getOperation();
IRRewriter rewriter(&getContext());

for (auto func : anchor.getOps<func::FuncOp>()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am really wondering about the efficiency of this pass. I already pushed a couple of improvements, but I can't help it and feel like this is not the way to go.

Is there any particular reason why this pass cannot simply be one that matches on StaticOp and checks for subsequent SWAPOp that also have a StaticOp as there second input?
Something as simple as

/**
 * @brief Absorb SWAP operations into static qubit allocations
 */
struct RemoveSWAPAfterStaticAllocation final : OpRewritePattern<SWAPOp> {
  using OpRewritePattern::OpRewritePattern;

  LogicalResult matchAndRewrite(SWAPOp op,
                                PatternRewriter& rewriter) const override {
    auto qubit0 = op.getInputQubit(0);
    if (!isa<StaticOp>(qubit0.getDefiningOp())) {
      return failure();
    }
    auto qubit1 = op.getInputQubit(1);
    if (!isa<StaticOp>(qubit1.getDefiningOp())) {
      return failure();
    }
    rewriter.replaceOp(op, {qubit1, qubit0});
    return success();
  }
};

This feels way simpler and more efficient than the pass implementation here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

i'll have a look at that 👍

Comment on lines +205 to +214
let summary = "This pass absorbs SWAP operations into the initial "
"program-to-hardware mapping.";
let description = [{
For a SWAP operation exchanging static qubits q0 and q1, the pass replaces the use of the
first (second) input qubit with the second (first) output qubit of the SWAP and subsequently
removes the operation. As a result, the initial program-to-hardware mapping is changed.
This process is repeated until no more SWAP operations can be absorbed.

The pass assumes that the quantum program is already mapped to static qubits.
}];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the wording here could be improved; maybe even the naming of the pass itself. As it stands, the name of the pass is not self explanatory.
"initial program-to-hardware mapping" is something that is oddly specific for a pass that just generally matches static operations and checks if any SWAPs are directly applied after the "allocation".

I am even wondering whether this should actually be limited to StaticOp. Why not extend the logic to AllocOp as well? In the pattern rewrite proposed in the other comment, that is likely to be fairly simple. Taking it one step further, qtensor.load could also qualify for such a simplification. And to go even further beyond, any mixture of StaticOp, AllocOp or LoadOp can equally be simplified.

Comment on lines +59 to +80
TEST_F(SwapAbsorbPassTest, PassDoesNotChangeSwaplessProgram) {

qco::QCOProgramBuilder builder(context.get());
builder.initialize();

const auto q00 = builder.staticQubit(0);
const auto q10 = builder.staticQubit(1);

const auto q01 = builder.h(q00);
const auto [q02, q11] = builder.cx(q01, q10);

builder.sink(q02);
builder.sink(q11);

auto moduleThroughPass = builder.finalize();
auto originalModule = moduleThroughPass->clone();

applySwapAbsorb(moduleThroughPass);
ASSERT_TRUE(mlir::OperationEquivalence::isEquivalentTo(
moduleThroughPass.get(), originalModule,
mlir::OperationEquivalence::Flags::IgnoreLocations));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Kind of a pointless test. Why would the pass change a program that does not contain the single operation the pass actually matches on?

@jmoosburger jmoosburger Jun 20, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

for me its a requirement that circuits without swaps remain untouched.
from the tests point of view it's not possible to determine where exactly the pass matches on.
you cannot be sure a future implementation of this pass accidentally changes a circuit without a swap unless you test it.

nevertheless, feel free to reach out, if you are uncomfortable with this. i'll remove this test then 👍

Comment on lines +92 to +93
const auto q02 = builder.id(q01);
const auto q12 = builder.id(q11);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Kind of pointless to use identity gates here, as they would generally be optimized away. Prefer to use non-trivial gates, if at all.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

it doesn't matter, which single qubit gate is used at this place, to test this particular pass. a gate is required to check, whether the pass reorders the circuit correctly. i took identity as it is the simplest one and does not add functionality to the test circuit

Comment on lines +101 to +102
ASSERT_EQ(q10, ((IdOp)q02.getDefiningOp()).getInputQubit(0));
ASSERT_EQ(q00, ((IdOp)q12.getDefiningOp()).getInputQubit(0));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do not use C-style casts. I am surprised clang-tidy is not screaming at this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done and replaced with mlir:cast

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These tests do not really follow the structure of most of the other tests, which always build a reference program and use our IR verifier to check for equivalence.
I'd prefer to have this as uniform as possible.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

i'll have a look at that 👍

Co-authored-by: Lukas Burgholzer <burgholzer@me.com>
Signed-off-by: Johannes Moosburger <96540096+jmoosburger@users.noreply.github.com>
@jmoosburger

Copy link
Copy Markdown
Author

@burgholzer

thanks for your feedback 🙏
i'll work it through as soon as possible

One other issue that I still have with this is that the new pass is not added to the default pass pipeline, which is an oversight in my opinion. Why would we implement a pass when we do not intend to use it?

this is actually no oversight but results from this comment: #1661 (comment)

denialhaag and others added 4 commits June 20, 2026 16:48
## Description

This PR should improve the stability of some of the timing-based QDMI
tests. We have recently seen them failing more and more.

## Checklist

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

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request MLIR Anything related to MLIR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants