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

Allow DAG-node substitution without overriding conditions #8615

Conversation

jakelishman
Copy link
Member

Summary

Currently, DAGCircuit.substitute_node_with_dag forcibly copies any
condition on the node onto all the operations in the replacement DAG.
This style was because the method was originally used by the
basis-translation passes (Decompose, BasisTranslator, etc), which
did not want to have to worry about the condition.

This model of the condition being a "addition" to a node is now too
restrictive with the move to more dynamic circuits; one cannot
replace an (old-style) conditioned node with a custom instruction or an
IfElseBlock that implements the condition internally, for example by
taking the condition bits in the clbits field and using the condition
on only a subset of the definition nodes.

This commit does not add any such option to DAGCircuit.substitute_node;
a new interface for wire-reordering in the arguments did not appear
quite as clear to me, and this commit can stand alone.

Details and comments

I'm making the PR because I need it for #8601, but I think it's valuable as a realignment of the importance of conditionals. When it was originally added, I believe it was considered as just an addition to a node, rather than an integral part of the operation being performed. This isn't the case in more dynamic circuits, and we need our low-level tooling to have ways of representing that.

I'm open to making another PR to add similar behaviour to DAGCircuit.substitute_node as well - that would actually be more natural for #8601, but it's not as clear to me what the additions to the interface for that would be.

cc: @kdk.

Currently, `DAGCircuit.substitute_node_with_dag` forcibly copies any
condition on the `node` onto all the operations in the replacement DAG.
This style was because the method was originally used by the
basis-translation passes (`Decompose`, `BasisTranslator`, etc), which
did not want to have to worry about the condition.

This model of the condition being a "addition" to a node is now too
restrictive with the move to more dynamic circuits; one cannot
replace an (old-style) conditioned node with a custom instruction or an
`IfElseBlock` that implements the condition internally, for example by
taking the condition bits in the `clbits` field and using the condition
on only a subset of the definition nodes.

This commit does not add any such option to `DAGCircuit.substitute_node`;
a new interface for wire-reordering in the arguments did not appear
quite as clear to me, and this commit can stand alone.
@jakelishman jakelishman added the Changelog: New Feature Include in the "Added" section of the changelog label Aug 25, 2022
@jakelishman jakelishman added this to the 0.22 milestone Aug 25, 2022
@jakelishman jakelishman requested a review from a team as a code owner August 25, 2022 14:46
@qiskit-bot
Copy link
Collaborator

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:

  • @Qiskit/terra-core

@coveralls
Copy link

coveralls commented Aug 25, 2022

Pull Request Test Coverage Report for Build 3062224283

  • 71 of 77 (92.21%) changed or added relevant lines in 1 file are covered.
  • 13 unchanged lines in 2 files lost coverage.
  • Overall coverage increased (+0.1%) to 84.464%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/dagcircuit/dagcircuit.py 71 77 92.21%
Files with Coverage Reduction New Missed Lines %
qiskit/extensions/quantum_initializer/squ.py 2 79.78%
qiskit/tools/parallel.py 11 67.69%
Totals Coverage Status
Change from base Build 3057765421: 0.1%
Covered Lines: 59437
Relevant Lines: 70370

💛 - Coveralls

@jakelishman
Copy link
Member Author

This commit also speeds up the process by being a bit more CPU efficient, and eliding some register checks with new methods for handling them that are valid now that registers are aliases of qubit sequences, not owners of the qubits. I ran the MultipleBasisPassBenchmarks suite as-is and got:

Benchmarks that have improved:

       before           after         ratio
     [c008008b]       [711ca1a0]
     <main>           <conditional-DAGCircuit.substitute_node_with_dag>
-      1.26±0.04s       1.11±0.02s     0.88  passes.MultipleBasisPassBenchmarks.time_basis_translator(14, 1024, ['rx', 'ry', 'rz', 'r', 'rxx', 'id'])
-      1.72±0.01s       1.56±0.01s     0.91  passes.MultipleBasisPassBenchmarks.time_basis_translator(20, 1024, ['rx', 'ry', 'rz', 'r', 'rxx', 'id'])

Benchmarks that have stayed the same:

       before           after         ratio
     [c008008b]       [711ca1a0]
     <main>           <conditional-DAGCircuit.substitute_node_with_dag>
         970±10ms         925±40ms     0.95  passes.MultipleBasisPassBenchmarks.time_basis_translator(14, 1024, ['rz', 'x', 'sx', 'cx', 'id'])
         813±10ms          751±4ms     0.92  passes.MultipleBasisPassBenchmarks.time_basis_translator(14, 1024, ['u', 'cx', 'id'])
       1.35±0.01s       1.25±0.03s     0.92  passes.MultipleBasisPassBenchmarks.time_basis_translator(20, 1024, ['rz', 'x', 'sx', 'cx', 'id'])
       1.13±0.01s       1.06±0.02s     0.94  passes.MultipleBasisPassBenchmarks.time_basis_translator(20, 1024, ['u', 'cx', 'id'])
          429±4ms         413±20ms     0.96  passes.MultipleBasisPassBenchmarks.time_basis_translator(5, 1024, ['rx', 'ry', 'rz', 'r', 'rxx', 'id'])
          376±5ms          346±2ms     0.92  passes.MultipleBasisPassBenchmarks.time_basis_translator(5, 1024, ['rz', 'x', 'sx', 'cx', 'id'])
          309±3ms          284±5ms     0.92  passes.MultipleBasisPassBenchmarks.time_basis_translator(5, 1024, ['u', 'cx', 'id'])
       1.97±0.01s       1.91±0.02s     0.97  passes.MultipleBasisPassBenchmarks.time_optimize_1q_commutation(14, 1024, ['rx', 'ry', 'rz', 'r', 'rxx', 'id'])
       1.29±0.01s          1.25±0s     0.97  passes.MultipleBasisPassBenchmarks.time_optimize_1q_commutation(14, 1024, ['rz', 'x', 'sx', 'cx', 'id'])
       1.16±0.01s       1.13±0.01s     0.97  passes.MultipleBasisPassBenchmarks.time_optimize_1q_commutation(14, 1024, ['u', 'cx', 'id'])
       2.95±0.05s       2.78±0.04s     0.94  passes.MultipleBasisPassBenchmarks.time_optimize_1q_commutation(20, 1024, ['rx', 'ry', 'rz', 'r', 'rxx', 'id'])
       1.97±0.04s       1.96±0.04s     0.99  passes.MultipleBasisPassBenchmarks.time_optimize_1q_commutation(20, 1024, ['rz', 'x', 'sx', 'cx', 'id'])
       1.82±0.01s       1.74±0.04s     0.96  passes.MultipleBasisPassBenchmarks.time_optimize_1q_commutation(20, 1024, ['u', 'cx', 'id'])
         821±10ms          774±9ms     0.94  passes.MultipleBasisPassBenchmarks.time_optimize_1q_commutation(5, 1024, ['rx', 'ry', 'rz', 'r', 'rxx', 'id'])
          466±5ms          444±8ms     0.95  passes.MultipleBasisPassBenchmarks.time_optimize_1q_commutation(5, 1024, ['rz', 'x', 'sx', 'cx', 'id'])
          415±4ms          400±4ms     0.96  passes.MultipleBasisPassBenchmarks.time_optimize_1q_commutation(5, 1024, ['u', 'cx', 'id'])
         885±20ms          846±9ms     0.96  passes.MultipleBasisPassBenchmarks.time_optimize_1q_decompose(14, 1024, ['rx', 'ry', 'rz', 'r', 'rxx', 'id'])
          406±7ms         387±20ms     0.95  passes.MultipleBasisPassBenchmarks.time_optimize_1q_decompose(14, 1024, ['rz', 'x', 'sx', 'cx', 'id'])
          348±5ms          322±7ms     0.92  passes.MultipleBasisPassBenchmarks.time_optimize_1q_decompose(14, 1024, ['u', 'cx', 'id'])
       1.21±0.03s          1.15±0s     0.95  passes.MultipleBasisPassBenchmarks.time_optimize_1q_decompose(20, 1024, ['rx', 'ry', 'rz', 'r', 'rxx', 'id'])
          537±4ms          510±3ms     0.95  passes.MultipleBasisPassBenchmarks.time_optimize_1q_decompose(20, 1024, ['rz', 'x', 'sx', 'cx', 'id'])
         456±10ms          435±8ms     0.95  passes.MultipleBasisPassBenchmarks.time_optimize_1q_decompose(20, 1024, ['u', 'cx', 'id'])
          399±3ms          382±4ms     0.96  passes.MultipleBasisPassBenchmarks.time_optimize_1q_decompose(5, 1024, ['rx', 'ry', 'rz', 'r', 'rxx', 'id'])
          185±3ms          175±3ms     0.95  passes.MultipleBasisPassBenchmarks.time_optimize_1q_decompose(5, 1024, ['rz', 'x', 'sx', 'cx', 'id'])
          154±4ms          151±4ms     0.98  passes.MultipleBasisPassBenchmarks.time_optimize_1q_decompose(5, 1024, ['u', 'cx', 'id'])

That's about a 5% improvement across the board for transpiler passes that use the substitution, including the basis translator.

That benchmark doesn't include any conditionals, however. I modified the BasisTranslator benchmark to generate conditionals on the operations about 10% of the time, and the result is much more significant:

Benchmarks that have improved:

       before           after         ratio
     [c008008b]       [711ca1a0]
     <main>           <conditional-DAGCircuit.substitute_node_with_dag>
-      2.89±0.05s       1.49±0.03s     0.51  passes.MultipleBasisPassBenchmarks.time_basis_translator(14, 1024, ['rx', 'ry', 'rz', 'r', 'rxx', 'id'])
-      1.88±0.02s       1.13±0.02s     0.60  passes.MultipleBasisPassBenchmarks.time_basis_translator(14, 1024, ['rz', 'x', 'sx', 'cx', 'id'])
-      1.53±0.04s          903±5ms     0.59  passes.MultipleBasisPassBenchmarks.time_basis_translator(14, 1024, ['u', 'cx', 'id'])
-      5.71±0.03s       2.32±0.05s     0.41  passes.MultipleBasisPassBenchmarks.time_basis_translator(20, 1024, ['rx', 'ry', 'rz', 'r', 'rxx', 'id'])
-      3.46±0.04s       1.70±0.02s     0.49  passes.MultipleBasisPassBenchmarks.time_basis_translator(20, 1024, ['rz', 'x', 'sx', 'cx', 'id'])
-      2.68±0.03s       1.37±0.01s     0.51  passes.MultipleBasisPassBenchmarks.time_basis_translator(20, 1024, ['u', 'cx', 'id'])
-         677±7ms         475±10ms     0.70  passes.MultipleBasisPassBenchmarks.time_basis_translator(5, 1024, ['rx', 'ry', 'rz', 'r', 'rxx', 'id'])
-        511±10ms          388±4ms     0.76  passes.MultipleBasisPassBenchmarks.time_basis_translator(5, 1024, ['rz', 'x', 'sx', 'cx', 'id'])
-         422±2ms          334±7ms     0.79  passes.MultipleBasisPassBenchmarks.time_basis_translator(5, 1024, ['u', 'cx', 'id'])

This is because the condition-propagation code is done quite a bit more efficiently now. (The optimisation passes see less improvement when there are conditionals just because conditionals reduce the opportunities for substitute_node_with_dag to be called, as less can actually be optimised.)

All that is with no change to the basis translator - it's just a speedup of the internal DAGCircuit.substitute_node_with_dag method.

Copy link
Member

@mtreinish mtreinish left a comment

Choose a reason for hiding this comment

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

Overall this LGTM, just one comment inline about some exceptions. It's not necessarily a blocker but I think it's worth looking at some of them to see if they're actually useful while we're refactoring this.

@@ -1112,93 +1018,157 @@ def replace_block_with_op(self, node_block, op, wire_pos_map, cycle_check=True):
for nd in node_block:
self._decrement_op(nd.op)

def substitute_node_with_dag(self, node, input_dag, wires=None):
def _substitute_node_with_dag__wire_map(self, node, input_dag, wires, propagate_condition):
Copy link
Member

Choose a reason for hiding this comment

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

Was the __ intentional here to mark this as part of substitute_node_with_dag? I've not seen that convention before. Typically I would just nest these in substitute_node_with_dag although that's a lot of nested functions in this case as there are already a bunch.

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, I wasn't super happy with this or with them in the method - these are all helper functions, but they're long enough on their own, let alone with all the rest of the main body. I was looking for a way of marking them as explicitly helper functions - if you know of something better, I'm quite happy to change them. Unfortunately they logically mutate self using private methods (well, at least one of them does).

The other helpers in the function need to close over some values (in order to have the correct calling signature), so there's not much scope for moving them (not really any necessity).

Copy link
Member Author

Choose a reason for hiding this comment

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

I inlined two of the helper functions in 82b5e3a, and gave the map_condition one a more general name (which was probably appropriate anyway).

Comment on lines 1039 to 1046
if len(wires) != len(node_wire_order):
raise DAGCircuitError(f"expected {len(node_wire_order)} wires, got {len(wires)}")
wire_map = dict(zip(wires, node_wire_order))
if len(wire_map) != len(node_wire_order):
raise DAGCircuitError("duplicate wires")
for input_dag_wire, our_wire in wire_map.items():
if our_wire not in self.input_map:
raise DAGCircuitError(f"{our_wire} is not in this DAG")
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit concerned about these error messages. I realize they're the same as in the old code but since we're refactoring this now it's a good time to look at them again. When these get raised from a private function like this they're not really actionable as the context from the inner private function is a bit opaque to users. I'm wondering if we actually need these checks or we can get away with letting things fail naturally later or maybe putting a more detailed message would help.

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, I'm not a big fan, and they just suck up CPU mostly unnecessarily. I'm happy to remove them if you're on board too. That would also mean that this function would be short enough to inline back into the main body. If that's ok by you, I can probably refactor the other two helper methods into private methods outside of DAGCircuit (and we can potentially follow up to swap the other use of DAGCircuit._map_condition in compose over to the new form).

Copy link
Member

Choose a reason for hiding this comment

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

To the best of my knowledge, (at least some of) these date back to the initial DAGCircuit implementation.

I do think they have a use, even though they're not user-actionable errors, in that they guard against conditions which can lead to invalid or incorrect output circuits (where we drop gates, conditions, etc.). Ideally, these are conditions that shouldn't ever happen (but in practice still sometimes do). These serve as guardrails either when we're refactoring passes or when users are working with the DAGCircuit to say "you're off the well beaten path, there's a bug somewhere, etc."

I'd be in favor of removing them if we can find an alternate way of catching these cases (through restructuring APIs, adding more thorough DAG validation that can be used in testing, or otherwise), but against dropping them outright.

Copy link
Member Author

Choose a reason for hiding this comment

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

Well-written tests should already catch all these errors, I think, because passes should be comparing their outputs to expected circuits. Equally, though, that means that users shouldn't be seeing these exceptions being generated other than by their own code calling DGCircuit.substitute_node_with_dag directly, so the concern about them being unactionable to users might not be so important.

I've left the error-checking in place, but inlined the helper function back into substitute_node_with_dag and added a "bit mapping invalid:" prefix to the messages to hopefully make the tracebacks and errors a little clearer, in 110757f. Happy to do a bit more, if we'd prefer.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I guess it comes down to what are usage expectations are for the API. For internal terra passes I agree we're unlikely to ever see this because we have sufficient testing. But it is possible (and definitely more likely) that an out of tree pass author could use the wire mapping argument incorrectly. That was really what I was thinking about for user actionability because if a user installs a bad/buggy plugin and trips these conditions I'm not sure they'd know what to do with these exceptions so low in the stack. But I guess I just talked myself into keeping the checks for that use case and the extra context you added to the error message is probably sufficient.

Copy link
Member

@mtreinish mtreinish left a comment

Choose a reason for hiding this comment

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

LGTM, thanks for the update

@mergify mergify bot merged commit 02d2d6e into Qiskit:main Sep 16, 2022
@jakelishman jakelishman deleted the conditional-DAGCircuit.substitute_node_with_dag branch September 21, 2022 22:57
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
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants