[Python] `!cc.measure_handle` slot-buffer stores in multi-iteration loops are elided in QIR
### Required prerequisites
- [x] Consult the [security policy](https://github.com/NVIDIA/cuda-quantum/security/policy). If reporting a security vulnerability, do not report the bug using this form. Use the process described in the policy to report the issue.
- [x] Make sure you've read the [documentation](https://nvidia.github.io/cuda-quantum/latest). Your issue may be addressed there.
- [x] Search the [issue tracker](https://github.com/NVIDIA/cuda-quantum/issues) to verify that this hasn't already been reported. +1 or comment there if it has.
- [ ] If possible, make a PR with a failing test to give us a starting point to work on!
### Describe the bug
## Summary
A Python `@cudaq.kernel` that:
1. Pre-allocates a slot buffer with `combined_syndrome = [cudaq.measure_handle() for ...]`,
2. Stores per-round measurement handles into the slots inside a multi-iteration round loop, and
3. Calls a device kernel that takes `std::vector<cudaq::measure_handle>&` (and discriminates internally) from inside that round loop,
produces wrong QIR. The slot stores after the first iteration get elided, so every device-call after the first reuses the *first round's* discriminate+pack value. The semantically-equivalent C++ kernel lowers correctly, so this is specific to the Python-AST-bridge-emitted IR shape interacting with the Quake → QIR pipeline.
The defect is silent. No diagnostic fires; the kernel runs to completion and just sends stale syndromes on every round but the first.
## Reproducer
CUDA-QX `update-cudaq-version` branch at `443f3b56b3c5801945af296cee545dce0f9e5729`, CUDA-Q pinned to `053480dda0413c219c224d42313aa14d7a17a7fe`:
```bash
cd /workspaces/cudaqx/build
ninja _pycudaqx_qec_the_suffix_matters_cudaq_qec
ctest -R "app_examples.py-surface_code-1-test" --output-on-failure --timeout 240
```
The `[d3-quantinuum-emulate-in-process]` subtest fails deterministically with `num_non_zero = 1000` (expected `<= 0`) and `num_corrections = 0` (expected `>= 1000`). Decoder runs without error; it just receives the same syndrome value 11 times in a row and (correctly) returns no corrections, so the apply-correction branch never fires.
The C++ analog (`app_examples.surface_code-1-quantinuum-emulate-test-distance-3-in-process`) passes under the same CUDA-Q pin.
The triggering kernel shape is in `libs/qec/unittests/realtime/app_examples/surface_code_1.py::custom_memory_circuit_stabs`:
```python
combined_syndrome = [
cudaq.measure_handle() for i in range(len(xstab_anc) + len(zstab_anc))
]
for window_idx in range(num_rounds // decoder_window):
if window_idx > 0:
qec.enqueue_syndromes(logical_qubit_idx, combined_syndrome, 0)
for round_idx in range(window_idx * decoder_window,
(window_idx + 1) * decoder_window):
syndrome_z = se_z_ft(logical, cnot_schedZ_flat) # -> List[measure_handle]
syndrome_x = se_x_ft(logical, cnot_schedX_flat) # -> List[measure_handle]
i = 0
for s in syndrome_z:
combined_syndrome[i] = s
i += 1
for s in syndrome_x:
combined_syndrome[i] = s
i += 1
qec.enqueue_syndromes(logical_qubit_idx, combined_syndrome, 0)
```
Minimal isolated kernels (slot-buffer alone, slot stores in nested loops alone, `cudaq.to_integer(cudaq.to_bools(...))` alone) do **not** trigger the bug. The trigger needs the full combination: handle slot buffer outside the round loop, per-round stores inside, and a device call inside that consumes the buffer.
## Smoking-gun comparison
`CUDAQ_DUMP_JIT_IR=1` on the same kernel shape, both frontends, distance 3, 12 rounds, decoder_window 6:
**Python QIR** — 14 calls to `enqueue_syndromes_ui64`, 13 of which reuse `%43`:
```
151: tail call void @enqueue_syndromes_ui64(i64 0, i64 8, i64 %21, i64 0)
223: tail call void @enqueue_syndromes_ui64(i64 0, i64 8, i64 %43, i64 0)
273: tail call void @enqueue_syndromes_ui64(i64 0, i64 8, i64 %43, i64 0)
322: tail call void @enqueue_syndromes_ui64(i64 0, i64 8, i64 %43, i64 0)
371: tail call void @enqueue_syndromes_ui64(i64 0, i64 8, i64 %43, i64 0)
420: tail call void @enqueue_syndromes_ui64(i64 0, i64 8, i64 %43, i64 0)
469: tail call void @enqueue_syndromes_ui64(i64 0, i64 8, i64 %43, i64 0)
470: tail call void @enqueue_syndromes_ui64(i64 0, i64 8, i64 %43, i64 0)
519: tail call void @enqueue_syndromes_ui64(i64 0, i64 8, i64 %43, i64 0)
568: tail call void @enqueue_syndromes_ui64(i64 0, i64 8, i64 %43, i64 0)
617: tail call void @enqueue_syndromes_ui64(i64 0, i64 8, i64 %43, i64 0)
666: tail call void @enqueue_syndromes_ui64(i64 0, i64 8, i64 %43, i64 0)
715: tail call void @enqueue_syndromes_ui64(i64 0, i64 8, i64 %43, i64 0)
764: tail call void @enqueue_syndromes_ui64(i64 0, i64 8, i64 %43, i64 0)
```
`%43` is built from `read_result(8..15)` — the round-2 measurement results. Subsequent rounds **do** measure (mz to result indices 16..23, 24..31, etc. all appear in the IR), but their bits are never re-packed; the second-round packed value is reused for every later enqueue.
**C++ QIR** — 3 calls (test ran 3 rounds), each with a fresh packed value:
```
97: tail call void @enqueue_syndromes_ui64(i64 0, i64 8, i64 %21, i64 0)
169: tail call void @enqueue_syndromes_ui64(i64 0, i64 8, i64 %43, i64 0)
242: tail call void @enqueue_syndromes_ui64(i64 0, i64 8, i64 %65, i64 0)
```
Each round's `read_result + or-pack` cluster appears immediately before its `enqueue_syndromes_ui64` call.
The host-side syndrome decoder (CUDA-QX `realtime_decoding.cpp::enqueue_syndromes`) confirms this is what reaches the runtime: a temporary `printf` of the first 12 calls' bytes shows Python sending the identical 8-bit pattern (`00001010`) every call, while C++ sends varying patterns per round.
## Quake IR shape (pre-codegen)
The Python AST bridge emits this for the slot allocation + init:
```mlir
%11 = cc.alloca !cc.measure_handle[%8 : i64]
%12 = cc.loop while ((%arg16 = %c0_i64) -> (i64)) {
%18 = arith.cmpi slt, %arg16, %8 : i64
cc.condition %18(%arg16 : i64)
} do {
^bb0(%arg16: i64):
%18 = cc.undef !cc.measure_handle
%19 = cc.compute_ptr %11[%arg16] : (!cc.ptr<!cc.array<!cc.measure_handle x ?>>, i64) -> !cc.ptr<!cc.measure_handle>
cc.store %18, %19 : !cc.ptr<!cc.measure_handle>
cc.continue %arg16 : i64
} step { ... } {invariant}
%13 = cc.stdvec_init %11, %8 : (!cc.ptr<!cc.array<!cc.measure_handle x ?>>, i64) -> !cc.stdvec<!cc.measure_handle>
```
Note the `{invariant}` attribute on the init loop. The per-round slot stores and the device call inside the round loop look correct in Quake (each iteration has its own `cc.store` to `%11[i]` followed by the call passing `%13`), so the elision happens between Quake and QIR.
The C++ frontend, for the equivalent kernel, does **not** emit the `{invariant}`-marked init loop or thread a `!cc.measure_handle` value through the round-loop iter-args — its Quake IR is structurally simpler around the slot buffer.
## Suspected mechanism
The `{invariant}` attribute on the slot-init loop, combined with the AST bridge plumbing the loop variable `s` (a `!cc.measure_handle`) through inner-loop iter-args, looks like it confuses a downstream pass (LICM, SROA, or similar) into treating the slot buffer's contents as loop-invariant after the first iteration. Once the per-round stores are dead-store-eliminated, the per-round discriminate at the call site folds to the first-iteration value and gets hoisted out of the round loop.
This is a hypothesis from IR shape, not a bisected pass. To confirm, run `--print-ir-after-all` through the Quake-to-QIR codegen pipeline on the failing kernel and find the first pass output where the per-round stores disappear.
## Expected behavior
Each `enqueue_syndromes` call inside the round loop should pack a fresh value derived from that round's measurement results. The C++ frontend demonstrates this is achievable from the same source-level kernel.
### Steps to reproduce the bug
See above
### Expected behavior
Example works with `measure_handle` list
### Is this a regression? If it is, put the last known working version (or commit) here.
Not a regression
### Environment
- **CUDA-Q version**: 053480dda0413c219c224d42313aa14d7a17a7fe
- **Python version**:
- **C++ compiler**:
- **Operating system**:
### Suggestions
_No response_
0 条评论