Skip to content

Quantum Multiplexer Simplification #13099

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 9 commits into from
Nov 14, 2024
Merged

Conversation

josejad42
Copy link
Contributor

@josejad42 josejad42 commented Sep 5, 2024

Summary

Multiplexer optimization by reducing unnecessary controls.

Details and comments

This method introduces a simplification in multiplexers that removes unnecessary controls and operators when a pattern of repetitions is detected. The main advantage is a significant reduction in both the number of CX gates and the overall circuit depth during separable state preparation.

@josejad42 josejad42 requested a review from a team as a code owner September 5, 2024 19:03
@qiskit-bot qiskit-bot added the Community PR PRs from contributors that are not 'members' of the Qiskit repo label Sep 5, 2024
@CLAassistant
Copy link

CLAassistant commented Sep 5, 2024

CLA assistant check
All committers have signed the CLA.

@qiskit-bot
Copy link
Collaborator

Thank you for opening a new pull request.

Before your PR can be merged it will first need to pass continuous integration tests and be reviewed. Sometimes the review process can be slow, so please be patient.

While you're waiting, please feel free to review other open PRs. While only a subset of people are authorized to approve pull requests for merging, everyone is encouraged to review open pull requests. Doing reviews helps reduce the burden on the core team and helps make the project's code better for everyone.

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

  • @Cryoris
  • @Qiskit/terra-core
  • @ajavadia

@coveralls
Copy link

coveralls commented Sep 6, 2024

Pull Request Test Coverage Report for Build 11779453889

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 70 of 70 (100.0%) changed or added relevant lines in 1 file are covered.
  • 3152 unchanged lines in 172 files lost coverage.
  • Overall coverage decreased (-0.2%) to 88.926%

Files with Coverage Reduction New Missed Lines %
qiskit/quantum_info/operators/mixins/multiply.py 1 92.86%
qiskit/transpiler/passes/layout/disjoint_utils.py 1 96.8%
qiskit/pulse/instructions/directives.py 1 97.14%
qiskit/transpiler/passes/optimization/elide_permutations.py 1 95.65%
qiskit/circuit/library/boolean_logic/inner_product.py 1 96.0%
qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py 1 96.3%
qiskit/synthesis/one_qubit/one_qubit_decompose.py 1 69.86%
qiskit/quantum_info/operators/mixins/group.py 1 92.0%
qiskit/pulse/instructions/play.py 1 97.5%
qiskit/quantum_info/operators/channel/quantum_channel.py 1 71.74%
Totals Coverage Status
Change from base Build 10725151093: -0.2%
Covered Lines: 79116
Relevant Lines: 88968

💛 - Coveralls

@Cryoris
Copy link
Contributor

Cryoris commented Sep 6, 2024

Hi @josejad42, thanks for opening a PR! Could you give some more context to what you're suggesting, e.g. is there a reference that describes this simplification, and could you benchmark your improvement?

@adjs
Copy link
Contributor

adjs commented Sep 6, 2024

Hi @Cryoris, the reference will appear on arxiv in the next few days.

statevector2 = Statevector(qc2)

print(qc1.count_ops())
print(qc2.count_ops())
Copy link
Member

Choose a reason for hiding this comment

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

I think that these prints could be removed. In addition, is there some upper bound on the number of ops that can be added to the tests?

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks, we removed the prints and included a test to verify if the Initialize reduced the number of cnots of a disentangled state.

@@ -39,6 +39,36 @@ class TestInitialize(QiskitTestCase):

_desired_fidelity = 0.99

def test_disentangled(self):
Copy link
Member

@ShellyGarion ShellyGarion Sep 8, 2024

Choose a reason for hiding this comment

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

Please note that the current Qiskit Initialize code is based on Isometry by Iten et al. Phys. Rev. A 93, 032318.
This change has been done recently in #12178 for Qiskit 1.2 release.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks, we fixed the optimization and now the test works.

@ShellyGarion ShellyGarion added the mod: circuit Related to the core of the `QuantumCircuit` class or the circuit library label Sep 8, 2024
fix diagonal gate size
@adjs
Copy link
Contributor

adjs commented Sep 10, 2024

Hi @Cryoris and @ShellyGarion
The multiplexer simplification is described in https://arxiv.org/pdf/2409.05618

Comment on lines 67 to 71
counts1 = qc1.count_ops()["cx"]
counts2 = qc2.count_ops()["cx"]

self.assertTrue(counts2 <= counts1)
self.assertTrue(np.allclose(statevector1, statevector2))
Copy link
Contributor

Choose a reason for hiding this comment

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

The UCGate simplification impacts state preparation.
If a real-valued state is separable, the number of cx gates of Initialize will be reduced.

However, there is no guarantees that the number of cnots will be optimal.
The initialization of [0, 1, 0, 0] produces a circuit with one cx (that can be removed because the control is always in zero).

In complex-valued states, we can have some optimization. But there is no guarantee that the simplification will work.

Copy link
Member

Choose a reason for hiding this comment

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

in the test above both counts1 and counts2 are equal (=12). Do you have an example that there is an inequality? (perhaps you would like to fix a seed).

@ShellyGarion
Copy link
Member

ShellyGarion commented Sep 10, 2024

Hi @Cryoris and @ShellyGarion The multiplexer simplification is described in https://arxiv.org/pdf/2409.05618

Could you please add some benchmarks comparing your state preparation code to the existing Qiskit code?
See e.g. the benchmarks in #12081

@adjs
Copy link
Contributor

adjs commented Sep 10, 2024

Hi @Cryoris and @ShellyGarion The multiplexer simplification is described in https://arxiv.org/pdf/2409.05618

Could you please add some benchmarks comparing your state preparation code to the existing Qiskit code? See e.g. the benchmarks in #12081

Yes, sure! The best case is a separable real-valued state.

qiskit 1.2.0

nqubits: 3 depth: 10 time: 0.03743324100003065
nqubits: 6 depth: 116 time: 0.08239613299997472
nqubits: 9 depth: 1006 time: 0.8098489340000015
nqubits: 12 depth: 8168 time: 27.270649252000112

this branch

nqubits: 3 depth: 2 time: 0.053231799975037575
nqubits: 6 depth: 4 time: 0.05455289990641177
nqubits: 9 depth: 10 time: 0.10728330002166331
nqubits: 12 depth: 24 time: 0.424586100038141

import numpy as np

from qiskit import transpile, QuantumCircuit
from time import perf_counter

for k in range(1, 5):
    size = 2 ** k
    state1 = np.random.rand(size)
    state1 = state1 / np.linalg.norm(state1)
    state2 = np.random.rand(size)
    state2 = state2 / np.linalg.norm(state2)
    state3 = np.random.rand(size)
    state3 = state3 / np.linalg.norm(state3)

    state = np.kron(state1, state2)
    state = np.kron(state, state3)

    nqubits = int(np.log2(len(state)))

    start = perf_counter()
    qc2 = QuantumCircuit(nqubits)
    qc2.initialize(state)
    qc2 = transpile(qc2, basis_gates=['u', 'cx'], optimization_level=2)
    end = perf_counter()
    print('nqubits: ', nqubits, 'depth: ',  qc2.depth(), 'time: ', end - start)

josejad42 and others added 2 commits September 10, 2024 09:31
Co-authored-by: Adenilton Silva <7927558+adjs@users.noreply.github.com>
Copy link
Member

@ShellyGarion ShellyGarion left a comment

Choose a reason for hiding this comment

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

@josejad42, @adjs - Thanks for your contribution to Qiskit!

My main question is whether you always reduce the number of CX gates in the StatePreparation or Initialize process ?
I tried the same complex random states that appears in #12081 and I see that the number of CX gates in unchanged. In the example you provided above, the number of CX gates is significantly reduced.
Are there states for which the number of CX gates may increase ?

You may also add your reference and an explanation to the API docs (line 64)

def __init__(self, gate_list: list[np.ndarray], up_to_diagonal: bool = False):
def __init__(
self, gate_list: list[np.ndarray], up_to_diagonal: bool = False, mux_simp: bool = True
):
Copy link
Member

Choose a reason for hiding this comment

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

  • why do you need the parameter mux_simp ?
    if it's needed - please also add it to the documentation (line 81).

  • if this parameter is needed, why don't you need to call it in the functions that call UCGate directly or indirectly (mcg_up_to_diagonal, Isometry, StatePreparation and Initialize) ? Since it's default value is True it will always affect these functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We think that some users may not want to search for simplifications or may need to use the original version of the multiplexer. Therefore, simplification is enabled by default but can be easily disabled. Documentation will be updated.

@@ -100,6 +107,21 @@ def test_inverse_ucg(self):

self.assertTrue(np.allclose(unitary_desired, unitary))

def test_ucge(self):
Copy link
Member

Choose a reason for hiding this comment

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

why do you need a specific test? Isn't it enough to add this specific gate_list of hadamards to squs above?

Comment on lines 67 to 71
counts1 = qc1.count_ops()["cx"]
counts2 = qc2.count_ops()["cx"]

self.assertTrue(counts2 <= counts1)
self.assertTrue(np.allclose(statevector1, statevector2))
Copy link
Member

Choose a reason for hiding this comment

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

in the test above both counts1 and counts2 are equal (=12). Do you have an example that there is an inequality? (perhaps you would like to fix a seed).

update documentation
@josejad42
Copy link
Contributor Author

josejad42 commented Sep 20, 2024

@josejad42, @adjs - Thanks for your contribution to Qiskit!

My main question is whether you always reduce the number of CX gates in the StatePreparation or Initialize process ? I tried the same complex random states that appears in #12081 and I see that the number of CX gates in unchanged. In the example you provided above, the number of CX gates is significantly reduced. Are there states for which the number of CX gates may increase ?

You may also add your reference and an explanation to the API docs (line 64)

In the context of state preparation, our simplification primarily applies when the state to be prepared is separable, as the presence of a given repetition pattern in multiplexers is quite common in these cases. Randomly generated states are less likely to exhibit such repetitions, meaning that the number of CX ports remains unchanged. The reference was updated in the documentation.

@ShellyGarion
Copy link
Member

Could you please also add release notes to your PR ?

@ACE07-Sev
Copy link

ACE07-Sev commented Sep 27, 2024

Wondering if this is needed

if mux_simplify:
    circuit.append(diagonal, [q_target] + q_controls)
else:
    circuit.append(diagonal, q)

Isn't q also [q_target] + q_controls? If we ignore mux_simplify, then q_target=q[0], and q_controls=q[1:], ergo q. Given the clause above, it would still be just q whether mux_simplify is True or not. Please correct me if I am wrong, I may be missing something.

@josejad42
Copy link
Contributor Author

Wondering if this is needed

if mux_simplify:
    circuit.append(diagonal, [q_target] + q_controls)
else:
    circuit.append(diagonal, q)

Isn't q also [q_target] + q_controls? If we ignore mux_simplify, then q_target=q[0], and q_controls=q[1:], ergo q. Given the clause above, it would still be just q whether mux_simplify is True or not. Please correct me if I am wrong, I may be missing something.

When there's simplification, q is not necessarily equal to [q_target] + q_controls, since q_controls contains only the necessary control qubits. Regardless of whether simplification is applied, we can always use

circuit.append(diagonal, [q_target] + q_controls)

to add the diagonal gate in the correct position.

@josejad42
Copy link
Contributor Author

Could you please also add release notes to your PR ?

Hi, @ShellyGarion. The release notes have been added.

@ACE07-Sev
Copy link

May I ask when this will be merged with main?

@ShellyGarion
Copy link
Member

May I ask when this will be merged with main?

I'm still reviewing it. I hope to finish it soon :)

Copy link
Member

@ShellyGarion ShellyGarion left a comment

Choose a reason for hiding this comment

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

Thanks for the fixes. I only have a few questions and suggestions about the tests.

@@ -44,6 +46,7 @@ class TestUCGate(QiskitTestCase):
[_id, _id],
[_id, 1j * _id],
[_id, _not, _id, _not],
[_rand, _had, _rand, _had, _rand, _had, _rand, _had],
Copy link
Member

Choose a reason for hiding this comment

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

I would also suggest to add here:
[_had, _had, _had, _had, _had, _had, _had, _had],

then after line 53, please also add:
mux_simp = [True, False],

and in line 56, call:
def test_ucg(self, squs, up_to_diagonal, mux_simp):

and update lines 62 and 71 to:
UCGate(squs, up_to_diagonal=up_to_diagonal, mux_simp=mux_simp)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Suggestions accepted and included.

uc2 = UCGate(gate_list, up_to_diagonal=False, mux_simp=True)
qc2.append(uc2, range(4))
op2 = Operator(qc2).data
self.assertTrue(np.allclose(op1, op2))
Copy link
Member

Choose a reason for hiding this comment

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

When I change up_to_diagonal=True in both lines 115 and 120, then the test fails, any idea why?
If mux_simp should not work in case that up_to_diagonal=True - then it's better to state it in the docs and raise a proper error that asserts that the the two variables can not both be True.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This occurs because the final operator when we have up_to_diagonal=True is not equal for the original and the simplified multiplexers. But when we multiply the two operators by the respective diagonal matrices obtained by self._get_diagonal(), the final operators (and the prepared states) are the same.

@alexanderivrii
Copy link
Member

This is a cool PR! Out of curiosity, how common are multiplexers with repetitions in "practical applications"?

I have not thought about the problem too much, but it strongly reminds me of the works on more concisely representing boolean functions. For instance, we can probably express the problem in terms of (multi-valued) decision diagrams, or alternatively as expressing a truth table of a boolean function as a sum-of-products with non-overlapping products.

One more question: the simplified representation is not unique, correct? For instance, if the following 3 control paths (a=0) & (b=0) & (c=0), (a=0) & (b=0) & (c=1), (a=0) & (b=1) & (c=0) lead to the same unitary, then we can simplify either to (a=0) & (b=0), (a=0) & (b=1) & (c=0), or to (a=0) & (c=0), (a=0) & (b=0) & (c=1).

@ACE07-Sev
Copy link

ACE07-Sev commented Nov 4, 2024

@alexanderivrii Multiplexors are essentially uniformly controlled gates. The main use of them for me has been in the implementation of state and unitary synthesis by Shende. If one can implement a multiplexor with less number of gates, then in those applications the overall circuit depth would be considerably smaller.

@ShellyGarion ShellyGarion added the Changelog: New Feature Include in the "Added" section of the changelog label Nov 5, 2024
@josejad42
Copy link
Contributor Author

@alexanderivrii As described above, quantum multiplexers are common in state preparation and unitary synthesis.

This method introduces a simplification when a specific pattern of repetitions is detected. This pattern is very common to separable states preparation and can be detected with low computational cost. So, this is not a general simplification.

Copy link
Member

@ShellyGarion ShellyGarion 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 @josejad42 for your contribution to Qiskit

@ShellyGarion ShellyGarion added this pull request to the merge queue Nov 14, 2024
Merged via the queue into Qiskit:main with commit b258efc Nov 14, 2024
17 checks passed
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 Community PR PRs from contributors that are not 'members' of the Qiskit repo mod: circuit Related to the core of the `QuantumCircuit` class or the circuit library
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

9 participants