Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8390ca2
freeing qubits allocated in an if branch
sacpis Aug 24, 2026
12dfd60
freeing qubits allocated in a loop body
sacpis Aug 24, 2026
6478123
freeing qubits allocated in a loop else block
sacpis Aug 24, 2026
9a89901
fixing memtoreg dropping a copy of the loop counter
sacpis Aug 24, 2026
d789931
emitting for-range loops in memory form
sacpis Aug 24, 2026
5e31634
dropping loop-carried values that nothing reads
sacpis Aug 25, 2026
de7fd8e
fixing spelling
sacpis Aug 25, 2026
e5c1d4c
Merge branch 'main' into fix_python_bridge_issues
sacpis Aug 25, 2026
fe158c0
freeing qubits on the break path out of nested scopes
sacpis Aug 25, 2026
b877171
Merge branch 'main' into fix_python_bridge_issues
sacpis Aug 25, 2026
1f4fdae
Merge branch 'main' into fix_python_bridge_issues
sacpis Aug 25, 2026
9172c7e
* making cc.continue and cc.break region-branch terminators
sacpis Aug 25, 2026
b14aa1c
running a loop's else block when the loop is unrolled or fused
sacpis Aug 25, 2026
0ae8b28
Merge branch 'main' into fix_python_bridge_issues
sacpis Aug 25, 2026
9f0b9f1
Merge branch 'main' into fix_python_bridge_issues
sacpis Aug 25, 2026
04e3fa4
Merge branch 'main' into fix_python_bridge_issues
sacpis Aug 26, 2026
e0a9f31
Merge branch 'main' into fix_python_bridge_issues
sacpis Aug 26, 2026
a6d3100
Merge branch 'main' into fix_python_bridge_issues
sacpis Aug 26, 2026
f46c960
reverting the block argument elimination attempt
sacpis Aug 26, 2026
ea82c73
dropping the for/else fixes now in #5282
sacpis Aug 26, 2026
07324e6
binding only the copy's target when a promoted def becomes a block ar…
sacpis Aug 26, 2026
22ba27b
Merge branch 'main' into fix_python_bridge_issues
sacpis Aug 26, 2026
7795ca5
Merge branch 'main' into fix_python_bridge_issues
sacpis Aug 27, 2026
1cdc065
Merge branch 'main' into fix_python_bridge_issues
sacpis Aug 27, 2026
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
6 changes: 4 additions & 2 deletions cudaq/include/cudaq/Optimizer/Dialect/CC/CCOps.td
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,8 @@ def cc_ConditionOp : CCOp<"condition",
}

def cc_ContinueOp : CCOp<"continue", [Pure, ReturnLike, Terminator,
ParentOneOf<["LoopOp", "ScopeOp", "IfOp"]>]> {
ParentOneOf<["LoopOp", "ScopeOp", "IfOp"]>,
DeclareOpInterfaceMethods<RegionBranchTerminatorOpInterface>]> {
let summary = "Continue branch.";
let description = [{
A ContinueOp is a generalized exit terminator for the dialect operations
Expand Down Expand Up @@ -521,7 +522,8 @@ def cc_ContinueOp : CCOp<"continue", [Pure, ReturnLike, Terminator,
}

def cc_BreakOp : CCOp<"break",
[Pure, ReturnLike, Terminator, ParentOneOf<["LoopOp"]>]> {
[Pure, ReturnLike, Terminator, ParentOneOf<["LoopOp"]>,
DeclareOpInterfaceMethods<RegionBranchTerminatorOpInterface>]> {
let summary = "Break branch.";
let description = [{
A BreakOp can be used in a LoopOp's body region (only). A BreakOp is a
Expand Down
25 changes: 25 additions & 0 deletions cudaq/include/cudaq/Optimizer/Transforms/Passes.td
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,31 @@ def LoopNormalize : Pass<"cc-loop-normalize"> {
];
}

def LoopPruneDeadArgs : Pass<"cc-loop-prune-dead-args", "mlir::func::FuncOp"> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This was going to be removed, eh?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This pass is what lets two workarounds on main go away. Both are from #5233.

  1. the alloca sinking in visit_For / __analyzeLoopLocalTargets (the bare cc.alloca in the loop body)
  2. the isNestedInLoop guard in VariableCoalesce.cpp, which refuses to raise any alloca nested in a cc.loop

This branch deletes both and prunes the dead loop-carried values in the optimizer instead. #5223 needs one of the three. With the pass disabled and nothing else changed, test_5223.py fails with the original 'arith.addi' op control-flow def-use not reversible.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

OK.

  1. Random stray alloca's inside high-level control-flow operations without scopes are 100% invalid IR from the front-end. The Python bridge needs to generate correct IR.
  2. The variable-coalesce pass doesn't move allocations. That's not what it is for. It merges variables based on their lifetimes. The stack-frame-prealloc pass is the pass that moves allocations to the prologue of the function. These are complimentary but orthogonal.

So the isNestedInLoop stuff to move cc.alloca around is misplaced and possibly redundant with what stack-frame-prealloc does.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Totally agreed on the front end, and that's what this PR does. The bridge emits cc.scope for if/else and loop bodies now, and the sinking that produced the bare cc.alloca is gone.

The isNestedInLoop guard was a consequence of that same front end bug. variable-coalesce says it relies on the correct and proper construction and use of cc.scope ops, and the bridge emitted none, so #5233 guarded the pass instead of fixing the IR. With scopes emitted the guard is not needed, and coalesce.qke's f2 goes back to 2 slots from 4.

stack-frame-prealloc is downstream of this (it runs in addLowerToCFGAndCleanup after lowering to CFG, whereas apply-op-specialization runs on the high-level IR, so it cannot cover this case).

let summary = "Drop loop-carried values that nothing reads.";
let description = [{
A Python local lives for the whole function, so its storage is in the entry
block and `memtoreg` promotes it to a value carried by every loop around
it. When nothing reads the value, the loop is left carrying it along with
the arithmetic that computes it.

That residue reads like a real dependence on an earlier loop's result, and
adjoint generation cannot reverse a block where a value produced by an op
it moves is read by one that stays. Where a carried value is dead, thread
the loop's own initial value around the loop instead and delete the
computations that go dead as a result.

Run this after `memtoreg`, which is what creates the carried value, and
before `apply-op-specialization`. Order relative to `cc-loop-normalize`
does not matter.

MLIR's generic `RegionBranchOpInterface` canonicalization cannot do this
job. It erases one dead value at a time, so it drops a slot from some of
the loop's regions and not the rest, and the `cc.loop` verifier rejects
that. A slot has to go from all four regions and the results at once.
}];
}

def LoopPeeling : Pass<"cc-loop-peeling"> {
let summary = "Peeling classical do-while loops.";
let description = [{
Expand Down
22 changes: 17 additions & 5 deletions cudaq/lib/Optimizer/Dialect/CC/CCOps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1983,7 +1983,6 @@ SmallVector<Region *> cudaq::cc::LoopOp::getLoopRegions() {

OperandRange
cudaq::cc::LoopOp::getEntrySuccessorOperands(RegionBranchPoint point) {
llvm::errs() << "getEntrySuccessorOperands: " << point << "\n";
assert(!point.isParent() && "invalid index region");
Operation *pred = point.getTerminatorPredecessorOrNull();
assert(pred && "must have a terminator");
Expand Down Expand Up @@ -2050,7 +2049,20 @@ struct HoistLoopInvariantArgs : public OpRewritePattern<cudaq::cc::LoopOp> {
if (block.hasNoSuccessors())
terminators.push_back(block.getTerminator());

// 2. Determine if any arguments are invariant.
// 2. Everything below is indexed by slot number. Bail if the lists
// disagree on how many slots there are, rather than index out of bounds.
const auto numSlots = loop.getInitialArgs().size();
for (auto *term : terminators) {
Comment thread
sacpis marked this conversation as resolved.
Outdated
if (term->getBlock()->getParent()->front().getNumArguments() != numSlots)
return failure();
auto forwarded = isa<cudaq::cc::ConditionOp>(term)
? term->getNumOperands() - 1
: term->getNumOperands();
if (forwarded != numSlots)
return failure();
}

// 3. Determine if any arguments are invariant.
SmallVector<bool> invariants;
bool hasInvariants = false;
for (auto iter : llvm::enumerate(loop.getInitialArgs())) {
Expand All @@ -2076,19 +2088,19 @@ struct HoistLoopInvariantArgs : public OpRewritePattern<cudaq::cc::LoopOp> {
invariants.push_back(isInvar);
}

// 3. For each invariant argument replace the uses with the original
// 4. For each invariant argument replace the uses with the original
// invariant value throughout.
if (hasInvariants) {
for (auto iter : llvm::enumerate(invariants)) {
if (iter.value()) {
auto i = iter.index();
Value initialVal = loop.getInitialArgs()[i];
loop.getResult(i).replaceAllUsesWith(initialVal);
rewriter.replaceAllUsesWith(loop.getResult(i), initialVal);
for (auto *reg : loop.getRegions()) {
if (reg->empty())
continue;
auto &entry = reg->front();
entry.getArgument(i).replaceAllUsesWith(initialVal);
rewriter.replaceAllUsesWith(entry.getArgument(i), initialVal);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions cudaq/lib/Optimizer/Transforms/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ add_cudaq_library(OptTransforms
LoopAnalysis.cpp
LoopInductionFusion.cpp
LoopNormalize.cpp
LoopPruneDeadArgs.cpp
LoopPeeling.cpp
LoopUnroll.cpp
LowerPhase.cpp
Expand Down
13 changes: 13 additions & 0 deletions cudaq/lib/Optimizer/Transforms/LoopAnalysis.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,19 @@ cudaq::opt::getSecondaryInductions(cudaq::cc::LoopOp loop,
if (i == primaryIdx)
continue;

// The else region runs once, on normal loop exit, so a value it recomputes
Comment thread
sacpis marked this conversation as resolved.
// has no closed form in terms of the primary. Only fuse `i` if the else
// region passes it through untouched.
if (loop.hasPythonElse()) {
Block &elseEntry = loop.getElseRegion().front();
if (i >= elseEntry.getNumArguments())
continue;
LoopRegionSite elseSite{&loop.getElseRegion(), /*isWhile=*/false};
Value carried = getCarriedValue(elseSite, i);
if (!carried || carried != elseEntry.getArgument(i))
continue;
}

Value stepVal;
bool isAdd = false;
bool isPrimaryAlias = false;
Expand Down
Loading
Loading