Skip to content

Commit

Permalink
Merge pull request #461 from qiboteam/fusion
Browse files Browse the repository at this point in the history
Updating gate fusion
  • Loading branch information
scarrazza authored Sep 22, 2021
2 parents f577a91 + c3e7997 commit fc1eb31
Show file tree
Hide file tree
Showing 15 changed files with 338 additions and 514 deletions.
11 changes: 8 additions & 3 deletions src/qibo/abstractions/abstract_gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,9 +432,7 @@ def parameters(self, x):
# ``circuit.set_parameters`` method works properly.
# pylint: disable=E1101
if isinstance(self, BaseBackendGate):
self._matrix = None
self._native_op_matrix = None
self._custom_op_matrix = None
self._reset_unitary()
for devgate in self.device_gates:
devgate.parameters = x

Expand Down Expand Up @@ -519,6 +517,13 @@ def _construct_unitary(self): # pragma: no cover
"""Constructs the gate's unitary matrix."""
return raise_error(NotImplementedError)

def _reset_unitary(self):
"""Resets the gate matrices back to ``None``.
Useful when the gate matrix need to be recalculated.
"""
self._matrix = None

@property
@abstractmethod
def cache(self): # pragma: no cover
Expand Down
49 changes: 27 additions & 22 deletions src/qibo/abstractions/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ class _ParametrizedGates(list):
total number of parameters.
"""

def __init__(self):
super(_ParametrizedGates, self).__init__(self)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set = set()
self.nparams = 0

def append(self, gate: gates.ParametrizedGate):
super(_ParametrizedGates, self).append(gate)
def append(self, gate):
super().append(gate)
self.set.add(gate)
self.nparams += gate.nparams

Expand Down Expand Up @@ -94,8 +94,6 @@ def __init__(self, nqubits):
self.measurement_gate = None
self.measurement_gate_result = None

self.fusion_groups = []

self._final_state = None
self.density_matrix = False
self.repeated_execution = False
Expand Down Expand Up @@ -180,6 +178,16 @@ def on_qubits(self, *q):
for gate in self.queue:
yield gate._on_qubits(*q)

def _shallow_copy(self):
"""Helper method for :meth:`qibo.abstractions.circuit.AbstractCircuit.copy`
and :meth:`qibo.core.circuit.Circuit.fuse`."""
new_circuit = self.__class__(**self.init_kwargs)
new_circuit.parametrized_gates = _ParametrizedGates(self.parametrized_gates)
new_circuit.trainable_gates = _ParametrizedGates(self.trainable_gates)
new_circuit.measurement_gate = self.measurement_gate
new_circuit.measurement_tuples = dict(self.measurement_tuples)
return new_circuit

def copy(self, deep: bool = False):
"""Creates a copy of the current ``circuit`` as a new ``Circuit`` model.
Expand All @@ -192,27 +200,25 @@ def copy(self, deep: bool = False):
The copied circuit object.
"""
import copy
new_circuit = self.__class__(**self.init_kwargs)
if deep:
new_circuit = self.__class__(**self.init_kwargs)
for gate in self.queue:
if isinstance(gate, gates.FusedGate): # pragma: no cover
# impractical case
raise_error(NotImplementedError, "Cannot create deep copy "
"of fused circuit.")

new_gate = copy.copy(gate)
new_circuit.queue.append(new_gate)
if isinstance(gate, gates.ParametrizedGate):
new_circuit.parametrized_gates.append(new_gate)
if gate.trainable:
new_circuit.trainable_gates.append(new_gate)
new_circuit.measurement_gate = copy.copy(self.measurement_gate)
if self.fusion_groups: # pragma: no cover
# impractical case
raise_error(NotImplementedError, "Cannot create deep copy of fused "
"circuit.")
new_circuit.measurement_tuples = dict(self.measurement_tuples)
else:
new_circuit = self._shallow_copy()
new_circuit.queue = copy.copy(self.queue)
new_circuit.parametrized_gates = list(self.parametrized_gates)
new_circuit.trainable_gates = list(self.trainable_gates)
new_circuit.measurement_gate = self.measurement_gate
new_circuit.fusion_groups = list(self.fusion_groups)
new_circuit.measurement_tuples = dict(self.measurement_tuples)
return new_circuit

def invert(self):
Expand Down Expand Up @@ -517,9 +523,6 @@ def _set_parameters_list(self, parameters: List, n: int):
"the circuit contains {} parametrized gates."
"".format(n, len(self.trainable_gates)))

for fusion_group in self.fusion_groups:
fusion_group.update()

def set_parameters(self, parameters):
"""Updates the parameters of the circuit's parametrized gates.
Expand Down Expand Up @@ -569,9 +572,6 @@ def set_parameters(self, parameters):
elif isinstance(parameters, self.param_tensor_types):
self._set_parameters_list(parameters, int(parameters.shape[0]))
elif isinstance(parameters, dict):
if self.fusion_groups:
raise_error(TypeError, "Cannot accept new parameters as dictionary "
"for fused circuits. Use list, tuple or array.")
diff = set(parameters.keys()) - self.trainable_gates.set
if diff:
raise_error(KeyError, "Dictionary contains gates {} which are "
Expand All @@ -582,6 +582,11 @@ def set_parameters(self, parameters):
else:
raise_error(TypeError, "Invalid type of parameters {}."
"".format(type(parameters)))
# Reset ``FusedGate`` matrices so that they are recalculated with the
# updated parameters.
for gate in self.queue:
if isinstance(gate, gates.FusedGate):
gate._reset_unitary()

def get_parameters(self, format: str = "list",
include_not_trainable: bool = False
Expand Down
38 changes: 38 additions & 0 deletions src/qibo/abstractions/gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -1550,3 +1550,41 @@ def __init__(self, q, t1, t2, time, excited_population=0, seed=None):
seed=seed)
# this case can only be applied to density matrices
self.density_matrix = True


class FusedGate(Gate):
"""Collection of gates that will be fused and applied as single gate during simulation.
This gate is constructed automatically by :meth:`qibo.core.circuits.Circuit.fuse`
and should not be used by user.
:class:`qibo.abstractions.gates.FusedGate` works with arbitrary number of
target qubits however the backend implementation
:class:`qibo.core.gates.FusedGate` assumes two target qubits.
"""

def __init__(self, *q):
super().__init__()
self.name = "fused"
self.target_qubits = tuple(q)
self.init_args = list(q)
self.qubit_set = set(q)
self.gates = []

def add(self, gate):
if not set(gate.qubits).issubset(self.qubit_set):
raise_error(ValueError, "Cannot add gate that targets {} "
"in fused gate acting on {}."
"".format(gate.qubits, self.qubits))
if isinstance(gate, self.__class__):
self.gates.extend(gate.gates)
else:
self.gates.append(gate)

def __iter__(self):
return iter(self.gates)

def _dagger(self):
dagger = self.__class__(*self.init_args)
for gate in self.gates[::-1]:
dagger.add(gate.dagger())
return dagger
155 changes: 116 additions & 39 deletions src/qibo/core/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ class Circuit(circuit.AbstractCircuit):
Args:
nqubits (int): Total number of qubits in the circuit.
"""
from qibo.core import fusion

def __init__(self, nqubits):
super(Circuit, self).__init__(nqubits)
Expand All @@ -47,37 +46,13 @@ def _add_layer(self, gate):
self._set_nqubits(gate.additional_unitary)
self.queue.append(gate.additional_unitary)

def _fuse_copy(self):
"""Helper method for ``circuit.fuse``.
For standard (non-distributed) circuits this creates a copy of the
circuit with deep-copying the parametrized gates only.
For distributed circuits a fully deep copy should be created.
"""
import copy
from qibo.abstractions.abstract_gates import ParametrizedGate
new_circuit = self.__class__(**self.init_kwargs)
for gate in self.queue:
if isinstance(gate, ParametrizedGate):
if gate.trainable:
new_gate = copy.copy(gate)
new_circuit.queue.append(new_gate)
new_circuit.parametrized_gates.append(new_gate)
new_circuit.trainable_gates.append(new_gate)
else:
new_circuit.queue.append(gate)
new_circuit.parametrized_gates.append(gate)
else:
new_circuit.queue.append(gate)
new_circuit.measurement_gate = copy.copy(self.measurement_gate)
new_circuit.measurement_tuples = dict(self.measurement_tuples)
return new_circuit

def fuse(self):
"""Creates an equivalent ``Circuit`` with gates fused up to two-qubits.
"""Creates an equivalent :class:`qibo.core.circuit.Circuit` with gates fused up to two-qubits.
The current fusion algorithm with create up to two-qubit fused gates.
Returns:
The equivalent ``Circuit`` object where the gates are fused.
The equivalent :class:`qibo.core.circuit.Circuit` object where the gates are fused.
Example:
::
Expand All @@ -89,18 +64,120 @@ def fuse(self):
c.add([gates.Y(0), gates.Y(1)])
# create circuit with fused gates
fused_c = c.fuse()
# now ``fused_c`` contains only one ``gates.Unitary`` gate
# that is equivalent to applying the five gates of the original
# circuit.
# now ``fused_c`` contains a single gate that is equivalent
# to applying the five gates of the original circuit.
"""
from qibo import gates
from qibo.abstractions.circuit import _Queue
new_circuit = self._fuse_copy()
new_circuit.fusion_groups = self.fusion.FusionGroup.from_queue(
new_circuit.queue)
new_circuit.queue = _Queue(self.nqubits)
for group in new_circuit.fusion_groups:
for gate in group.gates:
new_circuit.queue.append(gate)
from qibo.abstractions.abstract_gates import SpecialGate

class FusedQueue(_Queue):
"""Helper queue implementation that checks if a gate already exists
in queue to avoid re-appending it.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set = set()

def append(self, gate):
"""Appends a gate in queue only if it is not already in."""
# Use a ``set`` instead of the original ``list`` to check if
# the gate already exists in queue as lookup is typically
# more efficient for sets
# (although actual performance difference is probably negligible)
if gate not in self.set:
self.set.add(gate)
super().append(gate)

# new circuit queue that will hold the fused gates
fused_queue = FusedQueue(self.nqubits)
# dictionary that maps each qubit id (int) to the corresponding
# active ``FusedGate`` that is part of
fused_gates = collections.OrderedDict()
# use ``OrderedDict`` so that the original gate order is not changed
for gate in self.queue:
qubits = gate.qubits
if len(qubits) == 1:
# add one-qubit gates to the active ``FusedGate`` of this qubit
# or create a new one if it does not exist
q = qubits[0]
if q not in fused_gates:
fused_gates[q] = gates.FusedGate(q)
fused_gates.get(q).add(gate)

elif len(qubits) == 2:
# fuse two-qubit gates
q0, q1 = tuple(sorted(qubits))
if (q0 in fused_gates and q1 in fused_gates and
fused_gates.get(q0) == fused_gates.get(q1)):
# if the target qubit pair is compatible with the active
# ``FusedGate`` of both qubits then add it to the ``FusedGate``
fused_gates.get(q0).add(gate)
else:
# otherwise we need to create a new ``FusedGate`` and
# update the active gates of both target qubits
fgate = gates.FusedGate(q0, q1)
if q0 in fused_gates:
# first qubit has existing active gate
ogate = fused_gates.pop(q0)
if len(ogate.target_qubits) == 1:
# existing active gate is one-qubit so we just add
# it to the new ``FusedGate``
fgate.add(ogate)
else:
# existing active gate is two-qubit so we need to
# add it to the new queue
fused_queue.append(ogate)
if q1 in fused_gates:
# second qubit has existing active gate
ogate = fused_gates.pop(q1)
if len(ogate.target_qubits) == 1:
# existing active gate is one-qubit so we just add
# it to the new ``FusedGate``
fgate.add(ogate)
else:
# existing active gate is two-qubit so we need to
# add it to the new queue
fused_queue.append(ogate)
# add the two-qubit gate to the newly created ``FusedGate``
# and update the active ``FusedGate``s of both target qubits
fgate.add(gate)
fused_gates[q0], fused_gates[q1] = fgate, fgate

elif isinstance(gate, SpecialGate):
# ``SpecialGate``s act on all qubits (like a barrier) so we
# so we need to temporarily stop the fusion, add all active
# gates in the new queue and restart fusion after the barrier
for g in fused_gates.values():
fused_queue.append(g)
fused_gates = collections.OrderedDict()
fused_queue.append(gate)

else:
# gate has more than two target qubits so it cannot be included
# in the ``FusedGate``s which support up to two qubits.
# Therefore we deactivate the ``FusedGate``s of all target qubits
for q in qubits:
if q in fused_gates:
fused_queue.append(fused_gates.pop(q))
fused_queue.append(gate)

for gate in fused_gates.values():
# add remaining active ``FusedGate``s in the new queue
fused_queue.append(gate)

queue = _Queue(self.nqubits)
for gate in fused_queue:
if isinstance(gate, gates.FusedGate) and len(gate.gates) == 1:
# replace ``FusedGate``s that contain only one gate by this
# gate for efficiency
gate = gate.gates[0]
queue.append(gate)

# create a circuit and assign the new queue
new_circuit = self._shallow_copy()
new_circuit.queue = queue
return new_circuit

def _eager_execute(self, state):
Expand Down
9 changes: 2 additions & 7 deletions src/qibo/core/distcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,9 @@ def copy(self, deep: bool = True):
"circuits because they modify gate objects.")
return super().copy(deep)

def _fuse_copy(self):
return self.copy(deep=True)

def fuse(self):
if self.queues.queues:
raise_error(RuntimeError, "Cannot fuse distributed circuit after "
"its first execution.")
return super().fuse()
raise_error(NotImplementedError, "Fusion is not implemented for "
"distributed circuits.")

def with_noise(self, noise_map, measurement_noise=None):
raise_error(NotImplementedError, "Distributed circuit does not support "
Expand Down
Loading

0 comments on commit fc1eb31

Please sign in to comment.