Skip to content

Fix performance of Sabre rust<->Python boundary #10652

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 4 commits into from
Aug 17, 2023

Conversation

mtreinish
Copy link
Member

Summary

In #10366 the SabreLayout and SabreSwap passes were refactored to support nested control flow blocks. As part of that refactor a new struct SabreResult was created to store the nested results for each block. This new class however resulted in PyO3 cloning the full swap map on every access. Since we have at least 1 access per gate in the circuit (and another one for each swap) this extra clone() adds a lot of extra overhead for deep circuits. In extreme cases this regression could be quite extreme. To address this the result format is changed to be a tuple (as it was originally), while this is less ergonomic the advantage it provides is that for nested objects it moves the rust object to the pyo3 interface so we avoid a copy as Python owns the object on the return. However, for control flow blocks we're still using the SabreResult class as it simplifies the internal implementation (which is why #10366 introduced the struct). The potential impact of this is mitigated by switching to only a single clone per control flow block, by only accessing the SabreResult object's attribute on each recursive call. However, if it is an issue in the future we can work on flattening the nested structure before returning it to python to avoid any clones.

Details and comments

Fixes #10650

In Qiskit#10366 the SabreLayout and SabreSwap passes were refactored to
support nested control flow blocks. As part of that refactor a new
struct SabreResult was created to store the nested results for each
block. This new class however resulted in PyO3 cloning the full swap map
on every access. Since we have at least 1 access per gate in the circuit
(and another one for each swap) this extra clone() adds a lot of extra
overhead for deep circuits. In extreme cases this regression could be
quite extreme. To address this the result format is changed to be a
tuple (as it was originally), while this is less ergonomic the advantage
it provides is that for nested objects it moves the rust object to the
pyo3 interface so we avoid a copy as Python owns the object on the
return. However, for control flow blocks we're still using the
SabreResult class as it simplifies the internal implementation (which is
why Qiskit#10366 introduced the struct). The potential impact of this is
mitigated by switching to only a single clone per control flow block,
by only accessing the SabreResult object's attribute on each recursive
call. However, if it is an issue in the future we can work on flattening
the nested structure before returning it to python to avoid any clones.

Fixes Qiskit#10650
@mtreinish mtreinish added this to the 0.25.1 milestone Aug 16, 2023
@mtreinish mtreinish requested a review from a team as a code owner August 16, 2023 22:09
@qiskit-bot
Copy link
Collaborator

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

@mtreinish
Copy link
Member Author

I reran the reproduce script from #10650 and with this PR applied SabreLayout took 18403.93066 ms which is inline with the 0.24.x and 0.23.x releases.

Eric-Arellano
Eric-Arellano previously approved these changes Aug 16, 2023
@mtreinish mtreinish added the Changelog: Bugfix Include in the "Fixed" section of the changelog label Aug 16, 2023
@coveralls
Copy link

coveralls commented Aug 16, 2023

Pull Request Test Coverage Report for Build 5889966418

  • 25 of 25 (100.0%) changed or added relevant lines in 3 files are covered.
  • 7 unchanged lines in 2 files lost coverage.
  • Overall coverage increased (+0.01%) to 87.268%

Files with Coverage Reduction New Missed Lines %
qiskit/extensions/quantum_initializer/squ.py 2 80.0%
crates/qasm2/src/lex.rs 5 91.14%
Totals Coverage Status
Change from base Build 5883117042: 0.01%
Covered Lines: 74361
Relevant Lines: 85210

💛 - Coveralls

Copy link
Contributor

@kevinhartman kevinhartman 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, overall!

Thanks for the fix. Just one small comment that may be worth addressing.

This commit simplifies the logic in the recursive call logic in
_apply_sabre_result to always use a tuple so we avoid an isinstance
check.

Co-authored-by: Kevin Hartman <kevin@hart.mn>
Copy link
Contributor

@kevinhartman kevinhartman left a comment

Choose a reason for hiding this comment

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

LGTM!

@kevinhartman kevinhartman enabled auto-merge August 17, 2023 14:09
@kevinhartman kevinhartman added this pull request to the merge queue Aug 17, 2023
Merged via the queue into Qiskit:main with commit e6c431e Aug 17, 2023
@mtreinish mtreinish added the stable backport potential The bug might be minimal and/or import enough to be port to stable label Aug 17, 2023
mergify bot pushed a commit that referenced this pull request Aug 17, 2023
* Fix performance of Sabre rust<->Python boundary

In #10366 the SabreLayout and SabreSwap passes were refactored to
support nested control flow blocks. As part of that refactor a new
struct SabreResult was created to store the nested results for each
block. This new class however resulted in PyO3 cloning the full swap map
on every access. Since we have at least 1 access per gate in the circuit
(and another one for each swap) this extra clone() adds a lot of extra
overhead for deep circuits. In extreme cases this regression could be
quite extreme. To address this the result format is changed to be a
tuple (as it was originally), while this is less ergonomic the advantage
it provides is that for nested objects it moves the rust object to the
pyo3 interface so we avoid a copy as Python owns the object on the
return. However, for control flow blocks we're still using the
SabreResult class as it simplifies the internal implementation (which is
why #10366 introduced the struct). The potential impact of this is
mitigated by switching to only a single clone per control flow block,
by only accessing the SabreResult object's attribute on each recursive
call. However, if it is an issue in the future we can work on flattening
the nested structure before returning it to python to avoid any clones.

Fixes #10650

* Simplify recursive call logic in _apply_sabre_result

This commit simplifies the logic in the recursive call logic in
_apply_sabre_result to always use a tuple so we avoid an isinstance
check.

Co-authored-by: Kevin Hartman <kevin@hart.mn>

---------

Co-authored-by: Kevin Hartman <kevin@hart.mn>
(cherry picked from commit e6c431e)
@mtreinish mtreinish deleted the fix-sabre-perf branch August 17, 2023 18:39
github-merge-queue bot pushed a commit that referenced this pull request Aug 17, 2023
* Fix performance of Sabre rust<->Python boundary

In #10366 the SabreLayout and SabreSwap passes were refactored to
support nested control flow blocks. As part of that refactor a new
struct SabreResult was created to store the nested results for each
block. This new class however resulted in PyO3 cloning the full swap map
on every access. Since we have at least 1 access per gate in the circuit
(and another one for each swap) this extra clone() adds a lot of extra
overhead for deep circuits. In extreme cases this regression could be
quite extreme. To address this the result format is changed to be a
tuple (as it was originally), while this is less ergonomic the advantage
it provides is that for nested objects it moves the rust object to the
pyo3 interface so we avoid a copy as Python owns the object on the
return. However, for control flow blocks we're still using the
SabreResult class as it simplifies the internal implementation (which is
why #10366 introduced the struct). The potential impact of this is
mitigated by switching to only a single clone per control flow block,
by only accessing the SabreResult object's attribute on each recursive
call. However, if it is an issue in the future we can work on flattening
the nested structure before returning it to python to avoid any clones.

Fixes #10650

* Simplify recursive call logic in _apply_sabre_result

This commit simplifies the logic in the recursive call logic in
_apply_sabre_result to always use a tuple so we avoid an isinstance
check.

Co-authored-by: Kevin Hartman <kevin@hart.mn>

---------

Co-authored-by: Kevin Hartman <kevin@hart.mn>
(cherry picked from commit e6c431e)

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
SameerD-phys pushed a commit to SameerD-phys/qiskit-terra that referenced this pull request Sep 7, 2023
* Fix performance of Sabre rust<->Python boundary

In Qiskit#10366 the SabreLayout and SabreSwap passes were refactored to
support nested control flow blocks. As part of that refactor a new
struct SabreResult was created to store the nested results for each
block. This new class however resulted in PyO3 cloning the full swap map
on every access. Since we have at least 1 access per gate in the circuit
(and another one for each swap) this extra clone() adds a lot of extra
overhead for deep circuits. In extreme cases this regression could be
quite extreme. To address this the result format is changed to be a
tuple (as it was originally), while this is less ergonomic the advantage
it provides is that for nested objects it moves the rust object to the
pyo3 interface so we avoid a copy as Python owns the object on the
return. However, for control flow blocks we're still using the
SabreResult class as it simplifies the internal implementation (which is
why Qiskit#10366 introduced the struct). The potential impact of this is
mitigated by switching to only a single clone per control flow block,
by only accessing the SabreResult object's attribute on each recursive
call. However, if it is an issue in the future we can work on flattening
the nested structure before returning it to python to avoid any clones.

Fixes Qiskit#10650

* Simplify recursive call logic in _apply_sabre_result

This commit simplifies the logic in the recursive call logic in
_apply_sabre_result to always use a tuple so we avoid an isinstance
check.

Co-authored-by: Kevin Hartman <kevin@hart.mn>

---------

Co-authored-by: Kevin Hartman <kevin@hart.mn>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: Bugfix Include in the "Fixed" section of the changelog priority: high stable backport potential The bug might be minimal and/or import enough to be port to stable
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Runtime Performance Regression in SabreLayout
5 participants