-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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 fast-path construction to DAGCircuit
methods
#10753
Conversation
This optimises the constructor functions `DAGCircuit.apply_operation_back`, `apply_operation_front` and `compose` by giving callers a way to skip the validity checking when passing known-good values. In most cases when we're operating on the DAG, we are certain that we're already upholding the invariants of the data structure by nature of having a `copy_empty_like` or similar, and the user should not pay a runtime price for what we should catch during testing.
One or more of the the following people are requested to review this:
|
def _add_op_node(self, op, qargs, cargs): | ||
"""Add a new operation node to the graph and assign properties. | ||
|
||
Args: | ||
op (qiskit.circuit.Operation): the operation associated with the DAG node | ||
qargs (list[Qubit]): list of quantum wires to attach to. | ||
cargs (list[Clbit]): list of classical wires to attach to. | ||
Returns: | ||
int: The integer node index for the new op node on the DAG | ||
""" | ||
# Add a new operation node to the graph | ||
new_node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self) | ||
node_index = self._multi_graph.add_node(new_node) | ||
new_node._node_id = node_index | ||
self._increment_op(op) | ||
return node_index | ||
|
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 a fairly simple function used in only two places which already necessarily had a lot of duplication, and throws away information that the outer contexts need (the DAGOpNode
- we previously had to retrieve it from _multi_graph
again), so I just inlined it into its call sites.
It would be useful to design a canonical way to choose whether to do expensive safety checks. The simplest "design" is using the same name for the option to elide checks. For example, we have
qiskit/qiskit/circuit/library/generalized_gates/linear_function.py Lines 78 to 79 in 61e5bde
I'm concerned about adding more variety to these options, making the barrier to cleaning it up in the future even larger. Maybe this PR could use the design we want (again, maybe simplest is best and the design is just a naming convention). And a followup PR could make this uniform to the extent possible across qiskit. |
Fwiw we also have |
Short story. I am ok with using longer: For the sake of brevity, I didn't mention concerns about: corner cases, internal vs external api, conceptual and motivational differences in eliding checks, interactions between checks. Of course that all becomes immediately important. I won't suggest unifying I think standardizing on something like |
That sounds like an interesting topic to pursue potentially under the guise of defining a "naming convention" guide for Qiskit. Perhaps we could move that into a separate issue to track? edit: just pushed a commit to fix the merge conflict. |
In qiskit/qiskit/dagcircuit/dagcircuit.py Lines 572 to 574 in 7252db3
have elif isinstance(operation, SwitchCaseOp) ? without elif it's harder to read if you don't know the code. I mean it's not obvious that these are not mutually exclusive branches.
|
return getattr(operation, "condition", None) is not None or isinstance( | ||
operation, SwitchCaseOp | ||
) | ||
|
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 optimization is in addition to the optimization discussed in the opening comment, right? Did you benchmark this optimization? It's clear that set(self._bits_in_operation(op)).union(cargs)
will be slower than just cargs
even when the generator is empty. But I would not be surprised if this is not measurable in benchmarks.
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.
Yeah, looks like I didn't mention this little bit in the commit message. Removing the _may_have_bits
check and always eagerly calling _bits_in_operation
caused the microbenchmark at the top to go from 27.7(7)ms with the current PR to 29.3(9)ms when timed just 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.
That's worth the trouble although it adds a bit of clutter
I think that this qiskit/qiskit/dagcircuit/dagcircuit.py Lines 1194 to 1197 in 7252db3
should use |
The questions above are all I have. The main part of the PR, i.e. introducing a |
I think I wrote that |
I've done the |
(you need to leave an approving review to enable merge - the merge queue handles bringing the PR up-to-date with |
Huh. I tried to do that. Clicked on the green approve button.... |
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.
Looks good.
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.
Looks good.
Summary
This optimises the constructor functions
DAGCircuit.apply_operation_back
,apply_operation_front
andcompose
by giving callers a way to skip the validity checking when passing known-good values. In most cases when we're operating on the DAG, we are certain that we're already upholding the invariants of the data structure by nature of having acopy_empty_like
or similar, and the user should not pay a runtime price for what we should catch during testing.Details and comments
I ran a benchmark suite and
asv
didn't pop up anything statistically significant in changes but quite a bit of stuff saw ~5% improvements. I'm just running benchmarks on a laptop in other use, and our benchmark suite is quite outdated now.In a more targeted microbenchmark with setup
the call
circuit-to_dag(qc, copy_operations=False)
went from 34.3(7)ms to 28.6(7)ms, so a ~15% speedup. That's indicative of what's happening when we're doing things like rebuilding the DAG after Sabre or duringApplyLayout
.