Adding Hamiltonian Terms Takes Super Long

Hello! I’m running VQE for a water dimer with 16 electrons, 12 orbitals (24 qubits). It seems the bottleneck in the program is adding both Hamiltonians in this function:

The calculation of the res term in finite_diff_r1 takes insanely long, which isn’t a surprise because a single Hamiltonian looks like:

So +14,000 terms. Is there anyway to reduce the time taken to add these two together? I’d normally consider to ways to make a reduced Hamiltonian like qubit tapering(but this would probably require additional code, which means the new reduced Hamiltonian needs to recalculated for every new input of x), so I’d prefer to speed up adding them up instead.

Thanks so much

Edit: I did try qubit tapering, but that barely reduced the Hamiltonian:

The next thing I tried was adding using qml.Hamiltonian() using the SumWithQMLHamiltonian(obs_to_sum, coeffs_for_sum) from this question asked on the forum, but it still takes pretty long.

Now what I’m doing is using another function from that same post:

def SimplePythonSum(obs_to_sum, coeffs_for_sum):
    # Check that there is one multiplication factor for each Hamiltonian and one Hamiltonian for each multiplication factor.
    assert(len(obs_to_sum) == len(coeffs_for_sum))

    # Sum up all Hamiltonians in obst_to_sum times their respective coefficient from coeffs_for_sum.
    cost_h = sum([coeffs_for_sum[i]*obs_to_sum[i] for i in range(len(obs_to_sum))])
    # Return summed Hamiltonian.
    return cost_h

but trying to replace sum with cupy.sum to attempt to add two large Hamiltonians using the GPU-accelerated cupy library. However, when I try to convert this to a cupy.array, it says the type is unsupported:

Would any Pennylane expert know how to use Cupy to accelerate adding two Hamiltonians?, or any other suggestions would be amazing.Thanks!

Hi @ImranNasrullah,

Thanks for your question. Could you please post your full code and the output of qml.about()? Thanks!

Sure, here it is:

# Defining a simple Hamiltonian for test
symbols = ["He", "H"]
geometry = np.array([[0.00000000, 0.00000000, -0.87818361],
                     [0.00000000, 0.00000000,  0.87818362]])

t1 = qml.qchem.molecular_hamiltonian(symbols, geometry, charge=1)[0]

shift = np.zeros_like(geometry)
delta = 0.01
shift[1] += 10 * delta

t2 = qml.qchem.molecular_hamiltonian(symbols, geometry + shift, charge=1)[0]

# Constructing array to put into function
ops_to_add = [t1, t2]
coeffs_to_add = [1, -1]

###########################################################
# Unmodified Function
import numpy as npo
def SimplePythonSum(obs_to_sum, coeffs_for_sum):
    # Check that there is one multiplication factor for each Hamiltonian and one Hamiltonian for each multiplication factor.
    assert(len(obs_to_sum) == len(coeffs_for_sum))

    # Sum up all Hamiltonians in obst_to_sum times their respective coefficient from coeffs_for_sum.
    cost_h = npo.sum([coeffs_for_sum[i]*obs_to_sum[i] for i in range(len(obs_to_sum))])
    # Return summed Hamiltonian.
    return cost_h

s1 = time.time()
res1 = SimplePythonSum(ops_to_add,coeffs_to_add)
time.time()-s1

###########################################################
# Modified to attempt to use cupy
import cupy as cp
def SimplePythonSum(obs_to_sum, coeffs_for_sum):
    # Check that there is one multiplication factor for each Hamiltonian and one Hamiltonian for each multiplication factor.
    assert(len(obs_to_sum) == len(coeffs_for_sum))

    # Sum up all Hamiltonians in obst_to_sum times their respective coefficient from coeffs_for_sum.
    cost_h = cp.sum(cp.array(([coeffs_for_sum[i]*obs_to_sum[i] for i in range(len(obs_to_sum))])))
    # Return summed Hamiltonian.
    return cost_h

s1 = time.time()
res1 = SimplePythonSum(ops_to_add,coeffs_to_add)
time.time()-s1

Finally, here’s is the output of qml.about():

Hi @ImranNasrullah ,

The error that you’re seeing is originating in the cupy library. You should check with the cupy community because the issue doesn’t seem to be the size of the Hamiltonians, it’s an unsupported type issue. I don’t know how cupy works so I don’t know why you would prefer to use cp.array instead of a numpy array. I also don’t know if it’s a good idea to combine both, but my best guess is that you should use a numpy array instead of a cupy one.

I hope this helps you.

Hello @CatalinaAlbornoz ,
The reason I’m using the cup library is because I’m trying to efficiently add two large Hamiltonians of +14000 terms. So I thought the best way to do it was to use the cupy library, as it is GPU supported and could potneitally add the two Hamiltonians together very quickly.

If not cupy, could you recommend other ways to go about making the adding of the two Hamiltonians as quick as possible (I’ve tried qubit-tapering as well as adding through the “qml.Hamiltonians(simplify = True)” functionality)

Hi @ImranNasrullah ,

Your unmodified works well for me. It ran in 0.004403829574584961s. How much lower were you expecting? Or where do you start running into issues?

Oh, the Hamiltonian’s I provided above were to make simple the piece of code, because the part I was curious about was using cupy.

Here are the actual Hamiltonians I’m working with and trying to speedup:

################ Code from the 'Optimization of molecular geometries' tutorial##################
# imports and relevant defines:
import pennylane as qml
from pennylane import qchem
from pennylane import numpy as np
import time
import concurrent.futures
import multiprocessing
from multiprocessing import Value

# Simulation starting parameters:
symbols = ["O", "H", "H", "O", "H", "H"]

x = np.array([ 0.        ,  0.        ,  0.        ,  1.563923  ,  0.98312309,
        -0.60365174, -0.78395943,  1.43700227,  1.04747968,  4.26606551,
         0.56799183,  1.68673518,  4.63354675,  1.15239926,  3.50338239,
         6.10594353,  0.1835633 ,  1.19292837], requires_grad=True)

active_electrons = 16
active_orbitals = 10

# define the hamiltonian needed to compute cost-function
def H(x):
    return qml.qchem.molecular_hamiltonian(symbols, x, charge=0, active_electrons = active_electrons, active_orbitals = active_orbitals)[0]

hf = qml.qchem.hf_state(electrons=active_electrons, orbitals=active_orbitals*2)
print(hf)
num_wires = active_orbitals*2

H1 = qml.qchem.molecular_hamiltonian(symbols, x, charge=0,
                                            active_electrons = active_electrons,
                                            active_orbitals = active_orbitals, method = "pyscf")[0]
shift = np.zeros_like(x)
delta = 0.01
shift[4] += 0.5 * delta

H2 = qml.qchem.molecular_hamiltonian(symbols, x+shift, charge=0,
                                            active_electrons = active_electrons,
                                            active_orbitals = active_orbitals, method = "pyscf")[0]

# Adding Hamiltonians using  +
start = time.time()
res = H1 + H2
time.time()-start

Hi @ImranNasrullah ,

Thanks for sharing this code. And you’re right that sharing a small example is very helpful in most cases.

In this case I was able to run your full example in 0.1742s.

How long is it taking for you?

With 20 qubits, adding the water dimer Hamiltonians took you that little?? Did you have the same qml.about() environment as me?

On my end, the code runs for hours and it still doesn’t complete, and for a VQE workflow, I have 6 of those calculations to do, so that was the whole issue for me.

So then if it’s possible to add them that quickly, that’s amazing! I’m not sure what could be causing the discrepancy in our two timings though

The code just finished running for adding the Hamiltonian’s for the water dimer, which takes super long:

Hi @ImranNasrullah ,

I just ran it on Google Colab and it ran super fast indeed. But today I can’t seem to be able to connect so I can’t print the qml.about().

I literally just ran the code you shared so I’m not sure what’s causing your issue. I’ll try to see if I can run it again in Colab in the next few days and share the qml.about() with you.

Hi @ImranNasrullah,

I got to run it today again in Google Colab and here’s what I got. I’m trying to run it locally and it is taking a very long time though so it doesn’t look like you’re doing something wrong, it just seems to be a very big molecule that takes a long time to run.

[1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0]
0.1609053611755371
Name: PennyLane
Version: 0.36.0
Summary: PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Train a quantum computer the same way as a neural network.
Home-page: https://github.com/PennyLaneAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: /usr/local/lib/python3.10/dist-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, rustworkx, scipy, semantic-version, toml, typing-extensions
Required-by: PennyLane_Lightning

Platform info:           Linux-6.1.85+-x86_64-with-glibc2.35
Python version:          3.10.12
Numpy version:           1.25.2
Scipy version:           1.11.4
Installed devices:
- lightning.qubit (PennyLane_Lightning-0.36.0)
- default.clifford (PennyLane-0.36.0)
- default.gaussian (PennyLane-0.36.0)
- default.mixed (PennyLane-0.36.0)
- default.qubit (PennyLane-0.36.0)
- default.qubit.autograd (PennyLane-0.36.0)
- default.qubit.jax (PennyLane-0.36.0)
- default.qubit.legacy (PennyLane-0.36.0)
- default.qubit.tf (PennyLane-0.36.0)
- default.qubit.torch (PennyLane-0.36.0)
- default.qutrit (PennyLane-0.36.0)
- default.qutrit.mixed (PennyLane-0.36.0)
- null.qubit (PennyLane-0.36.0)

Hey @CatalinaAlbornoz , apologies for getting around to this rather late. So I tried adding the Hamiltonians on Google Collab, and it takes very quickly, around 0.2 seconds! Thanks so much for the catch

Everything is near smooth, now the only issue is that the memory requirements for 20 qubits blows up whenever I am trying to evaluate the finite different Hamiltonian expectation value to get gradient updates of my nuclear coordinates for a molecule.

Specifically, the numbers go up into the ~80 gb range, causing Google collab to crash. It’s strange, because I’m using the lightning.gpu module with diff_method="adjoint" for the circuit dev, which fixed the memory issue on the Linux server I was using before google collab, but on google collab, it explodes in the memory usage despite the above settings.

I’m wondering if you have any thoughts on what could be going wrong?

Thanks so much for the help as always!

Hi @ImranNasrullah ,

Google Colab only gives you limited free resources so there’s not much you can do there. If you can share your full new code with me I can see if I notice something but unfortunately there may not be a good answer if the problem is too big for both your server and Google Colab. This motivates the need for actual quantum hardware to solve these problems more efficiently.

Hey @CatalinaAlbornoz ,
Oh what I was trying to say was that using lightning.gpu and diff_method=adjoint fixed the memory explosion problems on the server while they weren’t causing the same change on Google Collab.

Either way though, the code I was using on Google collab was multiprocessing the the gradient updates. As soon as I removed the multiprocessed code, this fixed the memory issue, so I think all should be well.

Thanks!

Ah nice @ImranNasrullah ! I’m glad to hear that everything’s working well now.