GPU Batch VQE

July 7, 2026 · View on GitHub

scpn_quantum_control.phase.gpu_batch_vqe

Parallel evaluation of multiple VQE parameter sets using CPU NumPy or an explicit PyTorch/CUDA request. Useful for landscape scanning, hyperparameter search, or initialisation strategies.

Caveat: The built-in scan evaluates a product Ry-layer diagnostic ansatz with random parameters. It is a statevector expectation landscape scanner, not a gradient-optimised production VQE and not hardware execution. For production VQE with hardware-efficient ansätze, use Qiskit's VQE class, PennyLane, or a domain-specific ansatz passed through the lower-level energy evaluators.

Requires: pip install torch for GPU acceleration.


Theory

Batch Evaluation

A VQE with PP parameters needs one forward pass (wavefunction → energy) per parameter set. With BB parameter sets:

MethodCost
CPU sequentialB×TsingleB \times T_\text{single}
GPU batchedTsingle+overheadT_\text{single} + \text{overhead} (amortised across BB)

The GPU advantage depends on batch size, qubit count, transfer overhead, and CUDA device throughput. batch_vqe_scan(..., use_gpu=True) requires PyTorch and an available CUDA device; it raises instead of silently falling back to the NumPy path.

Ry-Layer Ansatz

The built-in ansatz applies Ry(θi)R_y(\theta_i) to each qubit:

ψ(θ)=iRy(θi)0n|\psi(\theta)\rangle = \prod_i R_y(\theta_i) |0\rangle^{\otimes n}

This product-state ansatz is sufficient for diagnostic landscape scanning but does not express entangled ground states. The lower-level batch_energy_numpy and batch_energy_torch functions accept custom ansätze.


API Reference

from scpn_quantum_control.phase.gpu_batch_vqe import (
    batch_energy_numpy,
    batch_energy_torch,
    batch_vqe_scan,
)

batch_energy_numpy

energies = batch_energy_numpy(
    H: np.ndarray,                    # (dim, dim) Hamiltonian
    param_sets: np.ndarray,           # (batch, n_params) parameter vectors
    ansatz_fn: Callable,              # params → statevector (dim,)
) -> np.ndarray  # (batch,) energies

CPU baseline. Evaluates each parameter set sequentially.

batch_energy_torch

energies = batch_energy_torch(
    H: np.ndarray,
    param_sets: np.ndarray,
    ansatz_fn: Callable,              # torch.Tensor → torch.Tensor
    device: str = "cuda",             # "cuda" or "cpu"
) -> np.ndarray  # (batch,)

PyTorch-backed batch evaluation. Passing device="cuda" requires an available CUDA runtime; device="cpu" is valid for torch parity checks.

batch_vqe_scan

result = batch_vqe_scan(
    K: np.ndarray,                    # (n, n) coupling matrix
    omega: np.ndarray,                # (n,) frequencies
    n_samples: int = 100,             # parameter sets to evaluate
    n_params: int | None = None,      # parameters per set (default: n)
    seed: int = 42,
    use_gpu: bool = False,            # use PyTorch GPU if available
    max_dense_gib: float | None = None,
) -> dict

max_dense_gib gates the dense Hamiltonian and batched statevector workspace before allocation; use_gpu=True remains an explicit CUDA request and is not silently downgraded to CPU.

Returns:

{
    "energies": np.ndarray,       # (n_samples,) all energies
    "params": np.ndarray,         # (n_samples, n_params) all parameter sets
    "best_energy": float,          # minimum energy found
    "best_params": np.ndarray,    # parameters that achieved best energy
    "n_samples": int,
    "backend": str,               # "numpy" or "torch_cuda"
    "ansatz_family": str,         # "product_ry_layers"
    "optimizer": str,             # "random_parameter_scan"
    "hardware_claim": str,        # "none_statevector_expectation_scan"
}

Tutorial

Random Parameter Scan

import numpy as np
from scpn_quantum_control.phase.gpu_batch_vqe import batch_vqe_scan

n = 6
K = 0.45 * np.exp(-0.3 * np.abs(np.subtract.outer(range(n), range(n))))
np.fill_diagonal(K, 0.0)
omega = np.linspace(0.8, 1.2, n)

result = batch_vqe_scan(K, omega, n_samples=200, seed=42)
print(f"Best energy: {result['best_energy']:.6f}")
print(f"Best params: {result['best_params']}")

# Compare with exact
from scpn_quantum_control.bridge.knm_hamiltonian import knm_to_dense_matrix
H = knm_to_dense_matrix(K, omega)
E_exact = np.linalg.eigvalsh(H)[0]
print(f"Exact ground: {E_exact:.6f}")
print(f"Gap: {result['best_energy'] - E_exact:.4f}")

GPU Acceleration

import torch

if torch.cuda.is_available():
    result_gpu = batch_vqe_scan(K, omega, n_samples=1000,
                                use_gpu=True, seed=42)
    print(f"GPU best energy: {result_gpu['best_energy']:.6f}")
else:
    print("No CUDA GPU available; call without use_gpu=True for the NumPy path")

Custom Ansatz

from scpn_quantum_control.phase.gpu_batch_vqe import batch_energy_numpy
from scpn_quantum_control.bridge.knm_hamiltonian import knm_to_dense_matrix

H = knm_to_dense_matrix(K, omega)
dim = 2**n

def custom_ansatz(params):
    """Two-layer Ry ansatz."""
    psi = np.zeros(dim, dtype=complex)
    psi[0] = 1.0
    # Layer 1
    for i in range(n):
        c, s = np.cos(params[i]/2), np.sin(params[i]/2)
        new_psi = np.zeros_like(psi)
        for k in range(dim):
            bit = (k >> i) & 1
            k_flip = k ^ (1 << i)
            if bit == 0:
                new_psi[k] += c * psi[k]
                new_psi[k_flip] += s * psi[k]
            else:
                new_psi[k] += c * psi[k]
                new_psi[k_flip] -= s * psi[k]
        psi = new_psi
    # Layer 2 (with params[n:2n])
    for i in range(n):
        c, s = np.cos(params[n+i]/2), np.sin(params[n+i]/2)
        new_psi = np.zeros_like(psi)
        for k in range(dim):
            bit = (k >> i) & 1
            k_flip = k ^ (1 << i)
            if bit == 0:
                new_psi[k] += c * psi[k]
                new_psi[k_flip] += s * psi[k]
            else:
                new_psi[k] += c * psi[k]
                new_psi[k_flip] -= s * psi[k]
        psi = new_psi
    return psi

param_sets = np.random.randn(50, 2*n)
energies = batch_energy_numpy(H, param_sets, custom_ansatz)
print(f"Best of 50: {energies.min():.6f}")

Comparison

FeatureThis moduleTorchQuantumPennyLane
GPU batchYes (PyTorch)YesYes
AnsatzSimple Ry / customHardware-efficientWide variety
Auto-diffNoYesYes
OptimiserScan onlyFull VQEFull VQE
Backendnumpy / PyTorchPyTorchMultiple

This module is a landscape scanning tool. For full VQE optimisation with gradient descent, use param_shift.py or PennyLane.


References

  1. Wang, H. et al. "QuantumNAS: Noise-adaptive search for robust quantum circuits." HPCA (2022). (TorchQuantum)

See Also