Skip to content

Commit

Permalink
Merge pull request #404 from Quantum-TII/matmuleinsum
Browse files Browse the repository at this point in the history
Remove matmuleinsum backend
  • Loading branch information
scarrazza committed May 4, 2021
2 parents 8cdcdcd + fc9dffd commit 4e2ef6a
Show file tree
Hide file tree
Showing 29 changed files with 160 additions and 460 deletions.
2 changes: 1 addition & 1 deletion examples/reuploading_classifier/qlassifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def minimize(self, method='BFGS', options=None, compile=True):
from qibo import K
circuit = self.circuit(self.training_set[0])
for gate in circuit.queue:
if K.name not in {"tensorflow_defaulteinsum", "tensorflow_matmuleinsum"}:
if K.name != "tensorflow":
from qibo.config import raise_error
raise_error(RuntimeError,
'SGD VQE requires native Tensorflow '
Expand Down
7 changes: 2 additions & 5 deletions examples/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ def run_script(args, script_name="main.py"):
script_name (str): Name of the script file.
max_time (float): Time-out time in seconds.
"""
import qibo
qibo.set_backend("custom")
code = open(script_name, "r").read()
end = code.find("\nif __name__ ==")
code = code[:end] + "\n\nmain(**args)"
Expand Down Expand Up @@ -146,7 +144,7 @@ def test_benchmarks(nqubits, type):
header = ("import argparse\nimport os\nimport time"
"\nimport qibo\nimport circuits\n\n")
args = {"nqubits": nqubits, "type": type,
"backend": "custom", "precision": "double",
"backend": "qibotf", "precision": "double",
"device": None, "accelerators": None,
"nshots": None, "fuse": False, "compile": False,
"nlayers": None, "gate_type": None, "params": {},
Expand Down Expand Up @@ -257,7 +255,7 @@ def test_falqon(nqubits, delta_t, max_layers):
sys.path[-1] = path
os.chdir(path)
run_script(args)


@pytest.mark.parametrize("nqubits", [5, 6, 7])
def test_grover_example1(nqubits):
Expand Down Expand Up @@ -287,4 +285,3 @@ def test_grover_example3(nqubits, num_1):
sys.path[-1] = path
os.chdir(path)
run_script(args, script_name="example3.py")

58 changes: 27 additions & 31 deletions src/qibo/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,43 +11,41 @@ def __init__(self):

# check if numpy is installed
if self.check_availability("numpy"):
from qibo.backends.numpy import NumpyDefaultEinsumBackend, NumpyMatmulEinsumBackend
self.available_backends["numpy"] = NumpyDefaultEinsumBackend
self.available_backends["numpy_defaulteinsum"] = NumpyDefaultEinsumBackend
self.available_backends["numpy_matmuleinsum"] = NumpyMatmulEinsumBackend
else: # pragma: no cover
raise_error(ModuleNotFoundError, "Numpy is not installed.")
from qibo.backends.numpy import NumpyBackend
self.available_backends["numpy"] = NumpyBackend
else: # pragma: no cover
raise_error(ModuleNotFoundError, "Numpy is not installed. "
"Please install it using "
"`pip install numpy`.")

# check if tensorflow is installed and use it as default backend.
if self.check_availability("tensorflow"):
from qibo.backends.tensorflow import TensorflowDefaultEinsumBackend, TensorflowMatmulEinsumBackend
os.environ["TF_CPP_MIN_LOG_LEVEL"] = str(config.LOG_LEVEL)
import tensorflow as tf # pylint: disable=E0401
self.available_backends["defaulteinsum"] = TensorflowDefaultEinsumBackend
self.available_backends["matmuleinsum"] = TensorflowMatmulEinsumBackend
self.available_backends["tensorflow_defaulteinsum"] = TensorflowDefaultEinsumBackend
self.available_backends["tensorflow_matmuleinsum"] = TensorflowMatmulEinsumBackend
import tensorflow as tf # pylint: disable=E0401
from qibo.backends.tensorflow import TensorflowBackend
self.available_backends["tensorflow"] = TensorflowBackend
active_backend = "tensorflow"
if self.check_availability("qibotf"):
from qibo.backends.tensorflow import TensorflowCustomBackend
self.available_backends["custom"] = TensorflowCustomBackend
self.available_backends["tensorflow"] = TensorflowCustomBackend
self.available_backends["qibotf"] = TensorflowCustomBackend
active_backend = "qibotf"
else: # pragma: no cover
log.warning("Einsum will be used to apply gates with Tensorflow. "
"Removing custom operators from available backends.")
self.available_backends["tensorflow"] = TensorflowDefaultEinsumBackend
active_backend = "tensorflow"
log.warning("qibotf library was not found. `tf.einsum` will be "
"used to apply gates. In order to install Qibo's "
"high performance custom operators please use "
"`pip install qibotf`.")
else: # pragma: no cover
# case not tested because CI has tf installed
log.warning("Tensorflow is not installed. Falling back to numpy. "
"Numpy does not support Qibo custom operators and GPU. "
"Einsum will be used to apply gates on CPU.")
# use numpy for defaulteinsum and matmuleinsum backends
self.available_backends["defaulteinsum"] = NumpyDefaultEinsumBackend
self.available_backends["matmuleinsum"] = NumpyMatmulEinsumBackend
log.warning("Tensorflow is not installed, falling back to numpy. "
"Numpy backend uses `np.einsum` and supports CPU only. "
"To enable GPU acceleration please install Tensorflow "
"with `pip install tensorflow`. To install the "
"optimized Qibo custom operators please use "
"`pip install qibotf` after installing Tensorflow.")

self.constructed_backends = {}
self._active_backend = None
self.qnp = self.construct_backend("numpy_defaulteinsum")
self.qnp = self.construct_backend("numpy")
# Create the default active backend
if "QIBO_BACKEND" in os.environ: # pragma: no cover
self.active_backend = os.environ.get("QIBO_BACKEND")
Expand Down Expand Up @@ -117,15 +115,13 @@ def check_availability(module_name):
numpy_matrices = K.qnp.matrices


def set_backend(backend="custom"):
def set_backend(backend="qibotf"):
"""Sets backend used for mathematical operations and applying gates.
The following backends are available:
'custom': Tensorflow backend with custom operators for applying gates,
'defaulteinsum': Tensorflow backend that applies gates using ``tf.einsum``,
'matmuleinsum': Tensorflow backend that applies gates using ``tf.matmul``,
'numpy_defaulteinsum': Numpy backend that applies gates using ``np.einsum``,
'numpy_matmuleinsum': Numpy backend that applies gates using ``np.matmul``,
'qibotf': Tensorflow backend with custom operators for applying gates,
'tensorflow': Tensorflow backend that applies gates using ``tf.einsum``,
'numpy': Numpy backend that applies gates using ``np.einsum``.
Args:
backend (str): A backend from the above options.
Expand Down
1 change: 1 addition & 0 deletions src/qibo/backends/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def __init__(self):
self.gpu_devices = []
self.default_device = []

self.op = None
self._matrices = None
self.numeric_types = None
self.tensor_types = None
Expand Down
179 changes: 31 additions & 148 deletions src/qibo/backends/einsum.py → src/qibo/backends/einsum_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,50 @@
Gates use ``einsum`` to apply gates to state vectors. The einsum string that
specifies the contraction indices is created and cached when a gate is created
so that it is not recalculated every time the gate is called on a state. This
functionality is implemented in :class:`qibo.core.einsum.DefaultEinsum`.
Due to an `issue <https://github.com/tensorflow/tensorflow/issues/37307>`_
with automatic differentiation and complex numbers in ``einsum``, we have
implemented an alternative calculation backend based on ``matmul`` in
:class:`qibo.core.einsum.MatmulEinsum`. Note that this is slower than
the default ``einsum`` on GPU but slightly faster on CPU.
The user can switch the default einsum used by the gates by changing the
``einsum`` variable in `config.py`. It is recommended to use the default unless
automatic differentiation is required. For the latter case, we refer to our
examples.
functionality is implemented in :class:`qibo.backends.numpy.NumpyBackend`.
"""
from abc import ABC, abstractmethod
from qibo.config import raise_error
from typing import Dict, List, Optional, Sequence


class BaseCache:
"""Base cache object for einsum backends defined in `einsum.py`.
class EinsumCache:
"""Cache object required to apply gates using ``einsum``.
``circuit.calculation_cache`` is an object of this class.
``self.vector`` returns the cache elements required for state vector
calculations.
``self.left``, ``self.right``, ``self.left0`` and ``self.right0`` return the cache
elements required for density matrix calculations.
Args:
qubits (list): List with the qubit indices that the gate is applied to.
nqubits (int): Total number of qubits in the circuit / state vector.
ncontrol (int): Number of control qubits for `controlled_by` gates.
"""
from qibo.config import EINSUM_CHARS as _chars

def __init__(self, nqubits, ncontrol: Optional[int] = None):
def __init__(self, qubits, nqubits, ncontrol=None):
self.nqubits = nqubits
self.ncontrol = ncontrol
if nqubits + len(qubits) > len(self._chars): # pragma: no cover
raise_error(NotImplementedError, "Not enough einsum characters.")

input_state = list(self._chars[:nqubits])
output_state = input_state[:]
gate_chars = list(self._chars[nqubits : nqubits + len(qubits)])

for i, q in enumerate(qubits):
gate_chars.append(input_state[q])
output_state[q] = gate_chars[i]

self.input = "".join(input_state)
self.output = "".join(output_state)
self.gate = "".join(gate_chars)
self.rest = self._chars[nqubits + len(qubits):]

# Cache for state vectors
self._vector = None
self._vector = f"{self.input},{self.gate}->{self.output}"

# Cache for density matrices
self._left = None
self._right = None
Expand Down Expand Up @@ -75,56 +85,8 @@ def right0(self):
self._calculate_density_matrix_controlled()
return self._right0

def cast_shapes(self, cast_func):
pass

@abstractmethod
def _calculate_density_matrix(self): # pragma: no cover
"""Calculates `left` and `right` elements."""
raise_error(NotImplementedError)

@abstractmethod
def _calculate_density_matrix_controlled(self): # pragma: no cover
"""Calculates `left0` and `right0` elements."""
raise_error(NotImplementedError)


class DefaultEinsumCache(BaseCache):
"""Cache object required by the :class:`qibo.core.einsum.DefaultEinsum` backend.
The ``vector``, ``left``, ``right``, ``left0``, ``right0`` properties are
strings that hold the einsum indices.
Args:
qubits (list): List with the qubit indices that the gate is applied to.
nqubits (int): Total number of qubits in the circuit / state vector.
ncontrol (int): Number of control qubits for `controlled_by` gates.
"""
from qibo.config import EINSUM_CHARS as _chars

def __init__(self, qubits: Sequence[int], nqubits: int,
ncontrol: Optional[int] = None):
super(DefaultEinsumCache, self).__init__(nqubits, ncontrol)

if nqubits + len(qubits) > len(self._chars): # pragma: no cover
raise_error(NotImplementedError, "Not enough einsum characters.")

input_state = list(self._chars[:nqubits])
output_state = input_state[:]
gate_chars = list(self._chars[nqubits : nqubits + len(qubits)])

for i, q in enumerate(qubits):
gate_chars.append(input_state[q])
output_state[q] = gate_chars[i]

self.input = "".join(input_state)
self.output = "".join(output_state)
self.gate = "".join(gate_chars)
self.rest = self._chars[nqubits + len(qubits):]

self._vector = f"{self.input},{self.gate}->{self.output}"

def _calculate_density_matrix(self):
"""Calculates `left` and `right` elements."""
if self.nqubits > len(self.rest): # pragma: no cover
raise_error(NotImplementedError, "Not enough einsum characters.")

Expand All @@ -133,93 +95,14 @@ def _calculate_density_matrix(self):
self._right = f"{rest}{self.input},{self.gate}->{rest}{self.output}"

def _calculate_density_matrix_controlled(self):
"""Calculates `left0` and `right0` elements."""
if self.nqubits + 1 > len(self.rest): # pragma: no cover
raise_error(NotImplementedError, "Not enough einsum characters.")
rest, c = self.rest[:self.nqubits], self.rest[self.nqubits]
self._left0 = f"{c}{self.input}{rest},{self.gate}->{c}{self.output}{rest}"
self._right0 = f"{c}{rest}{self.input},{self.gate}->{c}{rest}{self.output}"


class MatmulEinsumCache(BaseCache):
"""Cache object required by the :class:`qibo.core.einsum.MatmulEinsum` backend.
The ``vector``, ``left``, ``right``, ``left0``, ``right0`` properties are dictionaries
that hold the following keys:
* ``ids``: Indices for the transposition before matmul.
* ``inverse_ids``: Indices for the transposition after matmul.
* ``shapes``: Tuple with four shapes that are required for ``tf.reshape`` in the ``__call__`` method of :class:`qibo.core.einsum.MatmulEinsum`.
Args:
qubits (list): List with the qubit indices that the gate is applied to.
nqubits (int): Total number of qubits in the circuit / state vector.
ncontrol (int): Number of control qubits for `controlled_by` gates.
"""

def __init__(self, qubits: Sequence[int], nqubits: int,
ncontrol: Optional[int] = None):
super(MatmulEinsumCache, self).__init__(nqubits, ncontrol)
self.ntargets = len(qubits)
self.nrest = nqubits - self.ntargets
self.nstates = 2 ** nqubits

last_index = 0
target_ids, rest_ids = {}, []
self.shape = []
for q in sorted(qubits):
if q > last_index:
self.shape.append(2 ** (q - last_index))
rest_ids.append(len(self.shape) - 1)
self.shape.append(2)
target_ids[q] = len(self.shape) - 1
last_index = q + 1
if last_index < self.nqubits:
self.shape.append(2 ** (self.nqubits - last_index))
rest_ids.append(len(self.shape) - 1)

self.ids = [target_ids[q] for q in qubits] + rest_ids
self.transposed_shape = []
self.inverse_ids = len(self.ids) * [0]
for i, r in enumerate(self.ids):
self.inverse_ids[r] = i
self.transposed_shape.append(self.shape[r])

self.shape = tuple(self.shape)
self.transposed_shape = tuple(self.transposed_shape)
self._vector = {"ids": self.ids, "inverse_ids": self.inverse_ids,
"shapes": (self.shape,
(2 ** self.ntargets, 2 ** self.nrest),
self.transposed_shape,
self.nqubits * (2,)),
"conjugate": False}

def cast_shapes(self, cast_func):
for attr in ["_vector", "_left", "_right", "_left0", "_right0"]:
d = getattr(self, attr)
if d is not None:
d["shapes"] = tuple(cast_func(s) for s in d["shapes"])

def _calculate_density_matrix(self):
self._left = {"ids": self.ids + [len(self.ids)],
"inverse_ids": self.inverse_ids + [len(self.ids)],
"shapes": (self.shape + (self.nstates,),
(2 ** self.ntargets, (2 ** self.nrest) * self.nstates),
self.transposed_shape + (self.nstates,),
2 * self.nqubits * (2,)),
"conjugate": False}

self._right = dict(self._left)
self._right["inverse_ids"] = [len(self.ids)] + self.inverse_ids
self._right["conjugate"] = True

def _calculate_density_matrix_controlled(self): # pragma: no cover
# `MatmulEinsum` falls back to `DefaultEinsum` if `controlled_by`
# and density matrices are used simultaneously due to an error
raise_error(NotImplementedError,
"MatmulEinsum backend is not implemented when multicontrol "
"gates are used on density matrices.")


class ControlCache:
"""Helper tools for `controlled_by` gates.
Expand Down Expand Up @@ -285,7 +168,7 @@ def calculate_dm(self):
self._reverse_dm = self.revert(self._order_dm)

@staticmethod
def revert(transpose_order) -> List[int]:
def revert(transpose_order):
reverse_order = len(transpose_order) * [0]
for i, r in enumerate(transpose_order):
reverse_order[r] = i
Expand Down
Loading

0 comments on commit 4e2ef6a

Please sign in to comment.