Skip to content
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

Update Split2QUnitaries to handle SWAP unitary gates #13531

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

gadial
Copy link
Contributor

@gadial gadial commented Dec 5, 2024

Summary

Updates the Split2QUnitaries transpiler pass to handle 2-qubit unitary gates which can be decomposed as a swap gate and 1-qubit gates.

Closes #13476

Details and comments

This PR extends the Split2QUnitaries in the following manner:

  1. During the initial pass Split2QUnitaries performs, it checks whether any unitary encountered is a SWAP gate.
  2. If SWAP gates were found, another pass is performed on the DAG, replacing SWAP gates by the 1-qubit unitary gates in their decomposition (as is currently done in Split2QUnitaries) and instead of adding SWAP gates, performs a virtual swap by dynamically changing the qubit layout (affecting subsequent gates and the final layout) similar to what the ElidePermutations does.

Since step 2 requires generating a new DAG (as opposed to what Split2QUnitaries does to identity-equivalent unitary gates), it is not performed during the initial pass, only if SWAP gates were detected.

@gadial gadial requested a review from a team as a code owner December 5, 2024 15:28
@qiskit-bot
Copy link
Collaborator

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

  • @Qiskit/terra-core

@ShellyGarion ShellyGarion added this to the 2.0.0 milestone Dec 5, 2024
@coveralls
Copy link

coveralls commented Dec 5, 2024

Pull Request Test Coverage Report for Build 13158018594

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

  • 133 of 139 (95.68%) changed or added relevant lines in 3 files are covered.
  • 823 unchanged lines in 35 files lost coverage.
  • Overall coverage decreased (-0.3%) to 88.66%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/transpiler/passes/optimization/split_2q_unitaries.py 15 16 93.75%
qiskit/transpiler/preset_passmanagers/builtin_plugins.py 4 5 80.0%
crates/accelerate/src/split_2q_unitaries.rs 114 118 96.61%
Files with Coverage Reduction New Missed Lines %
qiskit/synthesis/linear/linear_depth_lnn.py 1 90.91%
qiskit/providers/fake_provider/fake_backend.py 1 85.51%
qiskit/providers/basic_provider/basic_provider_tools.py 1 83.33%
crates/circuit/src/operations.rs 1 89.61%
crates/accelerate/src/split_2q_unitaries.rs 1 96.27%
qiskit/pulse/transforms/canonicalization.py 1 93.44%
qiskit/pulse/transforms/base_transforms.py 2 86.96%
qiskit/dagcircuit/collect_blocks.py 2 98.34%
qiskit/transpiler/passes/optimization/collect_cliffords.py 2 92.0%
qiskit/pulse/schedule.py 3 88.6%
Totals Coverage Status
Change from base Build 12905478077: -0.3%
Covered Lines: 79062
Relevant Lines: 89174

💛 - Coveralls

Copy link
Member

@mtreinish mtreinish 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 doing this, from a quick glance the code looks good (but I only quickly skimmed it on my phone, I'll do a deeper reviewer later). But I had some quick comments on the tests to start.

Comment on lines 291 to 308
def test_2q_swap_with_1_qubit_gates(self):
"""Test that a 2q unitary matching a swap gate with 1-qubit gates before and after is correctly processed."""
qc = QuantumCircuit(2)
qc.h(0)
qc.x(1)
qc.swap(0,1)
qc.sx(0)
qc.sdg(1)
qc.global_phase += 1.2345
qc_split = QuantumCircuit(2)
qc_split.append(UnitaryGate(Operator(qc)), [0, 1])

pm = PassManager()
pm.append(Collect2qBlocks())
pm.append(ConsolidateBlocks())
pm.append(Split2QUnitaries())
res = pm.run(qc_split)
res_op = Operator.from_circuit(res, final_layout=res.layout.final_layout)
Copy link
Member

Choose a reason for hiding this comment

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

Can you add a test that has this in it as part of a larger circuit that runs through the full transpilation pipeline? I want to make sure that we validating that we're tracking and composing the final permutations correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Also added a smaller test which does non-trivial swapping (with swap gates removed using the elide permutation pass) before the unitary test which surfaced a possible bug in the layout composition phase. We need to discuss whether in this phase we should compose on the existing layout or (as was the case before my change) on its inverse.

self.assertTrue(expected_op.equiv(res_op))
self.assertTrue(
matrix_equal(expected_op.data, res_op.data, ignore_phase=False)
)
Copy link
Member

Choose a reason for hiding this comment

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

Can we add an assertion here that swap has been removed from the output?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added self.assertNotIn("swap", res.count_ops()) but I don't think it's enough - there's also the possibility that we didn't split the unitary at all (note that it's also not being checked in the tests relating to the original split unitary code).

Any idea how to easily verify that the unitary was removed? Note that just using count_ops is tricky since we have a unitary both before (as the 2-qubit unitary) and after (as two individual 1-qubit unitaries). I can compare the count itself (1 before vs 2 after) but it feel less robust than it should.

self.assertTrue(expected_op.equiv(res_op))
self.assertTrue(
matrix_equal(expected_op.data, res_op.data, ignore_phase=False)
)
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 add an assertion that we've removed the swap gate from the circuit. This test could still pass if there was no transformation on the input circuit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@alexanderivrii
Copy link
Contributor

If I recall correctly, we don't run the ElidePermutations pass if the user explicitly sets the initial layout (to prevent the transpiler from messing up circuits that are already routed). Is this something we should worry about for this pass as well? Would it make sense to have an additional flow where the pass would replace a 2-qubit unitary by a SWAP + 1-qubit unitaries without merging the SWAP into the final permutation?

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 your contribution. I have a few minor comments.

Copy link
Member

@mtreinish mtreinish left a comment

Choose a reason for hiding this comment

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

Overall this looks excellent thanks for doing this, this looks about ready now. I just have a few small comments and questions inline. The biggest one I think is over how/when you're thinking we should integrate this new feature into the preset pass managers. It's fine if you'd like to do that in a separate follow-up PR to keep this PR to a single logical change (that's probably preferable actually). I just wanted to understand your thinking on that.

params: (!new_op.params.is_empty()).then(|| Box::new(new_op.params)),
extra_attrs: new_op.extra_attrs,
#[cfg(feature = "cache_pygates")]
py_op: std::sync::OnceLock::new(),
Copy link
Member

Choose a reason for hiding this comment

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

You do have the py op available for caching here since you are generating this via python. But in this case it doesn't actually matter too much because this is always going to be a PyGate and if the cache is None the python object reference will just be taken from that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So, do you think something needs to change here?

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 this is fine as is. This was just more my idle musings.

new_dag.add_global_phase(py, &Param::Float(decomp.global_phase + PI4))?;
}
}
if !is_swap {
Copy link
Member

Choose a reason for hiding this comment

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

Instead of doing this with a mutable is_swap boolean variable, couldn't you just add a continue inside the if block for a swap equivalent unitary? and remove this !is_swap if statement. I feel like that might be a bit preferable because it reduce a layer of indentation here if nothing else. Although I suspect the compiler might be able to reason about the flow a bit better with a continue, but that would need to be validated and if true is of very minimal relevance to not be worth validating or benchmarking.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in a9bc3e1

"""

def __init__(self, fidelity: float = 1.0 - 1e-16):
def __init__(self, fidelity: float = 1.0 - 1e-16, split_swap: bool = False):
Copy link
Member

Choose a reason for hiding this comment

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

By default this means we don't run the swap splitting, are you planning a follow-up where you update https://github.com/Qiskit/qiskit/blob/main/qiskit/transpiler/preset_passmanagers/builtin_plugins.py#L192-L194 to set it to True unless routing=None is specified?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's exactly what I'm wondering about. If you think the specific logic you suggested here suffices, I'll add this as part of this PR.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah I think that's sufficient. You can mirror the exact logic that conditions the execution of ElidePermutations .

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in a9bc3e1


(new_dag, qubit_mapping) = result
input_qubit_mapping = {qubit: index for index, qubit in enumerate(dag.qubits)}
self.property_set["original_layout"] = Layout(input_qubit_mapping)
Copy link
Member

Choose a reason for hiding this comment

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

Does this behave correctly if we run elide permutations first? I realize this is just a straight copy of what elide permutations is doing, but I'm wondering if we need to check that we're not doing multiple permutations before applying the layout or do some other storing if we run these together.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It should work - in theory. in split_2q_unitaries.py we have

if current_layout := self.property_set["virtual_permutation_layout"]:
            self.property_set["virtual_permutation_layout"] = new_layout.compose(
                current_layout, dag.qubits
            )

And compose works, if I recall correctly. However, this is not covered by the current test suite, and that's one of the things I wanted to ask about - should the "joint" test be placed in TestSplit2QUnitaries or perhaps a different location? What flows should we check, other than adding Split2QUnitaries before and after ElidePermutations?

handle the case where the unitary in consideration can be written
as a SWAP gate and two 1-qubit gates. In this case, it splits the
unitary and also applies virtual swapping similar to what is done in
:class:`.ElidePermutations`.
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 probably mention the new argument here to document that it is being introduced. Something like:

Suggested change
:class:`.ElidePermutations`.
:class:`.ElidePermutations`. This functionality can be controlled with a new argument,
``split_swap``, on the constructor of :class`.Split2QUnitaries` which can be used to disable
splitting swap equivalent gates.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in a9bc3e1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for SWAP specialization in Split2qUnitaries
6 participants