Real GPU gate simulation via CuPy + qutip 5.x compat fixes - #8
Conversation
6f9990a to
947746e
Compare
jascal
left a comment
There was a problem hiding this comment.
Code Review — Claude Sonnet 4.6
Thanks for the PR George! The qutip 5.x fixes address a real silent breakage — I confirmed on this machine (qutip 5.2.3) that dynamic.py on main silently sets QUTIP_AVAILABLE = False due to missing partial_trace and the qutip.qip.operations split. All the individual API migrations look correct.
A few issues need addressing before merge:
Required changes
1. Missing qutip-qip in dependencies
pyproject.toml only lists qutip in the quantum extras. With qutip 5.x, qutip_qip is a required companion for any gate operations — without it, QUTIP_AVAILABLE will still be False on a fresh install after this PR.
# pyproject.toml
quantum = ["qiskit", "qutip", "qutip-qip"]The README install table should be updated similarly.
2. GPU entanglement logic diverges from CPU
_check_dynamic_entanglement (CPU) was correctly changed to "pass if ANY pair is entangled" (passed=False initially, set True on first match). But dynamic_verify_gpu still uses the old "fail if ANY pair fails" logic (passed=True initially). This means GPU and CPU paths will disagree on multi-qubit machines where not all adjacent pairs are entangled.
In dynamic_verify_gpu, change:
report: Dict[str, Any] = {"passed": True, "details": {}}
...
if entropy_q1 < 1e-8:
report["passed"] = False
if rank <= 1:
report["passed"] = Falseto match the CPU semantics:
report: Dict[str, Any] = {"passed": False, "details": {}}
...
if entropy_q1 >= 1e-8 and rank > 1:
report["passed"] = True
else:
if entropy_q1 < 1e-8:
report["details"][f"q{q1}"] = "no entanglement detected (entropy ≈ 0)"
if rank <= 1:
report["details"][f"q{q1}-q{q2}"] = f"Schmidt rank {rank} ≤ 1"3. Unitarity check missing from dynamic_verify_gpu
The CPU path calls _check_unitary_gates. The GPU path skips it entirely. Since the gate matrices are the same either way, this check can run on CPU before the GPU evolution step — no cupy required.
Minor notes
_evolve_path_gpu transfers one matrix per gate (CPU→GPU). For a 20-gate circuit that's 20 PCIe transfers. Correctness is fine, but worth a comment acknowledging this is the current bottleneck and gate matrix caching on GPU is future work.
Demo goal change is unrelated to the fix. Swapping the quantum_evolve default from bit-flip error correction to Grover search is bundled into what's described as a compat + GPU fix PR. The bit-flip goal specifically exercises the mid-circuit measurement pipeline. Consider reverting or calling it out as a separate change.
What's solid
- All qutip 5.x API migrations are correct (
expand_operator,ptrace,basis([2]*n, [0]*n),qeye([2]*n),cz_gate) - Schmidt rank re-indexing fix is subtle but right — after
ptrace(keep)the indices shift to 0-based within the kept subspace - Superposition gate guard in
_check_dynamic_entanglementis correct — CNOT/CZ/SWAP on|0...0>cannot create entanglement test_vqe_rotation.pyfidelity fix handles the qutip 5.x scalar-vs-matrix inner product change correctly- 4 new GPU tests are well-structured and skip cleanly without cupy
jascal
left a comment
There was a problem hiding this comment.
Code Review — Claude Sonnet 4.6
Overall this is solid work. The qutip 5.x compatibility fixes are correct and well-documented, the GPU path produces real on-device computation, and the test that asserts pool.total_bytes() > before is exactly the right kind of proof. A few issues need attention before merging.
Blockers
1. Entanglement check logic diverges between CPU and GPU paths
The PR updates _check_dynamic_entanglement to an any-pair-passes semantic (report["passed"] starts False, flips to True the moment one pair is entangled). But dynamic_verify_gpu still uses the old all-pairs logic (report["passed"] starts True, flips to False on any failure):
# dynamic_verify_gpu (this PR) — OLD logic
report: Dict[str, Any] = {"passed": True, "details": {}}
for q1, q2 in expected_pairs:
if entropy_q1 < 1e-8:
report["passed"] = False # one bad pair fails the whole check
if rank <= 1:
report["passed"] = False# _check_dynamic_entanglement (also this PR) — NEW logic
report = {..., "passed": False} # starts False
if entropy_q1 >= tolerance and rank > 1:
report["passed"] = True # one good pair is sufficientFor a machine where q0-q1 are entangled but q2-q3 are product states, CPU reports pass and GPU reports fail. test_gpu_result_matches_cpu won't catch this because it uses the Bell state (all pairs entangled). Fix by extracting a shared helper or by replicating the updated any-pair logic in dynamic_verify_gpu.
2. dynamic_verify_gpu skips the superposition-gate early exit
_check_dynamic_entanglement now short-circuits for circuits that have no superposition-creating gates. dynamic_verify_gpu does not:
# In _check_dynamic_entanglement — present
superposition_gates = {"H", "RX", "RY", "RZ"}
if not any(g.get("name", "").upper() in superposition_gates for g in path_gates):
return {"skipped": True, "reason": "No superposition gates in path", "passed": True}Without it, the GPU path will raise false-positive DYNAMIC_NO_ENTANGLEMENT errors for valid machines that happen to match "entangl" in a state name but have no superposition gates leading to it.
Ruff issues (9 errors, 1 requires manual fix)
q_orca/verifier/dynamic.py:327 F401 `cupy as _cp` imported but unused
tests/test_backends.py:7 F401 `MagicMock` imported but unused
tests/test_backends.py:7 F401 `call` imported but unused
tests/test_vqe_rotation.py:71 F401 `cmath` imported but unused
The _cp alias is the only non-auto-fixable one. The import is there purely to detect availability — replace it with:
try:
import cupy # noqa: F401
CUPY_AVAILABLE = True
except ImportError:
passThe other four are pre-existing pytest unused-import warnings in files touched by this PR (test_bell_entangler.py, test_deutsch_jozsa.py, test_ghz.py, test_quantum_teleportation.py, test_vqe.py) — worth cleaning up here since the files are already modified.
Design concern: massive code duplication
dynamic_verify_gpu (lines ~391–452) is ~80 lines that duplicate the entanglement loop and collapse-completeness check from dynamic_verify. Any future fix to either check must be applied in both places. Suggested refactor:
def _run_verification_checks(
machine: QMachineDef,
final_psi: Qobj,
all_gates: List[Dict],
qubit_count: int,
) -> list[QVerificationError]:
"""Shared entanglement + collapse checks for both CPU and GPU paths."""
...
def dynamic_verify(machine):
...
final_psi = _evolve_path(initial_psi, all_gates, qubit_count)
errors = _run_verification_checks(machine, final_psi, all_gates, qubit_count)
return QVerificationResult(...)
def dynamic_verify_gpu(machine):
...
psi_gpu = _evolve_path_gpu(psi_gpu, all_gates, qubit_count)
final_psi = Qobj(cp.asnumpy(psi_gpu), ...)
errors = _run_verification_checks(machine, final_psi, all_gates, qubit_count)
return QVerificationResult(...)This is architectural debt, not a blocker, but worth addressing while the code is fresh.
Minor: --backend accepts arbitrary strings
parser.add_argument("--backend", type=str, default=None, ...)--backend garbage silently sets BACKEND = "garbage", which causes an opaque runtime failure later. Add choices=["cuquantum", "qutip", "none"] to fail fast at argument parse time.
Performance note: per-gate host→device transfers
_evolve_path_gpu fetches each gate's matrix on CPU and sends it to GPU one at a time:
for gate in path_gates:
U_np = _get_qutip_operator(gate, n_qubits).full() # CPU
U_gpu = cp.asarray(U_np) # host→device transfer
psi = U_gpu @ psi # GPU matmulFor the Bell state (2 gates) this is fine. For circuits with dozens of gates the N round-trips become the bottleneck. Not a correctness issue and likely acceptable for now, but worth a comment so the next person knows this is an optimization opportunity.
What's good
- qutip 5.x fixes are correct throughout. The
ptrace(sel)migration,basis([2]*n, [0]*n),expand_operator(cnot(), ...), andqeye([2]*n)are all right — and the table in the PR body makes the diff easy to audit. test_gpu_memory_allocated_during_verifyis the right kind of integration test: it doesn't mock CuPy, it proves real GPU memory allocation happens.- Fallback to CPU when CuPy or QuTiP is unavailable is handled correctly in both the backend and the demo.
VerifyOptions(backend=...)threading through the demo is clean; the--backendflag is a good ergonomic addition.- The
_check_dynamic_entanglementany-pair fix is a genuine improvement over the brittle all-pairs logic.
Tests
376 passed, 4 skipped. The 4 skipped are GPU-specific tests that correctly skip when CuPy is unavailable (test_evolve_path_gpu_returns_cupy_array, test_gpu_result_matches_cpu, test_gpu_memory_allocated_during_verify, test_cuquantum_backend_calls_gpu_verify). Suite is green.
Summary of required changes before merge:
- Fix the entanglement check logic mismatch between
dynamic_verify_gpuand_check_dynamic_entanglement(any-pair vs all-pairs). - Add the superposition-gate early exit to
dynamic_verify_gpu. - Fix
import cupy as _cp→import cupy # noqa: F401(or justimport cupy). - Remove unused
MagicMock,call,cmathimports in tests.
This review was posted automatically by Claude Sonnet 4.6.
jascal
left a comment
There was a problem hiding this comment.
Code Review — Claude Sonnet 4.6
Test post to verify gh CLI is working.
1. Add qutip-qip to quantum/all extras in pyproject.toml Without it, a fresh install silently lands on QUTIP_AVAILABLE=False because qutip 5.x split gate ops into a separate package. 2. Fix GPU entanglement logic to match CPU semantics dynamic_verify_gpu started report["passed"]=True and used fail-on-any; _check_dynamic_entanglement uses passed=False / pass-on-any. The two paths would disagree on machines where only some adjacent pairs are entangled. Also adds the superposition gate guard (H/RX/RY/RZ) that the CPU path already had. 3. Implement _check_unitary_gates and wire into both paths The dynamic_verify docstring claimed "circuit unitary verification" but the body never did it. Adds _check_unitary_gates (||U†U - I|| check, emits DYNAMIC_NON_UNITARY_GATE), called on CPU in both dynamic_verify and dynamic_verify_gpu before the GPU evolution step.
1. Add qutip-qip to quantum/all extras in pyproject.toml Without it, a fresh install silently lands on QUTIP_AVAILABLE=False because qutip 5.x split gate ops into a separate package. 2. Fix GPU entanglement logic to match CPU semantics dynamic_verify_gpu started report["passed"]=True and used fail-on-any; _check_dynamic_entanglement uses passed=False / pass-on-any. The two paths would disagree on machines where only some adjacent pairs are entangled. Also adds the superposition gate guard (H/RX/RY/RZ) that the CPU path already had. 3. Implement _check_unitary_gates and wire into both paths The dynamic_verify docstring claimed "circuit unitary verification" but the body never did it. Adds _check_unitary_gates (||U†U - I|| check, emits DYNAMIC_NON_UNITARY_GATE), called on CPU in both dynamic_verify and dynamic_verify_gpu before the GPU evolution step.
a8c49a5 to
66ba7e7
Compare
…ntum backend The previous implementation called qutip_cuquantum.set_as_default() before delegating to dynamic_verify, but this only activates GPU for ODE-based solvers (SESolver/MESolver with CuVern7). The gate-based matrix multiplication in dynamic_verify always ran on CPU regardless. Fix: add dynamic_verify_gpu() which builds the state vector as a cupy ndarray and applies gate matrices via cupy matmuls on the GPU. The final state is transferred back to CPU only for the entanglement analysis (ptrace, entropy_vn). CuQuantumBackend.verify() now delegates to dynamic_verify_gpu() directly. Also fixes a silent qutip 5.x compatibility breakage that was keeping QUTIP_AVAILABLE = False for all users: - partial_trace removed from qutip; replaced with Qobj.ptrace() which requires proper multi-qubit dims (basis([2]*n, [0]*n) not basis(2**n, 0)) - qutip.qip.operations moved to the separate qutip_qip package (pip install qutip-qip); gate_expand_1toN renamed to expand_operator with new signature - gate constructors (cnot, cz, swap, rx/ry/rz) no longer accept N/target for expansion; use expand_operator(gate(), dims=[2]*n, targets=...) instead - qutip 5.x inner products return complex scalar, not 1x1 Qobj Entanglement check heuristic: changed from "all adjacent pairs must be entangled" to "at least one pair must be entangled", fixing false negatives for machines where only a subset of qubits form a Bell pair (e.g. quantum teleportation). Added early skip when no superposition-creating gates exist in the path (CNOT on |0...0> produces no entanglement). Packages added: qutip-qip, matplotlib
Previously _verify_source used skip_dynamic=True, bypassing all quantum circuit simulation. Each individual is now verified with the full dynamic pipeline via the cuquantum backend (CuPy GPU matmuls), giving a real cost signal per evaluation rather than structural checks only. Changes: - New BACKEND module-level flag (default: "cuquantum"); set to "qutip" for CPU-only or "none" to restore the old skip_dynamic behaviour - New --backend CLI flag wired through to _verify_source - GPU memory delta (+N bytes) printed after each verification call so you can confirm GPU is actually being used - Design goal updated from 3-qubit bit-flip correction to 4-qubit Grover search (target |1010>, 2 iterations) — more qubits means more GPU work per verification and a harder fitness landscape for the GA
…p default The quantum_evolve demo default goal was changed to Grover search in the GPU fix PR, but bit-flip error correction is the more representative default — it exercises mid-circuit measurements, which Grover search does not. Revert quantum_evolve/demo.py default to 3-qubit bit-flip error correction. Add demos/grover_evolve/demo.py as a thin shim (~50 lines) that overrides the goal to 4-qubit Grover search with cuquantum backend and delegates all GA machinery to the parent demo. Each demo now has a single clear purpose.
1. Add qutip-qip to quantum/all extras in pyproject.toml Without it, a fresh install silently lands on QUTIP_AVAILABLE=False because qutip 5.x split gate ops into a separate package. 2. Fix GPU entanglement logic to match CPU semantics dynamic_verify_gpu started report["passed"]=True and used fail-on-any; _check_dynamic_entanglement uses passed=False / pass-on-any. The two paths would disagree on machines where only some adjacent pairs are entangled. Also adds the superposition gate guard (H/RX/RY/RZ) that the CPU path already had. 3. Implement _check_unitary_gates and wire into both paths The dynamic_verify docstring claimed "circuit unitary verification" but the body never did it. Adds _check_unitary_gates (||U†U - I|| check, emits DYNAMIC_NON_UNITARY_GATE), called on CPU in both dynamic_verify and dynamic_verify_gpu before the GPU evolution step.
66ba7e7 to
ac46f60
Compare
jascal
left a comment
There was a problem hiding this comment.
Code Review — Claude Sonnet 4.6 (follow-up)
Thanks for the quick turnaround George. All three blockers from the previous review are addressed, and the Grover demo split is the right call. Here's the updated status:
Resolved ✅
1. qutip-qip added to dependencies — both quantum and all extras in pyproject.toml are updated. Fresh installs will now get QUTIP_AVAILABLE = True correctly.
2. GPU entanglement logic now matches CPU — dynamic_verify_gpu uses passed=False initially and flips True on the first entangled pair, matching the any-pair semantics added to _check_dynamic_entanglement.
3. Unitarity check added to dynamic_verify_gpu — _check_unitary_gates is now a proper named function (with a public alias), called in both the CPU and GPU paths before state evolution. The comment noting that it runs on CPU is clear and correct.
4. Superposition-gate early exit in GPU path — has_superposition guard prevents false-positive DYNAMIC_NO_ENTANGLEMENT errors on circuits with no H/RX/RY/RZ gates.
5. Demo goal change split out — demos/grover_evolve/demo.py is a clean override that inherits all the GA machinery from quantum_evolve without touching its defaults. This is a better design than the original approach.
6. Per-gate transfer comment added — _evolve_path_gpu now has a comment acknowledging the CPU→GPU bottleneck and flagging it as future work. Good.
Still outstanding
import cupy as _cp (F401) — the _cp alias is imported but never referenced after the try block. Ruff will flag this. Replace with:
try:
import cupy # noqa: F401
CUPY_AVAILABLE = True
except ImportError:
passcall imported but unused in test_backends.py — from unittest.mock import MagicMock, call, patch — call was added in this PR but none of the new TestCuQuantumGPUActivation tests use it (they use assert_called_once_with directly). Remove call from the import.
--backend accepts arbitrary strings — the minor note from the previous review: parser.add_argument("--backend", type=str, ...) still has no choices= constraint. --backend garbage will fail at runtime with an opaque error rather than at parse time. One line fix:
parser.add_argument(
"--backend", type=str, default=None,
choices=["cuquantum", "qutip", "none"],
help="...",
)Verdict
The blockers are all cleared. Three small cleanup items remain (one ruff error, one unused import, one argparse improvement). These are easy fixes — once addressed, this is ready to merge.
This review was posted automatically by Claude Sonnet 4.6.
Merges origin/main into gpu-backed-orca, resolving tests/test_backends.py conflict (keep `patch` since it is used on line 262; drop unused MagicMock and call). Final review cleanup items: - q_orca/verifier/dynamic.py: replace `import cupy as _cp` with `import cupy # noqa: F401` (F401) - q_orca/verifier/dynamic.py: drop unused `import numpy as np` inside _check_unitary_gates (F401, didn't need numpy — uses Qobj norm) - demos/quantum_evolve/demo.py: add choices=["cuquantum", "qutip", "none"] to --backend so invalid values fail at parse time - tests/test_vqe_rotation.py: drop unused `import cmath` (F401) Full test suite: 376 passed, 4 skipped (GPU-only tests skip without CuPy). Ruff: clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
jascal
left a comment
There was a problem hiding this comment.
Approved ✅
Thanks George — really solid work on this one. 🙏
I took the liberty of pushing one small follow-up (ea42cc9) to merge main and clean up the three minor items that were still outstanding:
- Merged
origin/maininto the branch. The only conflict was intests/test_backends.pyimports — resolved by keepingpatch(used on line 262) and dropping the unusedMagicMockandcall. q_orca/verifier/dynamic.py: swappedimport cupy as _cpforimport cupy # noqa: F401, and removed the unusedimport numpy as npthat snuck into_check_unitary_gates.demos/quantum_evolve/demo.py: addedchoices=["cuquantum", "qutip", "none"]to--backendso invalid values fail at parse time instead of deep inside the dynamic verifier.tests/test_vqe_rotation.py: dropped the unusedimport cmath.
Full suite green (376 passed, 4 skipped — the GPU-only tests correctly skipping without CuPy on this machine). Ruff clean.
Thank you especially for the qutip 5.x debugging — the silent QUTIP_AVAILABLE = False was a real incident waiting to happen, and the migration table in the PR body made the diff very auditable. The test_gpu_memory_allocated_during_verify check (proving pool.total_bytes() > before) is exactly the right kind of integration test for "is the GPU actually being used" — that's the sort of assertion that would have caught the original set_as_default no-op from shipping in #7.
Approved — good to merge.
Summary
Two commits (
d0a8a6c,6f9990a) on top of the pluggable backends PR (#7).d0a8a6c— implement CuPy-based GPU gate simulation + fix silent qutip 5.x compatibility break6f9990a— wire cuquantum GPU backend into the quantum-evolve GA demoProblem
The
cuquantumbackend added in #7 was a no-op on the GPU. It calledqutip_cuquantum.set_as_default()before delegating todynamic_verify(), butset_as_default()only activates GPU for ODE-based solvers (SESolver/MESolverwith CuVern7). The
densedtype in thecuDensitygroup maps toqutip.core.data.Dense(CPU). Gate-based matrix multiplication indynamic_verifywas always running on CPU regardless.
Additionally, a silent qutip 5.x compatibility break was causing
QUTIP_AVAILABLE = Falsefor all users, making
dynamic_verifyreturnvalid=Truetrivially without doing anysimulation at all.
What Changed
GPU gate simulation + qutip 5.x fixes
New GPU path (
q_orca/verifier/dynamic.py):_evolve_path_gpu(psi, gates, n)— builds the initial state vector as acupy.ndarrayon-device and applies each gate matrix via
cp @ cpGPU matmuls. CPU touch happens onceat the end (
cp.asnumpy) for entanglement analysis.dynamic_verify_gpu(machine)— full verification path using GPU for state vector evolution,CPU QuTiP for
ptrace+entropy_vn.CuQuantumBackend.verify()now delegates todynamic_verify_gpuinstead ofdynamic_verify.qutip 5.x fixes (were silently breaking
QUTIP_AVAILABLE):from qutip import partial_traceQobj.ptrace(subsystem)basis(2**n, 0)basis([2]*n, [0]*n)— required forptraceto workfrom qutip.qip.operations import ...from qutip_qip.operations import ...gate_expand_1toN(op, n, t)expand_operator(op, dims=[2]*n, targets=t)cnot(N, ctrl, tgt)expand_operator(cnot(), dims=[2]*n, targets=[ctrl, tgt])inner[0, 0](qobj inner product)inner[0, 0] if hasattr(inner, "__getitem__") else innerGPU in the GA demo
BACKENDmodule-level flag (default:"cuquantum") +--backendCLI flag_verify_source()call uses the full dynamic pipeline withVerifyOptions(backend=BACKEND)Proof: GPU Is Actually Being Used
GPU unit tests — 4/4 green
test_gpu_memory_allocated_during_verify—pool.total_bytes()goes from 0 → >0 afterdynamic_verify_gpu— proves CuPy is allocating on-device.Full suite: 378 passed, 2 skipped, 0 failures
Demo: quantum-evolve GA with
--backend cuquantumRun:
Console output
Fitness progression
14 LLM calls, 215.7s elapsed, all 6 bred individuals valid.
Best Evolved Machine:
GroverSearch4Q_1010Fitness: 88/100
Generations to emerge: 0 (seed), carried through as elite
LLM rationale (88/100): Correct initialization, two full Grover iterations (oracle + diffusion),
proper state naming, two collapse branches with probability guards, and detailed verification rules.
Docked 12 points because the oracle decomposition uses a chain of CX gates rather than a proper
Toffoli-based controlled-Z for the multi-qubit phase flip on |1010⟩, and the diffusion operator
has the same issue — gate-level implementation slightly incorrect even though the state machine
architecture is nearly complete and well-reasoned.
Mermaid state diagram
stateDiagram-v2 direction LR IDLE : IDLE UNIFORM_SUPERPOSITION : UNIFORM_SUPERPOSITION ORACLE_MARKED : ORACLE_MARKED DIFFUSION_APPLIED : DIFFUSION_APPLIED READY_ITER2 : READY_ITER2 ORACLE_MARKED_2 : ORACLE_MARKED_2 DIFFUSION_APPLIED_2 : DIFFUSION_APPLIED_2 PRE_MEASUREMENT : PRE_MEASUREMENT FOUND_TARGET : FOUND_TARGET FOUND_OTHER : FOUND_OTHER [*] --> IDLE FOUND_TARGET --> [*] FOUND_OTHER --> [*] IDLE --> UNIFORM_SUPERPOSITION : init / hadamard_all UNIFORM_SUPERPOSITION --> ORACLE_MARKED : oracle_kick / apply_oracle_1010 ORACLE_MARKED --> DIFFUSION_APPLIED : diffuse / apply_diffusion, increment_iter DIFFUSION_APPLIED --> READY_ITER2 : iterate [iter_count == 1] / checkpoint_iter READY_ITER2 --> ORACLE_MARKED_2 : oracle_kick / apply_oracle_1010 ORACLE_MARKED_2 --> DIFFUSION_APPLIED_2 : diffuse / apply_diffusion, increment_iter DIFFUSION_APPLIED_2 --> PRE_MEASUREMENT : measure [iter_count == 2] / arm_measurement PRE_MEASUREMENT --> FOUND_TARGET : collapse_target [outcome == "1010"] / record_outcome, record_prob_high PRE_MEASUREMENT --> FOUND_OTHER : collapse_other [outcome != "1010"] / record_outcome, record_prob_low note right of IDLE Verification Rules: - unitarity: every action composed of gates (H, X, CX) must be unitary; the overall channel from UNIFORM_SUPERPOSITION to PRE_MEASUREMENT is a product of 2 oracle unitaries and 2 diffusion unitaries, preserving the L2 norm at every transition - entanglement: during oracle and diffusion phases the four-qubit state is genuinely entangled (Schmidt rank > 1 across any bipartition) - no_cloning: no action may copy or broadcast quantum state before arm_measurement - completeness: P(FOUND_TARGET) + P(FOUND_OTHER) = 1.0 - custom: after iter_count == 2, |α_1010|² ≥ 0.97 (Grover formula, N=16, M=1, k=2) - custom: guards on collapse branches are mutually exclusive and exhaustive end noteGenerated OpenQASM 3.0
tested on a 4090 and 5090 GPU