-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
Refactoring commutativity analysis and a new commutative inverse cancellation transpiler pass #8184
Refactoring commutativity analysis and a new commutative inverse cancellation transpiler pass #8184
Conversation
Thank you for opening a new pull request. Before your PR can be merged it will first need to pass continuous integration tests and be reviewed. Sometimes the review process can be slow, so please be patient. While you're waiting, please feel free to review other open PRs. While only a subset of people are authorized to approve pull requests for merging, everyone is encouraged to review open pull requests. Doing reviews helps reduce the burden on the core team and helps make the project's code better for everyone. One or more of the the following people are requested to review this:
|
Pull Request Test Coverage Report for Build 2832797885
💛 - Coveralls |
The tests are adapted from CommutativeCancellation, InverseCancellation and issue 8020.
Does is make sense to limit commutation checking to only 1-qubit and 2-qubit gates? @ShellyGarion suggested to replace the check |
This is ready for review/feedback. |
I can 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.
Overall this LGTM, I left a few inline comments. I think the big things are some expansion of the unittests to cover more edge cases and where to put the commutation checker class. The other thing which I didn't put inline is I think have some dedicate tests for the commutation checker class would be good. Other than that this is missing release notes too
) | ||
|
||
|
||
class CommutationChecker: |
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.
Considering all the circular import issues around this I'm wondering if this makes more sense in something more central in the import tree like quantum_info
or dagcircuit
especially since the use of this isn't used exclusively by transpiler passes. Although if we make this part of quantum info it might be better to remove the use of the dag node classes and instead take in the args and op separately for node1
and node2
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.
These are all good ideas: removing the use of the dag node classes (handled in a1284b9) and moving the class to a more central place (still note sure where exactly to move it).
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 the previous suggestion, the function commute
in commutation_checker
now looks as
def commute(self, op1: Operation, qargs1: List, cargs1: List, op2: Operation, qargs2: List, cargs2: List)
In particular, it does not depend on DAGNode
or DAGDepNode
and can also be called on gates/operations in QuantumCircuit
.
After some discussion with @ShellyGarion and @eliarbel, I have moved commutation_checker
to qiskit/circuit
. This refactoring also allowed to avoid runtime imports.
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 guess the only question for me is why qiskit/circuit
instead of qiskit/quantum_info
? I could see a case for either location and don't feel to strongly one way or the other. I'm fine with this decision but having a comment in the PR review and/or as part of commit message for future reference might be useful in case someone attempts to revisit it's location again.
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 don't feel strongly one way or another either, but it seems that quantum_info
should be agnostic to the encompassing circuit (i.e. it can reason about how to represent a Clifford using stabilizer/destabilizer tables or even about how to create a circuit for a Clifford, but it should not reason about where Clifford sits in the larger circuit), while commutation checker does look where the pair of gates sit in the larger circuit (looking at qargs
and cargs
). So personally circuit
seems a better fit.
qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py
Outdated
Show resolved
Hide resolved
qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py
Outdated
Show resolved
Hide resolved
qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py
Outdated
Show resolved
Hide resolved
|
||
from .commutation_checker import CommutationChecker | ||
|
||
cc = CommutationChecker() |
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.
To keep the cache warm between executions of the pass I wonder if we want to scope this to the instance, or even put this in the pass manager itself so it can be shared between passes that use the checker?
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, this is a very good point, the cache of which pairs of gates commute or do not commute can be reused between different passes pertaining to the same quantum circuit (for instance, during the optimization look in transpile
with optimization_level=3
), or even for different quantum circuits (provided they have similar gates, or else the cache will not be useful). Though let's handle this extension in a follow-up PR.
# ToDo: Even less sure about the next line | ||
# if node.op.params: | ||
# return True |
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 the is_parameterized()
check should cover this too.
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.
Oh, I see the difference now: gate.is_parameterized()
returns True
for a parameterized gate with an unbound parameter (such as RZGate(Parameter("Theta"))
), but returns False
for a parameterized gate with a bound parameter (such as RZGate(np.pi/2)
). The current analysis takes the over-conservative approach in the unbound case (skipping such gates or declaring that such gates do not commute with other gates), and works well in the bound case (for instance, cancelling the inverse pair of RZGate(np.pi/2)
and RZGate(-np.pi/2)
).
return True | ||
if node.op.condition: | ||
return True | ||
# ToDo: Not sure about the next line |
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 this is fine, unless you know for a parameterized gate if the commutative and inverse checking will eval true even with a Parameter
or ParameterExpression
object are used for any gate parameters.
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.
We probably want to have this extension in the future, but let's keep things simple for now. This comment is also relevant to many other transpiler passes, which usually also ignore conditional gates, parameterized gates, and directives (e.g. collecting 1q or 2q runs).
from qiskit.transpiler.passes import CommutativeInverseCancellation | ||
|
||
|
||
class TestCommutativeInverseCancellation(QiskitTestCase): |
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'd be good to have some tests for some edge cases like with parameterized gates in the circuit, some directives like barrier. Also some of the operations explicitly skipped like reset. Just to make sure all those paths are exercised.
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 added these corner case tests, and have also added dedicated tests for CommutationChecker
.
…ellation.py Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
…ellation.py Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
@mtreinish, thanks for the review! I have incorporated all of your comments, adding corner-case tests for The only thing that I haven't done is adding |
Yeah, I'm fine with updating the preset pass managers in a follow up. It'll let us explore the performance in isolation and evaluate how it effects the runtime and weigh that against any improvements in the quality of output. |
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.
This LGTM, thanks for making all the updates.
Summary of this PR:
CommutationChecker
.CommutationAnalysis
andDagDependency
. There is no change to the actual functionality.CommutativeInverseCancellation
that cancels pairs of inverse gates exploiting commutation relations.CommutativeInverseCancellation
. These include tests adapted fromCommutativeCancellation
andInverseCancellation
, and problematic tests from CommutationAnalysis needs to be refactored #8020 .What's already in Qiskit
Let me try to summarize (based on my understanding) what we already have in Qiskit (related to this PR).
CommutativeAnalysis
:*) Efficiently checks commutativity between two DAG nodes, cleverly hashing both positive and negative results (i.e. whether various pairs of gates commute or not)
*) Creates a dictionary describing commutation relations on a given wire, however does not property handle transitivity (see #8020 for an in-depth discussion). As the result, transpiler passes based on the created dictionary may output incorrect results. This is definitely the case for
CommutativeCancellation
(see #8020) and probably for #8034. Personally I cannot think of an application where the created dictionary structure would be useful.CommutativeCancellation
:*) Transpiler pass that cancels self-inverse gates and rotations using commutativity analysis (as per
CommutativeAnalysis
).*) Additionally, combines multiple z-rotations and multiple x-rotations (but not y-rotations), also see #8178 for a related PR.
*) Additionally does something called "basis priority change" (I did not understand the purpose for that).
*) Only works with a specific predefined set of self-inverse and rotation gates.
*) Misses optimization opportunities and may lead to incorrect results (see #8020).
InverseCancellation
*) Transpiler pass that cancels consecutive pairs of inverse gates.
*) The pairs of inverse gates must be specified by the caller, e.g.,
InverseCancellation(gates_to_cancel = [CXGate(), HGate(), (RXGate(np.pi / 4), RXGate(-np.pi / 4))
]).*) Very efficient in terms of performance, based on
collect_runs
.*) Does not use commutativity analysis.
*) Not used in preset pass managers.
DagDependency
*) A canonical representation of non-commutativity in a circuit. Currently only used for
TemplateOptimization
transpiler pass (which by itself is not used in any of the preset pass managers).*) Uses similar (but not exactly the same) commutativity checking as
CommutativeAnalysis
, but does not hash results.*) For each node (in
DagDependency
), computes and stores the list of its transitive successors = all nodes forward reachable from this node, and the list of its transitive predecessors = all nodes backward reachable from this node. This leads to large time required to constructDagDependency
and to huge memory overhead (quadratic in the total number of nodes).*) On the other hand, keeping the lists of successors and predecessors allows an efficient check whether a pair of nodes in
DagDependency
commutes.*) Based on
DagDepNode
. Personally, I don't understand why we need separate classesDagDepNode
instead ofDagNode
, andDagDependency
instead ofDagCircuit
. After all, it's still a DAG.TemplateOptimization
*) A transpiler pass that allows to optimize circuits based on templates.
*) Uses commutativity analysis (as per
DagDependency
)*) Very slow to run (at least with default options).
*) Not used in preset pass managers.
Some experimental results
["x", "y", "z", "h", "s", "sdg", "cx", "cz", "swap"]
. Clifford circuits are obviously special, so please treat these results with a grain of salt. The defaultseed
is 0 (and the results are quite consistent with respect toseed
).Constructing
DagDependency
Memory consumption for
DagDependency
(usingmemory_profiler
,memory
reports the memory increment forcircuit_to_dagdependency
)Time to run
TemplateOptimization
Here
#gates
is the original number of gates, and#gates_opt
is the optimized number.Other optimization techniques
We are running
transpile
withoptimization_level=3
InverseCancellation
withgates_to_cancel = [CXGate(), HGate(), SwapGate(), CZGate(), ZGate(), XGate(), YGate()]
CommutativeCancellation
CommutativeInverseCancellation
)As before,
#gates
is the original number of gates, and#gates_opt
is the optimized number.Please note that the new pass
CommutativeInverseCancellation
results in most optimization opportunities, but is a bit slower thanInverseCancellation
andCommutativeCancellation
(but still faster than the time required to constructDagDependency
objects for these circuits).In this PR:
CommutationChecker
*) Essentially copy-pasting checking whether two gates commute from
CommutativeAnalysis
in a separate class. This includes includes hashing of commutativity results.*) Sufficient to implement simple transpiler passes, such as the new
CommutativeInverseCancellation
pass.*) Note:
CommutationChecker
(with its hashing) can be reused even when the circuit changes!Simplifying code for
CommutationAnalysis
andDagDependency
*) Replacing code to check whether a pair of gates commutes using
CommutationChecker
*) Need to see if the old code for
DagDependency
had some more optimizations that could be useful here (and also need to see @Sebastian-Brandhofer's improvements in #8081).A new
CommutativeInverseCancellation
transpiler pass*) Uses
CommutationChecker
to check whether a pair of nodes commutes.*) Does not construct a full
DagDependency
(which would be much slower), though is inspired by howDagDependency
is constructed.*) Does not do the additional things done by
CommutativeCancellation
, such as combining rotations exploiting commutativity relations, or doing basis priority change.*) Inverse gates detected via
nodes[idx2].op == nodes[idx1].op.inverse()
. This is reasonably fast, but may miss some opportunities, for instance it does not see that the inverse ofRx(np.pi)
isRx(np.pi}
(since it's technicallyRx(-np.pi)
.Further discussion
Rethink what kind of commutativity analysis is needed for specific applications
CommutationChecker
is needed (as in the newCommutativeInverseCancellation
transpiler pass). There is no need to construct the canonicalDagDependency
data structure. Furthermore, I don't know howDagDependency
would even help here.DagDependency
is needed, but there is no need to compute and store the lists of transitive successors and predecessors for every node. In this case, the algorithm to constructDagDependency
can be significantly improved, both in terms of performance and memory requirement.TemplateOptimization
) we do need the full lists of transitive successors and predecessors for every node. Then we probably have to pay the price.Further discussion
Do we need the
CommutativeAnalysis
transpiler pass at all? Definitely, the dictionary it now provides is not very useful as there is no transitivity. Should we fix these so that transitivity would hold?We should do something about
CommutativeCancellation
transpiler pass (especially as it may produce wrong results). Should we try to fix it? Or maybe its functionality can be replaced byCommutateInverseCancellation
+ transpiler passes that combine single-qubit rotations?Can we reimplement
DagDependency
to beDagCircuit
? There is no need to useDagDepNode
(which stores information required specifically forTemplateOptimization
pass).