-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Add DAGDependencyV2 and Converters #11705
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
One or more of the the following people are requested to review this:
|
Pull Request Test Coverage Report for Build 8705828497Warning: 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 |
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 am very excited about this new DAG-dependency interface because it's significantly closer to the DAGCircuit
interface and in principle allows to unify the two even further. Ideally I would like the two interfaces to be almost fully identical (modulo the comment below) and I have left several inline comments to that extent. In particular, I am suggesting to remove or to adapt all methods that don't have a close DAGCircuit
analogue (or add such methods to DAGCircuit
as well). (This does not apply to internal methods prefixed with _
).
But even right now this allows to write transpiler passes that can work with either circuit representation.
Other than that, we want release notes and a few more tests exploring various DAGDependencyV2
functionality (possibly many of these can be adapted from DAGDependency
tests).
The promised comment is that DAG-dependency is conceptually different from a DAG-circuit and some operations on DAG-dependency have to be implemented considerably differently. For instance, we don't yet have a DAG-dependency analogue of substitute_node_with_dag
because adding new nodes to DAG dependency generally involves performing commutativity checks and recursively examining descendants and ancestors of the existing nodes.
def get_node(self, node_id): | ||
""" | ||
Args: | ||
node_id (int): label of considered node. | ||
|
||
Returns: | ||
node: corresponding to the label. | ||
""" | ||
return self._multi_graph.get_node_data(node_id) |
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 am slightly confused: DAGCircuit
does not have this function nor ever checks _multi_graph.get_node_data
. Maybe we don't need these functions, given your changes to unifying the predecessor/successor interface? I.e. this looks like a point where the APIs for DAGCircuit
and DAGDependencyV2
slightly deviate, and I am wondering if we can avoid 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.
This is needed in template matching so not important there, but also in _update_edges
since that method is written using node indices instead of node object ids. I did a first try to convert this method to using object ids, but could not come up with an efficient way to do it. So for now I would leave this and address it later.
The use of node indices could also be a problem if nodes are inserted or removed and then iterated over.
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.
Leaving this in until I rewrite _update_edges
to not use indices. Also used in some of the tests, which will need to be redone later.
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.
Agreed. What about making get_node
into an internal function (i.e. just renaming it to _get_node
), so that we will able to freely change/remove it later?
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.
BTW, let's rewrite _update_edges
in a follow-up PR, not now.
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.
Agreed on _get_node
and redo _update_edges
later.
def remove_op_node(self, node): | ||
"""Remove an operation node n. | ||
|
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 am a little bit worried about this function in the context of DAG dependency. Though (after some thinking) I do believe that this method of removing nodes and reconnecting predecessors to successors is safe: we will not be able to incorrectly infer from dag dependency that two nodes commute when in fact they do not. It might not be compete: the dag dependency may say that two nodes do not commute when in fact they do. For example, suppose that we have a chain of nodes A -> B -> C
. By definition, A
and B
do not commute, and B
and C
do not commute (but this says nothing about whether A
and C
commute). If we remove B
and retain edges, we will end up with A -> C
, which might be over-conservative if A
and C
do commute. Note that it's not enough to check the commutativity of A
and C
and remove the edge if they do commute, since we should also recursively check if C
commutes with predecessors of A
, and A
commutes with successors of C
. So at the minimum let's update the docstring of this function. Hmm, when would we ever want to remove nodes from a DAG?
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.
The comment for replace_block_with_op
addresses the question of commutativity of the output, which is relevant here as well. remove_op_node
is used extensively in the transpiler passes.
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.
Agreed, replace_block_with_op
has the same flavor: the resulting DAG-dependency may hide some commutativity relations. So at some point we may want to add a function to reconstruct DAGDependency
from scratch.
target_dag.comm_checker = 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.
Can we do this instead: target_dag.comm_checker = self.comm_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.
Ok.
from test import QiskitTestCase | ||
|
||
from qiskit.converters.dagdependency_to_circuit import dagdependency_to_circuit | ||
from qiskit.converters.circuit_to_dagdependency import circuit_to_dagdependency | ||
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit | ||
from test import QiskitTestCase # pylint: disable=wrong-import-order |
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 can't say that I fully understand why PR #10998 replaced all lines of the form
from test import QiskitTestCase
to
from test import QiskitTestCase # pylint: disable=wrong-import-order
but it may be best to keep the change 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.
Not sure why either, but can leave as is.
class TestCircuitToDagCanonical(QiskitTestCase): | ||
"""Test QuantumCircuit to DAGDependency.""" |
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 the name of the class be TestCircuitToDagDependency
(not that it really matters).
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.
Ok.
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 for the quick updates. I have added a few more minor inline suggestions, with the only other thing being the release notes :).
if key is None: | ||
key = _key | ||
|
||
return iter(rx.lexicographical_topological_sort(self._multi_graph, key=_key)) |
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.
return iter(rx.lexicographical_topological_sort(self._multi_graph, key=_key)) | |
return iter(rx.lexicographical_topological_sort(self._multi_graph, key=key)) |
I just stumbled upon this typo while working on something else. :-)
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.
For others' reference: this DAGDependencyV2
is exploratory work that we're doing with Ed (@enavarro51) about reworking the DAGDependency
into a usable and much faster form for template matching, and potentially using it more throughout other parts of the transpiler pipeline.
We're looking to merge it into the main
Qiskit as a fully private object, mostly to support longer-term working on the project with Ed as an external contributor while working with us. There's a chance that the Python implementation never gets made fully public, but that instead we make a fully Rust version of this DAGDependencyV2
(with Python wrappers) and use that in things like template matching and commutation analysis. There are follow-up PRs to this that will do things like update the _update_wires
function and the core data structure a little to include the requirement to track a leaves
set when modifying the graph (in order to reduce the computational complexity of appends to the graph), but it's still in the largely exploratory phases at the moment.
Ed: I'm largely fine to merge this as-is, except for:
- let's remove the public release note
- we probably want to fix the typo Sebastian noticed
For the bits Sasha was talking about with remove_op_node
(and really any removals): the best situation is probably just to remove those functions from this PR - if they're broken, there's no real reason to include them in even the exploratory merge. I've no issue with removing all the functions that exist that we don't immediately have a use for in the exploratory stuff - we can add useful things as/when we need them. If anything, it's probably best to start with less than more, just so we don't forget to remove the cruft later.
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.
Let's remove the release note, to keep it very much seeming internal.
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.
Will remove the release note. As to other functions, I'll remove the remove_op_node
and remove_all_ops_named
functions. My question is, what does "all the functions that exist that we don't immediately have a use for" mean? There seems to be a difference of opinion on what that includes.
These `quantum_successors` etc have no meaning in `DAGDependency` (and V2) since the edges don't carry the data that the `DAGCircuit` functions check for; `DAGDependency` is in part a deliberate erasure of this data.
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 for this Ed. I've removed the quantum/classical successor/predecessor methods because they were checking edge data that completely doesn't exist on DAGDependency
. I'm not super convinced that replace_block_with_op
is completely right yet, but I think that might be the same as in DAGDependency
and I don't want to erase it out from under Sasha.
Just to highlight to anyone else reading this, again: all this is deliberately not public, and it's all so not public (undocumented, and has explicitly private names) that we're not even marking it with the "experimental" API. This is for near-term experimentation and there's a good chance it may even be replaced entirely with a pure-Rust implementation before being made public.
Summary
Add new DAGDependencyV2 and Converters
Details and comments
This PR is a replacement for #11310. In that PR, the new
DAGDependencyV2
was created and the associated template matching code was modified to use it. This PR only creates the dag and the 2 converters required. The following changes were made,direct_successors
is nowsuccessors
andsuccessors
is nowdescendants
. Similarly forpredecessors
andancestors
. This mirrors the usage in rustworkx.Doing some local tests on an 8000 gate circuit and converting the circuit to a dag dependency, V2 used ~300MB of memory and V1 used ~2.5GB of memory.
The next phase will be to refactor the template matching code to use V2 and to optimize the matching.
edit (@jakelishman):
Warning
Just to highlight to anyone else reading this, again: all this is deliberately not public, and it's all so not public (undocumented, and has explicitly private names) that we're not even marking it with the "experimental" API. This is for near-term experimentation and there's a good chance it may even be replaced entirely with a pure-Rust implementation before being made public.