Implement MCTS algorithm for synthesizing PauliEvolution gates#16581
Implement MCTS algorithm for synthesizing PauliEvolution gates#16581alexanderivrii wants to merge 27 commits into
MCTS algorithm for synthesizing PauliEvolution gates#16581Conversation
PauliList allows to store an arbitrary number of Paulis with associated phases in a packed format that is optimized for vectorized conjugation by Clifford gates. Clifford uses PauliList.
…cal to the rollout strategy in the python code (up to a small bug fix in the python code), ensuring that this implementation and the original python code give similar results. In particular, this is the reason why we are reversing the iterators when calling max_by_key -- to ensure that the first candidate with the best score gets chosen, or why we are considering the pair (j, i) directly after the pair (i, j).
…om the Rustiq paper. This changes the results, however on average the number of CX-gates is about the same as before. For efficiency, this uses table lookups instead of direct conjugation, similarly to the implementation in Rustiq, which further improves the runtime by 2x on larger instances.
fixing global phase with all-identity paulis cleanup
Making pass over tests Some cleanup
When computing the score of applying a given Clifford circuit on a pair of qubits, instead of iterating over all active Paulis and computing the score change for this pair of qubits, it is faster to first count the number of pairs of each type and then sum up the results. This gives up to 10x performance improvement for the larger circuits. Interestingly, the idea for this optimization was suggested by Claude when I privately asked it to make performance suggestions.
|
One or more of the following people are relevant to this code:
|
|
These are the benchmarks from the paper. The table below compares Update: I have removed the long text summaries for the 100 Hamiltonian benchmarks from Benchpress, and will add graphs instead. |
|
Do we have any precedence in the compiler of "try this until a timeout, else use a fallback"? The only thing that comes to my mind is VF2 but that just increases the number of tries with the optimization level and doesn't have a true timeout, right? |
We have tried this in the past with vf2 (there is still an option to do this in the pass but we don't use it by default), it was not a good idea in practice from a reproducibility PoV. The wall time of a pass inherently leads to non-deterministic outcones as the no two runs will take the exact same amount of time. Especially if running the pass on different computers the slower system is more likely to hit the timeout and fallback. When we tried this in VF2 we actually hit this issue in CI where unit test jobs started to fail on slower nodes that couldn't find the best solution in the specified time window (see: #8021). In general I'd say if you want to have a dial of heuristic effort it needs to be a function of the algorithm itself and not the underlying hardware executing the algorithm. |
We have noticed that on some of the HamLib instances, the bottleneck is constructing the (anti)-commutativity DAG, sometimes taking more than 99% of the overall runtime. The slowdown is due by checking commutativity of pairs of Pauli in dense format (with all the Is). However, in most of these problems, the support sizes of Pauli rotations are small, and thus it makes sense to construct the commutativity DAG by sorting the sparse Paulis and checking commutativity using the sparse representation. This reduces the time to construct the DAG to almost nothing. Additionally, this slightly refactors the ownership during the input conversion - avoiding unnecessary clones when possible.
|
By looking at The next case to consider in more details would be |
|
adding "on hold" label, since this PR is built on top of #16587 |
When iteratively synthesizing a single Pauli rotation, we compute the scores (or actually 2-qubit Pauli counts) for pairs (ctrl, targ) in the support. After the best chunk (leading to the smallest score) is applied, all the scores on qubits unaffected by the chunk remain the same. This optimization uses a hash_map to avoid recomputing unaffected scores.
|
Update: 082ba32 adds a simple caching optimization to avoid recomputing scores when synthesizing a single pauli rotation (see the commite message for additional details). This reduces the runtime of ToDo: investigate |
Instead early-returning when support's size becomes at least 2
* Re-exposing append methods from PauliList to Clifford, thus minimizing the churn for the changed lines. The direct access to entries in the Clifford tableau is using get_entry(row, column) method. * Improving docstrings from Clifford and SymplecticMatrix to describe the stabilizer/destabilizer/X/Z/phase layout. * Adding missing documentation and some missing tests.
…llel. Fixing the option num_simulations to represent the total number of simulations, including the simulation from the root node. Adding an option max_parallel_simulations to limit the number of simulations per a single batch. This might be important as MTCS node scores are updated after each batch completes. On the HamLib benchmark I am experimenting on, I see about 8x speedup with num_simulations=10 and max_parallel_simulations=10, without affecting the quality of the resulting circuit.
This is another performance optimization that avoids recomputing support sizes for active Paulis and instead keeps an additional field that is updated on the fly. This reduces the runtime on a specific benchmark with many qubits and paulis by 10x.
|
I have run out of the low-hanging performance optimizations, so this is ready for review. I will update this comment with the full data on the "100 representatives Hamiltonians" from Benchpress, but here is a quick summary for now (summed over all of the 100 benchmarks):
I believe we can set mcts to be the default synthesis, but I would prefer to do this in a separate PR. There is also an MCTS option |
Coverage Report for CI Build 29690644099Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage increased (+0.03%) to 87.669%Details
Uncovered Changes
Coverage Regressions83 previously-covered lines in 6 files lost coverage.
Coverage Stats
💛 - Coveralls |
Summary
This implements a synthesis algorithm for
PauliEvolutiongates described in the paper "MonteQ: A Monte Carlo Tree Search Based Quantum Circuit Synthesis Framework" (see the paper https://arxiv.org/abs/2604.19029 and their python code https://github.com/Mulundano/MonteQ). Or, more precisely, the part of the paper that deals with all-to-all connectivity (the architecture-aware synthesis is left as a potential follow-up, those note that we typically run synthesis on the abstract circuit and we don't take the connectivity constraints into account at this stage).The current implementation is in Rust. The very initial implementation (see 74dab2c) aimed to provide identical results to their Python code (modulo some minor bug fixes), however I have then completely reimplemented the "greedy synthesis" part of the code using chunking and precomputed tables ideas from Rustiq, leading to additional significant performance improvements.
This PR also adds the algorithm as a high level synthesis plugin for
PauliEvolutionGates and registers this plugin as the stevedore's entrypoint.A part of this PR is a refactoring of
Cliffordstructure in Rust. Update: following some preliminary feedback, this was moved into a separate PR #16587.By default, the new synthesis algorithm just does some form of greedy synthesis, without exploiting any of the power of Monte Carlo Tree Simulation approach developed in the paper, and so can be compared to Rustiq. The additional parameter
num_simulationscan control the number of additional node selections and greedy synthesis rollouts performed, setting it toNincreases the runtime by aboutNbut might find better solutions.This is a joint work with @mtreinish.
AI/LLM disclosure
I did privately ask a review from Claude, which was surprisingly quite helpful.