Skip to content

Preserve constant loop bounds through adjoint-lowering#2959

Open
dime10 wants to merge 6 commits into
mainfrom
optimize-adjoint-dyn-loop-bounds
Open

Preserve constant loop bounds through adjoint-lowering#2959
dime10 wants to merge 6 commits into
mainfrom
optimize-adjoint-dyn-loop-bounds

Conversation

@dime10

@dime10 dime10 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

As laid out in #2936, the --adjoint-lowering pass converts all classical values used by the reverse pass to dynamic ones via a tape caching mechanism (LIFO list.push/pop). This hinders analyzability for example in qp.specs. This PR skips the tape mechanism for constant loop bounds.

As a pre-requisite to implementing the change, the pass is restructured into a more coherent structure (all in the same directory) of pass driver (AdjointLowering.cpp), forward pass / classical value caching (Forward.cpp), reverse pass / inverse quantum instruction stream & adjoint propagation (Reverse.cpp), and a helper data structure (QuantumCache.cpp/hpp). The actual functional change is very small (2f6ad9e).

closes #2936
[sc-122139]

Future improvements: One could apply this fix to any constant classical value cached by the tape.

Additional note: To "fix" the example in the issue one should run the canonicalization pass before the adjoint lowering (standard in our pipeline) so that the detection of constant values is facilitated.

dime10 added 4 commits June 17, 2026 13:33
Relocate the forward pass into the same location as the reverse pass, as
having the two pieces of the pass in different locations is very
confusing. A subdirectory is introduced for the pass for better
organization.
@dime10 dime10 requested a review from a team June 17, 2026 17:44
@codecov

codecov Bot commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.97%. Comparing base (8a5fc60) to head (11981dc).
⚠️ Report is 9 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2959      +/-   ##
==========================================
+ Coverage   96.95%   96.97%   +0.02%     
==========================================
  Files         166      166              
  Lines       19143    19209      +66     
  Branches     1772     1788      +16     
==========================================
+ Hits        18560    18628      +68     
+ Misses        430      429       -1     
+ Partials      153      152       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@paul0403 paul0403 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.

Nice!

@dime10 dime10 requested a review from a team June 18, 2026 17:48

@joeycarter joeycarter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good, just a few editorial things and questions—nothing blocking. 👍

Comment thread doc/releases/changelog-dev.md Outdated
Comment thread mlir/lib/Quantum/Transforms/AdjointLowering/AdjointLowering.cpp Outdated
Comment thread mlir/lib/Quantum/Transforms/AdjointLowering/QuantumCache.cpp Outdated
Comment thread mlir/lib/Quantum/Transforms/AdjointLowering/QuantumCache.hpp Outdated
Comment on lines +423 to +429
// CHECK: catalyst.list_push %arg0
// CHECK: [[stop:%.+]] = catalyst.list_pop
// CHECK: [[for_out:%.+]]:2 = scf.for {{%.+}} = [[start]] to [[stop]] step [[step]]
%8:2 = scf.for %arg3 = %c0 to %stop step %c1 iter_args(%arg5 = %arg1, %arg6 = %arg2) -> (!quantum.bit, !quantum.bit) {
%out_qubits:2 = quantum.custom "gate1"() %arg5, %arg6 : !quantum.bit, !quantum.bit
%out_qubits_0:2 = quantum.custom "gate2"() %out_qubits#0, %out_qubits#1 : !quantum.bit, !quantum.bit
scf.yield %out_qubits_0#0, %out_qubits_0#1 : !quantum.bit, !quantum.bit

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The previous test checked that the order of gate1 and gate2 was reversed and that the adj unit attribute was added. Is it worth doing that here as well, or would it be redundant?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The reason is that the previous one was an existing test checked the action of the adjoint pass (the reversing you mentioned). This PR is only about preserving constants, which is the behaviour I'm unit testing here, the pass action is otherwise not relevant.
To be fair I also added these checks to the existing test, it might have been cleaner to create two new ones I suppose.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not critical, up to you.

Comment on lines +46 to +59
// TODO: Generalize to arbitrary dimensions
if (2 != shape.size()) {
op->emitOpError() << "Caching only supports tensors complex F64";
}
// TODO: Generalize to other types
Type elementType = aTensorType.getElementType();
if (!isa<ComplexType>(elementType)) {
op->emitOpError() << "Caching only supports tensors complex F64";
}
// TODO: Generalize to other types
Type f64 = cast<ComplexType>(elementType).getElementType();
if (!f64.isF64()) {
op->emitOpError() << "Caching only supports tensors complex F64";
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Optional] All the error messages are the same; could these checks be condensed into a single if statement? Or is this just to maintain a bit more flexibility in the future?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I touched as little logic as I could, if you go commit by commit it's a bit easier to see (first one just moves files, second spits them up a bit differently (with minor content edits, but in theory non-functional), third has the actual changes). This was relocated from QuantumSplitting.cpp without change.

That being said, your question is probably legitimate.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Up to you, the gain in refactoring these if blocks is pretty small.

Co-authored-by: Joey Carter <joey.snarrcarter@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

qp.specs output on for loops is symbolic after running adjoint.lowering

3 participants