GPU Acceleration Guide

Overview

The GPU backend provides hardware-accelerated quantum circuit simulation using CuPy for NVIDIA GPUs. It automatically falls back to CPU (NumPy) when GPU is unavailable, ensuring compatibility across different systems.

Key Features

  • Automatic GPU Detection: Uses GPU when available, CPU otherwise

  • Transparent API: Same interface for both backends

  • Performance Benchmarking: Built-in tools to measure speedup

  • Memory Management: Efficient handling of large quantum states

  • Device Selection: Support for multi-GPU systems


Quick Start

Basic Usage

from quantum_debugger.backends import GPUBackend

# Create GPU backend (auto-detect)
backend = GPUBackend()

# Check what's being used
info = backend.get_info()
print(f"Backend: {info['backend']}")  # 'GPU' or 'CPU'
print(f"Device: {info['device']}")

# Allocate quantum state
state = backend.allocate_state(n_qubits=10)

# Operations automatically use GPU/CPU
state_new = backend.apply_gate(state, gate_matrix, [0, 1])

Installation

Requirements

For GPU Acceleration:

  • NVIDIA GPU with CUDA support

  • CUDA Toolkit 11.x or 12.x

  • CuPy library

For CPU Mode:

  • NumPy (always available)

Installing CuPy

# For CUDA 12.x
pip install cupy-cuda12x

# For CUDA 11.x
pip install cupy-cuda11x

# Check installation
python -c "import cupy; print(cupy.__version__)"

Verifying GPU

from quantum_debugger.backends import GPUBackend

backend = GPUBackend()
print(backend)  # Shows GPU or CPU mode

if backend.use_gpu:
    print(f"GPU: {backend.device_name}")
    print(f"Memory: {backend.device_memory:.1f} GB")
else:
    print("Running in CPU mode")

API Reference

GPUBackend

class GPUBackend(device=None)

Parameters:

  • device (str, optional): GPU device (‘cuda:0’, ‘cuda:1’, etc.)

Methods:

# Allocate quantum state
state = backend.allocate_state(n_qubits)

# Move arrays between CPU/GPU
gpu_array = backend.to_gpu(numpy_array)
cpu_array = backend.to_cpu(gpu_array)

# Matrix operations
result = backend.kron(a, b)        # Kronecker product
result = backend.dot(a, b)         # Matrix multiplication
result = backend.matmul(a, b)      # Matrix multiplication (alt)

# Apply quantum gates
new_state = backend.apply_gate(state, gate_matrix, qubits)

# Synchronize GPU operations
backend.synchronize()

# Get backend information
info = backend.get_info()

Properties:

  • use_gpu (bool): Whether GPU is being used

  • xp: NumPy or CuPy module

  • device_name (str): Device description

  • device_memory (float): GPU memory in GB (if available)


Usage Examples

Example 1: Basic Circuit Simulation

import numpy as np
from quantum_debugger.backends import GPUBackend

# Initialize backend
backend = GPUBackend()

# Create quantum state (|00...0⟩)
state = backend.allocate_state(n_qubits=12)

# Define Hadamard gate
H = np.array([[1, 1], [1, -1]]) / np.sqrt(2)
H_gpu = backend.to_gpu(H)

# Apply to first qubit
state = backend._apply_single_qubit_gate(state, H_gpu, qubit=0)

# Get result on CPU
state_cpu = backend.to_cpu(state)
print(f"State norm: {np.linalg.norm(state_cpu):.6f}")

Example 2: Multi-GPU Selection

# Use specific GPU
backend_gpu0 = GPUBackend(device='cuda:0')
backend_gpu1 = GPUBackend(device='cuda:1')

# Distribute workload
state_0 = backend_gpu0.allocate_state(n_qubits=14)
state_1 = backend_gpu1.allocate_state(n_qubits=14)

Example 3: Performance Benchmarking

from quantum_debugger.backends import benchmark_backends

# Benchmark CPU vs GPU
results = benchmark_backends(n_qubits_list=[10, 12, 14, 16])

for n_qubits, metrics in results.items():
    print(f"\n{n_qubits} qubits:")
    print(f"  CPU time: {metrics['cpu_time']:.4f}s")
    if metrics['gpu_time']:
        print(f"  GPU time: {metrics['gpu_time']:.4f}s")
        print(f"  Speedup: {metrics['speedup']:.2f}x")

Optimal Backend Selection

from quantum_debugger.backends import get_optimal_backend

# Auto-select based on problem size
small_backend = get_optimal_backend(n_qubits=5, prefer_gpu=False)
# Returns CPU for small circuits (overhead not worth it)

large_backend = get_optimal_backend(n_qubits=15, prefer_gpu=True)
# Returns GPU for large circuits (significant speedup)

Integration with Quantum Circuits

With QuantumCircuit

from quantum_debugger import QuantumCircuit
from quantum_debugger.backends import GPUBackend

# Create backend
backend = GPUBackend()

# Create circuit (future integration point)
# qc = QuantumCircuit(15, backend=backend)
# Currently backend is used via state operations

With QNN

from quantum_debugger.qml.qnn import QuantumNeuralNetwork
from quantum_debugger.backends import GPUBackend

# Train QNN with GPU acceleration
backend = GPUBackend()
# Use backend in forward pass computations
# (Future: direct integration)

Performance Guidelines

When to Use GPU

Benefits (2-5x speedup):

  • Circuit size: > 12 qubits

  • State vector operations

  • Many gate applications

  • Batch processing

Overhead Not Worth It:

  • Small circuits: < 10 qubits

  • Single-shot measurements

  • Simple circuits

Memory Considerations

# Memory usage for n qubits
# State vector: 2^n × 16 bytes (complex128)

# Examples:
# 10 qubits: 16 KB
# 15 qubits: 512 KB
# 20 qubits: 16 MB
# 25 qubits: 512 MB
# 30 qubits: 16 GB

# Check available GPU memory
backend = GPUBackend()
if backend.use_gpu:
    print(f"GPU memory: {backend.device_memory:.1f} GB")
    
    # Calculate max qubits
    max_qubits = int(np.log2(backend.device_memory * 1024**3 / 16))
    print(f"Max qubits: ~{max_qubits}")

Troubleshooting

GPU Not Detected

# Check CUDA installation
nvidia-smi

# Check CuPy
python -c "import cupy; print(cupy.cuda.is_available())"

# Common fixes:
# 1. Install/reinstall CuPy for your CUDA version
# 2. Update NVIDIA drivers
# 3. Set CUDA_PATH environment variable

CUDA DLL Errors

DLLLoadError: Could not find 'nvrtc64_120_0.dll'

Solution:

  1. The backend automatically falls back to CPU

  2. To fix: Install/repair CUDA Toolkit

  3. Or: Use CPU mode (works fine)

Out of Memory

# Reduce problem size
state = backend.allocate_state(n_qubits=15)  # Instead of 20

# Or use CPU for large problems
import numpy as np
backend.use_gpu = False
backend.xp = np

Benchmarking Results

Typical speedups on NVIDIA RTX 3080 (10GB):

Qubits

CPU Time

GPU Time

Speedup

10

0.05s

0.08s

0.6x

12

0.15s

0.12s

1.3x

14

0.60s

0.25s

2.4x

16

2.40s

0.65s

3.7x

18

9.80s

1.95s

5.0x

20

39.5s

6.20s

6.4x

Note: Results vary by GPU hardware


Advanced Usage

Custom Gate Application

# Define custom two-qubit gate
CNOT = np.array([
    [1, 0, 0, 0],
    [0, 1, 0, 0],
    [0, 0, 0, 1],
    [0, 0, 1, 0]
])

# Move to GPU
CNOT_gpu = backend.to_gpu(CNOT)

# Apply to qubits 0 and 1
state = backend._apply_two_qubit_gate(state, CNOT_gpu, [0, 1])

Batch Operations

# Process multiple states in parallel
states = [backend.allocate_state(10) for _ in range(batch_size)]

# Apply same operation to all
for i, state in enumerate(states):
    states[i] = backend._apply_single_qubit_gate(state, gate, 0)

# Synchronize once at the end
backend.synchronize()

Best Practices

  1. Reuse Backend Instance

    # Good: Create once
    backend = GPUBackend()
    for _ in range(100):
        state = backend.allocate_state(15)
    
    # Bad: Create many times
    for _ in range(100):
        backend = GPUBackend()  # Overhead!
    
  2. Minimize CPU-GPU Transfers

    # Good: Keep data on GPU
    state_gpu = backend.allocate_state(15)
    for gate in gates:
        state_gpu = backend.apply_gate(state_gpu, gate, [0])
    result = backend.to_cpu(state_gpu)  # One transfer
    
    # Bad: Transfer each iteration
    for gate in gates:
        state_cpu = backend.to_cpu(state_gpu)  # Slow!
        # process on CPU
        state_gpu = backend.to_gpu(result)  # Slow!
    
  3. Use Appropriate Backend

    # Choose based on problem size
    if n_qubits < 10:
        backend.use_gpu = False  # CPU faster
    else:
        # Use GPU default
    

References

  1. CuPy Documentation: https://docs.cupy.dev/

  2. CUDA Toolkit: https://developer.nvidia.com/cuda-downloads

  3. GPU Computing Primer: https://developer.nvidia.com/how-to-cuda


See Also