Skip to content

Commit 60a1ac0

Browse files
authored
Merge pull request #149 from Quantum-TII/devices
Improved memory errors and device switcher
2 parents b89170a + 5bf149f commit 60a1ac0

13 files changed

+234
-114
lines changed

doc/source/examples.rst

+17-2
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,28 @@ one can use:
125125
# execute circuit on CPU with default initial state |000...0>.
126126
final_state = c()
127127
128-
Alternatively, running the command ``CUDA_VISIBLE_DEVICES="-1"`` in a terminal
128+
or switch the default QIBO device using ``qibo.set_device`` as:
129+
130+
.. code-block:: python
131+
132+
import qibo
133+
qibo.set_device("/CPU:0")
134+
final_state = c() # circuit will now be executed on CPU
135+
136+
The syntax of device names follows the pattern ``'/{device type}:{device number}'``
137+
where device type can be CPU or GPU and the device number is an integer that
138+
distinguishes multiple devices of the same type starting from 0. For more details
139+
we refer to `Tensorflow's tutorial <https://www.tensorflow.org/guide/gpu#manual_device_placement>`_
140+
on manual device placement.
141+
Alternatively, running the command ``CUDA_VISIBLE_DEVICES=""`` in a terminal
129142
hides GPUs from tensorflow. As a result, any program executed from the same
130143
terminal will run on CPU even if ``tf.device`` is not used.
131144

132145
GPUs provide much faster execution compared to CPU but have limited memory.
133146
A standard 12-16GB GPU can simulate up to 30 qubits with single-precision
134-
or 29 qubits with double-precision when QIBO's default gates are used.
147+
or 29 qubits with double-precision when QIBO's default gates are used. If the
148+
used device runs out of memory during a circuit execution an error will be
149+
raised prompting the user to switch the default device using ``qibo.set_device``.
135150

136151
QIBO supports distributed circuit execution on multiple GPUs. This feature can
137152
be used as follows:

src/qibo/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
__version__ = "0.0.1b2"
2-
from qibo.config import set_precision, set_backend, matrices, K
2+
from qibo.config import set_precision, set_backend, set_device, matrices, K
33
from qibo import callbacks
44
from qibo import models
55
from qibo import gates

src/qibo/base/circuit.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -297,8 +297,8 @@ def _set_nqubits(self, gate: gates.Gate):
297297
298298
Helper method for ``circuit.add(gate)``.
299299
"""
300-
if gate._nqubits is None:
301-
gate.nqubits = self.nqubits
300+
if gate._nqubits is None: # pragma: no cover
301+
raise NotImplementedError
302302
elif gate.nqubits != self.nqubits:
303303
raise ValueError("Attempting to add gate with {} total qubits to "
304304
"a circuit with {} qubits."
@@ -415,7 +415,7 @@ def summary(self) -> str:
415415
return "\n".join(logs)
416416

417417
@property
418-
def final_state(self):
418+
def final_state(self): # pragma: no cover
419419
"""Returns the final state after full simulation of the circuit.
420420
421421
If the circuit is executed more than once, only the last final state
@@ -424,11 +424,11 @@ def final_state(self):
424424
raise NotImplementedError
425425

426426
@abstractmethod
427-
def execute(self, *args):
427+
def execute(self, *args): # pragma: no cover
428428
"""Executes the circuit. Exact implementation depends on the backend."""
429429
raise NotImplementedError
430430

431-
def __call__(self, *args):
431+
def __call__(self, *args): # pragma: no cover
432432
"""Equivalent to ``circuit.execute``."""
433433
return self.execute(*args)
434434

src/qibo/base/gates.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,16 @@ def nqubits(self, n: int):
139139
"set to {}.".format(self._nqubits))
140140
self._nqubits = n
141141
self._nstates = 2**n
142+
self._prepare()
143+
144+
def _prepare(self): # pragma: no cover
145+
"""Prepares the gate for application to state vectors.
146+
147+
Called automatically by the ``nqubits`` setter.
148+
Calculates the ``matrix`` required to apply the gate to state vectors.
149+
This is not necessarily the same as the unitary matrix of the gate.
150+
"""
151+
raise NotImplementedError
142152

143153
def commutes(self, gate: "Gate") -> bool:
144154
"""Checks if two gates commute.
@@ -196,7 +206,7 @@ def decompose(self, *free) -> List["Gate"]:
196206
# original gate
197207
return [self.__class__(*self._init_args, **self._init_kwargs)]
198208

199-
def __call__(self, state, is_density_matrix):
209+
def __call__(self, state, is_density_matrix): # pragma: no cover
200210
"""Acts with the gate on a given state vector:
201211
202212
Args:
@@ -308,7 +318,7 @@ def decompose(self, *free: int, use_toffolis: bool = True) -> List[Gate]:
308318

309319
decomp_gates = [*part1, *part2]
310320

311-
else:
321+
else: # pragma: no cover
312322
raise NotImplementedError("X decomposition is not implemented for "
313323
"zero free qubits.")
314324

src/qibo/config.py

+42-12
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# Choose the least significant qubit
1111
LEAST_SIGNIFICANT_QUBIT = 0
1212

13-
if LEAST_SIGNIFICANT_QUBIT != 0:
13+
if LEAST_SIGNIFICANT_QUBIT != 0: # pragma: no cover
1414
raise NotImplementedError("The least significant qubit should be 0.")
1515

1616
# Load backend specifics
@@ -39,16 +39,22 @@
3939
# Gate backends
4040
BACKEND = {'GATES': 'custom', 'EINSUM': None}
4141

42-
# Set memory cut-off for using GPU when sampling
43-
GPU_MEASUREMENT_CUTOFF = 1300000000
44-
45-
# Find available CPUs as they may be needed for sampling
46-
_available_cpus = tf.config.list_logical_devices("CPU")
47-
if _available_cpus:
48-
CPU_NAME = _available_cpus[0].name
49-
else:
50-
CPU_NAME = None
51-
42+
# Set devices recognized by tensorflow
43+
DEVICES = {
44+
'CPU': tf.config.list_logical_devices("CPU"),
45+
'GPU': tf.config.list_logical_devices("GPU")
46+
}
47+
# set default device to GPU if it exists
48+
if DEVICES['GPU']: # pragma: no cover
49+
DEVICES['DEFAULT'] = DEVICES['GPU'][0].name
50+
elif DEVICES['CPU']:
51+
DEVICES['DEFAULT'] = DEVICES['CPU'][0].name
52+
else: # pragma: no cover
53+
raise RuntimeError("Unable to find Tensorflow devices.")
54+
55+
# Define numpy and tensorflow matrices
56+
# numpy matrices are exposed to user via ``from qibo import matrices``
57+
# tensorflow matrices are used by native gates (``/tensorflow/gates.py``)
5258
from qibo.tensorflow import matrices as _matrices
5359
matrices = _matrices.NumpyMatrices()
5460
tfmatrices = _matrices.TensorflowMatrices()
@@ -96,5 +102,29 @@ def set_precision(dtype='double'):
96102
matrices.allocate_matrices()
97103
tfmatrices.allocate_matrices()
98104

99-
else:
105+
106+
def set_device(device_name: str):
107+
"""Set default execution device.
108+
109+
Args:
110+
device_name (str): Device name. Should follow the pattern
111+
'/{device type}:{device number}' where device type is one of
112+
CPU or GPU.
113+
"""
114+
parts = device_name[1:].split(":")
115+
if device_name[0] != "/" or len(parts) != 2:
116+
raise ValueError("Device name should follow the pattern: "
117+
"/{device type}:{device number}.")
118+
device_type, device_number = parts[0], int(parts[1])
119+
if device_type not in {"CPU", "GPU"}:
120+
raise ValueError(f"Unknown device type {device_type}.")
121+
if device_number >= len(DEVICES[device_type]):
122+
raise ValueError(f"Device {device_name} does not exist.")
123+
124+
DEVICES['DEFAULT'] = device_name
125+
with tf.device(device_name):
126+
tfmatrices.allocate_matrices()
127+
128+
129+
else: # pragma: no cover
100130
raise NotImplementedError("Only Tensorflow backend is implemented.")

src/qibo/tensorflow/cgates.py

+19-24
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import numpy as np
55
import tensorflow as tf
66
from qibo.base import gates as base_gates
7-
from qibo.config import BACKEND, DTYPES, GPU_MEASUREMENT_CUTOFF, CPU_NAME
7+
from qibo.config import BACKEND, DTYPES, DEVICES
88
from qibo.tensorflow import custom_operators as op
99
from typing import Dict, List, Optional, Sequence, Tuple
1010

@@ -36,6 +36,15 @@ def construct_unitary(*args) -> tf.Tensor:
3636
"""
3737
raise NotImplementedError
3838

39+
def _prepare(self):
40+
"""Prepares the gate for application to state vectors.
41+
42+
Called automatically by the ``nqubits`` setter.
43+
Calculates the ``matrix`` required to apply the gate to state vectors.
44+
This is not necessarily the same as the unitary matrix of the gate.
45+
"""
46+
pass
47+
3948
def __call__(self, state: tf.Tensor, is_density_matrix: bool = False
4049
) -> tf.Tensor:
4150
"""Implements the `Gate` on a given state.
@@ -54,18 +63,7 @@ def __init__(self):
5463
super(MatrixGate, self).__init__()
5564
self.matrix = None
5665

57-
@base_gates.Gate.nqubits.setter
58-
def nqubits(self, n: int):
59-
base_gates.Gate.nqubits.fset(self, n) # pylint: disable=no-member
60-
self._prepare()
61-
62-
def _prepare(self):
63-
"""Prepares the gate for application to state vectors.
64-
65-
Called automatically by the ``nqubits`` setter.
66-
Calculates the ``matrix`` required to apply the gate to state vectors.
67-
This is not necessarily the same as the unitary matrix of the gate.
68-
"""
66+
def _prepare(self): # pragma: no cover
6967
raise NotImplementedError
7068

7169
def __call__(self, state: tf.Tensor, is_density_matrix: bool = False
@@ -194,16 +192,16 @@ def __call__(self, state: tf.Tensor, nshots: int,
194192
tf.reshape(state, shape), is_density_matrix)
195193
logits = tf.math.log(tf.reshape(probs, (probs_dim,)))
196194

197-
if nshots * probs_dim < GPU_MEASUREMENT_CUTOFF:
198-
# Use default device to perform sampling
195+
196+
oom_error = tf.python.framework.errors_impl.ResourceExhaustedError
197+
try:
199198
samples_dec = tf.random.categorical(logits[tf.newaxis], nshots,
200199
dtype=DTYPES.get('DTYPEINT'))[0]
201-
else: # pragma: no cover
202-
# Force using CPU to perform sampling because if GPU is used
203-
# it will cause a `ResourceExhaustedError`
204-
if CPU_NAME is None:
200+
except oom_error: # pragma: no cover
201+
# Force using CPU to perform sampling
202+
if not DEVICES['CPU']:
205203
raise RuntimeError("Cannot find CPU device to use for sampling.")
206-
with tf.device(CPU_NAME):
204+
with tf.device(DEVICES['CPU'][0]):
207205
samples_dec = tf.random.categorical(logits[tf.newaxis], nshots,
208206
dtype=DTYPES.get('DTYPEINT'))[0]
209207
if samples_only:
@@ -502,9 +500,6 @@ def __init__(self, coefficients):
502500
TensorflowGate.__init__(self)
503501
self.swap_reset = []
504502

505-
def _construct_matrix(self):
506-
pass
507-
508503
def __call__(self, state: tf.Tensor, is_density_matrix: bool = False
509504
) -> tf.Tensor:
510505
shape = tuple(state.shape)
@@ -536,7 +531,7 @@ def __call__(self, state: tf.Tensor, is_density_matrix: bool = False
536531
class TensorflowChannel(TensorflowGate):
537532

538533
def __new__(cls, *args, **kwargs):
539-
if BACKEND.get('GATES') == 'custom':
534+
if BACKEND.get('GATES') == 'custom': # pragma: no cover
540535
raise NotImplementedError("Density matrices are not supported by "
541536
"custom operator gates.")
542537
else:

src/qibo/tensorflow/circuit.py

+51-31
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import numpy as np
44
import tensorflow as tf
55
from qibo.base import circuit
6-
from qibo.config import DTYPES
6+
from qibo.config import DTYPES, DEVICES
77
from qibo.tensorflow import measurements
88
from qibo.tensorflow import custom_operators as op
99
from typing import List, Optional, Tuple, Union
10+
InitStateType = Union[np.ndarray, tf.Tensor]
11+
OutputType = Union[tf.Tensor, measurements.CircuitResult]
1012

1113

1214
class TensorflowCircuit(circuit.BaseCircuit):
@@ -20,6 +22,13 @@ def __init__(self, nqubits):
2022
super(TensorflowCircuit, self).__init__(nqubits)
2123
self._compiled_execute = None
2224

25+
def _set_nqubits(self, gate):
26+
if gate._nqubits is None:
27+
with tf.device(DEVICES['DEFAULT']):
28+
gate.nqubits = self.nqubits
29+
elif gate.nqubits != self.nqubits:
30+
super(TensorflowCircuit, self)._set_nqubits(gate)
31+
2332
def _eager_execute(self, state: tf.Tensor) -> tf.Tensor:
2433
"""Simulates the circuit gates in eager mode."""
2534
for gate in self.queue:
@@ -66,34 +75,9 @@ def using_tfgates(self) -> bool:
6675
from qibo.tensorflow import gates
6776
return gates.TensorflowGate == self.gate_module.TensorflowGate
6877

69-
def execute(self,
70-
initial_state: Optional[Union[np.ndarray, tf.Tensor]] = None,
71-
nshots: Optional[int] = None,
72-
) -> Union[tf.Tensor, measurements.CircuitResult]:
73-
"""Propagates the state through the circuit applying the corresponding gates.
74-
75-
In default usage the full final state vector or density matrix is returned.
76-
If the circuit contains measurement gates and `nshots` is given, then
77-
the final state is sampled and the samples are returned.
78-
Circuit execution uses by default state vectors but switches automatically
79-
to density matrices if
80-
81-
Args:
82-
initial_state (np.ndarray): Initial state vector as a numpy array of shape ``(2 ** nqubits,)``
83-
or a density matrix of shape ``(2 ** nqubits, 2 ** nqubits)``.
84-
A Tensorflow tensor with shape ``nqubits * (2,)`` (or ``2 * nqubits * (2,)`` for density matrices)
85-
is also allowed as an initial state but must have the `dtype` of the circuit.
86-
If ``initial_state`` is ``None`` the |000...0> state will be used.
87-
nshots (int): Number of shots to sample if the circuit contains
88-
measurement gates.
89-
If ``nshots`` None the measurement gates will be ignored.
90-
91-
Returns:
92-
If ``nshots`` is given and the circuit contains measurements
93-
A :class:`qibo.base.measurements.CircuitResult` object that contains the measured bitstrings.
94-
If ``nshots`` is ``None`` or the circuit does not contain measurements.
95-
The final state vector as a Tensorflow tensor of shape ``(2 ** nqubits,)`` or a density matrix of shape ``(2 ** nqubits, 2 ** nqubits)``.
96-
"""
78+
def _execute(self, initial_state: Optional[InitStateType] = None,
79+
nshots: Optional[int] = None) -> OutputType:
80+
"""Performs ``circuit.execute`` on specified device."""
9781
state = self._cast_initial_state(initial_state)
9882

9983
if self.using_tfgates:
@@ -124,8 +108,44 @@ def execute(self,
124108
return measurements.CircuitResult(
125109
self.measurement_tuples, self.measurement_gate_result)
126110

127-
def __call__(self, initial_state: Optional[tf.Tensor] = None,
128-
nshots: Optional[int] = None) -> tf.Tensor:
111+
def execute(self, initial_state: Optional[InitStateType] = None,
112+
nshots: Optional[int] = None) -> OutputType:
113+
"""Propagates the state through the circuit applying the corresponding gates.
114+
115+
In default usage the full final state vector or density matrix is returned.
116+
If the circuit contains measurement gates and `nshots` is given, then
117+
the final state is sampled and the samples are returned.
118+
Circuit execution uses by default state vectors but switches automatically
119+
to density matrices if
120+
121+
Args:
122+
initial_state (np.ndarray): Initial state vector as a numpy array of shape ``(2 ** nqubits,)``
123+
or a density matrix of shape ``(2 ** nqubits, 2 ** nqubits)``.
124+
A Tensorflow tensor with shape ``nqubits * (2,)`` (or ``2 * nqubits * (2,)`` for density matrices)
125+
is also allowed as an initial state but must have the `dtype` of the circuit.
126+
If ``initial_state`` is ``None`` the |000...0> state will be used.
127+
nshots (int): Number of shots to sample if the circuit contains
128+
measurement gates.
129+
If ``nshots`` None the measurement gates will be ignored.
130+
131+
Returns:
132+
If ``nshots`` is given and the circuit contains measurements
133+
A :class:`qibo.base.measurements.CircuitResult` object that contains the measured bitstrings.
134+
If ``nshots`` is ``None`` or the circuit does not contain measurements.
135+
The final state vector as a Tensorflow tensor of shape ``(2 ** nqubits,)`` or a density matrix of shape ``(2 ** nqubits, 2 ** nqubits)``.
136+
"""
137+
oom_error = tf.python.framework.errors_impl.ResourceExhaustedError
138+
device = DEVICES['DEFAULT']
139+
try:
140+
with tf.device(device):
141+
return self._execute(initial_state=initial_state, nshots=nshots)
142+
except oom_error:
143+
raise RuntimeError(f"State does not fit in {device} memory."
144+
"Please switch the execution device to a "
145+
"different one using ``qibo.set_device``.")
146+
147+
def __call__(self, initial_state: Optional[InitStateType] = None,
148+
nshots: Optional[int] = None) -> OutputType:
129149
"""Equivalent to ``circuit.execute``."""
130150
return self.execute(initial_state=initial_state, nshots=nshots)
131151

0 commit comments

Comments
 (0)