Fixing the Python bridge's loop and scope emission - #5251
Conversation
Signed-off-by: Sachin Pisal <spisal@nvidia.com>
Signed-off-by: Sachin Pisal <spisal@nvidia.com>
Signed-off-by: Sachin Pisal <spisal@nvidia.com>
Signed-off-by: Sachin Pisal <spisal@nvidia.com>
Signed-off-by: Sachin Pisal <spisal@nvidia.com>
Signed-off-by: Sachin Pisal <spisal@nvidia.com>
Signed-off-by: Sachin Pisal <spisal@nvidia.com>
CI Summary (
|
| Job | Result |
|---|---|
binaries |
⏩ skipped |
build_and_test |
✅ success |
changes |
✅ success |
config_devdeps |
✅ success |
config_source_build |
⏩ skipped |
config_wheeldeps |
✅ success |
devdeps |
✅ success |
docker_image |
⏩ skipped |
gen_code_coverage |
⏩ skipped |
metadata |
✅ success |
python_metapackages |
⏩ skipped |
python_wheels |
⏩ skipped |
source_build |
⏩ skipped |
wheeldeps |
✅ success |
⏩ Skipped jobs (7) — intentionally skipped on PR builds; run on merge_group / workflow_dispatch
| Job |
|---|
binaries |
config_source_build |
docker_image |
gen_code_coverage |
python_metapackages |
python_wheels |
source_build |
All sub-jobs (43) — every matrix leg, with links
| Job | Status | Link |
|---|---|---|
| Build and test (amd64, gcc12, openmpi) / Dev environment (Debug) | ✅ success | view |
| Build and test (amd64, gcc12, openmpi) / Dev environment (Python) | ✅ success | view |
| Build and test (amd64, llvm, openmpi) / Dev environment (Debug) | ✅ success | view |
| Build and test (amd64, llvm, openmpi) / Dev environment (Python) | ✅ success | view |
| Build and test (arm64, llvm, openmpi) / Dev environment (Debug) | ✅ success | view |
| Build and test (arm64, llvm, openmpi) / Dev environment (Python) | ✅ success | view |
| CI Summary | ❔ in_progress | view |
| Check for stable CUDA-Q changes | ✅ success | view |
| Configure build (devdeps) | ✅ success | view |
| Configure build (source_build) | ⏩ skipped | view |
| Configure build (wheeldeps) | ✅ success | view |
| Create CUDA Quantum installer | ⏩ skipped | view |
| Create Docker images | ⏩ skipped | view |
| Create Python metapackages | ⏩ skipped | view |
| Create Python wheels | ⏩ skipped | view |
| Gen code coverage | ⏩ skipped | view |
| Load dependencies (amd64, gcc12) / Caching | ✅ success | view |
| Load dependencies (amd64, gcc12) / Finalize | ✅ success | view |
| Load dependencies (amd64, gcc12) / Metadata | ✅ success | view |
| Load dependencies (amd64, llvm) / Caching | ✅ success | view |
| Load dependencies (amd64, llvm) / Finalize | ✅ success | view |
| Load dependencies (amd64, llvm) / Metadata | ✅ success | view |
| Load dependencies (arm64, gcc12) / Caching | ✅ success | view |
| Load dependencies (arm64, gcc12) / Finalize | ✅ success | view |
| Load dependencies (arm64, gcc12) / Metadata | ✅ success | view |
| Load dependencies (arm64, llvm) / Caching | ✅ success | view |
| Load dependencies (arm64, llvm) / Finalize | ✅ success | view |
| Load dependencies (arm64, llvm) / Metadata | ✅ success | view |
| Load source build cache | ⏩ skipped | view |
| Load wheel dependencies (amd64, 12.6) / Caching | ✅ success | view |
| Load wheel dependencies (amd64, 12.6) / Finalize | ✅ success | view |
| Load wheel dependencies (amd64, 12.6) / Metadata | ✅ success | view |
| Load wheel dependencies (amd64, 13.0) / Caching | ✅ success | view |
| Load wheel dependencies (amd64, 13.0) / Finalize | ✅ success | view |
| Load wheel dependencies (amd64, 13.0) / Metadata | ✅ success | view |
| Load wheel dependencies (arm64, 12.6) / Caching | ✅ success | view |
| Load wheel dependencies (arm64, 12.6) / Finalize | ✅ success | view |
| Load wheel dependencies (arm64, 12.6) / Metadata | ✅ success | view |
| Load wheel dependencies (arm64, 13.0) / Caching | ✅ success | view |
| Load wheel dependencies (arm64, 13.0) / Finalize | ✅ success | view |
| Load wheel dependencies (arm64, 13.0) / Metadata | ✅ success | view |
| Prepare cache clean-up | ❔ in_progress | view |
| Retrieve PR info | ✅ success | view |
✅ Required checks (6/6) — declared in .github/required-checks.yml for push
| Required check | Status | Link |
|---|---|---|
| Build and test (amd64, llvm, openmpi) / Dev environment (Debug) | ✅ success | view |
| Build and test (amd64, llvm, openmpi) / Dev environment (Python) | ✅ success | view |
| Build and test (arm64, llvm, openmpi) / Dev environment (Debug) | ✅ success | view |
| Build and test (arm64, llvm, openmpi) / Dev environment (Python) | ✅ success | view |
| Build and test (amd64, gcc12, openmpi) / Dev environment (Debug) | ✅ success | view |
| Build and test (amd64, gcc12, openmpi) / Dev environment (Python) | ✅ success | view |
Signed-off-by: Sachin Pisal <spisal@nvidia.com>
* deleting dead computations before erasing loop slots * dropping redundant asserts from createMonotonicForLoopInMemory Signed-off-by: Sachin Pisal <spisal@nvidia.com>
A Python for/else returned 3 where Python gives 13. Two passes dropped the else block independently. cc-loop-unroll never emitted the else region, and cc-loop-induction-fusion fused away a variable the else block updates, because getSecondaryInductions never inspects that region. Pre-existing, and while/else was wrong the same way. Signed-off-by: Sachin Pisal <spisal@nvidia.com>
| ]; | ||
| } | ||
|
|
||
| def LoopPruneDeadArgs : Pass<"cc-loop-prune-dead-args", "mlir::func::FuncOp"> { |
There was a problem hiding this comment.
This was going to be removed, eh?
There was a problem hiding this comment.
This pass is what lets two workarounds on main go away. Both are from #5233.
- the
allocasinking invisit_For/__analyzeLoopLocalTargets(the bare cc.alloca in the loop body) - the
isNestedInLoopguard inVariableCoalesce.cpp, which refuses to raise anyallocanested 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.
There was a problem hiding this comment.
OK.
- 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.
- 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.
There was a problem hiding this comment.
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).
| } | ||
|
|
||
| /// Point any binding in block that still refers to oldVal at newVal. | ||
| void updateBindingsOfValue(Block *block, Value oldVal, Value newVal) { |
There was a problem hiding this comment.
Still looks wrongheaded.
There was a problem hiding this comment.
Fixed in 07324e6. The sweep is gone. The binding update is now anchored to the store being rewritten. If the user we're rewriting is a cc.store of this def's value, we rebind only that store's target. No search by value identity, so it touches exactly one variable rather than every binding that happens to match.
Note: This one is a miscompile on main today. j = i in a loop body reads i's pre-loop value, in Python and in C++ through nvq++ alike. Happy to split it into its own PR.
There was a problem hiding this comment.
Here is the qke
func.func @repro(%n: i64) -> i64 {
%c0 = arith.constant 0 : i64
%c1 = arith.constant 1 : i64
%ctr = cc.alloca i64
%v = cc.alloca i64
cc.store %c0, %ctr : !cc.ptr<i64>
cc.store %c0, %v : !cc.ptr<i64>
cc.loop while {
%a = cc.load %ctr : !cc.ptr<i64>
%c = arith.cmpi slt, %a, %n : i64
cc.condition %c
} do {
%a = cc.load %ctr : !cc.ptr<i64>
cc.store %a, %v : !cc.ptr<i64>
cc.continue
} step {
%a = cc.load %ctr : !cc.ptr<i64>
%b = arith.addi %a, %c1 : i64
cc.store %b, %ctr : !cc.ptr<i64>
}
%r = cc.load %v : !cc.ptr<i64>
return %r : i64
}
after running cudaq-opt --memtoreg=quantum=0 repro.qke
module {
func.func @repro(%arg0: i64) -> i64 {
%c0_i64 = arith.constant 0 : i64
%c1_i64 = arith.constant 1 : i64
%0 = cc.undef i64
%1 = cc.undef i64
%2:2 = cc.loop while ((%arg1 = %c0_i64, %arg2 = %c0_i64) -> (i64, i64)) {
%3 = arith.cmpi slt, %arg2, %arg0 : i64
cc.condition %3(%arg1, %arg2 : i64, i64)
} do {
^bb0(%arg1: i64, %arg2: i64):
cc.continue %c0_i64, %arg2 : i64, i64
} step {
^bb0(%arg1: i64, %arg2: i64):
%3 = arith.addi %arg2, %c1_i64 : i64
cc.continue %arg1, %3 : i64, i64
}
return %2#0 : i64
}
}
Please see cc.continue %c0_i64, %arg2 in the do region. It carries the value the counter had before the loop, so the function returns 0 for any n > 0.
With this fix on this branch, we get cc.continue %arg2, %arg2.
With this code
int i = 0, j = 0;
while (i < 3) {
j = i;
i = i + 1;
}
x(q[j]);
The kernel above set j to 2.
So when we run the snippet on main, it gives
{ 1000:10 }
This fix gives the correct output as
{ 0010:10 }
Hope this helps.
schweitzpgi
left a comment
There was a problem hiding this comment.
I'd like to see the fixes for Python for-else pulled out into a completely separate PR.
This seems to be a grab bag of random changes. Changes to enable block argument elimination (that apparently don't work) and coupled with an entirely new pass to do that work instead. And, it's not clear any of it is needed at all as the original motivating example can have its adjoint manufactured without those changes.
The Python bridge fixes are mixed in as well when they are again a separate issue entirely and should be another separate PR.
This PR is just too scattered and trying to lump bug fixes with new features across the entire code base.
Signed-off-by: Sachin Pisal <spisal@nvidia.com>
Signed-off-by: Sachin Pisal <spisal@nvidia.com>
…gument Signed-off-by: Sachin Pisal <spisal@nvidia.com>
…VIDIA#5292) The Python bridge never emitted `cc.scope`, so compound statements had no lifetime structure. The `add-deallocs` pass had nothing to attach to, and a qubit allocated inside a loop body or an `if` branch was never deallocated. ``` r = cudaq.qubit() for i in range(3): # allocated every iteration, never freed q = cudaq.qvector(2) ``` Before - one `quake.dealloc` for r, none for the per iteration veq<2>. After - a `quake.dealloc` at scope exit, every iteration (the same shape cudaq-quake already produces for the equivalent C++ kernel). The fix adds buildScopedBlock, which wraps if/else branches, loop bodies, and the loop else block in a cc.scope. Python locals stay in the function entry block, since they are function-scoped and not block-scoped as in C++. The scope is only for allocation lifetime. Split out of NVIDIA#5251. --------- Signed-off-by: Sachin Pisal <spisal@nvidia.com>
Signed-off-by: Sachin Pisal <spisal@nvidia.com>
556c462 to
7795ca5
Compare
Follow-up to #5233. @schweitzpgi listed several problems with the IR the bridge emits for a nested-loop kernel. This fixes them and removes the two workarounds #5233 added.
cc.scopefor compound statements. The bridge emitted none, soadd-deallocshad nothing to attach to and a qubit allocated in an if branch or a loop body was never deallocated.forloops in memory form. The bridge lifted the counter to an iteration argument itself. Now eachforholds a counter slot, loaded to test and load/increment/stored to step. The counter is separate from the Python loop variable, since assigning that variable in the body must not change the trip count.cc-loop-prune-dead-argspass.memtoregpromotes an entry-block local to a value carried by every enclosing loop, read or not, which blocks adjoint generation. The pass drops those slots. Nested loops hold each other's alive, so liveness starts from assume dead and runs to a fixpoint.This lets both #5233 workarounds go. The bridge no longer allocates loop variables in the loop body, and
variable-coalesceno longer skips allocas under a loop.Python locals are back in the entry block, no bare allocas, no pre-lifted counters, and the QFT compiles with zero dead loop-carried values.