-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Add SolovayKitaev as part of the default unitary synthesis plugin #15285
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
Conversation
Exposing entrypoint Removing duplicated Python code Using CowArray
Extendid the SK algorithm to handle other opertations (and not just standard gates) Adding UnitarySynthesisData to avoid generating basic approximation on every synthesis call.
|
One or more of the following people are relevant to this code:
|
Pull Request Test Coverage Report for Build 19812159716Warning: This coverage report may be inaccurate.This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.
Details
💛 - Coveralls |
| if is_clifford_t_basis_set(target, basis_gates, PhysicalQubit::new(qubit.0)) { | ||
| let solovay_kitaev = unitary_synthesis_data.get_solovay_kitaev(); | ||
| let matrix_nalgebra = Matrix2::from_fn(|i, j| matrix[[i, j]]); | ||
| let circuit = solovay_kitaev.synthesize_matrix(&matrix_nalgebra, 5); |
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.
in the case of 1q synthesis, here the approximation depth of synthesize matrix is hardcoded to be 5. Is there a way to not hardcode this? and perhaps consider input argument for approximation_degree to dictate the approximation depth? although it is true that that parameter dictates fidelity in all other decomposition cases, I just thought it may make sense to give some user control over the 1q synthesis with the approximation parameter, in a differing context here. Please let me know what you think about this?
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 completely agree, ideally we should translate the user-specified approximation_degree to recursion_degree used by synthesize_matrix, and note that this also depends on the basic_approximations used by the algorithm (notably on basis_gates and depth used to construct these basic approximations). I don't remember the exact guarantees of the approach, but it might be even the case that for a given approximation_degree we must choose both depth (for constructing basic approximations) and recursion_degree (for running the algorithm). In fact, we have discussed your very exact suggestion multiple times in the past (cc @Cryoris and @mtreinish) and it has been on our to-do list since forever 😅.
Another important point is that Solovay-Kitaev has a poor convergence rate, and (if I remember the experiments correctly) requires ~1000 of T-gates for the error of ~
For now, the default values (i.e. 5) are exactly the same as would be set by default from the python version of the pass.
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 think it's fine to hardcode a default, as long as the documentation (1) clearly states these values, and (2) explains how custom values can be set via the plugin interface. Could you add these informations to the UnitarySynthesis docs?
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.
As per my previous comment, I have added some documentation for the default unitary synthesis plugin here: 88f9697. Please take a look.
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.
Hi, thank you so much for your contribution, your PR looks great and is a clean implementation. Moving the synthesis logic to Rust and integrating Solovay-Kitaev for Clifford+T circuits is a significant step forward for performance and consistency. The refactoring is clean, and the introduction of UnitarySynthesisData to cache SK approximations is a thoughtful touch. I had just one small clarification in the code.
Further another question I had, was whether the property_set of pass manager can be leveraged to cache UnitarySynthesisData for multiple python facing calls of the :class:DefaultUnitarySynthesis plugin, as an exceptional case? albeit this is a transformation pass, we have done such transmutation in the case of :class:BasePadding which also was a transformation pass. Or is this something we strictly avoid otherwise?
Let me know what you think.
I think this suggestion would work. Though, since this is a bit of an edge-case, I would prefer thinking of the best possible longer-term solution and maybe postponing it to another PR. For instance, this wold require exposing And again this complication would disappear if/when we can switch to the Ross-Selinger algorithm. |
| ) -> PyResult<DAGCircuit> { | ||
| let out_dag = dag.copy_empty_like(VarsMode::Alike)?; | ||
| let mut out_dag = out_dag.into_builder(); | ||
| let mut unitary_synthesis_data = UnitarySynthesisData::new(); |
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.
Each transpile run here will re-construct the same set of basic approximations in the default Clifford+T path. Do we have precedence of locally storing a cache? Because SK does have a safe serialization format that we could use instead.
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.
Hmm, good point, since here we are hardcoding the basis gates (T/Tdg/H) and depth (12) for constructing SK approximations, we can indeed store them in a file, and have the default plugin to load approximations from the file rather than recompute them. Do I understand the suggestion correctly? What would we a good location for this file?
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.
After some benchmarks I think we can leave caching into a file for a follow-up, in case it becomes necessary. In the most common workflow, where users previously just did something like
transpile(circuit, basis_gates=["t", "h", "cx"])This PR is actually faster than main tested on a 10-qubit QFT circuit:
(main) Took 12.168 +- 0.352s
(this PR) Took 10.449 +- 0.341s
We can point out in the docs that for repeated transpilation it would be useful to manually write the translation stage and set the SolovayKitaevPlugin with a predefined basic approximation.
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.
Hmm, apparently we did not have any documentation for the default unitary synthesis plugin: first, it's empty, and, second, I have accidentally broken a link to the right page quite some time ago. Please take a look at the changes in 88f9697.
| if is_clifford_t_basis_set(target, basis_gates, PhysicalQubit::new(qubit.0)) { | ||
| let solovay_kitaev = unitary_synthesis_data.get_solovay_kitaev(); | ||
| let matrix_nalgebra = Matrix2::from_fn(|i, j| matrix[[i, j]]); | ||
| let circuit = solovay_kitaev.synthesize_matrix(&matrix_nalgebra, 5); |
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 think it's fine to hardcode a default, as long as the documentation (1) clearly states these values, and (2) explains how custom values can be set via the plugin interface. Could you add these informations to the UnitarySynthesis docs?
ShellyGarion
left a comment
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 have some minor comments on the documentation. could be handled in a follow-up PR.
| The default unitary synthesis plugin defines the default algorithm used by | ||
| :class:`.UnitarySynthesis` transpiler pass to synthesize or approximate unitary | ||
| gates in the circuit. Its behavior depends on the number of qubits in the | ||
| target unitary and whether the target basis is Clifford+T. |
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 wonder if we should use the terminology of Clifford+T, or perhaps noisy vs. fault-tolerant, or pre-fault-tolerance vs. fault-tolerance?
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 slightly prefer Clifford+T since other options (noisy, fault-tolerant, etc.) might mean different things to different people. But I will defer to @Cryoris for the best name to use.
| * 1-qubit gates: Euler angle decompositions (see :class:`.OneQubitEulerDecomposer`). | ||
| * 2-qubit gates: KAK/Cartan decompositions (see :class:`.TwoQubitBasisDecomposer`, | ||
| :class:`.TwoQubitControlledUDecomposer`, :class:`.XXDecomposer`). |
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 we seperate here between parametrized and non-paramterized gates? and discrete vs. countionuous sets?
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.
Do we do this by default? Any particular suggestions?
qiskit/transpiler/passes/synthesis/default_unitary_synth_plugin.py
Outdated
Show resolved
Hide resolved
|
|
||
| out = UnitarySynthesis(basis_gates=["cx", "h", "t", "tdg"]).run(dag) | ||
| self.assertTrue( | ||
| set(out.count_ops()).issubset(set(get_clifford_gate_names()).union({"t", "tdg"})) |
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 we test that when providing clifford circuit the compilation only include clifford 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.
I think such tests should be (and I believe already are) a part of SolovayKitaev testing. Here we only want to make sure that the right plugin gets used.
Also, if we later include other Clifford+T synthesis plugins and extend the tests, I don't think we should require that clifford-only circuits get compiled in only-clifford gates (if I remember correctly, there are examples where using T-gates reduces the number of 2-qubit operations).
Cryoris
left a comment
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.
Some comments on the wording, otherwise LGTM
qiskit/transpiler/passes/synthesis/default_unitary_synth_plugin.py
Outdated
Show resolved
Hide resolved
qiskit/transpiler/passes/synthesis/default_unitary_synth_plugin.py
Outdated
Show resolved
Hide resolved
qiskit/transpiler/passes/synthesis/default_unitary_synth_plugin.py
Outdated
Show resolved
Hide resolved
qiskit/transpiler/passes/synthesis/default_unitary_synth_plugin.py
Outdated
Show resolved
Hide resolved
| # that they have been altered from the originals. | ||
| """ | ||
| ============================================================================== | ||
| AQC Synthesis Plugin (in :mod:`qiskit.transpiler.passes.synthesis.aqc_plugin`) |
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.
Why were these removed? Was it just for consistency with other headers? 🙂
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.
Yes. Also (very subjectively) this looks nicer than what we have now.
Summary
This PR addresses the second step from Matthew's 4-step plan #14952 (review) to correctly set the default unitary synthesis plugin for Clifford+T compilation.
It's built on top of #15265 (merged).
Details and comments
In this PR, in the unitary synthesis pass in rust, for 1-qubit unitaries we check if
target/basis_gatescorrespond to a Clifford+T set, in which case we use the Clifford+T synthesis algorithm (and not the one based on euler basis sets). The current version integrates theSolovayKitaevalgorithm but hopefully in the future we will be able to replace it by the Ross-Selinger algorithm.Using
SolovayKitaevleads to an additional complication: the first step of the algorithm is to compute the basic approximations and we definitely don't want to do this for every 1-qubit unitary in the circuit. So I have defined a data structureUnitarySynthesisDatathat's shared between different unitaries, and that's lazily computed (if SolovayKitaev is not going to be used, the basic approximations would not be computed). I think @jakelishman does something like this and much more in his work-in-progress of rewriting the unitary synthesis pass. I tried to keep the changes to the minimum.There is one caveat though. If someone will call
DefaultUnitarySynthesisplugin multiple times from the Python side, the basis approximations will still be computed on every call. However, in all of our main flows, we directly call the unitary synthesis pass in rust and not invoke the default plugin.