ITADN

[Python] `!cc.measure_handle` slot-buffer stores in multi-iteration loops are elided in QIR

#4527Openkhalatepradnya 创建于 2026-05-16
K
khalatepradnyacommented
### 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 条评论