-
Notifications
You must be signed in to change notification settings - Fork 33
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
Add support for generating subexperiments with LO's translated to a native gate set #517
base: main
Are you sure you want to change the base?
Conversation
Pull Request Test Coverage Report for Build 9084999560Details
💛 - Coveralls |
q = QuantumRegister(1, "q") | ||
def_rx = QuantumCircuit(q) | ||
theta = Parameter("theta") | ||
for inst in [RZGate(np.pi / 2), SXGate(), RZGate(theta + np.pi), RZGate(5 * np.pi / 2)]: |
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.
I used Qiskit to find these translations, and I didn't change anything, even if it looked suspicious. I'll label the ones I think can be simplified here, and we can discuss. This one rotates 5/2pi times at the end. I believe we can just rotate 1/2pi?
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.
@jakelishman , we should be able to simplify these strange rotation angles, right? I know the translation process uses a shortest-path algorithm, so I assume it can find paths containing these funky angles. Just wanted to be sure there wasn't something I was missing
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.
Since you're acting symbolically here, you can only really use the BasisTranslator
, which acts on a gate-by-gate basis, which just dumbly multiplies/divides parameters as appropriate during conversion. We don't do symbolic peephole optimisation (though we could), and we can't do full unitary resynthesis with symbolic parameters, so you'll end up with things like this trying to decompose symbolically in general.
In this particular case, we clearly could add a specific rx -> [rz,sx]
decomposition if we so chose (at the moment the default equivalence library does some funny roundabout thing). There's probably lots of places where we could improve the standard equivalence library for symbolic gates - we mostly just decompose to Euler angles and resynthesise into something more efficient during an optimisation pass.
# SXdgGate | ||
q = QuantumRegister(1, "q") | ||
def_sxdg = QuantumCircuit(q) | ||
for inst in [ |
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.
Another suspicious one. Should be able to do a single pi rotation on either side of sx
q = QuantumRegister(1, "q") | ||
def_ry = QuantumCircuit(q) | ||
theta = Parameter("theta") | ||
for inst in [SXGate(), RZGate(theta + np.pi), SXGate(), RZGate(3 * np.pi)]: |
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.
Should be able to rotate by pi
at the end here
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.
I suspect that the reason there are factors of greater than 2pi in the equivalence library is because a rotation of 2pi doesn't actually bring the wavefunction to its original state; instead, e.g., for RZGate, it leads to the wavefunction picking up a global phase of -1. The rotation must be by an angle of 4pi to bring its state completely to where it began (see also: the mathematics of spinors). Even though a rotation by 2pi leads to a global phase of -1, this will not result in any difference in the state that is actually physically observable, but nonetheless Qiskit carefully keeps track of global phases, and I believe this is one instance of where that leads to some rotation angles that seem a bit atypical.
Here's a quick sanity check (in julia) given the RZGate definition of a rotation by 2pi:
In [1]: RZ(λ) = [exp(-im * λ / 2) 0; 0 exp(im * λ / 2)]
Out[1]: RZ (generic function with 1 method)
In [2]: RZ(2π)
Out[2]: 2×2 Matrix{ComplexF64}:
-1.0-1.22465e-16im 0.0+0.0im
0.0+0.0im -1.0+1.22465e-16im
I was seeing some failures when I tried to use this in the roundtrip tests. For every roundtrip test, I would run the standard gate set and also the eagle gate set. 4 tests were failing. I suppose those tests should've been passing if all of our decompositions are logically equivalent. I'd like to integrate the different equivalences into the roundtrip tests elegantly somehow. Would love to hear any suggestions. I also need to figure out why the tests are failing before merging this |
All the roundtrip tests pass with Eagle translated LO's except those including |
…toolbox into translate-sampled-gates
@@ -142,8 +142,9 @@ def test_cutting_exact_reconstruction(example_circuit): | |||
subcircuits, bases, subobservables = partition_problem( | |||
qc, "AAB", observables=observables_nophase | |||
) | |||
qpu = [None, "eagle", "heron"][np.random.randint(2)] |
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.
Unit tests are fine to make sure the equivalences can be accessed and used in the ways we intend, but the equivalence bugs that worry me are bugs involving indexing and expval correctness, so I really want to fold the equivalence testing into the roundtrip tests.
That being said, I didn't want to double the runtime of this test by making a whole new loop testing the equivalences, so I will just randomly translate subexperiments and make sure we can reconstruct the exact expval.
I will sample the standard gate set 50% of the time and the eagle/heron gateset 50% of the time
@garrison @ibrahim-shehzad this is ready for final review now. |
I had left off an |
) | ||
|
||
_eagle_sel = HeronEquivalenceLibrary = EagleEquivalenceLibrary = EquivalenceLibrary() | ||
equivalence_libraries = defaultdict( |
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.
Right now, if a user passes an unsupported basis, everything will run fine and their subexperiments will be in the standard gate set. Maybe we'd prefer to error if they pass in a QPU architecture we don't support or doesn't exist?
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.
if a user passes an unsupported basis
By that, do you mean that they have a QPDBasis
that contains some gate(s) that are not supported by this equivalence library?
everything will run fine and their subexperiments will be in the standard gate set
What do you mean by "everything will run fine"? What will happen to the unsupported gates?
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.
If a user passes basis_gate_set="nonsense"
, their gates will come out in the standard gate set defined in decompositions.py
. In other words, it's a no-op
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.
By that, do you mean that they have a QPDBasis that contains some gate(s) that are not supported by this equivalence library?
I mean they pass in a string that doesn't describe a supported QPU architecture. A string not in {"heron", "eagle"}
Co-authored-by: Jim Garrison <garrison@ibm.com>
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.
Overall, one big comment: I wonder to what extent we could be relying on Qiskit for this functionality. We could get the same thing from the BasisTranslator, I believe. The problem you mentioned previously, from what I understand, is that the circuit -> dagcircuit -> circuit round-trip is inefficient. But, do we actually have data on how slow this overall process is compared to using the functionality introduced here? And if it is much slower, I wonder how much is because of the circuit <-> dagcircuit roundtrip vs. how much of it is the BasisTranslator itself.
If we were to use BasisTranslator, we would support any hardware that Qiskit does instead of having to add support for each backend's basis set individually. This feels like the wrong layer to do it if Qiskit already has that support -- and from the API perspective, the user has already chosen a backend once -- why should they have to specify again that it's Heron, Eagle, or whatever. But if Qiskit's benchmarks make this infeasible, we will have to work around it somehow, like this PR does.
Caleb's reply:
But, do we actually have data on how slow this overall process is compared to using the functionality introduced here?
Transpiling 1296 depth-4 133-qubit circuits takes 41 seconds to transpile in our cutting-pea experiments. So if these were utility-scale circuits that weren't simple graph states, you can multiply this by some factor; whereas, doing it the way introduced in this PR the relative runtime cost would be ~0.
For edit: Qiskit's |
…s/circuit-knitting-toolbox into translate-sampled-gates
I'm not sure what you mean here. They've potentially transpiled their cut circuit using their backend in their local environment, but they haven't given a backend to CKT at this point in the workflow. They're using this new kwarg to communicate to CKT how their cut circuit has been transpiled so we can prepare their gate decompositions correctly |
Instead of {"heron", "eagle"}, we could take the EDIT: This is kind of a bad idea. Using these two strings makes everything very straightforward. The info we need is being passed explicitly from the user. Passing in a backend means we have to account for all the different backend classes and make sure we are extracting their basis gate sets correctly and making sure we have accounted for all the permutations of basis gates across Runtime, qiskit provider, and Qiskit generic backends. Leaving this the way it is |
@garrison I know we run some transpilation during creation of the subexperiments (e.g. RemoveFinalResets), so we already do the copies for each subexperiment. If you think it is more appropriate to handle the sampled gates at that time, that would be equally effective. Totally open to adding these gates during transpilation or the way I have implemented here. I don't think users should have to do any extra copies for translation that can reasonably be avoided though, due to the volume of subexperiments. |
Obviously Caleb knows this already, but for anyone else watching this thread: we actually ended up removing these passes in #556 in order to improve performance, replacing them with functions that operate directly on the |
I am essentially on board with this. Indeed, I have always desired for the bulk of transpilation to occur before generating the subexperiments, and my modification and test in #303 were meant to the be first step toward supporting that kind of workflow. (My recent PR, #573, creates a how-to guide for demonstrating this.) However, obviously there are some steps which must be run after the subexperiments are generated. Previously, I've imagined that this will involve a pass manager that has a reduced set of passes, including some final basis translation together with some simple optimization passes (e.g., if two Hadamard gates appear on a qubit consecutively, they can be removed because I now think that there is a time where such passes on a However, there are plenty of occasions where one doesn't want to wait to optimize the circuits to the best of the transpiler's ability, and the current PR is valuable for this reason. After all, we should aim to have the simple case be quick. Note there are some limitations to this PR's approach, times at which conversion to
Right now, we don't support either case, but maybe we will have to this about this in the future, e.g. if we want to provide direct support for cutting Toffoli gates (#258 (comment)).
Actually, IMO taking the If the target instruction set of the backend matches something we recognize, then we can just use the equivalence library in this PR. If it does not match something we recognize, we can warn the user that the slow code path is being used, then allow Qiskit to do the basis translation. This way, the user will always get something that works, but will benefit from the improved performance on backends that we explicitly have written optimized equivalence libraries for. And maybe eventually some of this work can influence Qiskit, such that we don't even have to have our own equivalences here and can just rely on the ones in Qiskit to be as good as ours. Some day. |
In a conversation with @mtreinish, he suggested to me that we should be able to use the standard equivalence library provided by Qiskit. If it is lacking some desired equivalences, then we can always copy the object and add our own equivalances to our library's copy, then use that as we see fit. Or, if there are equivalences that are not in Qiskit, they are likely to be welcome upstream too. Either way, this allows us to avoid using the |
How can we use the |
Sorry, after talking to @mtreinish, I had thought the Dijkstra's algorithm search was in |
…toolbox into translate-sampled-gates
If you're trying to avoid using the basis translator and the dag then creating an equivalence library is not needed. You should just use a python dictionary. Under the covers the advantage the eagle_y_circ = QuantumCircuit(1)
eagle_y.rz(math.pi, 0)
eagle_y.x(0)
eagle_translation_dict = {
"y": eagle_y
} which will be faster and simpler. Also the other thing you can do is use the synthesis library to generate the definitions if you're not confident in the decomposition being good. Something like: from qiskit.synthesis import OneQubitEulerDecomposer, TwoQubitBasisDecomposer
from qiskit.circuit.library import ECRGate, CZGate, YGate
one_qubit_decomp = OneQubitEulerDecomposer("ZSXX")
eagle_decomp_two = TwoQubitBasisDecomposer(ECRGate(), euler_basis="ZSXX")
heron_decomp_two = TwoQubitBasisDecomposer(CZGate(), euler_basis="ZSXX")
eagle_translation_dict = {
"y": one_qubit_decomp(YGate()),
"cz": eagle_decomp_two(CZGate()),
}
heron_translation_dict = {
"y": eagle_translation_dict["y"],
"ecr": heron_decomp_two(ECRGate()),
} This won't always give the optimal decomposition, but it simplifies it as much as the transpiler can by default. For example, def_swap_cz = QuantumCircuit(q, global_phase=-pi / 2)
def_swap_cz.sx(0)
def_swap_cz.sx(1)
def_swap_cz.cz(0, 1)
def_swap_cz.sx(0)
def_swap_cz.sx(1)
def_swap_cz.cz(0, 1)
def_swap_cz.sx(0)
def_swap_cz.sx(1)
def_swap_cz.cz(0, 1) but the decomposition from |
Thanks Matthew, the reason I didn't do that is because the parameters pass through nicely from the input gate to the translated representation. I'm sure that functionality can be recreated w a normal Python dict, but I'd have to think how. EDIT: I think this is actually really easy. I just hadn't used python dicts like this before :) |
…ood. Move translation to QPD.
@@ -214,7 +224,13 @@ def _decompose_qpd_instructions( | |||
for data in inst.operation.definition.data: | |||
# Can ignore clbits here, as QPDGates don't use clbits directly | |||
assert data.clbits == () | |||
tmp_data.append(CircuitInstruction(data.operation, qubits=[qubits[0]])) | |||
if basis_gate_set is None or data.operation.name in {"qpd_measure"}: |
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.
Anything that comes out of QPDBasis
can be translated except our measurement objects. Those never need to be translated
…toolbox into translate-sampled-gates
Fixes #492
This PR introduces an
EagleEquivalenceLibrary
andHeronEquivalenceLibrary
for optionally translating all the gates we use in cutting decompositions to the corresponding native gate set during creation of subexperiments. This allows users to transpile their "Cut Circuit" one time, and when theygenerate_cutting_experiments
all of their subexperiments will already be translated to the native gate set for the processor type they specified. If they don't specify, the "standard" gate set will be used. Heron single qubit native gates are the same as Eagle, so we get that equivalence library for free.TODO: