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

Remove matmuleinsum backend #404

Merged
merged 21 commits into from
May 4, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
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
scarrazza marked this conversation as resolved.
Show resolved Hide resolved
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