Skip to content

Conversation

enavarro51
Copy link
Contributor

@enavarro51 enavarro51 commented Nov 23, 2023

Summary

Creates a new DAGDependencyV2

Details and comments

Previously DAGDependency was only used for the template matching transpiler pass. Additional uses are planned and there were several problems with the previous version.

  • DAGDependency used DAGDepNode which stored a lot of information at the node level in attributes such as successors, qindices, and matchedwith that were only meant for use for template matching. These created excessive memory usage and did not take advantage of the rustworkx graph functions at runtime.
  • The format and layout of the methods in DAGDependency had diverged significantly from DAGCircuit, even though they share a lot of functionality in common.
  • The template matching code is slow and needs refactoring and a change to DAGDependency is part of that refactoring process.

The significant changes made are

  • There is a new DAGDependencyV2 which no longer uses DAGDepNode at all, but instead uses the existing DAGOpNode without modification. The makes for a cleaner and much reduced memory profile for the nodes.
  • Additional methods from DAGCircuit, such as find_bit were added to the DAGDependencyV2 and other methods were modified to more closely match the 2 dags.
  • The information stored previously in the node attributes is now accessed directly in the template matching code through calls to the rustworkx functions.
  • The template matching code has been modified to accomodate all of the above changes. This should result in a reduced memory profile, but at this stage may only result in a minor speed up.
  • A terminology change was made in DAGDependencyV2 and the template code. direct_successors is now successors and successors is now descendants. Similarly for predecessors and ancestors. This mirrors the usage in rustworkx.

For the future, a refactor of the template matching code is in order. One area especially is that the code mixes usage of the integer node id returned from rustworkx calls and the node object id, which results in many instances of having to map one to the other.

@enavarro51 enavarro51 requested review from nonhermitian and a team as code owners November 23, 2023 17:54
@qiskit-bot
Copy link
Collaborator

One or more of the the following people are requested to review this:

@coveralls
Copy link

coveralls commented Nov 23, 2023

Pull Request Test Coverage Report for Build 7403861007

Warning: This coverage report may be inaccurate.

We've detected an issue with your CI configuration that might affect the accuracy of this pull request's coverage report.
To ensure accuracy in future PRs, please see these guidelines.
A quick fix for this PR: rebase it; your next report should be accurate.

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage decreased (-0.07%) to 87.439%

Totals Coverage Status
Change from base Build 7302309139: -0.07%
Covered Lines: 59485
Relevant Lines: 68030

💛 - Coveralls

@enavarro51 enavarro51 changed the title Create DAGDependencyV2 [WIP] Create DAGDependencyV2 Nov 23, 2023
Copy link
Member

@alexanderivrii alexanderivrii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far I only took a superficial look at the PR and I am definitely excited about the changes: DAGDependencyV2 is on the road to become cleaner, more efficient and more functional than DAGDependency, and the classes DAGCircuit and DAGDependencyV2 start looking closer to each other. I have left a few minor in-place comments and questions, but this is not a complete review.

What significantly confuses me (and I mean qiskit code in general, not particularly this PR) is the divergence of similar methods between QuantumCircuit, DAGCircuit and DAGDependency. As the simplest example, the size function in QuantumCircuit takes the argument filter_function, the size function in DAGCircuit takes the argument recurse, and the size function in DAGDependency does not take either (the same is true for DAGDependencyV2). Not only the size function should be used differently for different representations of the quantum circuit, it's likely to return different results depending on the representation. And this is just the tip of the iceberg. It would probably be nice to have an in-depth discussion about these 3 APIs and try to find the best solution.

A related point is that the API for DAGDependency and (as of right now) DAGDependencyV2 is based on integer node ids assigned by rustworkx graphs, while the API for DAGCircuit is based on DAGNodes. It would probably be good to have the DAGDependencyV2 API also in terms of nodes and not rustworkx ids.

Comment on lines +42 to +43

dagdependency.calibrations = circuit.calibrations
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems an oversight in the existing code as well, but shouldn't we also update the global_phase (as is done in dag_to_dagdependency_v2 code)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This goes to your work to coordinate the 3 classes, which involves add_calibration as well. Do these things belong in all 3 classes? And if so, can they be implemented the same way?

Comment on lines 102 to 104
self.dag.get_node(pred_id)
for pred_id in self.dag.direct_successors(node.node_id)
for pred_id in self.dag.get_successors(self.dag.node_map[node])
]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not yet looked at the internals of DAGDependencyV2, but getting the direct predecessors still significantly differs between DAGCircuit and DAGDependencyV2.

Using DAGCircuit:

[pred for pred in self.dag.predecessors(node) if isinstance(pred, DAGOpNode)]

Using DAGDependencyV2:

[self.dag.get_node(pred_id) for pred_id in self.dag.get_successors(self.dag.node_map[node])]

I am wondering if we could bring the two expressions closer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See _node_id reply below.

Comment on lines +3 to +4
# (C) Copyright IBM 2020.
#
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha! This code was written at least 3 years ago. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually almost 4 years ago, mostly by Romain Moyard, an author of the template matching paper.

Comment on lines +167 to +168
def add_calibration(self, gate, qubits, schedule, params=None):
"""Register a low-level, custom pulse definition for the given gate.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is adapted from DAGCircuit, correct? A naive question: why should DAGDependencyV2 support this method?

Comment on lines +227 to +228
def size(self):
"""Returns the number of gates in the circuit"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. The size function in DAGCircuit has an additional argument recurse, while the size function in QuantumCircuit has an additional argument filter_function. Do we want to extend this function here and how?

Comment on lines 119 to 120
# Map of node to node index
self.node_map = {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DAGNode/DAGOpNode have the field _node_id, which is what is used by DAGCircuit to map from a node to its rustworkx id. Can we use a similar strategy here instead of supporting node_map?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somehow I completely missed that _node_id was used throughout DAGCircuit. I thought it was only used minimally, so I didn't use it in DAGDependencyV2, but should have. Since it would also need to be used in the template matching code and the underscore implies some level of privacy, we'd probably want to add an @property for node_id to the DAGDependencyV2 code.

Comment on lines 507 to 508
def get_in_edges(self, node_id):
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A general comment/question about API. In this function and others, node_id is the "internal" integer id assigned by rustworkx graph, correct? Would it make sense to change the API to refer to DAGNodes (or DAGOpNodes) but not to these rustworkx ids? Is this how we already do this in DAGCircuit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I kept it this way currently is that is the way the template matching code uses it. I should probably do a second run through all this code to convert to using node.node_id as discussed above. This should simplify a lot of the code.

Comment on lines 604 to 605
for node in self.get_nodes():
dag._multi_graph.add_node(node.copy())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When copying dag dependencies, do we always need to copy the actual nodes too? As far as I can see, DAGCircuit does not have the function copy (and maybe it should). Ok, I guess we had to copy the nodes in the old template matching code where a lot of additional information was stored on nodes (and likely this information had to change in different parts of the algorithm), but maybe now (with this additional information stripped away) we can avoid this additional copying?

Update: looking at template matching code, I see that you are ahead of this and are no longer copying DAGDependencies. Do we still need this copy function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually removed all the dag_dep copying in the template code, so the copying here is only if a user wants to use it. We definitely do not have to copy the node here.

Comment on lines +82 to +85
self.successorstovisit = {}
self.isblocked = {}
self.matchedwith = {}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! So these are now used instead of the information that used to be stored on DAGDepNodes. This is much much cleaner.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only leftover that is not purely in the template code is qindices and cindices which are computed and stored at the DAGDependencyV2 level. It's done in the add_op_node method. It's just a list of indices for the qargs and cargs for a node. It seemed easiest to store this at this level, rather than having to rebuild the list each time it's needed for a node in the template code. This can probably be optimized with a template code rework.

@enavarro51
Copy link
Contributor Author

@alexanderivrii I did another pass through DAGDependencyV2 and the method calls now are a much closer match to the calls in DAGCircuit. See ddafcde.

I added 4 new methods - successor_indices, predecessor_indices, descendant_indices, and ancestor_indices which are needed for the template matching code since it uses the indices primarily. In the future, if that code is refactored to use node objects instead of indices, these 4 can be removed from DAGDependencyV2.

I also removed several edge-related functions, such as add_edge and get_edges since these were not in DAGCircuit and I assume to maintain the integrity of the commute relations, users should not be doing direct edge manipulation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants