Skip to content

Fix caching logic in SolovayKitaevSynthesis plugin #14304

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 2 commits into from
May 7, 2025

Conversation

alexanderivrii
Copy link
Member

@alexanderivrii alexanderivrii commented May 6, 2025

Summary

Fixes a problem described here: running the SolovayKitaevSynthesis unitary synthesis plugin repeatedly incorrectly reuses the set of basis gates specified on the very first run. For example,

circuit = QuantumCircuit(1)
circuit.rx(0.8, 0)
unitary = Operator(circuit).data
plugin = SolovayKitaevSynthesis()

# First, use the ["h", "t", "tdg"] set
out = plugin.run(unitary, basis_gates=["h", "t", "tdg"])
print(out.count_ops())
self.assertLessEqual(set(out.count_ops().keys()), {"h", "t", "tdg"})

# Second, use the ["h", "s"] set
out = plugin.run(unitary, basis_gates=["h", "s", "sdg"])
self.assertLessEqual(set(out.count_ops().keys()), {"h", "s", "sdg"})
print(out.count_ops())

synthesizes both times to the first specified basis set of ["h", "t", "tdg"]. Creating two different SolovayKitaevSynthesis plugins does not solve this, since the caching is done via a class-level attribute.

The fix (many thanks to @eliarbel for the suggestion!) consists of storing a dictionary from the basis gate sets to the correspoding basis approximations. UPDATE: after review and online discussion, we have decided not to store the dictionary but instead to regenerate the basic approximations whenever the set of basis gates or the depth used for generation changes from the previous run.

Needed for #14225.

@alexanderivrii alexanderivrii added the Changelog: Bugfix Include in the "Fixed" section of the changelog label May 6, 2025
@alexanderivrii alexanderivrii added this to the 2.1.0 milestone May 6, 2025
@alexanderivrii alexanderivrii requested a review from a team as a code owner May 6, 2025 09:03
@qiskit-bot
Copy link
Collaborator

One or more of the following people are relevant to this code:

  • @Qiskit/terra-core

@coveralls
Copy link

coveralls commented May 6, 2025

Pull Request Test Coverage Report for Build 14881523118

Details

  • 9 of 9 (100.0%) changed or added relevant lines in 1 file are covered.
  • 4 unchanged lines in 3 files lost coverage.
  • Overall coverage increased (+0.02%) to 87.847%

Files with Coverage Reduction New Missed Lines %
crates/accelerate/src/unitary_synthesis.rs 1 94.85%
crates/qasm2/src/expr.rs 1 94.23%
crates/qasm2/src/lex.rs 2 92.73%
Totals Coverage Status
Change from base Build 14848101443: 0.02%
Covered Lines: 74531
Relevant Lines: 84842

💛 - Coveralls

@Cryoris
Copy link
Contributor

Cryoris commented May 6, 2025

I think it would be safer to just invalidate the cache if the basis gate set changes. Storing multiple basic approximations could become quite memory-heavy and I'm not sure this is actually a use case we even need. Users could also explicitly load the basic approximations they care about, if they need to change in between them.

So, unless we have an explicit case for caching multiple decompositions, I think just invalidating the cache is safer and simpler 🙂

@mtreinish mtreinish added the stable backport potential The bug might be minimal and/or import enough to be port to stable label May 6, 2025
@mtreinish mtreinish modified the milestones: 2.1.0, 1.4.3 May 6, 2025
@alexanderivrii
Copy link
Member Author

As discussed offline, to be fully correct, we should also cache the depth (in addition to basis_gates) used to generate the basic approximations.

In practice the number of gate sequences we need to store to is surprisingly small: for instance, we only need to store 812 sequences for the default basis set [h, t, tdg] and the default depth 10, and we only need to store 6844 sequences for depth 16. This is because we do not store different gate sequences that lead to the same SO(3)-matrix. So we either need to cache the basic approximations both on basis_gates and depth, or to re-compute basic approximation if either of the two changes. I don't have a strong opinion which is better, but I am happy to go with the re-compute approach.

My only mild concern is that since _sk is a class-level attribute, we should never ever run two SolovayKitaevSynthesis plugins in parallel threads. @Cryoris, do you think this is a safe assumption? (On the other hand, this does not make the current behavior worse, and we can think more about this in the new amazing Rust code).

@alexanderivrii alexanderivrii requested a review from Cryoris May 7, 2025 10:52
@Cryoris
Copy link
Contributor

Cryoris commented May 7, 2025

Hm yes I guess that if you would have multiple processes with different depths or basis gates then that could be inefficient, since it would have to regenerate the sets a lot. I don't know if Python's multiprocessing has some kind of lock on used variables or if that could also result in wrong behavior... I don't think we can assume that no one will ever run in parallel, given that the pass manager tries running multiple processes (though here it'll use the same settings for the plugin).

Copy link
Contributor

@Cryoris Cryoris 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 fix!

@mtreinish
Copy link
Member

Hm yes I guess that if you would have multiple processes with different depths or basis gates then that could be inefficient, since it would have to regenerate the sets a lot. I don't know if Python's multiprocessing has some kind of lock on used variables or if that could also result in wrong behavior... I don't think we can assume that no one will ever run in parallel, given that the pass manager tries running multiple processes (though here it'll use the same settings for the plugin).

Multiprocessing doesn't have shared memory, that's kind of the point for Python and is how you get around GIL contention. Each process space has separate memory and when we do the parallel dispatch we make a copy of the pass manager (via pickle that's sent via the IPC to the subprocess). So the cache will be separate when running in parallel. There is potentially some reuse if you have more circuits than cpus the pass managers will potentially get reused per worker, but the execution is serialized in those cases.

@Cryoris Cryoris added this pull request to the merge queue May 7, 2025
Merged via the queue into Qiskit:main with commit 6d4c928 May 7, 2025
24 checks passed
mergify bot pushed a commit that referenced this pull request May 7, 2025
* extending SolovayKitaevSynthesis plugin caching logic to take the basis gates into account

* regenerating cached values following review comment

(cherry picked from commit 6d4c928)
@Cryoris
Copy link
Contributor

Cryoris commented May 7, 2025

@Mergifyio backport stable/1.4

Copy link
Contributor

mergify bot commented May 7, 2025

backport stable/1.4

✅ Backports have been created

mergify bot pushed a commit that referenced this pull request May 7, 2025
* extending SolovayKitaevSynthesis plugin caching logic to take the basis gates into account

* regenerating cached values following review comment

(cherry picked from commit 6d4c928)
github-merge-queue bot pushed a commit that referenced this pull request May 7, 2025
* extending SolovayKitaevSynthesis plugin caching logic to take the basis gates into account

* regenerating cached values following review comment

(cherry picked from commit 6d4c928)

Co-authored-by: Alexander Ivrii <alexi@il.ibm.com>
github-merge-queue bot pushed a commit that referenced this pull request May 8, 2025
* extending SolovayKitaevSynthesis plugin caching logic to take the basis gates into account

* regenerating cached values following review comment

(cherry picked from commit 6d4c928)

Co-authored-by: Alexander Ivrii <alexi@il.ibm.com>
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 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.

5 participants