Skip to content

Implement MCTS algorithm for synthesizing PauliEvolution gates#16581

Open
alexanderivrii wants to merge 27 commits into
Qiskit:mainfrom
alexanderivrii:mcts/2
Open

Implement MCTS algorithm for synthesizing PauliEvolution gates#16581
alexanderivrii wants to merge 27 commits into
Qiskit:mainfrom
alexanderivrii:mcts/2

Conversation

@alexanderivrii

@alexanderivrii alexanderivrii commented Jul 13, 2026

Copy link
Copy Markdown
Member

Summary

This implements a synthesis algorithm for PauliEvolution gates 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 Clifford structure 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_simulations can control the number of additional node selections and greedy synthesis rollouts performed, setting it to N increases the runtime by about N but might find better solutions.

This is a joint work with @mtreinish.

AI/LLM disclosure

  • I didn't use LLM tooling, or only used it privately.
  • I used the following tool to help write this PR description:
  • I used the following tool to generate or modify code:

I did privately ask a review from Claude, which was surprisingly quite helpful.

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.
@alexanderivrii alexanderivrii added this to the 2.6.0 milestone Jul 13, 2026
@alexanderivrii
alexanderivrii requested a review from a team as a code owner July 13, 2026 12:13
@alexanderivrii alexanderivrii added the Changelog: Added Add an "Added" entry in the GitHub Release changelog. label Jul 13, 2026
@alexanderivrii
alexanderivrii requested a review from gadial July 13, 2026 12:13
@alexanderivrii alexanderivrii added synthesis Rust This PR or issue is related to Rust code in the repository labels Jul 13, 2026
@qiskit-bot

Copy link
Copy Markdown
Collaborator

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

  • @Qiskit/terra-core

@alexanderivrii

alexanderivrii commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

These are the benchmarks from the paper. The table below compares rustiq vs mcts with num_simulations=0 vs mcts with num-simulations=100. It's worth noting that mtcs with num_simulations=0 outperforms rustiq both in runtime and the CX-count, while mtcs with num_simulations=100 may additional improve CX-counts on larger circuits.

  id  name               nq    terms    rustiq 2q    rustiq t    mcts 2q    mcts t    mcts100 2q    mcts100 t
----  ---------------  ----  -------  -----------  ----------  ---------  --------  ------------  -----------
   0  FermHub_2_3.pkl    12       46          176      0.0008        126    0.0004           118       0.0068
   1  FermHub_3_3.pkl    18       75          352      0.0015        242    0.0007           226       0.0201
   2  FermHub_4_3.pkl    24      104          624      0.0031        362    0.0013           358       0.0419
   3  FermHub_4_5.pkl    40      184         1370      0.0124        748    0.0036           674       0.1129
   4  FermHub_5_5.pkl    50      235         1634      0.0211       1042    0.0064           912       0.1966
   5  FermHub_6_5.pkl    60      286         1998      0.0337       1350    0.0106          1188       0.2953
   6  Heisen_2_2.pkl      8       48          122      0.0005        108    0.0004           104       0.0089
   7  Heisen_2_3.pkl     12       84          228      0.0012        188    0.0009           186       0.0226
   8  Heisen_3_3.pkl     18      144          444      0.0036        332    0.0017           326       0.0537
   9  Heisen_4_5.pkl     40      372         1242      0.0304        848    0.0101           846       0.2585
  10  Heisen_5_5.pkl     50      480         1710      0.0579       1096    0.0187          1092       0.4462
  11  Heisen_6_5.pkl     60      588         2174      0.0962       1340    0.0320          1332       0.6833
  12  LiH.pkl            10      275         1238      0.0113        948    0.0039           920       0.1452
  13  H2O.pkl            12      550         2624      0.0529       1926    0.0123          1926       0.5308
  14  UCC_2_4.pkl         4       12           30      0.0002         26    0.0001            26       0.0011
  15  UCC_4_8.pkl         8      160          608      0.0029        602    0.0017           518       0.0612
  16  UCC_6_12.pkl       12      828         3722      0.1438       3704    0.0291          3358       1.4357

Update: I have removed the long text summaries for the 100 Hamiltonian benchmarks from Benchpress, and will add graphs instead.

@Cryoris

Cryoris commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

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?

@mtreinish

Copy link
Copy Markdown
Member

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.
@alexanderivrii

alexanderivrii commented Jul 15, 2026

Copy link
Copy Markdown
Member Author

By looking at condensedmatter/​heisenberg/​heis/​graph-2D-grid-pbc-qubitnodes_Lx-5_Ly-186_h-3, we have realized that most of the MCTS runtime with num_simulations=0 is due to constructing the anti-commutativity graph, checking the commutativity of Paulis in dense representation. Since the original input Pauli network is in sparse format, and since for most of the problems the rotations involve only 1-q and 2-q interactions, it makes sense to construct the commutativity DAG using the original (albeit sorted) sparse representation. For the above benchmark, this reduces the runtime from 68.0 seconds to 0.78 seconds. This change has no effect on the synthesized circuits. The new HamLib benchmarks now look as follows:

The next case to consider in more details would be condensedmatter/fermihubbard/FH_D-2/fh-graph-2D-grid-pbc-qubitnodes_Lx-5_Ly-72_U-0_enc-parity, which still takes about 2 minutes. (update: improved in 082ba32).

@ShellyGarion

Copy link
Copy Markdown
Member

adding "on hold" label, since this PR is built on top of #16587

@ShellyGarion ShellyGarion added the on hold Can not fix yet label Jul 16, 2026
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.
@alexanderivrii

alexanderivrii commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

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 condensedmatter/fermihubbard/FH_D-2/fh-graph-2D-grid-pbc-qubitnodes_Lx-5_Ly-72_U-0_enc-parity from 113 seconds to 4 seconds. The caching mechanism has no effect on the actual heuristic used. There are possibly more things that can be done around this, such as reusing the cache across synthesizing multiple Paulis, or using a min-heap-like data structure to find minimum scores faster.

ToDo: investigate discreteoptimization/maxkcut/ham-gray-color02-1-fullins_5_k-5/1-fullins_5,n-48,rinst-7, which currently takes 11 seconds (update: 03384bd reduces the time by a factor of 2, but this still remains as the slowest benchmark).

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.
Setting default value of max_parallel_simulation to None
@alexanderivrii

Copy link
Copy Markdown
Member Author

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):

  • The default plugin takes about 3 seconds and produces 876_150 2-qubit gates.
  • The rustiq plugin takes 5510 seconds and produces 3_030_886 2-qubit gates (for 2-qubit counts: in many cases rustiq produces better results, yet there are a few larger examples where it performs significantly worse).
  • The new mcts plugin takes 18 seconds and produces a total of 501_204 2-qubit gates.

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 num_simulations that allows to run multiple simulations, often additionally slightly improving 2-qubit counts at the expense of additional runtime. For instance, setting num_simulations=100 decreases the total 2-qubit counts to 491_414. Running 100 simulations sequentially would be about 100x slower, however in practice we do exploit multi-threading.

@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 29690644099

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.03%) to 87.669%

Details

  • Coverage increased (+0.03%) from the base build.
  • Patch coverage: 77 uncovered changes across 5 files (1011 of 1088 lines covered, 92.92%).
  • 83 coverage regressions across 6 files.

Uncovered Changes

File Changed Covered %
crates/quantum_info/src/clifford.rs 423 380 89.83%
crates/synthesis/src/evolution/chunks.rs 14 0 0.0%
crates/synthesis/src/evolution/mcts.rs 500 488 97.6%
crates/synthesis/src/evolution/mod.rs 67 63 94.03%
qiskit/transpiler/passes/synthesis/hls_plugins.py 42 38 90.48%
Total (11 files) 1088 1011 92.92%

Coverage Regressions

83 previously-covered lines in 6 files lost coverage.

File Lines Losing Coverage Coverage
crates/circuit/src/dag_circuit.rs 53 85.4%
crates/qasm2/src/parse.rs 18 96.66%
crates/circuit/src/parameter/symbol_expr.rs 4 75.06%
crates/quantum_info/src/clifford.rs 4 89.97%
crates/qasm2/src/lex.rs 3 93.06%
crates/circuit/src/parameter/parameter_expression.rs 1 90.33%

Coverage Stats

Coverage Status
Relevant Lines: 129751
Covered Lines: 113752
Line Coverage: 87.67%
Coverage Strength: 993508.74 hits per line

💛 - Coveralls

@ShellyGarion ShellyGarion removed the on hold Can not fix yet label Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Changelog: Added Add an "Added" entry in the GitHub Release changelog. Rust This PR or issue is related to Rust code in the repository synthesis

Projects

Status: Ready

Development

Successfully merging this pull request may close these issues.

6 participants