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

Optimize shortestpath in SABRE #1408

Merged
merged 17 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/qibo/transpiler/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,15 @@ def remove_block(self, block: "Block"):
"The block you are trying to remove is not present in the circuit blocks.",
)

def return_last_block(self):
"""Return the last block in the circuit blocks."""
if len(self.block_list) == 0:
raise_error(
BlockingError,
"No blocks found in the circuit blocks.",
)
return self.block_list[-1]


def block_decomposition(circuit: Circuit, fuse: bool = True):
"""Decompose a circuit into blocks of gates acting on two qubits.
Expand Down
42 changes: 34 additions & 8 deletions src/qibo/transpiler/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,32 @@ def update(self, swap: tuple):
), self._circuit_logical.index(swap[1])
self._circuit_logical[idx_0], self._circuit_logical[idx_1] = swap[1], swap[0]

def undo(self, swap: tuple):
"""Undo the last swap.

If the last block is not a swap or does not match the swap to undo, an error is raised.
Method works in-place.

Args:
swap (tuple): tuple containing the logical qubits to be swapped.
"""
physical_swap = self.logical_to_physical(swap, index=True)
last_swap_block = self._routed_blocks.return_last_block()
if last_swap_block.gates[0].__class__ != gates.SWAP or sorted(
last_swap_block.qubits
) != sorted(physical_swap):
raise_error(
ConnectivityError,
"The last block does not match the swap to undo.",
)
self._routed_blocks.remove_block(last_swap_block)
self._swaps -= 1

idx_0, idx_1 = self._circuit_logical.index(
swap[0]
), self._circuit_logical.index(swap[1])
self._circuit_logical[idx_0], self._circuit_logical[idx_1] = swap[1], swap[0]
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks ok code-wise, the only thing that seems a bit weird to me is that the undo requires for you to pass the swap you wish to undo. I mean, since in principle you undo the last one, you wouldn't need to pass explicitely the swap, but just look for the last one added in a routed block, right? And, indeed, here swap is only used to check that the last swap you find is the one you expected.

Copy link
Member Author

Choose a reason for hiding this comment

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

I used swap to double-check if the undo function works correctly. I'll remove the checking step.

Also, could you take a look at the question I posted above?

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry for the delay. Double checking could always be useful, indeed, but, if you want to keep it, I wouldn't make it mandatory at least. In the sense that you could have the swap=None argument as optional. In any case, this check should be covered in the tests in principle.
Regarding the question above, instead, I had a look at it last week. Now, the best person to answer this is probably @Simone-Bordoni, but I believe he is currently on a leave. Thus, I will provide you with my guess, which however may be incorrect. By looking at the update function

def update(self, swap: tuple):

it seems that you update the circuit_logical mapping, not the physical one (physical_logical), meaning that in the layout dict, you do not swap the values, but rather the keys.
Therefore you start from:

{"q0": 0, "q1": 2, "q2": 1, "q3": 3}

after the (0, 2) swap you get:

{"q2": 0, "q1": 2, "q0": 1, "q3": 3}

and after (1, 2):

{"q1": 0, "q2": 2, "q0": 1, "q3": 3}

This means that you end up with circuit_logical = [1, 2, 0, 3], which is indeed checked in the test:

assert circuit_map._circuit_logical == [1, 2, 0, 3]

I hope this helped, assuming it is correct. In general, though, this small review of the transpiler code that I did to find the answer, made me think that probably, both, the naming scheme could be improved to make it clearer, and some further documentation should be added to the docstrings. Otherwise, everytime we go back to the code, we risk to waste time in re-understanding everything.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for your response. I noticed that while optimizing SABRE and a better naming scheme seems to be needed. I also found and implemented a more efficient way to manage the mapping. I think it would be best to wait until @Simone-Bordoni returns to discuss the new mapping scheme.

Copy link
Contributor

Choose a reason for hiding this comment

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

I am currently on leaves. If you want to implement a new mapping scheme you can do the PR and I will review it when I will be back.


def get_logical_qubits(self, block: Block):
"""Returns the current logical qubits where a block is acting on.

Expand Down Expand Up @@ -659,8 +685,7 @@ def __init__(
self.circuit = None
self._memory_map = None
self._final_measurements = None
self._temporary_added_swaps = 0
self._saved_circuit = None
self._temp_added_swaps = []
random.seed(seed)

def __call__(self, circuit: Circuit, initial_layout: dict):
Expand All @@ -674,7 +699,6 @@ def __call__(self, circuit: Circuit, initial_layout: dict):
(:class:`qibo.models.circuit.Circuit`, dict): routed circuit and final layout.
"""
self._preprocessing(circuit=circuit, initial_layout=initial_layout)
self._saved_circuit = deepcopy(self.circuit)
longest_path = np.max(self._dist_matrix)

while self._dag.number_of_nodes() != 0:
Expand All @@ -687,9 +711,12 @@ def __call__(self, circuit: Circuit, initial_layout: dict):
# If the number of added swaps is too high, the algorithm is stuck.
# Reset the circuit to the last saved state and make the nearest gate executable by manually adding SWAPs.
if (
self._temporary_added_swaps > self.swap_threshold * longest_path
len(self._temp_added_swaps) > self.swap_threshold * longest_path
): # threshold is arbitrary
self.circuit = deepcopy(self._saved_circuit)
while self._temp_added_swaps:
swap = self._temp_added_swaps.pop()
self.circuit.undo(swap)
self._temp_added_swaps = []
self._shortest_path_routing()

circuit_kwargs = circuit.init_kwargs
Expand Down Expand Up @@ -800,7 +827,7 @@ def _find_new_mapping(self):
for qubit in self.circuit.logical_to_physical(best_candidate, index=True):
self._delta_register[qubit] += self.delta
self.circuit.update(best_candidate)
self._temporary_added_swaps += 1
self._temp_added_swaps.append(best_candidate)

def _compute_cost(self, candidate: int):
"""Compute the cost associated to a possible SWAP candidate."""
Expand Down Expand Up @@ -897,8 +924,7 @@ def _execute_blocks(self, blocklist: list):
self._update_front_layer()
self._memory_map = []
self._delta_register = [1.0 for _ in self._delta_register]
self._temporary_added_swaps = 0
self._saved_circuit = deepcopy(self.circuit)
self._temp_added_swaps = []

def _shortest_path_routing(self):
"""Route a gate in the front layer using the shortest path. This method is executed when the standard SABRE fails to find an optimized solution.
Expand Down
24 changes: 24 additions & 0 deletions tests/test_transpiler_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,3 +367,27 @@ def test_block_on_qubits():
assert new_block.gates[2].qubits == (3,)
assert new_block.gates[3].qubits == (3, 2)
assert new_block.gates[4].qubits == (3,)


def test_return_last_block():
circ = Circuit(4)
circ.add(gates.CZ(0, 1))
circ.add(gates.CZ(1, 3))
circ.add(gates.CZ(1, 2))
circ.add(gates.CZ(2, 3))
circuit_blocks = CircuitBlocks(circ)
last_block = circuit_blocks.return_last_block()
assert_gates_equality(last_block.gates, [gates.CZ(2, 3)])

circuit_blocks.remove_block(last_block)
last_block_2 = circuit_blocks.return_last_block()
assert_gates_equality(last_block_2.gates, [gates.CZ(1, 2)])


def test_return_last_block_error():
circ = Circuit(4)
circuit_blocks = CircuitBlocks(circ)

# No blocks in the circuit
with pytest.raises(BlockingError):
last_block = circuit_blocks.return_last_block()
47 changes: 47 additions & 0 deletions tests/test_transpiler_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from qibo.models import Circuit
from qibo.quantum_info.random_ensembles import random_unitary
from qibo.transpiler._exceptions import ConnectivityError
from qibo.transpiler.blocks import Block
from qibo.transpiler.optimizer import Preprocessing
from qibo.transpiler.pipeline import (
assert_circuit_equivalence,
Expand Down Expand Up @@ -472,3 +473,49 @@ def test_star_router(nqubits, depth, middle_qubit, measurements, unitaries):
final_map=final_qubit_map,
initial_map=initial_layout,
)


def test_undo():
circ = Circuit(4)
initial_layout = {"q0": 0, "q1": 1, "q2": 2, "q3": 3}
circuit_map = CircuitMap(initial_layout=initial_layout, circuit=circ)

# Two SWAP gates are added
circuit_map.update((1, 2))
circuit_map.update((2, 3))
assert circuit_map._circuit_logical == [0, 3, 1, 2]
assert len(circuit_map._routed_blocks.block_list) == 2

# Undo the last SWAP gate
circuit_map.undo((2, 3))
assert circuit_map._circuit_logical == [0, 2, 1, 3]
assert circuit_map._swaps == 1
assert len(circuit_map._routed_blocks.block_list) == 1

# Undo the first SWAP gate
circuit_map.undo((1, 2))
assert circuit_map._circuit_logical == [0, 1, 2, 3]
assert circuit_map._swaps == 0
assert len(circuit_map._routed_blocks.block_list) == 0


def test_undo_error():
circ = Circuit(4)
initial_layout = {"q0": 0, "q1": 1, "q2": 2, "q3": 3}
circuit_map = CircuitMap(initial_layout=initial_layout, circuit=circ)

circuit_map.update((1, 2))
circuit_map.update((2, 3))

# The last block is a SWAP gate on qubits (2, 3)
with pytest.raises(ConnectivityError):
circuit_map.undo((1, 2))

circ_cz = Circuit(4)
circuit_map_cz = CircuitMap(initial_layout=initial_layout, circuit=circ_cz)
block = Block(qubits=(0, 1), gates=[gates.CZ(0, 1)])
circuit_map_cz._routed_blocks.add_block(block)

# The last block is a CZ gate
with pytest.raises(ConnectivityError):
circuit_map_cz.undo((0, 1))