Skip to content

Fix MergeAdjacentBarriers to scale with number of barriers #10900

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

Merged
merged 6 commits into from
Sep 28, 2023

Conversation

ElePT
Copy link
Contributor

@ElePT ElePT commented Sep 26, 2023

Summary

Proposed fix for #10860. I found that the easiest path would be to return the nodes to be replaced in the _collect_potential_merges method, which I also modified to match the documented return type, as it can now return barriers directly (it was returning the barrier qubits before).

Details and comments

I tried profiling the before/after with this circuit (modifying the number of hadamards):

qr = QuantumRegister(4, "q")

circuit = QuantumCircuit(qr)
for i in range(500000):
    circuit.h(0)
circuit.barrier([qr[0], qr[1]])
circuit.barrier([qr[1], qr[2]])
circuit.barrier(qr[3])

And the new version is faster in general but still scales linearly with the number of gates, I guess this is still somewhat expected?

Before Fix:

5 Hadamards Total: 0.007848978042602539
name                                  ncall  tsub      ttot      tavg
..rs.py:60 MergeAdjacentBarriers.run  1      0.000119  0.002610  0.002610

5000 H  Total: 1.225039005279541
name                                  ncall  tsub      ttot      tavg
..rs.py:60 MergeAdjacentBarriers.run  1      0.035354  0.629396  0.629396

500 000 H  Total: 119.47961711883545
name                                  ncall  tsub      ttot      tavg
..rs.py:60 MergeAdjacentBarriers.run  1      3.511733  62.77685  62.77685

——————————————————
After Fix:

5 Hadamards  Total: 0.007379055023193359
name                                  ncall  tsub      ttot      tavg
..rs.py:60 MergeAdjacentBarriers.run  1      0.000054  0.001563  0.001563

5000 H  Total: 0.6967248916625977
name                                  ncall  tsub      ttot      tavg
..rs.py:60 MergeAdjacentBarriers.run  1      0.000074  0.105973  0.105973

500 000 H  Total: 67.28817319869995
name                                  ncall  tsub      ttot      tavg
..rs.py:60 MergeAdjacentBarriers.run  1      0.004816  10.76225  10.76225

@ElePT ElePT requested a review from a team as a code owner September 26, 2023 10:01
@qiskit-bot
Copy link
Collaborator

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

  • @Qiskit/terra-core

Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

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

The changes in here look sensible to me, one copy aside.

Fundamentally, unless we can "jump" to only the barriers, I don't see how we could implement this as linear in the number of barriers rather than the number of all operations, because we have to inspect each operation to find the barriers. The current setup is the construction-space improvement we could have expected, though.

There's a larger-scale change we could consider making in a more wide-ranging PR: add an analysis pass that adds name: set[DAGOpNode] data to the property set for given names, and then update relevant passes to say whether they preserve that information or not. That doesn't solve the problem of the analysis step being linear, but it's possible that it could ameliorate the cost if we know we might need to run the same sort of transformation pass multiple times. It's not clear to me that that would be helpful in this case, but maybe it's worth thinking about (in the context of an entire transpilation pipeline).

@jakelishman jakelishman added performance Changelog: None Do not include in changelog mod: transpiler Issues and PRs related to Transpiler labels Sep 26, 2023
@jakelishman jakelishman added this to the 0.45.0 milestone Sep 26, 2023
@jakelishman jakelishman linked an issue Sep 26, 2023 that may be closed by this pull request
@coveralls
Copy link

coveralls commented Sep 26, 2023

Pull Request Test Coverage Report for Build 6312989890

  • 7 of 7 (100.0%) changed or added relevant lines in 1 file are covered.
  • 4 unchanged lines in 1 file lost coverage.
  • Overall coverage increased (+0.01%) to 87.026%

Files with Coverage Reduction New Missed Lines %
crates/qasm2/src/lex.rs 4 92.17%
Totals Coverage Status
Change from base Build 6301697315: 0.01%
Covered Lines: 74066
Relevant Lines: 85108

💛 - Coveralls

@mtreinish
Copy link
Member

Fundamentally, unless we can "jump" to only the barriers, I don't see how we could implement this as linear in the number of barriers rather than the number of all operations, because we have to inspect each operation to find the barriers. The current setup is the construction-space improvement we could have expected, though.

This is true, I'm also thinking shorter term we might be able to rely on filter_nodes in the next rustworkx release (Qiskit/rustworkx#886) to more quickly get the list of barriers even with a linear search. Although I'm not clear if the repeated python function call overhead would defeat any potential gains from a faster iteration there, we'll have to test it.

@jakelishman
Copy link
Member

filter_nodes would still linear in total node count, though - it's just a (potential) reduction in the constant-time factors because we spend less time in Python space looking at the nodes, right?

@mtreinish
Copy link
Member

Yeah exactly, it's still linear time overhead just hopefully a bit faster by doing more work in rust.

@ElePT
Copy link
Contributor Author

ElePT commented Sep 26, 2023

I can give filter_nodes a try and see if there's a noticeable improvement, but it probably makes sense to leave that to a follow-up PR.

jakelishman
jakelishman previously approved these changes Sep 26, 2023
Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

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

This looks good to me now. I was concerned at first about the determinism of the output, since we appear to be removing the sorting, but DAGCircuit.replace_block_with_op is required to impose a canonical order on the qargs/cargs (which it does via sorting using wire_pos_map) in order to have general-case non-symmetric replacements work. We also have an explicit test for non-determinism of the pass.

@jakelishman jakelishman added this pull request to the merge queue Sep 26, 2023
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.

This LGTM, just one small inline performance tweak we can make to slightly improve it further.

@jakelishman jakelishman removed this pull request from the merge queue due to a manual request Sep 26, 2023
Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
@alexanderivrii
Copy link
Member

This is somewhat off-topic, but we could reimplement MergeAdjacentBarriers using CollectAndCollapse pass, where the collection function would look for nodes named "barrier" and the collapsing function would create a bigger barrier out of multiple small ones. The code would be something along the following lines (note: the argument split_blocks=True splits barriers into groups of barriers with pairwise disjoint supports, i.e. does not merge barriers over disjoint qubits).

from qiskit.circuit import QuantumCircuit
from qiskit.circuit.library import Barrier
from functools import partial

from qiskit.transpiler.passes import MergeAdjacentBarriers
from qiskit.transpiler.passes.optimization.collect_and_collapse import (
    CollectAndCollapse,
    collect_using_filter_function,
    collapse_to_operation,
)

class CollectBarriers(CollectAndCollapse):

    def __init__(self):
        collect_function = partial(
            collect_using_filter_function,
            filter_function=_is_barrier,
            split_blocks=True,  # important: avoid merging non-intersecting barriers
            min_block_size=2,
        )
        collapse_function = partial(
            collapse_to_operation, collapse_function=_collapse_to_barrier
        )
        super().__init__(
            collect_function=collect_function,
            collapse_function=collapse_function,
        )

def _is_barrier(node):
    return node.op.name == "barrier"

def _collapse_to_barrier(circuit):
    """
    We have a quantum circuit consisting only of barriers.
    We want to merge these into a single barrier.
    We only need to know how many qubits there are.
    """
    qubits = set()
    for node in circuit:
        for q in node.qubits:
            qubits.add(q)
    return Barrier(len(qubits))

Under the hood this probably does a bit more work than MergeAdjacentBarriers, so I don't think this actually improves performance (would be fun to check this though).

Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

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

Sasha's point is interesting to me, and as we scale up, I think we might want to start trying to consolidate more of our passes into general utilities than having specific individual passes, if not only to lower the maintenance burden. This pass shouldn't be performance critical, so a minor regression on speed in favour of less to maintain sounds like it could make sense to me.

That said, this PR is good to go following Matt's last review and Elena's changes, so let's merge and see if we want to pull on the other thread later.

@jakelishman jakelishman added Changelog: New Feature Include in the "Added" section of the changelog and removed Changelog: None Do not include in changelog labels Sep 28, 2023
@jakelishman jakelishman added this pull request to the merge queue Sep 28, 2023
Merged via the queue into Qiskit:main with commit e3155c6 Sep 28, 2023
rupeshknn pushed a commit to rupeshknn/qiskit that referenced this pull request Oct 9, 2023
…10900)

* Modify scaling

* Remove copy, add reno

* Fix lint

* Fix reno

* Update qiskit/transpiler/passes/utils/merge_adjacent_barriers.py

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>

* Fix black

---------

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
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.

MergeAdjacentBarriers scales with instruction count
6 participants