-
Notifications
You must be signed in to change notification settings - Fork 62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SymbolicHamiltonian
refactor
#1548
SymbolicHamiltonian
refactor
#1548
Conversation
SymbolicHamiltonian
terms
and dense
settersSymbolicHamiltonian
refactor
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #1548 +/- ##
==========================================
- Coverage 99.61% 99.58% -0.04%
==========================================
Files 76 76
Lines 11449 11325 -124
==========================================
- Hits 11405 11278 -127
- Misses 44 47 +3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
I am still unsure whether it is worth keeping the two different ways for computing the dense form of the {
"2q": 36,
"3q": 81,
"4q": 144,
"5q": 225,
"6q": 324,
"7q": 441,
"8q": 576,
"9q": 729,
"10q": 900,
"11q": 1089
} the average runtimes across 10 runs are as follows: time vs nqubits time vs nterms the Long story short, if I had to pick only one of the two I'd probably lean towards the EDIT: a small update on this one: I've done some minor optimization to the ![]() at this point it is maybe not worth keeping |
Run on QPU
|
Just a small update on the GPU scaling, I tested the def multikron_reduce(matrix_list, kron):
return reduce(kron, matrix_list)
def multikron_einsum(matrix_list, einsum):
indices = list(range(2 * len(matrix_list)))
even, odd = indices[::2], indices[1::2]
lhs = zip(even, odd)
rhs = even + odd
einsum_args = [item for pair in zip(matrix_list, lhs) for item in pair]
N = 2 ** len(matrix_list)
h = einsum(*einsum_args, rhs)
return np.sum(np.reshape(h, (-1, N, N)), axis=0) the results with numpy and cupy with 10 2x2 matrices are (100 repetitions):
the GPU scales wonderfully as expected and I suspect this would transfer to the other similar functions that I encountered here. However, the drawback for the CPU seems too big to consider einsum a viable option, and differentiating among the backends would mean to move these functions to the backend directly. Thus I am not sure about what to do. |
Just as an attempt, could you try to use einsum(..., optimize=True) or even einsum(..., optimize="optimal") ? |
I tested |
Well, if NumPy is getting no benefit, than it's not worth in any case. However, this would be interesting to investigate: Just to exclude that the contraction's computation is actually taking a huge amount of time, I'd propose to compute the contraction executed by, and pass it explicitly to
EDIT: you may also compute the contraction path by a separate invocation of |
I tried calculating the path separately, but no difference at all, it looks like the contraction is taking that time and that's it. def get_path(matrix_list):
indices = list(range(2 * len(matrix_list)))
even, odd = indices[::2], indices[1::2]
lhs = zip(even, odd)
rhs = even + odd
einsum_args = [item for pair in zip(matrix_list, lhs) for item in pair]
return np.einsum_path(*einsum_args, rhs, optimize="optimal")
def multikron_einsum(matrix_list, einsum, path):
indices = list(range(2 * len(matrix_list)))
even, odd = indices[::2], indices[1::2]
lhs = zip(even, odd)
rhs = even + odd
einsum_args = [item for pair in zip(matrix_list, lhs) for item in pair]
N = 2 ** len(matrix_list)
h = einsum(*einsum_args, rhs, optimize=path[0])
return np.sum(np.reshape(h, (-1, N, N)), axis=0) |
Ok, sorry, I was a bit ignorant about the Kronecker product (I knew what it was, but definitely not familiar with it - so, I had no intuition). In any case, the Most likely, in the I tried measuring the increment (ratios of subsequent timing) and even the scaling itself is not clear: reduce [40.13 1.9 1.51 1.39 1.52 1.89 2.66 3.46 3.87 5.53]
einsum [0.67 1.2 1.3 1.64 2.5 5.57 3.71 3.56 4.66 4.47] Despite they may fluctuate a bit, mutual relations are overall pretty stable. So, all in all it is not clear to me there is neither a theoretical nor a practical advantage in doing one thing or the other. I'd suggest to just keep the simplest. In case of further doubts, I actually timed the I was doubting of the final summation, but I did not realize that is a summation over a dimension of length 1 (in fact, it is not even needed, you could directly |
Thanks to all of you for the comments, I should have addressed everything already.
|
I would go for the plain |
Run on QPU
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @BrunoLiegiBastonLiegi.
- What to do with the whole
einsum
vsreduce
argument we discussed above together with @alecandido. At the moment, it looks like it would be better to just stick withreduce
. Another option would be to make themultikron
and similar functions part of the backends, if it's worth it.
From my side, I don't think I ever tried to benchmark these methods, so I do not have any feedback in terms of performance. I would also go with reduce, as it looks simpler in terms of code. If there is any chance that a different implementation would provide a significant advantage in performance we could do it in a different PR after verifying such advantage (you could even open an issue if you think that is worth exploring).
- In the
hamiltonian.models
module most of the hamiltonians implement adense
construction separately from thesymbolic
one:
https://github.com/qiboteam/qibo/blob/617788e0c0cd5f8997fa7fbb5b382bf87442fa9f/src/qibo/hamiltonians/models.py#L84C1-L93C58
and I wonder whether this is still useful when one could just doSymbolicHamiltonian.dense()
I am also not a big fan of providing multiple, equally simple, ways to do the same thing, so I would agree with you that it is not very useful. However, one thing to point out is that removing the dense
option would be breaking for anyone using that interface and worse, even for people not using it, as it would change the current default (dense=True
). It could be fine, as there are a few other (potentially less) breaking changes implemented here and I am not sure if we are following a strict version convention for such changes in qibo.
Run on QPU
sim
completed!
This is an interesting QPU partition...
Thanks for the feedback, I can then maybe open two issues:
After that, I think this can be safely merge.
that's the best ever, it works perfectly everytime as long as you don't ask for more than 30 qubits |
Ok, after the tests passed this can be merged. |
Run on QPU
|
This should address #1494
EDIT: this turned out to be more in general a refactor of
SymbolicHamiltonian
and partly ofhamiltonian/models.py
. Namely,SymbolicHamiltonian
now only has aform
attribute,dense
andterms
are cached properties computed starting fromform
and, thus, they can no longer be manually setted. Concerninghamiltonian/models.py
, now the models are built starting from theform
and not theterms
as it was before. I also made the dense representation of the models more backend aware, replacing thenumpy
operations with the backend ones.Note: tests on
cupy
are still failing due to this qiboteam/qibojit#196 .EDIT 2:
I went ahead and provided both
Symbols
andSymbolicTerm
with abackend
attribute which is useful whenever you move to the dense form representation. Initially, I thought about makingbackend
an additional input argument to the functions that needed it (similarly to what we do with gates), but this would have meant breaking the api (e.g.symbol.matrix
->symbol.matrix(backend)
).What it means now though, is that there are cases where the backends of the different pieces may mismatch, thus causing problems. To mitigate this, I decided to forcely set the backend of the symbols here https://github.com/qiboteam/qibo/blob/4d8c9c1211c850a5e740607de943da75fc31f8b2/src/qibo/hamiltonians/terms.py#L176C1-L179C50
which allows for the end user to not care about which backend to construct a symbol with, maintaining in practice the usual API.
As part of the refactor, I removed the
symbol_map
argument of the hamiltonians, thefrom_symbolic
method and theTrotterHamiltonian
.Checklist: