Skip to content

✨ Add Quantum IPO Pass to mqt-cc - #1970

Closed
DRovara wants to merge 142 commits into
mainfrom
mlir/ipo-pass
Closed

✨ Add Quantum IPO Pass to mqt-cc#1970
DRovara wants to merge 142 commits into
mainfrom
mlir/ipo-pass

Conversation

@DRovara

@DRovara DRovara commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR introduces the IPO passes from the corresponding paper to mqt-cc.
It includes:

  • Quantum Argument Promotion
  • Auxiliary Qubit Hoisting
  • Quantum Function Boundary Commutation
  • QuantumContextSensitiveSpecialization

AI Notice: Gemini 3.1 Pro and Claude Opus 5.0 were used for the generation of some code.

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:

  • Any agent that created, edited, or submitted GitHub content was explicitly authorized for that scope, as required by our AI Usage Guidelines.
  • Every agent-authored or agent-edited public text body begins with the visible disclosure 🤖 *AI text below* 🤖 (titles are exempt).
  • I have disclosed AI assistance in the PR description.
  • I confirm that I have personally reviewed and understood all AI-generated content, and accept full responsibility for it.

DRovara and others added 30 commits July 14, 2026 03:11
Signed-off-by: Damian Rovara <93778306+DRovara@users.noreply.github.com>
DRovara added a commit that referenced this pull request Aug 20, 2026
`populateQuantumIPOPipeline` runs specialization, argument promotion, auxiliary
qubit hoisting and boundary commutation in the order they are meant to compose,
and registers them as the `quantum-ipo` pipeline. Commutation runs twice because
it can expose further cancellations. The passes stay individually registered, so
a caller assembling its own pipeline can pick only the ones it wants.

`test_qco_quantum_ipo.cpp` keeps only the scenarios that need more than one
pass. Everything about a single pass lives in that pass's own suite, scheduled
on the pass alone.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRovara added a commit that referenced this pull request Aug 24, 2026
`populateQuantumIPOPipeline` runs specialization, argument promotion, auxiliary
qubit hoisting and boundary commutation in the order they are meant to compose,
and registers them as the `quantum-ipo` pipeline. Commutation runs twice because
it can expose further cancellations. The passes stay individually registered, so
a caller assembling its own pipeline can pick only the ones it wants.

`test_qco_quantum_ipo.cpp` keeps only the scenarios that need more than one
pass. Everything about a single pass lives in that pass's own suite, scheduled
on the pass alone.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRovara added a commit that referenced this pull request Aug 24, 2026
`populateQuantumIPOPipeline` runs specialization, argument promotion, auxiliary
qubit hoisting and boundary commutation in the order they are meant to compose,
and registers them as the `quantum-ipo` pipeline. Commutation runs twice because
it can expose further cancellations. The passes stay individually registered, so
a caller assembling its own pipeline can pick only the ones it wants.

`test_qco_quantum_ipo.cpp` keeps only the scenarios that need more than one
pass. Everything about a single pass lives in that pass's own suite, scheduled
on the pass alone.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRovara added a commit that referenced this pull request Aug 25, 2026
`populateQuantumIPOPipeline` runs specialization, argument promotion, auxiliary
qubit hoisting and boundary commutation in the order they are meant to compose,
and registers them as the `quantum-ipo` pipeline. Commutation runs twice because
it can expose further cancellations. The passes stay individually registered, so
a caller assembling its own pipeline can pick only the ones it wants.

`test_qco_quantum_ipo.cpp` keeps only the scenarios that need more than one
pass. Everything about a single pass lives in that pass's own suite, scheduled
on the pass alone.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRovara added a commit that referenced this pull request Aug 25, 2026
`populateQuantumIPOPipeline` runs specialization, argument promotion, auxiliary
qubit hoisting and boundary commutation in the order they are meant to compose,
and registers them as the `quantum-ipo` pipeline. Commutation runs twice because
it can expose further cancellations. The passes stay individually registered, so
a caller assembling its own pipeline can pick only the ones it wants.

`test_qco_quantum_ipo.cpp` keeps only the scenarios that need more than one
pass. Everything about a single pass lives in that pass's own suite, scheduled
on the pass alone.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRovara added a commit that referenced this pull request Aug 25, 2026
`populateQuantumIPOPipeline` runs specialization, argument promotion, auxiliary
qubit hoisting and boundary commutation in the order they are meant to compose,
and registers them as the `quantum-ipo` pipeline. Commutation runs twice because
it can expose further cancellations. The passes stay individually registered, so
a caller assembling its own pipeline can pick only the ones it wants.

`test_qco_quantum_ipo.cpp` keeps only the scenarios that need more than one
pass. Everything about a single pass lives in that pass's own suite, scheduled
on the pass alone.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
burgholzer pushed a commit that referenced this pull request Aug 25, 2026
…#2193)

* ✨ Walk tensor chains across calls and compare untracked qubit tensors

`TensorIterator` ends a tensor chain at `func.call` and `func.return` rather
than aborting on them, so a chain reaching a call boundary can be walked. The
tensor a call returns starts a life-chain of its own.

`areModulesEquivalentWithPermutations` no longer assumes every qubit tensor
belongs to an equivalence group. Tensors that do not descend from a `qtensor`
allocation, such as function arguments, are compared through the regular SSA
mapping, and the tensor threaded out of a `qtensor.extract` is now mapped
alongside its qubit result. Both previously aborted inside the comparison
instead of reporting a result.

Extracted from #1970 as the first of a stack of smaller contributions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 📝 Note the interprocedural optimization work in the changelog

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ✅ Cover walking a tensor chain backward from a call result

The only branch this slice added that no existing test reached: a tensor a
call hands back starts its own life-chain, so stepping back from it stops at
the call rather than continuing into the tensor that was passed in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
burgholzer pushed a commit that referenced this pull request Aug 25, 2026
`quantum-argument-promotion` replaces a qubit-tensor argument by one qubit
argument and one qubit result per element the callee actually touches, so
untouched elements no longer cross the call boundary. Call sites are rewritten
to extract before the call and re-insert afterwards.

The rewrite lifts every extraction in front of the call and sinks every
insertion behind it, which is only sound when the accesses do not depend on
each other's order. Two shapes are therefore rejected: a slot the callee writes
before reading it again, which would otherwise be served from the caller's
original tensor and silently compute the wrong thing; and an insertion that
belongs to no promoted slot, which used to survive the rewrite still using the
tensor argument erased right after and abort on MLIR's `use_empty()` assertion.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
burgholzer pushed a commit that referenced this pull request Aug 25, 2026
`populateQuantumIPOPipeline` runs specialization, argument promotion, auxiliary
qubit hoisting and boundary commutation in the order they are meant to compose,
and registers them as the `quantum-ipo` pipeline. Commutation runs twice because
it can expose further cancellations. The passes stay individually registered, so
a caller assembling its own pipeline can pick only the ones it wants.

`test_qco_quantum_ipo.cpp` keeps only the scenarios that need more than one
pass. Everything about a single pass lives in that pass's own suite, scheduled
on the pass alone.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRovara added a commit that referenced this pull request Aug 26, 2026
`populateQuantumIPOPipeline` runs specialization, argument promotion, auxiliary
qubit hoisting and boundary commutation in the order they are meant to compose,
and registers them as the `quantum-ipo` pipeline. Commutation runs twice because
it can expose further cancellations. The passes stay individually registered, so
a caller assembling its own pipeline can pick only the ones it wants.

`test_qco_quantum_ipo.cpp` keeps only the scenarios that need more than one
pass. Everything about a single pass lives in that pass's own suite, scheduled
on the pass alone.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRovara added a commit that referenced this pull request Aug 26, 2026
`populateQuantumIPOPipeline` runs specialization, argument promotion, auxiliary
qubit hoisting and boundary commutation in the order they are meant to compose,
and registers them as the `quantum-ipo` pipeline. Commutation runs twice because
it can expose further cancellations. The passes stay individually registered, so
a caller assembling its own pipeline can pick only the ones it wants.

`test_qco_quantum_ipo.cpp` keeps only the scenarios that need more than one
pass. Everything about a single pass lives in that pass's own suite, scheduled
on the pass alone.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRovara added a commit that referenced this pull request Aug 26, 2026
`populateQuantumIPOPipeline` runs specialization, argument promotion, auxiliary
qubit hoisting and boundary commutation in the order they are meant to compose,
and registers them as the `quantum-ipo` pipeline. Commutation runs twice because
it can expose further cancellations. The passes stay individually registered, so
a caller assembling its own pipeline can pick only the ones it wants.

`test_qco_quantum_ipo.cpp` keeps only the scenarios that need more than one
pass. Everything about a single pass lives in that pass's own suite, scheduled
on the pass alone.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRovara added a commit that referenced this pull request Aug 27, 2026
`populateQuantumIPOPipeline` runs specialization, argument promotion, auxiliary
qubit hoisting and boundary commutation in the order they are meant to compose,
and registers them as the `quantum-ipo` pipeline. Commutation runs twice because
it can expose further cancellations. The passes stay individually registered, so
a caller assembling its own pipeline can pick only the ones it wants.

`test_qco_quantum_ipo.cpp` keeps only the scenarios that need more than one
pass. Everything about a single pass lives in that pass's own suite, scheduled
on the pass alone.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRovara added a commit that referenced this pull request Aug 27, 2026
`WireIterator` traverses `func.call` in both directions instead of stopping
there, and `atWireStart()` reports whether a wire begins at the current
operation, which a call result does only when the callee creates the qubit.

Which result continues which operand is derived by threading the callee's
arguments through its body rather than assumed to be positional, so a callee
that hands its qubits back in a different order is followed correctly. The
derivation is cached per callee by `CallQubitMapping`, with `CallTensorMapping`
providing the same for qubit tensors. External, recursive and multi-block
callees fall back to the positional pairing.

Also fixes two defects this surfaced: threading asked for a body's terminator
before checking that one exists, which trips on a function still being built,
and `qtensor.from_elements` was not treated as a wire sink even though it
consumes qubits into a tensor exactly like `qtensor.insert`.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRovara added a commit that referenced this pull request Aug 27, 2026
`startFunction`/`endFunction` open and close a function next to `main`, and
`call` emits a `func.call` to one. Test programs spanning several functions can
now be built with the builder instead of hand-written MLIR.

Linear values are isolated per function: `startFunction` sets aside the caller's
tracked qubits and tensors and restores them in `endFunction`, so an outer value
used inside a callee is rejected rather than silently threaded, and anything the
callee leaves behind is reported as a leak.

At a call, each linear operand is paired with the result that continues it,
derived from the callee through `CallQubitMapping` and `CallTensorMapping`
rather than by position. An operand the callee keeps counts as consumed, and a
result no operand flows into as freshly created.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRovara added a commit that referenced this pull request Aug 27, 2026
`quantum-context-sensitive-specialization` redirects a `func.call` to a
specialized copy of its callee whenever the call site pins something down: a
qubit argument known to be in |0>, letting operations that fix |0> be dropped;
one known to be in |+>, letting an `x` be dropped; or a compile-time constant
rotation angle from a small distinguished set, which is folded into the copy.

Copies are shared between call sites with the same context, and a callee left
without callers is erased — but only when this pass created that situation, so
unrelated unused functions are left alone. Specializations are private, so
optimizing a public callee does not export the generated symbol.

`IPOUtils.{h,cpp}` holds the two helpers the interprocedural passes share.

The tests are scheduled on this pass alone rather than on a pipeline, so a
regression here cannot be masked by a later pass. `IPOTestFixture.h` carries the
fixture the sibling passes will reuse.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRovara added a commit that referenced this pull request Aug 27, 2026
`quantum-argument-promotion` replaces a qubit-tensor argument by one qubit
argument and one qubit result per element the callee actually touches, so
untouched elements no longer cross the call boundary. Call sites are rewritten
to extract before the call and re-insert afterwards.

The rewrite lifts every extraction in front of the call and sinks every
insertion behind it, which is only sound when the accesses do not depend on
each other's order. Two shapes are therefore rejected: a slot the callee writes
before reading it again, which would otherwise be served from the caller's
original tensor and silently compute the wrong thing; and an insertion that
belongs to no promoted slot, which used to survive the rewrite still using the
tensor argument erased right after and abort on MLIR's `use_empty()` assertion.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRovara added a commit that referenced this pull request Aug 27, 2026
`quantum-auxiliary-qubit-hoisting` turns a qubit a callee allocates and releases
itself into an extra argument and an extra result, handed back reset, so the
caller owns the allocation and can reuse one qubit across several calls.
Externally visible functions, declarations and recursive functions are left
alone.

Candidates are visited callees first, in a post-order over the call graph.
Hoisting out of a callee puts an allocation into each of its callers, which may
itself be hoistable; processing in module order used to strand that allocation
wherever the declarations happened to sit, so the same call graph gave different
results depending on declaration order.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRovara added a commit that referenced this pull request Aug 27, 2026
`quantum-function-boundary-commutation` drops a self-inverse gate applied right
before a call when the callee starts by applying the same gate to that argument.
The caller-side gate is erased and the call is redirected to a copy of the
callee without the callee-side one.

Copies are cached per callee *and* parameter index. The gate removed inside the
copy belongs to one specific argument, so sharing a copy between parameters
would drop a gate the second call site never had.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRovara added a commit that referenced this pull request Aug 27, 2026
`populateQuantumIPOPipeline` runs specialization, argument promotion, auxiliary
qubit hoisting and boundary commutation in the order they are meant to compose,
and registers them as the `quantum-ipo` pipeline. Commutation runs twice because
it can expose further cancellations. The passes stay individually registered, so
a caller assembling its own pipeline can pick only the ones it wants.

`test_qco_quantum_ipo.cpp` keeps only the scenarios that need more than one
pass. Everything about a single pass lives in that pass's own suite, scheduled
on the pass alone.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRovara added a commit that referenced this pull request Aug 28, 2026
`populateQuantumIPOPipeline` runs specialization, argument promotion, auxiliary
qubit hoisting and boundary commutation in the order they are meant to compose,
and registers them as the `quantum-ipo` pipeline. Commutation runs twice because
it can expose further cancellations. The passes stay individually registered, so
a caller assembling its own pipeline can pick only the ones it wants.

`test_qco_quantum_ipo.cpp` keeps only the scenarios that need more than one
pass. Everything about a single pass lives in that pass's own suite, scheduled
on the pass alone.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRovara added a commit that referenced this pull request Aug 28, 2026
`populateQuantumIPOPipeline` runs specialization, argument promotion, auxiliary
qubit hoisting and boundary commutation in the order they are meant to compose,
and registers them as the `quantum-ipo` pipeline. Commutation runs twice because
it can expose further cancellations. The passes stay individually registered, so
a caller assembling its own pipeline can pick only the ones it wants.

`test_qco_quantum_ipo.cpp` keeps only the scenarios that need more than one
pass. Everything about a single pass lives in that pass's own suite, scheduled
on the pass alone.

Extracted from #1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@burgholzer

Copy link
Copy Markdown
Member

I will close this one for now as well, as individual PRs have been set up.

@burgholzer burgholzer closed this Sep 1, 2026
@mergify mergify Bot removed the conflict label Sep 1, 2026
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 feature New feature or request MLIR Anything related to MLIR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants