Skip to content
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

Merged
merged 5 commits into from
Sep 7, 2023

Conversation

jakelishman
Copy link
Member

Summary

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.

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

from qiskit.circuit.library import QuantumVolume
from qiskit.converters import circuit_to_dag
qc = QuantumVolume(100, seed=0).decompose()

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 during ApplyLayout.

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.
@jakelishman jakelishman added performance Changelog: New Feature Include in the "Added" section of the changelog mod: transpiler Issues and PRs related to Transpiler labels Aug 31, 2023
@jakelishman jakelishman added this to the 0.45.0 milestone Aug 31, 2023
@qiskit-bot
Copy link
Collaborator

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

  • @ElePT
  • @Qiskit/terra-core
  • @ikkoham
  • @woodsp-ibm

Comment on lines -595 to -611
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

Copy link
Member Author

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.

@jlapeyre
Copy link
Contributor

jlapeyre commented Sep 6, 2023

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

validate_input: if True, performs more expensive input validation checks,
such as checking that a given n x n matrix is invertible.

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.

@jakelishman
Copy link
Member Author

Fwiw we also have validate, require_cp, require_tp in quantum_info, and cycle_check within DAGCircuit, which are all types of input validation as well. I don't mind especially what we go for (I err on the shorter side when the meaning is unambiguous) - which do you prefer so I can update the PR?

@jlapeyre
Copy link
Contributor

jlapeyre commented Sep 6, 2023

Short story. I am ok with using check for now. It may not be the best name, but the marginal tech debt of a name that must be changed is small. Trying to figure this out now would derail this PR.

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 require_tp and require_cp into check, as they were already split (#3660) from require_cptp and given different defaults. No reason was given, but I'm sure there was one. I grepped all of the qiskit repos and only find these options in a few tests in qiskit-experiments, and always set to False.

I think standardizing on something like check_some_invariant, and check_all might be a good first step. I.e. in this case check becomes check_all and require_tp becomes check_tp. Some of these currently log a warning others error. That should probably be worked into the naming.

@jakelishman
Copy link
Member Author

jakelishman commented Sep 6, 2023

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.

@jlapeyre
Copy link
Contributor

jlapeyre commented Sep 7, 2023

In bits_in_operation, shouldn't this

if (condition := getattr(operation, "condition", None)) is not None:
yield from condition_resources(condition).clbits
if isinstance(operation, SwitchCaseOp):

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
)

Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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

@jlapeyre
Copy link
Contributor

jlapeyre commented Sep 7, 2023

I think that this

if not propagate_condition:
node_wire_order += [
bit for bit in self._bits_in_operation(node.op) if bit not in node_cargs
]

should use _operation_may_have_bits as well.

@jlapeyre
Copy link
Contributor

jlapeyre commented Sep 7, 2023

The questions above are all I have. The main part of the PR, i.e. introducing a check option looks good to me.

@jakelishman
Copy link
Member Author

I think I wrote that SwitchCaseOp if/if as a defensive thing - the two branches are mutually exclusive right now, but to me making it if/else felt like making more assumptions than were needed for the function.

@jakelishman
Copy link
Member Author

I've done the substitute_node_with_dag thing in 14b215c. In general I didn't look to optimise any of the other DAGCircuit functions to keep the scope of the PR targetted and because DAG reconstruction from SabreSwap is one place that bottlenecks transpile which only uses apply_operation_back at the moment.

@jakelishman
Copy link
Member Author

(you need to leave an approving review to enable merge - the merge queue handles bringing the PR up-to-date with main, so that's not necessary, but the code-owner approval is)

@jlapeyre
Copy link
Contributor

jlapeyre commented Sep 7, 2023

Huh. I tried to do that. Clicked on the green approve button....
I'll try again.
Maybe the first click only changed input focus ?

Copy link
Contributor

@jlapeyre jlapeyre left a comment

Choose a reason for hiding this comment

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

Looks good.

Copy link
Contributor

@jlapeyre jlapeyre left a comment

Choose a reason for hiding this comment

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

Looks good.

@jlapeyre jlapeyre added this pull request to the merge queue Sep 7, 2023
Merged via the queue into Qiskit:main with commit 3059193 Sep 7, 2023
13 checks passed
@jakelishman jakelishman deleted the dag-fast-path branch February 7, 2024 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: New Feature Include in the "Added" section of the changelog mod: transpiler Issues and PRs related to Transpiler performance
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants