Skip to content

Real GPU gate simulation via CuPy + qutip 5.x compat fixes - #8

Merged
jascal merged 5 commits into
jascal:mainfrom
shamangeorge:gpu-backed-orca
Apr 16, 2026
Merged

Real GPU gate simulation via CuPy + qutip 5.x compat fixes#8
jascal merged 5 commits into
jascal:mainfrom
shamangeorge:gpu-backed-orca

Conversation

@shamangeorge

@shamangeorge shamangeorge commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

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 break
  • 6f9990a — wire cuquantum GPU backend into the quantum-evolve GA demo

Problem

The cuquantum backend added in #7 was a no-op on the GPU. It called
qutip_cuquantum.set_as_default() before delegating to dynamic_verify(), but
set_as_default() only activates GPU for ODE-based solvers (SESolver/MESolver
with CuVern7). The dense dtype in the cuDensity group maps to
qutip.core.data.Dense (CPU). Gate-based matrix multiplication in dynamic_verify
was always running on CPU regardless.

Additionally, a silent qutip 5.x compatibility break was causing QUTIP_AVAILABLE = False
for all users, making dynamic_verify return valid=True trivially without doing any
simulation 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 a cupy.ndarray
    on-device and applies each gate matrix via cp @ cp GPU matmuls. CPU touch happens once
    at 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 to dynamic_verify_gpu instead of dynamic_verify.

qutip 5.x fixes (were silently breaking QUTIP_AVAILABLE):

Old New
from qutip import partial_trace removed; use Qobj.ptrace(subsystem)
basis(2**n, 0) basis([2]*n, [0]*n) — required for ptrace to work
from 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 inner

GPU in the GA demo

  • BACKEND module-level flag (default: "cuquantum") + --backend CLI flag
  • Each _verify_source() call uses the full dynamic pipeline with VerifyOptions(backend=BACKEND)
  • GPU memory delta printed per-evaluation so you can watch the GPU doing real work
  • Design goal updated to 4-qubit Grover search (harder fitness landscape, more qubits = more GPU work)

Proof: GPU Is Actually Being Used

GPU unit tests — 4/4 green

tests/test_backends.py::TestCuQuantumGPUActivation::test_evolve_path_gpu_returns_cupy_array  PASSED
tests/test_backends.py::TestCuQuantumGPUActivation::test_gpu_result_matches_cpu              PASSED
tests/test_backends.py::TestCuQuantumGPUActivation::test_gpu_memory_allocated_during_verify  PASSED
tests/test_backends.py::TestCuQuantumGPUActivation::test_cuquantum_backend_calls_gpu_verify  PASSED

4 passed in 1.87s

test_gpu_memory_allocated_during_verifypool.total_bytes() goes from 0 → >0 after
dynamic_verify_gpu — proves CuPy is allocating on-device.

Full suite: 378 passed, 2 skipped, 0 failures


Demo: quantum-evolve GA with --backend cuquantum

Run:

python demos/quantum_evolve/demo.py --backend cuquantum --population 3 --generations 2

Console output

╔══════════════════════════════════════════════════════════════════╗
║  🧬  QUANTUM EVOLVE                                             ║
║  Genetic Algorithm over Q-Orca Quantum State Machines           ║
║                                                                  ║
║  Outer loop:  Classical Orca GA controller (orca-runtime-python) ║
║  Population:  Q-Orca quantum state machines (q-orca)            ║
║  Evolution:   LLM-assisted fitness, crossover, and mutation     ║
╚══════════════════════════════════════════════════════════════════╝

  ·  LLM provider:    anthropic / claude-sonnet-4-6
  ·  Verify backend:  cuquantum
  ·  Population:      3   Max generations: 2
  ·  Fitness target:  99.0
  ·  Design goal:     Design a quantum state machine that implements Grover's search algorithm

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  🔧 PHASE 1: Load classical GA controller
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  ✓  Machine: QuantumEvolver
  ·  States:      ['idle', 'initializing', 'evaluating', 'selecting', 'breeding', 'converged', 'exhausted']
  ·  Transitions: 7

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  🧬 PHASE 3: Evolution
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  🧬  Seeding initial population
  ·  Generating 3 valid, unique quantum machines via LLM...

  ⟡  LLM #1: generate individual 0
       ↳ 27.8s  (6062 chars)
       ↳ GPU Δmem: +512 bytes       ← CuPy allocating on-device
  ·  g0-0: GroverSearch4Qubit  7 states  7 transitions  [valid]
  ✓  g0-0: accepted (1/3)
  ⟡  LLM #2: generate individual 1
       ↳ 29.5s  (5668 chars)
  ·  g0-1: GroverSearch4Q_1010  9 states  8 transitions  [valid]
  ✓  g0-1: accepted (2/3)
  ⟡  LLM #3: generate individual 2
       ↳ 31.6s  (6977 chars)
  ·  g0-2: GroverSearch4Q_1010  10 states  9 transitions  [valid]
  ✓  g0-2: accepted (3/3)

  ✓  Population seeded: 3 valid, unique individuals

  📊  Fitness evaluation  ·  Generation 0

  ID         Fitness                          V  Parents   Rationale
  ──────── ─────────  ──────────────────────  ─  ────────  ─────────────────────────
  g0-2       88.0/100  █████████████████░░░   ✓  seed      The machine is well-structur..
  g0-0       82.0/100  ████████████████░░░░   ✓  seed      The machine is well-structur..
  g0-1       82.0/100  ████████████████░░░░   ✓  seed      The machine is well-structur..

  ·  Best this gen:  g0-2  fitness=88.0
  ·  Best overall:   g0-2  fitness=88.0

  📊  Fitness evaluation  ·  Generation 1

  ID         Fitness                          V  Parents   Rationale
  ──────── ─────────  ──────────────────────  ─  ────────  ─────────────────────────
  g1-0       88.0/100  █████████████████░░░   ✓  g0-2      Elite carry-over from g0-2
  g1-1       82.0/100  ████████████████░░░░   ✓  g0-1      The machine is well-structur..
  g1-2       72.0/100  ██████████████░░░░░░   ✓  g0-1      The machine has correct over..

  📊  Fitness evaluation  ·  Generation 2

  ID         Fitness                          V  Parents      Rationale
  ──────── ─────────  ──────────────────────  ─  ──────────── ─────────────────────────
  g2-0       88.0/100  █████████████████░░░   ✓  g1-0         Elite carry-over from g1-0
  g2-1       88.0/100  █████████████████░░░   ✓  g1-1, g1-0   The machine is nearly fully..
  g2-2       82.0/100  ████████████████░░░░   ✓  g1-1         The machine is well-structur..

  ⏱️  Evolution Exhausted
  Final state:    exhausted
  Generations:    2
  Best fitness:   88.0 / 100
  LLM calls:      14
  Elapsed:        215.7s

Fitness progression

Gen Best Avg Valid
0 88.0 84.0 3/3
1 88.0 80.7 3/3
2 88.0 86.0 3/3

14 LLM calls, 215.7s elapsed, all 6 bred individuals valid.


Best Evolved Machine: GroverSearch4Q_1010

Fitness: 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 note
Loading

Generated OpenQASM 3.0

// Generated by Q-Orca compiler
// Machine: GroverSearch4Q_1010
OPENQASM 3.0;
include "stdgates.inc";

qubit[4] q;
bit[4] c;

int iter_count = 0;

// Gate sequence derived from state machine transitions
// IDLE -> UNIFORM_SUPERPOSITION via init
// UNIFORM_SUPERPOSITION -> ORACLE_MARKED via oracle_kick
// ORACLE_MARKED -> DIFFUSION_APPLIED via diffuse
// DIFFUSION_APPLIED -> READY_ITER2 via iterate
// READY_ITER2 -> ORACLE_MARKED_2 via oracle_kick
// ORACLE_MARKED_2 -> DIFFUSION_APPLIED_2 via diffuse
// DIFFUSION_APPLIED_2 -> PRE_MEASUREMENT via measure

// Measurement
c[0] = measure q[0];
c[1] = measure q[1];
c[2] = measure q[2];
c[3] = measure q[3];

tested on a 4090 and 5090 GPU

@jascal jascal left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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"] = False

to 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_entanglement is correct — CNOT/CZ/SWAP on |0...0> cannot create entanglement
  • test_vqe_rotation.py fidelity 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 jascal left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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 sufficient

For 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:
    pass

The 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 matmul

For 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(), ...), and qeye([2]*n) are all right — and the table in the PR body makes the diff easy to audit.
  • test_gpu_memory_allocated_during_verify is 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 --backend flag is a good ergonomic addition.
  • The _check_dynamic_entanglement any-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:

  1. Fix the entanglement check logic mismatch between dynamic_verify_gpu and _check_dynamic_entanglement (any-pair vs all-pairs).
  2. Add the superposition-gate early exit to dynamic_verify_gpu.
  3. Fix import cupy as _cpimport cupy # noqa: F401 (or just import cupy).
  4. Remove unused MagicMock, call, cmath imports in tests.

This review was posted automatically by Claude Sonnet 4.6.

@jascal jascal left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Code Review — Claude Sonnet 4.6

Test post to verify gh CLI is working.

shamangeorge added a commit to shamangeorge/q-orca-lang that referenced this pull request Apr 16, 2026
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.
shamangeorge added a commit to shamangeorge/q-orca-lang that referenced this pull request Apr 16, 2026
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.
…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.

@jascal jascal left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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 CPUdynamic_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 pathhas_superposition guard prevents false-positive DYNAMIC_NO_ENTANGLEMENT errors on circuits with no H/RX/RY/RZ gates.

5. Demo goal change split outdemos/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:
    pass

call imported but unused in test_backends.pyfrom unittest.mock import MagicMock, call, patchcall 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 jascal left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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/main into the branch. The only conflict was in tests/test_backends.py imports — resolved by keeping patch (used on line 262) and dropping the unused MagicMock and call.
  • q_orca/verifier/dynamic.py: swapped import cupy as _cp for import cupy # noqa: F401, and removed the unused import numpy as np that snuck into _check_unitary_gates.
  • demos/quantum_evolve/demo.py: added choices=["cuquantum", "qutip", "none"] to --backend so invalid values fail at parse time instead of deep inside the dynamic verifier.
  • tests/test_vqe_rotation.py: dropped the unused import 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.

@jascal
jascal merged commit d8a1833 into jascal:main Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants